001/* 002 * Copyright 2020 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.mobileapp.observer; 017 018import java.util.Collection; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.List; 022import java.util.Map; 023import java.util.Map.Entry; 024import java.util.Set; 025import java.util.function.Function; 026import java.util.stream.Collectors; 027 028import javax.jcr.Node; 029import javax.jcr.Repository; 030import javax.jcr.RepositoryException; 031import javax.jcr.Session; 032 033import org.apache.avalon.framework.context.Context; 034import org.apache.avalon.framework.context.ContextException; 035import org.apache.avalon.framework.context.Contextualizable; 036import org.apache.avalon.framework.service.ServiceException; 037import org.apache.avalon.framework.service.ServiceManager; 038import org.apache.avalon.framework.service.Serviceable; 039import org.apache.cocoon.components.ContextHelper; 040import org.apache.cocoon.environment.Request; 041import org.apache.commons.lang3.ArrayUtils; 042 043import org.ametys.cms.repository.Content; 044import org.ametys.core.observation.AsyncObserver; 045import org.ametys.core.observation.Event; 046import org.ametys.core.user.User; 047import org.ametys.core.user.UserManager; 048import org.ametys.core.user.population.UserPopulationDAO; 049import org.ametys.plugins.explorer.ObservationConstants; 050import org.ametys.plugins.explorer.threads.jcr.JCRThread; 051import org.ametys.plugins.mobileapp.FeedHelper; 052import org.ametys.plugins.mobileapp.PushNotificationManager; 053import org.ametys.plugins.mobileapp.UserPreferencesHelper; 054import org.ametys.plugins.repository.AmetysObject; 055import org.ametys.plugins.repository.AmetysObjectResolver; 056import org.ametys.plugins.repository.AmetysRepositoryException; 057import org.ametys.plugins.repository.events.EventType; 058import org.ametys.plugins.repository.events.EventTypeExtensionPoint; 059import org.ametys.plugins.repository.provider.AbstractRepository; 060import org.ametys.plugins.workspaces.WorkspacesConstants; 061import org.ametys.plugins.workspaces.calendars.Calendar; 062import org.ametys.plugins.workspaces.events.AbstractWorkspacesEventsObserver; 063import org.ametys.plugins.workspaces.project.ProjectManager; 064import org.ametys.plugins.workspaces.project.modules.WorkspaceModule; 065import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 066import org.ametys.plugins.workspaces.project.objects.Project; 067import org.ametys.plugins.workspaces.tasks.Task; 068import org.ametys.runtime.plugin.component.AbstractLogEnabled; 069import org.ametys.web.repository.content.WebContent; 070 071/** 072 * On validation, test each query to notify impacted users 073 */ 074public class ProjectEventObserver extends AbstractLogEnabled implements AsyncObserver, Serviceable, Contextualizable 075{ 076 /** Event type extension point */ 077 protected EventTypeExtensionPoint _eventTypeExtensionPoint; 078 079 /** Feed helper */ 080 protected FeedHelper _feedHelper; 081 082 /** User Preferences Helper */ 083 protected UserPreferencesHelper _userPreferencesHelper; 084 085 /** Push Notification Manager */ 086 protected PushNotificationManager _pushNotificationManager; 087 088 /** The user manager */ 089 protected UserManager _userManager; 090 091 /** The user population DAO */ 092 protected UserPopulationDAO _userPopulationDAO; 093 094 /** Project Manager */ 095 protected ProjectManager _projectManager; 096 097 /** Context */ 098 protected Context _context; 099 100 /** Ametys Object Resolver */ 101 protected AmetysObjectResolver _resolver; 102 103 /** Handled events */ 104 protected Set<String> _allowedEvents; 105 106 /** The repository */ 107 protected Repository _repository; 108 109 @Override 110 public void service(ServiceManager manager) throws ServiceException 111 { 112 _eventTypeExtensionPoint = (EventTypeExtensionPoint) manager.lookup(EventTypeExtensionPoint.ROLE); 113 _feedHelper = (FeedHelper) manager.lookup(FeedHelper.ROLE); 114 _userPreferencesHelper = (UserPreferencesHelper) manager.lookup(UserPreferencesHelper.ROLE); 115 _pushNotificationManager = (PushNotificationManager) manager.lookup(PushNotificationManager.ROLE); 116 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 117 _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE); 118 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 119 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 120 _repository = (Repository) manager.lookup(AbstractRepository.ROLE); 121 122 WorkspaceModuleExtensionPoint workspaceModuleExtensionPoint = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 123 _allowedEvents = workspaceModuleExtensionPoint.getModules().stream().map(WorkspaceModule::getAllowedEventTypes).flatMap(Set::stream).collect(Collectors.toSet()); 124 } 125 126 public void contextualize(Context context) throws ContextException 127 { 128 _context = context; 129 } 130 131 public boolean supports(Event event) 132 { 133 return _allowedEvents.contains(event.getId()); 134 } 135 136 public int getPriority(Event event) 137 { 138 return MIN_PRIORITY; 139 } 140 141 public void observe(Event event, Map<String, Object> transientVars) throws Exception 142 { 143 Project project = getProject(event); 144 EventType eventType = _eventTypeExtensionPoint.getEventType(event.getId()); 145 146 if (eventType != null && project != null) 147 { 148 getLogger().info("Listing push notification to send for event on project '{}'", project.getId()); 149 String nodeId = (String) transientVars.get(AbstractWorkspacesEventsObserver.NODE_ID_EVENT_TRANSIENT_VAR); 150 Node node = getNode(nodeId); 151 152 Map<String, Object> event2json = eventType.event2JSON(node); 153 Map<String, Object> mergedEvent = eventType.mergeEvents(List.of(event2json)); 154 155 Map<String, Object> projectJson = _feedHelper.projectToMap(project); 156 157 List<String> userPopulationsIds = _userPopulationDAO.getUserPopulationsIds(); 158 Collection<User> users = _userManager.getUsersByPopulationIds(userPopulationsIds); 159 160 // Get all map of language => tokens for all users, and generate a new big map language => tokens 161 Map<String, Set<String>> langAndTokens = new HashMap<>(); 162 for (User user : users) 163 { 164 Map<String, Set<String>> tokensForUser = _userPreferencesHelper.getUserImpactedTokens(user.getIdentity(), project, event.getId()); 165 for (Entry<String, Set<String>> tokensByLang : tokensForUser.entrySet()) 166 { 167 Set<String> tokens = langAndTokens.computeIfAbsent(tokensByLang.getKey(), __ -> new HashSet<>()); 168 tokens.addAll(tokensByLang.getValue()); 169 } 170 } 171 172 // Get the event infos for each languages 173 Map<String, Map<String, Object>> translatedNotificationContent = langAndTokens.keySet().stream() 174 .distinct() 175 .collect(Collectors.toMap(Function.identity(), lang -> _feedHelper.getEventInfos(mergedEvent, projectJson, lang))); 176 177 for (Entry<String, Set<String>> entry : langAndTokens.entrySet()) 178 { 179 String lang = entry.getKey(); 180 Set<String> tokens = entry.getValue(); 181 Map<String, Object> notificationData = translatedNotificationContent.get(lang); 182 183 try 184 { 185 @SuppressWarnings("unchecked") 186 Map<String, Object> category = (Map<String, Object>) ((Map<String, Object>) notificationData.get("project")).get("category"); 187 String categoryLabel = ((org.ametys.runtime.i18n.I18nizableText) category.get("title")).getLabel(); 188 category.put("title", categoryLabel); 189 } 190 catch (Exception e) 191 { 192 // Nothing, this is just a hack do pass the notification framework jsonifier 193 } 194 195 String title = project.getTitle(); 196 String message = (String) notificationData.get("short-description"); 197 _pushNotificationManager.pushNotifications(title, message, tokens, notificationData); 198 } 199 } 200 else 201 { 202 getLogger().error("Impossible to push a notification for event with id '" + event.getId() + "', no EventType or Project found."); 203 } 204 } 205 206 /** 207 * Get the event linked to this event 208 * @param event the event to read 209 * @return the project linked to this event 210 */ 211 protected Project getProject(Event event) 212 { 213 Project project = null; 214 Map<String, Object> args = event.getArguments(); 215 216 if (args.containsKey("projectName")) 217 { 218 String projectName = (String) args.get("projectName"); 219 project = _projectManager.getProject(projectName); 220 } 221 else if (args.containsKey(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR)) 222 { 223 Calendar calendar = (Calendar) args.get(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR); 224 project = getProject(calendar); 225 } 226 else if (args.containsKey(org.ametys.plugins.workspaces.ObservationConstants.ARGS_TASK)) 227 { 228 Task task = (Task) args.get(org.ametys.plugins.workspaces.ObservationConstants.ARGS_TASK); 229 project = getProject(task); 230 } 231 else if (args.containsKey(ObservationConstants.ARGS_THREAD)) 232 { 233 JCRThread thread = (JCRThread) args.get(ObservationConstants.ARGS_THREAD); 234 project = getProject(thread); 235 } 236 else if (args.containsKey(org.ametys.plugins.workspaces.ObservationConstants.ARGS_PROJECT)) 237 { 238 project = (Project) args.get(org.ametys.plugins.workspaces.ObservationConstants.ARGS_PROJECT); 239 } 240 else if (args.containsKey(org.ametys.cms.ObservationConstants.ARGS_CONTENT)) 241 { 242 Content content = (Content) args.get(org.ametys.cms.ObservationConstants.ARGS_CONTENT); 243 project = getProject(content); 244 245 if (project == null) 246 { 247 String[] cTypes = content.getTypes(); 248 if (ArrayUtils.contains(cTypes, WorkspacesConstants.PROJECT_ARTICLE_CONTENT_TYPE) && content instanceof WebContent) 249 { 250 Request request = ContextHelper.getRequest(_context); 251 String siteName = (String) request.getAttribute("siteName"); 252 List<String> projects = _projectManager.getProjectsForSite(siteName); 253 if (projects.size() > 0) 254 { 255 project = _projectManager.getProject(projects.get(0)); 256 } 257 } 258 } 259 } 260 else if (args.containsKey(ObservationConstants.ARGS_PARENT_ID)) 261 { 262 String parentId = (String) args.get(ObservationConstants.ARGS_PARENT_ID); 263 AmetysObject ao = _resolver.resolveById(parentId); 264 265 project = getProject(ao); 266 } 267 268 return project; 269 } 270 271 /** 272 * Get the parent project 273 * @param ao The ametys object 274 * @return The parent project or <code>null</code> if not found 275 */ 276 protected Project getProject(AmetysObject ao) 277 { 278 279 AmetysObject parentAO = _resolver.resolveById(ao.getId()); 280 while (parentAO != null) 281 { 282 if (parentAO instanceof Project) 283 { 284 return (Project) parentAO; 285 } 286 parentAO = parentAO.getParent(); 287 } 288 289 return null; 290 } 291 292 private Node getNode(String id) 293 { 294 Session session = null; 295 try 296 { 297 session = _repository.login(); 298 299 return session.getNodeByIdentifier(id); 300 } 301 catch (RepositoryException ex) 302 { 303 if (session != null) 304 { 305 session.logout(); 306 } 307 308 throw new AmetysRepositoryException("An error occured executing the Event : " + id, ex); 309 } 310 } 311}