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