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    }
201    
202    /**
203     * Get the event linked to this event
204     * @param event the event to read
205     * @return the project linked to this event
206     */
207    protected Project getProject(Event event)
208    {
209        Project project = null;
210        Map<String, Object> args = event.getArguments();
211
212        if (args.containsKey("projectName"))
213        {
214            String projectName = (String) args.get("projectName");
215            project = _projectManager.getProject(projectName);
216        }
217        else if (args.containsKey(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR))
218        {
219            Calendar calendar = (Calendar) args.get(org.ametys.plugins.workspaces.calendars.ObservationConstants.ARGS_CALENDAR);
220            project = getProject(calendar);
221        }
222        else if (args.containsKey(org.ametys.plugins.workspaces.ObservationConstants.ARGS_TASK))
223        {
224            Task task = (Task) args.get(org.ametys.plugins.workspaces.ObservationConstants.ARGS_TASK);
225            project = getProject(task);
226        }
227        else if (args.containsKey(ObservationConstants.ARGS_THREAD))
228        {
229            JCRThread thread = (JCRThread) args.get(ObservationConstants.ARGS_THREAD);
230            project = getProject(thread);
231        }
232        else if (args.containsKey(org.ametys.plugins.workspaces.ObservationConstants.ARGS_PROJECT))
233        {
234            project = (Project) args.get(org.ametys.plugins.workspaces.ObservationConstants.ARGS_PROJECT);
235        }
236        else if (args.containsKey(org.ametys.cms.ObservationConstants.ARGS_CONTENT))
237        {
238            Content content = (Content) args.get(org.ametys.cms.ObservationConstants.ARGS_CONTENT);
239            project = getProject(content);
240            
241            if (project == null)
242            {
243                String[] cTypes = content.getTypes();
244                if (ArrayUtils.contains(cTypes, WorkspacesConstants.PROJECT_ARTICLE_CONTENT_TYPE) && content instanceof WebContent)
245                {
246                    Request request = ContextHelper.getRequest(_context);
247                    String siteName = (String) request.getAttribute("siteName");
248                    List<String> projects = _projectManager.getProjectsForSite(siteName);
249                    if (projects.size() > 0)
250                    {
251                        project = _projectManager.getProject(projects.get(0));
252                    }
253                }
254            }
255        }
256        else if (args.containsKey(ObservationConstants.ARGS_PARENT_ID))
257        {
258            String parentId = (String) args.get(ObservationConstants.ARGS_PARENT_ID);
259            AmetysObject ao = _resolver.resolveById(parentId);
260            
261            project = getProject(ao);
262        }
263        
264        return project;
265    }
266    
267    /**
268     * Get the parent project
269     * @param ao The ametys object
270     * @return The parent project or <code>null</code> if not found
271     */
272    protected Project getProject(AmetysObject ao)
273    {
274        
275        AmetysObject parentAO = _resolver.resolveById(ao.getId());
276        while (parentAO != null)
277        {
278            if (parentAO instanceof Project)
279            {
280                return (Project) parentAO;
281            } 
282            parentAO = parentAO.getParent();
283        }
284        
285        return null;
286    }
287    
288    private Node getNode(String id)
289    {
290        Session session = null;
291        try
292        {
293            session = _repository.login();
294            
295            return session.getNodeByIdentifier(id);
296        }
297        catch (RepositoryException ex)
298        {
299            if (session != null)
300            {
301                session.logout();
302            }
303
304            throw new AmetysRepositoryException("An error occured executing the Event : " + id, ex);
305        }
306    }
307}