001/*
002 *  Copyright 2013 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.calendar.events;
017
018import java.time.LocalDate;
019import java.time.ZoneId;
020import java.time.ZonedDateTime;
021import java.time.format.DateTimeFormatter;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Calendar;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.LinkedHashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Optional;
034import java.util.Set;
035import java.util.stream.Collectors;
036
037import org.apache.avalon.framework.component.Component;
038import org.apache.avalon.framework.logger.AbstractLogEnabled;
039import org.apache.avalon.framework.service.ServiceException;
040import org.apache.avalon.framework.service.ServiceManager;
041import org.apache.avalon.framework.service.Serviceable;
042import org.apache.commons.lang3.ArrayUtils;
043import org.apache.commons.lang3.StringUtils;
044
045import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
046import org.ametys.cms.filter.ContentFilter;
047import org.ametys.cms.filter.ContentFilterExtensionPoint;
048import org.ametys.cms.tag.Tag;
049import org.ametys.cms.tag.TagProviderExtensionPoint;
050import org.ametys.core.util.DateUtils;
051import org.ametys.core.util.JSONUtils;
052import org.ametys.plugins.calendar.events.EventsFilter.EventFilterSearchContext;
053import org.ametys.plugins.repository.AmetysObjectResolver;
054import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
055import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeater;
056import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareRepeaterEntry;
057import org.ametys.plugins.repository.query.expression.AndExpression;
058import org.ametys.plugins.repository.query.expression.DateExpression;
059import org.ametys.plugins.repository.query.expression.Expression;
060import org.ametys.plugins.repository.query.expression.Expression.Operator;
061import org.ametys.plugins.repository.query.expression.MetadataExpression;
062import org.ametys.plugins.repository.query.expression.NotExpression;
063import org.ametys.plugins.repository.query.expression.OrExpression;
064import org.ametys.web.filter.WebContentFilter.AccessLimitation;
065import org.ametys.web.filter.WebContentFilter.Context;
066import org.ametys.web.filter.WebContentFilter.FilterSearchContext;
067import org.ametys.web.frontoffice.search.instance.model.SiteContextType;
068import org.ametys.web.repository.page.ZoneItem;
069import org.ametys.web.repository.site.SiteManager;
070
071/**
072 * Helper for events filter
073 *
074 */
075public class EventsFilterHelper extends AbstractLogEnabled implements Serviceable, Component
076{
077    /** The avalon role */
078    public static final String ROLE = EventsFilterHelper.class.getName();
079
080    /** The start date metadata. */
081    public static final String START_DATE_META = "start-date";
082
083    /** The end date metadata. */
084    public static final String END_DATE_META = "end-date";
085    
086    /** The events filter ID. */
087    public static final String EVENTS_FILTER_ID = "events";
088
089    
090    /** The tag provider extension point. */
091    protected TagProviderExtensionPoint _tagProviderEP;
092    /** Extension point for content filters */
093    protected ContentFilterExtensionPoint _filterExtPt;
094    /** The Ametys object resolver */
095    protected AmetysObjectResolver _ametysResolver;
096    /** Extension point for content type */
097    protected ContentTypeExtensionPoint _contentTypeEP;
098    /** The site manager */
099    protected SiteManager _siteManager;
100    /** The JSON utils */
101    protected JSONUtils _jsonUtils;
102
103    @Override
104    public void service(ServiceManager manager) throws ServiceException 
105    {
106        _tagProviderEP = (TagProviderExtensionPoint) manager.lookup(TagProviderExtensionPoint.ROLE);
107        _filterExtPt = (ContentFilterExtensionPoint) manager.lookup(ContentFilterExtensionPoint.ROLE);
108        _ametysResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
109        _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
110        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
111        _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE);
112    }
113
114    /**
115     * Create a events filter from the static "events" filter
116     * @param id the id of filter to create
117     * @return the created events filter
118     */
119    public EventsFilter createEventsFilter(String id)
120    {
121        EventsFilter eventsFilter = (EventsFilter) _filterExtPt.getExtension(EventsFilterHelper.EVENTS_FILTER_ID);
122        return new EventsFilter(id, eventsFilter, _ametysResolver, _contentTypeEP, _siteManager, _tagProviderEP);
123    }
124    
125    /**
126     * Get the service title.
127     * 
128     * @param zoneItem the zone item.
129     * @return the service title.
130     */
131    public String getTitle(ZoneItem zoneItem)
132    {
133        return zoneItem.getServiceParameters().getValue("title", false, "");
134    }
135
136    /**
137     * Get the link.
138     * 
139     * @param zoneItem the zone item.
140     * @return the link.
141     */
142    public String getLink(ZoneItem zoneItem)
143    {
144        return zoneItem.getServiceParameters().getValue("link", false, "");
145    }
146
147    /**
148     * Get the link title.
149     * 
150     * @param zoneItem the zone item.
151     * @return the link title.
152     */
153    public String getLinkTitle(ZoneItem zoneItem)
154    {
155        return zoneItem.getServiceParameters().getValue("link-title", false, "");
156    }
157
158    /**
159     * Get the default range type parameter value.
160     * 
161     * @param zoneItem the zone item.
162     * @return the default range type.
163     */
164    public String getDefaultRangeType(ZoneItem zoneItem)
165    {
166        ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
167        // First check that there is a definition because calendar service doesn't define the default-range parameter
168        return serviceParameters.hasDefinition("default-range") ? serviceParameters.getValue("default-range", false, StringUtils.EMPTY) : StringUtils.EMPTY;
169    }
170
171    /**
172     * Mask orphan?
173     * 
174     * @param zoneItem the zone item.
175     * @return mask orphan.
176     */
177    public boolean getMaskOrphan(ZoneItem zoneItem)
178    {
179        ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
180        // First check that there is a definition because calendar service doesn't define the mask-orphan parameter
181        return serviceParameters.hasDefinition("mask-orphan") ? serviceParameters.getValue("mask-orphan", false, false) : false;
182    }
183
184    /**
185     * Get the access limitation policy.
186     * @param zoneItem the zone item.
187     * @return the access limitation policy.
188     */
189    public AccessLimitation getAccessLimitation(ZoneItem zoneItem)
190    {
191        ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
192        // First check that there is a definition because calendar service doesn't define the handle-user-access parameter
193        if (serviceParameters.hasDefinition("handle-user-access"))
194        {
195            boolean handleUserAccess = serviceParameters.getValue("handle-user-access", false, false);
196            return handleUserAccess ? AccessLimitation.USER_ACCESS : AccessLimitation.PAGE_ACCESS;
197        }
198        else
199        {
200            return AccessLimitation.PAGE_ACCESS;
201        }
202    }
203    
204    /**
205     * Is there a PDF download link on the service?
206     * 
207     * @param zoneItem the zone item.
208     * @return true if a PDF download link will be present, false otherwise.
209     */
210    protected boolean getPdfDownload(ZoneItem zoneItem)
211    {
212        ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
213        // First check that there is a definition because calendar service doesn't define the pdf-download parameter
214        return serviceParameters.hasDefinition("pdf-download") ? serviceParameters.getValue("pdf-download", false, false) : false;
215    }
216
217    /**
218     * Is there an iCalendar export link on the service?
219     * 
220     * @param zoneItem the zone item.
221     * @return true if an iCalendar export link will be present, false
222     *         otherwise.
223     */
224    protected boolean getIcalDownload(ZoneItem zoneItem)
225    {
226        ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
227        // First check that there is a definition because calendar service doesn't define the ical-download parameter
228        return serviceParameters.hasDefinition("ical-download") ? serviceParameters.getValue("ical-download", false, false) : false;
229    }
230    
231    /**
232     * Get the content types through the service parameters.
233     * 
234     * @param zoneItem the zone item.
235     * @return the content types.
236     */
237    public Set<String> getContentTypes(ZoneItem zoneItem)
238    {
239        Set<String> contentTypes = new LinkedHashSet<>();
240        
241        String[] cTypes = zoneItem.getServiceParameters().getValue("content-types", false, ArrayUtils.EMPTY_STRING_ARRAY);
242        if (cTypes.length > 0 && cTypes[0].length() > 0)
243        {
244            for (String cType : cTypes)
245            {
246                if (!"".equals(cType))
247                {
248                    contentTypes.add(cType);
249                }
250            }
251        }
252
253        return contentTypes;
254    }
255
256    /**
257     * Get the all tags from the search contexts
258     * @param zoneItem the zone item.
259     * @param searchContexts the search contexts
260     * @return the tags.
261     */
262    public Set<String> getTags(ZoneItem zoneItem, List<Map<String, Object>> searchContexts)
263    {
264        HashSet<String> hashSet = new HashSet<>();
265        if (searchContexts == null)
266        {
267            // get search contexts from zoneitem
268            ModifiableModelAwareRepeater search = zoneItem.getServiceParameters().getValue("search");
269            for (ModifiableModelAwareRepeaterEntry repeaterEntry : search.getEntries())
270            {
271                hashSet.addAll(Arrays.asList(repeaterEntry.getValue("tags", false, ArrayUtils.EMPTY_STRING_ARRAY)));
272            }
273        }
274        else
275        {
276            for (Map<String, Object> entry : searchContexts)
277            {
278                hashSet.addAll(Arrays.asList((String[]) entry.get("tags")));
279            }
280        }
281
282        return hashSet;
283    }
284    
285    /**
286     * Get the all selected tags to be used as categories from the search contexts
287     * @param zoneItem the zone item.
288     * @param searchContexts the search contexts
289     * @param currentSiteName the current site name.
290     * @return the tags categories.
291     */
292    public Set<Tag> getTagCategories(ZoneItem zoneItem, List<Map<String, Object>> searchContexts, String currentSiteName)
293    {
294        Set<Tag> categories = new LinkedHashSet<>();
295        List<Map<String, Object>> search = searchContexts;
296
297        if (search == null)
298        {
299            // if null, get search contexts from zoneitem (only need sites and tag categories values)
300            search = new ArrayList<>();
301            ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters();
302
303            HashSet<String> hashSet = new HashSet<>();
304            ModifiableModelAwareRepeater searchRepeater = serviceParameters.getValue("search");
305            
306            for (ModifiableModelAwareRepeaterEntry repeaterEntry : searchRepeater.getEntries())
307            {
308                Map<String, Object> mapEntry = new HashMap<>();
309                // First check that there is a definition because calendar service doesn't define the tag-categories parameter
310                if (repeaterEntry.hasDefinition("tag-categories"))
311                {
312                    mapEntry.put("tag-categories", repeaterEntry.getValue("tag-categories", false, ArrayUtils.EMPTY_STRING_ARRAY));
313                    hashSet.addAll(Arrays.asList(repeaterEntry.getValue("tag-categories", false, ArrayUtils.EMPTY_STRING_ARRAY)));
314                }
315                if (repeaterEntry.hasValue("sites"))
316                {
317                    mapEntry.put("sites", repeaterEntry.getValue("sites", false, ArrayUtils.EMPTY_STRING_ARRAY));
318                }
319                search.add(mapEntry);
320            }
321            
322        }
323        
324        
325        search.forEach(searchContext -> 
326        {
327            if (searchContext.containsKey("tag-categories"))
328            {
329                String[] categoriesArray = (String[]) searchContext.get("tag-categories");
330                for (int i = 0; i < categoriesArray.length; i++)
331                {
332                    Map<String, Object> sitesData = _jsonUtils.convertJsonToMap((String) searchContext.get("sites"));
333                    
334                    @SuppressWarnings("unchecked")
335                    List<String> sites = (List<String>) sitesData.get("sites");
336                    String context = (String) sitesData.get("context");
337                    SiteContextType siteContextType = SiteContextType.fromClientSideName(context);
338
339                    Map<String, Object> tagParameters = new HashMap<>();
340                    if (SiteContextType.CURRENT.equals(siteContextType))
341                    {
342                        tagParameters.put("siteName", currentSiteName);
343                    }
344                    else if (SiteContextType.AMONG.equals(siteContextType) && sites.size() == 1)
345                    {
346                        tagParameters.put("siteName", sites.get(0));
347                    }
348                    else
349                    {
350                        tagParameters.put("siteName", null);
351                    }
352                    
353                    Tag category = _tagProviderEP.getTag(categoriesArray[i], tagParameters);
354                    if (category != null)
355                    {
356                        categories.add(category);
357                    }
358                    
359                }
360            }
361        });
362        
363
364        return categories;
365    }
366
367    /**
368     * Get all the descendant tags belonging to a collection of input tags.
369     * 
370     * @param tags the collection of input tag.
371     * @return an exhaustive set of the tags.
372     */
373    public Set<String> getAllTags(Collection<? extends Tag> tags)
374    {
375        Set<String> allTags = new LinkedHashSet<>();
376
377        for (Tag tag : tags)
378        {
379            Map<String, ? extends Tag> childTagsMap = tag.getTags();
380
381            if (childTagsMap != null)
382            {
383                Collection<? extends Tag> childTags = childTagsMap.values();
384                for (Tag child : childTags)
385                {
386                    allTags.add(child.getName());
387                }
388                
389                // recursive add.
390                allTags.addAll(getAllTags(childTags));
391            }
392        }
393        
394        return allTags;
395    }
396
397    /**
398     * Get all the descendant tags belonging to a single tag.
399     * 
400     * @param tag the tag.
401     * @return an exhaustive set of the tags.
402     */
403    protected Set<Tag> getAllTags(Tag tag)
404    {
405        Set<Tag> allTags = new LinkedHashSet<>();
406
407        Map<String, ? extends Tag> childTagsMap = tag.getTags();
408
409        if (childTagsMap != null)
410        {
411            Collection<? extends Tag> childTags = childTagsMap.values();
412            allTags.addAll(childTags);
413            
414            for (Tag child : childTags)
415            {
416                allTags.addAll(getAllTags(child));
417            }
418        }
419
420        return allTags;
421    }
422
423    /**
424     * Get the date range from the calendar type and parameters.
425     * 
426     * @param type the calendar mode: "calendar", "single-day" or "agenda".
427     * @param year the year.
428     * @param month the month.
429     * @param day the day.
430     * @param monthsBefore get x months before.
431     * @param monthsAfter get x months after.
432     * @param rangeType the range type, "month" or "week".
433     * @return the date range.
434     */
435    public DateTimeRange getDateRange(String type, int year, int month, int day, int monthsBefore, int monthsAfter, String rangeType)
436    {
437        DateTimeRange dateRange = null;
438
439        // Single day mode.
440        if ("single-day".equals(type))
441        {
442            LocalDate date = _getDate(year, month, day);
443            ZonedDateTime datetime = date.atStartOfDay(ZoneId.systemDefault());
444            dateRange = new DateTimeRange(datetime, datetime.plusDays(1));
445        }
446        // JS calendar mode.
447        else if ("calendar".equals(type) || "agenda".equals(type))
448        {
449            dateRange = _getDateRange(monthsBefore, monthsAfter);
450        }
451        else if ("period".equals(type))
452        {
453            LocalDate date = _getDate(year, month, day);
454            dateRange = _getDateRange(date, monthsBefore, monthsAfter);
455        }
456
457        if (getLogger().isInfoEnabled() && dateRange != null)
458        {
459            String start = dateRange.fromDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
460            String end = dateRange.untilDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
461            getLogger().info("Getting contents between " + start + " and " + end);
462        }
463
464        return dateRange;
465    }
466    
467    /**
468     * Get a date range from a period (x months before now and x months after).
469     * 
470     * @param monthsBefore the wanted number of months before the current month.
471     * @param monthsAfter the wanted number of months after the current month.
472     * @return the date range.
473     */
474    protected DateTimeRange _getDateRange(int monthsBefore, int monthsAfter)
475    {
476        return _getDateRange(LocalDate.now(), monthsBefore, monthsAfter);
477    }
478
479    /**
480     * Get a date range from a period, around a given date (x months before and
481     * x months after the date).
482     * 
483     * @param localDate the date.
484     * @param monthsBefore the wanted number of months before the current month.
485     * @param monthsAfter the wanted number of months after the current month.
486     * @return the date range.
487     */
488    protected DateTimeRange _getDateRange(LocalDate localDate, int monthsBefore, int monthsAfter)
489    {
490        LocalDate firstDayOfMonth = localDate.withDayOfMonth(1);
491        
492        LocalDate fromDate = firstDayOfMonth.minusMonths(monthsBefore);
493        ZonedDateTime fromDateTime = fromDate.atStartOfDay(ZoneId.systemDefault());
494        
495        LocalDate untilDate = firstDayOfMonth.plusMonths(monthsAfter + 1);
496        ZonedDateTime untilDateTime = untilDate.atStartOfDay(ZoneId.systemDefault());
497        
498        return new DateTimeRange(fromDateTime, untilDateTime);
499    }
500
501    /**
502     * Get a date object from a year-month-day set.
503     * 
504     * @param year the year.
505     * @param month the month (1 to 12).
506     * @param day the day (1 to 31).
507     * @return the date object.
508     */
509    protected LocalDate _getDate(int year, int month, int day)
510    {
511        LocalDate localDate = LocalDate.now();
512        
513        if (year > 0 && month > 0 && day > 0)
514        {
515            localDate.withYear(year);
516            localDate.withMonth(month);
517            localDate.withDayOfMonth(day);
518        }
519
520        return localDate;
521    }
522
523    /**
524     * Get the metadata expression from the calendar type and date range.
525     * 
526     * @param type the calendar type, may be "calendar", "single-day",
527     *            "agenda" or "period" (agenda = calendar = period).
528     * @param dateRange the date range. Only the start date is used for
529     *            "single-day" type.
530     * @return the metadata Expression on contents.
531     */
532    public Expression getExpression(String type, DateTimeRange dateRange)
533    {
534        Expression expression = null;
535
536        if (dateRange != null)
537        {
538            if ("single-day".equals(type))
539            {
540                expression = _getMetadataExpression(dateRange);
541            }
542            else if ("calendar".equals(type) || "agenda".equals(type) || "period".equals(type))
543            {
544                expression = _getMetadataExpression(dateRange);
545            }
546        }
547
548        return expression;
549    }
550
551    /**
552     * Get a metadata expression from a date range.
553     * 
554     * @param dateRange the date range.
555     * @return the date range metadata expression.
556     */
557    protected Expression _getMetadataExpression(DateTimeRange dateRange)
558    {
559        if (dateRange != null)
560        {
561            // start-date < untilDate AND (end-date >= fromDate OR not(end-date) AND start-date >= fromDate)
562            
563            Date fromDate = DateUtils.asDate(dateRange.fromDate());
564            Date untilDate = DateUtils.asDate(dateRange.untilDate());
565            
566            Expression startBeforeExpr = new DateExpression(START_DATE_META, Operator.LT, untilDate);
567            Expression endAfterExpr = new DateExpression(END_DATE_META, Operator.GE, fromDate);
568            Expression startAfterExpr = new DateExpression(START_DATE_META, Operator.GE, fromDate);
569            Expression noEndExpr = new NotExpression(new MetadataExpression(END_DATE_META));
570            
571            return new AndExpression(startBeforeExpr, new OrExpression(endAfterExpr, new AndExpression(noEndExpr, startAfterExpr)));
572        }
573
574        return null;
575    }
576
577    /**
578     * Get a metadata expression from a single date.
579     * 
580     * @param date the date.
581     * @return the date metadata expression.
582     */
583    protected Expression _getMetadataExpression(Date date)
584    {
585        Expression expression = null;
586        
587        if (date != null)
588        {
589            Date nextDay = Date.from(date.toInstant().atZone(ZoneId.systemDefault()).plusDays(1).toInstant());
590            
591            Expression noEndDate = new NotExpression(new MetadataExpression(END_DATE_META));
592            Expression startDate = new DateExpression(START_DATE_META, Operator.GE, date);
593            Expression startDateNextDay = new DateExpression(START_DATE_META, Operator.LT, nextDay);
594            Expression onlyStartDate = new AndExpression(noEndDate, startDate, startDateNextDay);
595            
596            Expression noStartDate = new NotExpression(new MetadataExpression(START_DATE_META));
597            Expression endDate = new DateExpression(END_DATE_META, Operator.GE, date);
598            Expression endDateNextDay = new DateExpression(END_DATE_META, Operator.LT, nextDay);
599            Expression onlyEndDate = new AndExpression(noStartDate, endDate, endDateNextDay);
600            
601            Expression afterStart = new DateExpression(START_DATE_META, Operator.LT, nextDay);
602            Expression beforeEnd = new DateExpression(END_DATE_META, Operator.GE, date);
603            Expression bothDates = new AndExpression(afterStart, beforeEnd);
604            
605            expression = new OrExpression(onlyStartDate, onlyEndDate, bothDates);
606        }
607        
608        return expression;
609    }
610
611    /**
612     * Get last day of the week at a given date.
613     * 
614     * @param cal the date as a Calendar.
615     * @return the last day of the week.
616     */
617    protected int getLastDayOfWeek(Calendar cal)
618    {
619        return Calendar.SUNDAY;
620    }
621    
622    /**
623     * Filter a list of categories depending on the service parameters (categoriesArray) and request tags (requestedCategoriesArray)
624     * @param searchContexts the search contexts
625     * @param requestedCategories list of requested categories (can be "all" to get all the categories from the service, or a list of categories)
626     * @param zoneItem zone Item
627     * @param siteName site name
628     * @return a set of tags
629     */
630    public Set<String> getFilteredCategories(List<Map<String, Object>> searchContexts, String[] requestedCategories, ZoneItem zoneItem, String siteName)
631    {
632     // Select a single tag or "all".
633        List<String> requestedTags = Arrays.asList(requestedCategories);
634        // Get the categories of the tags to match, from the zone item or from
635        // the parameters.
636        Set<Tag> categoriesSet = getTagCategories(zoneItem, searchContexts, siteName);
637        // Get all tags belonging to the categories.
638        Set<String> allCategoriesNames = getAllTags(categoriesSet);
639        // Restrain the list to the provided tags.
640        Set<String> filteredCategory = new HashSet<>();
641        if (requestedTags.contains("all"))
642        {
643            filteredCategory = allCategoriesNames;
644        }
645        else
646        {
647            filteredCategory = requestedTags.stream()
648                    .map(String::toUpperCase)
649                    .filter(allCategoriesNames::contains)
650                    .collect(Collectors.toSet());
651        }
652        
653        return filteredCategory;
654    }
655    
656    /**
657     * Generate the filter
658     * @param dateRange range of dates to search
659     * @param zoneItem used to get the tags
660     * @param view metadataset used
661     * @param type calendar, agenda, period (same thing) or "single-day" to get a single day
662     * @param filteredCategories list of categories filtered by those requested by the user
663     * @param searchContexts the search contexts
664     * @return an EventFilter to match the parameters
665     */
666    public EventsFilter generateEventFilter(DateTimeRange dateRange, ZoneItem zoneItem, String view, String type, Set<String> filteredCategories, List<Map<String, Object>> searchContexts)
667    {
668        String zoneItemId = zoneItem.getId();
669        
670        Set<String> contentTypes = getContentTypes(zoneItem);
671        boolean maskOrphan = getMaskOrphan(zoneItem);
672        AccessLimitation accessLimitation = getAccessLimitation(zoneItem);
673
674        EventsFilter eventsFilter = createEventsFilter(zoneItemId);
675
676        Expression expression = getExpression(type, dateRange); // calendar, agenda, period do the same thing
677        configureFilter(eventsFilter, expression, contentTypes, searchContexts, filteredCategories, view, maskOrphan, accessLimitation);
678        
679        return eventsFilter;
680    }
681    
682    /**
683     * Configure the filter to return the wanted contents.
684     * 
685     * @param eventsFilter the events filter.
686     * @param expression the metadata Expression, can be null.
687     * @param contentTypes a list of content types that will restrict the scope of the request.
688     * @param searchContexts the input searchContext
689     * @param orTags a list of tags among which only one is required for the
690     *            content to match.
691     * @param view the view.
692     * @param maskOrphan true to prevent getting orphan contents.
693     * @param accessLimitation The access limitation policy.
694     */
695    public void configureFilter(EventsFilter eventsFilter, Expression expression, Collection<String> contentTypes, List<Map<String, Object>> searchContexts, Set<String> orTags, String view, boolean maskOrphan, AccessLimitation accessLimitation)
696    {
697        // Set the content types expression, even if null
698        if (eventsFilter.getContentTypes() != null)
699        {
700            eventsFilter.getContentTypes().clear();
701        }
702        
703        for (String contentType : contentTypes)
704        {
705            eventsFilter.addContentType(contentType);
706        }
707
708        // Set the metadata expression, even if null.
709        eventsFilter.setMetadataExpression(expression);
710
711        if (StringUtils.isNotEmpty(view))
712        {
713            eventsFilter.setView(view);
714        }
715
716        eventsFilter.clearSearchContexts();
717        _setSearchContext(eventsFilter, searchContexts, orTags);
718        
719        eventsFilter.setMaskOrphanContents(maskOrphan);
720        
721        eventsFilter.setAccessLimitation(accessLimitation);
722    }
723
724    /**
725     * Set the search contexts in a filter from a service instance attributes.
726     * @param zoneItem the service parameters data holder.
727     * @param inputSearchContexts the input search contexts
728     * @return a list of search context
729     */
730    public List<Map<String, Object>> getSearchContext(ZoneItem zoneItem, List<Map<String, Object>> inputSearchContexts)
731    {
732        List<Map<String, Object>> searchContexts = inputSearchContexts;
733
734        if (searchContexts == null)
735        {
736            // if null, get search contexts from zoneitem
737            searchContexts = new ArrayList<>();
738
739            ModifiableModelAwareRepeater searchRepeater = zoneItem.getServiceParameters().getValue("search");
740            
741            for (ModifiableModelAwareRepeaterEntry repeaterEntry : searchRepeater.getEntries())
742            {
743                Map<String, Object> mapEntry = new HashMap<>();
744                // First check that there is a definition because calendar service doesn't define the tag-categories parameter
745                if (repeaterEntry.hasDefinition("tag-categories"))
746                {
747                    mapEntry.put("tag-categories", repeaterEntry.getValue("tag-categories", false, ArrayUtils.EMPTY_STRING_ARRAY));
748                }
749
750                mapEntry.put("sites", repeaterEntry.getValue("sites", false, ArrayUtils.EMPTY_STRING_ARRAY));
751                mapEntry.put("tags", repeaterEntry.getValue("tags", false, ArrayUtils.EMPTY_STRING_ARRAY));
752                mapEntry.put("search-context", repeaterEntry.getValue("search-context", false, ArrayUtils.EMPTY_STRING_ARRAY));
753                mapEntry.put("context-lang", repeaterEntry.getValue("context-lang", false, StringUtils.EMPTY));
754                mapEntry.put("strict-search-on-tags", repeaterEntry.getValue("strict-search-on-tags", false, false));
755                
756                searchContexts.add(mapEntry);
757            }
758            
759        }
760        return searchContexts;
761    }
762    
763    /**
764     * Get the search parameters from the search values.
765     * @param filter the filter to configure.
766     * @param searchContextValues the input searchContext
767     * @param orTags a list of tags among which only one is required for the
768     *            content to match.
769     */
770    protected void _setSearchContext(EventsFilter filter, List<Map<String, Object>> searchContextValues, Set<String> orTags)
771    {
772        List<Map<String, Object>> nonNullSearchContextValues = Optional.ofNullable(searchContextValues)
773                                                                .orElse(Collections.EMPTY_LIST);
774        
775        for (Map<String, Object> entry : nonNullSearchContextValues)
776        {
777            FilterSearchContext filterContext = filter.addSearchContext();
778
779            Map<String, Object> sitesData = _jsonUtils.convertJsonToMap((String) entry.get("sites"));
780            String searchContext = (String) sitesData.get("context");
781            
782            Map<String, Object> searchContextMap = _jsonUtils.convertJsonToMap((String) entry.get("search-context"));
783            String subSearchContext = (String) searchContextMap.get("context");
784            if (StringUtils.isEmpty(searchContext)
785                || subSearchContext != null && !Context.CURRENT_SITE.name().equals(subSearchContext))
786            {
787                // Delegate to the search-context enumeration context
788                searchContext = subSearchContext;
789            }
790            
791            if ("CHILD_PAGES".equals(searchContext) || "CHILD_PAGES_OF".equals(searchContext))
792            {
793                filterContext.setContext(Context.CHILD_PAGES);
794            }
795            else if ("DIRECT_CHILD_PAGES".equals(searchContext) || "DIRECT_CHILD_PAGES_OF".equals(searchContext))
796            {
797                // Direct child pages are child pages context with depth 1.
798                filterContext.setContext(Context.CHILD_PAGES);
799                filterContext.setDepth(1);
800            }
801            else
802            {
803                // Else, parse the context.
804                filterContext.setContext(Context.valueOf(searchContext));
805            }
806            
807            if (searchContextMap.get("page") != null)
808            {
809                filterContext.setPageId((String) searchContextMap.get("page"));
810            }
811            
812            @SuppressWarnings("unchecked")
813            List<String> sites = Optional.of("sites")
814                                         .map(sitesData::get)
815                                         .map(s -> (List<String>) s)
816                                         .orElse(Collections.EMPTY_LIST);
817            for (String site : sites)
818            {
819                filterContext.addSite(site);
820            }
821
822            String contextLang = (String) entry.get("context-lang");
823            if (StringUtils.isNotEmpty(contextLang))
824            {
825                filterContext.setContextLanguage(ContentFilter.ContextLanguage.valueOf(contextLang));
826            }
827
828            String[] tags = Optional.of("tags")
829                                    .map(entry::get)
830                                    .map(t -> (String[]) t)
831                                    .orElse(new String[0]);
832            for (String tag : tags)
833            {
834                filterContext.addTag(tag);
835            }
836            
837            
838            ((EventFilterSearchContext) filterContext).setOrTags(orTags);
839            
840            if (entry.containsKey("strict-search-on-tags"))
841            {
842                boolean strictSearchOnTags = (boolean) entry.get("strict-search-on-tags");
843                filterContext.setTagsAutoPosting(!strictSearchOnTags);
844            }
845        }
846    }
847    
848    /**
849     * Date range between two dates
850     * @param fromDate the start date
851     * @param untilDate the end date
852     */
853    public record DateTimeRange(ZonedDateTime fromDate, ZonedDateTime untilDate) { /* empty */ }
854}