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 org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.commons.collections4.CollectionUtils;
031
032import org.ametys.cms.ObservationConstants;
033import org.ametys.cms.data.RichText;
034import org.ametys.cms.data.RichTextHelper;
035import org.ametys.cms.data.type.ModelItemTypeConstants;
036import org.ametys.cms.repository.Content;
037import org.ametys.core.observation.AsyncObserver;
038import org.ametys.core.observation.Event;
039import org.ametys.core.right.AllowedUsers;
040import org.ametys.core.right.RightManager;
041import org.ametys.core.user.User;
042import org.ametys.core.user.UserIdentity;
043import org.ametys.core.user.UserManager;
044import org.ametys.core.user.population.UserPopulationDAO;
045import org.ametys.plugins.mobileapp.PushNotificationManager;
046import org.ametys.plugins.mobileapp.QueriesHelper;
047import org.ametys.plugins.mobileapp.UserPreferencesHelper;
048import org.ametys.plugins.queriesdirectory.Query;
049import org.ametys.plugins.repository.AmetysObjectResolver;
050import org.ametys.runtime.config.Config;
051import org.ametys.runtime.plugin.component.AbstractLogEnabled;
052
053/**
054 * On validation, test each query to notify impacted users
055 */
056public class ContentValidatedObserver extends AbstractLogEnabled implements AsyncObserver, Serviceable
057{
058    /** Max size of description field (when content is used) */
059    protected static final String __DESCRIPTION_MAX_SIZE_CONF_ID = "plugin.mobileapp.push.description.richtext.max";
060
061    /** The Ametys object resolver */
062    protected QueriesHelper _queryHelper;
063
064    /** User Preferences Helper */
065    protected UserPreferencesHelper _userPreferencesHelper;
066
067    /** Push Notification Manager */
068    protected PushNotificationManager _pushNotificationManager;
069
070    /** The user manager */
071    protected UserManager _userManager;
072
073    /** The user population DAO */
074    protected UserPopulationDAO _userPopulationDAO;
075
076    /** The Ametys object resolver */
077    protected AmetysObjectResolver _resolver;
078
079    /** Right Manager */
080    protected RightManager _rightManager;
081
082    /** RichText Helper */
083    RichTextHelper _richTextHelper;
084
085    @Override
086    public void service(ServiceManager manager) throws ServiceException
087    {
088        _queryHelper = (QueriesHelper) manager.lookup(QueriesHelper.ROLE);
089        _userPreferencesHelper = (UserPreferencesHelper) manager.lookup(UserPreferencesHelper.ROLE);
090        _pushNotificationManager = (PushNotificationManager) manager.lookup(PushNotificationManager.ROLE);
091
092        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
093        _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE);
094        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
095        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
096        _richTextHelper = (RichTextHelper) manager.lookup(RichTextHelper.ROLE);
097    }
098
099    public boolean supports(Event event)
100    {
101        return event.getId().equals(ObservationConstants.EVENT_CONTENT_VALIDATED);
102    }
103
104    public int getPriority(Event event)
105    {
106        return MIN_PRIORITY;
107    }
108
109    public void observe(Event event, Map<String, Object> transientVars) throws Exception
110    {
111        Content content = (Content) event.getArguments().get(ObservationConstants.ARGS_CONTENT);
112        List<Query> queries = _queryHelper.getQueriesForResult(content.getId());
113
114        getLogger().info("{} queries found for content {}", queries.size(), content.getId());
115
116        if (CollectionUtils.isNotEmpty(queries))
117        {
118            Content resolvedContent = (Content) _resolver.resolveById(content.getId());
119            Map<String, String> data = _queryHelper.getDataForContent(resolvedContent);
120            Map<String, Query> queryMap = queries.stream().collect(Collectors.toMap(Query::getId, Function.identity()));
121            List<String> feedIds = queries.stream().map(Query::getId).collect(Collectors.toList());
122            Map<String, String> sorts = queries.stream().collect(Collectors.toMap(Query::getId, q -> _queryHelper.getSortProperty(q, true).get(0).getField()));
123
124            AllowedUsers readAccessAllowedUsers = _rightManager.getReadAccessAllowedUsers(resolvedContent);
125            Set<UserIdentity> users;
126            if (readAccessAllowedUsers.isAnonymousAllowed() || readAccessAllowedUsers.isAnyConnectedUserAllowed())
127            {
128                List<String> userPopulationsIds = _userPopulationDAO.getUserPopulationsIds();
129                Collection<User> allUsers = _userManager.getUsersByPopulationIds(userPopulationsIds);
130                users = allUsers.stream().map(User::getIdentity).collect(Collectors.toSet());
131            }
132            else
133            {
134                users = readAccessAllowedUsers.resolveAllowedUsers(true);
135            }
136
137            Map<String, Map<UserIdentity, Set<String>>> notificationsNeeded = new HashMap<>();
138            for (UserIdentity user : users)
139            {
140                Map<String, Set<String>> tokensForUser = _userPreferencesHelper.getUserImpactedTokens(user, feedIds);
141                for (Entry<String, Set<String>> tokensByFeed : tokensForUser.entrySet())
142                {
143                    Map<UserIdentity, Set<String>> tokens = notificationsNeeded.computeIfAbsent(tokensByFeed.getKey(), __ -> new HashMap<>());
144                    tokens.put(user, tokensByFeed.getValue());
145                }
146            }
147
148            for (Entry<String, Map<UserIdentity, Set<String>>> entry : notificationsNeeded.entrySet())
149            {
150                Map<String, Object> notificationData = new HashMap<>();
151                notificationData.putAll(data);
152                String feedId = entry.getKey();
153                notificationData.put("feed_id", feedId);
154                if (queryMap.containsKey(feedId))
155                {
156                    notificationData.put("category_name", queryMap.get(feedId).getTitle());
157                }
158
159                String sortField = null;
160                if (sorts.containsKey(feedId))
161                {
162                    sortField = sorts.get(feedId);
163                }
164                String isoDate = _queryHelper.getContentFormattedDate(resolvedContent, sortField);
165                notificationData.put("date", isoDate);
166
167                String description = _getContentDescription(resolvedContent);
168
169                _pushNotificationManager.pushNotifications(resolvedContent.getTitle(), description, entry.getValue(), notificationData);
170            }
171        }
172    }
173
174    /**
175     * Get a description for the notification.
176     * It will first try to read a "abstract" value in the content, and if not available, will try to read a rich-text stored in "content" (and cut it down).
177     * If none is available, an empty String is returned.
178     * @param content The content to read
179     * @return a description for this content
180     */
181    protected String _getContentDescription(Content content)
182    {
183        Long maxSize = Config.getInstance().getValue(__DESCRIPTION_MAX_SIZE_CONF_ID);
184        String result = "";
185
186        if (content.hasValue("abstract") && org.ametys.runtime.model.type.ModelItemTypeConstants.STRING_TYPE_ID.equals(content.getDefinition("abstract").getType().getId()))
187        {
188            result = content.getValue("abstract", false, "");
189        }
190        else if (content.hasValue("content") && ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(content.getDefinition("content").getType().getId()))
191        {
192            RichText richText = content.getValue("content");
193            result = _richTextHelper.richTextToString(richText, maxSize.intValue());
194        }
195
196        return result;
197    }
198}