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