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}