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