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