001/* 002 * Copyright 2011 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.purge; 017 018import java.io.ByteArrayOutputStream; 019import java.io.IOException; 020import java.io.InputStream; 021import java.time.ZoneId; 022import java.util.Date; 023import java.util.HashMap; 024import java.util.Map; 025 026import org.apache.avalon.framework.configuration.Configuration; 027import org.apache.avalon.framework.configuration.ConfigurationException; 028import org.apache.avalon.framework.context.Context; 029import org.apache.avalon.framework.context.ContextException; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.cocoon.Constants; 033import org.apache.cocoon.environment.ObjectModelHelper; 034import org.apache.cocoon.environment.Request; 035import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 036import org.apache.commons.lang.StringUtils; 037import org.apache.commons.lang.exception.ExceptionUtils; 038import org.apache.excalibur.source.Source; 039import org.apache.excalibur.source.SourceResolver; 040import org.apache.excalibur.source.SourceUtil; 041import org.slf4j.Logger; 042import org.slf4j.LoggerFactory; 043 044import org.ametys.cms.repository.Content; 045import org.ametys.cms.repository.ContentQueryHelper; 046import org.ametys.cms.repository.WorkflowAwareContent; 047import org.ametys.core.authentication.AuthenticateAction; 048import org.ametys.core.engine.BackgroundEngineHelper; 049import org.ametys.core.engine.BackgroundEnvironment; 050import org.ametys.core.util.DateUtils; 051import org.ametys.core.util.I18nUtils; 052import org.ametys.core.util.mail.SendMailHelper; 053import org.ametys.plugins.repository.AmetysObjectIterable; 054import org.ametys.plugins.repository.AmetysObjectResolver; 055import org.ametys.plugins.repository.AmetysRepositoryException; 056import org.ametys.plugins.workflow.support.WorkflowProvider; 057import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 058import org.ametys.runtime.config.Config; 059import org.ametys.runtime.i18n.I18nizableText; 060 061import jakarta.mail.MessagingException; 062 063/** 064 * Runnable engine that removes old versions of contents. 065 */ 066public class PurgeContentsEngine implements Runnable 067{ 068 069 /** The logger. */ 070 protected static final Logger _LOGGER = LoggerFactory.getLogger(PurgeContentsEngine.class); 071 072 /** The avalon context. */ 073 protected Context _context; 074 075 /** The service manager. */ 076 protected ServiceManager _manager; 077 078 /** The server base URL. */ 079 protected String _baseUrl; 080 081 /** Is the engine initialized ? */ 082 protected boolean _initialized; 083 084 /** The cocoon environment context. */ 085 protected org.apache.cocoon.environment.Context _environmentContext; 086 087 /** The ametys object resolver. */ 088 protected AmetysObjectResolver _ametysResolver; 089 090 /** The avalon source resolver. */ 091 protected SourceResolver _sourceResolver; 092 093 /** The version purger. */ 094 protected PurgeVersionsManager _versionPurger; 095 096 /** The workflow provider */ 097 protected WorkflowProvider _workflowProvider; 098 099 /** The i18n utils. */ 100 protected I18nUtils _i18nUtils; 101 102 /** A Map of the validation step ID by workflow name. */ 103 protected Map<String, Long> _validationStepId; 104 105 /** The count of oldest versions to keep. */ 106 protected int _firstVersionsToKeep; 107 108 /** The content of "from" field in emails. */ 109 protected String _mailFrom; 110 111 /** The sysadmin mail address, to which will be sent the report e-mail. */ 112 protected String _sysadminMail; 113 114 /** 115 * Initialize the purge engine. 116 * @param manager the avalon service manager. 117 * @param context the avalon context. 118 * @throws ContextException If an error occurred 119 * @throws ServiceException If an error occurred 120 */ 121 public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException 122 { 123 _manager = manager; 124 _context = context; 125 _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 126 127 // Lookup the needed components. 128 _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 129 _versionPurger = (PurgeVersionsManager) manager.lookup(PurgeVersionsManager.ROLE); 130 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 131 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 132 133 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 134 135 _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/"); 136 _mailFrom = Config.getInstance().getValue("smtp.mail.from"); 137 _sysadminMail = Config.getInstance().getValue("smtp.mail.sysadminto"); 138 139 _initialized = true; 140 } 141 142 /** 143 * Configure the engine (called by the scheduler). 144 * @param configuration the component configuration. 145 * @throws ConfigurationException If an error occurred 146 */ 147 public void configure(Configuration configuration) throws ConfigurationException 148 { 149 _validationStepId = configureValidationStepId(configuration); 150 _firstVersionsToKeep = configuration.getChild("firstVersionsToKeep").getValueAsInteger(); 151 } 152 153 /** 154 * Get the validation step ID by workflow from the component configuration. 155 * @param configuration the component configuration. 156 * @return a Map of the validation step ID by workflow. 157 * @throws ConfigurationException If an error occurred 158 */ 159 protected Map<String, Long> configureValidationStepId(Configuration configuration) throws ConfigurationException 160 { 161 Map<String, Long> validationStepIds = new HashMap<>(); 162 163 for (Configuration workflowConf : configuration.getChildren("workflow")) 164 { 165 String workflowName = workflowConf.getAttribute("name"); 166 Long validationStepId = workflowConf.getChild("validationStepId").getValueAsLong(); 167 168 validationStepIds.put(workflowName, validationStepId); 169 } 170 171 return validationStepIds; 172 } 173 174 /** 175 * Check the initialization and throw an exception if not initialized. 176 */ 177 protected void checkInitialization() 178 { 179 if (!_initialized) 180 { 181 String message = "Le composant de synchronisation doit être initialisé avant d'être lancé."; 182 _LOGGER.error(message); 183 throw new IllegalStateException(message); 184 } 185 } 186 187 @Override 188 public void run() 189 { 190 Map<String, Object> environmentInformation = null; 191 long start = System.currentTimeMillis(); 192 long duration = 0; 193 try 194 { 195 _LOGGER.info("Preparing to purge the contents..."); 196 197 checkInitialization(); 198 199 // Create the environment. 200 environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_manager, _environmentContext, new SLF4JLoggerAdapter(_LOGGER)); 201 202 BackgroundEnvironment environment = (BackgroundEnvironment) environmentInformation.get("environment"); 203 204 // Authorize workflow actions and "check-auth" CMS action, from this background environment 205 Request request = (Request) environment.getObjectModel().get(ObjectModelHelper.REQUEST_OBJECT); 206 request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED, true); 207 208 // Get all the contents and purge the old versions. 209 purgeContents(); 210 } 211 catch (Exception e) 212 { 213 _LOGGER.error("An error occurred purging the contents.", e); 214 sendErrorMail(new Date(start), e); 215 } 216 finally 217 { 218 long end = System.currentTimeMillis(); 219 220 duration = (end - start) / 1000; 221 222 // Leave the environment. 223 if (environmentInformation != null) 224 { 225 BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation); 226 } 227 228 // Dispose of the resources. 229 dispose(); 230 _LOGGER.info("Contents purge ended after " + duration + " seconds."); 231 } 232 } 233 234 /** 235 * Dispose of the resources and looked-up components. 236 */ 237 protected void dispose() 238 { 239 // Release the components. 240 if (_manager != null) 241 { 242 _manager.release(_ametysResolver); 243 } 244 245 _ametysResolver = null; 246 247 _environmentContext = null; 248 _context = null; 249 _manager = null; 250 251 _initialized = false; 252 } 253 254 /** 255 * Get all the contents and purge the old versions. 256 * @throws AmetysRepositoryException if an error occurs. 257 */ 258 protected void purgeContents() throws AmetysRepositoryException 259 { 260 String query = ContentQueryHelper.getContentXPathQuery(null); 261 262 Date startDate = new Date(); 263 264 int totalContentsPurged = 0; 265 int totalVersionsPurged = 0; 266 267 try (AmetysObjectIterable<Content> contents = _ametysResolver.query(query)) 268 { 269 for (Content content : contents) 270 { 271 if (content instanceof WorkflowAwareContent) 272 { 273 WorkflowAwareContent workflowContent = (WorkflowAwareContent) content; 274 long workflowId = workflowContent.getWorkflowId(); 275 276 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(workflowContent); 277 String workflowName = workflow.getWorkflowName(workflowId); 278 279 if (_validationStepId.containsKey(workflowName)) 280 { 281 long validationStepId = _validationStepId.get(workflowName); 282 283 int purged = _versionPurger.purgeContent(workflowContent, validationStepId, _firstVersionsToKeep); 284 if (purged > 0) 285 { 286 totalVersionsPurged += purged; 287 totalContentsPurged++; 288 } 289 } 290 } 291 } 292 } 293 294 Date endDate = new Date(); 295 296 sendMail(startDate, endDate, totalContentsPurged, totalVersionsPurged); 297 } 298 299 /** 300 * Send the purge report e-mail. 301 * @param startDate the purge start date. 302 * @param endDate the purge end date. 303 * @param totalContentsPurged the total count of contents of which versions were purged. 304 * @param totalVersionsPurged the total count of content versions removed. 305 */ 306 protected void sendMail(Date startDate, Date endDate, int totalContentsPurged, int totalVersionsPurged) 307 { 308 try 309 { 310 Map<String, String> params = getEmailParams(startDate, endDate, totalContentsPurged, totalVersionsPurged); 311 312 I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_PURGE_CONTENTS_REPORT_SUBJECT"); 313 314 String subject = _i18nUtils.translate(i18nSubject); 315 String body = getMailBody(params); 316 317 if (StringUtils.isNotBlank(_sysadminMail)) 318 { 319 SendMailHelper.newMail() 320 .withSubject(subject) 321 .withTextBody(body) 322 .withSender(_mailFrom) 323 .withRecipient(_sysadminMail) 324 .sendMail(); 325 } 326 } 327 catch (MessagingException | IOException e) 328 { 329 _LOGGER.warn("Error sending the purge report e-mail.", e); 330 } 331 } 332 333 /** 334 * Send the error e-mail. 335 * @param startDate the purge start date. 336 * @param throwable the error. 337 */ 338 protected void sendErrorMail(Date startDate, Throwable throwable) 339 { 340 try 341 { 342 Map<String, String> params = getErrorEmailParams(startDate, throwable); 343 344 I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_PURGE_CONTENTS_REPORT_ERROR_SUBJECT"); 345 346 String subject = _i18nUtils.translate(i18nSubject); 347 String mailUri = getErrorMailUri(params); 348 String body = getMailBody(mailUri, params); 349 350 if (StringUtils.isNotBlank(_sysadminMail)) 351 { 352 SendMailHelper.newMail() 353 .withSubject(subject) 354 .withTextBody(body) 355 .withSender(_mailFrom) 356 .withRecipient(_sysadminMail) 357 .sendMail(); 358 } 359 } 360 catch (MessagingException | IOException e) 361 { 362 _LOGGER.warn("Error sending the purge error e-mail.", e); 363 } 364 } 365 366 /** 367 * Get the report e-mail parameters. 368 * @param startDate the purge start date. 369 * @param endDate the purge end date. 370 * @param totalContentsPurged the total count of contents of which versions were purged. 371 * @param totalVersionsPurged the total count of content versions removed. 372 * @return the e-mail parameters. 373 */ 374 protected Map<String, String> getEmailParams(Date startDate, Date endDate, int totalContentsPurged, int totalVersionsPurged) 375 { 376 Map<String, String> params = new HashMap<>(); 377 378 params.put("startDate", DateUtils.getISODateTimeFormatter().format(startDate.toInstant().atZone(ZoneId.systemDefault()))); 379 params.put("endDate", DateUtils.getISODateTimeFormatter().format(endDate.toInstant().atZone(ZoneId.systemDefault()))); 380 params.put("totalContentsPurged", Integer.toString(totalContentsPurged)); 381 params.put("totalVersionsPurged", Integer.toString(totalVersionsPurged)); 382 params.put("url", _baseUrl); 383 384 return params; 385 } 386 387 /** 388 * Get the error e-mail parameters. 389 * @param startDate the purge start date. 390 * @param throwable the error. 391 * @return the e-mail parameters. 392 */ 393 protected Map<String, String> getErrorEmailParams(Date startDate, Throwable throwable) 394 { 395 Map<String, String> params = new HashMap<>(); 396 397 params.put("startDate", DateUtils.getISODateTimeFormatter().format(startDate.toInstant().atZone(ZoneId.systemDefault()))); 398 params.put("stackTrace", ExceptionUtils.getStackTrace(throwable)); 399 params.put("url", _baseUrl); 400 401 return params; 402 } 403 404 /** 405 * Get a mail part. 406 * @param parameters the pipeline parameters. 407 * @return the mail part. 408 * @throws IOException If an error occurred 409 */ 410 protected String getMailBody(Map<String, String> parameters) throws IOException 411 { 412 String uri = getMailUri(parameters); 413 414 return getMailBody(uri, parameters); 415 } 416 417 /** 418 * Get a mail part. 419 * @param uri The url where to get the body of the mail 420 * @param parameters the pipeline parameters. 421 * @return the mail part. 422 * @throws IOException If an error occurred 423 */ 424 private String getMailBody(String uri, Map<String, String> parameters) throws IOException 425 { 426 Source source = null; 427 InputStream is = null; 428 try 429 { 430 source = _sourceResolver.resolveURI(uri, null, parameters); 431 is = source.getInputStream(); 432 433 try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) 434 { 435 SourceUtil.copy(is, bos); 436 437 return bos.toString("UTF-8"); 438 } 439 } 440 finally 441 { 442 if (is != null) 443 { 444 is.close(); 445 } 446 447 if (source != null) 448 { 449 _sourceResolver.release(source); 450 } 451 } 452 } 453 454 /** 455 * Get the pipeline uri for mail body 456 * @param parameters the mail parameters 457 * @return a pipeline uri 458 */ 459 protected String getMailUri(Map<String, String> parameters) 460 { 461 return "cocoon://_plugins/cms/purge/purge-mail.html"; 462 } 463 464 /** 465 * Get the pipeline uri for error mail body. 466 * @param parameters the mail parameters 467 * @return a pipeline uri 468 */ 469 protected String getErrorMailUri(Map<String, String> parameters) 470 { 471 return "cocoon://_plugins/cms/purge/purge-error-mail.html"; 472 } 473 474}