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 javax.mail.MessagingException; 027 028import org.apache.avalon.framework.configuration.Configuration; 029import org.apache.avalon.framework.configuration.ConfigurationException; 030import org.apache.avalon.framework.context.Context; 031import org.apache.avalon.framework.context.ContextException; 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.cocoon.Constants; 035import org.apache.cocoon.environment.ObjectModelHelper; 036import org.apache.cocoon.environment.Request; 037import org.apache.cocoon.environment.background.BackgroundEnvironment; 038import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 039import org.apache.commons.lang.StringUtils; 040import org.apache.commons.lang.exception.ExceptionUtils; 041import org.apache.excalibur.source.Source; 042import org.apache.excalibur.source.SourceResolver; 043import org.apache.excalibur.source.SourceUtil; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047import org.ametys.cms.repository.Content; 048import org.ametys.cms.repository.ContentQueryHelper; 049import org.ametys.cms.repository.WorkflowAwareContent; 050import org.ametys.core.authentication.AuthenticateAction; 051import org.ametys.core.engine.BackgroundEngineHelper; 052import org.ametys.core.util.I18nUtils; 053import org.ametys.core.util.mail.SendMailHelper; 054import org.ametys.plugins.repository.AmetysObjectIterable; 055import org.ametys.plugins.repository.AmetysObjectResolver; 056import org.ametys.plugins.repository.AmetysRepositoryException; 057import org.ametys.plugins.workflow.support.WorkflowProvider; 058import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 059import org.ametys.runtime.config.Config; 060import org.ametys.runtime.i18n.I18nizableText; 061import org.ametys.runtime.parameter.ParameterHelper; 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().getValueAsString("cms.url"), "index.html"), "/"); 136 _mailFrom = Config.getInstance().getValueAsString("smtp.mail.from"); 137 _sysadminMail = Config.getInstance().getValueAsString("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.sendMail(subject, null, body, _sysadminMail, _mailFrom); 320 } 321 } 322 catch (IOException e) 323 { 324 _LOGGER.warn("Error building the purge report e-mail.", e); 325 } 326 catch (MessagingException e) 327 { 328 _LOGGER.warn("Error sending the purge report e-mail.", e); 329 } 330 } 331 332 /** 333 * Send the error e-mail. 334 * @param startDate the purge start date. 335 * @param throwable the error. 336 */ 337 protected void sendErrorMail(Date startDate, Throwable throwable) 338 { 339 try 340 { 341 Map<String, String> params = getErrorEmailParams(startDate, throwable); 342 343 I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_PURGE_CONTENTS_REPORT_ERROR_SUBJECT"); 344 345 String subject = _i18nUtils.translate(i18nSubject); 346 String mailUri = getErrorMailUri(params); 347 String body = getMailBody(mailUri, params); 348 349 if (StringUtils.isNotBlank(_sysadminMail)) 350 { 351 SendMailHelper.sendMail(subject, null, body, _sysadminMail, _mailFrom); 352 } 353 } 354 catch (IOException e) 355 { 356 _LOGGER.warn("Error building the purge error e-mail.", e); 357 } 358 catch (MessagingException e) 359 { 360 _LOGGER.warn("Error sending the purge error e-mail.", e); 361 } 362 } 363 364 /** 365 * Get the report e-mail parameters. 366 * @param startDate the purge start date. 367 * @param endDate the purge end date. 368 * @param totalContentsPurged the total count of contents of which versions were purged. 369 * @param totalVersionsPurged the total count of content versions removed. 370 * @return the e-mail parameters. 371 */ 372 protected Map<String, String> getEmailParams(Date startDate, Date endDate, int totalContentsPurged, int totalVersionsPurged) 373 { 374 Map<String, String> params = new HashMap<>(); 375 376 params.put("startDate", ParameterHelper.getISODateTimeFormatter().format(startDate.toInstant().atZone(ZoneId.systemDefault()))); 377 params.put("endDate", ParameterHelper.getISODateTimeFormatter().format(endDate.toInstant().atZone(ZoneId.systemDefault()))); 378 params.put("totalContentsPurged", Integer.toString(totalContentsPurged)); 379 params.put("totalVersionsPurged", Integer.toString(totalVersionsPurged)); 380 params.put("url", _baseUrl); 381 382 return params; 383 } 384 385 /** 386 * Get the error e-mail parameters. 387 * @param startDate the purge start date. 388 * @param throwable the error. 389 * @return the e-mail parameters. 390 */ 391 protected Map<String, String> getErrorEmailParams(Date startDate, Throwable throwable) 392 { 393 Map<String, String> params = new HashMap<>(); 394 395 params.put("startDate", ParameterHelper.getISODateTimeFormatter().format(startDate.toInstant().atZone(ZoneId.systemDefault()))); 396 params.put("stackTrace", ExceptionUtils.getStackTrace(throwable)); 397 params.put("url", _baseUrl); 398 399 return params; 400 } 401 402 /** 403 * Get a mail part. 404 * @param parameters the pipeline parameters. 405 * @return the mail part. 406 * @throws IOException If an error occurred 407 */ 408 protected String getMailBody(Map<String, String> parameters) throws IOException 409 { 410 String uri = getMailUri(parameters); 411 412 return getMailBody(uri, parameters); 413 } 414 415 /** 416 * Get a mail part. 417 * @param uri The url where to get the body of the mail 418 * @param parameters the pipeline parameters. 419 * @return the mail part. 420 * @throws IOException If an error occurred 421 */ 422 private String getMailBody(String uri, Map<String, String> parameters) throws IOException 423 { 424 Source source = null; 425 InputStream is = null; 426 try 427 { 428 source = _sourceResolver.resolveURI(uri, null, parameters); 429 is = source.getInputStream(); 430 431 try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) 432 { 433 SourceUtil.copy(is, bos); 434 435 return bos.toString("UTF-8"); 436 } 437 } 438 finally 439 { 440 if (is != null) 441 { 442 is.close(); 443 } 444 445 if (source != null) 446 { 447 _sourceResolver.release(source); 448 } 449 } 450 } 451 452 /** 453 * Get the pipeline uri for mail body 454 * @param parameters the mail parameters 455 * @return a pipeline uri 456 */ 457 protected String getMailUri(Map<String, String> parameters) 458 { 459 return "cocoon://_plugins/cms/purge/purge-mail.html"; 460 } 461 462 /** 463 * Get the pipeline uri for error mail body. 464 * @param parameters the mail parameters 465 * @return a pipeline uri 466 */ 467 protected String getErrorMailUri(Map<String, String> parameters) 468 { 469 return "cocoon://_plugins/cms/purge/purge-error-mail.html"; 470 } 471 472}