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