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.events.activitystream; 017 018import java.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.Calendar; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.Date; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Map.Entry; 029import java.util.Set; 030import java.util.stream.Collectors; 031 032import javax.jcr.Node; 033import javax.jcr.NodeIterator; 034import javax.jcr.Repository; 035import javax.jcr.RepositoryException; 036import javax.jcr.Session; 037import javax.jcr.query.Query; 038 039import org.apache.avalon.framework.component.Component; 040import org.apache.avalon.framework.service.ServiceException; 041import org.apache.avalon.framework.service.ServiceManager; 042import org.apache.avalon.framework.service.Serviceable; 043import org.apache.commons.lang.StringUtils; 044 045import org.ametys.core.right.RightManager; 046import org.ametys.core.ui.Callable; 047import org.ametys.core.user.CurrentUserProvider; 048import org.ametys.core.user.UserIdentity; 049import org.ametys.core.util.DateUtils; 050import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 051import org.ametys.plugins.repository.AmetysRepositoryException; 052import org.ametys.plugins.repository.events.EventType; 053import org.ametys.plugins.repository.events.EventTypeExpression; 054import org.ametys.plugins.repository.events.EventTypeExtensionPoint; 055import org.ametys.plugins.repository.provider.AbstractRepository; 056import org.ametys.plugins.repository.query.SortCriteria; 057import org.ametys.plugins.repository.query.expression.AndExpression; 058import org.ametys.plugins.repository.query.expression.Expression; 059import org.ametys.plugins.repository.query.expression.Expression.Operator; 060import org.ametys.plugins.repository.query.expression.OrExpression; 061import org.ametys.plugins.repository.query.expression.StringExpression; 062import org.ametys.plugins.workspaces.project.ProjectManager; 063import org.ametys.plugins.workspaces.project.modules.WorkspaceModule; 064import org.ametys.plugins.workspaces.project.objects.Project; 065import org.ametys.runtime.i18n.I18nizableText; 066import org.ametys.runtime.plugin.component.AbstractLogEnabled; 067import org.ametys.runtime.plugin.component.PluginAware; 068 069/** 070 * Component gathering methods for the activity stream service 071 */ 072public class ActivityStreamClientInteraction extends AbstractLogEnabled implements Component, Serviceable, PluginAware 073{ 074 /** The Avalon role */ 075 public static final String ROLE = ActivityStreamClientInteraction.class.getName(); 076 077 private ProjectManager _projectManager; 078 private EventTypeExtensionPoint _eventTypeExtensionPoint; 079 080 private CurrentUserProvider _currentUserProvider; 081 082 private RightManager _rightManager; 083 084 private Repository _repository; 085 086 private String _pluginName; 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 _eventTypeExtensionPoint = (EventTypeExtensionPoint) serviceManager.lookup(EventTypeExtensionPoint.ROLE); 094 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 095 096 _repository = (Repository) serviceManager.lookup(AbstractRepository.ROLE); 097 } 098 099 public void setPluginInfo(String pluginName, String featureName, String id) 100 { 101 _pluginName = pluginName; 102 } 103 104 /** 105 * Get the events of the given project and of the given event types 106 * @param projectName the project's name 107 * @param filterEventTypes the type of events to retain 108 * @param limit The max number of events 109 * @return the retained events 110 */ 111 @Callable 112 public List<Map<String, Object>> getProjectEvents(String projectName, List<String> filterEventTypes, int limit) 113 { 114 if (projectName == null) 115 { 116 List<String> projectNames = transformProjectsToName(getProjectsForCurrentUser()); 117 return getEvents(projectNames, filterEventTypes, limit); 118 } 119 else 120 { 121 return getEvents(Collections.singletonList(projectName), filterEventTypes, limit); 122 } 123 } 124 125 /** 126 * Get the events of the given projects and of the given event types 127 * @param projectNames the names of the projects. Can not be null. 128 * @param filterEventTypes the type of events to retain. Can be empty to get all events. 129 * @param limit The max number of events 130 * @return the retained events 131 */ 132 @Callable 133 public List<Map<String, Object>> getEvents(List<String> projectNames, List<String> filterEventTypes, int limit) 134 { 135 List<Map<String, Object>> mergedEvents = new ArrayList<>(); 136 137 if (projectNames.isEmpty()) 138 { 139 return mergedEvents; 140 } 141 142 List<Expression> exprs = new ArrayList<>(); 143 144 for (String projectName : projectNames) 145 { 146 Project project = _projectManager.getProject(projectName); 147 148 Set<String> allowedEventTypes = _getAllowedEventTypesByProject(project); 149 150 if (filterEventTypes != null && filterEventTypes.size() > 0) 151 { 152 allowedEventTypes.retainAll(filterEventTypes); 153 } 154 155 if (allowedEventTypes.size() > 0) 156 { 157 Expression eventExpr = new EventTypeExpression(Operator.EQ, allowedEventTypes.toArray(new String[allowedEventTypes.size()])); 158 Expression projectExpr = new StringExpression("projectName", Operator.EQ, projectName); 159 160 exprs.add(new AndExpression(eventExpr, projectExpr)); 161 } 162 } 163 164 List<Map<String, Object>> events = new ArrayList<>(); 165 166 if (exprs.size() > 0) 167 { 168 // FIXME Not possible to use JCREventHelper or #_eventTypeExtensionPoint.getEvents here (waiting for Solr query) 169 170 SortCriteria sortCriteria = new SortCriteria(); 171 sortCriteria.addJCRPropertyCriterion(EventType.EVENT_DATE, false, false); 172 173 Expression finalExpr = new OrExpression(exprs.toArray(new Expression[exprs.size()])); 174 175 String xpathQuery = _getXPathQuery(finalExpr, sortCriteria); 176 NodeIterator nodesIt = _query(xpathQuery); 177 178 int count = 0; 179 while (nodesIt.hasNext() && count <= limit) 180 { 181 Node eventNode = nodesIt.nextNode(); 182 try 183 { 184 String type = eventNode.getProperty(EventType.EVENT_TYPE).getString(); 185 186 Map<String, Object> event2json = _eventTypeExtensionPoint.getEventType(type).event2JSON(eventNode); 187 if (!event2json.isEmpty()) 188 { 189 events.add(event2json); 190 count++; 191 } 192 } 193 catch (RepositoryException e) 194 { 195 getLogger().error("Unable to retrieve event '" + eventNode.toString() + "' for activity stream of project(s) " + projectNames + ". Event has been skipped from activity stream", e); 196 } 197 198 199 } 200 } 201 202 // FIXME After merge, the number of events could be lower than limit 203 mergedEvents.addAll(_eventTypeExtensionPoint.mergeEvents(events)); 204 205 return mergedEvents; 206 } 207 208 /** 209 * Get the date of last event regardless the current user's rights 210 * @param projectName The project's name 211 * @param excludeEventTypes the types of event to ignore from this search 212 * @return the date of last event or null if no event found or an error occurred 213 */ 214 public ZonedDateTime getDateOfLastEvent(String projectName, List<String> excludeEventTypes) 215 { 216 List<Expression> expressions = new ArrayList<>(); 217 218 for (String eventType : excludeEventTypes) 219 { 220 expressions.add(new EventTypeExpression(Operator.NE, eventType)); 221 } 222 223 return _getDateOfLastEvent(projectName, new AndExpression(expressions.toArray(new Expression[expressions.size()]))); 224 } 225 226 /** 227 * Get the date of last event regardless the current user's rights 228 * @param projectName The project's name 229 * @param includeEventTypes the types of event to ignore from this search 230 * @return the date of last event or null if no event found or an error occurred 231 */ 232 public ZonedDateTime getDateOfLastEventByEventType(String projectName, Collection<String> includeEventTypes) 233 { 234 List<Expression> expressions = new ArrayList<>(); 235 236 for (String eventType : includeEventTypes) 237 { 238 expressions.add(new EventTypeExpression(Operator.EQ, eventType)); 239 } 240 241 return _getDateOfLastEvent(projectName, new OrExpression(expressions.toArray(new Expression[expressions.size()]))); 242 } 243 244 private ZonedDateTime _getDateOfLastEvent(String projectName, Expression eventTypesExpression) 245 { 246 Expression projectNameExpression = new StringExpression("projectName", Operator.EQ, projectName); 247 248 Expression eventExpr = new AndExpression(projectNameExpression, eventTypesExpression); 249 SortCriteria sortCriteria = new SortCriteria(); 250 sortCriteria.addJCRPropertyCriterion(EventType.EVENT_DATE, false, false); 251 252 String xpathQuery = _getXPathQuery(eventExpr, sortCriteria); 253 NodeIterator nodesIt = _query(xpathQuery); 254 255 if (nodesIt.hasNext()) 256 { 257 Node eventNode = nodesIt.nextNode(); 258 try 259 { 260 Calendar date = eventNode.getProperty(EventType.EVENT_DATE).getDate(); 261 return DateUtils.asZonedDateTime(date); 262 } 263 catch (RepositoryException e) 264 { 265 getLogger().error("Fail to get the last event date for project %s", projectName); 266 } 267 } 268 269 return null; 270 } 271 272 // FIXME Temporary method, should be removed when the events will indexed in Solr 273 private String _getXPathQuery(Expression expr, SortCriteria sortCriteria) 274 { 275 String predicats = StringUtils.trimToNull(expr.build()); 276 277 return "//element(*, ametys:event)" 278 + (predicats != null ? "[" + predicats + "]" : "") 279 + ((sortCriteria != null) ? (" " + sortCriteria.build()) : ""); 280 } 281 282 // FIXME Temporary method, should be removed when the events will indexed in Solr 283 private NodeIterator _query(String jcrQuery) 284 { 285 Session session = null; 286 try 287 { 288 session = _repository.login(); 289 @SuppressWarnings("deprecation") 290 Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH); 291 return query.execute().getNodes(); 292 } 293 catch (RepositoryException ex) 294 { 295 if (session != null) 296 { 297 session.logout(); 298 } 299 300 throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex); 301 } 302 } 303 304 /** 305 * Get the allowed event types classified by categories 306 * @param projectName The project's name 307 * @return The allowed event types 308 */ 309 @Callable 310 public Map<String, Map<String, Object>> getAllowedEventTypes(String projectName) 311 { 312 // FIXME To be rewrite when event will be indexed in Solr 313 // TODO The events stored by workspaces should store their own category. This will be avoid to hard coded this classification. 314 315 Set<String> allowedTypes = new HashSet<>(); 316 317 if (StringUtils.isEmpty(projectName)) 318 { 319 Set<Project> userProjects = getProjectsForCurrentUser(); 320 for (Project userProject : userProjects) 321 { 322 allowedTypes.addAll(_getAllowedEventTypesByProject(userProject)); 323 } 324 } 325 else 326 { 327 Project project = _projectManager.getProject(projectName); 328 allowedTypes.addAll(_getAllowedEventTypesByProject(project)); 329 } 330 331 Map<String, Map<String, Object>> allowedEventTypes = new HashMap<>(); 332 333 allowedEventTypes.put("documents", new HashMap<>()); 334 allowedEventTypes.get("documents").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_DOCUMENTS_LABEL")); 335 336 allowedEventTypes.put("calendars", new HashMap<>()); 337 allowedEventTypes.get("calendars").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_CALENDARS_LABEL")); 338 339 allowedEventTypes.put("threads", new HashMap<>()); 340 allowedEventTypes.get("threads").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_THREADS_LABEL")); 341 342 allowedEventTypes.put("minisite", new HashMap<>()); 343 allowedEventTypes.get("minisite").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_MINISITE_LABEL")); 344 345 allowedEventTypes.put("tasks", new HashMap<>()); 346 allowedEventTypes.get("tasks").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_TASKS_LABEL")); 347 348 allowedEventTypes.put("projects", new HashMap<>()); 349 allowedEventTypes.get("projects").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_PROJECTS_LABEL")); 350 351 Set<String> eventTypeIds = _eventTypeExtensionPoint.getExtensionsIds(); 352 for (String eventTypeId : eventTypeIds) 353 { 354 EventType eventType = _eventTypeExtensionPoint.getExtension(eventTypeId); 355 Map<String, I18nizableText> supportedTypes = eventType.getSupportedTypes(); 356 357 for (Entry<String, I18nizableText> entry : supportedTypes.entrySet()) 358 { 359 if (allowedTypes.contains(entry.getKey())) 360 { 361 _classifyEventType(allowedEventTypes, entry.getKey(), entry.getValue()); 362 } 363 } 364 } 365 366 return allowedEventTypes; 367 } 368 369 private boolean _classifyEventType(Map<String, Map<String, Object>> allowedEventTypes, String eventTypeId, I18nizableText eventTypeLabel) 370 { 371 boolean classified = _classifyEventType("resource.", "documents", allowedEventTypes, eventTypeId, eventTypeLabel) 372 || _classifyEventType("calendar.", "calendars", allowedEventTypes, eventTypeId, eventTypeLabel) 373 || _classifyEventType("task.", "tasks", allowedEventTypes, eventTypeId, eventTypeLabel) 374 || _classifyEventType("thread.", "threads", allowedEventTypes, eventTypeId, eventTypeLabel) 375 || _classifyEventType("member.", "projects", allowedEventTypes, eventTypeId, eventTypeLabel) 376 || _classifyEventType("wallcontent.", "projects", allowedEventTypes, eventTypeId, eventTypeLabel) 377 || _classifyEventType("wiki.", "wiki", allowedEventTypes, eventTypeId, eventTypeLabel); 378 379 return classified; 380 381 } 382 383 @SuppressWarnings("unchecked") 384 private boolean _classifyEventType(String withPrefix, String toAllowedEventType, Map<String, Map<String, Object>> allowedEventTypes, String eventTypeId, I18nizableText eventTypeLabel) 385 { 386 if (eventTypeId.startsWith(withPrefix)) 387 { 388 if (!allowedEventTypes.get(toAllowedEventType).containsKey("eventTypes")) 389 { 390 allowedEventTypes.get(toAllowedEventType).put("eventTypes", new HashMap<String, Object>()); 391 } 392 393 ((Map<String, I18nizableText>) allowedEventTypes.get(toAllowedEventType).get("eventTypes")).put(eventTypeId, eventTypeLabel); 394 395 return true; 396 } 397 398 return false; 399 } 400 401 /** 402 * Get the list of allowed event types for the given projects 403 * @param projects The projects 404 * @return The allowed event types 405 */ 406 public Set<String> getAllowedEventTypes (Set<Project> projects) 407 { 408 Set<String> allowedTypes = new HashSet<>(); 409 410 for (Project project : projects) 411 { 412 allowedTypes.addAll(_getAllowedEventTypesByProject(project)); 413 } 414 415 return allowedTypes; 416 } 417 418 // FIXME temporary method 419 // The allowed types are hard coded according the user access on root modules. 420 private Set<String> _getAllowedEventTypesByProject (Project project) 421 { 422 Set<String> allowedTypes = new HashSet<>(); 423 424 for (WorkspaceModule moduleManager : _projectManager.getModules(project)) 425 { 426 ModifiableResourceCollection moduleRoot = moduleManager.getModuleRoot(project, false); 427 if (moduleRoot != null && _rightManager.currentUserHasReadAccess(moduleRoot)) 428 { 429 allowedTypes.addAll(moduleManager.getAllowedEventTypes()); 430 } 431 } 432 433 return allowedTypes; 434 } 435 436 /** 437 * Get the events for the current user with the allowed event types get into the user projects. 438 * @param limit The max number of results 439 * @return The events for the user projects 440 */ 441 public List<Map<String, Object>> getEventsForCurrentUser(int limit) 442 { 443 Set<Project> userProjects = getProjectsForCurrentUser(); 444 return getEventsForCurrentUser(userProjects, limit); 445 } 446 447 /** 448 * Get the events for the current user with the allowed event types get into the given projects. 449 * @param projects the projects 450 * @param limit The max number of results 451 * @return The events for the user projects 452 */ 453 public List<Map<String, Object>> getEventsForCurrentUser(Set<Project> projects, int limit) 454 { 455 List<String> projectNames = transformProjectsToName(projects); 456 Set<String> allowedEventTypes = getAllowedEventTypes(projects); 457 458 List<Map<String, Object>> events = getEvents(projectNames, new ArrayList<>(allowedEventTypes), limit); 459 460 // Add a parameter representing the date in the ISO 8601 format 461 events.stream().forEach(event -> 462 { 463 // start date 464 event.put("date-iso", DateUtils.dateToString((Date) event.get("date"))); 465 466 // optional end date 467 Date endDate = (Date) event.get("endDate"); 468 if (endDate != null) 469 { 470 event.put("end-date-iso", DateUtils.dateToString(endDate)); 471 } 472 }); 473 474 return events; 475 } 476 477 private Set<Project> getProjectsForCurrentUser() 478 { 479 UserIdentity user = _currentUserProvider.getUser(); 480 return _projectManager.getUserProjects(user).keySet(); 481 } 482 483 private List<String> transformProjectsToName(Set<Project> userProjects) 484 { 485 return userProjects.stream() 486 .map(p -> p.getName()) 487 .collect(Collectors.toList()); 488 } 489}