001/*
002 *  Copyright 2017 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 */
016
017package org.ametys.web.frontoffice;
018
019import java.io.IOException;
020import java.time.LocalDate;
021import java.time.format.DateTimeFormatter;
022import java.time.format.DateTimeParseException;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.Enumeration;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.List;
031import java.util.Locale;
032import java.util.Map;
033import java.util.Map.Entry;
034import java.util.Optional;
035import java.util.Set;
036import java.util.stream.Collectors;
037import java.util.stream.Stream;
038
039import org.apache.avalon.framework.service.ServiceException;
040import org.apache.avalon.framework.service.ServiceManager;
041import org.apache.cocoon.environment.ObjectModelHelper;
042import org.apache.cocoon.environment.Request;
043import org.apache.cocoon.xml.AttributesImpl;
044import org.apache.cocoon.xml.XMLUtils;
045import org.apache.commons.collections.MapUtils;
046import org.apache.commons.lang.StringEscapeUtils;
047import org.apache.commons.lang3.ArrayUtils;
048import org.apache.commons.lang3.StringUtils;
049import org.apache.solr.client.solrj.util.ClientUtils;
050import org.xml.sax.SAXException;
051
052import org.ametys.cms.content.indexing.solr.SolrFieldNames;
053import org.ametys.cms.contenttype.ContentAttributeDefinition;
054import org.ametys.cms.contenttype.ContentType;
055import org.ametys.cms.data.type.ModelItemTypeConstants;
056import org.ametys.cms.repository.Content;
057import org.ametys.cms.repository.ContentTypeExpression;
058import org.ametys.cms.repository.LanguageExpression;
059import org.ametys.cms.search.SearchField;
060import org.ametys.cms.search.SearchResult;
061import org.ametys.cms.search.SearchResults;
062import org.ametys.cms.search.SearchResultsIterable;
063import org.ametys.cms.search.SearchResultsIterator;
064import org.ametys.cms.search.Sort;
065import org.ametys.cms.search.Sort.Order;
066import org.ametys.cms.search.query.AndQuery;
067import org.ametys.cms.search.query.ContentTypeQuery;
068import org.ametys.cms.search.query.DateQuery;
069import org.ametys.cms.search.query.DocumentTypeQuery;
070import org.ametys.cms.search.query.FullTextQuery;
071import org.ametys.cms.search.query.JoinQuery;
072import org.ametys.cms.search.query.MatchAllQuery;
073import org.ametys.cms.search.query.NotQuery;
074import org.ametys.cms.search.query.OrQuery;
075import org.ametys.cms.search.query.Query;
076import org.ametys.cms.search.query.Query.Operator;
077import org.ametys.cms.search.query.QuerySyntaxException;
078import org.ametys.cms.search.query.StringQuery;
079import org.ametys.cms.search.query.TagQuery;
080import org.ametys.cms.search.solr.SearcherFactory.Searcher;
081import org.ametys.cms.search.solr.field.LastValidationSearchField;
082import org.ametys.cms.search.solr.field.StringSearchField;
083import org.ametys.cms.tag.CMSTag;
084import org.ametys.cms.tag.Tag;
085import org.ametys.cms.tag.TagProvider;
086import org.ametys.core.util.AvalonLoggerAdapter;
087import org.ametys.core.util.LambdaUtils;
088import org.ametys.plugins.explorer.resources.Resource;
089import org.ametys.plugins.repository.AmetysObject;
090import org.ametys.plugins.repository.AmetysObjectIterable;
091import org.ametys.plugins.repository.RepositoryConstants;
092import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
093import org.ametys.plugins.repository.query.QueryHelper;
094import org.ametys.plugins.repository.query.expression.AndExpression;
095import org.ametys.plugins.repository.query.expression.Expression;
096import org.ametys.plugins.repository.query.expression.OrExpression;
097import org.ametys.runtime.i18n.I18nizableText;
098import org.ametys.runtime.model.ElementDefinition;
099import org.ametys.runtime.model.ModelItem;
100import org.ametys.runtime.model.exception.UndefinedItemPathException;
101import org.ametys.web.frontoffice.FrontOfficeSearcherFactory.FrontOfficeSearcher;
102import org.ametys.web.frontoffice.FrontOfficeSearcherFactory.FrontOfficeSolrSearchResults;
103import org.ametys.web.frontoffice.FrontOfficeSearcherFactory.QueryFacet;
104import org.ametys.web.indexing.solr.SolrWebFieldNames;
105import org.ametys.web.repository.page.Page;
106import org.ametys.web.repository.page.ZoneItem;
107import org.ametys.web.repository.site.Site;
108import org.ametys.web.repository.site.SiteTypesExtensionPoint;
109import org.ametys.web.search.query.PageContentQuery;
110import org.ametys.web.search.query.PageQuery;
111import org.ametys.web.search.query.SiteQuery;
112import org.ametys.web.search.query.SitemapQuery;
113
114/**
115 * Generates the results of a search performed on front office
116 */
117public class SearchGenerator extends AbstractSearchGenerator
118{
119    private static final String __FACETS_CACHE = SearchGenerator.class.getName() + "$Cache-Facets";
120    
121    /** The query adapter extension point */
122    protected QueryAdapterFOSearchExtensionPoint _queryAdapterFOSearchEP;
123    /** The site type manager */
124    protected SiteTypesExtensionPoint _siteTypeEP;
125    
126    /**
127     * Enumeration for content type search
128     */
129    protected enum ContentTypeSearch
130    {
131        /** To search by content type with filter (facets)*/
132        FILTER("filter"),
133        /** To search by content type with combo box selection */
134        LIST("list"),
135        /** To search by content type with checkbox */
136        CHECKBOX("checkbox"),
137        /** To search by content type with checkbox and filter (facets)*/
138        CHECKBOX_FILTER("checkbox-filter"),
139        /** To not allow to search by content type */
140        NONE("none");
141        
142        private String _name;
143        
144        private ContentTypeSearch(String name) 
145        {
146            this._name = name;
147        }
148        
149        @Override
150        public String toString() 
151        {
152            return this._name;
153        }
154        
155        /**
156         * Get enumeration value
157         * @param value The value
158         * @return The enum
159         */
160        public static ContentTypeSearch getEnum(String value)
161        {
162            for (ContentTypeSearch ct : ContentTypeSearch.values())
163            {
164                if (ct.toString().equals(value))
165                {
166                    return ct;
167                }
168            }
169
170            throw new IllegalArgumentException("Invalid ContentTypeChoice value: " + value);
171        }
172    }
173
174    @Override
175    public void service(ServiceManager smanager) throws ServiceException
176    {
177        super.service(smanager);
178        _queryAdapterFOSearchEP = (QueryAdapterFOSearchExtensionPoint) smanager.lookup(QueryAdapterFOSearchExtensionPoint.ROLE);
179        _siteTypeEP = (SiteTypesExtensionPoint) smanager.lookup(SiteTypesExtensionPoint.ROLE);
180    }
181    
182    /**
183     * Return the type of content type's search
184     * @param request The request
185     * @return The type of search
186     */
187    protected ContentTypeSearch getContentTypeSearch(Request request)
188    {
189        return ContentTypeSearch.getEnum(parameters.getParameter("search-by-content-types-choice", "none"));
190    }
191    
192    @Override
193    protected SearchResults<AmetysObject> search(Request request, Collection<String> siteNames, String language, int pageIndex, int start, int maxResults) throws Exception
194    {
195        ContentTypeSearch searchCTypeType = getContentTypeSearch(request);
196        try
197        {
198            if (searchCTypeType.equals(ContentTypeSearch.CHECKBOX_FILTER) || searchCTypeType.equals(ContentTypeSearch.FILTER))
199            {
200                // Retrieve current workspace
201                String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
202
203                try
204                {
205                    XMLUtils.startElement(contentHandler, "content-types");
206
207                    List<Sort> sorts = new ArrayList<>();
208                    Sort sort = getSortField(request);
209                    sorts.addAll(getPrimarySortFields(request));
210                    sorts.add(sort);
211
212                    // Get first sort field
213                    saxSort(sort);
214
215                    Query queryObject = getQuery(request, siteNames, language);
216                    Collection<Query> filterQueries = getFilterQueries(request, siteNames, language);
217
218                    Collection<SearchField> facets = getFacets(request).values().stream().map(f -> f.getSearchField()).collect(Collectors.toList());
219                    Map<String, List<String>> facetValues = getFacetValues(request, siteNames, language);
220                    
221                    SearchResults<AmetysObject> results = null;
222                    
223                    String currentCtype = getContentTypeFilterValue(request);
224                    if (currentCtype == null)
225                    {
226                        // First, do search without 'content-types' facet value
227                        results = _getResultsForContentType(queryObject, filterQueries, facets, sorts, facetValues, null);
228                        
229                        Map<String, Map<String, Integer>> facetResults = results.getFacetResults();
230                        Map<String, Integer> cTypeFacets = facetResults.get(SolrWebFieldNames.PAGE_CONTENT_TYPES);
231                        Map<String, Integer> facetQueryResults = _getFacetQueryResults(results);
232                            
233                        long totalCount = results.getTotalCount();
234                        if (totalCount > 0)
235                        {
236                            Collection<String> cTypes = getContentTypes(request);
237                            
238                            // Get the first content types (in order) with at least one result
239                            for (String cType : cTypes)
240                            {
241                                long cTypeCount;
242                                if (cType.equals("resource"))
243                                {
244                                    cTypeCount = _getNbResource(facetQueryResults);
245                                }
246                                else if (cTypeFacets.containsKey(cType))
247                                {
248                                    cTypeCount = Long.valueOf(cTypeFacets.get(cType));
249                                }
250                                else
251                                {
252                                    continue;
253                                }
254                                
255                                if (cTypeCount > 0)
256                                {
257                                    // If facet count is equals to total count, no need to do the new search
258                                    if (cTypeCount < totalCount)
259                                    {
260                                        results = _getResultsForContentType(queryObject, filterQueries, facets, sorts, facetValues, cType);
261                                    }
262                                    
263                                    currentCtype = cType;
264                                    break;
265                                }
266                            }
267                        }
268                    }
269                    else
270                    {
271                        Searcher searcher = _searcherFactory.create()
272                                .withQuery(queryObject)
273                                .withFilterQueries(filterQueries)
274                                .withFacets(facets)
275                                .withFacetValues(facetValues)
276                                .withLimits(0, Integer.MAX_VALUE)
277                                .withSort(sorts)
278                                .setCheckRights(_checkRights());
279                        
280                        _additionalSearchProcessing(searcher);
281                        
282                        results = searcher.searchWithFacets();
283                    }
284
285                    _handleFacetResults(request, start, maxResults, results, currentCtype);
286
287                    XMLUtils.endElement(contentHandler, "content-types");
288
289                    return results;
290                }
291                finally
292                {
293                    // Restore context
294                    RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
295                }
296            }
297            else
298            {
299                return super.search(request, siteNames, language, pageIndex, start, maxResults);
300            }
301        }
302        catch (QuerySyntaxException e)
303        {
304            throw new IOException("Query syntax error while searching.", e);
305        }
306    }
307    
308    private Map<String, Integer> _getFacetQueryResults(SearchResults<AmetysObject> results)
309    {
310        return Optional.of(results)
311                       .filter(FrontOfficeSolrSearchResults.class::isInstance)
312                       .map(FrontOfficeSolrSearchResults.class::cast)
313                       .map(FrontOfficeSolrSearchResults::getFacetQueryResults)
314                       .orElseGet(Collections::emptyMap);
315    }
316    
317    private int _getNbResource(Map<String, Integer> facetQueryResults)
318    {
319        return Optional.ofNullable(facetQueryResults.get(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME)).orElse(0);
320    }
321    
322    private SearchResults<AmetysObject> _getResultsForContentType(Query queryObject, Collection<Query> filterQueries, Collection<SearchField> facets, List<Sort> sorts, Map<String, List<String>> initialFacetValues, String forceContentType) throws Exception
323    {
324        Map<String, List<String>> facetValues = new HashMap<>(initialFacetValues);
325        
326        if (forceContentType == null)
327        {
328            facetValues.remove(SolrWebFieldNames.PAGE_CONTENT_TYPES);
329        }
330        else
331        {
332            facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>());
333            facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(forceContentType);
334        }
335        
336        Searcher searcher = _searcherFactory.create()
337                .withQuery(queryObject)
338                .withFilterQueries(filterQueries)
339                .withFacets(facets)
340                .withFacetValues(facetValues)
341                .withLimits(0, Integer.MAX_VALUE)
342                .withSort(sorts)
343                .setCheckRights(_checkRights());
344        
345        _additionalSearchProcessing(searcher);
346        
347        return searcher.searchWithFacets();
348    }
349    
350    @Override
351    protected void _additionalSearchProcessing(Searcher searcher)
352    {
353        Request request = ObjectModelHelper.getRequest(objectModel);
354        
355        if (searcher instanceof FrontOfficeSearcher)
356        {
357            FrontOfficeSearcher foSearcher = (FrontOfficeSearcher) searcher;
358            Collection<QueryFacet> queryFacets = getQueryFacets(request);
359            if (!queryFacets.isEmpty())
360            {
361                foSearcher.withQueryFacets(queryFacets)
362                          .withQueryFacetValues(getQueryFacetValues(request));
363            }
364        }
365    }
366
367    private void _handleFacetResults(Request request, int start, int maxResults, SearchResults<AmetysObject> results, String currentCtype) throws SAXException
368    {
369        int count = 0;
370        Map<String, Map<String, Integer>> facetResults = results.getFacetResults();
371        Map<String, Integer> facetQueryResults = _getFacetQueryResults(results);
372        if (facetResults.containsKey(SolrWebFieldNames.PAGE_CONTENT_TYPES) || facetQueryResults.containsKey(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME))
373        {
374            Map<String, Integer> cTypeFacets = Optional.ofNullable(facetResults.get(SolrWebFieldNames.PAGE_CONTENT_TYPES)).orElseGet(Collections::emptyMap);
375            
376            for (String contentTypeId : getContentTypes(request))
377            {
378                int nbResults;
379                if ("resource".equals(contentTypeId))
380                {
381                    nbResults = _getNbResource(facetQueryResults);
382                }
383                else if (cTypeFacets.containsKey(contentTypeId) && cTypeFacets.get(contentTypeId) > 0)
384                {
385                    nbResults = cTypeFacets.get(contentTypeId);
386                }
387                else
388                {
389                    continue;
390                }
391
392                boolean current = contentTypeId.equals(currentCtype) || currentCtype == null && count == 0;
393                count++;
394
395                AttributesImpl attr = new AttributesImpl();
396                if (current)
397                {
398                    attr.addCDATAAttribute("current", "true");
399                }
400
401                XMLUtils.startElement(contentHandler, contentTypeId, attr);
402
403                if (contentTypeId.equals("resource"))
404                {
405                    new I18nizableText("plugin.web", "PLUGINS_WEB_SERVICE_FRONT_SEARCH_ON_DOCUMENTS").toSAX(contentHandler, "label");
406                }
407                else
408                {
409                    ContentType contentType = _cTypeExtPt.getExtension(contentTypeId);
410                    if (contentType != null)
411                    {
412                        contentType.getLabel().toSAX(contentHandler, "label");
413                    }
414                }
415
416                // SAX results
417                AttributesImpl atts = new AttributesImpl();
418                atts.addCDATAAttribute("total", String.valueOf(nbResults));
419                atts.addCDATAAttribute("maxScore", String.valueOf(results.getMaxScore()));
420
421                if (current)
422                {
423                    XMLUtils.startElement(contentHandler, "hits", atts);
424                    saxHits(results, start, maxResults);
425                    XMLUtils.endElement(contentHandler, "hits");
426
427                    // SAX pagination
428                    saxPagination(results.getTotalCount(), start, maxResults);
429                }
430                else
431                {
432                    XMLUtils.createElement(contentHandler, "hits", atts);
433                }
434
435                XMLUtils.endElement(contentHandler, contentTypeId);
436            }
437        }
438    }
439    
440    @Override
441    protected Query getQuery(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException
442    {
443        List<Query> wordingQueries = getWordingQueries(request, siteNames, language);
444        
445        // Query to execute on joined contents
446        List<Query> contentQueries = new ArrayList<>(wordingQueries); // add wording queries
447        contentQueries.addAll(getContentQueries(request, siteNames, language)); // add specific queries to contents
448        Query contentQuery = new AndQuery(contentQueries);
449        
450        List<Query> contentOrResourcesQueries = new ArrayList<>();
451        contentOrResourcesQueries.add(contentQuery);
452        if (!wordingQueries.isEmpty())
453        {
454            contentOrResourcesQueries.addAll(getContentResourcesOrAttachmentQueries(new AndQuery(wordingQueries))); // add queries on join content's resources
455        }
456        
457        Query finalContentQuery = new PageContentQuery(new OrQuery(contentOrResourcesQueries));
458        
459        // Query to execute on pages
460        List<Query> pagesQueries = new ArrayList<>(wordingQueries);  // add wording queries
461        pagesQueries.addAll(getPageQueries(request, siteNames, language)); // add specific queries to pages
462        Query pageQuery = pagesQueries.isEmpty() && contentQueries.isEmpty() ? new MatchAllQuery() : new AndQuery(pagesQueries);
463        
464        List<Query> pageOrResourcesQueries = new ArrayList<>();
465        pageOrResourcesQueries.add(pageQuery);
466        if (!wordingQueries.isEmpty())
467        {
468            pageOrResourcesQueries.addAll(getPageResourcesOrAttachmentQueries(new AndQuery(wordingQueries))); // add queries on join page's resources
469        }
470        
471        Query finalPageQuery = new OrQuery(pageOrResourcesQueries);
472        
473        // FIXME Remove this hack when CMS-11599 will be fixed
474        // The good code probably should be the old one => Query finalQuery = new OrQuery(finalPageQuery, finalContentQuery);
475        try
476        {
477            Query finalQuery = null;
478            if (StringUtils.isBlank(finalContentQuery.build()))
479            {
480                finalQuery = finalPageQuery;
481            }
482            else if (StringUtils.isBlank(finalPageQuery.build()))
483            {
484                finalQuery = finalContentQuery;
485            }
486            else
487            {
488                finalQuery = new OrQuery(finalPageQuery, finalContentQuery);
489            }
490        
491            for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter())
492            {
493                finalQuery = queryAdapter.modifyQuery(finalQuery, request, siteNames, language);
494            }
495
496            return finalQuery;
497        }
498        catch (QuerySyntaxException e)
499        {
500            throw new IllegalArgumentException("An error occurred building query", e);
501        }
502        
503    }
504    
505    /**
506     * Get the queries on wording (keywords, no words, exact wording, no words)
507     * @param request the request
508     * @param siteNames the site names
509     * @param language the language
510     * @return the queries on wording
511     */
512    protected List<Query> getWordingQueries(Request request, Collection<String> siteNames, String language)
513    {
514        List<Query> queries = new ArrayList<>();
515        
516        addTextFieldQuery(queries, language, request);
517        addAllWordsTextFieldQuery(queries, language, request);
518        addExactWordingTextFieldQuery(queries, language, request);
519        addNoWordsTextFieldQuery(queries, language, request);
520        
521        return queries;
522    }
523    
524    /**
525     * Get the queries to be apply on joined contents ONLY
526     * @param request the request
527     * @param siteNames the site names
528     * @param language the language
529     * @return the queries for contents only
530     */
531    protected List<Query> getContentQueries(Request request, Collection<String> siteNames, String language)
532    {
533        List<Query> queries = new ArrayList<>();
534        queries.add(new DocumentTypeQuery(SolrFieldNames.TYPE_CONTENT));
535        addAttributeQuery(queries, language, request);
536        return queries;
537    }
538    
539    /**
540     * Get the queries to be apply on pages ONLY
541     * @param request the request
542     * @param siteNames the site names
543     * @param language the language
544     * @return the queries for pages only
545     */
546    protected List<Query> getPageQueries(Request request, Collection<String> siteNames, String language)
547    {
548        return new ArrayList<>();
549    }
550
551    @Override
552    protected Collection<Query> getFilterQueries(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException
553    {
554        List<Query> queries = new ArrayList<>();
555
556        Query siteQuery = new SiteQuery(siteNames);
557        Query sitemapQuery = new SitemapQuery(language);
558        for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter())
559        {
560            siteQuery = queryAdapter.modifySiteQueryFilter(siteQuery, request, siteNames, language);
561            sitemapQuery = queryAdapter.modifySitemapQueryFilter(sitemapQuery, request, siteNames, language);
562        }
563
564        queries.add(siteQuery);
565        queries.add(sitemapQuery);
566        
567        addContentTypeQuery(queries, request);
568        addTagsQuery(queries, request);
569        addPagesQuery(queries, request);
570        addDateQuery(queries, request);
571
572        return queries;
573    }
574    
575    @Override
576    protected Map<String, List<String>> getFacetValues(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException
577    {
578        Map<String, List<String>> facetValues = new HashMap<>();
579        
580        String currentCType = getContentTypeFilterValue(request);
581        if (currentCType != null)
582        {
583            facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>());
584            if (!"resource".equals(currentCType))
585            {
586                facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(currentCType);
587            }
588        }
589        else if (getContentTypeSearch(request).equals(ContentTypeSearch.FILTER) || getContentTypeSearch(request).equals(ContentTypeSearch.CHECKBOX_FILTER))
590        {
591            // Get the first content types as facet value
592            facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>());
593            String firstContentType = getContentTypes(request).iterator().next();
594            if (!"resource".equals(firstContentType))
595            {
596                facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(firstContentType);
597            }
598        }
599        
600        Map<String, FacetField> facets = getFacets(request);
601        for (String fieldName : facets.keySet())
602        {
603            String[] parameterValues = request.getParameterValues("metadata-" + fieldName);
604            if (parameterValues != null && parameterValues.length > 0 && StringUtils.isNotEmpty(parameterValues[0]))
605            {
606                facetValues.put(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName, new ArrayList<>());
607                List<String> preparedParameterValues = Stream.of(parameterValues)
608                        .map(StringEscapeUtils::unescapeXml)
609                        .map(ClientUtils::escapeQueryChars)
610                        .collect(Collectors.toList());
611                facetValues.get(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName).addAll(preparedParameterValues);
612            }
613        }
614        
615        return facetValues;
616    }
617    
618    @Override
619    protected Collection<String> getQueryFacetValues(Request request)
620    {
621        List<String> queryFacetValues = new ArrayList<>();
622        
623        String currentCType = getContentTypeFilterValue(request);
624        if (currentCType != null)
625        {
626            if ("resource".equals(currentCType))
627            {
628                queryFacetValues.add(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME);
629            }
630        }
631        else if (getContentTypeSearch(request).equals(ContentTypeSearch.FILTER) || getContentTypeSearch(request).equals(ContentTypeSearch.CHECKBOX_FILTER))
632        {
633            // Get the first content types as facet value
634            String firstContentType = getContentTypes(request).iterator().next();
635            if ("resource".equals(firstContentType))
636            {
637                queryFacetValues.add(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME);
638            }
639        }
640        
641        return queryFacetValues;
642    }
643    
644    @Override
645    protected Map<String, FacetField> getFacets(Request request) throws IllegalArgumentException
646    {
647        ZoneItem zoneItem = getZoneItem(request);
648        String facetCacheAttrName = __FACETS_CACHE + "$" + Optional.ofNullable(zoneItem).map(ZoneItem::getId).orElse("null");
649        @SuppressWarnings("unchecked")
650        Map<String, FacetField> cache = (Map<String, FacetField>) request.getAttribute(facetCacheAttrName);
651        if (cache != null)
652        {
653            return cache;
654        }
655            
656        Map<String, FacetField> facets = new HashMap<>();
657        
658        // Facet for content types
659        ContentTypeSearch contentTypeSearch = getContentTypeSearch(request);
660        if (contentTypeSearch.equals(ContentTypeSearch.FILTER) || contentTypeSearch.equals(ContentTypeSearch.CHECKBOX_FILTER))
661        {
662            SearchField cTypeField = new ContentTypeSearchField();
663            
664            facets.put(PAGE_CONTENT_TYPES, new ContentTypeFacetField(cTypeField));
665        }
666        
667        if (useFacets())
668        {
669            Collection<String> contentTypes = getContentTypes(request);
670            
671            if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata"))
672            {
673                String[] metadataPaths = zoneItem.getServiceParameters().getValue("search-by-metadata");
674                for (String metadataPath : metadataPaths)
675                {
676                    _addAttributeFacet(facets, contentTypes, metadataPath);
677                }
678            }
679        }
680        
681        request.setAttribute(facetCacheAttrName, facets);
682        
683        return facets;
684    }
685
686    /**
687     * Add attribute facet to facets map
688     * @param facets the facets map
689     * @param contentTypes the content types
690     * @param attributePath the attribute path
691     */
692    protected void _addAttributeFacet(Map<String, FacetField> facets, Collection<String> contentTypes, String attributePath)
693    {
694        ModelItem modelItem = _getModelItemFromContentTypes(contentTypes, attributePath);
695        
696        if (modelItem != null && modelItem instanceof ElementDefinition)
697        {
698            ElementDefinition elementDefinition = (ElementDefinition) modelItem;
699            if (elementDefinition.getEnumerator() != null || elementDefinition.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID))
700            {
701                Optional<SearchField> searchField = _searchHelper.getSearchField(contentTypes, attributePath);
702                if (searchField.isPresent())
703                {
704                    String searchFieldName = searchField.get().getName();
705                    StringSearchField facetField = new StringSearchField(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + searchFieldName);
706                    facets.put(searchFieldName, new AttributeFacetField(facetField, modelItem, new AvalonLoggerAdapter(getLogger())));
707                }
708            }
709        }
710    }
711    
712    @Override
713    protected Set<QueryFacet> getQueryFacets(Request request)
714    {
715        Set<QueryFacet> queryFacets = new HashSet<>();
716        Collection<String> contentTypes = getContentTypes(request);
717        
718        // For handling "resource" content type in facets for content types
719        ContentTypeSearch contentTypeSearch = getContentTypeSearch(request);
720        if ((contentTypeSearch.equals(ContentTypeSearch.FILTER) || contentTypeSearch.equals(ContentTypeSearch.CHECKBOX_FILTER))
721            && contentTypes.contains("resource"))
722        {
723            queryFacets.add(new QueryFacet(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME, 
724                                           PAGE_CONTENT_TYPES, 
725                                           DOCUMENT_TYPE + ":" + TYPE_PAGE_RESOURCE));
726        }
727        
728        for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter())
729        {
730            queryFacets = queryAdapter.modifyQueryFacets(queryFacets, request);
731        }
732        
733        return queryFacets;
734    }
735
736    /**
737     * Get the filter queries for a single fixed content type.
738     * 
739     * @param request The request.
740     * @param cType The fixed content type.
741     * @param siteNames the site names.
742     * @param language The language.
743     * @return The filter queries.
744     */
745    protected Collection<Query> getFixedCTypeFilterQueries(Request request, String cType, Collection<String> siteNames, String language)
746    {
747        List<Query> queries = new ArrayList<>();
748
749        queries.add(new SiteQuery(siteNames));
750        queries.add(new SitemapQuery(language));
751
752        if ("resource".equals(cType))
753        {
754            queries.add(new DocumentTypeQuery(TYPE_PAGE_RESOURCE));
755        }
756        else
757        {
758            // Fixed content type query.
759            queries.add(new PageContentQuery(new ContentTypeQuery(cType)));
760        }
761        addTagsQuery(queries, request);
762        addPagesQuery(queries, request);
763        addDateQuery(queries, request);
764
765        return queries;
766    }
767
768    @Override
769    protected Collection<String> getDocumentTypes(Request request)
770    {
771        List<String> documentTypes = new ArrayList<>();
772        
773        Collection<String> cTypes = getContentTypes(request);
774        if (cTypes.size() == 1 && "resource".equals(cTypes.iterator().next()))
775        {
776            documentTypes.add(TYPE_PAGE_RESOURCE);
777        }
778        // An empty collections means "all".
779        else if (cTypes.isEmpty() || cTypes.contains("resource"))
780        {
781            documentTypes.addAll(Arrays.asList(TYPE_PAGE, TYPE_PAGE_RESOURCE));
782        }
783        else
784        {
785            documentTypes.add(TYPE_PAGE);
786        }
787        
788        // Add other documents types (by priority order)
789        for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter())
790        {
791            queryAdapter.addDocumentType(documentTypes);
792        }
793        
794        return documentTypes;
795    }
796
797    @Override
798    protected Collection<String> getFields()
799    {
800        return Collections.emptyList();
801    }
802
803    @Override
804    protected void saxHits(SearchResults<AmetysObject> results, int start, int maxResults) throws SAXException
805    {
806        SearchResultsIterable<SearchResult<AmetysObject>> resultsIt = results.getResults();
807        long limit = Math.min(start + maxResults, resultsIt.getSize());
808        float maxScore = results.getMaxScore();
809        
810        SearchResultsIterator<SearchResult<AmetysObject>> it = resultsIt.iterator();
811        it.skip(start);
812        for (int i = start; i < limit; i++)
813        {
814            if (it.hasNext()) // this should return true except if there is a inconsistency between repository and Solr index
815            {
816                SearchResult<AmetysObject> searchResult = it.next();
817                float score = searchResult.getScore();
818                AmetysObject ametysObject = searchResult.getObject();
819                if (ametysObject instanceof Page)
820                {
821                    saxPageHit(score, maxScore, (Page) ametysObject);
822                }
823                else if (ametysObject instanceof Resource)
824                {
825                    saxResourceHit(score, maxScore, (Resource) ametysObject);
826                }
827            }
828            
829        }
830    }
831
832    @Override
833    protected Sort getSortField(Request request)
834    {
835        if (request.getParameter("sort-by-title-for-sorting") != null || request.getParameter("sort-by-title") != null)
836        {
837            return new Sort(TITLE_SORT, Order.ASC);
838        }
839        else if (request.getParameter("sort-by-lastValidation") != null)
840        {
841            return new Sort(LastValidationSearchField.NAME, Order.DESC);
842        }
843        else
844        {
845            // Generic sort field (with hardcorded descending order)
846            Enumeration paramNames = request.getParameterNames();
847            while (paramNames.hasMoreElements())
848            {
849                String param = (String) paramNames.nextElement();
850                if (param.startsWith("sort-by-"))
851                {
852                    String fieldName = StringUtils.removeStart(param, "sort-by-");
853                    return new Sort(fieldName, Order.ASC);
854                }
855            }
856        }
857
858        return new Sort("score", Order.DESC);
859    }
860
861    @Override
862    protected List<Sort> getPrimarySortFields(Request request)
863    {
864        return new ArrayList<>();
865    }
866
867    @Override
868    protected void saxFormFields(Request request, String siteName, String lang) throws SAXException
869    {
870        XMLUtils.createElement(contentHandler, "textfield");
871
872        boolean advancedSearch = parameters.getParameterAsBoolean("advanced-search", true);
873        if (advancedSearch)
874        {
875            XMLUtils.createElement(contentHandler, "all-words");
876            XMLUtils.createElement(contentHandler, "exact-wording");
877            XMLUtils.createElement(contentHandler, "no-words");
878        }
879
880        ContentTypeSearch contentTypeSearch = getContentTypeSearch(request);
881        XMLUtils.createElement(contentHandler, "content-types-choice", contentTypeSearch.toString());
882
883        _saxContentTypeCriteria(request);
884        _saxAttributeCriteria(request, lang);
885        _saxTagsCriteria(siteName);
886        _saxSitemapCriteria();
887
888        boolean multisite = parameters.getParameterAsBoolean("search-multisite", false);
889        if (multisite)
890        {
891            XMLUtils.createElement(contentHandler, "multisite");
892
893            XMLUtils.startElement(contentHandler, "sites");
894            Collection<String> allSites = _siteManager.getSiteNames();
895            for (String name : allSites)
896            {
897                Site site = _siteManager.getSite(name);
898                if (!_isPrivate(site))
899                {
900                    AttributesImpl attr = new AttributesImpl();
901                    attr.addCDATAAttribute("name", name);
902                    if (name.equals(siteName))
903                    {
904                        attr.addCDATAAttribute("current", "true");
905                    }
906                    XMLUtils.createElement(contentHandler, "site", attr, StringUtils.defaultString(site.getTitle()));
907                }
908            }
909            XMLUtils.endElement(contentHandler, "sites");
910        }
911
912        if (StringUtils.isNotBlank(parameters.getParameter("startDate", "")))
913        {
914            XMLUtils.createElement(contentHandler, "dates", "true");
915        }
916    }
917    
918    private boolean _isPrivate(Site site)
919    {
920        String type = site.getType();
921        return _siteTypeEP.getExtension(type).isPrivateType();
922    }
923
924    private void _saxAttributeCriteria(Request request, String language) throws SAXException
925    {
926        ZoneItem zoneItem = getZoneItem(request);
927        if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata"))
928        {
929            String[] attributePaths = zoneItem.getServiceParameters().getValue("search-by-metadata");
930            if (attributePaths.length > 0)
931            {
932                Collection<String> cTypes = new ArrayList<>(getContentTypes(request));
933
934                for (String attributePath : attributePaths)
935                {
936                    ModelItem modelItem = _getModelItemFromContentTypes(cTypes, attributePath);
937                    saxAttributeDefinition(modelItem, attributePath, language);
938                }
939            }
940        }
941    }
942
943    /**
944     * Generates SAX events for attribute definition
945     * 
946     * @param modelItem The attribute definition.
947     * @param attributePath The attribute path
948     * @param language The current language
949     * @throws SAXException If an error occurred while generating SAX events
950     */
951    protected void saxAttributeDefinition(ModelItem modelItem, String attributePath, String language) throws SAXException
952    {
953        AttributesImpl attrs = new AttributesImpl();
954        attrs.addCDATAAttribute("name", attributePath);
955
956        XMLUtils.startElement(contentHandler, "metadata", attrs);
957        if (modelItem != null)
958        {
959            modelItem.getLabel().toSAX(contentHandler, "label");
960        }
961        else
962        {
963            XMLUtils.startElement(contentHandler, "label");
964            XMLUtils.data(contentHandler, attributePath);
965            XMLUtils.endElement(contentHandler, "label");
966        }
967
968        saxEnumeratorValueForAttribute(modelItem, attributePath, language);
969        XMLUtils.endElement(contentHandler, "metadata");
970    }
971
972    /**
973     * Sax enumeration value for enum or a content attribute
974     * @param modelItem The attribute definition.
975     * @param attributePath The attribute path
976     * @param language The current language
977     * @throws SAXException If an error occurred while saxing
978     */
979    protected void saxEnumeratorValueForAttribute(ModelItem modelItem, String attributePath, String language) throws SAXException
980    {
981        if (modelItem != null && modelItem instanceof ElementDefinition)
982        {
983            ElementDefinition attributeDefinition = (ElementDefinition) modelItem;
984            if (attributeDefinition.getEnumerator() != null)
985            {
986                XMLUtils.startElement(contentHandler, "enumeration");
987                try
988                {
989                    Map<Object, I18nizableText> entries = attributeDefinition.getEnumerator().getTypedEntries();
990                    for (Object key : entries.keySet())
991                    {
992                        AttributesImpl attrItem = new AttributesImpl();
993                        attrItem.addCDATAAttribute("value", (String) key);
994                        XMLUtils.startElement(contentHandler, "item", attrItem);
995                        entries.get(key).toSAX(contentHandler, "label");
996                        XMLUtils.endElement(contentHandler, "item");
997                    }
998                }
999                catch (Exception e)
1000                {
1001                    getLogger().error("An error occurred getting enumerator items for attribute : " + attributePath, e);
1002                }
1003                XMLUtils.endElement(contentHandler, "enumeration");
1004            }
1005            else if (attributeDefinition instanceof ContentAttributeDefinition)
1006            {
1007                ContentAttributeDefinition contentAttributeDefinition = (ContentAttributeDefinition) attributeDefinition;
1008                XMLUtils.startElement(contentHandler, "enumeration");
1009                Map<String, String> values = getContentValues(contentAttributeDefinition.getContentTypeId(), language);
1010                for (Entry<String, String> entry : values.entrySet())
1011                {
1012                    AttributesImpl attrItem = new AttributesImpl();
1013                    attrItem.addCDATAAttribute("value", entry.getKey());
1014                    XMLUtils.startElement(contentHandler, "item", attrItem);
1015                    XMLUtils.createElement(contentHandler, "label", entry.getValue());
1016                    XMLUtils.endElement(contentHandler, "item");
1017                }
1018                XMLUtils.endElement(contentHandler, "enumeration");
1019            }
1020        }
1021    }
1022    
1023    /**
1024     * Get values for contents enumeration
1025     * @param cTypeId The id of content type
1026     * @param language The current language
1027     * @return The contents
1028     */
1029    protected Map<String, String> getContentValues(String cTypeId, String language)
1030    {
1031        try
1032        {
1033            boolean multilingual = _cTypeExtPt.getExtension(cTypeId).isMultilingual();
1034            Expression expr = new AndExpression(
1035                    _getContentTypeExpression(cTypeId),
1036                    multilingual ? null : new LanguageExpression(org.ametys.plugins.repository.query.expression.Expression.Operator.EQ, language));
1037            AmetysObjectIterable<Content> contents = _resolver.query(QueryHelper.getXPathQuery(null, RepositoryConstants.NAMESPACE_PREFIX + ":content", expr));
1038            
1039            return contents.stream()
1040                    .collect(Collectors.toMap(Content::getId, c -> c.getTitle(new Locale(language))))
1041                    .entrySet()
1042                    .stream()
1043                    .sorted(Map.Entry.comparingByValue()) // sort by title
1044                    .collect(LambdaUtils.Collectors.toLinkedHashMap(Map.Entry::getKey, Map.Entry::getValue));
1045        }
1046        catch (Exception e)
1047        {
1048            getLogger().error("Failed to get content enumeration for content type " +  cTypeId, e);
1049            return MapUtils.EMPTY_MAP;
1050        }
1051    }
1052    
1053    private Expression _getContentTypeExpression(String parentCTypeId)
1054    {
1055        Stream<String> subCTypesIds = _cTypeExtPt.getSubTypes(parentCTypeId).stream();
1056        Expression[] exprs = Stream.concat(Stream.of(parentCTypeId), subCTypesIds)
1057                .map(cTypeId -> new ContentTypeExpression(org.ametys.plugins.repository.query.expression.Expression.Operator.EQ, cTypeId))
1058                .toArray(Expression[]::new);
1059        return new OrExpression(exprs);
1060    }
1061
1062    @Override
1063    protected void saxFormValues(Request request, int start, int offset) throws SAXException
1064    {
1065        _saxTextField(request);
1066        _saxMetadataValues(request);
1067        _saxAllWords(request);
1068        _saxExactWording(request);
1069        _saxNoWords(request);
1070        _saxContentType(request);
1071        _saxTags(request);
1072        _saxPages(request);
1073        _saxMultisite(request);
1074        _saxDates(request);
1075    }
1076
1077    private void _saxDates(Request request) throws SAXException
1078    {
1079        String startDate = request.getParameter("startDate");
1080        String endDate = request.getParameter("endDate");
1081
1082        if (StringUtils.isNotBlank(startDate) && StringUtils.isNotBlank(endDate) && startDate.compareTo(endDate) > 0)
1083        {
1084            String tmp = startDate;
1085            startDate = endDate;
1086            endDate = tmp;
1087        }
1088
1089        if (StringUtils.isNotBlank(startDate))
1090        {
1091            XMLUtils.createElement(contentHandler, "startDate", startDate);
1092        }
1093        if (StringUtils.isNotBlank(endDate))
1094        {
1095            XMLUtils.createElement(contentHandler, "endDate", endDate);
1096        }
1097    }
1098
1099    private void _saxTextField(Request request) throws SAXException
1100    {
1101        String textfield = request.getParameter("textfield");
1102        XMLUtils.createElement(contentHandler, "textfield", textfield != null ? textfield : "");
1103    }
1104
1105    private void _saxMetadataValues(Request request) throws SAXException
1106    {
1107        ZoneItem zoneItem = getZoneItem(request);
1108        if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata"))
1109        {
1110            String[] metadataPaths = zoneItem.getServiceParameters().getValue("search-by-metadata");
1111            if (metadataPaths.length > 0)
1112            {
1113                XMLUtils.startElement(contentHandler, "metadata");
1114                for (String metadataPath : metadataPaths)
1115                {
1116                    String[] values = request.getParameterValues("metadata-" + metadataPath.replaceAll("/", "."));
1117                    if (values != null)
1118                    {
1119                        for (String value : values)
1120                        {
1121                            AttributesImpl attrs = new AttributesImpl();
1122                            attrs.addCDATAAttribute("name", metadataPath);
1123                            XMLUtils.createElement(contentHandler, "metadata", attrs, value != null ? value : "");
1124                        }
1125                    }
1126                }
1127                XMLUtils.endElement(contentHandler, "metadata");
1128            }
1129        }
1130    }
1131
1132    private void _saxAllWords(Request request) throws SAXException
1133    {
1134        String textfield = request.getParameter("all-words");
1135        XMLUtils.createElement(contentHandler, "all-words", textfield != null ? textfield : "");
1136    }
1137
1138    private void _saxExactWording(Request request) throws SAXException
1139    {
1140        String textfield = request.getParameter("exact-wording");
1141        XMLUtils.createElement(contentHandler, "exact-wording", textfield != null ? textfield : "");
1142    }
1143
1144    private void _saxNoWords(Request request) throws SAXException
1145    {
1146        String textfield = request.getParameter("no-words");
1147        XMLUtils.createElement(contentHandler, "no-words", textfield != null ? textfield : "");
1148    }
1149
1150    private void _saxContentType(Request request) throws SAXException
1151    {
1152        String[] cTypes = request.getParameterValues("content-types");
1153        if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals("")))
1154        {
1155            for (String cType : cTypes)
1156            {
1157                XMLUtils.createElement(contentHandler, "content-type", cType);
1158            }
1159        }
1160    }
1161
1162    private void _saxTags(Request request) throws SAXException
1163    {
1164        String size = request.getParameter("tags-size");
1165        if (!StringUtils.isEmpty(size))
1166        {
1167            int nbCat = Integer.parseInt(size);
1168            for (int i = 1; i < nbCat + 1; i++)
1169            {
1170                String[] tags = request.getParameterValues("tags-" + i);
1171                if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals("")))
1172                {
1173                    if (tags.length == 1)
1174                    {
1175                        tags = tags[0].split(",");
1176                    }
1177
1178                    for (String tag : tags)
1179                    {
1180                        XMLUtils.createElement(contentHandler, "tag", tag);
1181                    }
1182
1183                }
1184            }
1185        }
1186
1187        String[] tags = request.getParameterValues("tags");
1188        if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals("")))
1189        {
1190            for (String tag : tags)
1191            {
1192                XMLUtils.createElement(contentHandler, "tag", tag);
1193            }
1194        }
1195    }
1196
1197    private void _saxPages(Request request) throws SAXException
1198    {
1199        String[] pages = request.getParameterValues("pages");
1200        if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals("")))
1201        {
1202            for (String id : pages)
1203            {
1204                XMLUtils.createElement(contentHandler, "page", id);
1205            }
1206        }
1207    }
1208
1209    private void _saxMultisite(Request request) throws SAXException
1210    {
1211        boolean multisite = request.getParameter("multisite") != null;
1212        if (multisite)
1213        {
1214            XMLUtils.createElement(contentHandler, "multisite");
1215
1216            String[] sites = request.getParameterValues("sites");
1217            if (sites != null && sites.length > 0 && !(sites.length == 1 && sites[0].equals("")))
1218            {
1219                for (String site : sites)
1220                {
1221                    XMLUtils.createElement(contentHandler, "site", site);
1222                }
1223            }
1224
1225        }
1226    }
1227
1228    /**
1229     * Get the content type's
1230     * 
1231     * @param request The request
1232     * @return the content type's
1233     */
1234    protected Collection<String> getContentTypes(Request request)
1235    {
1236        String[] cTypes = request.getParameterValues("content-types");
1237
1238        if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals("")))
1239        {
1240            if (cTypes.length == 1)
1241            {
1242                cTypes = cTypes[0].split(",");
1243            }
1244
1245            return Arrays.asList(cTypes);
1246        }
1247        return _getAvailableContentTypes(request);
1248    }
1249
1250    private Collection<String> _getAvailableContentTypes(Request request)
1251    {
1252        @SuppressWarnings("unchecked")
1253        Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
1254        if (parentContext == null)
1255        {
1256            parentContext = Collections.emptyMap();
1257        }
1258
1259        String[] serviceCTypes = (String[]) parentContext.get("search-by-content-types");
1260
1261        ContentTypeSearch contentTypeSearch = getContentTypeSearch(request);
1262        
1263        if (ArrayUtils.isNotEmpty(serviceCTypes) && StringUtils.isNotBlank(serviceCTypes[0]))
1264        {
1265            return Arrays.asList(serviceCTypes);
1266        }
1267        else if (!contentTypeSearch.equals(ContentTypeSearch.NONE))
1268        {
1269            return _getAllContentTypes();
1270        }
1271
1272        return Collections.emptyList();
1273    }
1274
1275    private Set<String> _getAllContentTypes()
1276    {
1277        Set<String> allCTypes = new HashSet<>(_cTypeExtPt.getExtensionsIds());
1278        allCTypes.add("resource");
1279        return allCTypes;
1280    }
1281
1282    private void _saxContentTypeCriteria(Request request) throws SAXException
1283    {
1284        Collection<String> cTypes = _getAvailableContentTypes(request);
1285
1286        XMLUtils.startElement(contentHandler, "content-types");
1287        for (String cTypeId : cTypes)
1288        {
1289            if ("resource".equals(cTypeId))
1290            {
1291                AttributesImpl attr = new AttributesImpl();
1292                attr.addCDATAAttribute("id", cTypeId);
1293                XMLUtils.startElement(contentHandler, "type", attr);
1294                new I18nizableText("plugin.web", "PLUGINS_WEB_SERVICE_FRONT_SEARCH_ON_DOCUMENTS").toSAX(contentHandler);
1295                XMLUtils.endElement(contentHandler, "type");
1296            }
1297            else if (StringUtils.isNotEmpty(cTypeId))
1298            {
1299                ContentType cType = _cTypeExtPt.getExtension(cTypeId);
1300
1301                if (cType != null)
1302                {
1303                    AttributesImpl attr = new AttributesImpl();
1304                    attr.addCDATAAttribute("id", cTypeId);
1305                    XMLUtils.startElement(contentHandler, "type", attr);
1306                    cType.getLabel().toSAX(contentHandler);
1307                    XMLUtils.endElement(contentHandler, "type");
1308                }
1309                else
1310                {
1311                    getLogger().warn("Cannot sax information about the unexising ContentType '" + cTypeId + "' for url " + request.getRequestURI());
1312                }
1313            }
1314
1315        }
1316        XMLUtils.endElement(contentHandler, "content-types");
1317    }
1318
1319    private void _saxTagsCriteria(String siteName) throws SAXException
1320    {
1321        @SuppressWarnings("unchecked")
1322        Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
1323        if (parentContext == null)
1324        {
1325            parentContext = Collections.emptyMap();
1326        }
1327
1328        Map<String, Object> contextParameters = new HashMap<>();
1329        contextParameters.put("siteName", siteName);
1330
1331        String[] tagArray = (String[]) parentContext.get("search-by-tags");
1332        if (ArrayUtils.isNotEmpty(tagArray))
1333        {
1334            XMLUtils.startElement(contentHandler, "tags");
1335            for (String tagName : tagArray)
1336            {
1337                if (StringUtils.isNotEmpty(tagName))
1338                {
1339                    boolean found = false;
1340                    Set<String> extensionsIds = _tagExtPt.getExtensionsIds();
1341
1342                    for (String id : extensionsIds)
1343                    {
1344                        if (found)
1345                        {
1346                            continue;
1347                        }
1348
1349                        I18nizableText label = null;
1350                        Map<String, CMSTag> tags = null;
1351
1352                        TagProvider<CMSTag> tagProvider = _tagExtPt.getExtension(id);
1353                        if (tagName.startsWith("provider_") && id.equals(tagName.substring("provider_".length())))
1354                        {
1355                            found = true;
1356
1357                            label = tagProvider.getLabel();
1358
1359                            tags = tagProvider.getTags(contextParameters);
1360                        }
1361                        else if (tagProvider.hasTag(tagName, contextParameters))
1362                        {
1363                            found = true;
1364
1365                            CMSTag tag = tagProvider.getTag(tagName, contextParameters);
1366                            label = tag.getTitle();
1367                            tags = tag.getTags();
1368                        }
1369
1370                        if (found)
1371                        {
1372                            AttributesImpl attr = new AttributesImpl();
1373                            attr.addCDATAAttribute("id", tagName);
1374                            XMLUtils.startElement(contentHandler, "tag", attr);
1375
1376                            if (label != null)
1377                            {
1378                                label.toSAX(contentHandler, "title");
1379                            }
1380
1381                            if (tags != null)
1382                            {
1383                                for (CMSTag child : tags.values())
1384                                {
1385                                    if (child.getTarget().getName().equals("CONTENT"))
1386                                    {
1387                                        _saxTag(child, true);
1388                                    }
1389                                }
1390                            }
1391
1392                            XMLUtils.endElement(contentHandler, "tag");
1393                        }
1394                    }
1395                }
1396            }
1397            XMLUtils.endElement(contentHandler, "tags");
1398        }
1399    }
1400
1401    private void _saxTag(Tag tag, boolean recursive) throws SAXException
1402    {
1403        AttributesImpl attr = new AttributesImpl();
1404        attr.addCDATAAttribute("id", tag.getName());
1405        XMLUtils.startElement(contentHandler, "tag", attr);
1406        tag.getTitle().toSAX(contentHandler, "title");
1407
1408        if (recursive)
1409        {
1410            for (Tag child : tag.getTags().values())
1411            {
1412                _saxTag(child, true);
1413            }
1414        }
1415
1416        XMLUtils.endElement(contentHandler, "tag");
1417    }
1418
1419    private void _saxSitemapCriteria() throws SAXException
1420    {
1421        @SuppressWarnings("unchecked")
1422        Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
1423        if (parentContext == null)
1424        {
1425            parentContext = Collections.emptyMap();
1426        }
1427
1428        String[] pages = (String[]) parentContext.get("search-by-pages");
1429        if (ArrayUtils.isNotEmpty(pages))
1430        {
1431            XMLUtils.startElement(contentHandler, "pages");
1432
1433            for (String pageID : pages)
1434            {
1435                if (StringUtils.isNotEmpty(pageID))
1436                {
1437                    Page page = _resolver.resolveById(pageID);
1438                    AttributesImpl attr = new AttributesImpl();
1439                    attr.addCDATAAttribute("id", pageID);
1440                    attr.addCDATAAttribute("path", page.getSitemap().getName() + "/" + page.getPathInSitemap());
1441                    attr.addCDATAAttribute("title", page.getTitle());
1442                    attr.addCDATAAttribute("long-title", page.getLongTitle());
1443                    XMLUtils.createElement(contentHandler, "page", attr);
1444                }
1445            }
1446
1447            XMLUtils.endElement(contentHandler, "pages");
1448        }
1449    }
1450
1451    /**
1452     * Get the content type's filter value
1453     * 
1454     * @param request The request
1455     * @return the content type's filter value
1456     */
1457    protected String getContentTypeFilterValue(Request request)
1458    {
1459        Enumeration<String> paramNames = request.getParameterNames();
1460        while (paramNames.hasMoreElements())
1461        {
1462            String paramName = paramNames.nextElement();
1463            if (paramName.startsWith("ctype-filter-"))
1464            {
1465                return paramName.substring("ctype-filter-".length());
1466            }
1467        }
1468
1469        if (request.getParameter("current-ctype-filter") != null)
1470        {
1471            return request.getParameter("current-ctype-filter");
1472        }
1473
1474        return null;
1475    }
1476
1477    /**
1478     * Add the text field query
1479     * @param queries the queries
1480     * @param language the language
1481     * @param request the request
1482     */
1483    protected void addTextFieldQuery(Collection<Query> queries, String language, Request request)
1484    {
1485        String text = request.getParameter("textfield");
1486
1487        if (StringUtils.isNotBlank(text))
1488        {
1489            String trimText = text.trim();
1490            String escapedText = _escapeQueryCharsButNotQuotes(trimText);
1491
1492            Query textFieldQuery = new OrQuery(
1493                    new FullTextQuery(escapedText, language, Operator.SEARCH, true),
1494                    new StringQuery(Content.ATTRIBUTE_TITLE, Operator.SEARCH, escapedText, language, true));
1495            queries.add(textFieldQuery);
1496        }
1497    }
1498    
1499    /**
1500     * Get the queries for content's attachments and resources
1501     * @param subQuery the query for attachments and resources
1502     * @return the join query for content's attachments and resources
1503     */
1504    protected List<Query> getContentResourcesOrAttachmentQueries(Query subQuery)
1505    {
1506        if (subQuery instanceof MatchAllQuery)
1507        {
1508            return Collections.emptyList();
1509        }
1510        
1511        List<Query> queries = new ArrayList<>();
1512        // join query on outgoing resources
1513        queries.add(new JoinQuery(subQuery, CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS));
1514        // join query on content's attachments
1515        queries.add(() -> "{!join from=" + RESOURCE_ANCESTOR_IDS + " to=" + CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS + " v=\"" + _buildSubQuery(subQuery) + "\"}");
1516        return queries;
1517    }
1518    
1519    /**
1520     * Get the queries for page's attachments and resources
1521     * @param subQuery the query for attachments and resources
1522     * @return the join query for content's attachments and resources
1523     */
1524    protected List<Query> getPageResourcesOrAttachmentQueries(Query subQuery)
1525    {
1526        List<Query> queries = new ArrayList<>();
1527        // join query on outgoing resources
1528        queries.add(new JoinQuery(subQuery, PAGE_OUTGOING_REFEERENCES_RESOURCE_IDS));
1529        // join query on page's attachments or explorer folder service
1530        queries.add(() -> "{!join from=" + RESOURCE_ANCESTOR_IDS + " to=" + PAGE_OUTGOING_REFEERENCES_RESOURCE_IDS + " v=\"" + _buildSubQuery(subQuery) + "\"}");
1531        return queries;
1532    }
1533    
1534    private String _buildSubQuery(Query subQuery) throws QuerySyntaxException
1535    {
1536        return ClientUtils.escapeQueryChars(subQuery.build());
1537    }
1538    
1539    /*
1540     * Do not escape double quotes for exact searching
1541     */
1542    private String _escapeQueryCharsButNotQuotes(String text)
1543    {
1544        StringBuilder sb = new StringBuilder();
1545        String[] parts = StringUtils.splitPreserveAllTokens(text, '"');
1546        for (int i = 0; i < parts.length; i++)
1547        {
1548            if (i != 0)
1549            {
1550                sb.append("\"");
1551            }
1552            String part = parts[i];
1553            sb.append(ClientUtils.escapeQueryChars(part));
1554        }
1555
1556        return sb.toString();
1557    }
1558
1559    /**
1560     * Add query for each attribute
1561     * 
1562     * @param queries The list of query
1563     * @param language The lang
1564     * @param request The request
1565     * @throws IllegalArgumentException If an error occurred
1566     */
1567    protected void addAttributeQuery(Collection<Query> queries, String language, Request request) throws IllegalArgumentException
1568    {
1569        ZoneItem zoneItem = getZoneItem(request);
1570        Set<String> facetFields = getFacets(request).keySet();
1571        if (zoneItem != null && zoneItem.getServiceParameters().hasValue("search-by-metadata"))
1572        {
1573            String[] attributePaths = zoneItem.getServiceParameters().getValue("search-by-metadata");
1574            for (String attributePath : attributePaths)
1575            {
1576                if (facetFields.contains(attributePath))
1577                {
1578                    // will be handled in org.ametys.web.frontoffice.SearchGenerator.getFacetValues(Request, Collection<String>, String)
1579                    continue;
1580                }
1581                String value = request.getParameter("metadata-" + attributePath.replaceAll("/", "."));
1582
1583                if (StringUtils.isNotBlank(value))
1584                {
1585                    Collection<String> contentTypes = getContentTypes(request);
1586                    ModelItem modelItem = _getModelItemFromContentTypes(contentTypes, attributePath);
1587                    queries.add(_getStringAttributeQuery(language, attributePath, value, modelItem));
1588                }
1589            }
1590        }
1591    }
1592
1593    /**
1594     * Get query from string attribute
1595     * @param language the language
1596     * @param attributePath the attribute path
1597     * @param value the value
1598     * @param modelItem the model item
1599     * @return the query
1600     */
1601    protected Query _getStringAttributeQuery(String language, String attributePath, String value, ModelItem modelItem)
1602    {
1603        Query query;
1604        if (modelItem != null && modelItem instanceof ElementDefinition
1605                && (((ElementDefinition) modelItem).getEnumerator() != null || modelItem.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID)))
1606        {
1607            // Exact search
1608            query = new StringQuery(attributePath, Operator.EQ, value, language);
1609        }
1610        else if (value.startsWith("\"") && value.endsWith("\""))
1611        {
1612            String escapedText = ClientUtils.escapeQueryChars(value.substring(1, value.length() - 1)); // the StringQuery with EQ operator will add quotes characters anyway, so remove them
1613            query = new StringQuery(attributePath, Operator.EQ, escapedText, language, true);
1614        }
1615        else
1616        {
1617            String escapedText = _escapedTextForSearch(value);
1618            Operator op = escapedText.startsWith("*") || escapedText.endsWith("*") ? Operator.LIKE : Operator.SEARCH;
1619            boolean doNotUseLanguage = op == Operator.LIKE && modelItem != null && !(modelItem.getType().getId().equals(ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID));
1620            query = new StringQuery(attributePath, op, escapedText, doNotUseLanguage ? null : language, true);
1621        }
1622        return query;
1623    }
1624    
1625    private String _escapedTextForSearch(String value)
1626    {
1627        // Allow wildcards at the beginning and at the end
1628        // Ex.:
1629        // zert will stay zert to not match azerty
1630        // *zert will stay *zert to not match azerty and match azert
1631        // zert* will stay zert* to not match azerty and match zerty
1632        // *zert* will stay *zert* to match azerty
1633        // where zert is escaped
1634        
1635        String valueToEscape = value;
1636        String begin = "";
1637        String end = "";
1638        boolean startsWithStar = value.startsWith("*");
1639        boolean endsWithStar = value.endsWith("*");
1640        if (startsWithStar)
1641        {
1642            valueToEscape = valueToEscape.substring(1);
1643            begin = "*";
1644        }
1645        if (endsWithStar)
1646        {
1647            valueToEscape = valueToEscape.substring(0, valueToEscape.length() - 1);
1648            end = "*";
1649        }
1650        
1651        // escape special characters
1652        String escapedValue = ClientUtils.escapeQueryChars(valueToEscape).toLowerCase();
1653        String escapedText = begin + escapedValue + end;
1654        return escapedText;
1655    }
1656
1657    /**
1658     * Add content query for "all words"
1659     * @param queries The queries
1660     * @param language The current language
1661     * @param request the request
1662     */
1663    protected void addAllWordsTextFieldQuery(Collection<Query> queries, String language, Request request) 
1664    {
1665        // TODO All words? OrQuery of AndQuery instead?
1666        String words = request.getParameter("all-words");
1667
1668        if (StringUtils.isNotBlank(words))
1669        {
1670            StringBuilder allWords = new StringBuilder();
1671            for (String word : StringUtils.split(words))
1672            {
1673                String escapedWord = ClientUtils.escapeQueryChars(word);
1674                if (allWords.length() > 0)
1675                {
1676                    allWords.append(' ');
1677                }
1678                allWords.append('+').append(escapedWord);
1679            }
1680
1681            // queries.add(new FullTextQuery(allWords.toString(), language,
1682            // Operator.SEARCH));
1683
1684            queries.add(new FullTextQuery(allWords.toString(), language, Operator.SEARCH, true));
1685        }
1686    }
1687
1688    /**
1689     * Add content query for exact wording or phrase
1690     * @param queries The queries
1691     * @param language The current language
1692     * @param request the request
1693     */
1694    protected void addExactWordingTextFieldQuery(Collection<Query> queries, String language, Request request)
1695    {
1696        String exact = request.getParameter("exact-wording");
1697
1698        if (StringUtils.isNotBlank(exact))
1699        {
1700            queries.add(new FullTextQuery(exact, language, Operator.EQ));
1701        }
1702    }
1703
1704    /**
1705     * Add content query for "none of these words"
1706     * @param queries The queries
1707     * @param language The current language
1708     * @param request the request
1709     */
1710    protected void addNoWordsTextFieldQuery(Collection<Query> queries, String language, Request request)
1711    {
1712        String noWords = request.getParameter("no-words");
1713
1714        if (StringUtils.isNotBlank(noWords))
1715        {
1716            StringBuilder allWords = new StringBuilder();
1717            for (String word : StringUtils.split(noWords))
1718            {
1719                String escapedWord = ClientUtils.escapeQueryChars(word);
1720                if (allWords.length() > 0)
1721                {
1722                    allWords.append(' ');
1723                }
1724                allWords.append('+').append(escapedWord);
1725            }
1726
1727            // queries.add(new NotQuery(new FullTextQuery(allWords.toString(),
1728            // language, Operator.SEARCH)));
1729
1730            queries.add(new NotQuery(new FullTextQuery(allWords.toString(), language, Operator.SEARCH, true)));
1731        }
1732    }
1733
1734    /** 
1735     * Add the content type query
1736     * @param queries The queries
1737     * @param request The request
1738     */
1739    protected void addContentTypeQuery(Collection<Query> queries, Request request)
1740    {
1741        Collection<String> cTypes = new ArrayList<>(getContentTypes(request));
1742
1743        // Resource is handled in the "document types" filter.
1744        cTypes.remove("resource");
1745
1746        if (!cTypes.isEmpty())
1747        {
1748            Query notAPageQuery = new NotQuery(new DocumentTypeQuery(TYPE_PAGE));
1749            Query pageWithContentOfTypesQuery = new PageContentQuery(new ContentTypeQuery(cTypes));
1750            // If document is not a page, it will match this fq (it will be filtered if necessary in the "document types" filter, see #getDocumentTypesQuery())
1751            // If document is a page, then one of its contents must match one of the given content types
1752            queries.add(new OrQuery(notAPageQuery, pageWithContentOfTypesQuery));
1753        }
1754    }
1755
1756    /** 
1757     * Add the tags query
1758     * @param queries The queries
1759     * @param request The request
1760     */
1761    protected void addTagsQuery(Collection<Query> queries, Request request)
1762    {
1763        String size = request.getParameter("tags-size");
1764        if (!StringUtils.isEmpty(size))
1765        {
1766            @SuppressWarnings("unchecked")
1767            Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
1768            if (parentContext == null)
1769            {
1770                parentContext = Collections.emptyMap();
1771            }
1772
1773            boolean isStrictSearch = parameters.getParameterAsBoolean("strict-search-on-tags", true);
1774
1775            int nbCat = Integer.parseInt(size);
1776            for (int i = 1; i < nbCat + 1; i++)
1777            {
1778                String[] tags = request.getParameterValues("tags-" + i);
1779                if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals("")))
1780                {
1781                    List<Query> tagQueries = new ArrayList<>();
1782
1783                    for (String tag : tags)
1784                    {
1785                        String[] values = StringUtils.split(tag, ',');
1786
1787                        tagQueries.add(new TagQuery(Operator.EQ, !isStrictSearch, values));
1788                    }
1789
1790                    queries.add(new PageContentQuery(new OrQuery(tagQueries)));
1791                }
1792            }
1793        }
1794    }
1795
1796    /** 
1797     * Add the pages query
1798     * @param queries The queries
1799     * @param request The request
1800     */
1801    protected void addPagesQuery(Collection<Query> queries, Request request)
1802    {
1803        List<Query> pageQueries = new ArrayList<>();
1804
1805        String[] pages = request.getParameterValues("pages");
1806        if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals("")))
1807        {
1808            for (String pageIds : pages)
1809            {
1810                for (String pageId : StringUtils.split(pageIds, ","))
1811                {
1812                    pageQueries.add(new PageQuery(pageId, true));
1813                }
1814            }
1815        }
1816
1817        queries.add(new OrQuery(pageQueries));
1818    }
1819
1820    /** 
1821     * Add the query on start and end dates
1822     * @param queries The queries
1823     * @param request The request
1824     */
1825    protected void addDateQuery(Collection<Query> queries, Request request)
1826    {
1827        String startDateId = parameters.getParameter("startDate", "");
1828        String endDateId = parameters.getParameter("endDate", "");
1829
1830        String startDateStr = request.getParameter("startDate");
1831        String endDateStr = request.getParameter("endDate");
1832
1833        // We check if startDateStr < endDateStr. If not, we swap.
1834        if (StringUtils.isNotBlank(endDateStr) && StringUtils.isNotBlank(startDateStr) && startDateStr.compareTo(endDateStr) > 0)
1835        {
1836            String dateAux = startDateStr;
1837            startDateStr = endDateStr;
1838            endDateStr = dateAux;
1839        }
1840
1841        String startPropId = startDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : startDateId;
1842        String endPropId = endDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : endDateId;
1843
1844        if (StringUtils.isNotBlank(endDateStr))
1845        {
1846            LocalDate endDate = _toDate(endDateStr);
1847            // Join for returning page documents
1848            queries.add(new PageContentQuery(new DateQuery(startPropId, Operator.LE, endDate)));
1849        }
1850
1851        if (StringUtils.isNotBlank(startDateStr))
1852        {
1853            LocalDate startDate = _toDate(startDateStr);
1854
1855            if (startDate != null)
1856            {
1857                // Two cases are possible :
1858                // An event could have an end date (query 1)
1859                // If not, the start date is also the end date (query 2)
1860                List<Query> endDateQueries = new ArrayList<>();
1861
1862                if (StringUtils.isNotBlank(endPropId))
1863                {
1864                    endDateQueries.add(new DateQuery(endPropId, Operator.GE, startDate));
1865                }
1866                endDateQueries.add(new DateQuery(startPropId, Operator.GE, startDate));
1867
1868                // Join for returning page documents
1869                queries.add(new PageContentQuery(new OrQuery(endDateQueries)));
1870            }
1871        }
1872    }
1873
1874    private LocalDate _toDate(String dateStr)
1875    {
1876        try
1877        {
1878            return LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE);
1879        }
1880        catch (DateTimeParseException e)
1881        {
1882            getLogger().error("Invalid date format " + dateStr, e);
1883        }
1884        return null;
1885    }
1886    
1887    private List<QueryAdapterFOSearch> _getSortedListQueryAdapter()
1888    {
1889        return _queryAdapterFOSearchEP.getExtensionsIds()
1890            .stream()
1891            .map(_queryAdapterFOSearchEP::getExtension)
1892            .collect(Collectors.toList());
1893    }
1894    
1895    private ModelItem _getModelItemFromContentTypes(Collection<String> contentTypeIds, String attributePath)
1896    {
1897        if (contentTypeIds.isEmpty())
1898        {
1899            return null;
1900        }
1901        
1902        try
1903        {
1904            String[] contentTypeIdsAsArray = contentTypeIds.toArray(new String[contentTypeIds.size()]);
1905            return _contentTypesHelper.getModelItem(attributePath, contentTypeIdsAsArray, new String[0]);
1906        }
1907        catch (UndefinedItemPathException e)
1908        {
1909            // No such attribute exists, retrieve null
1910            return null;
1911        }
1912    }
1913    
1914    static class ContentTypeSearchField implements SearchField
1915    {
1916        @Override
1917        public String getSortField()
1918        {
1919            return PAGE_CONTENT_TYPES;
1920        }
1921        
1922        @Override
1923        public String getName()
1924        {
1925            return PAGE_CONTENT_TYPES;
1926        }
1927        
1928        @Override
1929        public String getFacetField()
1930        {
1931            return PAGE_CONTENT_TYPES + "_s_dv";
1932        }
1933        
1934        @Override
1935        public boolean isJoined()
1936        {
1937            return false;
1938        }
1939        
1940        @Override
1941        public List<String> getJoinedPaths()
1942        {
1943            return Collections.emptyList();
1944        }
1945    }
1946
1947}