001/*
002 *  Copyright 2010 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.web.repository.page.actions;
017
018import java.time.LocalDate;
019import java.time.ZoneId;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Optional;
027
028import org.apache.avalon.framework.parameters.ParameterException;
029import org.apache.avalon.framework.parameters.Parameters;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.cocoon.acting.ServiceableAction;
033import org.apache.cocoon.environment.ObjectModelHelper;
034import org.apache.cocoon.environment.Redirector;
035import org.apache.cocoon.environment.Request;
036import org.apache.cocoon.environment.SourceResolver;
037import org.apache.commons.lang3.ArrayUtils;
038import org.apache.commons.lang3.StringUtils;
039
040import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
041import org.ametys.cms.filter.ContentFilter;
042import org.ametys.cms.filter.ContentFilterExtensionPoint;
043import org.ametys.cms.repository.Content;
044import org.ametys.cms.tag.TagProviderExtensionPoint;
045import org.ametys.core.util.JSONUtils;
046import org.ametys.plugins.repository.AmetysObjectResolver;
047import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
048import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeater;
049import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeaterEntry;
050import org.ametys.plugins.repository.query.expression.AndExpression;
051import org.ametys.plugins.repository.query.expression.BooleanExpression;
052import org.ametys.plugins.repository.query.expression.DateExpression;
053import org.ametys.plugins.repository.query.expression.DoubleExpression;
054import org.ametys.plugins.repository.query.expression.Expression;
055import org.ametys.plugins.repository.query.expression.Expression.Operator;
056import org.ametys.plugins.repository.query.expression.ExpressionContext;
057import org.ametys.plugins.repository.query.expression.MetadataExpression;
058import org.ametys.plugins.repository.query.expression.NotExpression;
059import org.ametys.plugins.repository.query.expression.StringExpression;
060import org.ametys.plugins.repository.query.expression.StringWildcardExpression;
061import org.ametys.runtime.i18n.I18nizableText;
062import org.ametys.runtime.model.ElementDefinition;
063import org.ametys.web.WebConstants;
064import org.ametys.web.filter.DefaultWebContentFilter;
065import org.ametys.web.filter.WebContentFilter;
066import org.ametys.web.filter.WebContentFilter.AccessLimitation;
067import org.ametys.web.filter.WebContentFilter.Context;
068import org.ametys.web.filter.WebContentFilter.FilterSearchContext;
069import org.ametys.web.repository.page.Page;
070import org.ametys.web.repository.page.ZoneItem;
071import org.ametys.web.repository.site.SiteManager;
072import org.ametys.web.service.Service;
073import org.ametys.web.service.ServiceExtensionPoint;
074
075/**
076 * This action creates a filter from the sitemap parameters or request parameter 'filterId' and set the filter in request attributes.
077 *
078 */
079@SuppressWarnings("deprecation")
080public class SetFilterInRequestAttributesAction extends ServiceableAction
081{
082    private static final String __FILTERS_EQUAL = "ft-eq";
083    private static final String __FILTERS_DIFFERENT = "ft-neq";
084    private static final String __FILTERS_STARTSWITH = "ft-sw";
085    private static final String __FILTERS_ENDSWITH = "ft-ew";
086    private static final String __FILTERS_CONTAINS = "ft-co";
087    private static final String __FILTERS_NOTCONTAINS = "ft-nco";
088    private static final String __FILTERS_EMPTY = "ft-em";
089    private static final String __FILTERS_NONEMPTY = "ft-nem";
090    private static final String __FILTERS_LESSTHAN = "ft-lt";
091    private static final String __FILTERS_GREATERTHAN = "ft-gt";
092    private static final String __FILTERS_PAST = "ft-past";
093    private static final String __FILTERS_FUTURE = "ft-future";
094    private static final String __FILTERS_TRUE = "ft-true";
095    private static final String __FILTERS_FALSE = "ft-false";
096
097    /** The Ametys resolver */
098    protected AmetysObjectResolver _resolver;
099    /** The extension point for content types */
100    protected ContentTypeExtensionPoint _contentTypeEP;
101    /** The content filter EP */
102    protected ContentFilterExtensionPoint _filterExtPt;
103    /** The service EP */
104    protected ServiceExtensionPoint _serviceEP;
105    /** The site manager */
106    protected SiteManager _siteManager;
107    /** The tag provider EP */
108    protected TagProviderExtensionPoint _tagProviderEP;
109    /** The JSON utils */
110    protected JSONUtils _jsonUtils;
111
112    @Override
113    public void service(ServiceManager smanager) throws ServiceException
114    {
115        super.service(smanager);
116        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
117        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
118        _filterExtPt = (ContentFilterExtensionPoint) smanager.lookup(ContentFilterExtensionPoint.ROLE);
119        _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE);
120        _serviceEP = (ServiceExtensionPoint) smanager.lookup(ServiceExtensionPoint.ROLE);
121        _tagProviderEP = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE);
122        _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE);
123    }
124
125    @Override
126    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
127    {
128        Map<String, String> result = new HashMap<>();
129
130        Request request = ObjectModelHelper.getRequest(objectModel);
131        String filterId = parameters.getParameter("filterId", null);
132
133        ContentFilter filter = null;
134        if (filterId != null)
135        {
136            // Get the static filter from its id
137            filter = _getStaticFilterById(filterId, parameters.getParameter("siteName", ""));
138        }
139        else if (parameters.getParameter("zoneItemId", null) != null)
140        {
141            filter = _getFilterFromZoneItem(parameters, result);
142        }
143        else
144        {
145            @SuppressWarnings("unchecked")
146            Map<String, Object> parentContextAttributes = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
147            ZoneItem zi = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
148            filter = _getFilterFromParams(parameters, zi, parentContextAttributes);
149        }
150
151        // Set the filter in request attributes
152        if (filter != null)
153        {
154            request.setAttribute("filter", filter);
155        }
156
157        return result;
158    }
159
160    /**
161     * Get the filter from the action parameters.
162     * @param parameters The Action parameters.
163     * @param zoneItem the zone item
164     * @param parentContextAttributes The parent context attributes.
165     * @return the content filter.
166     */
167    protected WebContentFilter _getFilterFromParams(Parameters parameters, ZoneItem zoneItem, Map<String, Object> parentContextAttributes)
168    {
169        // Creates filter from sitemap parameters
170        WebContentFilter filter = _createFilter(null, _resolver, _contentTypeEP, _siteManager, _tagProviderEP);
171
172        try
173        {
174            // The filter inputs
175            String[] cTypes;
176            String cTypesStr = parameters.getParameter("contentTypes", null);
177            if (cTypesStr == null)
178            {
179                cTypes = (String[]) parentContextAttributes.get("content-types");
180            }
181            else
182            {
183                cTypes = StringUtils.split(cTypesStr, ',');
184            }
185            
186            if (cTypes.length > 0 && cTypes[0].length() > 0)
187            {
188                for (String cType : cTypes)
189                {
190                    if (StringUtils.isNotEmpty(cType))
191                    {
192                        filter.addContentType(cType);
193                    }
194                }
195            }
196            else if (zoneItem != null)
197            {
198                List<String> allCTypes = _getContentTypes (zoneItem);
199                for (String cType : allCTypes)
200                {
201                    filter.addContentType(cType);
202                }
203            }
204
205            String searchContext = parameters.getParameter("searchContext", "");
206            if (StringUtils.isNotEmpty(searchContext))
207            {
208                _setSearchContext(filter, parameters);
209            }
210            else
211            {
212                @SuppressWarnings("unchecked")
213                List<Map<String, Object>> searchValues = (List<Map<String, Object>>) parentContextAttributes.get("search");
214                _setSearchContext(filter, searchValues);
215            }
216
217            filter.setTitle(new I18nizableText(parameters.getParameter("service-title")));
218            filter.setView(parameters.getParameter("viewName"));
219            filter.setLength(parameters.getParameterAsInteger("length", Integer.MAX_VALUE));
220
221            // Filter criteria
222            Map<String, Object> filterBy = _getFilterMap(parameters, parentContextAttributes);
223            _setFilterCriteria(filter, filterBy);
224
225            // Sort criteria
226            List<Map<String, String>> sortBy = _getSortList(parameters, parentContextAttributes);
227            _setSortCriteria(filter, cTypes, sortBy);
228
229            // Mask orphan
230            boolean mask = parameters.getParameterAsBoolean("mask-orphan", false);
231            filter.setMaskOrphanContents(mask);
232            
233            // Handle user access.
234            boolean handleUserAccess = parameters.getParameterAsBoolean("handle-user-access", false);
235            filter.setAccessLimitation(handleUserAccess ? AccessLimitation.USER_ACCESS : AccessLimitation.PAGE_ACCESS);
236        }
237        catch (ParameterException e)
238        {
239            getLogger().error("Missing at least one parameter", e);
240        }
241
242        return filter;
243    }
244    
245    /**
246     * Retrieves the filter map in the parameters or parent context attribute
247     * @param parameters The parameters
248     * @param parentContextAttributes The parent context attributes
249     * @return The filter map
250     */
251    protected Map<String, Object> _getFilterMap(Parameters parameters, Map<String, Object> parentContextAttributes)
252    {
253        String filterByAsString = parameters.getParameter("filterBy", "");
254        if (StringUtils.isEmpty(filterByAsString))
255        {
256            filterByAsString = (String) parentContextAttributes.get("filterBy");
257        }
258        
259        if (StringUtils.isNotEmpty(filterByAsString))
260        {
261            return _jsonUtils.convertJsonToMap(filterByAsString);
262        }
263        else
264        {
265            return null;
266        }
267    }
268    
269    /**
270     * Retrieves the filter map from a data holder (service parameters)
271     * @param dataHolder The data holder
272     * @return The filter map
273     */
274    protected Map<String, Object> _getFilterMap(ModelAwareDataHolder dataHolder)
275    {
276        Map<String, Object> filterBy = null;
277        
278        String value = dataHolder.getValue("filterBy", false, "");
279        if (StringUtils.isNotBlank(value))
280        {
281            filterBy = _jsonUtils.convertJsonToMap(value);
282        }
283        
284        return filterBy;
285    }
286    
287    /**
288     * Retrieves the sort list in the parameters or parent context attribute
289     * @param parameters The parameters
290     * @param parentContextAttributes The parent context attributes
291     * @return The sort list
292     */
293    @SuppressWarnings("unchecked")
294    protected List<Map<String, String>> _getSortList(Parameters parameters, Map<String, Object> parentContextAttributes)
295    {
296        String sortByAsString = parameters.getParameter("sortBy", "");
297        if (StringUtils.isEmpty(sortByAsString))
298        {
299            sortByAsString = (String) parentContextAttributes.get("sortBy");
300        }
301        
302        if (StringUtils.isNotEmpty(sortByAsString))
303        {
304            List<Map<String, String>> sortBy = new ArrayList<>();
305            for (Object entry : _jsonUtils.convertJsonToList(sortByAsString))
306            {
307                sortBy.add((Map<String, String>) entry);
308            }
309            return sortBy;
310        }
311        else
312        {
313            return null;
314        }
315    }
316    
317    /**
318     * Retrieves the sort list map from a data holder (service parameters)
319     * @param metadata The data holder
320     * @return The filter map
321     */
322    @SuppressWarnings("unchecked")
323    protected List<Map<String, String>> _getSortList(ModelAwareDataHolder metadata)
324    {
325        List<Map<String, String>> sortBy = null;
326        
327        String value = metadata.getValue("sortBy", false, "");
328        if (StringUtils.isNotBlank(value))
329        {
330            sortBy = new ArrayList<>();
331            for (Object entry : _jsonUtils.convertJsonToList(value))
332            {
333                sortBy.add((Map<String, String>) entry);
334            }
335        }
336        
337        return sortBy;
338    }
339    
340    private List<String> _getContentTypes (ZoneItem zoneItem) throws ParameterException
341    {
342        List<String> contentTypes = new ArrayList<>();
343
344        String serviceId = zoneItem.getServiceId();
345        Service service = _serviceEP.getExtension(serviceId);
346
347        if (service.hasModelItem("content-types"))
348        {
349            @SuppressWarnings("unchecked")
350            ElementDefinition<String> contentTypesParam = (ElementDefinition<String>) service.getModelItem("content-types");
351
352            try
353            {
354                Map<String, I18nizableText> entries = contentTypesParam.getEnumerator().getTypedEntries();
355                for (String contentType : entries.keySet())
356                {
357                    contentTypes.add(contentType);
358                }
359
360                return contentTypes;
361            }
362            catch (Exception e)
363            {
364                throw new ParameterException("Unable to get the list of available content types from service parameters", e);
365            }
366        }
367
368        return contentTypes;
369
370    }
371
372    /**
373     * Get the content filter from a ZoneItem.
374     * @param parameters the action parameters, including the ZoneItem id.
375     * @param result the action result.
376     * @return the content filter.
377     * @throws ParameterException if parameters are invalid
378     */
379    protected WebContentFilter _getFilterFromZoneItem(Parameters parameters, Map<String, String> result) throws ParameterException
380    {
381        WebContentFilter filter = null;
382
383        String zoneItemId = parameters.getParameter("zoneItemId");
384        String protocol = parameters.getParameter("protocol");
385
386        ZoneItem zoneItem = _resolver.resolveById(protocol + "://" + zoneItemId);
387        if (_isValid(zoneItem))
388        {
389            // Creates filter from services parameters
390            filter = _getFilterFromZoneItem(zoneItem);
391        }
392
393        Page page = zoneItem.getZone().getPage();
394        result.put("siteName", page.getSiteName());
395        result.put("lang", page.getSitemapName());
396        result.put("path", page.getPathInSitemap());
397
398        return filter;
399    }
400
401    /**
402     * Get the content filter from a ZoneItem.
403     * @param zoneItem the ZoneItem.
404     * @return the content filter.
405     * @throws ParameterException if parameters are invalid
406     */
407    protected WebContentFilter _getFilterFromZoneItem(ZoneItem zoneItem) throws ParameterException
408    {
409        WebContentFilter filter = _createFilter(null, _resolver, _contentTypeEP, _siteManager, _tagProviderEP);
410
411        ModelAwareDataHolder dataHolder = zoneItem.getServiceParameters();
412
413        String[] contentTypes =  dataHolder.getValue("content-types");
414        if (contentTypes.length > 0 && contentTypes[0].length() > 0)
415        {
416            for (String contentType : contentTypes)
417            {
418                if (!StringUtils.isEmpty(contentType))
419                {
420                    filter.addContentType(contentType);
421                }
422            }
423        }
424        else
425        {
426            List<String> allContentTypes = _getContentTypes(zoneItem);
427            for (String contentType : allContentTypes)
428            {
429                filter.addContentType(contentType);
430            }
431        }
432
433        filter.setView(dataHolder.getValue("metadataSetName"));
434        filter.setLength(Math.toIntExact(dataHolder.getValue("nb-max", true, Integer.valueOf(Integer.MAX_VALUE).longValue())));
435        filter.setTitle(new I18nizableText(dataHolder.getValue("header")));
436
437        // Contexts
438        _setSearchContext(filter, dataHolder);
439
440        Map<String, Object> filterBy = _getFilterMap(dataHolder);
441        _setFilterCriteria(filter, filterBy);
442
443        // Sort criteria
444        List<Map<String, String>> sortBy = _getSortList(dataHolder);
445        _setSortCriteria(filter, contentTypes, sortBy);
446
447        // Mask orphan
448        boolean maskOrphan = dataHolder.getValue("mask-orphan", false, false);
449        filter.setMaskOrphanContents(maskOrphan);
450
451        // Handle user access
452        boolean handleUserAccess = dataHolder.getValue("handle-user-access", false, false);
453        filter.setAccessLimitation(handleUserAccess ? AccessLimitation.USER_ACCESS : AccessLimitation.PAGE_ACCESS);
454        
455        return filter;
456    }
457
458    /**
459     * Create a WebContentFilter.
460     * @param id the filter ID.
461     * @param resolver the AmetysObjectResolver.
462     * @param contentTypeExtensionPoint The extension point for content types
463     * @param siteManager The site manager
464     * @param tagProviderEP The tag provider extension point
465     * @return the WebContentFilter.
466     */
467    protected WebContentFilter _createFilter(String id, AmetysObjectResolver resolver, ContentTypeExtensionPoint contentTypeExtensionPoint, SiteManager siteManager, TagProviderExtensionPoint tagProviderEP)
468    {
469        return new DefaultWebContentFilter(id, resolver, contentTypeExtensionPoint, siteManager, tagProviderEP);
470    }
471
472    /**
473     * Test if the ZoneItem is valid.
474     * @param zoneItem the ZoneItem.
475     * @return true if the ZoneItem is valid, false otherwise.
476     */
477    protected boolean _isValid(ZoneItem zoneItem)
478    {
479        return zoneItem.getType().equals(ZoneItem.ZoneType.SERVICE)
480                && "org.ametys.web.service.FilteredContentsService".equals(zoneItem.getServiceId());
481    }
482
483    /**
484     * Get the search parameters from the search values as parameters.
485     * @param filter the filter to configure.
486     * @param parameters the parameters.
487     * @throws ParameterException if an error occurs.
488     */
489    protected void _setSearchContext(WebContentFilter filter, Parameters parameters) throws ParameterException
490    {
491        FilterSearchContext filterContext = filter.addSearchContext();
492
493        String searchContext = parameters.getParameter("searchContext");
494        if ("DIRECT_CHILD_PAGES".equals(searchContext))
495        {
496            // Direct child pages are child pages context with depth 1.
497            filterContext.setContext(Context.CHILD_PAGES);
498            filterContext.setDepth(1);
499        }
500        else
501        {
502            // Else, parse the context.
503            filterContext.setContext(Context.valueOf(searchContext));
504        }
505
506        String sitesAsStr = parameters.getParameter("sites", "");
507        if (sitesAsStr.length() > 0)
508        {
509            String[] sites = sitesAsStr.split(",");
510            for (String site : sites)
511            {
512                filterContext.addSite(site);
513            }
514        }
515        
516        if (!"".equals(parameters.getParameter("contextLang")))
517        {
518            filterContext.setContextLanguage(ContentFilter.ContextLanguage.valueOf(parameters.getParameter("contextLang")));
519        }
520
521        String tagAsStr = parameters.getParameter("tags", "");
522        if (tagAsStr.length() > 0)
523        {
524            String[] tags = tagAsStr.split(",");
525            for (String tag : tags)
526            {
527                filterContext.addTag(tag);
528            }
529        }
530        
531        if (parameters.isParameter("strict-search-on-tags"))
532        {
533            boolean strictSearchOnTags = parameters.getParameterAsBoolean("strict-search-on-tags");
534            filterContext.setTagsAutoPosting(!strictSearchOnTags);
535        }
536    }
537
538    /**
539     * Get the search parameters from the search values.
540     * @param filter the filter to configure.
541     * @param searchValues the search values.
542     */
543    protected void _setSearchContext(WebContentFilter filter, List<Map<String, Object>> searchValues)
544    {
545        List<Map<String, Object>> nonNullSearchValues = Optional.ofNullable(searchValues)
546                                                                .orElse(Collections.EMPTY_LIST);
547        
548        for (Map<String, Object> entry : nonNullSearchValues)
549        {
550            FilterSearchContext filterContext = filter.addSearchContext();
551
552            Map<String, Object> sitesData = _jsonUtils.convertJsonToMap((String) entry.get("sites"));
553            String searchContext = (String) sitesData.get("context");
554            
555            Map<String, Object> searchContextMap = _jsonUtils.convertJsonToMap((String) entry.get("search-context"));
556            String subSearchContext = (String) searchContextMap.get("context");
557            if (StringUtils.isEmpty(searchContext)
558                || subSearchContext != null && !Context.CURRENT_SITE.name().equals(subSearchContext))
559            {
560                // Delegate to the search-context enumeration context
561                searchContext = subSearchContext;
562            }
563            
564            if ("CHILD_PAGES".equals(searchContext) || "CHILD_PAGES_OF".equals(searchContext))
565            {
566                filterContext.setContext(Context.CHILD_PAGES);
567            }
568            else if ("DIRECT_CHILD_PAGES".equals(searchContext) || "DIRECT_CHILD_PAGES_OF".equals(searchContext))
569            {
570                // Direct child pages are child pages context with depth 1.
571                filterContext.setContext(Context.CHILD_PAGES);
572                filterContext.setDepth(1);
573            }
574            else
575            {
576                // Else, parse the context.
577                filterContext.setContext(Context.valueOf(searchContext));
578            }
579            
580            if (searchContextMap.get("page") != null)
581            {
582                filterContext.setPageId((String) searchContextMap.get("page"));
583            }
584            
585            @SuppressWarnings("unchecked")
586            List<String> sites = Optional.of("sites")
587                                         .map(sitesData::get)
588                                         .map(s -> (List<String>) s)
589                                         .orElse(Collections.EMPTY_LIST);
590            for (String site : sites)
591            {
592                filterContext.addSite(site);
593            }
594
595            String contextLang = (String) entry.get("context-lang");
596            if (StringUtils.isNotEmpty(contextLang))
597            {
598                filterContext.setContextLanguage(ContentFilter.ContextLanguage.valueOf(contextLang));
599            }
600
601            String[] tags = Optional.of("tags")
602                                    .map(entry::get)
603                                    .map(t -> (String[]) t)
604                                    .orElse(new String[0]);
605            for (String tag : tags)
606            {
607                filterContext.addTag(tag);
608            }
609            
610            if (entry.containsKey("strict-search-on-tags"))
611            {
612                boolean strictSearchOnTags = (boolean) entry.get("strict-search-on-tags");
613                filterContext.setTagsAutoPosting(!strictSearchOnTags);
614            }
615        }
616    }
617
618    /**
619     * Set the search contexts in a filter from a service instance attributes.
620     * @param filter the filter.
621     * @param dataHolder the service parameters data holder.
622     */
623    protected void _setSearchContext(WebContentFilter filter, ModelAwareDataHolder dataHolder)
624    {
625        ModelAwareRepeater searchRepeater = dataHolder.getRepeater("search");
626
627        for (ModelAwareRepeaterEntry searchRepeaterEntry : searchRepeater.getEntries())
628        {
629            FilterSearchContext filterContext = filter.addSearchContext();
630            
631            Map<String, Object> sitesData = _jsonUtils.convertJsonToMap(searchRepeaterEntry.getValue("sites", false, ""));
632            String searchContext = (String) sitesData.get("context");
633
634            Map<String, Object> searchContextMap = _jsonUtils.convertJsonToMap(searchRepeaterEntry.getValue("search-context", false, ""));
635            String subSearchContext = (String) searchContextMap.get("context");
636            if (StringUtils.isEmpty(searchContext)
637                || subSearchContext != null && !Context.CURRENT_SITE.name().equals(subSearchContext))
638            {
639                // Delegate to the search-context enumeration context
640                searchContext = subSearchContext;
641            }
642
643            if ("CHILD_PAGES".equals(searchContext) || "CHILD_PAGES_OF".equals(searchContext))
644            {
645                filterContext.setContext(Context.CHILD_PAGES);
646            }
647            else if ("DIRECT_CHILD_PAGES".equals(searchContext) || "DIRECT_CHILD_PAGES_OF".equals(searchContext))
648            {
649                // Direct child pages are child pages context with depth 1.
650                filterContext.setContext(Context.CHILD_PAGES);
651                filterContext.setDepth(1);
652            }
653            else
654            {
655                // Else, parse the context.
656                filterContext.setContext(Context.valueOf(searchContext));
657            }
658
659            if (searchContextMap.get("page") != null)
660            {
661                filterContext.setPageId((String) searchContextMap.get("page"));
662            }
663
664            @SuppressWarnings("unchecked")
665            List<String> sites = Optional.of("sites")
666                                         .map(sitesData::get)
667                                         .map(s -> (List<String>) s)
668                                         .orElse(Collections.EMPTY_LIST);
669            for (String site : sites)
670            {
671                filterContext.addSite(site);
672            }
673
674            String contextLang = searchRepeaterEntry.getValue("context-lang", false, "");
675            if (StringUtils.isNotEmpty(contextLang))
676            {
677                filterContext.setContextLanguage(ContentFilter.ContextLanguage.valueOf(contextLang));
678            }
679
680            String[] tags = searchRepeaterEntry.getValue("tags", false, ArrayUtils.EMPTY_STRING_ARRAY);
681            for (String tag : tags)
682            {
683                filterContext.addTag(tag);
684            }
685            
686            if (searchRepeaterEntry.hasValue("strict-search-on-tags"))
687            {
688                boolean strictSearchOnTags = searchRepeaterEntry.getValue("strict-search-on-tags");
689                filterContext.setTagsAutoPosting(!strictSearchOnTags);
690            }
691        }
692    }
693
694    /**
695     * Set the filters's filter expression
696     * @param contentFilter The filter to edit
697     * @param filterBy The map of filters. Key are the medataname. Value represents the filter.
698     */
699    protected void _setFilterCriteria(ContentFilter contentFilter, Map<String, Object> filterBy)
700    {
701        if (filterBy != null && !filterBy.isEmpty())
702        {
703            List<Expression> exprs = new ArrayList<>();
704
705            for (String metadataName : filterBy.keySet())
706            {
707                String[] filter = StringUtils.split((String) filterBy.get(metadataName), '_');
708                
709                String filterName = filter[0];
710                boolean lowerCase = filter.length >= 2 && "LC".equals(filter[1]);
711                String filterValue = filter.length >= 3 ? filter[2] : null;
712                
713                Expression expr = _convertToExpression(metadataName, lowerCase, filterName, filterValue);
714                exprs.add(expr);
715            }
716
717            Expression[] exprsAsArray = new Expression[exprs.size()];
718            exprs.toArray(exprsAsArray);
719            Expression expr = new AndExpression(exprsAsArray);
720            contentFilter.setAdditionalFilterExpression(expr);
721        }
722    }
723
724    private Expression _convertToExpression(String metadataName, boolean lowerCase, String metadataCondition, String metadataConditionValue)
725    {
726        ExpressionContext exprContext = ExpressionContext.newInstance().withCaseInsensitive(lowerCase);
727        if (__FILTERS_EQUAL.equals(metadataCondition))
728        {
729            return new StringExpression(metadataName, Operator.EQ, metadataConditionValue, exprContext);
730        }
731        else if (__FILTERS_DIFFERENT.equals(metadataCondition))
732        {
733            return new StringExpression(metadataName, Operator.NE, metadataName, exprContext);
734        }
735        else if (__FILTERS_STARTSWITH.equals(metadataCondition))
736        {
737            // TODO CMS-3881
738            return new StringWildcardExpression(metadataName, metadataConditionValue, exprContext);
739        }
740        else if (__FILTERS_ENDSWITH.equals(metadataCondition))
741        {
742            // TODO CMS-3881
743            return new StringWildcardExpression(metadataName, metadataConditionValue, exprContext);
744        }
745        else if (__FILTERS_CONTAINS.equals(metadataCondition))
746        {
747            // TODO CMS-3881
748            return new StringWildcardExpression(metadataName, metadataConditionValue, exprContext);
749        }
750        else if (__FILTERS_NOTCONTAINS.equals(metadataCondition))
751        {
752            // TODO CMS-3881
753            return new NotExpression(new StringWildcardExpression(metadataName, metadataConditionValue, exprContext));
754        }
755        else if (__FILTERS_EMPTY.equals(metadataCondition))
756        {
757            return new NotExpression(new MetadataExpression(metadataName));
758        }
759        else if (__FILTERS_NONEMPTY.equals(metadataCondition))
760        {
761            return new MetadataExpression(metadataName);
762        }
763        else if (__FILTERS_LESSTHAN.equals(metadataCondition))
764        {
765            return new DoubleExpression(metadataName, Operator.LE, Double.parseDouble(metadataConditionValue));
766        }
767        else if (__FILTERS_GREATERTHAN.equals(metadataCondition))
768        {
769            return new DoubleExpression(metadataName, Operator.GE, Double.parseDouble(metadataConditionValue));
770        }
771        else if (__FILTERS_PAST.equals(metadataCondition))
772        {
773            
774            return new DateExpression(metadataName, Operator.LE, Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()));
775        }
776        else if (__FILTERS_FUTURE.equals(metadataCondition))
777        {
778            return new DateExpression(metadataName, Operator.GE, Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant()));
779        }
780        else if (__FILTERS_TRUE.equals(metadataCondition))
781        {
782            return new BooleanExpression(metadataName, true);
783        }
784        else if (__FILTERS_FALSE.equals(metadataCondition))
785        {
786            return new BooleanExpression(metadataName, false);
787        }
788        else
789        {
790            getLogger().error("Unknown condition '" + metadataCondition + "'");
791            return null;
792        }
793    }
794
795    /**
796     * Set the filter's sort criteria.
797     * @param filter The filter to edit
798     * @param cTypes The content types
799     * @param sortBy The sort list. Each entry in the list is a map representing a sort. Map have two keys: name, the name of the metadata, and sort, the name of the sort
800     */
801    protected void _setSortCriteria(ContentFilter filter, String[] cTypes, List<Map<String, String>> sortBy)
802    {
803        if (sortBy != null && !sortBy.isEmpty())
804        {
805            for (Map<String, String> sortEntry : sortBy)
806            {
807                String[] sort = StringUtils.split(sortEntry.get("sort"), '_');
808                
809                boolean ascending = sort == null || "ASC".equals(sort[0]);
810                boolean useLowerCase = sort != null && sort.length >= 2 && "LC".equals(sort[1]);
811                
812                filter.addSortCriteria(sortEntry.get("name"), ascending, useLowerCase);
813            }
814        }
815        else
816        {
817            filter.addSortCriteria(Content.ATTRIBUTE_TITLE, true, true);
818        }
819    }
820    
821    /**
822     * Retrieve the content filter with its id 
823     * @param filterId the id of the filter to retrieve
824     * @param siteName The site name
825     * @return the content filter associated with this id
826     */
827    protected ContentFilter _getStaticFilterById(String filterId, String siteName)
828    {
829        return _filterExtPt.getExtension(filterId);
830    }
831}