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.Collections; 022import java.util.Date; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027import java.util.function.Predicate; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.core.right.RightManager; 037import org.ametys.core.ui.Callable; 038import org.ametys.core.user.CurrentUserProvider; 039import org.ametys.core.user.UserIdentity; 040import org.ametys.core.userpref.UserPreferencesException; 041import org.ametys.core.userpref.UserPreferencesManager; 042import org.ametys.core.util.DateUtils; 043import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 044import org.ametys.plugins.repository.AmetysObjectIterable; 045import org.ametys.plugins.repository.AmetysObjectResolver; 046import org.ametys.plugins.repository.activities.Activity; 047import org.ametys.plugins.repository.activities.ActivityHelper; 048import org.ametys.plugins.repository.activities.ActivityTypeExpression; 049import org.ametys.plugins.repository.activities.ActivityTypeExtensionPoint; 050import org.ametys.plugins.repository.query.expression.AndExpression; 051import org.ametys.plugins.repository.query.expression.DateExpression; 052import org.ametys.plugins.repository.query.expression.Expression; 053import org.ametys.plugins.repository.query.expression.Expression.Operator; 054import org.ametys.plugins.repository.query.expression.ExpressionContext; 055import org.ametys.plugins.repository.query.expression.OrExpression; 056import org.ametys.plugins.repository.query.expression.StringExpression; 057import org.ametys.plugins.workspaces.activities.AbstractWorkspacesActivityType; 058import org.ametys.plugins.workspaces.project.ProjectManager; 059import org.ametys.plugins.workspaces.project.modules.WorkspaceModule; 060import org.ametys.plugins.workspaces.project.objects.Project; 061import org.ametys.runtime.plugin.component.AbstractLogEnabled; 062 063/** 064 * Component gathering methods for the activity stream service 065 */ 066public class ActivityStreamClientInteraction extends AbstractLogEnabled implements Component, Serviceable 067{ 068 /** The Avalon role */ 069 public static final String ROLE = ActivityStreamClientInteraction.class.getName(); 070 071 /** the user preferences context for activity stream */ 072 public static final String ACTIVITY_STREAM_USER_PREF_CONTEXT = "/workspaces/activity-stream"; 073 074 /** the id of user preferences for the last update of activity stream*/ 075 public static final String ACTIVITY_STREAM_USER_PREF_LAST_UPDATE = "lastUpdate"; 076 077 private ProjectManager _projectManager; 078 private ActivityTypeExtensionPoint _activityTypeExtensionPoint; 079 080 private CurrentUserProvider _currentUserProvider; 081 082 private RightManager _rightManager; 083 084 private UserPreferencesManager _userPrefManager; 085 086 private AmetysObjectResolver _resolver; 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 _activityTypeExtensionPoint = (ActivityTypeExtensionPoint) serviceManager.lookup(ActivityTypeExtensionPoint.ROLE); 094 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 095 _userPrefManager = (UserPreferencesManager) serviceManager.lookup(UserPreferencesManager.ROLE); 096 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 097 } 098 099 /** 100 * Get the activities of the given projects and of the given event types 101 * @param projectNames the names of the projects. Can not be null. 102 * @param filterEventTypes the type of events to retain. Can be empty to get all activities. 103 * @param limit The max number of activities 104 * @return the retained activities 105 */ 106 @Callable 107 public List<Map<String, Object>> getActivities(List<String> projectNames, List<String> filterEventTypes, int limit) 108 { 109 return getActivities(projectNames, filterEventTypes, null, null, null, limit); 110 } 111 112 /** 113 * Get the activities of the given projects and of the given event types 114 * @param projectNames the names of the projects. Can not be null. 115 * @param filterEventTypes the type of events to retain. Can be empty to get all activities. 116 * @param fromDate To get activities after the given date. Can be null. 117 * @param untilDate To get activities before the given date. Can be null. 118 * @param pattern A filter pattern. Can be null or empty 119 * @param limit The max number of activities 120 * @return the retained activities 121 */ 122 public List<Map<String, Object>> getActivities(List<String> projectNames, List<String> filterEventTypes, Date fromDate, Date untilDate, String pattern, int limit) 123 { 124 List<Map<String, Object>> mergedActivities = new ArrayList<>(); 125 126 if (projectNames.isEmpty()) 127 { 128 return mergedActivities; 129 } 130 131 try (AmetysObjectIterable<Activity> activitiesIterable = _getActivities(projectNames, filterEventTypes, fromDate, untilDate, pattern)) 132 { 133 if (activitiesIterable != null) 134 { 135 // FIXME Add 20% to requested limit to take into account merge of activities 136 List<Activity> activities = activitiesIterable.stream().limit(Math.round(limit + limit * 0.5)).toList(); 137 138 // FIXME After merge, the number of activities could be lower than limit 139 mergedActivities.addAll(_activityTypeExtensionPoint.mergeActivities(activities)); 140 } 141 } 142 143 return mergedActivities.stream().limit(limit).toList(); 144 } 145 146 private AmetysObjectIterable<Activity> _getActivities(List<String> projectNames, List<String> filterEventTypes, Date fromDate, Date untilDate, String pattern) 147 { 148 List<Expression> exprs = new ArrayList<>(); 149 150 Set<String> allAllowedEventTypes = new HashSet<>(); 151 152 for (String projectName : projectNames) 153 { 154 Project project = _projectManager.getProject(projectName); 155 156 Set<String> allowedEventTypes = _getAllowedEventTypesByProject(project); 157 158 if (filterEventTypes != null && filterEventTypes.size() > 0) 159 { 160 allowedEventTypes.retainAll(filterEventTypes); 161 } 162 163 allAllowedEventTypes.addAll(allowedEventTypes); 164 165 if (allowedEventTypes.size() > 0) 166 { 167 Expression activityTypeExpr = new ActivityTypeExpression(Operator.EQ, allowedEventTypes.toArray(new String[allowedEventTypes.size()])); 168 Expression projectExpr = new StringExpression(AbstractWorkspacesActivityType.PROJECT_NAME, Operator.EQ, projectName); 169 170 exprs.add(new AndExpression(activityTypeExpr, projectExpr)); 171 } 172 } 173 174 if (exprs.size() > 0) 175 { 176 List<Expression> finalExprs = new ArrayList<>(); 177 178 finalExprs.add(new OrExpression(exprs.toArray(new Expression[exprs.size()]))); 179 180 if (untilDate != null) 181 { 182 finalExprs.add(new DateExpression("date", Operator.LT, untilDate)); 183 } 184 185 if (fromDate != null) 186 { 187 finalExprs.add(new DateExpression("date", Operator.GT, fromDate)); 188 } 189 190 if (StringUtils.isNotEmpty(pattern)) 191 { 192 List<Expression> patternExprs = new ArrayList<>(); 193 194 patternExprs.add(new StringExpression(AbstractWorkspacesActivityType.PROJECT_TITLE, Operator.WD, pattern, ExpressionContext.newInstance().withCaseInsensitive(true))); 195 196 for (String allowedEventType : allAllowedEventTypes) 197 { 198 _activityTypeExtensionPoint.getActivityTypes(allowedEventType) 199 .stream() 200 .filter(AbstractWorkspacesActivityType.class::isInstance) 201 .map(AbstractWorkspacesActivityType.class::cast) 202 .forEach(wsActivityType -> { 203 Expression patternExpr = wsActivityType.getFilterPatternExpression(pattern); 204 if (patternExpr != null) 205 { 206 patternExprs.add(patternExpr); 207 } 208 }); 209 } 210 211 finalExprs.add(new OrExpression(patternExprs.toArray(new Expression[patternExprs.size()]))); 212 } 213 214 Expression finalExpr = new AndExpression(finalExprs.toArray(new Expression[finalExprs.size()])); 215 216 String xpathQuery = ActivityHelper.getActivityXPathQuery(finalExpr); 217 return _resolver.query(xpathQuery); 218 } 219 220 221 return null; 222 } 223 224 /** 225 * Get the date of last activity regardless the current user's rights 226 * @param projectName The project's name 227 * @param excludeActivityTypes the types of activity to ignore from this search 228 * @return the date of last activity or null if no activity found or an error occurred 229 */ 230 public ZonedDateTime getDateOfLastActivity(String projectName, List<String> excludeActivityTypes) 231 { 232 List<Expression> expressions = new ArrayList<>(); 233 234 for (String eventType : excludeActivityTypes) 235 { 236 expressions.add(new ActivityTypeExpression(Operator.NE, eventType)); 237 } 238 239 return _getDateOfLastActivity(projectName, new AndExpression(expressions.toArray(new Expression[expressions.size()]))); 240 } 241 242 /** 243 * Get the date of last activity regardless the current user's rights 244 * @param projectName The project's name 245 * @param includeActivityTypes the types of activity to ignore from this search 246 * @return the date of last activity or null if no activity found or an error occurred 247 */ 248 public ZonedDateTime getDateOfLastActivityByActivityType(String projectName, Collection<String> includeActivityTypes) 249 { 250 List<Expression> expressions = new ArrayList<>(); 251 252 for (String eventType : includeActivityTypes) 253 { 254 expressions.add(new ActivityTypeExpression(Operator.EQ, eventType)); 255 } 256 257 return _getDateOfLastActivity(projectName, new OrExpression(expressions.toArray(new Expression[expressions.size()]))); 258 } 259 260 private ZonedDateTime _getDateOfLastActivity(String projectName, Expression eventTypesExpression) 261 { 262 Expression projectNameExpression = new StringExpression("projectName", Operator.EQ, projectName); 263 264 Expression eventExpr = new AndExpression(projectNameExpression, eventTypesExpression); 265 266 String xpathQuery = ActivityHelper.getActivityXPathQuery(eventExpr); 267 AmetysObjectIterable<Activity> activities = _resolver.query(xpathQuery); 268 269 for (Activity activity: activities) 270 { 271 return activity.getDate(); 272 } 273 274 return null; 275 } 276 277 /** 278 * Get the list of allowed event types for the given projects 279 * @param projects The projects 280 * @return The allowed event types 281 */ 282 public Set<String> getAllowedEventTypes (Set<Project> projects) 283 { 284 Set<String> allowedTypes = new HashSet<>(); 285 286 for (Project project : projects) 287 { 288 allowedTypes.addAll(_getAllowedEventTypesByProject(project)); 289 } 290 291 return allowedTypes; 292 } 293 294 // FIXME temporary method 295 // The allowed types are hard coded according the user access on root modules. 296 private Set<String> _getAllowedEventTypesByProject (Project project) 297 { 298 Set<String> allowedTypes = new HashSet<>(); 299 300 for (WorkspaceModule moduleManager : _projectManager.getModules(project)) 301 { 302 ModifiableResourceCollection moduleRoot = moduleManager.getModuleRoot(project, false); 303 if (moduleRoot != null && _rightManager.currentUserHasReadAccess(moduleRoot)) 304 { 305 allowedTypes.addAll(moduleManager.getAllowedEventTypes()); 306 } 307 } 308 309 return allowedTypes; 310 } 311 312 /** 313 * Get the number of unread events for the current user 314 * @return the number of unread events or -1 if user never read events 315 */ 316 @Callable 317 public long getNumberOfUnreadActivitiesForCurrentUser() 318 { 319 Date lastUpdate = _getLastReadDate(); 320 if (lastUpdate != null) 321 { 322 Set<Project> userProjects = _getProjectsForCurrentUser(null); 323 List<String> projectNames = transformProjectsToName(userProjects); 324 Set<String> allowedEventTypes = getAllowedEventTypes(userProjects); 325 326 AmetysObjectIterable<Activity> activities = _getActivities(projectNames, new ArrayList<>(allowedEventTypes), lastUpdate, null, null); 327 return activities != null ? activities.getSize() : -1; 328 } 329 return -1; 330 } 331 332 /** 333 * Get the activities for the current user with the allowed event types get from the user projects. 334 * @param limit The max number of results 335 * @return The activities for the user projects 336 */ 337 public List<Map<String, Object>> getActivitiesForCurrentUser(int limit) 338 { 339 return getActivitiesForCurrentUser((String) null, null, null, limit); 340 } 341 342 /** 343 * Get the activities for the current user with the allowed event types get from the user projects. 344 * @param pattern Pattern to search on activity. Can null or empty to not filter on pattern. 345 * @param activityTypes the type of activities to retrieve. Can null or empty to not filter on activity types. 346 * @param categories the categories of projects to retrieve. Can null or empty to not filter on themes. 347 * @param limit The max number of results 348 * @return The activities for the user projects 349 */ 350 public List<Map<String, Object>> getActivitiesForCurrentUser(String pattern, Set<String> categories, Set<String> activityTypes, int limit) 351 { 352 return getActivitiesForCurrentUser(pattern, categories, activityTypes, null, null, limit); 353 } 354 355 /** 356 * Get the activities for the current user with the allowed event types get from the user projects. 357 * @param pattern Pattern to search on activity. Can null or empty to not filter on pattern. 358 * @param activityTypes the type of activities to retrieve. Can null or empty to not filter on activity types. 359 * @param categories the categories of projects to retrieve. Can null or empty to not filter on themes. 360 * @param fromDate To get activities after the given date. Can be null. 361 * @param untilDate To get activities before the given date. Can be null. 362 * @param limit The max number of results 363 * @return The activities for the user projects 364 */ 365 public List<Map<String, Object>> getActivitiesForCurrentUser(String pattern, Set<String> categories, Set<String> activityTypes, Date fromDate, Date untilDate, int limit) 366 { 367 Set<Project> userProjects = _getProjectsForCurrentUser(categories); 368 return getActivitiesForCurrentUser(userProjects, activityTypes, fromDate, untilDate, pattern, limit); 369 } 370 371 /** 372 * Get the activities for the current user with the allowed event types get from the given projects. 373 * @param projects the projects 374 * @param activityTypes the type of activities to retrieve. Can null or empty to not filter on activity types. 375 * @param fromDate To get activities after the given date. Can be null. 376 * @param untilDate To get activities before the given date. Can be null. 377 * @param pattern Pattern to search on activity. Can null or empty to not filter on pattern. 378 * @param limit The max number of results 379 * @return The activities for the user projects 380 */ 381 public List<Map<String, Object>> getActivitiesForCurrentUser(Set<Project> projects, Set<String> activityTypes, Date fromDate, Date untilDate, String pattern, int limit) 382 { 383 List<String> projectNames = transformProjectsToName(projects); 384 Set<String> allowedActivityTypes = getAllowedEventTypes(projects); 385 386 if (activityTypes != null && activityTypes.size() > 0) 387 { 388 allowedActivityTypes.retainAll(activityTypes); 389 } 390 391 List<Map<String, Object>> activities = getActivities(projectNames, new ArrayList<>(allowedActivityTypes), fromDate, untilDate, pattern, limit); 392 393 // Add a parameter representing the date in the ISO 8601 format 394 activities.stream().forEach(activity -> 395 { 396 String eventDate = (String) activity.get("date"); 397 398 Date lastReadDate = _getLastReadDate(); 399 if (lastReadDate != null) 400 { 401 activity.put("unread", DateUtils.parse(eventDate).compareTo(lastReadDate) > 0); 402 } 403 // start date 404 activity.put("date-iso", eventDate); 405 406 // optional end date 407 String endDate = (String) activity.get("endDate"); 408 if (endDate != null) 409 { 410 activity.put("end-date-iso", endDate); 411 } 412 }); 413 414 return activities; 415 } 416 417 private Date _getLastReadDate() 418 { 419 UserIdentity user = _currentUserProvider.getUser(); 420 try 421 { 422 return _userPrefManager.getUserPreferenceAsDate(user, ACTIVITY_STREAM_USER_PREF_CONTEXT, Map.of(), ACTIVITY_STREAM_USER_PREF_LAST_UPDATE); 423 } 424 catch (UserPreferencesException e) 425 { 426 getLogger().warn("Unable to get last unread events date from user preferences", e); 427 return null; 428 } 429 } 430 431 private Set<Project> _getProjectsForCurrentUser(Set<String> filteredCategories) 432 { 433 UserIdentity user = _currentUserProvider.getUser(); 434 435 Predicate<Project> matchCategories = p -> filteredCategories == null || filteredCategories.isEmpty() || !Collections.disjoint(p.getCategories(), filteredCategories); 436 437 return _projectManager.getUserProjects(user).keySet() 438 .stream() 439 .filter(matchCategories) 440 .collect(Collectors.toSet()); 441 } 442 443 private List<String> transformProjectsToName(Set<Project> userProjects) 444 { 445 return userProjects.stream() 446 .map(p -> p.getName()) 447 .collect(Collectors.toList()); 448 } 449}