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