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 } 188 } 189 catch (RepositoryException e) 190 { 191 getLogger().error("Unable to retrieve event '" + eventNode.toString() + "' for activity stream of project(s) " + projectNames + ". Event has been skipped from activity stream", e); 192 } 193 194 count++; 195 } 196 } 197 198 // FIXME After merge, the number of events could be lower than limit 199 mergedEvents.addAll(_eventTypeExtensionPoint.mergeEvents(events)); 200 201 return mergedEvents; 202 } 203 204 // FIXME Temporary method, should be removed when the events will indexed in Solr 205 private String _getXPathQuery(Expression expr, SortCriteria sortCriteria) 206 { 207 String predicats = StringUtils.trimToNull(expr.build()); 208 209 return "//element(*, ametys:event)" 210 + (predicats != null ? "[" + predicats + "]" : "") 211 + ((sortCriteria != null) ? (" " + sortCriteria.build()) : ""); 212 } 213 214 // FIXME Temporary method, should be removed when the events will indexed in Solr 215 private NodeIterator _query(String jcrQuery) 216 { 217 Session session = null; 218 try 219 { 220 session = _repository.login(); 221 @SuppressWarnings("deprecation") 222 Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH); 223 return query.execute().getNodes(); 224 } 225 catch (RepositoryException ex) 226 { 227 if (session != null) 228 { 229 session.logout(); 230 } 231 232 throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex); 233 } 234 } 235 236 /** 237 * Get the allowed event types classified by categories 238 * @param projectName The project's name 239 * @return The allowed event types 240 */ 241 @Callable 242 public Map<String, Map<String, Object>> getAllowedEventTypes(String projectName) 243 { 244 // FIXME To be rewrite when event will be indexed in Solr 245 // TODO The events stored by workspaces should store their own category. This will be avoid to hard coded this classification. 246 247 Set<String> allowedTypes = new HashSet<>(); 248 249 if (StringUtils.isEmpty(projectName)) 250 { 251 List<Project> userProjects = getProjectsForCurrentUser(); 252 for (Project userProject : userProjects) 253 { 254 allowedTypes.addAll(_getAllowedEventTypesByProject(userProject)); 255 } 256 } 257 else 258 { 259 Project project = _projectManager.getProject(projectName); 260 allowedTypes.addAll(_getAllowedEventTypesByProject(project)); 261 } 262 263 Map<String, Map<String, Object>> allowedEventTypes = new HashMap<>(); 264 265 allowedEventTypes.put("documents", new HashMap<>()); 266 allowedEventTypes.get("documents").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_DOCUMENTS_LABEL")); 267 268 allowedEventTypes.put("calendars", new HashMap<>()); 269 allowedEventTypes.get("calendars").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_CALENDARS_LABEL")); 270 271 allowedEventTypes.put("threads", new HashMap<>()); 272 allowedEventTypes.get("threads").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_THREADS_LABEL")); 273 274 allowedEventTypes.put("wiki", new HashMap<>()); 275 allowedEventTypes.get("wiki").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_WIKI_LABEL")); 276 277 allowedEventTypes.put("tasks", new HashMap<>()); 278 allowedEventTypes.get("tasks").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_TASKS_LABEL")); 279 280 allowedEventTypes.put("projects", new HashMap<>()); 281 allowedEventTypes.get("projects").put("label", new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_SERVICE_ACTIVITY_STREAM_PROJECTS_LABEL")); 282 283 Set<String> eventTypeIds = _eventTypeExtensionPoint.getExtensionsIds(); 284 for (String eventTypeId : eventTypeIds) 285 { 286 EventType eventType = _eventTypeExtensionPoint.getExtension(eventTypeId); 287 Map<String, I18nizableText> supportedTypes = eventType.getSupportedTypes(); 288 289 for (Entry<String, I18nizableText> entry : supportedTypes.entrySet()) 290 { 291 if (allowedTypes.contains(entry.getKey())) 292 { 293 _classifyEventType(allowedEventTypes, entry.getKey(), entry.getValue()); 294 } 295 } 296 } 297 298 return allowedEventTypes; 299 } 300 301 private boolean _classifyEventType(Map<String, Map<String, Object>> allowedEventTypes, String eventTypeId, I18nizableText eventTypeLabel) 302 { 303 boolean classified = _classifyEventType("resource.", "documents", allowedEventTypes, eventTypeId, eventTypeLabel) 304 || _classifyEventType("calendar.", "calendars", allowedEventTypes, eventTypeId, eventTypeLabel) 305 || _classifyEventType("task.", "tasks", allowedEventTypes, eventTypeId, eventTypeLabel) 306 || _classifyEventType("thread.", "threads", allowedEventTypes, eventTypeId, eventTypeLabel) 307 || _classifyEventType("member.", "projects", allowedEventTypes, eventTypeId, eventTypeLabel) 308 || _classifyEventType("wiki.", "wiki", allowedEventTypes, eventTypeId, eventTypeLabel); 309 310 return classified; 311 312 } 313 314 @SuppressWarnings("unchecked") 315 private boolean _classifyEventType(String withPrefix, String toAllowedEventType, Map<String, Map<String, Object>> allowedEventTypes, String eventTypeId, I18nizableText eventTypeLabel) 316 { 317 if (eventTypeId.startsWith(withPrefix)) 318 { 319 if (!allowedEventTypes.get(toAllowedEventType).containsKey("eventTypes")) 320 { 321 allowedEventTypes.get(toAllowedEventType).put("eventTypes", new HashMap<String, Object>()); 322 } 323 324 ((Map<String, I18nizableText>) allowedEventTypes.get(toAllowedEventType).get("eventTypes")).put(eventTypeId, eventTypeLabel); 325 326 return true; 327 } 328 329 return false; 330 } 331 332 /** 333 * Get the list of allowed event types for the given projects 334 * @param projects The projects 335 * @return The allowed event types 336 */ 337 public Set<String> getAllowedEventTypes (List<Project> projects) 338 { 339 Set<String> allowedTypes = new HashSet<>(); 340 341 for (Project project : projects) 342 { 343 allowedTypes.addAll(_getAllowedEventTypesByProject(project)); 344 } 345 346 return allowedTypes; 347 } 348 349 // FIXME temporary method 350 // The allowed types are hard coded according the user access on root modules. 351 private Set<String> _getAllowedEventTypesByProject (Project project) 352 { 353 Set<String> allowedTypes = new HashSet<>(); 354 355 for (WorkspaceModule moduleManager : _projectManager.getModules(project)) 356 { 357 ModifiableResourceCollection moduleRoot = moduleManager.getModuleRoot(project, false); 358 if (moduleRoot != null && _rightManager.currentUserHasReadAccess(moduleRoot)) 359 { 360 allowedTypes.addAll(moduleManager.getAllowedEventTypes()); 361 } 362 } 363 364 if (_rightManager.currentUserHasReadAccess(project)) 365 { 366 Collections.addAll(allowedTypes, "member.added"); 367 } 368 369 return allowedTypes; 370 } 371 372 /** 373 * Get the events for the current user with the allowed event types get into the user projects. 374 * @param limit The max number of results 375 * @return The events for the user projects 376 */ 377 public List<Map<String, Object>> getEventsForCurrentUser(int limit) 378 { 379 List<Project> userProjects = getProjectsForCurrentUser(); 380 List<String> projectNames = transformProjectsToName(userProjects); 381 Set<String> allowedEventTypes = getAllowedEventTypes(userProjects); 382 383 List<Map<String, Object>> events = getEvents(projectNames, new ArrayList<>(allowedEventTypes), limit); 384 385 // Add a parameter representing the date in the ISO 8601 format 386 events.stream().forEach(event -> 387 { 388 // start date 389 event.put("date-iso", DateUtils.dateToString((Date) event.get("date"))); 390 391 // optional end date 392 Date endDate = (Date) event.get("endDate"); 393 if (endDate != null) 394 { 395 event.put("end-date-iso", DateUtils.dateToString(endDate)); 396 } 397 }); 398 399 return events; 400 } 401 402 private List<Project> getProjectsForCurrentUser() 403 { 404 UserIdentity user = _currentUserProvider.getUser(); 405 return _projectManager.getUserProjects(user); 406 } 407 408 private List<String> transformProjectsToName(List<Project> userProjects) 409 { 410 return userProjects.stream() 411 .map(p -> p.getName()) 412 .collect(Collectors.toList()); 413 } 414}