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.activities.activitystream;
017
018import java.time.ZonedDateTime;
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Date;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032
033import org.ametys.core.right.RightManager;
034import org.ametys.core.ui.Callable;
035import org.ametys.core.user.CurrentUserProvider;
036import org.ametys.core.user.UserIdentity;
037import org.ametys.core.userpref.UserPreferencesException;
038import org.ametys.core.userpref.UserPreferencesManager;
039import org.ametys.core.util.DateUtils;
040import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
041import org.ametys.plugins.repository.AmetysObjectIterable;
042import org.ametys.plugins.repository.AmetysObjectResolver;
043import org.ametys.plugins.repository.activities.Activity;
044import org.ametys.plugins.repository.activities.ActivityHelper;
045import org.ametys.plugins.repository.activities.ActivityTypeExpression;
046import org.ametys.plugins.repository.activities.ActivityTypeExtensionPoint;
047import org.ametys.plugins.repository.query.expression.AndExpression;
048import org.ametys.plugins.repository.query.expression.DateExpression;
049import org.ametys.plugins.repository.query.expression.Expression;
050import org.ametys.plugins.repository.query.expression.Expression.Operator;
051import org.ametys.plugins.repository.query.expression.OrExpression;
052import org.ametys.plugins.repository.query.expression.StringExpression;
053import org.ametys.plugins.workspaces.activities.AbstractWorkspacesActivityType;
054import org.ametys.plugins.workspaces.project.ProjectManager;
055import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
056import org.ametys.plugins.workspaces.project.objects.Project;
057import org.ametys.runtime.plugin.component.AbstractLogEnabled;
058
059/**
060 * Component gathering methods for the activity stream service
061 */
062public class ActivityStreamClientInteraction extends AbstractLogEnabled implements Component, Serviceable
063{
064    /** The Avalon role */
065    public static final String ROLE = ActivityStreamClientInteraction.class.getName();
066    
067    /** the user preferences context for activity stream */
068    public static final String ACTIVITY_STREAM_USER_PREF_CONTEXT = "/workspaces/activity-stream";
069    
070    /** the id of user preferences for the last update of activity stream*/
071    public static final String ACTIVITY_STREAM_USER_PREF_LAST_UPDATE = "lastUpdate";
072
073    private ProjectManager _projectManager;
074    private ActivityTypeExtensionPoint _activityTypeExtensionPoint;
075
076    private CurrentUserProvider _currentUserProvider;
077
078    private RightManager _rightManager;
079
080    private UserPreferencesManager _userPrefManager;
081
082    private AmetysObjectResolver _resolver;
083
084    @Override
085    public void service(ServiceManager serviceManager) throws ServiceException
086    {
087        _projectManager = (ProjectManager) serviceManager.lookup(ProjectManager.ROLE);
088        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
089        _activityTypeExtensionPoint = (ActivityTypeExtensionPoint) serviceManager.lookup(ActivityTypeExtensionPoint.ROLE);
090        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
091        _userPrefManager = (UserPreferencesManager) serviceManager.lookup(UserPreferencesManager.ROLE);
092        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
093    }
094    
095    /**
096     * Get the activities of the given projects and of the given event types
097     * @param projectNames the names of the projects. Can not be null. 
098     * @param filterEventTypes the type of events to retain. Can be empty to get all activities.
099     * @param limit The max number of activities
100     * @return the retained activities
101     */
102    @Callable
103    public List<Map<String, Object>> getActivities(List<String> projectNames, List<String> filterEventTypes, int limit)
104    {
105        List<Map<String, Object>> mergedActivities = new ArrayList<>();
106        
107        if (projectNames.isEmpty())
108        {
109            return mergedActivities;
110        }
111        
112        AmetysObjectIterable<Activity> activitiesIterable = _getActivities(projectNames, filterEventTypes, null, null);
113        
114        List<Activity> activities = activitiesIterable.stream().limit(limit).toList();
115        
116        // FIXME After merge, the number of activities could be lower than limit
117        mergedActivities.addAll(_activityTypeExtensionPoint.mergeActivities(activities));
118        
119        return mergedActivities;
120    }
121    
122    private AmetysObjectIterable<Activity> _getActivities(List<String> projectNames, List<String> filterEventTypes, Date before, Date after)
123    {
124        List<Expression> exprs = new ArrayList<>();
125        
126        for (String projectName : projectNames)
127        {
128            Project project = _projectManager.getProject(projectName);
129            
130            Set<String> allowedEventTypes = _getAllowedEventTypesByProject(project);
131            
132            if (filterEventTypes != null && filterEventTypes.size() > 0)
133            {
134                allowedEventTypes.retainAll(filterEventTypes);
135            }
136            
137            if (allowedEventTypes.size() > 0)
138            {
139                Expression activityTypeExpr = new ActivityTypeExpression(Operator.EQ, allowedEventTypes.toArray(new String[allowedEventTypes.size()]));
140                Expression projectExpr = new StringExpression(AbstractWorkspacesActivityType.PROJECT_NAME, Operator.EQ, projectName);
141                
142                exprs.add(new AndExpression(activityTypeExpr, projectExpr));
143            }
144        }
145        
146        if (exprs.size() > 0)
147        {
148            List<Expression> finalExprs = new ArrayList<>();
149            
150            finalExprs.add(new OrExpression(exprs.toArray(new Expression[exprs.size()])));
151            
152            if (before != null)
153            {
154                finalExprs.add(new DateExpression("date", Operator.LE, before));
155            }
156            
157            if (after != null)
158            {
159                finalExprs.add(new DateExpression("date", Operator.GE, after));
160            }
161            
162            Expression finalExpr = new AndExpression(finalExprs.toArray(new Expression[finalExprs.size()]));
163            
164            String xpathQuery = ActivityHelper.getActivityXPathQuery(finalExpr);
165            return _resolver.query(xpathQuery);
166        }
167        
168        return null;
169    }
170    
171    /**
172     * Get the date of last activity regardless the current user's rights
173     * @param projectName The project's name
174     * @param excludeActivityTypes the types of activity to ignore from this search
175     * @return the date of last activity or null if no activity found or an error occurred
176     */
177    public ZonedDateTime getDateOfLastActivity(String projectName, List<String> excludeActivityTypes)
178    {
179        List<Expression> expressions = new ArrayList<>();
180        
181        for (String eventType : excludeActivityTypes)
182        {
183            expressions.add(new ActivityTypeExpression(Operator.NE, eventType));
184        }
185        
186        return _getDateOfLastActivity(projectName, new AndExpression(expressions.toArray(new Expression[expressions.size()])));
187    }
188
189    /**
190     * Get the date of last activity regardless the current user's rights
191     * @param projectName The project's name
192     * @param includeActivityTypes the types of activity to ignore from this search
193     * @return the date of last activity or null if no activity found or an error occurred
194     */
195    public ZonedDateTime getDateOfLastActivityByActivityType(String projectName, Collection<String> includeActivityTypes)
196    {
197        List<Expression> expressions = new ArrayList<>();
198        
199        for (String eventType : includeActivityTypes)
200        {
201            expressions.add(new ActivityTypeExpression(Operator.EQ, eventType));
202        }
203        
204        return _getDateOfLastActivity(projectName, new OrExpression(expressions.toArray(new Expression[expressions.size()])));
205    }
206    
207    private ZonedDateTime _getDateOfLastActivity(String projectName, Expression eventTypesExpression)
208    {
209        Expression projectNameExpression = new StringExpression("projectName", Operator.EQ, projectName);
210        
211        Expression eventExpr = new AndExpression(projectNameExpression, eventTypesExpression);
212        
213        String xpathQuery = ActivityHelper.getActivityXPathQuery(eventExpr);
214        AmetysObjectIterable<Activity> activities = _resolver.query(xpathQuery);
215        
216        for (Activity activity: activities)
217        {
218            return activity.getDate();
219        }
220        
221        return null;
222    }
223    
224    /**
225     * Get the list of allowed event types for the given projects
226     * @param projects The projects
227     * @return The allowed event types
228     */
229    public Set<String> getAllowedEventTypes (Set<Project> projects)
230    {
231        Set<String> allowedTypes = new HashSet<>();
232        
233        for (Project project : projects)
234        {
235            allowedTypes.addAll(_getAllowedEventTypesByProject(project));
236        }
237        
238        return allowedTypes;
239    }
240    
241    // FIXME temporary method
242    // The allowed types are hard coded according the user access on root modules.
243    private Set<String> _getAllowedEventTypesByProject (Project project)
244    {
245        Set<String> allowedTypes = new HashSet<>();
246        
247        for (WorkspaceModule moduleManager : _projectManager.getModules(project))
248        {
249            ModifiableResourceCollection moduleRoot = moduleManager.getModuleRoot(project, false);
250            if (moduleRoot != null && _rightManager.currentUserHasReadAccess(moduleRoot))
251            {
252                allowedTypes.addAll(moduleManager.getAllowedEventTypes());
253            }
254        }
255        
256        return allowedTypes;
257    }
258    
259    /**
260     * Get the number of unread events for the current user
261     * @return the number of unread events or -1 if user never read events
262     */
263    @Callable
264    public long getNumberOfUnreadActivitiesForCurrentUser()
265    {
266        Date lastUpdate = _getLastReadDate();
267        if (lastUpdate != null)
268        {
269            Set<Project> userProjects = getProjectsForCurrentUser();
270            List<String> projectNames = transformProjectsToName(userProjects);
271            Set<String> allowedEventTypes = getAllowedEventTypes(userProjects);
272            
273            AmetysObjectIterable<Activity> activities = _getActivities(projectNames, new ArrayList<>(allowedEventTypes), null, lastUpdate);
274            return activities != null ? activities.getSize() : -1;
275        }
276        return -1;
277    }
278    
279    /**
280     * Get the activities for the current user with the allowed event types get from the user projects.
281     * @param limit The max number of results
282     * @return The activities for the user projects
283     */
284    public List<Map<String, Object>> getActivitiesForCurrentUser(int limit)
285    {
286        Set<Project> userProjects = getProjectsForCurrentUser();
287        return getActivitiesForCurrentUser(userProjects, limit);
288    }
289    
290    /**
291     * Get the activities for the current user with the allowed event types get from the given projects.
292     * @param projects the projects
293     * @param limit The max number of results
294     * @return The activities for the user projects
295     */
296    public List<Map<String, Object>> getActivitiesForCurrentUser(Set<Project> projects, int limit)
297    {
298        List<String> projectNames = transformProjectsToName(projects);
299        Set<String> allowedActivityTypes = getAllowedEventTypes(projects);
300        
301        List<Map<String, Object>> activities = getActivities(projectNames, new ArrayList<>(allowedActivityTypes), limit);
302        
303        // Add a parameter representing the date in the ISO 8601 format
304        activities.stream().forEach(activity -> 
305        {
306            String eventDate = (String) activity.get("date");
307            
308            Date lastReadDate = _getLastReadDate();
309            if (lastReadDate != null)
310            {
311                activity.put("unread", DateUtils.parse(eventDate).compareTo(lastReadDate) > 0);
312            }
313            // start date
314            activity.put("date-iso", eventDate);
315            
316            // optional end date
317            String endDate = (String) activity.get("endDate");
318            if (endDate != null)
319            {
320                activity.put("end-date-iso", endDate);
321            }
322        });
323        
324        return activities;
325    }
326    
327    private Date _getLastReadDate()
328    {
329        UserIdentity user = _currentUserProvider.getUser();
330        try
331        {
332            return _userPrefManager.getUserPreferenceAsDate(user, ACTIVITY_STREAM_USER_PREF_CONTEXT, Map.of(), ACTIVITY_STREAM_USER_PREF_LAST_UPDATE);
333        }
334        catch (UserPreferencesException e)
335        {
336            getLogger().warn("Unable to get last unread events date from user preferences", e);
337            return null;
338        }
339    }
340    
341    private Set<Project> getProjectsForCurrentUser()
342    {
343        UserIdentity user = _currentUserProvider.getUser();
344        return _projectManager.getUserProjects(user).keySet();
345    }
346    
347    private List<String> transformProjectsToName(Set<Project> userProjects)
348    {
349        return userProjects.stream()
350            .map(p -> p.getName())
351            .collect(Collectors.toList());
352    }
353}