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}