001/* 002 * Copyright 2021 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.activity.Initializable; 027import org.apache.avalon.framework.configuration.Configuration; 028import org.apache.avalon.framework.configuration.ConfigurationException; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.commons.lang.StringUtils; 032import org.apache.commons.lang.exception.ExceptionUtils; 033import org.apache.excalibur.source.Source; 034import org.apache.excalibur.source.SourceResolver; 035import org.apache.excalibur.source.SourceUtil; 036import org.quartz.JobExecutionContext; 037 038import org.ametys.cms.repository.Content; 039import org.ametys.cms.repository.ContentQueryHelper; 040import org.ametys.cms.repository.WorkflowAwareContent; 041import org.ametys.core.schedule.progression.ContainerProgressionTracker; 042import org.ametys.core.ui.mail.StandardMailBodyHelper; 043import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder; 044import org.ametys.core.util.DateUtils; 045import org.ametys.core.util.I18nUtils; 046import org.ametys.core.util.mail.SendMailHelper; 047import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable; 048import org.ametys.plugins.repository.AmetysObjectIterable; 049import org.ametys.plugins.repository.AmetysObjectResolver; 050import org.ametys.plugins.workflow.support.WorkflowProvider; 051import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 052import org.ametys.runtime.config.Config; 053import org.ametys.runtime.i18n.I18nizableText; 054 055import jakarta.mail.MessagingException; 056 057/** 058 * Schedulable that removes old versions of contents. 059 */ 060public class PurgeContentsSchedulable extends AbstractStaticSchedulable implements Initializable 061{ 062 /** The server base URL. */ 063 protected String _baseUrl; 064 065 /** The ametys object resolver. */ 066 protected AmetysObjectResolver _ametysResolver; 067 068 /** The avalon source resolver. */ 069 protected SourceResolver _sourceResolver; 070 071 /** The version purger. */ 072 protected PurgeVersionsManager _versionPurger; 073 074 /** The workflow provider */ 075 protected WorkflowProvider _workflowProvider; 076 077 /** The i18n utils. */ 078 protected I18nUtils _i18nUtils; 079 080 /** A Map of the validation step ID by workflow name. */ 081 protected Map<String, Long> _validationStepId; 082 083 /** The count of oldest versions to keep. */ 084 protected int _firstVersionsToKeep; 085 086 /** The content of "from" field in emails. */ 087 protected String _mailFrom; 088 089 /** The sysadmin mail address, to which will be sent the report e-mail. */ 090 protected String _sysadminMail; 091 092 @Override 093 public void service(ServiceManager manager) throws ServiceException 094 { 095 super.service(manager); 096 // Lookup the needed components. 097 _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 098 _versionPurger = (PurgeVersionsManager) manager.lookup(PurgeVersionsManager.ROLE); 099 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 100 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 101 102 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 103 } 104 105 public void initialize() throws Exception 106 { 107 108 _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/"); 109 _mailFrom = Config.getInstance().getValue("smtp.mail.from"); 110 _sysadminMail = Config.getInstance().getValue("smtp.mail.sysadminto"); 111 } 112 113 @Override 114 public void configure(Configuration configuration) throws ConfigurationException 115 { 116 super.configure(configuration); 117 _validationStepId = configureValidationStepId(configuration); 118 _firstVersionsToKeep = configuration.getChild("firstVersionsToKeep").getValueAsInteger(); 119 } 120 121 /** 122 * Get the validation step ID by workflow from the component configuration. 123 * @param configuration the component configuration. 124 * @return a Map of the validation step ID by workflow. 125 * @throws ConfigurationException If an error occurred 126 */ 127 protected Map<String, Long> configureValidationStepId(Configuration configuration) throws ConfigurationException 128 { 129 Map<String, Long> validationStepIds = new HashMap<>(); 130 131 for (Configuration workflowConf : configuration.getChildren("workflow")) 132 { 133 String workflowName = workflowConf.getAttribute("name"); 134 Long validationStepId = workflowConf.getChild("validationStepId").getValueAsLong(); 135 136 validationStepIds.put(workflowName, validationStepId); 137 } 138 139 return validationStepIds; 140 } 141 142 @Override 143 public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception 144 { 145 String query = ContentQueryHelper.getContentXPathQuery(null); 146 147 Date startDate = new Date(); 148 149 int totalContentsPurged = 0; 150 int totalVersionsPurged = 0; 151 152 try 153 { 154 try (AmetysObjectIterable<Content> contents = _ametysResolver.query(query)) 155 { 156 for (Content content : contents) 157 { 158 if (content instanceof WorkflowAwareContent) 159 { 160 WorkflowAwareContent workflowContent = (WorkflowAwareContent) content; 161 long workflowId = workflowContent.getWorkflowId(); 162 163 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(workflowContent); 164 String workflowName = workflow.getWorkflowName(workflowId); 165 166 if (_validationStepId.containsKey(workflowName)) 167 { 168 long validationStepId = _validationStepId.get(workflowName); 169 170 int purged = _versionPurger.purgeContent(workflowContent, validationStepId, _firstVersionsToKeep); 171 if (purged > 0) 172 { 173 totalVersionsPurged += purged; 174 totalContentsPurged++; 175 } 176 } 177 } 178 } 179 } 180 181 Date endDate = new Date(); 182 183 sendMail(startDate, endDate, totalContentsPurged, totalVersionsPurged); 184 } 185 catch (Exception e) 186 { 187 sendErrorMail(startDate, e); 188 } 189 190 } 191 192 /** 193 * Send the purge report e-mail. 194 * @param startDate the purge start date. 195 * @param endDate the purge end date. 196 * @param totalContentsPurged the total count of contents of which versions were purged. 197 * @param totalVersionsPurged the total count of content versions removed. 198 */ 199 protected void sendMail(Date startDate, Date endDate, int totalContentsPurged, int totalVersionsPurged) 200 { 201 try 202 { 203 Map<String, String> params = getEmailParams(startDate, endDate, totalContentsPurged, totalVersionsPurged); 204 205 I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_PURGE_CONTENTS_REPORT_SUBJECT"); 206 207 String subject = _i18nUtils.translate(i18nSubject); 208 String body = getMailBody(params); 209 210 if (StringUtils.isNotBlank(_sysadminMail)) 211 { 212 SendMailHelper.newMail() 213 .withSubject(subject) 214 .withHTMLBody(body) 215 .withSender(_mailFrom) 216 .withRecipient(_sysadminMail) 217 .sendMail(); 218 } 219 } 220 catch (MessagingException | IOException e) 221 { 222 getLogger().warn("Error sending the purge report e-mail.", e); 223 } 224 } 225 226 /** 227 * Send the error e-mail. 228 * @param startDate the purge start date. 229 * @param throwable the error. 230 */ 231 protected void sendErrorMail(Date startDate, Throwable throwable) 232 { 233 try 234 { 235 Map<String, String> params = getErrorEmailParams(startDate, throwable); 236 237 I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_PURGE_CONTENTS_REPORT_ERROR_SUBJECT"); 238 239 String subject = _i18nUtils.translate(i18nSubject); 240 String mailUri = getErrorMailUri(params); 241 String body = getMailBody(mailUri, params, throwable); 242 243 if (StringUtils.isNotBlank(_sysadminMail)) 244 { 245 SendMailHelper.newMail() 246 .withSubject(subject) 247 .withHTMLBody(body) 248 .withSender(_mailFrom) 249 .withRecipient(_sysadminMail) 250 .sendMail(); 251 } 252 } 253 catch (MessagingException | IOException e) 254 { 255 getLogger().warn("Error sending the purge error e-mail.", e); 256 } 257 } 258 259 /** 260 * Get the report e-mail parameters. 261 * @param startDate the purge start date. 262 * @param endDate the purge end date. 263 * @param totalContentsPurged the total count of contents of which versions were purged. 264 * @param totalVersionsPurged the total count of content versions removed. 265 * @return the e-mail parameters. 266 */ 267 protected Map<String, String> getEmailParams(Date startDate, Date endDate, int totalContentsPurged, int totalVersionsPurged) 268 { 269 Map<String, String> params = new HashMap<>(); 270 271 params.put("startDate", DateUtils.getISODateTimeFormatter().format(startDate.toInstant().atZone(ZoneId.systemDefault()))); 272 params.put("endDate", DateUtils.getISODateTimeFormatter().format(endDate.toInstant().atZone(ZoneId.systemDefault()))); 273 params.put("totalContentsPurged", Integer.toString(totalContentsPurged)); 274 params.put("totalVersionsPurged", Integer.toString(totalVersionsPurged)); 275 params.put("url", _baseUrl); 276 277 return params; 278 } 279 280 /** 281 * Get the error e-mail parameters. 282 * @param startDate the purge start date. 283 * @param throwable the error. 284 * @return the e-mail parameters. 285 */ 286 protected Map<String, String> getErrorEmailParams(Date startDate, Throwable throwable) 287 { 288 Map<String, String> params = new HashMap<>(); 289 290 params.put("startDate", DateUtils.getISODateTimeFormatter().format(startDate.toInstant().atZone(ZoneId.systemDefault()))); 291 params.put("url", _baseUrl); 292 293 return params; 294 } 295 296 /** 297 * Get a mail part. 298 * @param parameters the pipeline parameters. 299 * @return the mail part. 300 * @throws IOException If an error occurred 301 */ 302 protected String getMailBody(Map<String, String> parameters) throws IOException 303 { 304 String uri = getMailUri(parameters); 305 306 return getMailBody(uri, parameters, null); 307 } 308 309 /** 310 * Get a mail part. 311 * @param uri The url where to get the body of the mail 312 * @param parameters the pipeline parameters. 313 * @return the mail part. 314 * @throws IOException If an error occurred 315 */ 316 private String getMailBody(String uri, Map<String, String> parameters, Throwable throwable) throws IOException 317 { 318 Source source = null; 319 InputStream is = null; 320 try 321 { 322 source = _sourceResolver.resolveURI(uri, null, parameters); 323 is = source.getInputStream(); 324 325 try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) 326 { 327 SourceUtil.copy(is, bos); 328 329 String message = bos.toString("UTF-8"); 330 MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody() 331 .withTitle(new I18nizableText("plugin.cms", throwable != null ? "PLUGINS_CMS_PURGE_CONTENTS_REPORT_ERROR_SUBJECT" : "PLUGINS_CMS_PURGE_CONTENTS_REPORT_SUBJECT")) 332 .withMessage(message) 333 .withLink(_baseUrl, new I18nizableText("plugin.cms", "PLUGINS_CMS_PURGE_CONTENTS_REPORT_BODY_LINK")); 334 335 if (throwable != null) 336 { 337 String stackTrace = ExceptionUtils.getStackTrace(throwable); 338 bodyBuilder.withDetails(new I18nizableText("plugin.cms", "PLUGINS_CMS_PURGE_CONTENTS_REPORT_BODY_ERROR_TITLE"), stackTrace, true); 339 } 340 341 return bodyBuilder.build(); 342 } 343 } 344 finally 345 { 346 if (is != null) 347 { 348 is.close(); 349 } 350 351 if (source != null) 352 { 353 _sourceResolver.release(source); 354 } 355 } 356 } 357 358 /** 359 * Get the pipeline uri for mail body 360 * @param parameters the mail parameters 361 * @return a pipeline uri 362 */ 363 protected String getMailUri(Map<String, String> parameters) 364 { 365 return "cocoon://_plugins/cms/purge/purge-mail.html"; 366 } 367 368 /** 369 * Get the pipeline uri for error mail body. 370 * @param parameters the mail parameters 371 * @return a pipeline uri 372 */ 373 protected String getErrorMailUri(Map<String, String> parameters) 374 { 375 return "cocoon://_plugins/cms/purge/purge-error-mail.html"; 376 } 377 378}