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