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