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}