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