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