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