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.plugins.newsletter.auto;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.Date;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025
026import org.apache.avalon.framework.configuration.Configuration;
027import org.apache.avalon.framework.configuration.ConfigurationException;
028import org.apache.avalon.framework.context.Context;
029import org.apache.avalon.framework.context.ContextException;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.Constants;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.ObjectModelHelper;
035import org.apache.cocoon.environment.Request;
036import org.apache.cocoon.environment.background.BackgroundEnvironment;
037import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
038import org.apache.commons.lang.StringUtils;
039import org.joda.time.DateTime;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043import org.ametys.cms.filter.ContentFilter;
044import org.ametys.cms.filter.ContentFilterExtensionPoint;
045import org.ametys.cms.repository.Content;
046import org.ametys.cms.repository.RequestAttributeWorkspaceSelector;
047import org.ametys.cms.repository.WorkflowAwareContent;
048import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
049import org.ametys.cms.workflow.CreateContentFunction;
050import org.ametys.cms.workflow.SendMailFunction;
051import org.ametys.core.authentication.AuthenticateAction;
052import org.ametys.core.engine.BackgroundEngineHelper;
053import org.ametys.core.util.I18nUtils;
054import org.ametys.plugins.newsletter.category.Category;
055import org.ametys.plugins.newsletter.category.CategoryProvider;
056import org.ametys.plugins.newsletter.category.CategoryProviderExtensionPoint;
057import org.ametys.plugins.newsletter.workflow.CreateNewsletterFunction;
058import org.ametys.plugins.repository.AmetysObjectIterable;
059import org.ametys.plugins.repository.AmetysObjectResolver;
060import org.ametys.plugins.workflow.AbstractWorkflowComponent;
061import org.ametys.plugins.workflow.support.WorkflowProvider;
062import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
063import org.ametys.runtime.i18n.I18nizableText;
064import org.ametys.web.WebConstants;
065import org.ametys.web.filter.ContentFilterHelper;
066import org.ametys.web.filter.WebContentFilter;
067import org.ametys.web.repository.site.Site;
068import org.ametys.web.repository.site.SiteManager;
069import org.ametys.web.repository.sitemap.Sitemap;
070
071import com.opensymphony.workflow.WorkflowException;
072
073/**
074 * Runnable engine that creates the automatic newsletter contents.
075 */
076public class AutomaticNewslettersEngine implements Runnable
077{
078    
079    /** The logger. */
080    protected static final Logger _LOGGER = LoggerFactory.getLogger(AutomaticNewslettersEngine.class);
081    
082    /** The newsletter content type. */
083    protected static final String _NEWSLETTER_CONTENT_TYPE = "org.ametys.plugins.newsletter.Content.newsletter";
084    
085    /** The avalon context. */
086    protected Context _context;
087    
088    /** The service manager. */
089    protected ServiceManager _manager;
090    
091    /** Is the engine initialized ? */
092    protected boolean _initialized;
093    
094    /** The instant the engine was started. */
095    protected Date _runDate;
096    
097    /** The newsletter workflow name. */
098    protected String _workflowName;
099    
100    /** The workflow initial action ID. */
101    protected int _wfInitialActionId;
102    
103    /** A list of action IDs to validate a newsletter from initial step. */
104    protected List<Integer> _wfValidateActionIds;
105    
106    /** A map of the content IDs by filter, reset on each run. */
107    protected Map<String, List<String>> _filterContentIdCache;
108    
109    /** The cocoon environment context. */
110    protected org.apache.cocoon.environment.Context _environmentContext;
111    
112    /** The ametys object resolver. */
113    protected AmetysObjectResolver _resolver;
114    
115    /** The site manager. */
116    protected SiteManager _siteManager;
117    
118    /** The workflow provider. */
119    protected WorkflowProvider _workflowProvider;
120    
121    /** The automatic newsletter extension point. */
122    protected AutomaticNewsletterExtensionPoint _autoNewsletterEP;
123    
124    /** The newsletter category provider extension point. */
125    protected CategoryProviderExtensionPoint _categoryEP;
126    
127    /** The content filter extension point. */
128    protected ContentFilterExtensionPoint _contentFilterEP;
129    
130    /** The content filter helper. */
131    protected ContentFilterHelper _contentFilterHelper;
132    
133    /** The i18n utils. */
134    protected I18nUtils _i18nUtils;
135    
136    /**
137     * Initialize the engine.
138     * @param manager the avalon service manager.
139     * @param context the avalon context.
140     * @throws ContextException if an error occurred
141     * @throws ServiceException if an error occurred
142     */
143    public void initialize(ServiceManager manager, Context context) throws ContextException, ServiceException
144    {
145        _manager = manager;
146        _context = context;
147        _environmentContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
148        
149        // Lookup the needed components.
150        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
151        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
152        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
153        _autoNewsletterEP = (AutomaticNewsletterExtensionPoint) manager.lookup(AutomaticNewsletterExtensionPoint.ROLE);
154        _categoryEP = (CategoryProviderExtensionPoint) manager.lookup(CategoryProviderExtensionPoint.ROLE);
155        _contentFilterEP = (ContentFilterExtensionPoint) manager.lookup(ContentFilterExtensionPoint.ROLE);
156        _contentFilterHelper = (ContentFilterHelper) manager.lookup(ContentFilterHelper.ROLE);
157        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
158        
159        _filterContentIdCache = new HashMap<>();
160        
161        _initialized = true;
162    }
163    
164    /**
165     * Configure the engine (to be called by the scheduler or the action).
166     * @param configuration the component configuration.
167     * @throws ConfigurationException if an error occurs in the configuration.
168     */
169    public void configure(Configuration configuration) throws ConfigurationException
170    {
171        Configuration workflowConf = configuration.getChild("workflow");
172        _workflowName = workflowConf.getAttribute("name");
173        _wfInitialActionId = workflowConf.getAttributeAsInteger("initialActionId");
174        
175        String[] validateActionIds = StringUtils.split(workflowConf.getAttribute("validateActionIds"), ", ");
176        _wfValidateActionIds = new ArrayList<>(validateActionIds.length);
177        for (String actionId : validateActionIds)
178        {
179            try
180            {
181                _wfValidateActionIds.add(Integer.valueOf(actionId));
182            }
183            catch (NumberFormatException e)
184            {
185                throw new ConfigurationException("Invalid validation action ID.", e);
186            }
187        }
188    }
189    
190    /**
191     * Check the initialization and throw an exception if not initialized.
192     */
193    protected void checkInitialization()
194    {
195        if (!_initialized)
196        {
197            String message = "The automatic newsletter engine must be properly initialized before it's run.";
198            _LOGGER.error(message);
199            throw new IllegalStateException(message);
200        }
201    }
202    
203    @Override
204    public void run()
205    {
206        Map<String, Object> environmentInformation = null;
207        long duration = 0;
208        try
209        {
210            if (_LOGGER.isInfoEnabled())
211            {
212                _LOGGER.info("Preparing to create the automatic newsletter contents...");
213            }
214            
215            checkInitialization();
216            
217            // Store the date and time.
218            _runDate = new Date();
219            
220            // Create the environment.
221            environmentInformation = BackgroundEngineHelper.createAndEnterEngineEnvironment(_manager, _environmentContext, new SLF4JLoggerAdapter(_LOGGER));
222            
223            BackgroundEnvironment environment = (BackgroundEnvironment) environmentInformation.get("environment");
224            
225            // Authorize workflow actions and "check-auth" CMS action, from this background environment.
226            Request request = (Request) environment.getObjectModel().get(ObjectModelHelper.REQUEST_OBJECT);
227            request.setAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_INTERNAL_ALLOWED, true);
228            
229            long start = System.currentTimeMillis();
230            
231            // Get all the contents and purge the old versions.
232            createAutomaticNewsletters();
233            
234            long end = System.currentTimeMillis();
235            
236            duration = (end - start) / 1000;
237        }
238        catch (Exception e)
239        {
240            _LOGGER.error("An error occurred creating the automatic newsletter contents.", e);
241        }
242        finally
243        {
244            // Leave the environment.
245            if (environmentInformation != null)
246            {
247                BackgroundEngineHelper.leaveEngineEnvironment(environmentInformation);
248            }
249            
250            // Dispose of the resources.
251            dispose();
252            
253            if (_LOGGER.isInfoEnabled())
254            {
255                _LOGGER.info("Automatic newsletter creation ended after " + duration + " seconds.");
256            }
257        }
258    }
259    
260    /**
261     * Dispose of the resources and looked-up components.
262     */
263    protected void dispose()
264    {
265        // Release the components.
266        if (_manager != null)
267        {
268            _manager.release(_resolver);
269        }
270        
271        _resolver = null;
272        
273        _environmentContext = null;
274        _context = null;
275        _manager = null;
276        
277        _runDate = null;
278        
279        _initialized = false;
280    }
281    
282    /**
283     * Launch the creation process for each site and sitemap.
284     */
285    protected void createAutomaticNewsletters()
286    {
287        // Reset the cache.
288        _filterContentIdCache.clear();
289        
290        try (AmetysObjectIterable<Site> sites = _siteManager.getSites();)
291        {
292            for (Site site : sites)
293            {
294                try (AmetysObjectIterable<Sitemap> sitemaps = site.getSitemaps();)
295                {
296                    for (Sitemap sitemap : sitemaps)
297                    {
298                        createAutomaticNewsletters(site.getName(), sitemap.getName());
299                    }
300                }
301            }
302        }
303    }
304    
305    /**
306     * Test each category in a site and sitemap and launch the newsletter creation if needed.
307     * @param siteName the site name.
308     * @param sitemapName the sitemap name.
309     */
310    protected void createAutomaticNewsletters(String siteName, String sitemapName)
311    {
312        Request request = ContextHelper.getRequest(_context);
313        request.setAttribute("siteName", siteName);
314        
315        for (String providerId : _categoryEP.getExtensionsIds())
316        {
317            CategoryProvider provider = _categoryEP.getExtension(providerId);
318            
319            // Browse all categories for this site and sitemap. 
320            for (Category category : provider.getAllCategories(siteName, sitemapName))
321            {
322                // Get the automatic newsletter assigned to this category.
323                Collection<String> automaticIds = provider.getAutomaticIds(category.getId());
324                
325                for (String autoNewsletterId : automaticIds)
326                {
327                    AutomaticNewsletter autoNewsletter = _autoNewsletterEP.getExtension(autoNewsletterId);
328                    
329                    // Test if an automatic newsletter content has to be created today.
330                    if (autoNewsletter != null && createNow(autoNewsletter))
331                    {
332                        createAndValidateAutomaticNewsletter(siteName, sitemapName, category, provider, autoNewsletter);
333                    }
334                }
335            }
336            
337        }
338    }
339    
340    /**
341     * Create an automatic newsletter content in a category.
342     * @param sitemapName the sitemap name.
343     * @param siteName the site name.
344     * @param category the newsletter category.
345     * @param provider the category provider.
346     * @param autoNewsletter the associated automatic newsletter.
347     */
348    protected void createAndValidateAutomaticNewsletter(String siteName, String sitemapName, Category category, CategoryProvider provider, AutomaticNewsletter autoNewsletter)
349    {
350        if (_LOGGER.isInfoEnabled())
351        {
352            _LOGGER.info("Preparing to create an automatic newsletter for category " + category.getId() + " in " + siteName + " and sitemap " + sitemapName);
353        }
354        
355        // Get the list of content IDs by filter name.
356        Map<String, AutomaticNewsletterFilterResult> contentsByFilter = getFilterResults(siteName, sitemapName, autoNewsletter);
357        
358        try
359        {
360            if (hasResults(contentsByFilter.values()))
361            {
362                // Compute the next newsletter number in this category.
363                long nextNumber = getNextNumber(category, provider, siteName, sitemapName);
364                
365                // Create newsletter content.
366                WorkflowAwareContent content = createNewsletterContent(siteName, sitemapName, category, autoNewsletter, nextNumber, contentsByFilter);
367                
368                // Validate and send.
369                validateNewsletter(content);
370            }
371            else
372            {
373                if (_LOGGER.isInfoEnabled())
374                {
375                    _LOGGER.info("No content has been returned by the filters for the automatic newsletter in category " + category.getId() + " in site " + siteName + " and sitemap " + sitemapName + ": no newsletter has been created.");
376                }
377            }
378        }
379        catch (WorkflowException e)
380        {
381            _LOGGER.error("Unable to create and validate an automatic newsletter for category " + category.getId() + " in site " + siteName + " and sitemap " + sitemapName, e);
382        }
383    }
384    
385    /**
386     * Get the list of contents for the automatic newsletter filters.
387     * @param siteName the site name.
388     * @param sitemapName the sitemap name.
389     * @param autoNewsletter the automatic newsletter.
390     * @return the results, indexed by filter name (in the auto newsletter).
391     */
392    protected Map<String, AutomaticNewsletterFilterResult> getFilterResults(String siteName, String sitemapName, AutomaticNewsletter autoNewsletter)
393    {
394        Map<String, AutomaticNewsletterFilterResult> contentsByFilter = new HashMap<>();
395        
396        Request request = ContextHelper.getRequest(_context);
397        
398        Map<String, String> filters = autoNewsletter.getFilters();
399        
400        for (String name : filters.keySet())
401        {
402            String filterId = filters.get(name);
403            
404            AutomaticNewsletterFilterResult result = new AutomaticNewsletterFilterResult();
405            contentsByFilter.put(name, result);
406            
407            List<String> contentIds = new ArrayList<>();
408            
409            ContentFilter filter = _contentFilterEP.getExtension(filterId);
410            
411            if (filter != null && filter instanceof WebContentFilter)
412            {
413                WebContentFilter webFilter = (WebContentFilter) filter;
414                result.setMetadataSetName(filter.getView());
415                
416                String cacheKey = siteName + "/" + sitemapName + "/" + filterId;
417                
418                if (_filterContentIdCache.containsKey(cacheKey))
419                {
420                    contentIds = _filterContentIdCache.get(cacheKey);
421                }
422                else
423                {
424                    // Get the contents in the live workspace.
425                    String originalWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
426                    RequestAttributeWorkspaceSelector.setForcedWorkspace(request, WebConstants.LIVE_WORKSPACE);
427                    
428                    contentIds = _contentFilterHelper.getMatchingContentIds(webFilter, siteName, sitemapName, null);
429                    
430                    // Set the workspace back to its original value.
431                    RequestAttributeWorkspaceSelector.setForcedWorkspace(request, originalWorkspace);
432                    
433                    // Cache the results.
434                    _filterContentIdCache.put(cacheKey, contentIds);
435                }
436            }
437            
438            result.setContentIds(contentIds);
439        }
440        
441        return contentsByFilter;
442    }
443    
444    /**
445     * Create the newsletter content.
446     * @param siteName the site name.
447     * @param language the language.
448     * @param category the category.
449     * @param autoNewsletter the automatic newsletter.
450     * @param newsletterNumber the newsletter number.
451     * @param filterResults the filter results (content IDs for each filter).
452     * @return The newly created newsletter content.
453     * @throws WorkflowException if a workflow error occurs.
454     */
455    protected WorkflowAwareContent createNewsletterContent(String siteName, String language, Category category, AutomaticNewsletter autoNewsletter, long newsletterNumber, Map<String, AutomaticNewsletterFilterResult> filterResults) throws WorkflowException
456    {
457        String contentName = category.getName() + "-" + newsletterNumber;
458        
459        String title = getNewsletterTitle(language, category, autoNewsletter, newsletterNumber);
460        
461        Map<String, Object> params = new HashMap<>();
462        
463        // Workflow result.
464        Map<String, Object> workflowResult = new HashMap<>();
465        params.put(AbstractWorkflowComponent.RESULT_MAP_KEY, workflowResult);
466        
467        // Workflow parameters.
468        params.put("workflowName", _workflowName);
469        params.put(org.ametys.web.workflow.CreateContentFunction.SITE_KEY, siteName);
470        params.put(CreateContentFunction.CONTENT_NAME_KEY, contentName);
471        params.put(CreateContentFunction.CONTENT_TITLE_KEY, title);
472        params.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[]{_NEWSLETTER_CONTENT_TYPE});
473        params.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, language);
474        params.put(CreateNewsletterFunction.NEWSLETTER_CATEGORY_KEY, category.getId());
475        params.put(CreateNewsletterFunction.NEWSLETTER_NUMBER_KEY, new Long(newsletterNumber));
476        params.put(CreateNewsletterFunction.NEWSLETTER_DATE_KEY, _runDate);
477        params.put(CreateNewsletterFunction.NEWSLETTER_IS_AUTOMATIC_KEY, "true");
478        params.put(CreateNewsletterFunction.NEWSLETTER_PROCESS_AUTO_SECTIONS_KEY, "true");
479        params.put(CreateNewsletterFunction.NEWSLETTER_CONTENT_ID_MAP_KEY, filterResults);
480        
481        // Trigger the creation.
482        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow();
483        workflow.initialize(_workflowName, _wfInitialActionId, params);
484        
485        // Get the content in the results and return it.
486        WorkflowAwareContent content = (WorkflowAwareContent) workflowResult.get(AbstractContentWorkflowComponent.CONTENT_KEY);
487        
488        return content;
489    }
490    
491    /**
492     * Validate the newly created newsletter.
493     * @param newsletterContent the newsletter content, must be in draft state.
494     * @throws WorkflowException if a workflow error occurs.
495     */
496    protected void validateNewsletter(WorkflowAwareContent newsletterContent) throws WorkflowException
497    {
498        long workflowId = newsletterContent.getWorkflowId();
499        
500        Map<String, Object> inputs = new HashMap<>();
501        
502        inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, newsletterContent);
503        // Do not send workflow mail notifications.
504        inputs.put(SendMailFunction.SEND_MAIL, "false");
505        
506        // Without this attribute, the newsletter is not sent to subscribers.
507        Request request = ContextHelper.getRequest(_context);
508        request.setAttribute("send", "true");
509        
510        // Successively execute all the configured actions.
511        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(newsletterContent);
512        for (Integer actionId : _wfValidateActionIds)
513        {
514            workflow.doAction(workflowId, actionId, inputs);
515        }
516    }
517    
518    /**
519     * Compute the newsletter title.
520     * @param language the language.
521     * @param category the newsletter category.
522     * @param autoNewsletter the automatic newsletter.
523     * @param newsletterNumber the newsletter number.
524     * @return the newsletter title.
525     */
526    protected String getNewsletterTitle(String language, Category category, AutomaticNewsletter autoNewsletter, long newsletterNumber)
527    {
528        String title = "";
529        
530        I18nizableText newsletterTitle = autoNewsletter.getNewsletterTitle();
531        if (newsletterTitle == null || StringUtils.isEmpty(newsletterTitle.toString()))
532        {
533            // The newsletter title is not set in the auto newsletter:
534            // create the newsletter title from the category title.
535            String categoryTitle = _i18nUtils.translate(category.getTitle(), language);
536            title = categoryTitle + " " + newsletterNumber;
537        }
538        else if (newsletterTitle.isI18n())
539        {
540            // The newsletter title is set as a parametrizable I18nizableText in the auto newsletter.
541            Map<String, I18nizableText> params = Collections.singletonMap("number", new I18nizableText(Long.toString(newsletterNumber)));
542            I18nizableText titleI18n = new I18nizableText(newsletterTitle.getCatalogue(), newsletterTitle.getKey(), params);
543            title = _i18nUtils.translate(titleI18n, language);
544        }
545        else
546        {
547            // The newsletter title is set as a non-18n I18nizableText.
548            title = newsletterTitle.getLabel() + " " + newsletterNumber;
549        }
550        
551        return title;
552    }
553    
554    /**
555     * Compute the newsletter number.
556     * @param category the newsletter category.
557     * @param provider the category provider.
558     * @param siteName the site name.
559     * @param language the language.
560     * @return the newsletter number.
561     */
562    protected long getNextNumber(Category category, CategoryProvider provider, String siteName, String language)
563    {
564        long number = 0;
565        
566        // Browse all existing numbers to get the highest number.
567        try (AmetysObjectIterable<Content> newsletters = provider.getNewsletters(category.getId(), siteName, language);)
568        {
569            for (Content newsletterContent : newsletters)
570            {
571                long contentNumber = newsletterContent.getMetadataHolder().getLong("newsletter-number", 0);
572                
573                // Keep the number if it's higher.
574                number = Math.max(number, contentNumber);
575            }
576            
577            // Return the next newsletter number.
578            return number + 1;
579        }
580    }
581    
582    /**
583     * Test if there is at least one content in a collection of filter results.
584     * @param results a collection of filter results.
585     * @return true if at least one filter yielded a result, false otherwise.
586     */
587    protected boolean hasResults(Collection<AutomaticNewsletterFilterResult> results)
588    {
589        boolean hasResults = false;
590        
591        for (AutomaticNewsletterFilterResult result : results)
592        {
593            if (result.hasResults())
594            {
595                hasResults = true;
596            }
597        }
598        
599        return hasResults;
600    }
601    
602    /**
603     * Test if an automatic newsletter content has to be created now.
604     * @param autoNewsletter the automatic newsletter.
605     * @return true if an automatic newsletter content has to be created now, false otherwise.
606     */
607    protected boolean createNow(AutomaticNewsletter autoNewsletter)
608    {
609        boolean createToday = false;
610        
611        // The time the engine was launched.
612        DateTime runDate = new DateTime(_runDate.getTime());
613        
614        // The days at which the newsletter has to be created.
615        Collection<Integer> dayNumbers = autoNewsletter.getDayNumbers();
616        
617        switch(autoNewsletter.getFrequencyType())
618        {
619            case MONTH:
620                // Test with a month frequency.
621                createToday = testMonth(dayNumbers, runDate);
622                break;
623            case WEEK:
624                // Test with a week frequency.
625                createToday = testWeek(dayNumbers, runDate);
626                break;
627            default:
628                break;
629        }
630        
631        return createToday;
632    }
633    
634    /**
635     * Test if we are in the configured month creation period.
636     * @param dayNumbers the days in the month on which a newsletter is to be created.
637     * @param runDate the instant the engine was launched.
638     * @return true if we are in the configured month creation period, false otherwise.
639     */
640    protected boolean testMonth(Collection<Integer> dayNumbers, DateTime runDate)
641    {
642        boolean createToday = false;
643        
644        int dayOfMonth = runDate.getDayOfMonth();
645        int lastDayOfMonth = runDate.dayOfMonth().withMaximumValue().getDayOfMonth();
646        
647        for (Integer dayNumber : dayNumbers)
648        {
649            if (dayNumber.intValue() == dayOfMonth)
650            {
651                createToday = true;
652            }
653            else if (dayNumber.intValue() > lastDayOfMonth && dayOfMonth == lastDayOfMonth)
654            {
655                // If the configured day is outside the current month (for instance, if "31" is configured),
656                // run the last day of the current month.
657                createToday = true;
658            }
659        }
660        
661        return createToday;
662    }
663    
664    /**
665     * Test if we are in the configured week creation period.
666     * @param dayNumbers the days in the month on which a newsletter is to be created.
667     * @param runDate the instant the engine was launched.
668     * @return true if we are in the configured week creation period, false otherwise.
669     */
670    protected boolean testWeek(Collection<Integer> dayNumbers, DateTime runDate)
671    {
672        boolean createToday = false;
673        
674        int dayOfWeek = runDate.getDayOfWeek();
675        
676        for (Integer dayNumber : dayNumbers)
677        {
678            if (dayNumber.intValue() == dayOfWeek)
679            {
680                createToday = true;
681            }
682        }
683        
684        return createToday;
685    }
686    
687}