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