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