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