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.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 = new HashMap<>();
139            for (UserIdentity user : users)
140            {
141                Map<String, Set<String>> tokensForUser = _userPreferencesHelper.getUserImpactedTokens(user, feedIds);
142                for (Entry<String, Set<String>> tokensByFeed : tokensForUser.entrySet())
143                {
144                    Set<String> tokens = notificationsNeeded.computeIfAbsent(tokensByFeed.getKey(), __ -> new HashSet<>());
145                    tokens.addAll(tokensByFeed.getValue());
146                }
147            }
148            
149            for (Entry<String, Set<String>> entry : notificationsNeeded.entrySet())
150            {
151                Map<String, Object> notificationData = new HashMap<>();
152                notificationData.putAll(data);
153                String feedId = entry.getKey();
154                notificationData.put("feed_id", feedId);
155                if (queryMap.containsKey(feedId))
156                {
157                    notificationData.put("category_name", queryMap.get(feedId).getTitle());
158                }
159                
160                String sortField = null;
161                if (sorts.containsKey(feedId))
162                {
163                    sortField = sorts.get(feedId);
164                }
165                String isoDate = _queryHelper.getContentFormattedDate(resolvedContent, sortField);
166                notificationData.put("date", isoDate);
167                
168                String description = _getContentDescription(resolvedContent);
169                
170                _pushNotificationManager.pushNotifications(resolvedContent.getTitle(), description, entry.getValue(), notificationData);
171            }
172        }
173    }
174    
175    /**
176     * Get a description for the notification.
177     * 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).
178     * If none is available, an empty String is returned.
179     * @param content The content to read
180     * @return a description for this content
181     */
182    protected String _getContentDescription(Content content)
183    {
184        Long maxSize = Config.getInstance().getValue(__DESCRIPTION_MAX_SIZE_CONF_ID);
185        String result = "";
186        
187        if (content.hasValue("abstract") && org.ametys.runtime.model.type.ModelItemTypeConstants.STRING_TYPE_ID.equals(content.getDefinition("abstract").getType().getId()))
188        {
189            result = content.getValue("abstract", false, "");
190        }
191        else if (content.hasValue("content") && ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID.equals(content.getDefinition("content").getType().getId()))
192        {
193            RichText richText = content.getValue("content");
194            result = _richTextHelper.richTextToString(richText, maxSize.intValue());
195        }
196        
197        return result;
198    }
199}