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