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