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