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