001/*
002 *  Copyright 2012 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.archive;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.net.MalformedURLException;
021import java.text.DateFormat;
022import java.util.ArrayList;
023import java.util.Date;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import javax.mail.MessagingException;
030
031import org.apache.avalon.framework.configuration.Configuration;
032import org.apache.avalon.framework.configuration.ConfigurationException;
033import org.apache.avalon.framework.context.Context;
034import org.apache.avalon.framework.context.ContextException;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.cocoon.Constants;
038import org.apache.cocoon.environment.ObjectModelHelper;
039import org.apache.cocoon.environment.Request;
040import org.apache.cocoon.environment.background.BackgroundEnvironment;
041import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
042import org.apache.commons.lang.StringUtils;
043import org.apache.excalibur.source.Source;
044import org.apache.excalibur.source.SourceResolver;
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047
048import org.ametys.cms.content.archive.ArchiveConstants;
049import org.ametys.cms.repository.Content;
050import org.ametys.cms.repository.ContentQueryHelper;
051import org.ametys.core.authentication.AuthenticateAction;
052import org.ametys.core.engine.BackgroundEngineHelper;
053import org.ametys.core.right.RightManager;
054import org.ametys.core.user.User;
055import org.ametys.core.user.UserIdentity;
056import org.ametys.core.user.UserManager;
057import org.ametys.core.user.population.PopulationContextHelper;
058import org.ametys.core.util.I18nUtils;
059import org.ametys.core.util.mail.SendMailHelper;
060import org.ametys.plugins.repository.AmetysObjectIterable;
061import org.ametys.plugins.repository.AmetysObjectResolver;
062import org.ametys.plugins.repository.AmetysRepositoryException;
063import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
064import org.ametys.plugins.repository.query.expression.DateExpression;
065import org.ametys.plugins.repository.query.expression.Expression;
066import org.ametys.plugins.repository.query.expression.Expression.Operator;
067import org.ametys.runtime.config.Config;
068import org.ametys.runtime.i18n.I18nizableText;
069
070/**
071 * Runnable engine that archive the contents that have an scheduled archiving
072 * date set before the current date.
073 */
074public class ArchiveContentsEngine implements Runnable
075{
076    /** The logger. */
077    protected Logger _logger = LoggerFactory.getLogger(ArchiveContentsEngine.class);
078
079    /** The avalon context. */
080    protected Context _context;
081
082    /** The service manager. */
083    protected ServiceManager _manager;
084
085    /** The server base URL. */
086    protected String _baseUrl;
087
088    /** Is the engine initialized ? */
089    protected boolean _initialized;
090
091    /** The cocoon environment context. */
092    protected org.apache.cocoon.environment.Context _environmentContext;
093
094    /** The ametys object resolver. */
095    protected AmetysObjectResolver _ametysResolver;
096
097    /** The avalon source resolver. */
098    protected SourceResolver _sourceResolver;
099    
100    /** The rights manager */
101    protected RightManager _rightManager;
102
103    /** The users manager. */
104    protected UserManager _userManager;
105    
106    /** The i18n utils. */
107    protected I18nUtils _i18nUtils;
108    
109    /** The content of "from" field in emails. */
110    protected String _mailFrom;
111
112    /** The sysadmin mail address, to which will be sent the report e-mail. */
113    protected String _sysadminMail;
114    
115    /** The user e-mail notification will be sent to users that have this at least one of this rights. */
116    protected Set<String> _archiveRights;
117    
118    /** The user notification mail body i18n key. */
119    protected String _userMailBody;
120    
121    /** The user notification mail subject i18n key. */
122    protected String _userMailSubject;
123    
124    /** The user notification error mail body i18n key. */
125    protected String _userErrorMailBody;
126    
127    /** The user notification error mail subject i18n key. */
128    protected String _userErrorMailSubject;
129    
130    /**
131     * Initialize the archive engine.
132     * 
133     * @param manager the avalon service manager.
134     * @param context the avalon context.
135     * @throws ContextException If an error occurred
136     * @throws ServiceException If an error occurred
137     */
138    public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException
139    {
140        _manager = manager;
141        _context = context;
142        _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
143
144        // Lookup the needed components.
145        _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
146        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
147        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
148        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
149
150        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
151
152        _baseUrl = StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValueAsString("cms.url"), "index.html"), "/");
153        _mailFrom = Config.getInstance().getValueAsString("smtp.mail.from");
154        _sysadminMail = Config.getInstance().getValueAsString("smtp.mail.sysadminto");
155
156        _initialized = true;
157    }
158
159    /**
160     * Configure the engine (called by the scheduler).
161     * 
162     * @param configuration the component configuration.
163     * @throws ConfigurationException If an error occurred
164     */
165    public void configure(Configuration configuration) throws ConfigurationException
166    {
167        // Rights
168        Configuration rightsConf = configuration.getChild("rights");
169        
170        _archiveRights = new HashSet<>();
171        
172        for (Configuration rightConf : rightsConf.getChildren("right"))
173        {
174            String right = rightConf.getValue("");
175            if (StringUtils.isNotBlank(right))
176            {
177                _archiveRights.add(right);
178            }
179        }
180        
181        // Mails
182        Configuration validMailsConf = configuration.getChild("mails").getChild("valid");
183        Configuration errorMailsConf = configuration.getChild("mails").getChild("error");
184        
185        _userMailBody = validMailsConf.getChild("bodyKey").getValue();
186        _userMailSubject = validMailsConf.getChild("subjectKey").getValue();
187        
188        _userErrorMailBody = errorMailsConf.getChild("bodyKey").getValue();
189        _userErrorMailSubject = errorMailsConf.getChild("subjectKey").getValue();
190
191    }
192
193    /**
194     * Check the initialization and throw an exception if not initialized.
195     */
196    protected void checkInitialization()
197    {
198        if (!_initialized)
199        {
200            String message = "The engine must be initialized before it can be runned.";
201            _logger.error(message);
202            throw new IllegalStateException(message);
203        }
204    }
205
206    @Override
207    public void run()
208    {
209        Map<String, Object> environmentInformation = null;
210        long duration = 0;
211        try
212        {
213            _logger.info("Preparing the scheduled archiving process on contents...");
214
215            checkInitialization();
216
217            // Create the environment.
218            environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_manager, _environmentContext, new SLF4JLoggerAdapter(_logger));
219            BackgroundEnvironment environment = (BackgroundEnvironment) environmentInformation.get("environment");
220
221            // Authorize workflow actions and "check-auth" CMS action, from this background environment 
222            Request request = (Request) environment.getObjectModel().get(ObjectModelHelper.REQUEST_OBJECT);
223            request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED, true);
224
225            long start = System.currentTimeMillis();
226
227            // Get all the needed contents and archive them.
228            archiveContents(request);
229
230            long end = System.currentTimeMillis();
231
232            duration = (end - start) / 1000;
233        }
234        catch (Exception e)
235        {
236            _logger.error("An error occurred while archiving the contents.", e);
237        }
238        finally
239        {
240            // Leave the environment.
241            if (environmentInformation != null)
242            {
243                BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation);
244            }
245
246            // Dispose of the resources.
247            dispose();
248            _logger.info("Scheduled archiving process ended after " + duration + " seconds.");
249        }
250    }
251
252    /**
253     * Dispose of the resources and looked-up components.
254     */
255    protected void dispose()
256    {
257        // Release the components.
258        if (_manager != null)
259        {
260            _manager.release(_ametysResolver);
261        }
262
263        _ametysResolver = null;
264        _sourceResolver = null;
265        _userManager = null;
266        
267        _i18nUtils = null;
268
269        _environmentContext = null;
270        _context = null;
271        _manager = null;
272
273        _initialized = false;
274    }
275
276    /**
277     * Get the contents that need to be archived, and archive them.
278     * @param request The current request object
279     * 
280     * @throws AmetysRepositoryException if an error occurs.
281     * @throws IOException If an error occurred
282     * @throws MalformedURLException If an error occurred
283     */
284    protected void archiveContents(Request request) throws AmetysRepositoryException, MalformedURLException, IOException
285    {
286        // Get all the content which scheduled archiving date is passed.
287        Expression dateExpression = new DateExpression(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE, Operator.LE, new Date(), true);
288        String query = ContentQueryHelper.getContentXPathQuery(dateExpression);
289
290        try (AmetysObjectIterable<Content> contents = _ametysResolver.query(query))
291        {
292            List<Content> archivedContents = new ArrayList<>();
293            List<Content> contentsWithError = new ArrayList<>();
294           
295            try
296            {
297                for (Content content : contents)
298                {
299                    setRequestAttributes(request, content);
300                    
301                    Set<UserIdentity> users = _getAuthorizedContributors(content);
302                    
303                    Source source = null;
304                    try
305                    {
306                        String contentId = content.getId();
307                        
308                        String uri = getArchiveActionUri(contentId);
309                        source = _sourceResolver.resolveURI(uri);
310                        
311                        try (InputStream is =  source.getInputStream())
312                        {
313                            String workspaceBackup = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
314                            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, ArchiveConstants.ARCHIVE_WORKSPACE);
315                            
316                            Content archivedContent = _ametysResolver.resolveById(contentId);
317                            archivedContents.add(archivedContent);
318                            
319                            sendMailToContributors(archivedContent, users);
320                            
321                            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceBackup);
322                        }
323                    }
324                    catch (Exception e)
325                    {
326                        contentsWithError.add (content);
327                        sendErrorMailToContributors(content, users);
328                        _logger.error("Error while trying to archive the content : " + content.getId() + "\nThis content is probably not archived.", e);
329                    }
330                    finally
331                    {
332                        if (source != null)
333                        {
334                            _sourceResolver.release(source);
335                        }
336                    }
337                }
338            }
339            finally
340            {
341                // Send mail to administrator
342                sendMailToAdministrator(archivedContents, contentsWithError);
343            }
344        }
345    }
346    
347    /**
348     * Set the necessary request attributes 
349     * @param request The request
350     * @param content The content
351     */
352    protected void setRequestAttributes (Request request, Content content)
353    {
354        // Set the population contexts to be able to get allowed users
355        List<String> populationContexts = new ArrayList<>();
356        populationContexts.add("/application");
357        
358        request.setAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR, populationContexts);
359    }
360    
361    /**
362     * Get the pipeline uri for the archive action
363     * @param contentId the current contend id
364     * @return a pipeline uri
365     */
366    protected String getArchiveActionUri(String contentId)
367    {
368        return "cocoon://_plugins/cms/archives/archive/" + ArchiveConstants.ARCHIVE_WORKFLOW_ACTION_ID + "?contentId=" + contentId;
369    }
370
371    /**
372     * Send the archive report e-mail.
373     * @param archivedContents The list of archived contents
374     * @param contentsWithError The list of contents with error
375     */
376    protected void sendMailToAdministrator(List<Content> archivedContents, List<Content> contentsWithError)
377    {
378        if (archivedContents.size() != 0 || contentsWithError.size() != 0)
379        {
380            try
381            {
382                I18nizableText i18nSubject = new I18nizableText("plugin.cms", "PLUGINS_CMS_ARCHIVE_CONTENTS_REPORT_SUBJECT");
383                
384                List<String> bodyParams = getAdminEmailParams(archivedContents, contentsWithError);
385                I18nizableText i18nBody = new I18nizableText("plugin.cms", "PLUGINS_CMS_ARCHIVE_CONTENTS_REPORT_BODY", bodyParams);
386                
387                String subject = _i18nUtils.translate(i18nSubject);
388                String body = _i18nUtils.translate(i18nBody);
389                
390                if (StringUtils.isNotBlank(_sysadminMail))
391                {
392                    SendMailHelper.sendMail(subject, null, body, _sysadminMail, _mailFrom);
393                }
394            }
395            catch (MessagingException e)
396            {
397                _logger.warn("Error sending the archive report e-mail.", e);
398            }
399        }
400    }
401    
402
403    /**
404     * Get the report e-mail parameters.
405     * @param archivedContents The list of archived contents
406     * @param contentsWithError The list of contents with error
407     * @return the e-mail parameters.
408     */
409    protected List<String> getAdminEmailParams(List<Content> archivedContents, List<Content> contentsWithError)
410    {
411        List<String> params = new ArrayList<>();
412        
413        DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT);
414        params.add(df.format(new Date())); // {0}
415        
416        params.add(String.valueOf(archivedContents.size())); // {1}
417        params.add(_getContentsListAsString(archivedContents)); // {2}
418
419        params.add(String.valueOf(contentsWithError.size())); // {3}
420        params.add(_getContentsListAsString(contentsWithError)); // {4}
421        
422        params.add(_getRequestURI(null) + "/index.html"); // {5}
423
424        return params;
425    }
426    
427    /**
428     * Get the contents list as String
429     * @param contents The contents
430     * @return the list of contents as String
431     */
432    protected String _getContentsListAsString (List<Content> contents)
433    {
434        List<String> contentNames = new ArrayList<>();
435        for (Content content : contents)
436        {
437            contentNames.add("- " + content.getTitle(null));
438        }
439        
440        if (contentNames.size() > 0)
441        {
442            return StringUtils.join(contentNames, "\n");
443        }
444        else
445        {
446            return "";
447        }
448    }
449    
450    /**
451     * Get the authorized contributors to receive mail notification
452     * @param content The content to be archived
453     * @return The user logins
454     */
455    protected Set<UserIdentity> _getAuthorizedContributors (Content content)
456    {
457        Set<UserIdentity> users = new HashSet<>();
458        for (String right : _archiveRights)
459        {
460            users.addAll(_rightManager.getAllowedUsers(right, content).resolveAllowedUsers(Config.getInstance().getValueAsBoolean("runtime.mail.massive.sending")));
461        }
462        
463        return users;
464    }
465
466    /**
467     * Send the mail to alert users that the content has been archived.
468     * @param content The archived content 
469     * @param users The users
470     */
471    protected void sendMailToContributors(Content content, Set<UserIdentity> users)
472    {
473        try
474        {
475            List<String> params = getBodyParamsForContributors (content, true);
476            
477            I18nizableText i18nSubject = new I18nizableText(null, _userMailSubject, params);
478            I18nizableText i18nBody = new I18nizableText(null, _userMailBody, params);
479            
480            String subject = _i18nUtils.translate(i18nSubject);
481            String body = _i18nUtils.translate(i18nBody);
482            
483            _sendMailsToUsers(subject, body, users, _mailFrom);
484        }
485        catch (Exception e)
486        {
487            _logger.error("Unable to send mail to contributors after archiving content '" + content.getName() + "'", e);
488        }
489    }
490    
491    /**
492     * Get email body parameters
493     * @param content the archived content
494     * @param archived true if the content has archived
495     * @return The mail parameters
496     */
497    protected List<String> getBodyParamsForContributors (Content content, boolean archived)
498    {
499        List<String> params = new ArrayList<>();
500        
501        params.add(content.getTitle(null)); // {0}
502        params.add(DateFormat.getDateInstance(DateFormat.LONG).format(new Date())); // {1}
503        
504        if (archived)
505        {
506            params.add(_getRequestURI(content) + "/index.html?uitool=uitool-content,id:%27" + content.getId() + "%27,workspace:%27archives%27,%27ignore-workflow%27:%27true%27,%27content-message-type%27:%27archived-content%27"); // {2}
507        }
508        else
509        {
510            params.add(_getRequestURI(content) + "/index.html?uitool=uitool-content,id:%27" + content.getId() + "%27");
511        }
512        
513        return params;
514    }
515    
516    /**
517     * Send the mail to alert users that an error has occurred while trying to archive the content.
518     * @param content The content 
519     * @param users The users
520     */
521    protected void sendErrorMailToContributors(Content content, Set<UserIdentity> users)
522    {
523        try
524        {
525            List<String> params = getBodyParamsForContributors (content, false);
526            
527            I18nizableText i18nSubject = new I18nizableText(null, _userErrorMailSubject, params);
528            I18nizableText i18nBody = new I18nizableText(null, _userErrorMailBody, params);
529            
530            String subject = _i18nUtils.translate(i18nSubject);
531            String body = _i18nUtils.translate(i18nBody);
532            
533            _sendMailsToUsers(subject, body, users, _mailFrom);
534        }
535        catch (Exception e)
536        {
537            _logger.error("Unable to send mail to contributors after archiving content '" + content.getName() + "' failed", e);
538        }
539    }
540    
541    /**
542     * Get the request URI to set in mail
543     * @param content The content. Can be null
544     * @return the request URI
545     */
546    protected String _getRequestURI (Content content)
547    {
548        return _baseUrl;
549    }
550
551    /**
552     * Send the emails to users (contributors)
553     * @param subject the e-mail subject.
554     * @param body the e-mail body.
555     * @param users users to send the mail to.
556     * @param from the address sending the e-mail.
557     */
558    protected void _sendMailsToUsers(String subject, String body, Set<UserIdentity> users, String from)
559    {
560        for (UserIdentity userIdentity : users)
561        {
562            User user = _userManager.getUser(userIdentity.getPopulationId(), userIdentity.getLogin());
563            
564            if (user != null && StringUtils.isNotBlank(user.getEmail()))
565            {
566                String mail = user.getEmail();
567                
568                try
569                {
570                    SendMailHelper.sendMail(subject, null, body, mail, from);
571                }
572                catch (MessagingException e)
573                {
574                    if (_logger.isWarnEnabled())
575                    {
576                        _logger.warn("Could not send an archive notification e-mail to " + mail, e);
577                    }
578                }
579            }
580        }
581    }
582}