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