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