001/*
002 *  Copyright 2016 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.workspaces.events.activitystream;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.Date;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Map.Entry;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import javax.jcr.Node;
030import javax.jcr.NodeIterator;
031import javax.jcr.Repository;
032import javax.jcr.RepositoryException;
033import javax.jcr.Session;
034import javax.jcr.query.Query;
035
036import org.apache.avalon.framework.component.Component;
037import org.apache.avalon.framework.service.ServiceException;
038import org.apache.avalon.framework.service.ServiceManager;
039import org.apache.avalon.framework.service.Serviceable;
040import org.apache.commons.lang.StringUtils;
041
042import org.ametys.core.right.RightManager;
043import org.ametys.core.ui.Callable;
044import org.ametys.core.user.CurrentUserProvider;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.core.util.DateUtils;
047import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
048import org.ametys.plugins.repository.AmetysRepositoryException;
049import org.ametys.plugins.repository.events.EventType;
050import org.ametys.plugins.repository.events.EventTypeExpression;
051import org.ametys.plugins.repository.events.EventTypeExtensionPoint;
052import org.ametys.plugins.repository.provider.AbstractRepository;
053import org.ametys.plugins.repository.query.SortCriteria;
054import org.ametys.plugins.repository.query.expression.AndExpression;
055import org.ametys.plugins.repository.query.expression.Expression;
056import org.ametys.plugins.repository.query.expression.Expression.Operator;
057import org.ametys.plugins.repository.query.expression.OrExpression;
058import org.ametys.plugins.repository.query.expression.StringExpression;
059import org.ametys.plugins.workspaces.project.ProjectManager;
060import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
061import org.ametys.plugins.workspaces.project.objects.Project;
062import org.ametys.runtime.i18n.I18nizableText;
063import org.ametys.runtime.plugin.component.AbstractLogEnabled;
064import org.ametys.runtime.plugin.component.PluginAware;
065
066/**
067 * Component gathering methods for the activity stream service
068 */
069public class ActivityStreamClientInteraction extends AbstractLogEnabled implements Component, Serviceable, PluginAware
070{
071    /** The Avalon role */
072    public static final String ROLE = ActivityStreamClientInteraction.class.getName();
073    
074    private ProjectManager _projectManager;
075    private EventTypeExtensionPoint _eventTypeExtensionPoint;
076
077    private CurrentUserProvider _currentUserProvider;
078
079    private RightManager _rightManager;
080
081    private Repository _repository;
082
083    private String _pluginName;
084
085    @Override
086    public void service(ServiceManager serviceManager) throws ServiceException
087    {
088        _projectManager = (ProjectManager) serviceManager.lookup(ProjectManager.ROLE);
089        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
090        _eventTypeExtensionPoint = (EventTypeExtensionPoint) serviceManager.lookup(EventTypeExtensionPoint.ROLE);
091        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
092        
093        _repository = (Repository) serviceManager.lookup(AbstractRepository.ROLE);
094    }
095    
096    public void setPluginInfo(String pluginName, String featureName, String id)
097    {
098        _pluginName = pluginName;
099    }
100    
101    /**
102     * Get the events of the given project and of the given event types
103     * @param projectName the project's name
104     * @param filterEventTypes the type of events to retain 
105     * @param limit The max number of events
106     * @return the retained events
107     */
108    @Callable
109    public List<Map<String, Object>> getProjectEvents(String projectName, List<String> filterEventTypes, int limit)
110    {
111        if (projectName == null)
112        {
113            List<String> projectNames = transformProjectsToName(getProjectsForCurrentUser());
114            return getEvents(projectNames, filterEventTypes, limit);
115        }
116        else
117        {
118            return getEvents(Collections.singletonList(projectName), filterEventTypes, limit);
119        }
120    }
121    
122    /**
123     * Get the events of the given projects and of the given event types
124     * @param projectNames the names of the projects. Can not be null. 
125     * @param filterEventTypes the type of events to retain. Can be empty to get all events.
126     * @param limit The max number of events
127     * @return the retained events
128     */
129    @Callable
130    public List<Map<String, Object>> getEvents(List<String> projectNames, List<String> filterEventTypes, int limit)
131    {
132        List<Map<String, Object>> mergedEvents = new ArrayList<>();
133        
134        if (projectNames.isEmpty())
135        {
136            return mergedEvents;
137        }
138        
139        List<Expression> exprs = new ArrayList<>();
140        
141        for (String projectName : projectNames)
142        {
143            Project project = _projectManager.getProject(projectName);
144            
145            Set<String> allowedEventTypes = _getAllowedEventTypesByProject(project);
146            
147            if (filterEventTypes != null && filterEventTypes.size() > 0)
148            {
149                allowedEventTypes.retainAll(filterEventTypes);
150            }
151            
152            if (allowedEventTypes.size() > 0)
153            {
154                Expression eventExpr = new EventTypeExpression(Operator.EQ, allowedEventTypes.toArray(new String[allowedEventTypes.size()]));
155                Expression projectExpr = new StringExpression("projectName", Operator.EQ, projectName);
156                
157                exprs.add(new AndExpression(eventExpr, projectExpr));
158            }
159        }
160        
161        List<Map<String, Object>> events = new ArrayList<>();
162        
163        if (exprs.size() > 0)
164        {
165            // FIXME Not possible to use JCREventHelper or #_eventTypeExtensionPoint.getEvents here (waiting for Solr query)
166            
167            SortCriteria sortCriteria = new SortCriteria();
168            sortCriteria.addJCRPropertyCriterion(EventType.EVENT_DATE, false, false);
169            
170            Expression finalExpr = new OrExpression(exprs.toArray(new Expression[exprs.size()]));
171            
172            String xpathQuery = _getXPathQuery(finalExpr, sortCriteria);
173            NodeIterator nodesIt = _query(xpathQuery);
174            
175            int count = 0;
176            while (nodesIt.hasNext() && count <= limit)
177            {
178                Node eventNode = nodesIt.nextNode();
179                try
180                {
181                    String type = eventNode.getProperty(EventType.EVENT_TYPE).getString();
182                    
183                    Map<String, Object> event2json = _eventTypeExtensionPoint.getEventType(type).event2JSON(eventNode);
184                    if (!event2json.isEmpty())
185                    {
186                        events.add(event2json);
187                        count++;
188                    }
189                }
190                catch (RepositoryException e)
191                {
192                    getLogger().error("Unable to retrieve event '" + eventNode.toString() + "' for activity stream of project(s) " + projectNames + ". Event has been skipped from activity stream", e);
193                }
194                
195                
196            }
197        }
198        
199        // FIXME After merge, the number of events could be lower than limit
200        mergedEvents.addAll(_eventTypeExtensionPoint.mergeEvents(events));
201        
202        return mergedEvents;
203    }
204    
205    // FIXME Temporary method, should be removed when the events will indexed in Solr
206    private String _getXPathQuery(Expression expr, SortCriteria sortCriteria)
207    {
208        String predicats = StringUtils.trimToNull(expr.build());
209        
210        return "//element(*, ametys:event)" 
211            + (predicats != null ? "[" + predicats + "]" : "") 
212            + ((sortCriteria != null) ? (" " + sortCriteria.build()) : "");
213    }
214    
215    // FIXME Temporary method, should be removed when the events will indexed in Solr
216    private NodeIterator _query(String jcrQuery)
217    {
218        Session session = null;
219        try
220        {
221            session = _repository.login();
222            @SuppressWarnings("deprecation")
223            Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH);
224            return query.execute().getNodes();
225        }
226        catch (RepositoryException ex)
227        {
228            if (session != null)
229            {
230                session.logout();
231            }
232
233            throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex);
234        }
235    }
236    
237    /**
238     * Get the allowed event types classified by categories
239     * @param projectName The project's name
240     * @return The allowed event types
241     */
242    @Callable
243    public Map<String, Map<String, Object>> getAllowedEventTypes(String projectName)
244    {
245        // FIXME To be rewrite when event will be indexed in Solr
246        // TODO The events stored by workspaces should store their own category. This will be avoid to hard coded this classification.
247        
248        Set<String> allowedTypes = new HashSet<>();
249        
250        if (StringUtils.isEmpty(projectName))
251        {
252            List<Project> userProjects = getProjectsForCurrentUser();
253            for (Project userProject : userProjects)
254            {
255                allowedTypes.addAll(_getAllowedEventTypesByProject(userProject));
256            }
257        }
258        else
259        {
260            Project project = _projectManager.getProject(projectName);
261            allowedTypes.addAll(_getAllowedEventTypesByProject(project));
262        }
263        
264        Map<String, Map<String, Object>> allowedEventTypes = new HashMap<>();
265        
266        allowedEventTypes.put("documents", new HashMap<>());
267        allowedEventTypes.get("documents").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_DOCUMENTS_LABEL"));
268         
269        allowedEventTypes.put("calendars", new HashMap<>());
270        allowedEventTypes.get("calendars").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_CALENDARS_LABEL"));
271        
272        allowedEventTypes.put("threads", new HashMap<>());
273        allowedEventTypes.get("threads").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_THREADS_LABEL"));
274        
275        allowedEventTypes.put("wiki", new HashMap<>());
276        allowedEventTypes.get("wiki").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_WIKI_LABEL"));
277        
278        allowedEventTypes.put("tasks", new HashMap<>());
279        allowedEventTypes.get("tasks").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_TASKS_LABEL"));
280        
281        allowedEventTypes.put("projects", new HashMap<>());
282        allowedEventTypes.get("projects").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_PROJECTS_LABEL"));
283        
284        Set<String> eventTypeIds = _eventTypeExtensionPoint.getExtensionsIds();
285        for (String eventTypeId : eventTypeIds)
286        {
287            EventType eventType = _eventTypeExtensionPoint.getExtension(eventTypeId);
288            Map<String, I18nizableText> supportedTypes = eventType.getSupportedTypes();
289            
290            for (Entry<String, I18nizableText> entry : supportedTypes.entrySet())
291            {
292                if (allowedTypes.contains(entry.getKey()))
293                {
294                    _classifyEventType(allowedEventTypes, entry.getKey(), entry.getValue());
295                }
296            }
297        }
298        
299        return allowedEventTypes;
300    }
301    
302    private boolean _classifyEventType(Map<String, Map<String, Object>> allowedEventTypes, String eventTypeId, I18nizableText eventTypeLabel)
303    {
304        boolean classified = _classifyEventType("resource.", "documents", allowedEventTypes, eventTypeId, eventTypeLabel) 
305                                || _classifyEventType("calendar.", "calendars", allowedEventTypes, eventTypeId, eventTypeLabel)
306                                || _classifyEventType("task.", "tasks", allowedEventTypes, eventTypeId, eventTypeLabel)
307                                || _classifyEventType("thread.", "threads", allowedEventTypes, eventTypeId, eventTypeLabel)
308                                || _classifyEventType("member.", "projects", allowedEventTypes, eventTypeId, eventTypeLabel)
309                                || _classifyEventType("wallcontent.", "projects", allowedEventTypes, eventTypeId, eventTypeLabel)
310                                || _classifyEventType("wiki.", "wiki", allowedEventTypes, eventTypeId, eventTypeLabel);
311        
312        return classified;
313        
314    }
315    
316    @SuppressWarnings("unchecked")
317    private boolean _classifyEventType(String withPrefix, String toAllowedEventType, Map<String, Map<String, Object>> allowedEventTypes, String eventTypeId, I18nizableText eventTypeLabel)
318    {
319        if (eventTypeId.startsWith(withPrefix))
320        {
321            if (!allowedEventTypes.get(toAllowedEventType).containsKey("eventTypes"))
322            {
323                allowedEventTypes.get(toAllowedEventType).put("eventTypes", new HashMap<String, Object>());
324            }
325            
326            ((Map<String, I18nizableText>) allowedEventTypes.get(toAllowedEventType).get("eventTypes")).put(eventTypeId, eventTypeLabel);
327            
328            return true;
329        }
330        
331        return false;
332    }
333    
334    /**
335     * Get the list of allowed event types for the given projects
336     * @param projects The projects
337     * @return The allowed event types
338     */
339    public Set<String> getAllowedEventTypes (List<Project> projects)
340    {
341        Set<String> allowedTypes = new HashSet<>();
342        
343        for (Project project : projects)
344        {
345            allowedTypes.addAll(_getAllowedEventTypesByProject(project));
346        }
347        
348        return allowedTypes;
349    }
350    
351    // FIXME temporary method
352    // The allowed types are hard coded according the user access on root modules.
353    private Set<String> _getAllowedEventTypesByProject (Project project)
354    {
355        Set<String> allowedTypes = new HashSet<>();
356        
357        for (WorkspaceModule moduleManager : _projectManager.getModules(project))
358        {
359            ModifiableResourceCollection moduleRoot = moduleManager.getModuleRoot(project, false);
360            if (moduleRoot != null && _rightManager.currentUserHasReadAccess(moduleRoot))
361            {
362                allowedTypes.addAll(moduleManager.getAllowedEventTypes());
363            }
364        }
365        
366        if (_rightManager.currentUserHasReadAccess(project))
367        {
368            Collections.addAll(allowedTypes, "member.added", "wallcontent.added");
369        }
370        
371        return allowedTypes;
372    }
373    
374    /**
375     * Get the events for the current user with the allowed event types get into the user projects.
376     * @param limit The max number of results
377     * @return The events for the user projects
378     */
379    public List<Map<String, Object>> getEventsForCurrentUser(int limit)
380    {
381        List<Project> userProjects = getProjectsForCurrentUser();
382        return getEventsForCurrentUser(userProjects, limit);
383    }
384    
385    /**
386     * Get the events for the current user with the allowed event types get into the given projects.
387     * @param projects the projects
388     * @param limit The max number of results
389     * @return The events for the user projects
390     */
391    public List<Map<String, Object>> getEventsForCurrentUser(List<Project> projects, int limit)
392    {
393        List<String> projectNames = transformProjectsToName(projects);
394        Set<String> allowedEventTypes = getAllowedEventTypes(projects);
395        
396        List<Map<String, Object>> events = getEvents(projectNames, new ArrayList<>(allowedEventTypes), limit);
397        
398        // Add a parameter representing the date in the ISO 8601 format
399        events.stream().forEach(event -> 
400        {
401            // start date
402            event.put("date-iso", DateUtils.dateToString((Date) event.get("date")));
403            
404            // optional end date
405            Date endDate = (Date) event.get("endDate");
406            if (endDate != null)
407            {
408                event.put("end-date-iso", DateUtils.dateToString(endDate));
409            }
410        });
411        
412        return events;
413    }
414    
415    private List<Project> getProjectsForCurrentUser()
416    {
417        UserIdentity user = _currentUserProvider.getUser();
418        return _projectManager.getUserProjects(user);
419    }
420    
421    private List<String> transformProjectsToName(List<Project> userProjects)
422    {
423        return userProjects.stream()
424            .map(p -> p.getName())
425            .collect(Collectors.toList());
426    }
427}