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