001/*
002 *  Copyright 2010 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.cms.alerts;
017
018import java.io.IOException;
019import java.time.LocalDate;
020import java.time.ZonedDateTime;
021import java.util.ArrayList;
022import java.util.Calendar;
023import java.util.Collections;
024import java.util.Date;
025import java.util.GregorianCalendar;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import org.apache.avalon.framework.configuration.Configuration;
032import org.apache.avalon.framework.configuration.ConfigurationException;
033import org.apache.avalon.framework.context.Context;
034import org.apache.avalon.framework.context.ContextException;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.cocoon.Constants;
038import org.apache.cocoon.components.ContextHelper;
039import org.apache.cocoon.environment.Request;
040import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
041import org.apache.commons.lang.StringUtils;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045import org.ametys.cms.content.archive.ArchiveConstants;
046import org.ametys.cms.repository.Content;
047import org.ametys.cms.repository.ContentQueryHelper;
048import org.ametys.cms.repository.ModifiableContent;
049import org.ametys.cms.repository.WorkflowStepExpression;
050import org.ametys.cms.repository.WorkflowStepExpression.LogicalOperator;
051import org.ametys.core.engine.BackgroundEngineHelper;
052import org.ametys.core.right.RightManager;
053import org.ametys.core.user.User;
054import org.ametys.core.user.UserIdentity;
055import org.ametys.core.user.UserManager;
056import org.ametys.core.user.population.PopulationContextHelper;
057import org.ametys.core.util.DateUtils;
058import org.ametys.core.util.I18nUtils;
059import org.ametys.core.util.mail.SendMailHelper;
060import org.ametys.plugins.repository.AmetysObjectIterable;
061import org.ametys.plugins.repository.AmetysObjectResolver;
062import org.ametys.plugins.repository.AmetysRepositoryException;
063import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
064import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
065import org.ametys.plugins.repository.query.expression.AndExpression;
066import org.ametys.plugins.repository.query.expression.BooleanExpression;
067import org.ametys.plugins.repository.query.expression.DateExpression;
068import org.ametys.plugins.repository.query.expression.Expression;
069import org.ametys.plugins.repository.query.expression.ExpressionContext;
070import org.ametys.plugins.repository.query.expression.Expression.Operator;
071import org.ametys.plugins.repository.query.expression.MetadataExpression;
072import org.ametys.plugins.repository.query.expression.NotExpression;
073import org.ametys.plugins.repository.query.expression.OrExpression;
074import org.ametys.plugins.repository.version.DataAndVersionAwareAmetysObject;
075import org.ametys.plugins.repository.version.ModifiableDataAwareVersionableAmetysObject;
076import org.ametys.runtime.config.Config;
077import org.ametys.runtime.i18n.I18nizableText;
078
079import jakarta.mail.MessagingException;
080
081/**
082 * Alerts engine: sends alerts mail.
083 */
084public class AlertEngine implements Runnable
085{
086    
087    /** The logger. */
088    protected static final Logger _LOGGER = LoggerFactory.getLogger(AlertEngine.class);
089    
090    /** The context used for validation alerts expressions */
091    protected static final ExpressionContext __VALIDATION_ALERT_EXPR_CONTEXT = ExpressionContext.newInstance().withUnversioned(true);
092    
093    /** The avalon context. */
094    protected Context _context;
095    
096    /** The service manager. */
097    protected ServiceManager _manager;
098    
099    /** The server base URL. */
100    protected String _baseUrl;
101    
102    /** Is the engine initialized ? */
103    protected boolean _initialized;
104    
105    /** The cocoon environment context. */
106    protected org.apache.cocoon.environment.Context _environmentContext;
107    
108    /** The ametys object resolver. */
109    protected AmetysObjectResolver _ametysResolver;
110    
111    /** The rights manager. */
112    protected RightManager _rightManager;
113    
114    /** The users manager. */
115    protected UserManager _userManager;
116    
117    /** The i18n utils. */
118    protected I18nUtils _i18nUtils;
119    
120    /** The content of "from" field in emails. */
121    protected String _mailFrom;
122    
123    /** The "waiting for validation" e-mail will be sent to users that have at least one of this rights. */
124    protected Set<String> _awaitingValidationRights;
125    
126    /** The "waiting for validation" e-mail subject i18n key. */
127    protected String _awaitingValidationSubject;
128    
129    /** The "waiting for validation" e-mail body i18n key. */
130    protected String _awaitingValidationBody;
131    
132    /** Only contents in this steps will be taken into account for the "unmodified content" alert. */
133    protected int[] _unmodifiedContentStepIds;
134    
135    /** The "unmodified content" e-mail will be sent to users that have at least one of this rights. */
136    protected Set<String> _unmodifiedContentRights;
137    
138    /** The "unmodified content" e-mail subject i18n key. */
139    protected String _unmodifiedContentSubject;
140    
141    /** The "unmodified content" e-mail body i18n key. */
142    protected String _unmodifiedContentBody;
143    
144    /** The reminder e-mail will be sent to users that have this at least one of this rights. */
145    protected Set<String> _reminderRights;
146    
147    /** The reminder e-mail subject i18n key. */
148    protected String _reminderSubject;
149    
150    /** The reminder e-mail body i18n key. */
151    protected String _reminderBody;
152    
153    /** The scheduled archiving reminder e-mail will be sent to users that have this at least one of this rights. */
154    protected Set<String> _scheduledArchivingReminderRights;
155    
156    /** The scheduled archiving reminder e-mail subject i18n key. */
157    protected String _scheduledArchivingReminderSubject;
158    
159    /** The scheduled archiving reminder e-mail body i18n key. */
160    protected String _scheduledArchivingReminderBody;
161   
162    /** The instant alert e-mail will be sent to users that have this at least one of this rights. */
163    protected Set<String> _instantAlertRights;
164    /** The instant alert e-mail subject i18n key. */
165    protected String _instantAlertSubject;
166    /** The instant alert e-mail body i18n key. */
167    protected String _instantAlertBody;
168    
169    /** True if the engine was been run in instant mode (for instant alerts only) **/
170    protected boolean _instantMode;
171    /** The list of contents' id (for instant alerts only) **/
172    private List<String> _instantAlertContentIds;
173    /** The email message (for instant alerts only) **/
174    private String _instantAlertMessage;
175    
176    /**
177     * Default constructor
178     */
179    public AlertEngine ()
180    {
181        _instantMode = false;
182    }
183    
184    /**
185     * Constructor used to send instant alerts
186     * @param contentIds The content's id
187     * @param message the message
188     */
189    public AlertEngine (List<String> contentIds, String message)
190    {
191        _instantMode = true;
192        _instantAlertContentIds = contentIds;
193        _instantAlertMessage = message;
194    }
195    
196    /**
197     * Initialize the alert engine.
198     * @param manager the avalon service manager.
199     * @param context the avalon context.
200     * @throws ContextException if the CONTEXT_ENVIRONMENT_CONTEXT cannot be found
201     * @throws ServiceException if some components cannot be resolved
202     */
203    public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException
204    {
205        _manager = manager;
206        _context = context;
207        _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
208        
209        // Lookup the needed components.
210        _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
211        
212        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
213        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
214        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
215        
216        _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/");
217        _mailFrom = Config.getInstance().getValue("smtp.mail.from");
218        
219        _initialized = true;
220    }
221    
222    /**
223     * Configure the engine (called by the scheduler).
224     * @param configuration the component configuration.
225     * @throws ConfigurationException if the configuration is not valid
226     */
227    public void configure(Configuration configuration) throws ConfigurationException
228    {
229        Configuration instantConf = configuration.getChild("instantAlert");
230        Configuration validationConf = configuration.getChild("awaitingValidation");
231        Configuration unmodifiedConf = configuration.getChild("unmodifiedContent");
232        Configuration reminderConf = configuration.getChild("reminder");
233        Configuration scheduledArchivingReminderConf = configuration.getChild("scheduledArchiving");
234        
235        // Configure the rights.
236        _instantAlertRights = _getRightsFromConf(instantConf);
237        _awaitingValidationRights = _getRightsFromConf(validationConf);
238        _unmodifiedContentRights = _getRightsFromConf(unmodifiedConf);
239        _reminderRights = _getRightsFromConf(reminderConf);
240        _scheduledArchivingReminderRights = _getRightsFromConf(scheduledArchivingReminderConf);
241        Configuration[] stepIds = unmodifiedConf.getChildren("stepId");
242        _unmodifiedContentStepIds = new int[stepIds.length];
243        int i = 0;
244        for (Configuration stepId : stepIds)
245        {
246            try
247            {
248                _unmodifiedContentStepIds[i] = Integer.parseInt(stepId.getValue(""));
249                i++;
250            }
251            catch (NumberFormatException e)
252            {
253                // Ignore
254            }
255        }
256        
257        // Configure the i18n texts.
258        _awaitingValidationSubject = validationConf.getChild("subjectKey").getValue();
259        _awaitingValidationBody = validationConf.getChild("bodyKey").getValue();
260        _unmodifiedContentSubject = unmodifiedConf.getChild("subjectKey").getValue();
261        _unmodifiedContentBody = unmodifiedConf.getChild("bodyKey").getValue();
262        _reminderSubject = reminderConf.getChild("subjectKey").getValue();
263        _reminderBody = reminderConf.getChild("bodyKey").getValue();
264        _scheduledArchivingReminderSubject = scheduledArchivingReminderConf.getChild("subjectKey").getValue();
265        _scheduledArchivingReminderBody = scheduledArchivingReminderConf.getChild("bodyKey").getValue();
266        _instantAlertSubject = instantConf.getChild("subjectKey").getValue();
267        _instantAlertBody = instantConf.getChild("bodyKey").getValue();
268    }
269    
270    /**
271     * Check the initialization and throw an exception if not initialized.
272     */
273    protected void _checkInitialization()
274    {
275        if (!_initialized)
276        {
277            String message = "Le composant de synchronisation doit être initialisé avant d'être lancé.";
278            _LOGGER.error(message);
279            throw new IllegalStateException(message);
280        }
281    }
282    
283    @Override
284    public void run()
285    {
286        Map<String, Object> environmentInformation = null;
287        try
288        {
289            _LOGGER.info("Preparing to send the alerts...");
290            
291            _checkInitialization();
292            
293            // Create the environment.
294            environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_manager, _environmentContext, new SLF4JLoggerAdapter(_LOGGER));
295            
296            // Prepare and send all the alerts.
297            _sendAlerts();
298        }
299        catch (Exception e)
300        {
301            _LOGGER.error("An error occurred sending the alerts.", e);
302        }
303        finally
304        {
305            // Leave the environment.
306            if (environmentInformation != null)
307            {
308                BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation);
309            }
310            // Dispose of the resources.
311            _dispose();
312            _LOGGER.info("Alerts sent.");
313        }
314    }
315    
316    /**
317     * Set the necessary request attributes
318     * @param content The content
319     */
320    protected void _setRequestAttributes (Content content)
321    {
322        Request request = ContextHelper.getRequest(_context);
323        
324        List<String> populationContexts = new ArrayList<>();
325        populationContexts.add("/application");
326        
327        request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts);
328    }
329    
330    /**
331     * Dispose of the resources and looked-up components.
332     */
333    protected void _dispose()
334    {
335        // Release the components.
336        if (_manager != null)
337        {
338            _manager.release(_ametysResolver);
339            _manager.release(_rightManager);
340            _manager.release(_userManager);
341        }
342        
343        _ametysResolver = null;
344        _rightManager = null;
345        _userManager = null;
346        
347        _environmentContext = null;
348        _context = null;
349        _manager = null;
350        
351        _initialized = false;
352    }
353    
354    /**
355     * Send all the alerts. Can be overridden to add alerts.
356     * @throws AmetysRepositoryException if an error occurs.
357     */
358    protected void _sendAlerts() throws AmetysRepositoryException
359    {
360        if (_instantMode)
361        {
362            _sendInstantAlerts();
363        }
364        else
365        {
366            _sendAwaitingValidationAlerts();
367            _sendUnmodifiedAlerts();
368            _sendReminders();
369            _sendScheduledArchivingReminders();
370        }
371    }
372    
373    /**
374     * Send instant alerts on contents
375     * @throws AmetysRepositoryException if an error occurred
376     */
377    protected void _sendInstantAlerts () throws AmetysRepositoryException
378    {
379        if (_instantAlertContentIds != null && !_instantAlertContentIds.isEmpty())
380        {
381            for (String contentId : _instantAlertContentIds)
382            {
383                Content content = _ametysResolver.resolveById(contentId);
384                _sendInstantAlertEmail (content, _instantAlertMessage);
385            }
386        }
387    }
388    
389    /**
390     * Send a instant e-mail alert to all the users who have the right to edit the content.
391     * @param content the content about which to send the alert.
392     * @param message the message
393     * @throws AmetysRepositoryException if an error occurred
394     */
395    protected void _sendInstantAlertEmail(Content content, String message) throws AmetysRepositoryException
396    {
397        _setRequestAttributes(content);
398        
399        Set<UserIdentity> users = new HashSet<>();
400        for (String right : _instantAlertRights)
401        {
402            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
403        }
404        
405        List<String> params = _getInstantAlertParams(content, message);
406        
407        I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_instantAlertSubject, content), params);
408        I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_instantAlertBody, content), params);
409        
410        String subject = _i18nUtils.translate(i18nSubject);
411        String body = _i18nUtils.translate(i18nBody);
412        
413        _sendMails(subject, body, users, _mailFrom);
414    }
415    
416    /**
417     * Send the "awaiting validation" alerts.
418     * @throws AmetysRepositoryException if an error occurs.
419     */
420    protected void _sendAwaitingValidationAlerts() throws AmetysRepositoryException
421    {
422        Long delay = Config.getInstance().getValue("remind.content.validation.delay");
423        if (delay != null && delay > 0)
424        {
425            Calendar calendar = new GregorianCalendar();
426            _removeTimeParts(calendar);
427            calendar.add(Calendar.DAY_OF_MONTH, 1 - delay.intValue());
428            
429            // No last date and X days after the proposal date.
430            Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, __VALIDATION_ALERT_EXPR_CONTEXT));
431            Expression waitingExpression = new DateExpression("proposalDate", Operator.LT, calendar.getTime(), __VALIDATION_ALERT_EXPR_CONTEXT);
432            // Or proposal date before the last "awaiting validation" date and the and X days after the last "awaiting validation" date.
433            Expression proposalBeforeLastDateExpr = new BinaryExpression("proposalDate", __VALIDATION_ALERT_EXPR_CONTEXT, Operator.LT, AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, __VALIDATION_ALERT_EXPR_CONTEXT);
434            Expression lastDateExpr = new DateExpression(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, Operator.LT, calendar.getTime(), __VALIDATION_ALERT_EXPR_CONTEXT);
435            Expression expression = new OrExpression(new AndExpression(noLastDateExpr, waitingExpression),
436                                                     new AndExpression(proposalBeforeLastDateExpr, lastDateExpr));
437            
438            String query = ContentQueryHelper.getContentXPathQuery(expression);
439            
440            try (AmetysObjectIterable<ModifiableContent> contents = _ametysResolver.query(query))
441            {
442                if (_LOGGER.isInfoEnabled())
443                {
444                    _LOGGER.info("Contents waiting for validation: " + contents.getSize());
445                }
446                
447                for (ModifiableContent content : contents)
448                {
449                    // Send the alert e-mails.
450                    _sendAwaitingValidationEmail(content);
451                    
452                    // Set the last validation alert date to now.
453                    ModifiableModelLessDataHolder dataHolder = ((ModifiableDataAwareVersionableAmetysObject) content).getUnversionedDataHolder();
454                    dataHolder.setValue(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, ZonedDateTime.now());
455                    
456                    content.saveChanges();
457                }
458            }
459        }
460    }
461    
462    /**
463     * Send the unmodified content alerts.
464     * @throws AmetysRepositoryException if an error occurs.
465     */
466    protected void _sendUnmodifiedAlerts() throws AmetysRepositoryException
467    {
468        Long delay = Config.getInstance().getValue("remind.unmodified.content.delay");
469        if (delay != null && delay > 0)
470        {
471            Calendar calendar = new GregorianCalendar();
472            _removeTimeParts(calendar);
473            calendar.add(Calendar.DAY_OF_MONTH, 1 - delay.intValue());
474            
475            ExpressionContext exprContext = ExpressionContext.newInstance().withUnversioned(true);
476            
477            // If no step is configured, stepExpr will return the empty string.
478            Expression stepExpr = new WorkflowStepExpression(Operator.EQ, _unmodifiedContentStepIds, LogicalOperator.OR);
479            // Get only the contents on which the alert is enabled.
480            Expression unmodifiedExpr = new BooleanExpression(AlertsConstants.UNMODIFIED_ALERT_ENABLED, true, exprContext);
481            // No last date and X days after the proposal date, or X days after the last date.
482            Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, exprContext));
483            Expression lastModifiedExpression = new DateExpression("lastModified", Operator.LT, calendar.getTime());
484            Expression lastDateExpr = new DateExpression(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, Operator.LT, calendar.getTime(), exprContext);
485            Expression dateExpr = new OrExpression(new AndExpression(noLastDateExpr, lastModifiedExpression), lastDateExpr);
486            // Full AND expression.
487            Expression expression = new AndExpression(unmodifiedExpr, dateExpr, stepExpr);
488            
489            String query = ContentQueryHelper.getContentXPathQuery(expression);
490            
491            try (AmetysObjectIterable<ModifiableContent> contents = _ametysResolver.query(query))
492            {
493                if (_LOGGER.isInfoEnabled())
494                {
495                    _LOGGER.info("Contents not modified for " + delay + " days: " + contents.getSize());
496                }
497                
498                for (ModifiableContent content : contents)
499                {
500                    // Send the alert e-mail.
501                    _sendUnmodifiedContentEmail(content);
502                    
503                    // Set the last unmodified alert date to now.
504                    ModifiableModelLessDataHolder dataHolder = ((ModifiableDataAwareVersionableAmetysObject) content).getUnversionedDataHolder();
505                    dataHolder.setValue(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, ZonedDateTime.now());
506                    
507                    content.saveChanges();
508                }
509            }
510        }
511    }
512    
513    /**
514     * Send the content reminders.
515     * @throws AmetysRepositoryException if an error occurs.
516     */
517    protected void _sendReminders() throws AmetysRepositoryException
518    {
519        Date now = DateUtils.asDate(LocalDate.now());
520        
521        ExpressionContext exprContext = ExpressionContext.newInstance().withUnversioned(true);
522        Expression reminderExpr = new BooleanExpression(AlertsConstants.REMINDER_ENABLED, Operator.EQ, true, exprContext);
523        Expression dateExpr = new DateExpression(AlertsConstants.REMINDER_DATE, Operator.EQ, now, exprContext);
524        Expression expression = new AndExpression(reminderExpr, dateExpr);
525        
526        String query = ContentQueryHelper.getContentXPathQuery(expression);
527        
528        try (AmetysObjectIterable<Content> contents = _ametysResolver.query(query))
529        {
530            if (_LOGGER.isInfoEnabled())
531            {
532                _LOGGER.info("Contents with reminder today: " + contents.getSize());
533            }
534            
535            for (Content content : contents)
536            {
537                _sendReminderEmail(content);
538            }
539        }
540    }
541    
542    /**
543     * Send the scheduled archiving reminders on contents.
544     * @throws AmetysRepositoryException if an error occurs.
545     */
546    protected void _sendScheduledArchivingReminders() throws AmetysRepositoryException
547    {
548        Long delay = Config.getInstance().getValue("archive.scheduler.reminder.delay");
549        if (delay != null && delay > 0)
550        {
551            Calendar calendar = new GregorianCalendar();
552            _removeTimeParts(calendar);
553            calendar.add(Calendar.DAY_OF_MONTH, delay.intValue());
554            
555            ExpressionContext exprContext = ExpressionContext.newInstance().withUnversioned(true);
556            
557            // No last date and X days before the scheduled date.
558            Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.SCHEDULED_ARCHIVING_REMINDER_LAST_DATE, exprContext));
559            Expression scheduledDelayExpression = new DateExpression(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE, Operator.LE, calendar.getTime(), exprContext);
560            Expression expression = new AndExpression(noLastDateExpr, scheduledDelayExpression);
561            
562            String query = ContentQueryHelper.getContentXPathQuery(expression);
563            
564            try (AmetysObjectIterable<ModifiableContent> contents = _ametysResolver.query(query))
565            {
566                if (_LOGGER.isInfoEnabled())
567                {
568                    _LOGGER.info("Contents with scheduled archiving reminder today: " + contents.getSize());
569                }
570                
571                for (ModifiableContent content : contents)
572                {
573                    _sendScheduledArchivingReminderEmail(content);
574                    
575                    // Set the last scheduled archiving reminder date to now.
576                    ModifiableModelLessDataHolder dataHolder = ((ModifiableDataAwareVersionableAmetysObject) content).getUnversionedDataHolder();
577                    dataHolder.setValue(AlertsConstants.SCHEDULED_ARCHIVING_REMINDER_LAST_DATE, ZonedDateTime.now());
578                    
579                    content.saveChanges();
580                }
581            }
582        }
583    }
584    
585    /**
586     * Send a "waiting for validation" e-mail alert to all the users who have the right to validate.
587     * @param content the content about which to send the alert.
588     * @throws AmetysRepositoryException if an error occured on the repository
589     */
590    protected void _sendAwaitingValidationEmail(Content content) throws AmetysRepositoryException
591    {
592        _setRequestAttributes(content);
593        
594        Set<UserIdentity> users = new HashSet<>();
595        for (String right : _awaitingValidationRights)
596        {
597            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
598        }
599        
600        List<String> params = _getAwaitingValidationParams(content);
601        
602        I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_awaitingValidationSubject, content), params);
603        I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_awaitingValidationBody, content), params);
604        
605        String subject = _i18nUtils.translate(i18nSubject);
606        String body = _i18nUtils.translate(i18nBody);
607        
608        _sendMails(subject, body, users, _mailFrom);
609    }
610    
611    /**
612     * Send a "unmodified content" e-mail alert to all the users who have the right to edit.
613     * @param content the content about which to send the alert.
614     * @throws AmetysRepositoryException if an error occured on the repository
615     */
616    protected void _sendUnmodifiedContentEmail(Content content) throws AmetysRepositoryException
617    {
618        _setRequestAttributes(content);
619        
620        Set<UserIdentity> users = new HashSet<>();
621        for (String right : _unmodifiedContentRights)
622        {
623            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
624        }
625        
626        List<String> params = _getUnmodifiedContentParams(content);
627        
628        I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_unmodifiedContentSubject, content), params);
629        I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_unmodifiedContentBody, content), params);
630        
631        String subject = _i18nUtils.translate(i18nSubject);
632        String body = _i18nUtils.translate(i18nBody);
633        
634        _sendMails(subject, body, users, _mailFrom);
635    }
636    
637    /**
638     * Send a reminder e-mail to all the users who have the right to edit.
639     * @param content the content about which to send the reminder.
640     * @throws AmetysRepositoryException if an error occured on the repository
641     */
642    protected void _sendReminderEmail(Content content) throws AmetysRepositoryException
643    {
644        _setRequestAttributes(content);
645        
646        Set<UserIdentity> users = new HashSet<>();
647        for (String right : _reminderRights)
648        {
649            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
650        }
651        
652        List<String> params = _getReminderParams(content);
653        
654        I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_reminderSubject, content), params);
655        I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_reminderBody, content), params);
656        
657        String subject = _i18nUtils.translate(i18nSubject);
658        String body = _i18nUtils.translate(i18nBody);
659        
660        _sendMails(subject, body, users, _mailFrom);
661    }
662    
663    /**
664     * Send a "scheduled archiving reminder" e-mail to all the users who have the right to archive.
665     * @param content the content about which to send the alert.
666     * @throws AmetysRepositoryException if an error occured on the repository
667     */
668    protected void _sendScheduledArchivingReminderEmail(ModifiableContent content) throws AmetysRepositoryException
669    {
670        _setRequestAttributes(content);
671        
672        Set<UserIdentity> users = new HashSet<>();
673        for (String right : _scheduledArchivingReminderRights)
674        {
675            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending")));
676        }
677        
678        List<String> params = _getScheduledArchivingReminderParams(content);
679        
680        I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_scheduledArchivingReminderSubject, content), params);
681        I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_scheduledArchivingReminderBody, content), params);
682        
683        String subject = _i18nUtils.translate(i18nSubject);
684        String body = _i18nUtils.translate(i18nBody);
685        
686        _sendMails(subject, body, users, _mailFrom);
687    }
688    
689    /**
690     * Get the transform i18n body key for specific content
691     * @param bodyI18nKey the original body key
692     * @param content the content
693     * @return the transform i18n body key
694     */
695    protected String getI18nKeyBody(String bodyI18nKey, Content content)
696    {
697        return bodyI18nKey;
698    }
699    
700    /**
701     * Get the mail parameters for instant alert.
702     * @param content the content.
703     * @param message the message
704     * @return the parameters.
705     */
706    protected List<String> _getInstantAlertParams(Content content, String message)
707    {
708        List<String> params = new ArrayList<>();
709        
710        params.add(content.getTitle(null)); // {0}
711        params.add(_getContentUrl(content)); // {1}
712        params.add(message); // {2}
713        
714        params.addAll(_getAdditionalParams(content));
715        
716        return params;
717    }
718    
719    /**
720     * Get the additional i18n parameters for content
721     * @param content The content
722     * @return The additional i18n parameters
723     */
724    protected List<String> _getAdditionalParams (Content content)
725    {
726        return Collections.EMPTY_LIST;
727    }
728    
729    /**
730     * Get the mail parameters.
731     * @param content the content.
732     * @return the parameters.
733     */
734    protected List<String> _getAwaitingValidationParams(Content content)
735    {
736        List<String> params = new ArrayList<>();
737        
738        Long delay = Config.getInstance().getValue("remind.content.validation.delay");
739        
740        params.add(content.getTitle(null)); // {0}
741        params.add(_getContentUrl(content)); // {1}
742        params.add(String.valueOf(delay)); // {2}
743        
744        params.addAll(_getAdditionalParams(content));
745        
746        return params;
747    }
748    
749    /**
750     * Get the mail parameters.
751     * @param content the content.
752     * @return the parameters.
753     */
754    protected List<String> _getUnmodifiedContentParams(Content content)
755    {
756        List<String> params = new ArrayList<>();
757        
758        Long delay = Config.getInstance().getValue("remind.unmodified.content.delay");
759        
760        params.add(content.getTitle(null)); // {0}
761        params.add(_getContentUrl(content)); // {1}
762        params.add(String.valueOf(delay));  // {2}
763        
764        ModelLessDataHolder dataHolder = ((DataAndVersionAwareAmetysObject) content).getUnversionedDataHolder();
765        String alertText = dataHolder.getValue(AlertsConstants.UNMODIFIED_ALERT_TEXT, StringUtils.EMPTY);
766        params.add(alertText);  // {3}
767        
768        params.addAll(_getAdditionalParams(content));
769        
770        return params;
771    }
772    
773    /**
774     * Get the mail parameters.
775     * @param content the content.
776     * @return the parameters.
777     */
778    protected List<String> _getReminderParams(Content content)
779    {
780        List<String> params = new ArrayList<>();
781        
782        // Should never trigger a ClassCastException, as we query on an unversioned metadata.
783        ModelLessDataHolder dataHolder = ((DataAndVersionAwareAmetysObject) content).getUnversionedDataHolder();
784        String reminderText = dataHolder.getValue(AlertsConstants.REMINDER_TEXT, StringUtils.EMPTY);
785        
786        params.add(content.getTitle(null)); // {0}
787        params.add(_getContentUrl(content)); // {1}
788        params.add(reminderText);  // {2}
789        
790        params.addAll(_getAdditionalParams(content));
791        
792        return params;
793    }
794    
795    /**
796     * Get the mail parameters.
797     * @param content the content.
798     * @return the parameters.
799     */
800    protected List<String> _getScheduledArchivingReminderParams(ModifiableContent content)
801    {
802        List<String> params = new ArrayList<>();
803        
804        Long delay = Config.getInstance().getValue("archive.scheduler.reminder.delay");
805         
806        params.add(content.getTitle(null)); // {0}
807        params.add(_getContentUrl(content)); // {1}
808        params.add(String.valueOf(delay)); // {2}
809        
810        params.addAll(_getAdditionalParams(content));
811        
812        return params;
813    }
814    
815    /**
816     * Send the alert emails.
817     * @param subject the e-mail subject.
818     * @param body the e-mail body.
819     * @param users users to send the mail to.
820     * @param from the address sending the e-mail.
821     */
822    protected void _sendMails(String subject, String body, Set<UserIdentity> users, String from)
823    {
824        for (UserIdentity identity : users)
825        {
826            User user = _userManager.getUser(identity.getPopulationId(), identity.getLogin());
827            
828            if (user != null && StringUtils.isNotBlank(user.getEmail()))
829            {
830                String mail = user.getEmail();
831                
832                try
833                {
834                    SendMailHelper.newMail()
835                                  .withSubject(subject)
836                                  .withTextBody(body)
837                                  .withSender(from)
838                                  .withRecipient(mail)
839                                  .sendMail();
840                }
841                catch (MessagingException | IOException e)
842                {
843                    if (_LOGGER.isWarnEnabled())
844                    {
845                        _LOGGER.warn("Could not send an alert e-mail to " + mail, e);
846                    }
847                }
848            }
849        }
850    }
851    
852    /**
853     * Get the URL to the given content tool.
854     * @param content the content.
855     * @return the content URL.
856     */
857    protected String _getContentUrl(Content content)
858    {
859        StringBuilder url = new StringBuilder(_baseUrl);
860        url.append("/index.html?uitool=uitool-content,id:%27").append(content.getId()).append("%27");
861        
862        return url.toString();
863    }
864    
865    /**
866     * Remove the time parts from a calendar, leaving only date parts.
867     * @param calendar the calendar.
868     */
869    protected void _removeTimeParts(Calendar calendar)
870    {
871        calendar.set(Calendar.HOUR_OF_DAY, 0);
872        calendar.set(Calendar.MINUTE, 0);
873        calendar.set(Calendar.SECOND, 0);
874        calendar.set(Calendar.MILLISECOND, 0);
875    }
876    
877    /**
878     * Get a set of rights from a configuration.
879     * @param configuration the configuration.
880     * @return the set of rights.
881     * @throws ConfigurationException if the configuration is not valid.
882     */
883    protected Set<String> _getRightsFromConf(Configuration configuration) throws ConfigurationException
884    {
885        Set<String> rights = new HashSet<>();
886        
887        for (Configuration rightConf : configuration.getChildren("right"))
888        {
889            String right = rightConf.getValue("");
890            if (StringUtils.isNotBlank(right))
891            {
892                rights.add(right);
893            }
894        }
895        
896        return rights;
897    }
898    
899    /**
900     * Binary date expression: test on two metadatas.
901     */
902    protected class BinaryExpression implements Expression
903    {
904        private MetadataExpression _metadata1;
905        private MetadataExpression _metadata2;
906        private Operator _operator;
907        
908        /**
909         * Creates the comparison Expression.
910         * @param metadata1 the first metadata name.
911         * @param operator the operator to make the comparison
912         * @param metadata2 the second metadata name.
913         */
914        public BinaryExpression(String metadata1, Operator operator, String metadata2)
915        {
916            _metadata1 = new MetadataExpression(metadata1);
917            _operator = operator;
918            _metadata2 = new MetadataExpression(metadata2);
919        }
920        
921        /**
922         * Creates the comparison Expression.
923         * @param metadata1 the first metadata name.
924         * @param context1 context of the expression for the first metadata
925         * @param operator the operator to make the comparison.
926         * @param metadata2 the second metadata name.
927         * @param context2 context of the expression for the second metadata 
928         */
929        public BinaryExpression(String metadata1, ExpressionContext context1, Operator operator, String metadata2, ExpressionContext context2)
930        {
931            _metadata1 = new MetadataExpression(metadata1, context1);
932            _operator = operator;
933            _metadata2 = new MetadataExpression(metadata2, context2);
934        }
935        
936        @Override
937        public String build()
938        {
939            return _metadata1.build() + " " + _operator + " " + _metadata2.build();
940        }
941    }
942    
943}