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}