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