### Eclipse Workspace Patch 1.0 #P Ametys - 07 CMS Index: main/plugin-cms/i18n/messages_en.xml =================================================================== --- main/plugin-cms/i18n/messages_en.xml (revision 42093) +++ main/plugin-cms/i18n/messages_en.xml (working copy) @@ -2184,4 +2184,15 @@ The name of one or several columns does not correspond to the chosen content type One or several facets do not correspont to the chosen content type + + + Sending instant alert on content {0} + Sending instant alert on content "{0}" ({1}) + Sending differed alert on content {0} + Sending differed alert on content "{0}" ({1}) + Send alerts on contents + This task goes through the contents and determines if alerts must be sent + Daily sending potential alerts on contents + Daily sending potential alerts on contents + Index: main/plugin-cms/i18n/messages_fr.xml =================================================================== --- main/plugin-cms/i18n/messages_fr.xml (revision 42093) +++ main/plugin-cms/i18n/messages_fr.xml (working copy) @@ -2180,4 +2180,14 @@ Le nom d'une ou plusieurs colonnes ne correspond pas au type de contenu choisi. Une ou plusieurs facettes ne correspondent pas au type de contenu choisi. + + Envoi d'une alerte instantanée sur le contenu {0} + Envoi d'une alerte instantanée sur le contenu "{0}" ({1}) + Envoi d'une alerte différée sur le contenu {0} + Envoi d'une alerte différée sur le contenu "{0}" ({1}) + Envoyer des alertes sur les contenus + Cette tâche parcourt les contenus et détermine si des alertes doivent être envoyées + Envoi quotidien d'éventuelles alertes sur les contenus + Envoi quotidien d'éventuelles alertes sur les contenus + Index: main/plugin-cms/resources/js/Ametys/plugins/cms/content/actions/AlertsActions.js =================================================================== --- main/plugin-cms/resources/js/Ametys/plugins/cms/content/actions/AlertsActions.js (revision 42093) +++ main/plugin-cms/resources/js/Ametys/plugins/cms/content/actions/AlertsActions.js (working copy) @@ -358,7 +358,8 @@ 'tristatechange': Ext.bind (this._onCheckReminder, this) } },{ - xtype: 'edition.date', + xtype: 'edition.datetime', + submitFormat: 'Y-m-d H:i', name: 'reminderDate', hideLabel: true, disabled: true, Index: main/plugin-cms/src/org/ametys/cms/alerts/AlertEngine.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/alerts/AlertEngine.java (revision 42093) +++ main/plugin-cms/src/org/ametys/cms/alerts/AlertEngine.java (working copy) @@ -1,901 +0,0 @@ -/* - * Copyright 2010 Anyware Services - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ametys.cms.alerts; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.mail.MessagingException; - -import org.apache.avalon.framework.configuration.Configuration; -import org.apache.avalon.framework.configuration.ConfigurationException; -import org.apache.avalon.framework.context.Context; -import org.apache.avalon.framework.context.ContextException; -import org.apache.avalon.framework.service.ServiceException; -import org.apache.avalon.framework.service.ServiceManager; -import org.apache.cocoon.Constants; -import org.apache.cocoon.util.log.SLF4JLoggerAdapter; -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.ametys.cms.content.archive.ArchiveConstants; -import org.ametys.cms.repository.Content; -import org.ametys.cms.repository.ContentQueryHelper; -import org.ametys.cms.repository.ModifiableContent; -import org.ametys.cms.repository.WorkflowStepExpression; -import org.ametys.cms.repository.WorkflowStepExpression.LogicalOperator; -import org.ametys.core.engine.BackgroundEngineHelper; -import org.ametys.core.right.RightsManager; -import org.ametys.core.user.User; -import org.ametys.core.user.UserIdentity; -import org.ametys.core.user.UserManager; -import org.ametys.core.util.I18nUtils; -import org.ametys.core.util.mail.SendMailHelper; -import org.ametys.plugins.repository.AmetysObjectIterable; -import org.ametys.plugins.repository.AmetysObjectResolver; -import org.ametys.plugins.repository.AmetysRepositoryException; -import org.ametys.plugins.repository.metadata.CompositeMetadata; -import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; -import org.ametys.plugins.repository.query.expression.AndExpression; -import org.ametys.plugins.repository.query.expression.BooleanExpression; -import org.ametys.plugins.repository.query.expression.DateExpression; -import org.ametys.plugins.repository.query.expression.Expression; -import org.ametys.plugins.repository.query.expression.Expression.Operator; -import org.ametys.plugins.repository.query.expression.MetadataExpression; -import org.ametys.plugins.repository.query.expression.NotExpression; -import org.ametys.plugins.repository.query.expression.OrExpression; -import org.ametys.plugins.repository.version.MetadataAndVersionAwareAmetysObject; -import org.ametys.plugins.repository.version.ModifiableMetadataAwareVersionableAmetysObject; -import org.ametys.runtime.config.Config; -import org.ametys.runtime.i18n.I18nizableText; - -/** - * Alerts engine: sends alerts mail. - */ -public class AlertEngine implements Runnable -{ - - /** The logger. */ - protected static final Logger _LOGGER = LoggerFactory.getLogger(AlertEngine.class); - - /** The avalon context. */ - protected Context _context; - - /** The service manager. */ - protected ServiceManager _manager; - - /** The server base URL. */ - protected String _baseUrl; - - /** Is the engine initialized ? */ - protected boolean _initialized; - - /** The cocoon environment context. */ - protected org.apache.cocoon.environment.Context _environmentContext; - - /** The ametys object resolver. */ - protected AmetysObjectResolver _ametysResolver; - - /** The rights manager. */ - protected RightsManager _rightsManager; - - /** The users manager. */ - protected UserManager _userManager; - - /** The i18n utils. */ - protected I18nUtils _i18nUtils; - - /** The content of "from" field in emails. */ - protected String _mailFrom; - - /** The "waiting for validation" e-mail will be sent to users that have at least one of this rights. */ - protected Set _awaitingValidationRights; - - /** The "waiting for validation" e-mail subject i18n key. */ - protected String _awaitingValidationSubject; - - /** The "waiting for validation" e-mail body i18n key. */ - protected String _awaitingValidationBody; - - /** Only contents in this steps will be taken into account for the "unmodified content" alert. */ - protected int[] _unmodifiedContentStepIds; - - /** The "unmodified content" e-mail will be sent to users that have at least one of this rights. */ - protected Set _unmodifiedContentRights; - - /** The "unmodified content" e-mail subject i18n key. */ - protected String _unmodifiedContentSubject; - - /** The "unmodified content" e-mail body i18n key. */ - protected String _unmodifiedContentBody; - - /** The reminder e-mail will be sent to users that have this at least one of this rights. */ - protected Set _reminderRights; - - /** The reminder e-mail subject i18n key. */ - protected String _reminderSubject; - - /** The reminder e-mail body i18n key. */ - protected String _reminderBody; - - /** The scheduled archiving reminder e-mail will be sent to users that have this at least one of this rights. */ - protected Set _scheduledArchivingReminderRights; - - /** The scheduled archiving reminder e-mail subject i18n key. */ - protected String _scheduledArchivingReminderSubject; - - /** The scheduled archiving reminder e-mail body i18n key. */ - protected String _scheduledArchivingReminderBody; - - /** The instant alert e-mail will be sent to users that have this at least one of this rights. */ - protected Set _instantAlertRights; - /** The instant alert e-mail subject i18n key. */ - protected String _instantAlertSubject; - /** The instant alert e-mail body i18n key. */ - protected String _instantAlertBody; - - /** True if the engine was been run in instant mode (for instant alerts only) **/ - protected boolean _instantMode; - /** The list of contents' id (for instant alerts only) **/ - private List _instantAlertContentIds; - /** The email message (for instant alerts only) **/ - private String _instantAlertMessage; - - /** - * Default constructor - */ - public AlertEngine () - { - _instantMode = false; - } - - /** - * Constructor used to send instant alerts - * @param contentIds The content's id - * @param message the message - */ - public AlertEngine (List contentIds, String message) - { - _instantMode = true; - _instantAlertContentIds = contentIds; - _instantAlertMessage = message; - } - - /** - * Initialize the alert engine. - * @param manager the avalon service manager. - * @param context the avalon context. - * @throws ContextException if the CONTEXT_ENVIRONMENT_CONTEXT cannot be found - * @throws ServiceException if some components cannot be resolved - */ - public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException - { - _manager = manager; - _context = context; - _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); - - // Lookup the needed components. - _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); - - _rightsManager = (RightsManager) manager.lookup(RightsManager.ROLE); - _userManager = (UserManager) manager.lookup(UserManager.ROLE); - _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); - - _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValueAsString("cms.url"), "index.html"), "/"); - _mailFrom = Config.getInstance().getValueAsString("smtp.mail.from"); - - _initialized = true; - } - - /** - * Configure the engine (called by the scheduler). - * @param configuration the component configuration. - * @throws ConfigurationException if the configuration is not valid - */ - public void configure(Configuration configuration) throws ConfigurationException - { - Configuration instantConf = configuration.getChild("instantAlert"); - Configuration validationConf = configuration.getChild("awaitingValidation"); - Configuration unmodifiedConf = configuration.getChild("unmodifiedContent"); - Configuration reminderConf = configuration.getChild("reminder"); - Configuration scheduledArchivingReminderConf = configuration.getChild("scheduledArchiving"); - - // Configure the rights. - _instantAlertRights = _getRightsFromConf(instantConf); - _awaitingValidationRights = _getRightsFromConf(validationConf); - _unmodifiedContentRights = _getRightsFromConf(unmodifiedConf); - _reminderRights = _getRightsFromConf(reminderConf); - _scheduledArchivingReminderRights = _getRightsFromConf(scheduledArchivingReminderConf); - Configuration[] stepIds = unmodifiedConf.getChildren("stepId"); - _unmodifiedContentStepIds = new int[stepIds.length]; - int i = 0; - for (Configuration stepId : stepIds) - { - try - { - _unmodifiedContentStepIds[i] = Integer.parseInt(stepId.getValue("")); - i++; - } - catch (NumberFormatException e) - { - // Ignore - } - } - - // Configure the i18n texts. - _awaitingValidationSubject = validationConf.getChild("subjectKey").getValue(); - _awaitingValidationBody = validationConf.getChild("bodyKey").getValue(); - _unmodifiedContentSubject = unmodifiedConf.getChild("subjectKey").getValue(); - _unmodifiedContentBody = unmodifiedConf.getChild("bodyKey").getValue(); - _reminderSubject = reminderConf.getChild("subjectKey").getValue(); - _reminderBody = reminderConf.getChild("bodyKey").getValue(); - _scheduledArchivingReminderSubject = scheduledArchivingReminderConf.getChild("subjectKey").getValue(); - _scheduledArchivingReminderBody = scheduledArchivingReminderConf.getChild("bodyKey").getValue(); - _instantAlertSubject = instantConf.getChild("subjectKey").getValue(); - _instantAlertBody = instantConf.getChild("bodyKey").getValue(); - } - - /** - * Check the initialization and throw an exception if not initialized. - */ - protected void _checkInitialization() - { - if (!_initialized) - { - String message = "Le composant de synchronisation doit être initialisé avant d'être lancé."; - _LOGGER.error(message); - throw new IllegalStateException(message); - } - } - - @Override - public void run() - { - Map environmentInformation = null; - try - { - _LOGGER.info("Preparing to send the alerts..."); - - _checkInitialization(); - - // Create the environment. - environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_manager, _environmentContext, new SLF4JLoggerAdapter(_LOGGER)); - - // Prepare and send all the alerts. - _sendAlerts(); - } - catch (Exception e) - { - _LOGGER.error("An error occurred sending the alerts.", e); - } - finally - { - // Leave the environment. - if (environmentInformation != null) - { - BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation); - } - // Dispose of the resources. - _dispose(); - _LOGGER.info("Alerts sent."); - } - } - - /** - * Dispose of the resources and looked-up components. - */ - protected void _dispose() - { - // Release the components. - if (_manager != null) - { - _manager.release(_ametysResolver); - _manager.release(_rightsManager); - _manager.release(_userManager); - } - - _ametysResolver = null; - _rightsManager = null; - _userManager = null; - - _environmentContext = null; - _context = null; - _manager = null; - - _initialized = false; - } - - /** - * Send all the alerts. Can be overridden to add alerts. - * @throws AmetysRepositoryException if an error occurs. - */ - protected void _sendAlerts() throws AmetysRepositoryException - { - if (_instantMode) - { - _sendInstantAlerts(); - } - else - { - _sendAwaitingValidationAlerts(); - _sendUnmodifiedAlerts(); - _sendReminders(); - _sendScheduledArchivingReminders(); - } - } - - /** - * Send instant alerts on contents - * @throws AmetysRepositoryException if an error occurred - */ - protected void _sendInstantAlerts () throws AmetysRepositoryException - { - if (_instantAlertContentIds != null && !_instantAlertContentIds.isEmpty()) - { - for (String contentId : _instantAlertContentIds) - { - Content content = _ametysResolver.resolveById(contentId); - _sendInstantAlertEmail (content, _instantAlertMessage); - } - } - } - - /** - * Send a instant e-mail alert to all the users who have the right to edit the content. - * @param content the content about which to send the alert. - * @param message the message - * @throws AmetysRepositoryException if an error occurred - */ - protected void _sendInstantAlertEmail(Content content, String message) throws AmetysRepositoryException - { - String context = _getContentContext(content); - - Set users = new HashSet<>(); - for (String right : _instantAlertRights) - { - users.addAll(_rightsManager.getGrantedUsers(right, context)); - } - - List params = _getInstantAlertParams(content, message); - - I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_instantAlertSubject, content), params); - I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_instantAlertBody, content), params); - - String subject = _i18nUtils.translate(i18nSubject); - String body = _i18nUtils.translate(i18nBody); - - _sendMails(subject, body, users, _mailFrom); - } - - /** - * Send the "awaiting validation" alerts. - * @throws AmetysRepositoryException if an error occurs. - */ - protected void _sendAwaitingValidationAlerts() throws AmetysRepositoryException - { - Long delay = Config.getInstance().getValueAsLong("remind.content.validation.delay"); - if (delay != null && delay > 0) - { - Calendar calendar = new GregorianCalendar(); - _removeTimeParts(calendar); - calendar.add(Calendar.DAY_OF_MONTH, 1 - delay.intValue()); - - // No last date and X days after the proposal date. - Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, true)); - Expression waitingExpression = new DateExpression("proposalDate", Operator.LT, calendar.getTime(), true); - // Or proposal date before the last "awaiting validation" date and the and X days after the last "awaiting validation" date. - Expression proposalBeforeLastDateExpr = new BinaryExpression("proposalDate", true, Operator.LT, AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, true); - Expression lastDateExpr = new DateExpression(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, Operator.LT, calendar.getTime(), true); - Expression expression = new OrExpression(new AndExpression(noLastDateExpr, waitingExpression), - new AndExpression(proposalBeforeLastDateExpr, lastDateExpr)); - - String query = ContentQueryHelper.getContentXPathQuery(expression); - - - - try (AmetysObjectIterable contents = _ametysResolver.query(query)) - { - if (_LOGGER.isInfoEnabled()) - { - _LOGGER.info("Contents waiting for validation: " + contents.getSize()); - } - - for (ModifiableContent content : contents) - { - // Send the alert e-mails. - _sendAwaitingValidationEmail(content); - - // Set the last validation alert date to now. - ModifiableCompositeMetadata meta = ((ModifiableMetadataAwareVersionableAmetysObject) content).getUnversionedMetadataHolder(); - meta.setMetadata(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, new Date()); - - content.saveChanges(); - } - } - } - } - - /** - * Send the unmodified content alerts. - * @throws AmetysRepositoryException if an error occurs. - */ - protected void _sendUnmodifiedAlerts() throws AmetysRepositoryException - { - Long delay = Config.getInstance().getValueAsLong("remind.unmodified.content.delay"); - if (delay != null && delay > 0) - { - Calendar calendar = new GregorianCalendar(); - _removeTimeParts(calendar); - calendar.add(Calendar.DAY_OF_MONTH, 1 - delay.intValue()); - - // If no step is configured, stepExpr will return the empty string. - Expression stepExpr = new WorkflowStepExpression(Operator.EQ, _unmodifiedContentStepIds, LogicalOperator.OR); - // Get only the contents on which the alert is enabled. - Expression unmodifiedExpr = new BooleanExpression(AlertsConstants.UNMODIFIED_ALERT_ENABLED, true, true); - // No last date and X days after the proposal date, or X days after the last date. - Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, true)); - Expression lastModifiedExpression = new DateExpression("lastModified", Operator.LT, calendar.getTime()); - Expression lastDateExpr = new DateExpression(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, Operator.LT, calendar.getTime(), true); - Expression dateExpr = new OrExpression(new AndExpression(noLastDateExpr, lastModifiedExpression), lastDateExpr); - // Full AND expression. - Expression expression = new AndExpression(unmodifiedExpr, dateExpr, stepExpr); - - String query = ContentQueryHelper.getContentXPathQuery(expression); - - try (AmetysObjectIterable contents = _ametysResolver.query(query)) - { - if (_LOGGER.isInfoEnabled()) - { - _LOGGER.info("Contents not modified for " + delay + " days: " + contents.getSize()); - } - - for (ModifiableContent content : contents) - { - // Send the alert e-mail. - _sendUnmodifiedContentEmail(content); - - // Set the last unmodified alert date to now. - ModifiableCompositeMetadata meta = ((ModifiableMetadataAwareVersionableAmetysObject) content).getUnversionedMetadataHolder(); - meta.setMetadata(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, new Date()); - - content.saveChanges(); - } - } - } - } - - /** - * Send the content reminders. - * @throws AmetysRepositoryException if an error occurs. - */ - protected void _sendReminders() throws AmetysRepositoryException - { - Calendar now = new GregorianCalendar(); - _removeTimeParts(now); - - Expression reminderExpr = new BooleanExpression(AlertsConstants.REMINDER_ENABLED, Operator.EQ, true, true); - Expression dateExpr = new DateExpression(AlertsConstants.REMINDER_DATE, Operator.EQ, now.getTime(), true); - Expression expression = new AndExpression(reminderExpr, dateExpr); - - String query = ContentQueryHelper.getContentXPathQuery(expression); - - try (AmetysObjectIterable contents = _ametysResolver.query(query)) - { - if (_LOGGER.isInfoEnabled()) - { - _LOGGER.info("Contents with reminder today: " + contents.getSize()); - } - - for (Content content : contents) - { - _sendReminderEmail(content); - } - } - } - - /** - * Send the scheduled archiving reminders on contents. - * @throws AmetysRepositoryException if an error occurs. - */ - protected void _sendScheduledArchivingReminders() throws AmetysRepositoryException - { - Long delay = Config.getInstance().getValueAsLong("archive.scheduler.reminder.delay"); - if (delay != null && delay > 0) - { - Calendar calendar = new GregorianCalendar(); - _removeTimeParts(calendar); - calendar.add(Calendar.DAY_OF_MONTH, delay.intValue()); - - // No last date and X days before the scheduled date. - Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.SCHEDULED_ARCHIVING_REMINDER_LAST_DATE, true)); - Expression scheduledDelayExpression = new DateExpression(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE, Operator.LE, calendar.getTime(), true); - Expression expression = new AndExpression(noLastDateExpr, scheduledDelayExpression); - - String query = ContentQueryHelper.getContentXPathQuery(expression); - - try (AmetysObjectIterable contents = _ametysResolver.query(query)) - { - if (_LOGGER.isInfoEnabled()) - { - _LOGGER.info("Contents with scheduled archiving reminder today: " + contents.getSize()); - } - - for (ModifiableContent content : contents) - { - _sendScheduledArchivingReminderEmail(content); - - // Set the last scheduled archiving reminder date to now. - ModifiableCompositeMetadata meta = ((ModifiableMetadataAwareVersionableAmetysObject) content).getUnversionedMetadataHolder(); - meta.setMetadata(AlertsConstants.SCHEDULED_ARCHIVING_REMINDER_LAST_DATE, new Date()); - - content.saveChanges(); - } - } - } - } - - /** - * Send a "waiting for validation" e-mail alert to all the users who have the right to validate. - * @param content the content about which to send the alert. - * @throws AmetysRepositoryException if an error occured on the repository - */ - protected void _sendAwaitingValidationEmail(Content content) throws AmetysRepositoryException - { - String context = _getContentContext(content); - - Set users = new HashSet<>(); - for (String right : _awaitingValidationRights) - { - users.addAll(_rightsManager.getGrantedUsers(right, context)); - } - - List params = _getAwaitingValidationParams(content); - - I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_awaitingValidationSubject, content), params); - I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_awaitingValidationBody, content), params); - - String subject = _i18nUtils.translate(i18nSubject); - String body = _i18nUtils.translate(i18nBody); - - _sendMails(subject, body, users, _mailFrom); - } - - /** - * Send a "unmodified content" e-mail alert to all the users who have the right to edit. - * @param content the content about which to send the alert. - * @throws AmetysRepositoryException if an error occured on the repository - */ - protected void _sendUnmodifiedContentEmail(Content content) throws AmetysRepositoryException - { - String context = _getContentContext(content); - - Set users = new HashSet<>(); - for (String right : _unmodifiedContentRights) - { - users.addAll(_rightsManager.getGrantedUsers(right, context)); - } - - List params = _getUnmodifiedContentParams(content); - - I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_unmodifiedContentSubject, content), params); - I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_unmodifiedContentBody, content), params); - - String subject = _i18nUtils.translate(i18nSubject); - String body = _i18nUtils.translate(i18nBody); - - _sendMails(subject, body, users, _mailFrom); - } - - /** - * Send a reminder e-mail to all the users who have the right to edit. - * @param content the content about which to send the reminder. - * @throws AmetysRepositoryException if an error occured on the repository - */ - protected void _sendReminderEmail(Content content) throws AmetysRepositoryException - { - String context = _getContentContext(content); - - Set users = new HashSet<>(); - for (String right : _reminderRights) - { - users.addAll(_rightsManager.getGrantedUsers(right, context)); - } - - List params = _getReminderParams(content); - - I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_reminderSubject, content), params); - I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_reminderBody, content), params); - - String subject = _i18nUtils.translate(i18nSubject); - String body = _i18nUtils.translate(i18nBody); - - _sendMails(subject, body, users, _mailFrom); - } - - /** - * Send a "scheduled archiving reminder" e-mail to all the users who have the right to archive. - * @param content the content about which to send the alert. - * @throws AmetysRepositoryException if an error occured on the repository - */ - protected void _sendScheduledArchivingReminderEmail(ModifiableContent content) throws AmetysRepositoryException - { - String context = _getContentContext(content); - - Set users = new HashSet<>(); - for (String right : _scheduledArchivingReminderRights) - { - users.addAll(_rightsManager.getGrantedUsers(right, context)); - } - - List params = _getScheduledArchivingReminderParams(content); - - I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_scheduledArchivingReminderSubject, content), params); - I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_scheduledArchivingReminderBody, content), params); - - String subject = _i18nUtils.translate(i18nSubject); - String body = _i18nUtils.translate(i18nBody); - - _sendMails(subject, body, users, _mailFrom); - } - - /** - * Get the transform i18n body key for specific content - * @param bodyI18nKey the original body key - * @param content the content - * @return the transform i18n body key - */ - protected String getI18nKeyBody(String bodyI18nKey, Content content) - { - return bodyI18nKey; - } - - /** - * Get the mail parameters for instant alert. - * @param content the content. - * @param message the message - * @return the parameters. - */ - protected List _getInstantAlertParams(Content content, String message) - { - List params = new ArrayList<>(); - - params.add(content.getTitle()); // {0} - params.add(_getContentUrl(content)); // {1} - params.add(message); // {2} - - return params; - } - - /** - * Get the mail parameters. - * @param content the content. - * @return the parameters. - */ - protected List _getAwaitingValidationParams(Content content) - { - List params = new ArrayList<>(); - - String delay = Config.getInstance().getValueAsString("remind.content.validation.delay"); - - params.add(content.getTitle()); // {0} - params.add(_getContentUrl(content)); // {1} - params.add(delay); // {2} - - return params; - } - - /** - * Get the mail parameters. - * @param content the content. - * @return the parameters. - */ - protected List _getUnmodifiedContentParams(Content content) - { - List params = new ArrayList<>(); - - String delay = Config.getInstance().getValueAsString("remind.unmodified.content.delay"); - - params.add(content.getTitle()); // {0} - params.add(_getContentUrl(content)); // {1} - params.add(delay); // {2} - - CompositeMetadata meta = ((MetadataAndVersionAwareAmetysObject) content).getUnversionedMetadataHolder(); - String alertText = meta.getString(AlertsConstants.UNMODIFIED_ALERT_TEXT, ""); - params.add(alertText); // {3} - - return params; - } - - /** - * Get the mail parameters. - * @param content the content. - * @return the parameters. - */ - protected List _getReminderParams(Content content) - { - List params = new ArrayList<>(); - - // Should never trigger a ClassCastException, as we query on an unversioned metadata. - CompositeMetadata meta = ((MetadataAndVersionAwareAmetysObject) content).getUnversionedMetadataHolder(); - String reminderText = meta.getString(AlertsConstants.REMINDER_TEXT, ""); - - params.add(content.getTitle()); // {0} - params.add(_getContentUrl(content)); // {1} - params.add(reminderText); // {2} - - return params; - } - - /** - * Get the mail parameters. - * @param content the content. - * @return the parameters. - */ - protected List _getScheduledArchivingReminderParams(ModifiableContent content) - { - List params = new ArrayList<>(); - - String delay = Config.getInstance().getValueAsString("archive.scheduler.reminder.delay"); - - params.add(content.getTitle()); // {0} - params.add(_getContentUrl(content)); // {1} - params.add(delay); // {2} - - return params; - } - - /** - * Send the alert emails. - * @param subject the e-mail subject. - * @param body the e-mail body. - * @param users users to send the mail to. - * @param from the address sending the e-mail. - */ - protected void _sendMails(String subject, String body, Set users, String from) - { - for (UserIdentity identity : users) - { - User user = _userManager.getUser(identity.getPopulationId(), identity.getLogin()); - - if (user != null && StringUtils.isNotBlank(user.getEmail())) - { - String mail = user.getEmail(); - - try - { - SendMailHelper.sendMail(subject, null, body, mail, from); - } - catch (MessagingException e) - { - if (_LOGGER.isWarnEnabled()) - { - _LOGGER.warn("Could not send an alert e-mail to " + mail, e); - } - } - } - } - } - - /** - * Get the rights context on the given content. - * @param content the content. - * @return the rights context. - * @throws AmetysRepositoryException if an error occured on the repository - */ - protected String _getContentContext(Content content) throws AmetysRepositoryException - { - return "/contents/" + content.getName(); - } - - /** - * Get the URL to the given content tool. - * @param content the content. - * @return the content URL. - */ - protected String _getContentUrl(Content content) - { - StringBuilder url = new StringBuilder(_baseUrl); - url.append("/index.html?uitool=uitool-content,id:%27").append(content.getId()).append("%27"); - - return url.toString(); - } - - /** - * Remove the time parts from a calendar, leaving only date parts. - * @param calendar the calendar. - */ - protected void _removeTimeParts(Calendar calendar) - { - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - } - - /** - * Get a set of rights from a configuration. - * @param configuration the configuration. - * @return the set of rights. - * @throws ConfigurationException if the configuration is not valid. - */ - protected Set _getRightsFromConf(Configuration configuration) throws ConfigurationException - { - Set rights = new HashSet<>(); - - for (Configuration rightConf : configuration.getChildren("right")) - { - String right = rightConf.getValue(""); - if (StringUtils.isNotBlank(right)) - { - rights.add(right); - } - } - - return rights; - } - - /** - * Binary date expression: test on two metadatas. - */ - protected class BinaryExpression implements Expression - { - private MetadataExpression _metadata1; - private MetadataExpression _metadata2; - private Operator _operator; - - /** - * Creates the comparison Expression. - * @param metadata1 the first metadata name. - * @param operator the operator to make the comparison - * @param metadata2 the second metadata name. - */ - public BinaryExpression(String metadata1, Operator operator, String metadata2) - { - _metadata1 = new MetadataExpression(metadata1); - _operator = operator; - _metadata2 = new MetadataExpression(metadata2); - } - - /** - * Creates the comparison Expression. - * @param metadata1 the first metadata name. - * @param unversioned1 true if the first metadata is unversioned. - * @param operator the operator to make the comparison. - * @param metadata2 the second metadata name. - * @param unversioned2 true if the second metadata is unversioned. - */ - public BinaryExpression(String metadata1, boolean unversioned1, Operator operator, String metadata2, boolean unversioned2) - { - _metadata1 = new MetadataExpression(metadata1, unversioned1); - _operator = operator; - _metadata2 = new MetadataExpression(metadata2, unversioned2); - } - - @Override - public String build() - { - return _metadata1.build() + " " + _operator + " " + _metadata2.build(); - } - } - -} Index: main/plugin-cms/src/org/ametys/cms/alerts/AlertEngineRunnable.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/alerts/AlertEngineRunnable.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/alerts/AlertEngineRunnable.java (revision 0) @@ -0,0 +1,47 @@ +/* + * Copyright 2016 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.alerts; + +import org.apache.avalon.framework.configuration.Configuration; +import org.apache.avalon.framework.configuration.ConfigurationException; +import org.apache.commons.lang3.StringUtils; + +import org.ametys.core.schedule.Runnable; +import org.ametys.plugins.core.impl.schedule.StaticRunnable; +import org.ametys.runtime.config.Config; + +/** + * {@link Runnable} for scheduling an {@link AlertEngineSchedulable} to run once a day depending on a configuration parameter. + */ +public class AlertEngineRunnable extends StaticRunnable +{ + @Override + public void configure(Configuration configuration) throws ConfigurationException + { + _fireProcess = FireProcess.CRON; + super.configure(configuration); + String hourStr = Config.getInstance().getValueAsString("alerts.scheduler.hour"); + if (StringUtils.isNotEmpty(hourStr) && hourStr.indexOf(':') > 0) + { + int hour = 0; + int minute = 0; + String[] hourArray = StringUtils.split(hourStr, ':'); + hour = Integer.parseInt(hourArray[0]); + minute = Integer.parseInt(hourArray[1]); + _cronExpression = "0 " + minute + " " + hour + " * * ? *"; + } + } +} Index: main/plugin-cms/src/org/ametys/cms/alerts/AlertEngineSchedulable.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/alerts/AlertEngineSchedulable.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/alerts/AlertEngineSchedulable.java (revision 0) @@ -0,0 +1,62 @@ +/* + * Copyright 2016 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.alerts; + +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.quartz.JobExecutionContext; + +import org.ametys.core.schedule.Schedulable; +import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable; +import org.ametys.runtime.config.Config; + +/** + * A {@link Schedulable} job that looks at the contents and computes if they need to have some email alerts sent. + */ +public class AlertEngineSchedulable extends AbstractStaticSchedulable +{ + /** The service manager */ + protected ServiceManager _manager; + /** The helper for the alerts on contents */ + protected ContentAlertHelper _contentAlertHelper; + + @Override + public void service(ServiceManager manager) throws ServiceException + { + super.service(manager); + _manager = manager; + } + + @Override + public void execute(JobExecutionContext context) throws Exception + { + if (_contentAlertHelper == null) + { + // Delayed initialize because of circular dependency + _delayedInitializeAlertHelper(); + } + + if (Config.getInstance().getValueAsBoolean("remind.content.enabled")) + { + _contentAlertHelper.sendAlerts(); + } + } + + private void _delayedInitializeAlertHelper() throws ServiceException + { + _contentAlertHelper = (ContentAlertHelper) _manager.lookup(ContentAlertHelper.ROLE); + } +} Index: main/plugin-cms/src/org/ametys/cms/alerts/AlertScheduler.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/alerts/AlertScheduler.java (revision 42093) +++ main/plugin-cms/src/org/ametys/cms/alerts/AlertScheduler.java (working copy) @@ -1,239 +0,0 @@ -/* - * Copyright 2014 Anyware Services - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ametys.cms.alerts; - -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; - -import org.apache.avalon.framework.activity.Disposable; -import org.apache.avalon.framework.activity.Initializable; -import org.apache.avalon.framework.component.Component; -import org.apache.avalon.framework.configuration.Configurable; -import org.apache.avalon.framework.configuration.Configuration; -import org.apache.avalon.framework.configuration.ConfigurationException; -import org.apache.avalon.framework.context.Context; -import org.apache.avalon.framework.context.ContextException; -import org.apache.avalon.framework.context.Contextualizable; -import org.apache.avalon.framework.logger.LogEnabled; -import org.apache.avalon.framework.logger.Logger; -import org.apache.avalon.framework.service.ServiceException; -import org.apache.avalon.framework.service.ServiceManager; -import org.apache.avalon.framework.service.Serviceable; -import org.apache.cocoon.environment.Request; -import org.apache.commons.lang.StringUtils; - -import org.ametys.runtime.config.Config; - -/** - * Alerts scheduler: launches a cron which sends the alerts each night. - */ -public class AlertScheduler extends TimerTask implements Initializable, LogEnabled, Serviceable, Disposable, Contextualizable, Configurable, Component -{ - /** The Avalon role */ - public static final String ROLE = AlertScheduler.class.getName(); - - /** The service manager. */ - protected ServiceManager _manager; - - /** The component configuration. */ - protected Configuration _configuration; - - /** The avalon context. */ - protected Context _context; - - /** The logger. */ - protected Logger _logger; - - /** The timer. */ - protected Timer _timer; - - @Override - public void service(ServiceManager manager) throws ServiceException - { - _manager = manager; - } - - @Override - public void contextualize(Context context) throws ContextException - { - _context = context; - } - - @Override - public void configure(Configuration configuration) throws ConfigurationException - { - _configuration = configuration; - } - - @Override - public void enableLogging(Logger logger) - { - _logger = logger; - } - - @Override - public void initialize() throws Exception - { - if (!Config.getInstance().getValueAsBoolean("remind.content.enabled")) - { - return; - } - - if (_logger.isDebugEnabled()) - { - _logger.debug("Initializing the alert scheduler..."); - } - - // Schedule a timer to run each night. - String hourStr = Config.getInstance().getValueAsString("alerts.scheduler.hour"); - int hour = 0; - int minute = 0; - if (StringUtils.isNotEmpty(hourStr) && hourStr.indexOf(':') > 0) - { - String[] hourArray = StringUtils.split(hourStr, ':'); - hour = Integer.parseInt(hourArray[0]); - minute = Integer.parseInt(hourArray[1]); - } - - GregorianCalendar calendar = new GregorianCalendar(); - calendar.set(Calendar.AM_PM, hour < 12 ? Calendar.AM : Calendar.PM); - calendar.set(Calendar.HOUR, hour % 12); - calendar.set(Calendar.MINUTE, minute); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - - // Each day. - long period = 86400000; - Date firstTime = calendar.getTime(); - - Date now = new Date(); - - // If the given time today is past, schedule for tomorrow. - if (firstTime.compareTo(now) < 0) - { - calendar.add(Calendar.DAY_OF_MONTH, 1); - firstTime = calendar.getTime(); - } - - if (_logger.isInfoEnabled()) - { - _logger.info("Alerts scheduler: the alerts engine will run each day, starting " + firstTime.toString()); - } - - _timer = new Timer("AlertsScheduler", true); - _timer.schedule(this, firstTime, period); - } - - @Override - public void run() - { - AlertEngine alertEngine = new AlertEngine(); - - try - { - // Initialize and configure the engine. - alertEngine.initialize(_manager, _context); - alertEngine.configure(_configuration); - } - catch (Exception e) - { - throw new RuntimeException("Unable to initialize the alerts engine", e); - } - - // The thread will be started as daemon, as the scheduler is marked daemon itself. - new Thread(alertEngine, "AlertEngine").start(); - } - - /** - * Run the scheduler to send instant alerts on contents - * @param contentIds The ids of concerned contents - * @param message the message to send to authorized users - */ - public void sendInstantAlerts (List contentIds, String message) - { - AlertEngine alertEngine = new AlertEngine(contentIds, message); - - try - { - // Initialize and configure the engine. - alertEngine.initialize(_manager, _context); - alertEngine.configure(_configuration); - } - catch (Exception e) - { - throw new RuntimeException("Unable to initialize the alerts engine", e); - } - - // The thread will be started as daemon, as the scheduler is marked daemon itself. - new Thread(alertEngine, "AlertEngine").start(); - } - - /** - * Get the request URI. - * @param request the request object. - * @return the full request URI. - */ - protected String _getRequestURI(Request request) - { - StringBuilder sb = new StringBuilder(); - sb.append(request.getScheme()); - sb.append("://"); - sb.append(request.getServerName()); - - // Construire une uri sans :80 en http et sans :443 en https - if (request.isSecure()) - { - if (request.getServerPort() != 443) - { - sb.append(":"); - sb.append(request.getServerPort()); - } - } - else - { - if (request.getServerPort() != 80) - { - sb.append(":"); - sb.append(request.getServerPort()); - } - } - - sb.append(request.getContextPath()); - - return sb.toString(); - } - - @Override - public void dispose() - { - _context = null; - _logger = null; - _manager = null; - _configuration = null; - - cancel(); - if (_timer != null) - { - _timer.cancel(); - _timer = null; - } - } - -} Index: main/plugin-cms/src/org/ametys/cms/alerts/AlertsConstants.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/alerts/AlertsConstants.java (revision 42093) +++ main/plugin-cms/src/org/ametys/cms/alerts/AlertsConstants.java (working copy) @@ -33,15 +33,6 @@ /** The name of the metadata storing when the "unmodified content" alert was last sent. */ public static final String UNMODIFIED_ALERT_LAST_DATE = "unmodifiedAlertLastDate"; - /** The name of the metadata storing whether the reminder is enabled. */ - public static final String REMINDER_ENABLED = "reminderEnabled"; - - /** The name of the metadata storing the reminder date. */ - public static final String REMINDER_DATE = "reminderDate"; - - /** The name of the metadata storing the reminder text. */ - public static final String REMINDER_TEXT = "reminderText"; - /** The name of the metadata storing when the "scheduled archiving" alert was last sent. */ public static final String SCHEDULED_ARCHIVING_REMINDER_LAST_DATE = "scheduledArchivingLastDate"; Index: main/plugin-cms/src/org/ametys/cms/alerts/ContentAlertHelper.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/alerts/ContentAlertHelper.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/alerts/ContentAlertHelper.java (revision 0) @@ -0,0 +1,826 @@ +/* + * Copyright 2016 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.alerts; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.mail.MessagingException; + +import org.apache.avalon.framework.activity.Disposable; +import org.apache.avalon.framework.activity.Initializable; +import org.apache.avalon.framework.component.Component; +import org.apache.avalon.framework.configuration.Configurable; +import org.apache.avalon.framework.configuration.Configuration; +import org.apache.avalon.framework.configuration.ConfigurationException; +import org.apache.avalon.framework.context.Context; +import org.apache.avalon.framework.context.ContextException; +import org.apache.avalon.framework.context.Contextualizable; +import org.apache.avalon.framework.service.ServiceException; +import org.apache.avalon.framework.service.ServiceManager; +import org.apache.avalon.framework.service.Serviceable; +import org.apache.commons.lang.StringUtils; +import org.quartz.JobKey; +import org.quartz.SchedulerException; + +import org.ametys.cms.content.archive.ArchiveConstants; +import org.ametys.cms.repository.Content; +import org.ametys.cms.repository.ContentQueryHelper; +import org.ametys.cms.repository.ModifiableContent; +import org.ametys.cms.repository.WorkflowStepExpression; +import org.ametys.cms.repository.WorkflowStepExpression.LogicalOperator; +import org.ametys.core.right.RightsManager; +import org.ametys.core.user.User; +import org.ametys.core.user.UserIdentity; +import org.ametys.core.user.UserManager; +import org.ametys.core.util.I18nUtils; +import org.ametys.core.util.mail.SendMailHelper; +import org.ametys.plugins.core.schedule.Scheduler; +import org.ametys.plugins.repository.AmetysObjectIterable; +import org.ametys.plugins.repository.AmetysObjectResolver; +import org.ametys.plugins.repository.AmetysRepositoryException; +import org.ametys.plugins.repository.metadata.CompositeMetadata; +import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; +import org.ametys.plugins.repository.query.expression.AndExpression; +import org.ametys.plugins.repository.query.expression.BooleanExpression; +import org.ametys.plugins.repository.query.expression.DateExpression; +import org.ametys.plugins.repository.query.expression.Expression; +import org.ametys.plugins.repository.query.expression.Expression.Operator; +import org.ametys.plugins.repository.query.expression.MetadataExpression; +import org.ametys.plugins.repository.query.expression.NotExpression; +import org.ametys.plugins.repository.query.expression.OrExpression; +import org.ametys.plugins.repository.version.MetadataAndVersionAwareAmetysObject; +import org.ametys.plugins.repository.version.ModifiableMetadataAwareVersionableAmetysObject; +import org.ametys.runtime.config.Config; +import org.ametys.runtime.i18n.I18nizableText; +import org.ametys.runtime.plugin.component.AbstractLogEnabled; + +/** + * Helper for content alerts. + */ +public class ContentAlertHelper extends AbstractLogEnabled implements Component, Serviceable, Configurable, Initializable, Contextualizable, Disposable +{ + /** The Avalon role */ + public static final String ROLE = ContentAlertHelper.class.getName(); + + /** The avalon context. */ + protected Context _context; + /** The ametys object resolver. */ + protected AmetysObjectResolver _ametysResolver; + /** The right manager. */ + protected RightsManager _rightManager; + /** The user manager. */ + protected UserManager _userManager; + /** The i18n utils. */ + protected I18nUtils _i18nUtils; + /** The scheduler */ + protected Scheduler _scheduler; + + /** The reminder e-mail will be sent to users that have this at least one of this rights. */ + protected Set _reminderRights; + /** The reminder e-mail subject i18n key. */ + protected String _reminderSubject; + /** The reminder e-mail body i18n key. */ + protected String _reminderBody; + + /** The instant alert e-mail will be sent to users that have this at least one of this rights. */ + protected Set _instantAlertRights; + /** The instant alert e-mail subject i18n key. */ + protected String _instantAlertSubject; + /** The instant alert e-mail body i18n key. */ + protected String _instantAlertBody; + + /** The "waiting for validation" e-mail will be sent to users that have at least one of this rights. */ + protected Set _awaitingValidationRights; + /** The "waiting for validation" e-mail subject i18n key. */ + protected String _awaitingValidationSubject; + /** The "waiting for validation" e-mail body i18n key. */ + protected String _awaitingValidationBody; + + /** Only contents in this steps will be taken into account for the "unmodified content" alert. */ + protected int[] _unmodifiedContentStepIds; + /** The "unmodified content" e-mail will be sent to users that have at least one of this rights. */ + protected Set _unmodifiedContentRights; + /** The "unmodified content" e-mail subject i18n key. */ + protected String _unmodifiedContentSubject; + /** The "unmodified content" e-mail body i18n key. */ + protected String _unmodifiedContentBody; + + /** The scheduled archiving reminder e-mail will be sent to users that have this at least one of this rights. */ + protected Set _scheduledArchivingReminderRights; + /** The scheduled archiving reminder e-mail subject i18n key. */ + protected String _scheduledArchivingReminderSubject; + /** The scheduled archiving reminder e-mail body i18n key. */ + protected String _scheduledArchivingReminderBody; + + /** The server base URL. */ + protected String _baseUrl; + /** The content of "from" field in emails. */ + protected String _mailFrom; + + @Override + public void configure(Configuration configuration) throws ConfigurationException + { + Configuration instantConf = configuration.getChild("instantAlert"); + Configuration validationConf = configuration.getChild("awaitingValidation"); + Configuration unmodifiedConf = configuration.getChild("unmodifiedContent"); + Configuration reminderConf = configuration.getChild("reminder"); + Configuration scheduledArchivingReminderConf = configuration.getChild("scheduledArchiving"); + + // Configure the rights. + _instantAlertRights = _getRightsFromConf(instantConf); + _awaitingValidationRights = _getRightsFromConf(validationConf); + _unmodifiedContentRights = _getRightsFromConf(unmodifiedConf); + _reminderRights = _getRightsFromConf(reminderConf); + _scheduledArchivingReminderRights = _getRightsFromConf(scheduledArchivingReminderConf); + Configuration[] stepIds = unmodifiedConf.getChildren("stepId"); + _unmodifiedContentStepIds = new int[stepIds.length]; + int i = 0; + for (Configuration stepId : stepIds) + { + try + { + _unmodifiedContentStepIds[i] = Integer.parseInt(stepId.getValue("")); + i++; + } + catch (NumberFormatException e) + { + // Ignore + } + } + + // Configure the i18n texts. + _awaitingValidationSubject = validationConf.getChild("subjectKey").getValue(); + _awaitingValidationBody = validationConf.getChild("bodyKey").getValue(); + _unmodifiedContentSubject = unmodifiedConf.getChild("subjectKey").getValue(); + _unmodifiedContentBody = unmodifiedConf.getChild("bodyKey").getValue(); + _reminderSubject = reminderConf.getChild("subjectKey").getValue(); + _reminderBody = reminderConf.getChild("bodyKey").getValue(); + _scheduledArchivingReminderSubject = scheduledArchivingReminderConf.getChild("subjectKey").getValue(); + _scheduledArchivingReminderBody = scheduledArchivingReminderConf.getChild("bodyKey").getValue(); + _instantAlertSubject = instantConf.getChild("subjectKey").getValue(); + _instantAlertBody = instantConf.getChild("bodyKey").getValue(); + } + + /** + * Get a set of rights from a configuration. + * @param configuration the configuration. + * @return the set of rights. + * @throws ConfigurationException if the configuration is not valid. + */ + protected Set _getRightsFromConf(Configuration configuration) throws ConfigurationException + { + Set rights = new HashSet<>(); + + for (Configuration rightConf : configuration.getChildren("right")) + { + String right = rightConf.getValue(""); + if (StringUtils.isNotBlank(right)) + { + rights.add(right); + } + } + + return rights; + } + + @Override + public void service(ServiceManager manager) throws ServiceException + { + _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); + _rightManager = (RightsManager) manager.lookup(RightsManager.ROLE); + _userManager = (UserManager) manager.lookup(UserManager.ROLE); + _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); + _scheduler = (Scheduler) manager.lookup(Scheduler.ROLE); + } + + @Override + public void contextualize(Context context) throws ContextException + { + _context = context; + } + + @Override + public void initialize() throws Exception + { + _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValueAsString("cms.url"), "index.html"), "/"); + _mailFrom = Config.getInstance().getValueAsString("smtp.mail.from"); + } + + @Override + public void dispose() + { + _ametysResolver = null; + _rightManager = null; + _userManager = null; + + _context = null; + } + + /** + * Send instant alerts on contents + * @param contentIds The content ids + * @param message The email message + * @throws AmetysRepositoryException if an error occurred + */ + public void sendInstantAlerts(List contentIds, String message) + { + if (contentIds != null && !contentIds.isEmpty()) + { + for (String contentId : contentIds) + { + Content content = _ametysResolver.resolveById(contentId); + _scheduleInstantAlertEmail(content, message); + } + } + } + + /** + * Schedule the sending of a reminder e-mail to all the users who have the right to edit. + * @param content the content about which to send the reminder. + * @param reminderText The text included in the mail + * @param date The date when the mail will be sent + */ + public void scheduleReminderEmail(Content content, String reminderText, ZonedDateTime date) + { + String context = _getContentContext(content); + + Set users = new HashSet<>(); + for (String right : _reminderRights) + { + users.addAll(_rightManager.getGrantedUsers(right, context)); + } + + List params = _getReminderParams(content, reminderText); + + I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_reminderSubject, content), params); + I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_reminderBody, content), params); + + String subject = _i18nUtils.translate(i18nSubject); + String body = _i18nUtils.translate(i18nBody); + + org.ametys.core.schedule.Runnable reminderContentAlertRunnable = new ReminderContentAlertRunnable(content.getId(), content.getName(), _mailFrom, _getMails(users), subject, body, date, reminderText); + try + { + JobKey jobKey = new JobKey(reminderContentAlertRunnable.getId(), Scheduler.JOB_GROUP); + if (_scheduler.getScheduler().checkExists(jobKey)) + { + _scheduler.getScheduler().deleteJob(jobKey); + } + _scheduler.scheduleJob(reminderContentAlertRunnable); + } + catch (SchedulerException e) + { + if (getLogger().isErrorEnabled()) + { + getLogger().error("An error occured when trying to schedule the reminder alert of the content " + content.getId(), e); + } + } + } + + /** + * Unschedule the sending of a reminder email + * @param content the content concerned by the alert + */ + public void unscheduleReminderEmail(Content content) + { + try + { + JobKey jobKey = new JobKey(ReminderContentAlertRunnable.ID_PREFIX + content.getId(), Scheduler.JOB_GROUP); + if (_scheduler.getScheduler().checkExists(jobKey)) + { + _scheduler.getScheduler().deleteJob(jobKey); + } + } + catch (SchedulerException e) + { + if (getLogger().isErrorEnabled()) + { + getLogger().error("An error occured when trying to unschedule the reminder alert of the content " + content.getId(), e); + } + } + } + + /** + * Schedule the sending if an instant e-mail alert to all the users who have the right to edit the content. + * @param content the content about which to send the alert. + * @param message the message + */ + protected void _scheduleInstantAlertEmail(Content content, String message) + { + String context = _getContentContext(content); + + Set users = new HashSet<>(); + for (String right : _instantAlertRights) + { + users.addAll(_rightManager.getGrantedUsers(right, context)); + } + + List params = _getInstantAlertParams(content, message); + + I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_instantAlertSubject, content), params); + I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_instantAlertBody, content), params); + + String subject = _i18nUtils.translate(i18nSubject); + String body = _i18nUtils.translate(i18nBody); + + org.ametys.core.schedule.Runnable instantContentAlertRunnable = new InstantContentAlertRunnable(content.getId(), content.getName(), _mailFrom, _getMails(users), subject, body); + try + { + JobKey jobKey = new JobKey(instantContentAlertRunnable.getId(), Scheduler.JOB_GROUP); + if (_scheduler.getScheduler().checkExists(jobKey)) + { + _scheduler.getScheduler().deleteJob(jobKey); + } + _scheduler.scheduleJob(instantContentAlertRunnable); + } + catch (SchedulerException e) + { + if (getLogger().isErrorEnabled()) + { + getLogger().error("An error occured when trying to schedule the instant alert of the content " + content.getId(), e); + } + } + } + + private String _getMails(Set users) + { + StringBuilder mails = new StringBuilder(); + for (UserIdentity identity : users) + { + User user = _userManager.getUser(identity.getPopulationId(), identity.getLogin()); + + if (user != null && StringUtils.isNotBlank(user.getEmail())) + { + if (mails.length() != 0) + { + mails.append("\\n"); + } + mails.append(user.getEmail()); + } + } + + return mails.toString(); + } + + /** + * Get the mail parameters. + * @param content the content. + * @param reminderText The reminder text + * @return the parameters. + */ + protected List _getReminderParams(Content content, String reminderText) + { + List params = new ArrayList<>(); + + params.add(content.getTitle()); // {0} + params.add(_getContentUrl(content)); // {1} + params.add(reminderText); // {2} + + return params; + } + + /** + * Send all the alerts that cannot be a Runnable themselves (for example, the instant and reminder alerts are not sent through this call). Can be overridden to add alerts. + * @throws AmetysRepositoryException if an error occurs. + */ + public void sendAlerts() throws AmetysRepositoryException + { + _sendAwaitingValidationAlerts(); + _sendUnmodifiedAlerts(); + _sendScheduledArchivingReminders(); + } + + /** + * Send the "awaiting validation" alerts. + * @throws AmetysRepositoryException if an error occurs. + */ + protected void _sendAwaitingValidationAlerts() throws AmetysRepositoryException + { + Long delay = Config.getInstance().getValueAsLong("remind.content.validation.delay"); + if (delay != null && delay > 0) + { + Calendar calendar = new GregorianCalendar(); + _removeTimeParts(calendar); + calendar.add(Calendar.DAY_OF_MONTH, 1 - delay.intValue()); + + // No last date and X days after the proposal date. + Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, true)); + Expression waitingExpression = new DateExpression("proposalDate", Operator.LT, calendar.getTime(), true); + // Or proposal date before the last "awaiting validation" date and the and X days after the last "awaiting validation" date. + Expression proposalBeforeLastDateExpr = new BinaryExpression("proposalDate", true, Operator.LT, AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, true); + Expression lastDateExpr = new DateExpression(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, Operator.LT, calendar.getTime(), true); + Expression expression = new OrExpression(new AndExpression(noLastDateExpr, waitingExpression), + new AndExpression(proposalBeforeLastDateExpr, lastDateExpr)); + + String query = ContentQueryHelper.getContentXPathQuery(expression); + + + + try (AmetysObjectIterable contents = _ametysResolver.query(query)) + { + if (getLogger().isInfoEnabled()) + { + getLogger().info("Contents waiting for validation: " + contents.getSize()); + } + + for (ModifiableContent content : contents) + { + // Send the alert e-mails. + _sendAwaitingValidationEmail(content); + + // Set the last validation alert date to now. + ModifiableCompositeMetadata meta = ((ModifiableMetadataAwareVersionableAmetysObject) content).getUnversionedMetadataHolder(); + meta.setMetadata(AlertsConstants.AWAITING_VALIDATION_ALERT_LAST_DATE, new Date()); + + content.saveChanges(); + } + } + } + } + + /** + * Send the unmodified content alerts. + * @throws AmetysRepositoryException if an error occurs. + */ + protected void _sendUnmodifiedAlerts() throws AmetysRepositoryException + { + Long delay = Config.getInstance().getValueAsLong("remind.unmodified.content.delay"); + if (delay != null && delay > 0) + { + Calendar calendar = new GregorianCalendar(); + _removeTimeParts(calendar); + calendar.add(Calendar.DAY_OF_MONTH, 1 - delay.intValue()); + + // If no step is configured, stepExpr will return the empty string. + Expression stepExpr = new WorkflowStepExpression(Operator.EQ, _unmodifiedContentStepIds, LogicalOperator.OR); + // Get only the contents on which the alert is enabled. + Expression unmodifiedExpr = new BooleanExpression(AlertsConstants.UNMODIFIED_ALERT_ENABLED, true, true); + // No last date and X days after the proposal date, or X days after the last date. + Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, true)); + Expression lastModifiedExpression = new DateExpression("lastModified", Operator.LT, calendar.getTime()); + Expression lastDateExpr = new DateExpression(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, Operator.LT, calendar.getTime(), true); + Expression dateExpr = new OrExpression(new AndExpression(noLastDateExpr, lastModifiedExpression), lastDateExpr); + // Full AND expression. + Expression expression = new AndExpression(unmodifiedExpr, dateExpr, stepExpr); + + String query = ContentQueryHelper.getContentXPathQuery(expression); + + try (AmetysObjectIterable contents = _ametysResolver.query(query)) + { + if (getLogger().isInfoEnabled()) + { + getLogger().info("Contents not modified for " + delay + " days: " + contents.getSize()); + } + + for (ModifiableContent content : contents) + { + // Send the alert e-mail. + _sendUnmodifiedContentEmail(content); + + // Set the last unmodified alert date to now. + ModifiableCompositeMetadata meta = ((ModifiableMetadataAwareVersionableAmetysObject) content).getUnversionedMetadataHolder(); + meta.setMetadata(AlertsConstants.UNMODIFIED_ALERT_LAST_DATE, new Date()); + + content.saveChanges(); + } + } + } + } + + /** + * Send the scheduled archiving reminders on contents. + * @throws AmetysRepositoryException if an error occurs. + */ + protected void _sendScheduledArchivingReminders() throws AmetysRepositoryException + { + Long delay = Config.getInstance().getValueAsLong("archive.scheduler.reminder.delay"); + if (delay != null && delay > 0) + { + Calendar calendar = new GregorianCalendar(); + _removeTimeParts(calendar); + calendar.add(Calendar.DAY_OF_MONTH, delay.intValue()); + + // No last date and X days before the scheduled date. + Expression noLastDateExpr = new NotExpression(new MetadataExpression(AlertsConstants.SCHEDULED_ARCHIVING_REMINDER_LAST_DATE, true)); + Expression scheduledDelayExpression = new DateExpression(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE, Operator.LE, calendar.getTime(), true); + Expression expression = new AndExpression(noLastDateExpr, scheduledDelayExpression); + + String query = ContentQueryHelper.getContentXPathQuery(expression); + + try (AmetysObjectIterable contents = _ametysResolver.query(query)) + { + if (getLogger().isInfoEnabled()) + { + getLogger().info("Contents with scheduled archiving reminder today: " + contents.getSize()); + } + + for (ModifiableContent content : contents) + { + _sendScheduledArchivingReminderEmail(content); + + // Set the last scheduled archiving reminder date to now. + ModifiableCompositeMetadata meta = ((ModifiableMetadataAwareVersionableAmetysObject) content).getUnversionedMetadataHolder(); + meta.setMetadata(AlertsConstants.SCHEDULED_ARCHIVING_REMINDER_LAST_DATE, new Date()); + + content.saveChanges(); + } + } + } + } + + /** + * Send a "waiting for validation" e-mail alert to all the users who have the right to validate. + * @param content the content about which to send the alert. + * @throws AmetysRepositoryException if an error occured on the repository + */ + protected void _sendAwaitingValidationEmail(Content content) throws AmetysRepositoryException + { + String context = _getContentContext(content); + + Set users = new HashSet<>(); + for (String right : _awaitingValidationRights) + { + users.addAll(_rightManager.getGrantedUsers(right, context)); + } + + List params = _getAwaitingValidationParams(content); + + I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_awaitingValidationSubject, content), params); + I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_awaitingValidationBody, content), params); + + String subject = _i18nUtils.translate(i18nSubject); + String body = _i18nUtils.translate(i18nBody); + + _sendMails(subject, body, users, _mailFrom); + } + + /** + * Send a "unmodified content" e-mail alert to all the users who have the right to edit. + * @param content the content about which to send the alert. + * @throws AmetysRepositoryException if an error occured on the repository + */ + protected void _sendUnmodifiedContentEmail(Content content) throws AmetysRepositoryException + { + String context = _getContentContext(content); + + Set users = new HashSet<>(); + for (String right : _unmodifiedContentRights) + { + users.addAll(_rightManager.getGrantedUsers(right, context)); + } + + List params = _getUnmodifiedContentParams(content); + + I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_unmodifiedContentSubject, content), params); + I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_unmodifiedContentBody, content), params); + + String subject = _i18nUtils.translate(i18nSubject); + String body = _i18nUtils.translate(i18nBody); + + _sendMails(subject, body, users, _mailFrom); + } + + /** + * Send a "scheduled archiving reminder" e-mail to all the users who have the right to archive. + * @param content the content about which to send the alert. + * @throws AmetysRepositoryException if an error occured on the repository + */ + protected void _sendScheduledArchivingReminderEmail(ModifiableContent content) throws AmetysRepositoryException + { + String context = _getContentContext(content); + + Set users = new HashSet<>(); + for (String right : _scheduledArchivingReminderRights) + { + users.addAll(_rightManager.getGrantedUsers(right, context)); + } + + List params = _getScheduledArchivingReminderParams(content); + + I18nizableText i18nSubject = new I18nizableText(null, getI18nKeyBody(_scheduledArchivingReminderSubject, content), params); + I18nizableText i18nBody = new I18nizableText(null, getI18nKeyBody(_scheduledArchivingReminderBody, content), params); + + String subject = _i18nUtils.translate(i18nSubject); + String body = _i18nUtils.translate(i18nBody); + + _sendMails(subject, body, users, _mailFrom); + } + + /** + * Get the transform i18n body key for specific content + * @param bodyI18nKey the original body key + * @param content the content + * @return the transform i18n body key + */ + protected String getI18nKeyBody(String bodyI18nKey, Content content) + { + return bodyI18nKey; + } + + /** + * Get the mail parameters for instant alert. + * @param content the content. + * @param message the message + * @return the parameters. + */ + protected List _getInstantAlertParams(Content content, String message) + { + List params = new ArrayList<>(); + + params.add(content.getTitle()); // {0} + params.add(_getContentUrl(content)); // {1} + params.add(message); // {2} + + return params; + } + + /** + * Get the mail parameters. + * @param content the content. + * @return the parameters. + */ + protected List _getAwaitingValidationParams(Content content) + { + List params = new ArrayList<>(); + + String delay = Config.getInstance().getValueAsString("remind.content.validation.delay"); + + params.add(content.getTitle()); // {0} + params.add(_getContentUrl(content)); // {1} + params.add(delay); // {2} + + return params; + } + + /** + * Get the mail parameters. + * @param content the content. + * @return the parameters. + */ + protected List _getUnmodifiedContentParams(Content content) + { + List params = new ArrayList<>(); + + String delay = Config.getInstance().getValueAsString("remind.unmodified.content.delay"); + + params.add(content.getTitle()); // {0} + params.add(_getContentUrl(content)); // {1} + params.add(delay); // {2} + + CompositeMetadata meta = ((MetadataAndVersionAwareAmetysObject) content).getUnversionedMetadataHolder(); + String alertText = meta.getString(AlertsConstants.UNMODIFIED_ALERT_TEXT, ""); + params.add(alertText); // {3} + + return params; + } + + /** + * Get the mail parameters. + * @param content the content. + * @return the parameters. + */ + protected List _getScheduledArchivingReminderParams(ModifiableContent content) + { + List params = new ArrayList<>(); + + String delay = Config.getInstance().getValueAsString("archive.scheduler.reminder.delay"); + + params.add(content.getTitle()); // {0} + params.add(_getContentUrl(content)); // {1} + params.add(delay); // {2} + + return params; + } + + /** + * Send the alert emails. + * @param subject the e-mail subject. + * @param body the e-mail body. + * @param users users to send the mail to. + * @param from the address sending the e-mail. + */ + protected void _sendMails(String subject, String body, Set users, String from) + { + for (UserIdentity identity : users) + { + User user = _userManager.getUser(identity.getPopulationId(), identity.getLogin()); + + if (user != null && StringUtils.isNotBlank(user.getEmail())) + { + String mail = user.getEmail(); + + try + { + SendMailHelper.sendMail(subject, null, body, mail, from); + } + catch (MessagingException e) + { + if (getLogger().isWarnEnabled()) + { + getLogger().warn("Could not send an alert e-mail to " + mail, e); + } + } + } + } + } + + /** + * Get the rights context on the given content. + * @param content the content. + * @return the rights context. + * @throws AmetysRepositoryException if an error occured on the repository + */ + protected String _getContentContext(Content content) throws AmetysRepositoryException + { + return "/contents/" + content.getName(); + } + + /** + * Get the URL to the given content tool. + * @param content the content. + * @return the content URL. + */ + protected String _getContentUrl(Content content) + { + StringBuilder url = new StringBuilder(_baseUrl); + url.append("/index.html?uitool=uitool-content,id:%27").append(content.getId()).append("%27"); + + return url.toString(); + } + + /** + * Remove the time parts from a calendar, leaving only date parts. + * @param calendar the calendar. + */ + protected void _removeTimeParts(Calendar calendar) + { + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + } + + /** + * Binary date expression: test on two metadatas. + */ + protected class BinaryExpression implements Expression + { + private MetadataExpression _metadata1; + private MetadataExpression _metadata2; + private Operator _operator; + + /** + * Creates the comparison Expression. + * @param metadata1 the first metadata name. + * @param operator the operator to make the comparison + * @param metadata2 the second metadata name. + */ + public BinaryExpression(String metadata1, Operator operator, String metadata2) + { + _metadata1 = new MetadataExpression(metadata1); + _operator = operator; + _metadata2 = new MetadataExpression(metadata2); + } + + /** + * Creates the comparison Expression. + * @param metadata1 the first metadata name. + * @param unversioned1 true if the first metadata is unversioned. + * @param operator the operator to make the comparison. + * @param metadata2 the second metadata name. + * @param unversioned2 true if the second metadata is unversioned. + */ + public BinaryExpression(String metadata1, boolean unversioned1, Operator operator, String metadata2, boolean unversioned2) + { + _metadata1 = new MetadataExpression(metadata1, unversioned1); + _operator = operator; + _metadata2 = new MetadataExpression(metadata2, unversioned2); + } + + @Override + public String build() + { + return _metadata1.build() + " " + _operator + " " + _metadata2.build(); + } + } +} Index: main/plugin-cms/src/org/ametys/cms/alerts/ContentAlertsClientSideElement.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/alerts/ContentAlertsClientSideElement.java (revision 42093) +++ main/plugin-cms/src/org/ametys/cms/alerts/ContentAlertsClientSideElement.java (working copy) @@ -15,12 +15,12 @@ */ package org.ametys.cms.alerts; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,12 +28,16 @@ import org.apache.avalon.framework.service.ServiceException; import org.apache.avalon.framework.service.ServiceManager; import org.apache.commons.lang.StringUtils; +import org.quartz.JobDataMap; +import org.quartz.JobKey; +import org.quartz.SchedulerException; import org.ametys.cms.lock.LockContentManager; import org.ametys.cms.repository.Content; import org.ametys.cms.repository.ModifiableContent; import org.ametys.core.ui.Callable; import org.ametys.core.ui.StaticClientSideElement; +import org.ametys.plugins.core.schedule.Scheduler; import org.ametys.plugins.repository.AmetysObjectResolver; import org.ametys.plugins.repository.AmetysRepositoryException; import org.ametys.plugins.repository.lock.LockAwareAmetysObject; @@ -50,7 +54,7 @@ public class ContentAlertsClientSideElement extends StaticClientSideElement { /** The date format. */ - protected static final DateFormat _DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + protected static final String __CLIENT_DATE_FORMAT = "yyyy-MM-dd HH:mm"; /** Repository content. */ protected AmetysObjectResolver _resolver; @@ -60,7 +64,13 @@ /** The service manager */ protected ServiceManager _smanager; - + + /** The content alert helper */ + protected ContentAlertHelper _contentAlertHelper; + + /** The scheduler */ + protected Scheduler _scheduler; + @Override public void service(ServiceManager serviceManager) throws ServiceException { @@ -68,6 +78,8 @@ _smanager = serviceManager; _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); _lockManager = (LockContentManager) serviceManager.lookup(LockContentManager.ROLE); + _contentAlertHelper = (ContentAlertHelper) serviceManager.lookup(ContentAlertHelper.ROLE); + _scheduler = (Scheduler) serviceManager.lookup(Scheduler.ROLE); } enum AlertsStatus @@ -86,10 +98,11 @@ * Get information on reminders state. * @param contentsId The list of contents' ids * @return informations on reminders state. + * @throws SchedulerException if an error occurs */ @SuppressWarnings("unchecked") @Callable - public Map getAlertsInformations(List contentsId) + public Map getAlertsInformations(List contentsId) throws SchedulerException { Map results = new HashMap<>(); @@ -135,11 +148,14 @@ unmodifiedAlertStatus = AlertsStatus.MIXED; } - boolean reminderEnabled = meta.getBoolean(AlertsConstants.REMINDER_ENABLED, false); + JobKey jobKey = new JobKey(ReminderContentAlertRunnable.ID_PREFIX + contentId, Scheduler.JOB_GROUP); + boolean reminderEnabled = _scheduler.getScheduler().checkExists(jobKey); if (reminderEnabled) { - String reminderDateStr = meta.getString(AlertsConstants.REMINDER_DATE, ""); - String reminderText = meta.getString(AlertsConstants.REMINDER_TEXT, ""); + JobDataMap jobDataMap = _scheduler.getScheduler().getJobDetail(jobKey).getJobDataMap(); + long reminderDate = jobDataMap.getLong(Scheduler.PARAM_VALUES_PREFIX + ReminderContentAlertRunnable.DATE_KEY); + String reminderDateStr = DateTimeFormatter.ofPattern(__CLIENT_DATE_FORMAT).format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(reminderDate), ZoneId.systemDefault())); + String reminderText = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + ReminderContentAlertRunnable.REMINDER_TEXT_KEY); results.put("reminderDate", reminderDateStr); results.put("reminderText", reminderText); @@ -257,20 +273,14 @@ for (String contentId : contentIds) { Content content = _resolver.resolveById(contentId); - if (content instanceof ModifiableMetadataAwareVersionableAmetysObject) - { - _setAlerts((ModifiableMetadataAwareVersionableAmetysObject) content, params); - } + _setAlerts(content, params); } - String role = params.containsKey("role") ? (String) params.get("role") : AlertScheduler.ROLE; - AlertScheduler alertScheduler = (AlertScheduler) _smanager.lookup(role); - boolean instantAlertEnabled = (Boolean) params.get("instantAlertEnabled"); if (instantAlertEnabled) { String instantAlertText = (String) params.get("instantAlertText"); - alertScheduler.sendInstantAlerts(contentIds, org.apache.commons.lang3.StringUtils.trimToEmpty(instantAlertText)); + _contentAlertHelper.sendInstantAlerts(contentIds, org.apache.commons.lang3.StringUtils.trimToEmpty(instantAlertText)); } } @@ -280,47 +290,40 @@ * @param params the alerts' parameters * @throws AmetysRepositoryException if a repository error occurs. */ - protected void _setAlerts(ModifiableMetadataAwareVersionableAmetysObject content, Map params) throws AmetysRepositoryException + protected void _setAlerts(Content content, Map params) throws AmetysRepositoryException { - ModifiableCompositeMetadata meta = content.getUnversionedMetadataHolder(); - // Set alert for unmodified contents if (params.get("unmodifiedAlertEnabled") != null) { - boolean unmodifiedAlertEnabled = (Boolean) params.get("unmodifiedAlertEnabled"); - String unmodifiedAlertText = (String) params.get("unmodifiedAlertText"); - - meta.setMetadata(AlertsConstants.UNMODIFIED_ALERT_ENABLED, unmodifiedAlertEnabled); - meta.setMetadata(AlertsConstants.UNMODIFIED_ALERT_TEXT, org.apache.commons.lang3.StringUtils.trimToEmpty(unmodifiedAlertText)); + if (content instanceof ModifiableMetadataAwareVersionableAmetysObject) + { + ModifiableCompositeMetadata meta = ((ModifiableMetadataAwareVersionableAmetysObject) content).getUnversionedMetadataHolder(); + boolean unmodifiedAlertEnabled = (Boolean) params.get("unmodifiedAlertEnabled"); + String unmodifiedAlertText = (String) params.get("unmodifiedAlertText"); + + meta.setMetadata(AlertsConstants.UNMODIFIED_ALERT_ENABLED, unmodifiedAlertEnabled); + meta.setMetadata(AlertsConstants.UNMODIFIED_ALERT_TEXT, org.apache.commons.lang3.StringUtils.trimToEmpty(unmodifiedAlertText)); + + ((ModifiableMetadataAwareVersionableAmetysObject) content).saveChanges(); + } } // Set reminders - if (params.get("reminderEnabled") != null) + if (params.get("reminderEnabled") != null && (Boolean) params.get("reminderEnabled")) { - boolean reminderEnabled = (Boolean) params.get("reminderEnabled"); - meta.setMetadata(AlertsConstants.REMINDER_ENABLED, reminderEnabled); - String reminderDateStr = (String) params.get("reminderDate"); String reminderText = (String) params.get("reminderText"); - meta.setMetadata(AlertsConstants.REMINDER_TEXT, org.apache.commons.lang3.StringUtils.trimToEmpty(reminderText)); - // Parse the reminder date. - Date reminderDate = null; - try + if (StringUtils.isNotBlank(reminderDateStr)) { - if (StringUtils.isNotBlank(reminderDateStr)) - { - reminderDate = _DATE_FORMAT.parse(reminderDateStr); - meta.setMetadata(AlertsConstants.REMINDER_DATE, reminderDate); - } - } - catch (ParseException e) - { - // Ignore + ZonedDateTime reminderDate = DateTimeFormatter.ofPattern(__CLIENT_DATE_FORMAT).withZone(ZoneId.systemDefault()).parse(reminderDateStr, ZonedDateTime::from); + _contentAlertHelper.scheduleReminderEmail(content, reminderText, reminderDate); } } - - content.saveChanges(); + else + { + _contentAlertHelper.unscheduleReminderEmail(content); + } } @Override Index: main/plugin-cms/src/org/ametys/cms/alerts/InstantContentAlertRunnable.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/alerts/InstantContentAlertRunnable.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/alerts/InstantContentAlertRunnable.java (revision 0) @@ -0,0 +1,145 @@ +/* + * Copyright 2016 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.alerts; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.ametys.core.schedule.Runnable; +import org.ametys.plugins.core.impl.schedule.SendMailSchedulable; +import org.ametys.runtime.i18n.I18nizableText; + +/** + * A {@link Runnable} which schedules a {@link SendMailSchedulable} for sending instantly an alert email about a content. + */ +public class InstantContentAlertRunnable implements Runnable +{ + /** The content id */ + protected String _contentId; + /** The content name */ + protected String _contentName; + /** The sender of the mail */ + protected String _sender; + /** The recipients of the mail (separated by a newline character)*/ + protected String _recipients; + /** The subject of the mail */ + protected String _subject; + /** The body of the mail */ + protected String _body; + + /** + * Constructor + * @param contentId The id of the contenet + * @param contentName + * @param sender The sender + * @param recipients The recipients + * @param subject The subject + * @param body The body + */ + public InstantContentAlertRunnable(String contentId, String contentName, String sender, String recipients, String subject, String body) + { + _contentId = contentId; + _contentName = contentName; + _sender = sender; + _recipients = recipients; + _subject = subject; + _body = body; + } + + @Override + public String getId() + { + return this.getClass().getName() + "." + _contentId; + } + + @Override + public I18nizableText getLabel() + { + return new I18nizableText("plugin.cms", "PLUGINS_CMS_INSTANT_CONTENT_ALERT_RUNNABLE_LABEL", Collections.singletonList(_contentName)); + } + + @Override + public I18nizableText getDescription() + { + List parameters = new ArrayList<>(); + parameters.add(_contentId); + parameters.add(_contentName); + return new I18nizableText("plugin.cms", "PLUGINS_CMS_INSTANT_CONTENT_ALERT_RUNNABLE_DESCRIPTION", parameters); + } + + @Override + public FireProcess getFireProcess() + { + return FireProcess.NOW; + } + + @Override + public String getCronExpression() + { + return null; + } + + @Override + public String getSchedulableId() + { + return "org.ametys.core.schedule.SendMail"; + } + + @Override + public boolean isRemovable() + { + return false; + } + + @Override + public boolean isModifiable() + { + return false; + } + + @Override + public boolean isDeactivatable() + { + return false; + } + + @Override + public MisfirePolicy getMisfirePolicy() + { + return MisfirePolicy.FIRE_ONCE; + } + + @Override + public boolean isVolatile() + { + return false; + } + + @Override + public Map getParameterValues() + { + Map values = new HashMap<>(); + values.put(SendMailSchedulable.SENDER_KEY, _sender); + values.put(SendMailSchedulable.RECIPIENTS_KEY, _recipients); + values.put(SendMailSchedulable.SUBJECT_KEY, _subject); + values.put(SendMailSchedulable.BODY_KEY, _body); + values.put(SendMailSchedulable.IS_HTML_BODY_KEY, false); + return values; + } +} Index: main/plugin-cms/src/org/ametys/cms/alerts/ReminderContentAlertRunnable.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/alerts/ReminderContentAlertRunnable.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/alerts/ReminderContentAlertRunnable.java (revision 0) @@ -0,0 +1,165 @@ +/* + * Copyright 2016 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.alerts; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.ametys.core.schedule.Runnable; +import org.ametys.plugins.core.impl.schedule.SendMailSchedulable; +import org.ametys.runtime.i18n.I18nizableText; + +/** + * A {@link Runnable} which schedules a {@link SendMailSchedulable} for sending a reminder alert email about a content. + */ +public class ReminderContentAlertRunnable implements Runnable +{ + /** The prefix for the id of this kinf of Runnable. The full id is the concatenation of this prefix and the content id */ + public static final String ID_PREFIX = ReminderContentAlertRunnable.class.getName() + "."; + /** The key for the date */ + public static final String DATE_KEY = "date"; + /** The key for the reminder text */ + public static final String REMINDER_TEXT_KEY = "reminderText"; + + /** The content id */ + protected String _contentId; + /** The content name */ + protected String _contentName; + /** The sender of the mail */ + protected String _sender; + /** The recipients of the mail (separated by a newline character)*/ + protected String _recipients; + /** The subject of the mail */ + protected String _subject; + /** The body of the mail */ + protected String _body; + /** The date when to send the mail */ + protected ZonedDateTime _date; + /** The reminder text */ + protected String _reminderText; + + /** + * Constructor + * @param contentId The id of the contenet + * @param contentName + * @param sender The sender + * @param recipients The recipients + * @param subject The subject + * @param body The body + * @param date The date when to send the mail + * @param reminderText The reminder text + */ + public ReminderContentAlertRunnable(String contentId, String contentName, String sender, String recipients, String subject, String body, ZonedDateTime date, String reminderText) + { + _contentId = contentId; + _contentName = contentName; + _sender = sender; + _recipients = recipients; + _subject = subject; + _body = body; + _date = date; + _reminderText = reminderText; + } + + @Override + public String getId() + { + return ID_PREFIX + _contentId; + } + + @Override + public I18nizableText getLabel() + { + return new I18nizableText("plugin.cms", "PLUGINS_CMS_REMINDER_CONTENT_ALERT_RUNNABLE_LABEL", Collections.singletonList(_contentName)); + } + + @Override + public I18nizableText getDescription() + { + List parameters = new ArrayList<>(); + parameters.add(_contentId); + parameters.add(_contentName); + return new I18nizableText("plugin.cms", "PLUGINS_CMS_REMINDER_CONTENT_ALERT_RUNNABLE_DESCRIPTION", parameters); + } + + @Override + public FireProcess getFireProcess() + { + return FireProcess.CRON; + } + + @Override + public String getCronExpression() + { + return _date.getSecond() + " " + _date.getMinute() + " " + _date.getHour() + " " + _date.getDayOfMonth() + " " + _date.getMonthValue() + " ? " + _date.getYear(); + } + + @Override + public String getSchedulableId() + { + return "org.ametys.core.schedule.SendMail"; + } + + @Override + public boolean isRemovable() + { + return false; + } + + @Override + public boolean isModifiable() + { + return false; + } + + @Override + public boolean isDeactivatable() + { + return false; + } + + @Override + public MisfirePolicy getMisfirePolicy() + { + return MisfirePolicy.FIRE_ONCE; + } + + @Override + public boolean isVolatile() + { + return false; + } + + @Override + public Map getParameterValues() + { + Map values = new HashMap<>(); + // Schedulable parameters + values.put(SendMailSchedulable.SENDER_KEY, _sender); + values.put(SendMailSchedulable.RECIPIENTS_KEY, _recipients); + values.put(SendMailSchedulable.SUBJECT_KEY, _subject); + values.put(SendMailSchedulable.BODY_KEY, _body); + values.put(SendMailSchedulable.IS_HTML_BODY_KEY, false); + // Additional parameters + values.put(DATE_KEY, _date.toInstant().toEpochMilli()); + values.put(REMINDER_TEXT_KEY, _reminderText); + return values; + } +} Index: main/plugin-cms/src/org/ametys/cms/alerts/UnmodifiedContentAlertRunnable.java =================================================================== --- main/plugin-cms/src/org/ametys/cms/alerts/UnmodifiedContentAlertRunnable.java (revision 0) +++ main/plugin-cms/src/org/ametys/cms/alerts/UnmodifiedContentAlertRunnable.java (revision 0) @@ -0,0 +1,151 @@ +/* + * Copyright 2016 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.cms.alerts; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.ametys.core.schedule.Runnable; +import org.ametys.plugins.core.impl.schedule.SendMailSchedulable; +import org.ametys.runtime.i18n.I18nizableText; + +/** + * A {@link Runnable} which schedules a {@link SendMailSchedulable} for sending an alert email about a content which is unmodified for a certain number of days. + */ +public class UnmodifiedContentAlertRunnable implements Runnable +{ + /** The content id */ + protected String _contentId; + /** The content name */ + protected String _contentName; + /** The sender of the mail */ + protected String _sender; + /** The recipients of the mail (separated by a newline character)*/ + protected String _recipients; + /** The subject of the mail */ + protected String _subject; + /** The body of the mail */ + protected String _body; + /** The date when to send the mail */ + protected ZonedDateTime _date; + + /** + * Constructor + * @param contentId The id of the contenet + * @param contentName + * @param sender The sender + * @param recipients The recipients + * @param subject The subject + * @param body The body + * @param date The date when to send the mail + */ + public UnmodifiedContentAlertRunnable(String contentId, String contentName, String sender, String recipients, String subject, String body, ZonedDateTime date) + { + _contentId = contentId; + _contentName = contentName; + _sender = sender; + _recipients = recipients; + _subject = subject; + _body = body; + _date = date; + } + + @Override + public String getId() + { + return UnmodifiedContentAlertRunnable.class.getName() + "." + _contentId; + } + + @Override + public I18nizableText getLabel() + { + return new I18nizableText("plugin.cms", "PLUGINS_CMS_UNMODIFIED_CONTENT_ALERT_RUNNABLE_LABEL", Collections.singletonList(_contentName)); + } + + @Override + public I18nizableText getDescription() + { + List parameters = new ArrayList<>(); + parameters.add(_contentId); + parameters.add(_contentName); + return new I18nizableText("plugin.cms", "PLUGINS_CMS_UNMODIFIED_CONTENT_ALERT_RUNNABLE_DESCRIPTION", parameters); + } + + @Override + public FireProcess getFireProcess() + { + return FireProcess.CRON; + } + + @Override + public String getCronExpression() + { + return _date.getSecond() + " " + _date.getMinute() + " " + _date.getHour() + " " + _date.getDayOfMonth() + " " + _date.getMonthValue() + " ? " + _date.getYear(); + } + + @Override + public String getSchedulableId() + { + return "org.ametys.core.schedule.SendMail"; + } + + @Override + public boolean isRemovable() + { + return false; + } + + @Override + public boolean isModifiable() + { + return false; + } + + @Override + public boolean isDeactivatable() + { + return false; + } + + @Override + public MisfirePolicy getMisfirePolicy() + { + return MisfirePolicy.FIRE_ONCE; + } + + @Override + public boolean isVolatile() + { + return false; + } + + @Override + public Map getParameterValues() + { + Map values = new HashMap<>(); + values.put(SendMailSchedulable.SENDER_KEY, _sender); + values.put(SendMailSchedulable.RECIPIENTS_KEY, _recipients); + values.put(SendMailSchedulable.SUBJECT_KEY, _subject); + values.put(SendMailSchedulable.BODY_KEY, _body); + values.put(SendMailSchedulable.IS_HTML_BODY_KEY, false); + return values; + } + +} #P Ametys - 08 WEB Index: main/plugin-web/src/org/ametys/web/alerts/AlertEngine.java =================================================================== --- main/plugin-web/src/org/ametys/web/alerts/AlertEngine.java (revision 42093) +++ main/plugin-web/src/org/ametys/web/alerts/AlertEngine.java (working copy) @@ -1,401 +0,0 @@ -/* - * Copyright 2010 Anyware Services - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.ametys.web.alerts; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -import org.apache.avalon.framework.configuration.Configuration; -import org.apache.avalon.framework.configuration.ConfigurationException; -import org.apache.cocoon.components.ContextHelper; -import org.apache.cocoon.environment.Request; - -import org.ametys.cms.repository.Content; -import org.ametys.cms.repository.ModifiableContent; -import org.ametys.core.user.UserIdentity; -import org.ametys.plugins.repository.AmetysObjectIterable; -import org.ametys.plugins.repository.AmetysRepositoryException; -import org.ametys.plugins.repository.query.expression.DateExpression; -import org.ametys.plugins.repository.query.expression.Expression; -import org.ametys.plugins.repository.query.expression.Expression.Operator; -import org.ametys.runtime.config.Config; -import org.ametys.runtime.i18n.I18nizableText; -import org.ametys.web.repository.content.WebContent; -import org.ametys.web.repository.page.Page; -import org.ametys.web.repository.page.PageQueryHelper; -import org.ametys.web.repository.page.jcr.DefaultPage; - -/** - * Alerts engine: sends alerts mail. - * This is the web version of the engine: it sets the currently processed content's - * site name in the request object, and sends additional site and page information - * in the alerts e-mails. - */ -public class AlertEngine extends org.ametys.cms.alerts.AlertEngine -{ - /** True if the alert of page publication ending is enabled */ - protected boolean _pagePublicationEnabled; - - /** The "page nearing end of publication" e-mail will be sent to users that have this right. */ - protected Set _pagePublicationEndRights; - - /** The "page nearing end of publication" e-mail subject i18n key. */ - protected String _pagePublicationEndSubject; - - /** The "page nearing end of publication" e-mail body i18n key. */ - protected String _pagePublicationEndBody; - - /** - * Default constructor - */ - public AlertEngine() - { - super(); - } - - /** - * Constructor used to send instant alerts - * @param contentIds The content's id - * @param message the message - */ - public AlertEngine (List contentIds, String message) - { - super(contentIds, message); - } - - @Override - public void configure(Configuration configuration) throws ConfigurationException - { - super.configure(configuration); - - Configuration pagePublicationEndConf = configuration.getChild("pagePublicationEnd", false); - - if (pagePublicationEndConf != null) - { - _pagePublicationEnabled = true; - _pagePublicationEndRights = _getRightsFromConf(pagePublicationEndConf); - _pagePublicationEndSubject = pagePublicationEndConf.getChild("subjectKey").getValue(); - _pagePublicationEndBody = pagePublicationEndConf.getChild("bodyKey").getValue(); - } - } - - @Override - protected void _sendAlerts() throws AmetysRepositoryException - { - // Send the content alerts. - super._sendAlerts(); - - if (!_instantMode && _pagePublicationEnabled) - { - // Send the page alerts. - _sendPagePublicationEndAlerts(); - } - - } - - /** - * Send the "page nearing end of publication" alerts. - * @throws AmetysRepositoryException if an error occurs. - */ - protected void _sendPagePublicationEndAlerts() throws AmetysRepositoryException - { - Long delay = Config.getInstance().getValueAsLong("remind.before.publication.end"); - if (delay != null && delay > 0) - { - Calendar calendar = new GregorianCalendar(); - _removeTimeParts(calendar); - calendar.add(Calendar.DAY_OF_MONTH, delay.intValue()); - - // Get all the pages which publication end is in X days. - Expression nearPublicationEndExpr = new DateExpression(DefaultPage.METADATA_PUBLICATION_END_DATE, Operator.EQ, calendar.getTime()); - - String query = PageQueryHelper.getPageXPathQuery(null, null, null, nearPublicationEndExpr, null); - - try (AmetysObjectIterable pages = _ametysResolver.query(query)) - { - if (_LOGGER.isInfoEnabled()) - { - _LOGGER.info("Pages within " + Long.toString(delay) + " days of publication end: " + pages.getSize()); - } - - for (Page page : pages) - { - // Send the alert e-mails. - _sendPagePublicationEndEmail(page); - } - } - } - } - - @Override - protected void _sendInstantAlertEmail(Content content, String message) throws AmetysRepositoryException - { - setSiteNameInRequestAttribute(content); - super._sendInstantAlertEmail(content, message); - } - - @Override - protected void _sendAwaitingValidationEmail(Content content) throws AmetysRepositoryException - { - setSiteNameInRequestAttribute(content); - super._sendAwaitingValidationEmail(content); - } - - @Override - protected void _sendUnmodifiedContentEmail(Content content) throws AmetysRepositoryException - { - setSiteNameInRequestAttribute(content); - super._sendUnmodifiedContentEmail(content); - } - - @Override - protected void _sendReminderEmail(Content content) throws AmetysRepositoryException - { - setSiteNameInRequestAttribute(content); - super._sendReminderEmail(content); - } - - /** - * Set the site name in request attributes if found - * @param content the content - */ - protected void setSiteNameInRequestAttribute (Content content) - { - // Set the site name into the request for the other components to be able to retrieve it. - if (content instanceof WebContent) - { - Request request = ContextHelper.getRequest(_context); - request.setAttribute("siteName", ((WebContent) content).getSiteName()); - } - } - - @Override - protected void _sendScheduledArchivingReminderEmail(ModifiableContent content) throws AmetysRepositoryException - { - // Set the site name into the request for the other components to be able to retrieve it. - if (content instanceof WebContent) - { - Request request = ContextHelper.getRequest(_context); - request.setAttribute("siteName", ((WebContent) content).getSiteName()); - } - - super._sendScheduledArchivingReminderEmail(content); - } - - /** - * Send the "page publication end" alert e-mail. - * @param page the page nearing publication end. - * @throws AmetysRepositoryException if an error occurs. - */ - protected void _sendPagePublicationEndEmail(Page page) throws AmetysRepositoryException - { - // Set the site name into the request for the other components to be able to retrieve it. - Request request = ContextHelper.getRequest(_context); - request.setAttribute("siteName", page.getSiteName()); - - String context = _getPageContext(page); - - Set users = new HashSet<>(); - for (String right : _pagePublicationEndRights) - { - users.addAll(_rightsManager.getGrantedUsers(right, context)); - } - - List params = _getPagePublicationEndParams(page); - - I18nizableText i18nSubject = new I18nizableText(null, _pagePublicationEndSubject, params); - I18nizableText i18nBody = new I18nizableText(null, _pagePublicationEndBody, params); - - String subject = _i18nUtils.translate(i18nSubject); - String body = _i18nUtils.translate(i18nBody); - - _sendMails(subject, body, users, _mailFrom); - } - - @Override - protected String getI18nKeyBody(String bodyI18nKey, Content content) - { - String i18nKey = bodyI18nKey; - - if (content instanceof WebContent) - { - WebContent webContent = (WebContent) content; - if (webContent.getReferencingPages().size() == 0) - { - // Orphan content - i18nKey += "_ORPHAN"; - } - } - else - { - i18nKey += "_NOSITE"; - } - - return i18nKey; - } - - /** - * Get the additional i18n parameters for a web content. - * Checks if the content is a Web content. If true, add the site and page title. - * @param content the content - * @return the additional i18n parameters - */ - protected List _getAdditionalParamsForWebContent (Content content) - { - List params = new ArrayList<>(); - - if (content instanceof WebContent) - { - WebContent webContent = (WebContent) content; - - params.add(webContent.getSite().getTitle()); // {4} - - Iterator pages = webContent.getReferencingPages().iterator(); - if (pages.hasNext()) - { - Page page = pages.next(); - params.add(page.getTitle()); // {5} - } - } - - return params; - } - - @Override - protected List _getInstantAlertParams(Content content, String message) - { - List params = super._getInstantAlertParams(content, message); - params.addAll(_getAdditionalParamsForWebContent(content)); - return params; - } - - @Override - protected List _getUnmodifiedContentParams(Content content) - { - List params = super._getUnmodifiedContentParams(content); - params.addAll(_getAdditionalParamsForWebContent(content)); - return params; - } - - @Override - protected List _getAwaitingValidationParams(Content content) - { - List params = super._getAwaitingValidationParams(content); - params.addAll(_getAdditionalParamsForWebContent(content)); - return params; - } - - @Override - protected List _getReminderParams(Content content) - { - List params = super._getReminderParams(content); - params.addAll(_getAdditionalParamsForWebContent(content)); - return params; - } - - @Override - protected List _getScheduledArchivingReminderParams(ModifiableContent content) - { - List params = super._getScheduledArchivingReminderParams(content); - params.addAll(_getAdditionalParamsForWebContent(content)); - return params; - } - - /** - * Get the "page nearing end of publication" mail parameters. - * @param page the page. - * @return the mail parameters. - */ - protected List _getPagePublicationEndParams(Page page) - { - List params = new ArrayList<>(); - - String siteTitle = page.getSite().getTitle(); - String delay = Config.getInstance().getValueAsString("remind.before.publication.end"); - - params.add(page.getTitle()); - params.add(_getPageUrl(page)); - params.add(delay); - params.add(siteTitle); - - return params; - } - - /** - * Get the page rights context. - * @param page the page. - * @return the page context. - * @throws AmetysRepositoryException if an error occurs. - */ - protected String _getPageContext(Page page) throws AmetysRepositoryException - { - return "/pages/" + page.getSitemapName() + "/" + page.getPathInSitemap(); - } - - @Override - protected String _getContentUrl(Content content) - { - if (content instanceof WebContent) - { - WebContent webContent = (WebContent) content; - - StringBuilder url = new StringBuilder(_baseUrl); - if (!_baseUrl.endsWith("/")) - { - url.append('/'); - } - - Iterator pages = webContent.getReferencingPages().iterator(); - if (pages.hasNext()) - { - Page page = pages.next(); - url.append(webContent.getSite().getName()).append("/index.html?uitool=uitool-page,id:%27").append(page.getId()).append("%27"); - } - else - { - url.append(webContent.getSite().getName()).append("/index.html?uitool=uitool-content,id:%27").append(content.getId()).append("%27"); - } - - return url.toString(); - } - else - { - return super._getContentUrl(content); - } - } - - /** - * Get the URL of the back-office, opening on the page tool. - * @param page the page to link to. - * @return the page URL. - */ - protected String _getPageUrl(Page page) - { - StringBuilder url = new StringBuilder(_baseUrl); - if (!_baseUrl.endsWith("/")) - { - url.append('/'); - } - url.append(page.getSite().getName()).append("/index.html?uitool=uitool-page,id:%27").append(page.getId()).append("%27"); - return url.toString(); - } - -} Index: main/plugin-web/src/org/ametys/web/alerts/AlertScheduler.java =================================================================== --- main/plugin-web/src/org/ametys/web/alerts/AlertScheduler.java (revision 42093) +++ main/plugin-web/src/org/ametys/web/alerts/AlertScheduler.java (working copy) @@ -1,67 +0,0 @@ -/* - * Copyright 2010 Anyware Services - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.ametys.web.alerts; - -import java.util.List; - -/** - * Alerts scheduler: launches a cron which sends the alerts each night. - * This scheduler is the same as the CMS' AlertScheduler, except it runs the web version of AlertEngine. - */ -public class AlertScheduler extends org.ametys.cms.alerts.AlertScheduler -{ - @Override - public void run() - { - AlertEngine alertEngine = new AlertEngine(); - - try - { - // Initialize and configure the engine. - alertEngine.initialize(_manager, _context); - alertEngine.configure(_configuration); - } - catch (Exception e) - { - throw new RuntimeException("Unable to initialize the alerts engine", e); - } - - // The thread will be started as daemon, as the scheduler is marked daemon itself. - new Thread(alertEngine, "AlertEngine").start(); - } - - @Override - public void sendInstantAlerts(List contentIds, String message) - { - AlertEngine alertEngine = new AlertEngine(contentIds, message); - - try - { - // Initialize and configure the engine. - alertEngine.initialize(_manager, _context); - alertEngine.configure(_configuration); - } - catch (Exception e) - { - throw new RuntimeException("Unable to initialize the alerts engine", e); - } - - // The thread will be started as daemon, as the scheduler is marked daemon itself. - new Thread(alertEngine, "AlertEngine").start(); - } - -} Index: main/plugin-web/src/org/ametys/web/alerts/ContentAlertHelper.java =================================================================== --- main/plugin-web/src/org/ametys/web/alerts/ContentAlertHelper.java (revision 0) +++ main/plugin-web/src/org/ametys/web/alerts/ContentAlertHelper.java (revision 0) @@ -0,0 +1,375 @@ +/* + * Copyright 2016 Anyware Services + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.ametys.web.alerts; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.apache.avalon.framework.configuration.Configuration; +import org.apache.avalon.framework.configuration.ConfigurationException; +import org.apache.cocoon.components.ContextHelper; +import org.apache.cocoon.environment.Request; + +import org.ametys.cms.repository.Content; +import org.ametys.cms.repository.ModifiableContent; +import org.ametys.core.user.UserIdentity; +import org.ametys.plugins.repository.AmetysObjectIterable; +import org.ametys.plugins.repository.AmetysRepositoryException; +import org.ametys.plugins.repository.query.expression.DateExpression; +import org.ametys.plugins.repository.query.expression.Expression; +import org.ametys.plugins.repository.query.expression.Expression.Operator; +import org.ametys.runtime.config.Config; +import org.ametys.runtime.i18n.I18nizableText; +import org.ametys.web.repository.content.WebContent; +import org.ametys.web.repository.page.Page; +import org.ametys.web.repository.page.PageQueryHelper; +import org.ametys.web.repository.page.jcr.DefaultPage; + +/** + * Helper for web content alerts. + */ +public class ContentAlertHelper extends org.ametys.cms.alerts.ContentAlertHelper +{ + /** True if the alert of page publication ending is enabled */ + protected boolean _pagePublicationEnabled; + /** The "page nearing end of publication" e-mail will be sent to users that have this right. */ + protected Set _pagePublicationEndRights; + /** The "page nearing end of publication" e-mail subject i18n key. */ + protected String _pagePublicationEndSubject; + /** The "page nearing end of publication" e-mail body i18n key. */ + protected String _pagePublicationEndBody; + + @Override + public void configure(Configuration configuration) throws ConfigurationException + { + super.configure(configuration); + + Configuration pagePublicationEndConf = configuration.getChild("pagePublicationEnd", false); + + if (pagePublicationEndConf != null) + { + _pagePublicationEnabled = true; + _pagePublicationEndRights = _getRightsFromConf(pagePublicationEndConf); + _pagePublicationEndSubject = pagePublicationEndConf.getChild("subjectKey").getValue(); + _pagePublicationEndBody = pagePublicationEndConf.getChild("bodyKey").getValue(); + } + } + + @Override + public void sendAlerts() throws AmetysRepositoryException + { + // Send the content alerts. + super.sendAlerts(); + + if (_pagePublicationEnabled) + { + // Send the page alerts. + _sendPagePublicationEndAlerts(); + } + } + + /** + * Send the "page nearing end of publication" alerts. + * @throws AmetysRepositoryException if an error occurs. + */ + protected void _sendPagePublicationEndAlerts() throws AmetysRepositoryException + { + Long delay = Config.getInstance().getValueAsLong("remind.before.publication.end"); + if (delay != null && delay > 0) + { + Calendar calendar = new GregorianCalendar(); + _removeTimeParts(calendar); + calendar.add(Calendar.DAY_OF_MONTH, delay.intValue()); + + // Get all the pages which publication end is in X days. + Expression nearPublicationEndExpr = new DateExpression(DefaultPage.METADATA_PUBLICATION_END_DATE, Operator.EQ, calendar.getTime()); + + String query = PageQueryHelper.getPageXPathQuery(null, null, null, nearPublicationEndExpr, null); + + try (AmetysObjectIterable pages = _ametysResolver.query(query)) + { + getLogger().info("Pages within {} days of publication end: {}", Long.toString(delay), pages.getSize()); + + for (Page page : pages) + { + // Send the alert e-mails. + _sendPagePublicationEndEmail(page); + } + } + } + } + + + @Override + protected void _scheduleInstantAlertEmail(Content content, String message) throws AmetysRepositoryException + { + setSiteNameInRequestAttribute(content); + super._scheduleInstantAlertEmail(content, message); + } + + @Override + protected void _sendAwaitingValidationEmail(Content content) throws AmetysRepositoryException + { + setSiteNameInRequestAttribute(content); + super._sendAwaitingValidationEmail(content); + } + + @Override + protected void _sendUnmodifiedContentEmail(Content content) throws AmetysRepositoryException + { + setSiteNameInRequestAttribute(content); + super._sendUnmodifiedContentEmail(content); + } + + @Override + public void scheduleReminderEmail(Content content, String reminderText, ZonedDateTime date) + { + setSiteNameInRequestAttribute(content); + super.scheduleReminderEmail(content, reminderText, date); + } + + + /** + * Set the site name in request attributes if found + * @param content the content + */ + protected void setSiteNameInRequestAttribute (Content content) + { + // Set the site name into the request for the other components to be able to retrieve it. + if (content instanceof WebContent) + { + Request request = ContextHelper.getRequest(_context); + request.setAttribute("siteName", ((WebContent) content).getSiteName()); + } + } + + @Override + protected void _sendScheduledArchivingReminderEmail(ModifiableContent content) throws AmetysRepositoryException + { + // Set the site name into the request for the other components to be able to retrieve it. + if (content instanceof WebContent) + { + Request request = ContextHelper.getRequest(_context); + request.setAttribute("siteName", ((WebContent) content).getSiteName()); + } + + super._sendScheduledArchivingReminderEmail(content); + } + + /** + * Send the "page publication end" alert e-mail. + * @param page the page nearing publication end. + * @throws AmetysRepositoryException if an error occurs. + */ + protected void _sendPagePublicationEndEmail(Page page) throws AmetysRepositoryException + { + // Set the site name into the request for the other components to be able to retrieve it. + Request request = ContextHelper.getRequest(_context); + request.setAttribute("siteName", page.getSiteName()); + + String context = _getPageContext(page); + + Set users = new HashSet<>(); + for (String right : _pagePublicationEndRights) + { + users.addAll(_rightManager.getGrantedUsers(right, context)); + } + + List params = _getPagePublicationEndParams(page); + + I18nizableText i18nSubject = new I18nizableText(null, _pagePublicationEndSubject, params); + I18nizableText i18nBody = new I18nizableText(null, _pagePublicationEndBody, params); + + String subject = _i18nUtils.translate(i18nSubject); + String body = _i18nUtils.translate(i18nBody); + + _sendMails(subject, body, users, _mailFrom); + } + + @Override + protected String getI18nKeyBody(String bodyI18nKey, Content content) + { + String i18nKey = bodyI18nKey; + + if (content instanceof WebContent) + { + WebContent webContent = (WebContent) content; + if (webContent.getReferencingPages().size() == 0) + { + // Orphan content + i18nKey += "_ORPHAN"; + } + } + else + { + i18nKey += "_NOSITE"; + } + + return i18nKey; + } + + /** + * Get the additional i18n parameters for a web content. + * Checks if the content is a Web content. If true, add the site and page title. + * @param content the content + * @return the additional i18n parameters + */ + protected List _getAdditionalParamsForWebContent (Content content) + { + List params = new ArrayList<>(); + + if (content instanceof WebContent) + { + WebContent webContent = (WebContent) content; + + params.add(webContent.getSite().getTitle()); // {4} + + Iterator pages = webContent.getReferencingPages().iterator(); + if (pages.hasNext()) + { + Page page = pages.next(); + params.add(page.getTitle()); // {5} + } + } + + return params; + } + + @Override + protected List _getInstantAlertParams(Content content, String message) + { + List params = super._getInstantAlertParams(content, message); + params.addAll(_getAdditionalParamsForWebContent(content)); + return params; + } + + @Override + protected List _getUnmodifiedContentParams(Content content) + { + List params = super._getUnmodifiedContentParams(content); + params.addAll(_getAdditionalParamsForWebContent(content)); + return params; + } + + @Override + protected List _getAwaitingValidationParams(Content content) + { + List params = super._getAwaitingValidationParams(content); + params.addAll(_getAdditionalParamsForWebContent(content)); + return params; + } + + @Override + protected List _getReminderParams(Content content, String reminderText) + { + List params = super._getReminderParams(content, reminderText); + params.addAll(_getAdditionalParamsForWebContent(content)); + return params; + } + + @Override + protected List _getScheduledArchivingReminderParams(ModifiableContent content) + { + List params = super._getScheduledArchivingReminderParams(content); + params.addAll(_getAdditionalParamsForWebContent(content)); + return params; + } + + /** + * Get the "page nearing end of publication" mail parameters. + * @param page the page. + * @return the mail parameters. + */ + protected List _getPagePublicationEndParams(Page page) + { + List params = new ArrayList<>(); + + String siteTitle = page.getSite().getTitle(); + String delay = Config.getInstance().getValueAsString("remind.before.publication.end"); + + params.add(page.getTitle()); + params.add(_getPageUrl(page)); + params.add(delay); + params.add(siteTitle); + + return params; + } + + /** + * Get the page rights context. + * @param page the page. + * @return the page context. + * @throws AmetysRepositoryException if an error occurs. + */ + protected String _getPageContext(Page page) throws AmetysRepositoryException + { + return "/pages/" + page.getSitemapName() + "/" + page.getPathInSitemap(); + } + + @Override + protected String _getContentUrl(Content content) + { + if (content instanceof WebContent) + { + WebContent webContent = (WebContent) content; + + StringBuilder url = new StringBuilder(_baseUrl); + if (!_baseUrl.endsWith("/")) + { + url.append('/'); + } + + Iterator pages = webContent.getReferencingPages().iterator(); + if (pages.hasNext()) + { + Page page = pages.next(); + url.append(webContent.getSite().getName()).append("/index.html?uitool=uitool-page,id:%27").append(page.getId()).append("%27"); + } + else + { + url.append(webContent.getSite().getName()).append("/index.html?uitool=uitool-content,id:%27").append(content.getId()).append("%27"); + } + + return url.toString(); + } + else + { + return super._getContentUrl(content); + } + } + + /** + * Get the URL of the back-office, opening on the page tool. + * @param page the page to link to. + * @return the page URL. + */ + protected String _getPageUrl(Page page) + { + StringBuilder url = new StringBuilder(_baseUrl); + if (!_baseUrl.endsWith("/")) + { + url.append('/'); + } + url.append(page.getSite().getName()).append("/index.html?uitool=uitool-page,id:%27").append(page.getId()).append("%27"); + return url.toString(); + } +} #P Ametys - Template CMS Index: webapp/plugins/default-workflow/plugin.xml =================================================================== --- webapp/plugins/default-workflow/plugin.xml (revision 42093) +++ webapp/plugins/default-workflow/plugin.xml (working copy) @@ -269,4 +269,63 @@ + + + + + Workflow_Rights_Edition_Online + plugin.cms:CONTENT_ALERTS_MAIL_REMINDER_SUBJECT + plugin.cms:CONTENT_ALERTS_MAIL_REMINDER_BODY + + + Workflow_Rights_Validate + plugin.cms:CONTENT_ALERTS_MAIL_AWAITING_VALIDATION_SUBJECT + plugin.cms:CONTENT_ALERTS_MAIL_AWAITING_VALIDATION_BODY + + + 1 + 2 + 3 + Workflow_Rights_Edition_Online + plugin.cms:CONTENT_ALERTS_MAIL_UNMODIFIED_CONTENT_SUBJECT + plugin.cms:CONTENT_ALERTS_MAIL_UNMODIFIED_CONTENT_BODY + + + Workflow_Rights_Edition_Online + plugin.cms:CONTENT_ALERTS_MAIL_REMINDER_SUBJECT + plugin.cms:CONTENT_ALERTS_MAIL_REMINDER_BODY + + + Workflow_Rights_Archive + plugin.cms:CONTENT_ALERTS_MAIL_SCHEDULED_ARCHIVING_REMINDER_SUBJECT + plugin.cms:CONTENT_ALERTS_MAIL_SCHEDULED_ARCHIVING_REMINDER_BODY + + + + + + + + plugin.cms:PLUGINS_CMS_CONTENT_ALERT_ENGINE_SCHEDULABLE_DESCRIPTION + flaticon-envelope14 + true + + + + + plugin.cms:PLUGINS_CMS_CONTENT_ALERT_ENGINE_RUNNABLE_DESCRIPTION + org.ametys.cms.alerts.AlertEngineSchedulable + false + false + false + fire_once + + + #P Ametys - Template CMSWEB Index: webapp/cms/plugins/default-workflow/plugin.xml =================================================================== --- webapp/cms/plugins/default-workflow/plugin.xml (revision 42093) +++ webapp/cms/plugins/default-workflow/plugin.xml (working copy) @@ -350,11 +350,10 @@ - - + + Workflow_Rights_Edition_Online plugin.web:CONTENT_ALERTS_MAIL_REMINDER_SUBJECT @@ -390,5 +389,28 @@ + + + + + plugin.cms:PLUGINS_CMS_CONTENT_ALERT_ENGINE_SCHEDULABLE_DESCRIPTION + flaticon-envelope14 + true + + + + + plugin.cms:PLUGINS_CMS_CONTENT_ALERT_ENGINE_RUNNABLE_DESCRIPTION + org.ametys.cms.alerts.AlertEngineSchedulable + false + false + false + fire_once + +