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}