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                    }
188                }
189                catch (RepositoryException e)
190                {
191                    getLogger().error("Unable to retrieve event '" + eventNode.toString() + "' for activity stream of project(s) " + projectNames + ". Event has been skipped from activity stream", e);
192                }
193                
194                count++;
195            }
196        }
197        
198        // FIXME After merge, the number of events could be lower than limit
199        mergedEvents.addAll(_eventTypeExtensionPoint.mergeEvents(events));
200        
201        return mergedEvents;
202    }
203    
204    // FIXME Temporary method, should be removed when the events will indexed in Solr
205    private String _getXPathQuery(Expression expr, SortCriteria sortCriteria)
206    {
207        String predicats = StringUtils.trimToNull(expr.build());
208        
209        return "//element(*, ametys:event)" 
210            + (predicats != null ? "[" + predicats + "]" : "") 
211            + ((sortCriteria != null) ? (" " + sortCriteria.build()) : "");
212    }
213    
214    // FIXME Temporary method, should be removed when the events will indexed in Solr
215    private NodeIterator _query(String jcrQuery)
216    {
217        Session session = null;
218        try
219        {
220            session = _repository.login();
221            @SuppressWarnings("deprecation")
222            Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH);
223            return query.execute().getNodes();
224        }
225        catch (RepositoryException ex)
226        {
227            if (session != null)
228            {
229                session.logout();
230            }
231
232            throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex);
233        }
234    }
235    
236    /**
237     * Get the allowed event types classified by categories
238     * @param projectName The project's name
239     * @return The allowed event types
240     */
241    @Callable
242    public Map<String, Map<String, Object>> getAllowedEventTypes(String projectName)
243    {
244        // FIXME To be rewrite when event will be indexed in Solr
245        // TODO The events stored by workspaces should store their own category. This will be avoid to hard coded this classification.
246        
247        Set<String> allowedTypes = new HashSet<>();
248        
249        if (StringUtils.isEmpty(projectName))
250        {
251            List<Project> userProjects = getProjectsForCurrentUser();
252            for (Project userProject : userProjects)
253            {
254                allowedTypes.addAll(_getAllowedEventTypesByProject(userProject));
255            }
256        }
257        else
258        {
259            Project project = _projectManager.getProject(projectName);
260            allowedTypes.addAll(_getAllowedEventTypesByProject(project));
261        }
262        
263        Map<String, Map<String, Object>> allowedEventTypes = new HashMap<>();
264        
265        allowedEventTypes.put("documents", new HashMap<>());
266        allowedEventTypes.get("documents").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_DOCUMENTS_LABEL"));
267         
268        allowedEventTypes.put("calendars", new HashMap<>());
269        allowedEventTypes.get("calendars").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_CALENDARS_LABEL"));
270        
271        allowedEventTypes.put("threads", new HashMap<>());
272        allowedEventTypes.get("threads").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_THREADS_LABEL"));
273        
274        allowedEventTypes.put("wiki", new HashMap<>());
275        allowedEventTypes.get("wiki").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_WIKI_LABEL"));
276        
277        allowedEventTypes.put("tasks", new HashMap<>());
278        allowedEventTypes.get("tasks").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_TASKS_LABEL"));
279        
280        allowedEventTypes.put("projects", new HashMap<>());
281        allowedEventTypes.get("projects").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_PROJECTS_LABEL"));
282        
283        Set<String> eventTypeIds = _eventTypeExtensionPoint.getExtensionsIds();
284        for (String eventTypeId : eventTypeIds)
285        {
286            EventType eventType = _eventTypeExtensionPoint.getExtension(eventTypeId);
287            Map<String, I18nizableText> supportedTypes = eventType.getSupportedTypes();
288            
289            for (Entry<String, I18nizableText> entry : supportedTypes.entrySet())
290            {
291                if (allowedTypes.contains(entry.getKey()))
292                {
293                    _classifyEventType(allowedEventTypes, entry.getKey(), entry.getValue());
294                }
295            }
296        }
297        
298        return allowedEventTypes;
299    }
300    
301    private boolean _classifyEventType(Map<String, Map<String, Object>> allowedEventTypes, String eventTypeId, I18nizableText eventTypeLabel)
302    {
303        boolean classified = _classifyEventType("resource.", "documents", allowedEventTypes, eventTypeId, eventTypeLabel) 
304                                || _classifyEventType("calendar.", "calendars", allowedEventTypes, eventTypeId, eventTypeLabel)
305                                || _classifyEventType("task.", "tasks", allowedEventTypes, eventTypeId, eventTypeLabel)
306                                || _classifyEventType("thread.", "threads", allowedEventTypes, eventTypeId, eventTypeLabel)
307                                || _classifyEventType("member.", "projects", allowedEventTypes, eventTypeId, eventTypeLabel)
308                                || _classifyEventType("wiki.", "wiki", allowedEventTypes, eventTypeId, eventTypeLabel);
309        
310        return classified;
311        
312    }
313    
314    @SuppressWarnings("unchecked")
315    private boolean _classifyEventType(String withPrefix, String toAllowedEventType, Map<String, Map<String, Object>> allowedEventTypes, String eventTypeId, I18nizableText eventTypeLabel)
316    {
317        if (eventTypeId.startsWith(withPrefix))
318        {
319            if (!allowedEventTypes.get(toAllowedEventType).containsKey("eventTypes"))
320            {
321                allowedEventTypes.get(toAllowedEventType).put("eventTypes", new HashMap<String, Object>());
322            }
323            
324            ((Map<String, I18nizableText>) allowedEventTypes.get(toAllowedEventType).get("eventTypes")).put(eventTypeId, eventTypeLabel);
325            
326            return true;
327        }
328        
329        return false;
330    }
331    
332    /**
333     * Get the list of allowed event types for the given projects
334     * @param projects The projects
335     * @return The allowed event types
336     */
337    public Set<String> getAllowedEventTypes (List<Project> projects)
338    {
339        Set<String> allowedTypes = new HashSet<>();
340        
341        for (Project project : projects)
342        {
343            allowedTypes.addAll(_getAllowedEventTypesByProject(project));
344        }
345        
346        return allowedTypes;
347    }
348    
349    // FIXME temporary method
350    // The allowed types are hard coded according the user access on root modules.
351    private Set<String> _getAllowedEventTypesByProject (Project project)
352    {
353        Set<String> allowedTypes = new HashSet<>();
354        
355        for (WorkspaceModule moduleManager : _projectManager.getModules(project))
356        {
357            ModifiableResourceCollection moduleRoot = moduleManager.getModuleRoot(project, false);
358            if (moduleRoot != null && _rightManager.currentUserHasReadAccess(moduleRoot))
359            {
360                allowedTypes.addAll(moduleManager.getAllowedEventTypes());
361            }
362        }
363        
364        if (_rightManager.currentUserHasReadAccess(project))
365        {
366            Collections.addAll(allowedTypes, "member.added");
367        }
368        
369        return allowedTypes;
370    }
371    
372    /**
373     * Get the events for the current user with the allowed event types get into the user projects.
374     * @param limit The max number of results
375     * @return The events for the user projects
376     */
377    public List<Map<String, Object>> getEventsForCurrentUser(int limit)
378    {
379        List<Project> userProjects = getProjectsForCurrentUser();
380        List<String> projectNames = transformProjectsToName(userProjects);
381        Set<String> allowedEventTypes = getAllowedEventTypes(userProjects);
382        
383        List<Map<String, Object>> events = getEvents(projectNames, new ArrayList<>(allowedEventTypes), limit);
384        
385        // Add a parameter representing the date in the ISO 8601 format
386        events.stream().forEach(event -> 
387        {
388            // start date
389            event.put("date-iso", DateUtils.dateToString((Date) event.get("date")));
390            
391            // optional end date
392            Date endDate = (Date) event.get("endDate");
393            if (endDate != null)
394            {
395                event.put("end-date-iso", DateUtils.dateToString(endDate));
396            }
397        });
398        
399        return events;
400    }
401    
402    private List<Project> getProjectsForCurrentUser()
403    {
404        UserIdentity user = _currentUserProvider.getUser();
405        return _projectManager.getUserProjects(user);
406    }
407    
408    private List<String> transformProjectsToName(List<Project> userProjects)
409    {
410        return userProjects.stream()
411            .map(p -> p.getName())
412            .collect(Collectors.toList());
413    }
414}