001/* 002 * Copyright 2024 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.pagesubscription.notification; 017 018import java.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.List; 021import java.util.Map; 022import java.util.Optional; 023 024import org.apache.avalon.framework.activity.Initializable; 025import org.apache.avalon.framework.component.Component; 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.avalon.framework.service.Serviceable; 029 030import org.ametys.core.cache.AbstractCacheManager; 031import org.ametys.core.cache.Cache; 032import org.ametys.core.ui.Callable; 033import org.ametys.core.user.CurrentUserProvider; 034import org.ametys.core.user.UserIdentity; 035import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 036import org.ametys.plugins.core.ui.user.ProfileImageResolverHelper; 037import org.ametys.plugins.core.user.UserHelper; 038import org.ametys.plugins.pagesubscription.BroadcastChannelHelper.BroadcastChannel; 039import org.ametys.plugins.pagesubscription.FrequencyHelper; 040import org.ametys.plugins.pagesubscription.FrequencyHelper.Frequency; 041import org.ametys.plugins.pagesubscription.Subscription; 042import org.ametys.plugins.pagesubscription.context.PageSubscriptionContext; 043import org.ametys.plugins.pagesubscription.type.PageSubscriptionType; 044import org.ametys.plugins.pagesubscription.type.SubscriptionType.FrequencyTiming; 045import org.ametys.plugins.pagesubscription.type.SubscriptionTypeExtensionPoint; 046import org.ametys.plugins.repository.AmetysObjectIterable; 047import org.ametys.plugins.repository.AmetysObjectResolver; 048import org.ametys.plugins.repository.activities.Activity; 049import org.ametys.plugins.repository.activities.ActivityFactory; 050import org.ametys.plugins.repository.activities.ActivityHelper; 051import org.ametys.plugins.repository.activities.ActivityType; 052import org.ametys.plugins.repository.activities.ActivityTypeExtensionPoint; 053import org.ametys.plugins.repository.query.expression.AndExpression; 054import org.ametys.plugins.repository.query.expression.Expression; 055import org.ametys.plugins.repository.query.expression.Expression.Operator; 056import org.ametys.plugins.repository.query.expression.OrExpression; 057import org.ametys.plugins.repository.query.expression.StringExpression; 058import org.ametys.runtime.i18n.I18nizableText; 059import org.ametys.runtime.plugin.component.AbstractLogEnabled; 060import org.ametys.web.activities.AbstractPageActivityType; 061import org.ametys.web.activities.AbstractSiteAwareActivityType; 062import org.ametys.web.activities.PageResourcesUpdatedActivityType; 063import org.ametys.web.activities.PageUpdatedActivityType; 064import org.ametys.web.repository.page.Page; 065import org.ametys.web.repository.site.Site; 066import org.ametys.web.repository.site.SiteManager; 067import org.ametys.web.rights.PageRightAssignmentContext; 068 069/** 070 * Helper for page notifications 071 */ 072public class PageNotificationsHelper extends AbstractLogEnabled implements Component, Serviceable, Initializable 073{ 074 /** The avalon role */ 075 public static final String ROLE = PageNotificationsHelper.class.getName(); 076 077 /** The ametys object resolver */ 078 protected AmetysObjectResolver _resolver; 079 080 /** The site manager */ 081 protected SiteManager _siteManager; 082 083 /** The subscription type extension point */ 084 protected SubscriptionTypeExtensionPoint _subscriptionTypeEP; 085 086 /** The current user provider */ 087 protected CurrentUserProvider _currentUserProvider; 088 089 /** The user helper */ 090 protected UserHelper _userHelper; 091 092 /** The cache manager */ 093 protected AbstractCacheManager _cacheManager; 094 095 /** The activity extension point */ 096 protected ActivityTypeExtensionPoint _activityTypeEP; 097 098 public void service(ServiceManager smanager) throws ServiceException 099 { 100 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 101 _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE); 102 _subscriptionTypeEP = (SubscriptionTypeExtensionPoint) smanager.lookup(SubscriptionTypeExtensionPoint.ROLE); 103 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 104 _userHelper = (UserHelper) smanager.lookup(UserHelper.ROLE); 105 _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE); 106 _activityTypeEP = (ActivityTypeExtensionPoint) smanager.lookup(ActivityTypeExtensionPoint.ROLE); 107 } 108 109 public void initialize() throws Exception 110 { 111 _cacheManager.createMemoryCache(ROLE, 112 new I18nizableText("plugin.page-subscription", "PLUGINS_PAGE_SUBSCRIBE_USER_SUBSCRIPTIONS_PAGE_CACHE"), 113 new I18nizableText("plugin.page-subscription", "PLUGINS_PAGE_SUBSCRIBE_USER_SUBSCRIPTIONS_PAGE_CACHE_DESCRIPTION"), 114 true, 115 null); 116 } 117 118 /** 119 * Get the unread page notifications for each subscription of the current user 120 * @param siteName the site name 121 * @return the unread page notifications 122 */ 123 @Callable(rights = Callable.SKIP_BUILTIN_CHECK) 124 public List<Map<String, Object>> getUnreadPages(String siteName) 125 { 126 List<Map<String, Object>> unreadPages = new ArrayList<>(); 127 128 Site site = _siteManager.getSite(siteName); 129 130 UserIdentity user = _currentUserProvider.getUser(); 131 PageSubscriptionType pageSubscriptionType = (PageSubscriptionType) _subscriptionTypeEP.getExtension(PageSubscriptionType.ID); 132 133 // Ignore subscription frequency for the notifications on intranet (BroadcastChannel.SITE) 134 List<Subscription> subscriptions = pageSubscriptionType.getUserSubscriptions(site, null, BroadcastChannel.SITE, user, false, null); 135 for (Subscription subscription : subscriptions) 136 { 137 String pageId = subscription.getValue(PageSubscriptionType.PAGE); 138 139 Activity lastPageActivity = getLastActivity(siteName, pageId, Frequency.INSTANT); 140 if (lastPageActivity != null) 141 { 142 ZonedDateTime lastPageActivityDate = lastPageActivity.getDate(); 143 ZonedDateTime lastReadDate = pageSubscriptionType.getLastReadDate(subscription, user); 144 boolean hasRead = lastReadDate != null && lastReadDate.isAfter(lastPageActivityDate); 145 if (!hasRead) 146 { 147 UserIdentity author = lastPageActivity.getAuthor(); 148 Map<String, Object> author2json = _userHelper.user2json(author); 149 author2json.put("imgUrl", ProfileImageResolverHelper.resolve(author.getLogin(), author.getPopulationId(), 64, null)); 150 151 unreadPages.add(Map.of( 152 "id", subscription.getId(), 153 "pageId", pageId, 154 "lastActivityDate", lastPageActivityDate, 155 "author", author2json 156 )); 157 } 158 } 159 } 160 161 return unreadPages; 162 } 163 164 /** 165 * Get the last page activity 166 * @param siteName the site name 167 * @param pageId the page 168 * @param frequency the frequency 169 * @return the last page activity 170 */ 171 public Activity getLastActivity(String siteName, String pageId, Frequency frequency) 172 { 173 FrequencyTiming timing = new FrequencyTiming(FrequencyHelper.getDefaultFrequencyDay(), FrequencyHelper.getDefaultFrequencyTime()); 174 ZonedDateTime notificationDate = FrequencyHelper.getNotificationDate(frequency, timing); 175 176 PageSubscriptionKey key = PageSubscriptionKey.of(siteName, pageId, frequency, notificationDate); 177 return Optional.ofNullable(_getCache().get(key, k -> _getLastActivity(siteName, pageId, frequency, notificationDate))) 178 .map(this::_resolveSilently) 179 .orElse(null); 180 } 181 182 private String _getLastActivity(String siteName, String pageId, Frequency frequency, ZonedDateTime notificationDate) 183 { 184 return _getPageActivities(siteName, pageId, frequency, notificationDate) 185 .stream() 186 .max((a1, a2) -> a1.getDate().compareTo(a2.getDate())) 187 .map(Activity::getId) 188 .orElse(null); 189 } 190 191 private AmetysObjectIterable<Activity> _getPageActivities(String siteName, String pageId, Frequency frequency, ZonedDateTime notificationDate) 192 { 193 List<Expression> finalExprs = new ArrayList<>(); 194 195 finalExprs.addAll(FrequencyHelper.getDateExpressions(frequency, notificationDate)); 196 197 Expression[] activityTypeExpressions = _activityTypeEP.getExtensionsIds() 198 .stream() 199 .map(_activityTypeEP::getExtension) 200 .filter(this::_filterActivity) 201 .map(type -> new StringExpression(ActivityFactory.ACTIVITY_TYPE_ID, Operator.EQ, type.getId())) 202 .toArray(Expression[]::new); 203 finalExprs.add(new OrExpression(activityTypeExpressions)); 204 finalExprs.add(new StringExpression(AbstractPageActivityType.PAGE_ID, Operator.EQ, pageId)); 205 finalExprs.add(new StringExpression(AbstractSiteAwareActivityType.SITE_NAME, Operator.EQ, siteName)); 206 207 Expression finalExpr = new AndExpression(finalExprs.toArray(new Expression[finalExprs.size()])); 208 209 String xpathQuery = ActivityHelper.getActivityXPathQuery(finalExpr); 210 return _resolver.query(xpathQuery); 211 } 212 213 private boolean _filterActivity(ActivityType type) 214 { 215 return type instanceof PageUpdatedActivityType || type instanceof PageResourcesUpdatedActivityType; 216 } 217 218 private Activity _resolveSilently(String activityId) 219 { 220 try 221 { 222 return _resolver.resolveById(activityId); 223 } 224 catch (Exception e) 225 { 226 getLogger().warn("Can't resolve activity with id '{}'", activityId, e); 227 } 228 229 return null; 230 } 231 232 /** 233 * Mark page as read for the current user 234 * @param pageId the page id 235 */ 236 @Callable(rights = Callable.READ_ACCESS, paramIndex = 0, rightContext = PageRightAssignmentContext.ID) 237 public void markPageAsRead(String pageId) 238 { 239 PageSubscriptionType pageSubscriptionType = (PageSubscriptionType) _subscriptionTypeEP.getExtension(PageSubscriptionType.ID); 240 Page page = _resolver.resolveById(pageId); 241 UserIdentity user = _currentUserProvider.getUser(); 242 243 PageSubscriptionContext context = PageSubscriptionContext.newInstance().withPage(page); 244 List<Subscription> subscriptions = pageSubscriptionType.getUserSubscriptions(page.getSite(), null, BroadcastChannel.SITE, user, false, context); 245 for (Subscription subscription : subscriptions) 246 { 247 pageSubscriptionType.markAsRead(subscription, user); 248 } 249 } 250 251 private Cache<PageSubscriptionKey, String> _getCache() 252 { 253 return _cacheManager.get(ROLE); 254 } 255 256 /** 257 * Clear page subscription cache 258 * @param siteName the site name 259 * @param pageId the page id 260 */ 261 public void clearCache(String siteName, String pageId) 262 { 263 _getCache().invalidate(PageSubscriptionKey.of(siteName, pageId)); 264 } 265 266 static class PageSubscriptionKey extends AbstractCacheKey 267 { 268 PageSubscriptionKey(String siteName, String pageId, Frequency frequency, ZonedDateTime notificationDate) 269 { 270 super(siteName, pageId, frequency, notificationDate); 271 } 272 273 static PageSubscriptionKey of(String siteName, String pageId, Frequency frequency, ZonedDateTime notificationDate) 274 { 275 return new PageSubscriptionKey(siteName, pageId, frequency, notificationDate); 276 } 277 278 static PageSubscriptionKey of(String siteName, String pageId) 279 { 280 return new PageSubscriptionKey(siteName, pageId, null, null); 281 } 282 } 283}