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.Date;
029import java.util.Enumeration;
030import java.util.HashMap;
031import java.util.HashSet;
032import java.util.LinkedHashMap;
033import java.util.List;
034import java.util.Locale;
035import java.util.Map;
036import java.util.Map.Entry;
037import java.util.Optional;
038import java.util.Set;
039import java.util.stream.Collectors;
040import java.util.stream.Stream;
041
042import org.apache.avalon.framework.service.ServiceException;
043import org.apache.avalon.framework.service.ServiceManager;
044import org.apache.cocoon.environment.ObjectModelHelper;
045import org.apache.cocoon.environment.Request;
046import org.apache.cocoon.xml.AttributesImpl;
047import org.apache.cocoon.xml.XMLUtils;
048import org.apache.commons.collections.MapUtils;
049import org.apache.commons.lang.StringEscapeUtils;
050import org.apache.commons.lang3.ArrayUtils;
051import org.apache.commons.lang3.StringUtils;
052import org.apache.solr.client.solrj.util.ClientUtils;
053import org.xml.sax.SAXException;
054
055import org.ametys.cms.content.indexing.solr.SolrFieldNames;
056import org.ametys.cms.contenttype.ContentType;
057import org.ametys.cms.contenttype.MetadataDefinition;
058import org.ametys.cms.contenttype.MetadataType;
059import org.ametys.cms.repository.Content;
060import org.ametys.cms.repository.ContentLanguageExpression;
061import org.ametys.cms.repository.ContentTypeExpression;
062import org.ametys.cms.search.SearchField;
063import org.ametys.cms.search.SearchResult;
064import org.ametys.cms.search.SearchResults;
065import org.ametys.cms.search.SearchResultsIterable;
066import org.ametys.cms.search.SearchResultsIterator;
067import org.ametys.cms.search.Sort;
068import org.ametys.cms.search.Sort.Order;
069import org.ametys.cms.search.query.AndQuery;
070import org.ametys.cms.search.query.ContentTypeQuery;
071import org.ametys.cms.search.query.DateQuery;
072import org.ametys.cms.search.query.DocumentTypeQuery;
073import org.ametys.cms.search.query.FullTextQuery;
074import org.ametys.cms.search.query.JoinQuery;
075import org.ametys.cms.search.query.MatchAllQuery;
076import org.ametys.cms.search.query.NotQuery;
077import org.ametys.cms.search.query.OrQuery;
078import org.ametys.cms.search.query.Query;
079import org.ametys.cms.search.query.Query.Operator;
080import org.ametys.cms.search.query.QuerySyntaxException;
081import org.ametys.cms.search.query.StringQuery;
082import org.ametys.cms.search.query.TagQuery;
083import org.ametys.cms.search.solr.SearcherFactory.Searcher;
084import org.ametys.cms.search.solr.field.StringSearchField;
085import org.ametys.cms.tag.Tag;
086import org.ametys.cms.tag.TagProvider;
087import org.ametys.core.util.AvalonLoggerAdapter;
088import org.ametys.core.util.DateUtils;
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.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        addMetadataQuery(queries, language, request);
510        return queries;
511    }
512    
513    /**
514     * Get the queries to be apply on pages ONLY
515     * @param request the request
516     * @param siteNames the site names
517     * @param language the language
518     * @return the queries for pages only
519     */
520    protected List<Query> getPageQueries(Request request, Collection<String> siteNames, String language)
521    {
522        return new ArrayList<>();
523    }
524
525    @Override
526    protected Collection<Query> getFilterQueries(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException
527    {
528        List<Query> queries = new ArrayList<>();
529
530        Query siteQuery = new SiteQuery(siteNames);
531        Query sitemapQuery = new SitemapQuery(language);
532        for (QueryAdapterFOSearch queryAdapter : _getSortedListQueryAdapter())
533        {
534            siteQuery = queryAdapter.modifySiteQueryFilter(siteQuery, request, siteNames, language);
535            sitemapQuery = queryAdapter.modifySitemapQueryFilter(sitemapQuery, request, siteNames, language);
536        }
537
538        queries.add(siteQuery);
539        queries.add(sitemapQuery);
540        
541        addContentTypeQuery(queries, request);
542        addTagsQuery(queries, request);
543        addPagesQuery(queries, request);
544        addDateQuery(queries, request);
545
546        return queries;
547    }
548    
549    @Override
550    protected Map<String, List<String>> getFacetValues(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException
551    {
552        Map<String, List<String>> facetValues = new HashMap<>();
553        
554        String currentCType = getContentTypeFilterValue(request);
555        if (currentCType != null)
556        {
557            facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>());
558            if (!"resource".equals(currentCType))
559            {
560                facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(currentCType);
561            }
562        }
563        else if (getContentTypeSearch(request).equals(ContentTypeSearch.FILTER) || getContentTypeSearch(request).equals(ContentTypeSearch.CHECKBOX_FILTER))
564        {
565            // Get the first content types as facet value
566            facetValues.put(SolrWebFieldNames.PAGE_CONTENT_TYPES, new ArrayList<>());
567            String firstContentType = getContentTypes(request).iterator().next();
568            if (!"resource".equals(firstContentType))
569            {
570                facetValues.get(SolrWebFieldNames.PAGE_CONTENT_TYPES).add(firstContentType);
571            }
572        }
573        
574        Map<String, FacetField> facets = getFacets(request);
575        for (String fieldName : facets.keySet())
576        {
577            String[] parameterValues = request.getParameterValues("metadata-" + fieldName);
578            if (parameterValues != null && parameterValues.length > 0 && StringUtils.isNotEmpty(parameterValues[0]))
579            {
580                facetValues.put(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName, new ArrayList<>());
581                List<String> preparedParameterValues = Stream.of(parameterValues)
582                        .map(StringEscapeUtils::unescapeXml)
583                        .map(ClientUtils::escapeQueryChars)
584                        .collect(Collectors.toList());
585                facetValues.get(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + fieldName).addAll(preparedParameterValues);
586            }
587        }
588        
589        return facetValues;
590    }
591    
592    @Override
593    protected Collection<String> getQueryFacetValues(Request request)
594    {
595        List<String> queryFacetValues = new ArrayList<>();
596        
597        String currentCType = getContentTypeFilterValue(request);
598        if (currentCType != null)
599        {
600            if ("resource".equals(currentCType))
601            {
602                queryFacetValues.add(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME);
603            }
604        }
605        else if (getContentTypeSearch(request).equals(ContentTypeSearch.FILTER) || getContentTypeSearch(request).equals(ContentTypeSearch.CHECKBOX_FILTER))
606        {
607            // Get the first content types as facet value
608            String firstContentType = getContentTypes(request).iterator().next();
609            if ("resource".equals(firstContentType))
610            {
611                queryFacetValues.add(DOCUMENT_TYPE_IS_PAGE_RESOURCE_FACET_NAME);
612            }
613        }
614        
615        return queryFacetValues;
616    }
617    
618    @Override
619    protected Map<String, FacetField> getFacets(Request request) throws IllegalArgumentException
620    {
621        ZoneItem zoneItem = getZoneItem(request);
622        String facetCacheAttrName = __FACETS_CACHE + "$" + Optional.ofNullable(zoneItem).map(ZoneItem::getId).orElse("null");
623        @SuppressWarnings("unchecked")
624        Map<String, FacetField> cache = (Map<String, FacetField>) request.getAttribute(facetCacheAttrName);
625        if (cache != null)
626        {
627            return cache;
628        }
629            
630        Map<String, FacetField> facets = new HashMap<>();
631        
632        // Facet for content types
633        ContentTypeSearch contentTypeSearch = getContentTypeSearch(request);
634        if (contentTypeSearch.equals(ContentTypeSearch.FILTER) || contentTypeSearch.equals(ContentTypeSearch.CHECKBOX_FILTER))
635        {
636            SearchField cTypeField = new SearchField()
637            {
638                @Override
639                public String getSortField()
640                {
641                    return PAGE_CONTENT_TYPES;
642                }
643                
644                @Override
645                public String getName()
646                {
647                    return PAGE_CONTENT_TYPES;
648                }
649                
650                @Override
651                public String getFacetField()
652                {
653                    return PAGE_CONTENT_TYPES + "_s_dv";
654                }
655            };
656            
657            facets.put(PAGE_CONTENT_TYPES, new ContentTypeFacetField(cTypeField));
658        }
659        
660        if (useFacets())
661        {
662            Collection<String> contentTypes = getContentTypes(request);
663            
664            if (zoneItem != null && zoneItem.getServiceParameters().hasMetadata("search-by-metadata"))
665            {
666                String[] metadataPaths = zoneItem.getServiceParameters().getStringArray("search-by-metadata");
667                for (String metadataPath : metadataPaths)
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                        SearchField searchField = _searchHelper.getSearchField(contentTypes, metadataPath);
675                        if (searchField != null)
676                        {
677                            StringSearchField facetField = new StringSearchField(SolrWebFieldNames.FACETABLE_CONTENT_FIELD_PREFIX + searchField.getName());
678                            facets.put(searchField.getName(), new MetadataFacetField(facetField, metadataDefinition, new AvalonLoggerAdapter(getLogger())));
679                        }
680                    }
681                }
682            }
683        }
684        
685        request.setAttribute(facetCacheAttrName, facets);
686        
687        return facets;
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 Collection<String> getFields()
777    {
778        return Collections.emptyList();
779    }
780
781    @Override
782    protected void saxHits(SearchResults<AmetysObject> results, int start, int maxResults) throws SAXException, IOException
783    {
784        SearchResultsIterable<SearchResult<AmetysObject>> resultsIt = results.getResults();
785        long limit = Math.min(start + maxResults, resultsIt.getSize());
786        float maxScore = results.getMaxScore();
787        
788        SearchResultsIterator<SearchResult<AmetysObject>> it = resultsIt.iterator();
789        it.skip(start);
790        for (int i = start; i < limit; i++)
791        {
792            if (it.hasNext()) // this should return true except if there is a inconsistency between repository and Solr index
793            {
794                SearchResult<AmetysObject> searchResult = it.next();
795                float score = searchResult.getScore();
796                AmetysObject ametysObject = searchResult.getObject();
797                if (ametysObject instanceof Page)
798                {
799                    saxPageHit(score, maxScore, (Page) ametysObject);
800                }
801                else if (ametysObject instanceof Resource)
802                {
803                    saxResourceHit(score, maxScore, (Resource) ametysObject);
804                }
805            }
806            
807        }
808    }
809
810    @Override
811    protected Sort getSortField(Request request)
812    {
813        if (request.getParameter("sort-by-title-for-sorting") != null || request.getParameter("sort-by-title") != null)
814        {
815            return new Sort(TITLE_SORT, Order.ASC);
816        }
817        else if (request.getParameter("sort-by-lastValidation") != null)
818        {
819            return new Sort(LAST_VALIDATION, Order.DESC);
820        }
821        else
822        {
823            // Generic sort field (with hardcorded descending order)
824            Enumeration paramNames = request.getParameterNames();
825            while (paramNames.hasMoreElements())
826            {
827                String param = (String) paramNames.nextElement();
828                if (param.startsWith("sort-by-"))
829                {
830                    String fieldName = StringUtils.removeStart(param, "sort-by-");
831                    return new Sort(fieldName, Order.ASC);
832                }
833            }
834        }
835
836        return new Sort("score", Order.DESC);
837    }
838
839    @Override
840    protected List<Sort> getPrimarySortFields(Request request)
841    {
842        return new ArrayList<>();
843    }
844
845    @Override
846    protected void saxFormFields(Request request, String siteName, String lang) throws SAXException
847    {
848        XMLUtils.createElement(contentHandler, "textfield");
849
850        boolean advancedSearch = parameters.getParameterAsBoolean("advanced-search", true);
851        if (advancedSearch)
852        {
853            XMLUtils.createElement(contentHandler, "all-words");
854            XMLUtils.createElement(contentHandler, "exact-wording");
855            XMLUtils.createElement(contentHandler, "no-words");
856        }
857
858        ContentTypeSearch contentTypeSearch = getContentTypeSearch(request);
859        XMLUtils.createElement(contentHandler, "content-types-choice", contentTypeSearch.toString());
860
861        _saxContentTypeCriteria(request);
862        _saxMetadataCriteria(request, lang);
863        _saxTagsCriteria(siteName);
864        _saxSitemapCriteria();
865
866        boolean multisite = parameters.getParameterAsBoolean("search-multisite", false);
867        if (multisite)
868        {
869            XMLUtils.createElement(contentHandler, "multisite");
870
871            XMLUtils.startElement(contentHandler, "sites");
872            Collection<String> allSites = _siteManager.getSiteNames();
873            for (String name : allSites)
874            {
875                Site site = _siteManager.getSite(name);
876                if (!_isPrivate(site))
877                {
878                    AttributesImpl attr = new AttributesImpl();
879                    attr.addCDATAAttribute("name", name);
880                    if (name.equals(siteName))
881                    {
882                        attr.addCDATAAttribute("current", "true");
883                    }
884                    XMLUtils.createElement(contentHandler, "site", attr, StringUtils.defaultString(site.getTitle()));
885                }
886            }
887            XMLUtils.endElement(contentHandler, "sites");
888        }
889
890        if (StringUtils.isNotBlank(parameters.getParameter("startDate", "")))
891        {
892            XMLUtils.createElement(contentHandler, "dates", "true");
893        }
894    }
895    
896    private boolean _isPrivate(Site site)
897    {
898        String type = site.getType();
899        return _siteTypeEP.getExtension(type).isPrivateType();
900    }
901
902    private void _saxMetadataCriteria(Request request, String language) throws SAXException
903    {
904        ZoneItem zoneItem = getZoneItem(request);
905        if (zoneItem != null && zoneItem.getServiceParameters().hasMetadata("search-by-metadata"))
906        {
907            String[] metadataPaths = zoneItem.getServiceParameters().getStringArray("search-by-metadata");
908            if (metadataPaths.length > 0)
909            {
910                Collection<String> cTypes = new ArrayList<>(getContentTypes(request));
911
912                for (String metadataPath : metadataPaths)
913                {
914                    MetadataDefinition metadataDef = _contentTypesHelper.getMetadataDefinitionByMetadataValuePath(metadataPath, cTypes.toArray(new String[cTypes.size()]),
915                            new String[0]);
916
917                    saxMetadataDef(metadataDef, metadataPath, language);
918                }
919            }
920        }
921    }
922
923    /**
924     * Sax metadata set information for metadata
925     * 
926     * @param metadataDef The metadata definition.
927     * @param metadataPath The metadata path
928     * @param language The current language
929     * @throws SAXException If an error occurred while saxing
930     */
931    protected void saxMetadataDef(MetadataDefinition metadataDef, String metadataPath, String language) throws SAXException
932    {
933        AttributesImpl attrs = new AttributesImpl();
934        attrs.addCDATAAttribute("name", metadataPath);
935
936        XMLUtils.startElement(contentHandler, "metadata", attrs);
937        if (metadataDef != null)
938        {
939            metadataDef.getLabel().toSAX(contentHandler, "label");
940        }
941        else
942        {
943            XMLUtils.startElement(contentHandler, "label");
944            XMLUtils.data(contentHandler, metadataPath);
945            XMLUtils.endElement(contentHandler, "label");
946        }
947
948        saxEnumeratorValueForMetadata(metadataDef, metadataPath, language);
949        XMLUtils.endElement(contentHandler, "metadata");
950    }
951
952    /**
953     * Sax enumeration value for enum or a content metadata
954     * @param metadataDef The metadata definition.
955     * @param metadataPath The metadata path
956     * @param language The current language
957     * @throws SAXException If an error occurred while saxing
958     */
959    protected void saxEnumeratorValueForMetadata(MetadataDefinition metadataDef, String metadataPath, String language) throws SAXException
960    {
961        if (metadataDef != null && metadataDef.getEnumerator() != null)
962        {
963            XMLUtils.startElement(contentHandler, "enumeration");
964            try
965            {
966                Map<Object, I18nizableText> entries = metadataDef.getEnumerator().getEntries();
967                for (Object key : entries.keySet())
968                {
969                    AttributesImpl attrItem = new AttributesImpl();
970                    attrItem.addCDATAAttribute("value", (String) key);
971                    XMLUtils.startElement(contentHandler, "item", attrItem);
972                    entries.get(key).toSAX(contentHandler, "label");
973                    XMLUtils.endElement(contentHandler, "item");
974                }
975            }
976            catch (Exception e)
977            {
978                getLogger().error("An error occurred getting enumerator items for metadata : " + metadataPath, e);
979            }
980            XMLUtils.endElement(contentHandler, "enumeration");
981        }
982        else if (metadataDef != null && metadataDef.getType().equals(MetadataType.CONTENT))
983        {
984            XMLUtils.startElement(contentHandler, "enumeration");
985            Map<String, String> values = getContentValues(metadataDef.getContentType(), language);
986            for (Entry<String, String> entry : values.entrySet())
987            {
988                AttributesImpl attrItem = new AttributesImpl();
989                attrItem.addCDATAAttribute("value", entry.getKey());
990                XMLUtils.startElement(contentHandler, "item", attrItem);
991                XMLUtils.createElement(contentHandler, "label", entry.getValue());
992                XMLUtils.endElement(contentHandler, "item");
993            }
994            XMLUtils.endElement(contentHandler, "enumeration");
995        }
996    }
997    
998    /**
999     * Get values for contents enumeration
1000     * @param cTypeId The id of content type
1001     * @param language The current language
1002     * @return The contents
1003     */
1004    protected Map<String, String> getContentValues(String cTypeId, String language)
1005    {
1006        try
1007        {
1008            boolean multilingual = _cTypeExtPt.getExtension(cTypeId).isMultilingual();
1009            Expression expr = new AndExpression(
1010                    new ContentTypeExpression(org.ametys.plugins.repository.query.expression.Expression.Operator.EQ, cTypeId),
1011                    multilingual ? null : new ContentLanguageExpression(org.ametys.plugins.repository.query.expression.Expression.Operator.EQ, language));
1012            AmetysObjectIterable<Content> contents = _resolver.query(QueryHelper.getXPathQuery(null, RepositoryConstants.NAMESPACE_PREFIX + ":content", expr));
1013            
1014            return contents.stream()
1015                    .collect(Collectors.toMap(Content::getId, c -> c.getTitle(new Locale(language))))
1016                    .entrySet()
1017                    .stream()
1018                    .sorted(Map.Entry.comparingByValue()) // sort by title
1019                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
1020        }
1021        catch (Exception e)
1022        {
1023            getLogger().error("Failed to get content enumeration for content type " +  cTypeId, e);
1024            return MapUtils.EMPTY_MAP;
1025        }
1026    }
1027
1028    @Override
1029    protected void saxFormValues(Request request, int start, int offset) throws SAXException
1030    {
1031        _saxTextField(request);
1032        _saxMetadataValues(request);
1033        _saxAllWords(request);
1034        _saxExactWording(request);
1035        _saxNoWords(request);
1036        _saxContentType(request);
1037        _saxTags(request);
1038        _saxPages(request);
1039        _saxMultisite(request);
1040        _saxDates(request);
1041    }
1042
1043    private void _saxDates(Request request) throws SAXException
1044    {
1045        String startDate = request.getParameter("startDate");
1046        String endDate = request.getParameter("endDate");
1047
1048        if (StringUtils.isNotBlank(startDate) && StringUtils.isNotBlank(endDate) && startDate.compareTo(endDate) > 0)
1049        {
1050            String tmp = startDate;
1051            startDate = endDate;
1052            endDate = tmp;
1053        }
1054
1055        if (StringUtils.isNotBlank(startDate))
1056        {
1057            XMLUtils.createElement(contentHandler, "startDate", startDate);
1058        }
1059        if (StringUtils.isNotBlank(endDate))
1060        {
1061            XMLUtils.createElement(contentHandler, "endDate", endDate);
1062        }
1063    }
1064
1065    private void _saxTextField(Request request) throws SAXException
1066    {
1067        String textfield = request.getParameter("textfield");
1068        XMLUtils.createElement(contentHandler, "textfield", textfield != null ? textfield : "");
1069    }
1070
1071    private void _saxMetadataValues(Request request) throws SAXException
1072    {
1073        ZoneItem zoneItem = getZoneItem(request);
1074        if (zoneItem != null && zoneItem.getServiceParameters().hasMetadata("search-by-metadata"))
1075        {
1076            String[] metadataPaths = zoneItem.getServiceParameters().getStringArray("search-by-metadata");
1077            if (metadataPaths.length > 0)
1078            {
1079                XMLUtils.startElement(contentHandler, "metadata");
1080                for (String metadataPath : metadataPaths)
1081                {
1082                    String[] values = request.getParameterValues("metadata-" + metadataPath.replaceAll("/", "."));
1083                    if (values != null)
1084                    {
1085                        for (String value : values)
1086                        {
1087                            AttributesImpl attrs = new AttributesImpl();
1088                            attrs.addCDATAAttribute("name", metadataPath);
1089                            XMLUtils.createElement(contentHandler, "metadata", attrs, value != null ? value : "");
1090                        }
1091                    }
1092                }
1093                XMLUtils.endElement(contentHandler, "metadata");
1094            }
1095        }
1096    }
1097
1098    private void _saxAllWords(Request request) throws SAXException
1099    {
1100        String textfield = request.getParameter("all-words");
1101        XMLUtils.createElement(contentHandler, "all-words", textfield != null ? textfield : "");
1102    }
1103
1104    private void _saxExactWording(Request request) throws SAXException
1105    {
1106        String textfield = request.getParameter("exact-wording");
1107        XMLUtils.createElement(contentHandler, "exact-wording", textfield != null ? textfield : "");
1108    }
1109
1110    private void _saxNoWords(Request request) throws SAXException
1111    {
1112        String textfield = request.getParameter("no-words");
1113        XMLUtils.createElement(contentHandler, "no-words", textfield != null ? textfield : "");
1114    }
1115
1116    private void _saxContentType(Request request) throws SAXException
1117    {
1118        String[] cTypes = request.getParameterValues("content-types");
1119        if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals("")))
1120        {
1121            for (String cType : cTypes)
1122            {
1123                XMLUtils.createElement(contentHandler, "content-type", cType);
1124            }
1125        }
1126    }
1127
1128    private void _saxTags(Request request) throws SAXException
1129    {
1130        String size = request.getParameter("tags-size");
1131        if (!StringUtils.isEmpty(size))
1132        {
1133            int nbCat = Integer.parseInt(size);
1134            for (int i = 1; i < nbCat + 1; i++)
1135            {
1136                String[] tags = request.getParameterValues("tags-" + i);
1137                if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals("")))
1138                {
1139                    if (tags.length == 1)
1140                    {
1141                        tags = tags[0].split(",");
1142                    }
1143
1144                    for (String tag : tags)
1145                    {
1146                        XMLUtils.createElement(contentHandler, "tag", tag);
1147                    }
1148
1149                }
1150            }
1151        }
1152
1153        String[] tags = request.getParameterValues("tags");
1154        if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals("")))
1155        {
1156            for (String tag : tags)
1157            {
1158                XMLUtils.createElement(contentHandler, "tag", tag);
1159            }
1160        }
1161    }
1162
1163    private void _saxPages(Request request) throws SAXException
1164    {
1165        String[] pages = request.getParameterValues("pages");
1166        if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals("")))
1167        {
1168            for (String id : pages)
1169            {
1170                XMLUtils.createElement(contentHandler, "page", id);
1171            }
1172        }
1173    }
1174
1175    private void _saxMultisite(Request request) throws SAXException
1176    {
1177        boolean multisite = request.getParameter("multisite") != null;
1178        if (multisite)
1179        {
1180            XMLUtils.createElement(contentHandler, "multisite");
1181
1182            String[] sites = request.getParameterValues("sites");
1183            if (sites != null && sites.length > 0 && !(sites.length == 1 && sites[0].equals("")))
1184            {
1185                for (String site : sites)
1186                {
1187                    XMLUtils.createElement(contentHandler, "site", site);
1188                }
1189            }
1190
1191        }
1192    }
1193
1194    /**
1195     * Get the content type's
1196     * 
1197     * @param request The request
1198     * @return the content type's
1199     */
1200    protected Collection<String> getContentTypes(Request request)
1201    {
1202        String[] cTypes = request.getParameterValues("content-types");
1203
1204        if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals("")))
1205        {
1206            if (cTypes.length == 1)
1207            {
1208                cTypes = cTypes[0].split(",");
1209            }
1210
1211            return Arrays.asList(cTypes);
1212        }
1213        return _getAvailableContentTypes(request);
1214    }
1215
1216    private Collection<String> _getAvailableContentTypes(Request request)
1217    {
1218        @SuppressWarnings("unchecked")
1219        Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
1220        if (parentContext == null)
1221        {
1222            parentContext = Collections.emptyMap();
1223        }
1224
1225        String[] serviceCTypes = (String[]) parentContext.get("search-by-content-types");
1226
1227        ContentTypeSearch contentTypeSearch = getContentTypeSearch(request);
1228        
1229        if (ArrayUtils.isNotEmpty(serviceCTypes) && StringUtils.isNotBlank(serviceCTypes[0]))
1230        {
1231            return Arrays.asList(serviceCTypes);
1232        }
1233        else if (!contentTypeSearch.equals(ContentTypeSearch.NONE))
1234        {
1235            return _getAllContentTypes();
1236        }
1237
1238        return Collections.emptyList();
1239    }
1240
1241    private Set<String> _getAllContentTypes()
1242    {
1243        Set<String> allCTypes = new HashSet<>(_cTypeExtPt.getExtensionsIds());
1244        allCTypes.add("resource");
1245        return allCTypes;
1246    }
1247
1248    private void _saxContentTypeCriteria(Request request) throws SAXException
1249    {
1250        Collection<String> cTypes = _getAvailableContentTypes(request);
1251
1252        XMLUtils.startElement(contentHandler, "content-types");
1253        for (String cTypeId : cTypes)
1254        {
1255            if ("resource".equals(cTypeId))
1256            {
1257                AttributesImpl attr = new AttributesImpl();
1258                attr.addCDATAAttribute("id", cTypeId);
1259                XMLUtils.startElement(contentHandler, "type", attr);
1260                new I18nizableText("plugin.web", "PLUGINS_WEB_SERVICE_FRONT_SEARCH_ON_DOCUMENTS").toSAX(contentHandler);
1261                XMLUtils.endElement(contentHandler, "type");
1262            }
1263            else if (StringUtils.isNotEmpty(cTypeId))
1264            {
1265                ContentType cType = _cTypeExtPt.getExtension(cTypeId);
1266
1267                if (cType != null)
1268                {
1269                    AttributesImpl attr = new AttributesImpl();
1270                    attr.addCDATAAttribute("id", cTypeId);
1271                    XMLUtils.startElement(contentHandler, "type", attr);
1272                    cType.getLabel().toSAX(contentHandler);
1273                    XMLUtils.endElement(contentHandler, "type");
1274                }
1275                else
1276                {
1277                    getLogger().warn("Cannot sax information about the unexising ContentType '" + cTypeId + "' for url " + request.getRequestURI());
1278                }
1279            }
1280
1281        }
1282        XMLUtils.endElement(contentHandler, "content-types");
1283    }
1284
1285    private void _saxTagsCriteria(String siteName) throws SAXException
1286    {
1287        @SuppressWarnings("unchecked")
1288        Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
1289        if (parentContext == null)
1290        {
1291            parentContext = Collections.emptyMap();
1292        }
1293
1294        Map<String, Object> contextParameters = new HashMap<>();
1295        contextParameters.put("siteName", siteName);
1296
1297        String[] tagArray = (String[]) parentContext.get("search-by-tags");
1298        if (ArrayUtils.isNotEmpty(tagArray))
1299        {
1300            XMLUtils.startElement(contentHandler, "tags");
1301            for (String tagName : tagArray)
1302            {
1303                if (StringUtils.isNotEmpty(tagName))
1304                {
1305                    boolean found = false;
1306                    Set<String> extensionsIds = _tagExtPt.getExtensionsIds();
1307
1308                    for (String id : extensionsIds)
1309                    {
1310                        if (found)
1311                        {
1312                            continue;
1313                        }
1314
1315                        I18nizableText label = null;
1316                        Map<String, Tag> tags = null;
1317
1318                        TagProvider tagProvider = _tagExtPt.getExtension(id);
1319                        if (tagName.startsWith("provider_") && id.equals(tagName.substring("provider_".length())))
1320                        {
1321                            found = true;
1322
1323                            label = tagProvider.getLabel();
1324
1325                            tags = tagProvider.getTags(contextParameters);
1326                        }
1327                        else if (tagProvider.hasTag(tagName, contextParameters))
1328                        {
1329                            found = true;
1330
1331                            Tag tag = tagProvider.getTag(tagName, contextParameters);
1332                            label = tag.getTitle();
1333                            tags = tag.getTags();
1334                        }
1335
1336                        if (found)
1337                        {
1338                            AttributesImpl attr = new AttributesImpl();
1339                            attr.addCDATAAttribute("id", tagName);
1340                            XMLUtils.startElement(contentHandler, "tag", attr);
1341
1342                            if (label != null)
1343                            {
1344                                label.toSAX(contentHandler, "title");
1345                            }
1346
1347                            if (tags != null)
1348                            {
1349                                for (Tag child : tags.values())
1350                                {
1351                                    if (child.getTarget().getName().equals("CONTENT"))
1352                                    {
1353                                        _saxTag(child, true);
1354                                    }
1355                                }
1356                            }
1357
1358                            XMLUtils.endElement(contentHandler, "tag");
1359                        }
1360                    }
1361                }
1362            }
1363            XMLUtils.endElement(contentHandler, "tags");
1364        }
1365    }
1366
1367    private void _saxTag(Tag tag, boolean recursive) throws SAXException
1368    {
1369        AttributesImpl attr = new AttributesImpl();
1370        attr.addCDATAAttribute("id", tag.getName());
1371        XMLUtils.startElement(contentHandler, "tag", attr);
1372        tag.getTitle().toSAX(contentHandler, "title");
1373
1374        if (recursive)
1375        {
1376            for (Tag child : tag.getTags().values())
1377            {
1378                _saxTag(child, true);
1379            }
1380        }
1381
1382        XMLUtils.endElement(contentHandler, "tag");
1383    }
1384
1385    private void _saxSitemapCriteria() throws SAXException
1386    {
1387        @SuppressWarnings("unchecked")
1388        Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
1389        if (parentContext == null)
1390        {
1391            parentContext = Collections.emptyMap();
1392        }
1393
1394        String[] pages = (String[]) parentContext.get("search-by-pages");
1395        if (ArrayUtils.isNotEmpty(pages))
1396        {
1397            XMLUtils.startElement(contentHandler, "pages");
1398
1399            for (String pageID : pages)
1400            {
1401                if (StringUtils.isNotEmpty(pageID))
1402                {
1403                    Page page = _resolver.resolveById(pageID);
1404                    AttributesImpl attr = new AttributesImpl();
1405                    attr.addCDATAAttribute("id", pageID);
1406                    attr.addCDATAAttribute("path", page.getSitemap().getName() + "/" + page.getPathInSitemap());
1407                    attr.addCDATAAttribute("title", page.getTitle());
1408                    attr.addCDATAAttribute("long-title", page.getLongTitle());
1409                    XMLUtils.createElement(contentHandler, "page", attr);
1410                }
1411            }
1412
1413            XMLUtils.endElement(contentHandler, "pages");
1414        }
1415    }
1416
1417    /**
1418     * Get the content type's filter value
1419     * 
1420     * @param request The request
1421     * @return the content type's filter value
1422     */
1423    protected String getContentTypeFilterValue(Request request)
1424    {
1425        Enumeration<String> paramNames = request.getParameterNames();
1426        while (paramNames.hasMoreElements())
1427        {
1428            String paramName = paramNames.nextElement();
1429            if (paramName.startsWith("ctype-filter-"))
1430            {
1431                return paramName.substring("ctype-filter-".length());
1432            }
1433        }
1434
1435        if (request.getParameter("current-ctype-filter") != null)
1436        {
1437            return request.getParameter("current-ctype-filter");
1438        }
1439
1440        return null;
1441    }
1442
1443    /**
1444     * Add the text field query
1445     * @param queries the queries
1446     * @param language the language
1447     * @param request the request
1448     */
1449    protected void addTextFieldQuery(Collection<Query> queries, String language, Request request)
1450    {
1451        String text = request.getParameter("textfield");
1452
1453        if (StringUtils.isNotBlank(text))
1454        {
1455            String trimText = text.trim();
1456            String escapedText = _escapeQueryCharsButNotQuotes(trimText);
1457
1458            queries.add(new FullTextQuery(escapedText, language, Operator.SEARCH, true));
1459        }
1460    }
1461    
1462    /**
1463     * Get the queries for content's attachments and resources
1464     * @param subQuery the query for attachments and resources
1465     * @return the join query for content's attachments and resources
1466     */
1467    protected List<Query> getContentResourcesOrAttachmentQueries(Query subQuery)
1468    {
1469        if (subQuery instanceof MatchAllQuery)
1470        {
1471            return Collections.emptyList();
1472        }
1473        
1474        List<Query> queries = new ArrayList<>();
1475        // join query on outgoing resources
1476        queries.add(new JoinQuery(subQuery, CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS));
1477        // join query on content's attachments
1478        queries.add(() -> "{!join from=" + RESOURCE_ANCESTOR_IDS + " to=" + CONTENT_OUTGOING_REFEERENCES_RESOURCE_IDS + " v=\"" + _buildSubQuery(subQuery) + "\"}");
1479        return queries;
1480    }
1481    
1482    /**
1483     * Get the queries for page's attachments and resources
1484     * @param subQuery the query for attachments and resources
1485     * @return the join query for content's attachments and resources
1486     */
1487    protected List<Query> getPageResourcesOrAttachmentQueries(Query subQuery)
1488    {
1489        List<Query> queries = new ArrayList<>();
1490        // join query on outgoing resources
1491        queries.add(new JoinQuery(subQuery, PAGE_OUTGOING_REFEERENCES_RESOURCE_IDS));
1492        // join query on page's attachments or explorer folder service
1493        queries.add(() -> "{!join from=" + RESOURCE_ANCESTOR_IDS + " to=" + PAGE_OUTGOING_REFEERENCES_RESOURCE_IDS + " v=\"" + _buildSubQuery(subQuery) + "\"}");
1494        return queries;
1495    }
1496    
1497    private String _buildSubQuery(Query subQuery) throws QuerySyntaxException
1498    {
1499        return ClientUtils.escapeQueryChars(subQuery.build());
1500    }
1501    
1502    /*
1503     * Do not escape double quotes for exact searching
1504     */
1505    private String _escapeQueryCharsButNotQuotes(String text)
1506    {
1507        StringBuilder sb = new StringBuilder();
1508        String[] parts = StringUtils.splitPreserveAllTokens(text, '"');
1509        for (int i = 0; i < parts.length; i++)
1510        {
1511            if (i != 0)
1512            {
1513                sb.append("\"");
1514            }
1515            String part = parts[i];
1516            sb.append(ClientUtils.escapeQueryChars(part));
1517        }
1518
1519        return sb.toString();
1520    }
1521
1522    /**
1523     * Add query for each metadata
1524     * 
1525     * @param queries The list of query
1526     * @param language The lang
1527     * @param request The request
1528     * @throws IllegalArgumentException If an error occurred
1529     */
1530    protected void addMetadataQuery(Collection<Query> queries, String language, Request request) throws IllegalArgumentException
1531    {
1532        ZoneItem zoneItem = getZoneItem(request);
1533        Set<String> facetFields = getFacets(request).keySet();
1534        if (zoneItem != null && zoneItem.getServiceParameters().hasMetadata("search-by-metadata"))
1535        {
1536            String[] metadataPaths = zoneItem.getServiceParameters().getStringArray("search-by-metadata");
1537            for (String metadataPath : metadataPaths)
1538            {
1539                if (facetFields.contains(metadataPath))
1540                {
1541                    // will be handled in org.ametys.web.frontoffice.SearchGenerator.getFacetValues(Request, Collection<String>, String)
1542                    continue;
1543                }
1544                String value = request.getParameter("metadata-" + metadataPath.replaceAll("/", "."));
1545
1546                if (StringUtils.isNotBlank(value))
1547                {
1548                    Collection<String> contentTypes = getContentTypes(request);
1549
1550                    MetadataDefinition metadataDefinition = contentTypes.isEmpty() ? null
1551                            : _contentTypesHelper.getMetadataDefinition(metadataPath, (String[]) contentTypes.toArray(), null);
1552                    
1553                    Query query;
1554                    if (metadataDefinition != null && (metadataDefinition.getEnumerator() != null || metadataDefinition.getType().equals(MetadataType.CONTENT)))
1555                    {
1556                        // Exact search
1557                        query = new StringQuery(metadataPath, Operator.EQ, value, language);
1558                    }
1559                    else if (value.startsWith("\"") && value.endsWith("\""))
1560                    {
1561                        String escapedText = ClientUtils.escapeQueryChars(value.substring(1, value.length() - 1)); // the StringQuery with EQ operator will add quotes characters anyway, so remove them
1562                        query = new StringQuery(metadataPath, Operator.EQ, escapedText, language, true);
1563                    }
1564                    else
1565                    {
1566                        String escapedText = _escapedTextForSearch(value);
1567                        Operator op = escapedText.startsWith("*") || escapedText.endsWith("*") ? Operator.LIKE : Operator.SEARCH;
1568                        boolean doNotUseLanguage = op == Operator.LIKE && metadataDefinition != null && metadataDefinition.getType() != MetadataType.MULTILINGUAL_STRING;
1569                        query = new StringQuery(metadataPath, op, escapedText, doNotUseLanguage ? null : language, true);
1570                    }
1571                    
1572                    queries.add(query);
1573                }
1574            }
1575        }
1576    }
1577    
1578    private String _escapedTextForSearch(String value)
1579    {
1580        // Allow wildcards at the beginning and at the end
1581        // Ex.:
1582        // zert will stay zert to not match azerty
1583        // *zert will stay *zert to not match azerty and match azert
1584        // zert* will stay zert* to not match azerty and match zerty
1585        // *zert* will stay *zert* to match azerty
1586        // where zert is escaped
1587        
1588        String valueToEscape = value;
1589        String begin = "";
1590        String end = "";
1591        boolean startsWithStar = value.startsWith("*");
1592        boolean endsWithStar = value.endsWith("*");
1593        if (startsWithStar)
1594        {
1595            valueToEscape = valueToEscape.substring(1);
1596            begin = "*";
1597        }
1598        if (endsWithStar)
1599        {
1600            valueToEscape = valueToEscape.substring(0, valueToEscape.length() - 1);
1601            end = "*";
1602        }
1603        
1604        // escape special characters
1605        String escapedValue = ClientUtils.escapeQueryChars(valueToEscape).toLowerCase();
1606        String escapedText = begin + escapedValue + end;
1607        return escapedText;
1608    }
1609
1610    /**
1611     * Add content query for "all words"
1612     * @param queries The queries
1613     * @param language The current language
1614     * @param request the request
1615     */
1616    protected void addAllWordsTextFieldQuery(Collection<Query> queries, String language, Request request) 
1617    {
1618        // TODO All words? OrQuery of AndQuery instead?
1619        String words = request.getParameter("all-words");
1620
1621        if (StringUtils.isNotBlank(words))
1622        {
1623            StringBuilder allWords = new StringBuilder();
1624            for (String word : StringUtils.split(words))
1625            {
1626                String escapedWord = ClientUtils.escapeQueryChars(word);
1627                if (allWords.length() > 0)
1628                {
1629                    allWords.append(' ');
1630                }
1631                allWords.append('+').append(escapedWord);
1632            }
1633
1634            // queries.add(new FullTextQuery(allWords.toString(), language,
1635            // Operator.SEARCH));
1636
1637            queries.add(new FullTextQuery(allWords.toString(), language, Operator.SEARCH, true));
1638        }
1639    }
1640
1641    /**
1642     * Add content query for exact wording or phrase
1643     * @param queries The queries
1644     * @param language The current language
1645     * @param request the request
1646     */
1647    protected void addExactWordingTextFieldQuery(Collection<Query> queries, String language, Request request)
1648    {
1649        String exact = request.getParameter("exact-wording");
1650
1651        if (StringUtils.isNotBlank(exact))
1652        {
1653            queries.add(new FullTextQuery(exact, language, Operator.EQ));
1654        }
1655    }
1656
1657    /**
1658     * Add content query for "none of these words"
1659     * @param queries The queries
1660     * @param language The current language
1661     * @param request the request
1662     */
1663    protected void addNoWordsTextFieldQuery(Collection<Query> queries, String language, Request request)
1664    {
1665        String noWords = request.getParameter("no-words");
1666
1667        if (StringUtils.isNotBlank(noWords))
1668        {
1669            StringBuilder allWords = new StringBuilder();
1670            for (String word : StringUtils.split(noWords))
1671            {
1672                String escapedWord = ClientUtils.escapeQueryChars(word);
1673                if (allWords.length() > 0)
1674                {
1675                    allWords.append(' ');
1676                }
1677                allWords.append('+').append(escapedWord);
1678            }
1679
1680            // queries.add(new NotQuery(new FullTextQuery(allWords.toString(),
1681            // language, Operator.SEARCH)));
1682
1683            queries.add(new NotQuery(new FullTextQuery(allWords.toString(), language, Operator.SEARCH, true)));
1684        }
1685    }
1686
1687    /** 
1688     * Add the content type query
1689     * @param queries The queries
1690     * @param request The request
1691     */
1692    protected void addContentTypeQuery(Collection<Query> queries, Request request)
1693    {
1694        Collection<String> cTypes = new ArrayList<>(getContentTypes(request));
1695
1696        // Resource is handled in the "document types" filter.
1697        cTypes.remove("resource");
1698
1699        if (!cTypes.isEmpty())
1700        {
1701            Query notAPageQuery = new DocumentTypeQuery(TYPE_PAGE, false);
1702            Query pageWithContentOfTypesQuery = new PageContentQuery(new ContentTypeQuery(cTypes));
1703            // If document is not a page, it will match this fq (it will be filtered if necessary in the "document types" filter, see #getDocumentTypesQuery())
1704            // If document is a page, then one of its contents must match one of the given content types
1705            queries.add(new OrQuery(notAPageQuery, pageWithContentOfTypesQuery));
1706        }
1707    }
1708
1709    /** 
1710     * Add the tags query
1711     * @param queries The queries
1712     * @param request The request
1713     */
1714    protected void addTagsQuery(Collection<Query> queries, Request request)
1715    {
1716        String size = request.getParameter("tags-size");
1717        if (!StringUtils.isEmpty(size))
1718        {
1719            @SuppressWarnings("unchecked")
1720            Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
1721            if (parentContext == null)
1722            {
1723                parentContext = Collections.emptyMap();
1724            }
1725
1726            boolean isStrictSearch = parameters.getParameterAsBoolean("strict-search-on-tags", true);
1727
1728            int nbCat = Integer.parseInt(size);
1729            for (int i = 1; i < nbCat + 1; i++)
1730            {
1731                String[] tags = request.getParameterValues("tags-" + i);
1732                if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals("")))
1733                {
1734                    List<Query> tagQueries = new ArrayList<>();
1735
1736                    for (String tag : tags)
1737                    {
1738                        String[] values = StringUtils.split(tag, ',');
1739
1740                        tagQueries.add(new TagQuery(Operator.EQ, !isStrictSearch, values));
1741                    }
1742
1743                    queries.add(new PageContentQuery(new OrQuery(tagQueries)));
1744                }
1745            }
1746        }
1747    }
1748
1749    /** 
1750     * Add the pages query
1751     * @param queries The queries
1752     * @param request The request
1753     */
1754    protected void addPagesQuery(Collection<Query> queries, Request request)
1755    {
1756        List<Query> pageQueries = new ArrayList<>();
1757
1758        String[] pages = request.getParameterValues("pages");
1759        if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals("")))
1760        {
1761            for (String pageIds : pages)
1762            {
1763                for (String pageId : StringUtils.split(pageIds, ","))
1764                {
1765                    pageQueries.add(new PageQuery(pageId, true));
1766                }
1767            }
1768        }
1769
1770        queries.add(new OrQuery(pageQueries));
1771    }
1772
1773    /** 
1774     * Add the query on start and end dates
1775     * @param queries The queries
1776     * @param request The request
1777     */
1778    protected void addDateQuery(Collection<Query> queries, Request request)
1779    {
1780        String startDateId = parameters.getParameter("startDate", "");
1781        String endDateId = parameters.getParameter("endDate", "");
1782
1783        String startDateStr = request.getParameter("startDate");
1784        String endDateStr = request.getParameter("endDate");
1785
1786        // We check if startDateStr < endDateStr. If not, we swap.
1787        if (StringUtils.isNotBlank(endDateStr) && StringUtils.isNotBlank(startDateStr) && startDateStr.compareTo(endDateStr) > 0)
1788        {
1789            String dateAux = startDateStr;
1790            startDateStr = endDateStr;
1791            endDateStr = dateAux;
1792        }
1793
1794        String startPropId = startDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : startDateId;
1795        String endPropId = endDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : endDateId;
1796
1797        if (StringUtils.isNotBlank(endDateStr))
1798        {
1799            Date endDate = _toDate(endDateStr);
1800            // Join for returning page documents
1801            queries.add(new PageContentQuery(new DateQuery(startPropId, Operator.LE, endDate)));
1802        }
1803
1804        if (StringUtils.isNotBlank(startDateStr))
1805        {
1806            Date startDate = _toDate(startDateStr);
1807
1808            if (startDate != null)
1809            {
1810                // Two cases are possible :
1811                // An event could have an end date (query 1)
1812                // If not, the start date is also the end date (query 2)
1813                List<Query> endDateQueries = new ArrayList<>();
1814
1815                if (StringUtils.isNotBlank(endPropId))
1816                {
1817                    endDateQueries.add(new DateQuery(endPropId, Operator.GE, startDate));
1818                }
1819                endDateQueries.add(new DateQuery(startPropId, Operator.GE, startDate));
1820
1821                // Join for returning page documents
1822                queries.add(new PageContentQuery(new OrQuery(endDateQueries)));
1823            }
1824        }
1825    }
1826
1827    private Date _toDate(String dateStr)
1828    {
1829        try
1830        {
1831            LocalDate dt = LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE);
1832            return DateUtils.asDate(dt);
1833        }
1834        catch (DateTimeParseException e)
1835        {
1836            getLogger().error("Invalid date format " + dateStr, e);
1837        }
1838        return null;
1839    }
1840    
1841    private List<QueryAdapterFOSearch> _getSortedListQueryAdapter()
1842    {
1843        List<QueryAdapterFOSearch> queryAdapters = new ArrayList<>();
1844        for (String queryAdapterFOSearchId : _queryAdapterFOSearchEP.getExtensionsIds())
1845        {
1846            queryAdapters.add(_queryAdapterFOSearchEP.getExtension(queryAdapterFOSearchId));
1847        }
1848        
1849        // Order queryAdapters (0 is first, Integer.MAX_INT is last)
1850        Collections.sort(queryAdapters, new Comparator<QueryAdapterFOSearch>()
1851        {
1852            @Override
1853            public int compare(QueryAdapterFOSearch q1, QueryAdapterFOSearch q2)
1854            {
1855                return new Integer(q1.getPriority()).compareTo(new Integer(q2.getPriority()));
1856            }
1857        });
1858        
1859        return queryAdapters;
1860    }
1861
1862}