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}