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}