001/* 002 * Copyright 2012 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.workflow.archive; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.net.MalformedURLException; 021import java.text.DateFormat; 022import java.util.ArrayList; 023import java.util.Date; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028 029import javax.mail.MessagingException; 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.environment.ObjectModelHelper; 039import org.apache.cocoon.environment.Request; 040import org.apache.cocoon.environment.background.BackgroundEnvironment; 041import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 042import org.apache.commons.lang.StringUtils; 043import org.apache.excalibur.source.Source; 044import org.apache.excalibur.source.SourceResolver; 045import org.slf4j.Logger; 046import org.slf4j.LoggerFactory; 047 048import org.ametys.cms.content.archive.ArchiveConstants; 049import org.ametys.cms.repository.Content; 050import org.ametys.cms.repository.ContentQueryHelper; 051import org.ametys.core.authentication.AuthenticateAction; 052import org.ametys.core.engine.BackgroundEngineHelper; 053import org.ametys.core.right.RightManager; 054import org.ametys.core.user.User; 055import org.ametys.core.user.UserIdentity; 056import org.ametys.core.user.UserManager; 057import org.ametys.core.user.population.PopulationContextHelper; 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.provider.RequestAttributeWorkspaceSelector; 064import org.ametys.plugins.repository.query.expression.DateExpression; 065import org.ametys.plugins.repository.query.expression.Expression; 066import org.ametys.plugins.repository.query.expression.Expression.Operator; 067import org.ametys.runtime.config.Config; 068import org.ametys.runtime.i18n.I18nizableText; 069 070/** 071 * Runnable engine that archive the contents that have an scheduled archiving 072 * date set before the current date. 073 */ 074public class ArchiveContentsEngine implements Runnable 075{ 076 /** The logger. */ 077 protected Logger _logger = LoggerFactory.getLogger(ArchiveContentsEngine.class); 078 079 /** The avalon context. */ 080 protected Context _context; 081 082 /** The service manager. */ 083 protected ServiceManager _manager; 084 085 /** The server base URL. */ 086 protected String _baseUrl; 087 088 /** Is the engine initialized ? */ 089 protected boolean _initialized; 090 091 /** The cocoon environment context. */ 092 protected org.apache.cocoon.environment.Context _environmentContext; 093 094 /** The ametys object resolver. */ 095 protected AmetysObjectResolver _ametysResolver; 096 097 /** The avalon source resolver. */ 098 protected SourceResolver _sourceResolver; 099 100 /** The rights manager */ 101 protected RightManager _rightManager; 102 103 /** The users manager. */ 104 protected UserManager _userManager; 105 106 /** The i18n utils. */ 107 protected I18nUtils _i18nUtils; 108 109 /** The content of "from" field in emails. */ 110 protected String _mailFrom; 111 112 /** The sysadmin mail address, to which will be sent the report e-mail. */ 113 protected String _sysadminMail; 114 115 /** The user e-mail notification will be sent to users that have this at least one of this rights. */ 116 protected Set<String> _archiveRights; 117 118 /** The user notification mail body i18n key. */ 119 protected String _userMailBody; 120 121 /** The user notification mail subject i18n key. */ 122 protected String _userMailSubject; 123 124 /** The user notification error mail body i18n key. */ 125 protected String _userErrorMailBody; 126 127 /** The user notification error mail subject i18n key. */ 128 protected String _userErrorMailSubject; 129 130 /** 131 * Initialize the archive engine. 132 * 133 * @param manager the avalon service manager. 134 * @param context the avalon context. 135 * @throws ContextException If an error occurred 136 * @throws ServiceException If an error occurred 137 */ 138 public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException 139 { 140 _manager = manager; 141 _context = context; 142 _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 143 144 // Lookup the needed components. 145 _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 146 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 147 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 148 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 149 150 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 151 152 _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/"); 153 _mailFrom = Config.getInstance().getValue("smtp.mail.from"); 154 _sysadminMail = Config.getInstance().getValue("smtp.mail.sysadminto"); 155 156 _initialized = true; 157 } 158 159 /** 160 * Configure the engine (called by the scheduler). 161 * 162 * @param configuration the component configuration. 163 * @throws ConfigurationException If an error occurred 164 */ 165 public void configure(Configuration configuration) throws ConfigurationException 166 { 167 // Rights 168 Configuration rightsConf = configuration.getChild("rights"); 169 170 _archiveRights = new HashSet<>(); 171 172 for (Configuration rightConf : rightsConf.getChildren("right")) 173 { 174 String right = rightConf.getValue(""); 175 if (StringUtils.isNotBlank(right)) 176 { 177 _archiveRights.add(right); 178 } 179 } 180 181 // Mails 182 Configuration validMailsConf = configuration.getChild("mails").getChild("valid"); 183 Configuration errorMailsConf = configuration.getChild("mails").getChild("error"); 184 185 _userMailBody = validMailsConf.getChild("bodyKey").getValue(); 186 _userMailSubject = validMailsConf.getChild("subjectKey").getValue(); 187 188 _userErrorMailBody = errorMailsConf.getChild("bodyKey").getValue(); 189 _userErrorMailSubject = errorMailsConf.getChild("subjectKey").getValue(); 190 191 } 192 193 /** 194 * Check the initialization and throw an exception if not initialized. 195 */ 196 protected void checkInitialization() 197 { 198 if (!_initialized) 199 { 200 String message = "The engine must be initialized before it can be runned."; 201 _logger.error(message); 202 throw new IllegalStateException(message); 203 } 204 } 205 206 @Override 207 public void run() 208 { 209 Map<String, Object> environmentInformation = null; 210 long duration = 0; 211 try 212 { 213 _logger.info("Preparing the scheduled archiving process on contents..."); 214 215 checkInitialization(); 216 217 // Create the environment. 218 environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_manager, _environmentContext, new SLF4JLoggerAdapter(_logger)); 219 BackgroundEnvironment environment = (BackgroundEnvironment) environmentInformation.get("environment"); 220 221 // Authorize workflow actions and "check-auth" CMS action, from this background environment 222 Request request = (Request) environment.getObjectModel().get(ObjectModelHelper.REQUEST_OBJECT); 223 request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED, true); 224 225 long start = System.currentTimeMillis(); 226 227 // Get all the needed contents and archive them. 228 archiveContents(request); 229 230 long end = System.currentTimeMillis(); 231 232 duration = (end - start) / 1000; 233 } 234 catch (Exception e) 235 { 236 _logger.error("An error occurred while archiving the contents.", e); 237 } 238 finally 239 { 240 // Leave the environment. 241 if (environmentInformation != null) 242 { 243 BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation); 244 } 245 246 // Dispose of the resources. 247 dispose(); 248 _logger.info("Scheduled archiving process ended after " + duration + " seconds."); 249 } 250 } 251 252 /** 253 * Dispose of the resources and looked-up components. 254 */ 255 protected void dispose() 256 { 257 // Release the components. 258 if (_manager != null) 259 { 260 _manager.release(_ametysResolver); 261 } 262 263 _ametysResolver = null; 264 _sourceResolver = null; 265 _userManager = null; 266 267 _i18nUtils = null; 268 269 _environmentContext = null; 270 _context = null; 271 _manager = null; 272 273 _initialized = false; 274 } 275 276 /** 277 * Get the contents that need to be archived, and archive them. 278 * @param request The current request object 279 * 280 * @throws AmetysRepositoryException if an error occurs. 281 * @throws IOException If an error occurred 282 * @throws MalformedURLException If an error occurred 283 */ 284 protected void archiveContents(Request request) throws AmetysRepositoryException, MalformedURLException, IOException 285 { 286 // Get all the content which scheduled archiving date is passed. 287 Expression dateExpression = new DateExpression(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE, Operator.LE, new Date(), true); 288 String query = ContentQueryHelper.getContentXPathQuery(dateExpression); 289 290 try (AmetysObjectIterable<Content> contents = _ametysResolver.query(query)) 291 { 292 List<Content> archivedContents = new ArrayList<>(); 293 List<Content> contentsWithError = new ArrayList<>(); 294 295 try 296 { 297 for (Content content : contents) 298 { 299 setRequestAttributes(request, content); 300 301 Set<UserIdentity> users = _getAuthorizedContributors(content); 302 303 Source source = null; 304 try 305 { 306 String contentId = content.getId(); 307 308 String uri = getArchiveActionUri(contentId); 309 source = _sourceResolver.resolveURI(uri); 310 311 try (InputStream is = source.getInputStream()) 312 { 313 String workspaceBackup = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 314 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, ArchiveConstants.ARCHIVE_WORKSPACE); 315 316 Content archivedContent = _ametysResolver.resolveById(contentId); 317 archivedContents.add(archivedContent); 318 319 sendMailToContributors(archivedContent, users); 320 321 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceBackup); 322 } 323 } 324 catch (Exception e) 325 { 326 contentsWithError.add (content); 327 sendErrorMailToContributors(content, users); 328 _logger.error("Error while trying to archive the content : " + content.getId() + "\nThis content is probably not archived.", e); 329 } 330 finally 331 { 332 if (source != null) 333 { 334 _sourceResolver.release(source); 335 } 336 } 337 } 338 } 339 finally 340 { 341 // Send mail to administrator 342 sendMailToAdministrator(archivedContents, contentsWithError); 343 } 344 } 345 } 346 347 /** 348 * Set the necessary request attributes 349 * @param request The request 350 * @param content The content 351 */ 352 protected void setRequestAttributes (Request request, Content content) 353 { 354 // Set the population contexts to be able to get allowed users 355 List<String> populationContexts = new ArrayList<>(); 356 populationContexts.add("/application"); 357 358 request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts); 359 } 360 361 /** 362 * Get the pipeline uri for the archive action 363 * @param contentId the current contend id 364 * @return a pipeline uri 365 */ 366 protected String getArchiveActionUri(String contentId) 367 { 368 return "cocoon://_plugins/cms/archives/archive/" + ArchiveConstants.ARCHIVE_WORKFLOW_ACTION_ID + "?contentId=" + contentId; 369 } 370 371 /** 372 * Send the archive report e-mail. 373 * @param archivedContents The list of archived contents 374 * @param contentsWithError The list of contents with error 375 */ 376 protected void sendMailToAdministrator(List<Content> archivedContents, List<Content> contentsWithError) 377 { 378 if (archivedContents.size() != 0 || contentsWithError.size() != 0) 379 { 380 try 381 { 382 I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_ARCHIVE_CONTENTS_REPORT_SUBJECT"); 383 384 List<String> bodyParams = getAdminEmailParams(archivedContents, contentsWithError); 385 I18nizableText i18nBody = new I18nizableText("plugin.cms", "PLUGINS_CMS_ARCHIVE_CONTENTS_REPORT_BODY", bodyParams); 386 387 String subject = _i18nUtils.translate(i18nSubject); 388 String body = _i18nUtils.translate(i18nBody); 389 390 if (StringUtils.isNotBlank(_sysadminMail)) 391 { 392 SendMailHelper.sendMail(subject, null, body, _sysadminMail, _mailFrom); 393 } 394 } 395 catch (MessagingException e) 396 { 397 _logger.warn("Error sending the archive report e-mail.", e); 398 } 399 } 400 } 401 402 403 /** 404 * Get the report e-mail parameters. 405 * @param archivedContents The list of archived contents 406 * @param contentsWithError The list of contents with error 407 * @return the e-mail parameters. 408 */ 409 protected List<String> getAdminEmailParams(List<Content> archivedContents, List<Content> contentsWithError) 410 { 411 List<String> params = new ArrayList<>(); 412 413 DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT); 414 params.add(df.format(new Date())); // {0} 415 416 params.add(String.valueOf(archivedContents.size())); // {1} 417 params.add(_getContentsListAsString(archivedContents)); // {2} 418 419 params.add(String.valueOf(contentsWithError.size())); // {3} 420 params.add(_getContentsListAsString(contentsWithError)); // {4} 421 422 params.add(_getRequestURI(null) + "/index.html"); // {5} 423 424 return params; 425 } 426 427 /** 428 * Get the contents list as String 429 * @param contents The contents 430 * @return the list of contents as String 431 */ 432 protected String _getContentsListAsString (List<Content> contents) 433 { 434 List<String> contentNames = new ArrayList<>(); 435 for (Content content : contents) 436 { 437 contentNames.add("- " + content.getTitle(null)); 438 } 439 440 if (contentNames.size() > 0) 441 { 442 return StringUtils.join(contentNames, "\n"); 443 } 444 else 445 { 446 return ""; 447 } 448 } 449 450 /** 451 * Get the authorized contributors to receive mail notification 452 * @param content The content to be archived 453 * @return The user logins 454 */ 455 protected Set<UserIdentity> _getAuthorizedContributors (Content content) 456 { 457 Set<UserIdentity> users = new HashSet<>(); 458 for (String right : _archiveRights) 459 { 460 users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValue("runtime.mail.massive.sending"))); 461 } 462 463 return users; 464 } 465 466 /** 467 * Send the mail to alert users that the content has been archived. 468 * @param content The archived content 469 * @param users The users 470 */ 471 protected void sendMailToContributors(Content content, Set<UserIdentity> users) 472 { 473 try 474 { 475 List<String> params = getBodyParamsForContributors (content, true); 476 477 I18nizableText i18nSubject = new I18nizableText(null, _userMailSubject, params); 478 I18nizableText i18nBody = new I18nizableText(null, _userMailBody, params); 479 480 String subject = _i18nUtils.translate(i18nSubject); 481 String body = _i18nUtils.translate(i18nBody); 482 483 _sendMailsToUsers(subject, body, users, _mailFrom); 484 } 485 catch (Exception e) 486 { 487 _logger.error("Unable to send mail to contributors after archiving content '" + content.getName() + "'", e); 488 } 489 } 490 491 /** 492 * Get email body parameters 493 * @param content the archived content 494 * @param archived true if the content has archived 495 * @return The mail parameters 496 */ 497 protected List<String> getBodyParamsForContributors (Content content, boolean archived) 498 { 499 List<String> params = new ArrayList<>(); 500 501 params.add(content.getTitle(null)); // {0} 502 params.add(DateFormat.getDateInstance(DateFormat.LONG).format(new Date())); // {1} 503 504 if (archived) 505 { 506 params.add(_getRequestURI(content) + "/index.html?uitool=uitool-content,id:%27" + content.getId() + "%27,workspace:%27archives%27,%27ignore-workflow%27:%27true%27,%27content-message-type%27:%27archived-content%27"); // {2} 507 } 508 else 509 { 510 params.add(_getRequestURI(content) + "/index.html?uitool=uitool-content,id:%27" + content.getId() + "%27"); 511 } 512 513 return params; 514 } 515 516 /** 517 * Send the mail to alert users that an error has occurred while trying to archive the content. 518 * @param content The content 519 * @param users The users 520 */ 521 protected void sendErrorMailToContributors(Content content, Set<UserIdentity> users) 522 { 523 try 524 { 525 List<String> params = getBodyParamsForContributors (content, false); 526 527 I18nizableText i18nSubject = new I18nizableText(null, _userErrorMailSubject, params); 528 I18nizableText i18nBody = new I18nizableText(null, _userErrorMailBody, params); 529 530 String subject = _i18nUtils.translate(i18nSubject); 531 String body = _i18nUtils.translate(i18nBody); 532 533 _sendMailsToUsers(subject, body, users, _mailFrom); 534 } 535 catch (Exception e) 536 { 537 _logger.error("Unable to send mail to contributors after archiving content '" + content.getName() + "' failed", e); 538 } 539 } 540 541 /** 542 * Get the request URI to set in mail 543 * @param content The content. Can be null 544 * @return the request URI 545 */ 546 protected String _getRequestURI (Content content) 547 { 548 return _baseUrl; 549 } 550 551 /** 552 * Send the emails to users (contributors) 553 * @param subject the e-mail subject. 554 * @param body the e-mail body. 555 * @param users users to send the mail to. 556 * @param from the address sending the e-mail. 557 */ 558 protected void _sendMailsToUsers(String subject, String body, Set<UserIdentity> users, String from) 559 { 560 for (UserIdentity userIdentity : users) 561 { 562 User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin()); 563 564 if (user != null && StringUtils.isNotBlank(user.getEmail())) 565 { 566 String mail = user.getEmail(); 567 568 try 569 { 570 SendMailHelper.sendMail(subject, null, body, mail, from); 571 } 572 catch (MessagingException e) 573 { 574 if (_logger.isWarnEnabled()) 575 { 576 _logger.warn("Could not send an archive notification e-mail to " + mail, e); 577 } 578 } 579 } 580 } 581 } 582}