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