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