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