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