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.Date;
028import java.util.Enumeration;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034
035import org.apache.cocoon.environment.ObjectModelHelper;
036import org.apache.cocoon.environment.Request;
037import org.apache.cocoon.xml.AttributesImpl;
038import org.apache.cocoon.xml.XMLUtils;
039import org.apache.commons.lang3.ArrayUtils;
040import org.apache.commons.lang3.StringUtils;
041import org.apache.solr.client.solrj.util.ClientUtils;
042import org.xml.sax.SAXException;
043
044import org.ametys.cms.content.indexing.solr.SolrFieldNames;
045import org.ametys.cms.contenttype.ContentType;
046import org.ametys.cms.contenttype.MetadataDefinition;
047import org.ametys.cms.repository.RequestAttributeWorkspaceSelector;
048import org.ametys.cms.search.SearchResult;
049import org.ametys.cms.search.SearchResults;
050import org.ametys.cms.search.SearchResultsIterable;
051import org.ametys.cms.search.SearchResultsIterator;
052import org.ametys.cms.search.Sort;
053import org.ametys.cms.search.Sort.Order;
054import org.ametys.cms.search.query.AndQuery;
055import org.ametys.cms.search.query.ContentTypeQuery;
056import org.ametys.cms.search.query.DateQuery;
057import org.ametys.cms.search.query.DocumentTypeQuery;
058import org.ametys.cms.search.query.FullTextQuery;
059import org.ametys.cms.search.query.NotQuery;
060import org.ametys.cms.search.query.OrQuery;
061import org.ametys.cms.search.query.Query;
062import org.ametys.cms.search.query.Query.Operator;
063import org.ametys.cms.search.query.QuerySyntaxException;
064import org.ametys.cms.search.query.StringQuery;
065import org.ametys.cms.search.query.TagQuery;
066import org.ametys.cms.search.solr.SearcherFactory.Searcher;
067import org.ametys.cms.tag.Tag;
068import org.ametys.cms.tag.TagProvider;
069import org.ametys.core.util.DateUtils;
070import org.ametys.plugins.explorer.resources.Resource;
071import org.ametys.plugins.repository.AmetysObject;
072import org.ametys.runtime.i18n.I18nizableText;
073import org.ametys.web.repository.page.Page;
074import org.ametys.web.repository.page.ZoneItem;
075import org.ametys.web.repository.site.Site;
076import org.ametys.web.search.query.PageContentQuery;
077import org.ametys.web.search.query.PageQuery;
078import org.ametys.web.search.query.SiteQuery;
079import org.ametys.web.search.query.SitemapQuery;
080
081/**
082 * Generates the results of a search performed on front office
083 */
084public class SearchGenerator extends AbstractSearchGenerator
085{
086    /** Constants for content type's choice : filter */
087    protected static final String CONTENT_TYPE_CHOICE_FILTER = "filter";
088
089    /** Constants for content type's choice : list */
090    protected static final String CONTENT_TYPE_CHOICE_LIST = "list";
091
092    /** Constants for content type's choice : checkbox then filter */
093    protected static final String CONTENT_TYPE_CHOICE_CHECKBOX = "checkbox";
094
095    /** Constants for content type's choice : checkbox then filter */
096    protected static final String CONTENT_TYPE_CHOICE_CHECKBOX_AND_FILTER = "checkbox-filter";
097
098    /** Constants for content type's choice : none */
099    protected static final String CONTENT_TYPE_CHOICE_NONE = "none";
100
101    @Override
102    protected SearchResults<AmetysObject> search(Request request, Collection<String> siteNames, String language, int pageIndex, int start, int maxResults) throws Exception
103    {
104        String searchCTypeType = parameters.getParameter("search-by-content-types-choice", CONTENT_TYPE_CHOICE_NONE);
105
106        try
107        {
108            // TODO Use facets instead of one query by content type!
109            if (CONTENT_TYPE_CHOICE_FILTER.equals(searchCTypeType) || CONTENT_TYPE_CHOICE_CHECKBOX_AND_FILTER.equals(searchCTypeType))
110            {
111                // Retrieve current workspace
112                String currentWsp = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
113                SearchResults<AmetysObject> currentResults = null;
114
115                try
116                {
117                    XMLUtils.startElement(contentHandler, "content-types");
118                    Collection<String> cTypes = getContentTypes(request);
119
120                    List<Sort> sorts = new ArrayList<>();
121                    Sort sort = getSortField(request);
122                    sorts.addAll(getPrimarySortFields(request));
123                    sorts.add(sort);
124
125                    // Get first sort field
126                    XMLUtils.createElement(contentHandler, sort == null || sort.getField() == null ? "sort-by-score" : "sort-by-" + sort.getField());
127
128                    // TODO Use facets instead of making one search by content
129                    // type.
130                    String currentCType = getContentTypeFilterValue(request);
131                    int count = 0;
132                    for (String cType : cTypes)
133                    {
134                        Query queryObject = getQuery(request, siteNames, language);
135
136                        Collection<Query> filterQueries = getFixedCTypeFilterQueries(request, cType, siteNames, language);
137
138                        Collection<String> documentTypes = getDocumentTypes(request);
139                        Query documentTypesQuery = getDocumentTypesQuery(documentTypes);
140
141                        Searcher searcher = _searcherFactory.create().withQuery(queryObject).withFilterQueries(filterQueries).addFilterQuery(documentTypesQuery).withLimits(0,
142                                Integer.MAX_VALUE).withSort(sorts).setCheckRights(true);
143
144                        _additionalSearchProcessing(searcher);
145
146                        SearchResults<AmetysObject> results = searcher.searchWithFacets();
147
148                        _searcherFactory.create().withQuery(queryObject).withFilterQueries(filterQueries).addFilterQuery(documentTypesQuery).withLimits(0,
149                                Integer.MAX_VALUE).withSort(getPrimarySortFields(request)).addSort(sort).setCheckRights(true);
150
151                        if (results.getTotalCount() > 0)
152                        {
153                            boolean current = cType.equals(currentCType) || currentCType == null && count == 0;
154                            count++;
155                            AttributesImpl attr = new AttributesImpl();
156                            if (current)
157                            {
158                                attr.addCDATAAttribute("current", "true");
159                            }
160
161                            XMLUtils.startElement(contentHandler, cType, attr);
162
163                            if (cType.equals("resource"))
164                            {
165                                new I18nizableText("plugin.web", "PLUGINS_WEB_SERVICE_FRONT_SEARCH_ON_DOCUMENTS").toSAX(contentHandler, "label");
166                            }
167                            else
168                            {
169                                ContentType contentType = _cTypeExtPt.getExtension(cType);
170                                if (contentType != null)
171                                {
172                                    contentType.getLabel().toSAX(contentHandler, "label");
173                                }
174                            }
175
176                            // SAX results
177                            AttributesImpl atts = new AttributesImpl();
178                            atts.addCDATAAttribute("total", String.valueOf(results.getTotalCount()));
179                            atts.addCDATAAttribute("maxScore", String.valueOf(results.getMaxScore()));
180
181                            if (current)
182                            {
183                                XMLUtils.startElement(contentHandler, "hits", atts);
184                                saxHits(results, start, maxResults);
185                                XMLUtils.endElement(contentHandler, "hits");
186
187                                // SAX pagination
188                                saxPagination(results.getTotalCount(), start, maxResults);
189
190                                currentResults = results;
191                            }
192                            else
193                            {
194                                XMLUtils.createElement(contentHandler, "hits", atts);
195                            }
196
197                            XMLUtils.endElement(contentHandler, cType);
198                        }
199                    }
200                    XMLUtils.endElement(contentHandler, "content-types");
201
202                    return currentResults;
203                }
204                finally
205                {
206                    // Restore context
207                    RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWsp);
208                }
209            }
210            else
211            {
212                return super.search(request, siteNames, language, pageIndex, start, maxResults);
213            }
214        }
215        catch (QuerySyntaxException e)
216        {
217            throw new IOException("Query syntax error while searching.", e);
218        }
219    }
220
221    @Override
222    protected Query getQuery(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException
223    {
224        List<Query> queries = new ArrayList<>();
225
226        addMetadataQuery(queries, language, request);
227        _addTextFieldQuery(queries, language, request);
228        _addAllWordsTextFieldQuery(queries, language, request);
229        _addExactWordingTextFieldQuery(queries, language, request);
230        _addNoWordsTextFieldQuery(queries, language, request);
231
232        return new AndQuery(queries);
233    }
234
235    @Override
236    protected Collection<Query> getFilterQueries(Request request, Collection<String> siteNames, String language) throws IllegalArgumentException
237    {
238        List<Query> queries = new ArrayList<>();
239
240        queries.add(new SiteQuery(siteNames));
241        queries.add(new SitemapQuery(language));
242
243        _addContentTypeQuery(queries, request);
244        _addTagsQuery(queries, request);
245        _addPagesQuery(queries, request);
246        _addDateQuery(queries, request);
247
248        return queries;
249    }
250
251    /**
252     * Get the filter queries for a single fixed content type.
253     * 
254     * @param request The request.
255     * @param cType The fixed content type.
256     * @param siteNames the site names.
257     * @param language The language.
258     * @return The filter queries.
259     */
260    protected Collection<Query> getFixedCTypeFilterQueries(Request request, String cType, Collection<String> siteNames, String language)
261    {
262        List<Query> queries = new ArrayList<>();
263
264        queries.add(new SiteQuery(siteNames));
265        queries.add(new SitemapQuery(language));
266
267        if ("resource".equals(cType))
268        {
269            queries.add(new DocumentTypeQuery(TYPE_PAGE_RESOURCE));
270        }
271        else
272        {
273            // Fixed content type query.
274            queries.add(new PageContentQuery(new ContentTypeQuery(cType)));
275        }
276        _addTagsQuery(queries, request);
277        _addPagesQuery(queries, request);
278        _addDateQuery(queries, request);
279
280        return queries;
281    }
282
283    @Override
284    protected Collection<String> getDocumentTypes(Request request)
285    {
286        Collection<String> cTypes = getContentTypes(request);
287
288        if (cTypes.size() == 1 && "resource".equals(cTypes.iterator().next()))
289        {
290            return Collections.singletonList(TYPE_PAGE_RESOURCE);
291        }
292        // An empty collections means "all".
293        else if (cTypes.isEmpty() || cTypes.contains("resource"))
294        {
295            return Arrays.asList(TYPE_PAGE, TYPE_PAGE_RESOURCE);
296        }
297        else
298        {
299            return Collections.singletonList(TYPE_PAGE);
300        }
301    }
302
303    @Override
304    protected Collection<String> getFields()
305    {
306        return Collections.emptyList();
307    }
308
309    @Override
310    protected Map<String, Criterion> getCriteria(Request request, String siteName, String lang)
311    {
312        return Collections.emptyMap();
313    }
314
315    @Override
316    protected void saxHits(SearchResults<AmetysObject> results, int start, int maxResults) throws SAXException, IOException
317    {
318        SearchResultsIterable<SearchResult<AmetysObject>> resultsIt = results.getResults();
319        long limit = Math.min(start + maxResults, resultsIt.getSize());
320        float maxScore = results.getMaxScore();
321        
322        SearchResultsIterator<SearchResult<AmetysObject>> it = resultsIt.iterator();
323        it.skip(start);
324        for (int i = start; i < limit; i++)
325        {
326            if (it.hasNext()) // this should return true except if there is a inconsistency between repository and Solr index
327            {
328                SearchResult<AmetysObject> searchResult = it.next();
329                float score = searchResult.getScore();
330                AmetysObject ametysObject = searchResult.getObject();
331                if (ametysObject instanceof Page)
332                {
333                    saxPageHit(score, maxScore, (Page) ametysObject);
334                }
335                else if (ametysObject instanceof Resource)
336                {
337                    saxResourceHit(score, maxScore, (Resource) ametysObject);
338                }
339            }
340            
341        }
342    }
343
344    @Override
345    protected Sort getSortField(Request request)
346    {
347        if (request.getParameter("sort-by-title-for-sorting") != null)
348        {
349            return new Sort(TITLE_SORT, Order.ASC);
350        }
351        else if (request.getParameter("sort-by-last-validation") != null)
352        {
353            return new Sort(LAST_VALIDATION, Order.DESC);
354        }
355        else
356        {
357            // Generic sort field (with hardcorded descending order)
358            Enumeration paramNames = request.getParameterNames();
359            while (paramNames.hasMoreElements())
360            {
361                String param = (String) paramNames.nextElement();
362                if (param.startsWith("sort-by-"))
363                {
364                    String fieldName = StringUtils.removeStart(param, "sort-by-");
365                    return new Sort(fieldName, Order.DESC);
366                }
367            }
368        }
369
370        return new Sort("score", Order.DESC);
371    }
372
373    @Override
374    protected List<Sort> getPrimarySortFields(Request request)
375    {
376        return new ArrayList<>();
377    }
378
379    @Override
380    protected void saxFormFields(Request request, SearchResults<AmetysObject> searchResults, String siteName, String lang) throws SAXException
381    {
382        XMLUtils.createElement(contentHandler, "textfield");
383
384        boolean advancedSearch = parameters.getParameterAsBoolean("advanced-search", true);
385        if (advancedSearch)
386        {
387            XMLUtils.createElement(contentHandler, "all-words");
388            XMLUtils.createElement(contentHandler, "exact-wording");
389            XMLUtils.createElement(contentHandler, "no-words");
390        }
391
392        String searchCTypeType = parameters.getParameter("search-by-content-types-choice", CONTENT_TYPE_CHOICE_NONE);
393        XMLUtils.createElement(contentHandler, "content-types-choice", searchCTypeType);
394
395        _saxContentTypeCriteria();
396        _saxMetadataCriteria(request);
397        _saxTagsCriteria(siteName);
398        _saxSitemapCriteria();
399
400        boolean multisite = parameters.getParameterAsBoolean("search-multisite", false);
401        if (multisite)
402        {
403            XMLUtils.createElement(contentHandler, "multisite");
404
405            XMLUtils.startElement(contentHandler, "sites");
406            Collection<String> allSites = _siteManager.getSiteNames();
407            for (String name : allSites)
408            {
409                Site site = _siteManager.getSite(name);
410                AttributesImpl attr = new AttributesImpl();
411                attr.addCDATAAttribute("name", name);
412                if (name.equals(siteName))
413                {
414                    attr.addCDATAAttribute("current", "true");
415                }
416                XMLUtils.createElement(contentHandler, "site", attr, StringUtils.defaultString(site.getTitle()));
417            }
418            XMLUtils.endElement(contentHandler, "sites");
419        }
420
421        if (StringUtils.isNotBlank(parameters.getParameter("startDate", "")))
422        {
423            XMLUtils.createElement(contentHandler, "dates", "true");
424        }
425    }
426
427    private void _saxMetadataCriteria(Request request) throws SAXException
428    {
429        ZoneItem zoneItem = (ZoneItem) request.getAttribute(org.ametys.web.repository.page.ZoneItem.class.getName());
430        if (zoneItem.getServiceParameters().hasMetadata("search-by-metadata"))
431        {
432            String[] metadataPaths = zoneItem.getServiceParameters().getStringArray("search-by-metadata");
433            if (metadataPaths.length > 0)
434            {
435                Collection<String> cTypes = new ArrayList<>(getContentTypes(request));
436
437                for (String metadataPath : metadataPaths)
438                {
439                    MetadataDefinition metadataDef = _contentTypesHelper.getMetadataDefinitionByMetadataValuePath(metadataPath, cTypes.toArray(new String[cTypes.size()]),
440                            new String[0]);
441                    AttributesImpl attrs = new AttributesImpl();
442                    attrs.addCDATAAttribute("name", metadataPath);
443                    XMLUtils.startElement(contentHandler, "metadata", attrs);
444
445                    if (metadataDef != null)
446                    {
447                        metadataDef.getLabel().toSAX(contentHandler);
448                    }
449                    else
450                    {
451                        XMLUtils.data(contentHandler, metadataPath);
452                    }
453                    XMLUtils.endElement(contentHandler, "metadata");
454
455                    saxMetadataDef(metadataDef, metadataPath);
456                }
457            }
458        }
459    }
460
461    /**
462     * Sax metadata set information for metadata
463     * 
464     * @param metadataDef The metadata definition.
465     * @param metadataPath The metadata path
466     * @throws SAXException If an error occurred while saxing
467     */
468    protected void saxMetadataDef(MetadataDefinition metadataDef, String metadataPath) throws SAXException
469    {
470        AttributesImpl attrs = new AttributesImpl();
471        attrs.addCDATAAttribute("name", metadataPath);
472
473        XMLUtils.startElement(contentHandler, "metadataDef", attrs);
474        if (metadataDef != null)
475        {
476            metadataDef.getLabel().toSAX(contentHandler, "label");
477        }
478        else
479        {
480            XMLUtils.startElement(contentHandler, "label");
481            XMLUtils.data(contentHandler, metadataPath);
482            XMLUtils.endElement(contentHandler, "label");
483        }
484
485        saxEnumeratorValueForMetadata(metadataDef, metadataPath);
486        XMLUtils.endElement(contentHandler, "metadataDef");
487    }
488
489    /**
490     * Sax enumeration value for enum metadata
491     * 
492     * @param metadataDef The metadata definition.
493     * @param metadataPath The metadata path
494     * @throws SAXException If an error occurred while saxing
495     */
496    protected void saxEnumeratorValueForMetadata(MetadataDefinition metadataDef, String metadataPath) throws SAXException
497    {
498        if (metadataDef != null && metadataDef.getEnumerator() != null)
499        {
500            XMLUtils.startElement(contentHandler, "enumeration");
501            try
502            {
503                Map<Object, I18nizableText> entries = metadataDef.getEnumerator().getEntries();
504                for (Object key : entries.keySet())
505                {
506                    AttributesImpl attrItem = new AttributesImpl();
507                    attrItem.addCDATAAttribute("value", (String) key);
508                    XMLUtils.startElement(contentHandler, "item", attrItem);
509                    entries.get(key).toSAX(contentHandler, "label");
510                    XMLUtils.endElement(contentHandler, "item");
511                }
512            }
513            catch (Exception e)
514            {
515                getLogger().error("An error occurred getting enumerator items for metadata : " + metadataPath, e);
516            }
517            XMLUtils.endElement(contentHandler, "enumeration");
518        }
519    }
520
521    @Override
522    protected void saxFormValues(Request request, SearchResults<AmetysObject> searchResults, int start, int offset) throws SAXException
523    {
524        _saxTextField(request);
525        _saxMetadataValues(request);
526        _saxAllWords(request);
527        _saxExactWording(request);
528        _saxNoWords(request);
529        _saxContentType(request);
530        _saxTags(request);
531        _saxPages(request);
532        _saxMultisite(request);
533        _saxDates(request);
534    }
535
536    private void _saxDates(Request request) throws SAXException
537    {
538        String startDate = request.getParameter("startDate");
539        String endDate = request.getParameter("endDate");
540
541        if (StringUtils.isNotBlank(startDate) && StringUtils.isNotBlank(endDate) && startDate.compareTo(endDate) > 0)
542        {
543            String tmp = startDate;
544            startDate = endDate;
545            endDate = tmp;
546        }
547
548        if (StringUtils.isNotBlank(startDate))
549        {
550            XMLUtils.createElement(contentHandler, "startDate", startDate);
551        }
552        if (StringUtils.isNotBlank(endDate))
553        {
554            XMLUtils.createElement(contentHandler, "endDate", endDate);
555        }
556    }
557
558    private void _saxTextField(Request request) throws SAXException
559    {
560        String textfield = request.getParameter("textfield");
561        XMLUtils.createElement(contentHandler, "textfield", textfield != null ? textfield : "");
562    }
563
564    private void _saxMetadataValues(Request request) throws SAXException
565    {
566        ZoneItem zoneItem = (ZoneItem) request.getAttribute(org.ametys.web.repository.page.ZoneItem.class.getName());
567        if (zoneItem.getServiceParameters().hasMetadata("search-by-metadata"))
568        {
569            String[] metadataPaths = zoneItem.getServiceParameters().getStringArray("search-by-metadata");
570            if (metadataPaths.length > 0)
571            {
572                XMLUtils.startElement(contentHandler, "metadata");
573                for (String metadataPath : metadataPaths)
574                {
575                    String textfield = request.getParameter("metadata-" + metadataPath.replaceAll("/", "."));
576
577                    AttributesImpl attrs = new AttributesImpl();
578                    attrs.addCDATAAttribute("name", metadataPath);
579                    XMLUtils.createElement(contentHandler, "metadata", attrs, textfield != null ? textfield : "");
580                }
581                XMLUtils.endElement(contentHandler, "metadata");
582            }
583        }
584    }
585
586    private void _saxAllWords(Request request) throws SAXException
587    {
588        String textfield = request.getParameter("all-words");
589        XMLUtils.createElement(contentHandler, "all-words", textfield != null ? textfield : "");
590    }
591
592    private void _saxExactWording(Request request) throws SAXException
593    {
594        String textfield = request.getParameter("exact-wording");
595        XMLUtils.createElement(contentHandler, "exact-wording", textfield != null ? textfield : "");
596    }
597
598    private void _saxNoWords(Request request) throws SAXException
599    {
600        String textfield = request.getParameter("no-words");
601        XMLUtils.createElement(contentHandler, "no-words", textfield != null ? textfield : "");
602    }
603
604    private void _saxContentType(Request request) throws SAXException
605    {
606        String[] cTypes = request.getParameterValues("content-types");
607        if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals("")))
608        {
609            for (String cType : cTypes)
610            {
611                XMLUtils.createElement(contentHandler, "content-type", cType);
612            }
613        }
614    }
615
616    private void _saxTags(Request request) throws SAXException
617    {
618        String size = request.getParameter("tags-size");
619        if (!StringUtils.isEmpty(size))
620        {
621            int nbCat = Integer.parseInt(size);
622            for (int i = 1; i < nbCat + 1; i++)
623            {
624                String[] tags = request.getParameterValues("tags-" + i);
625                if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals("")))
626                {
627                    if (tags.length == 1)
628                    {
629                        tags = tags[0].split(",");
630                    }
631
632                    for (String tag : tags)
633                    {
634                        XMLUtils.createElement(contentHandler, "tag", tag);
635                    }
636
637                }
638            }
639        }
640
641        String[] tags = request.getParameterValues("tags");
642        if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals("")))
643        {
644            for (String tag : tags)
645            {
646                XMLUtils.createElement(contentHandler, "tag", tag);
647            }
648        }
649    }
650
651    private void _saxPages(Request request) throws SAXException
652    {
653        String[] pages = request.getParameterValues("pages");
654        if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals("")))
655        {
656            for (String id : pages)
657            {
658                XMLUtils.createElement(contentHandler, "page", id);
659            }
660        }
661    }
662
663    private void _saxMultisite(Request request) throws SAXException
664    {
665        boolean multisite = request.getParameter("multisite") != null;
666        if (multisite)
667        {
668            XMLUtils.createElement(contentHandler, "multisite");
669
670            String[] sites = request.getParameterValues("sites");
671            if (sites != null && sites.length > 0 && !(sites.length == 1 && sites[0].equals("")))
672            {
673                for (String site : sites)
674                {
675                    XMLUtils.createElement(contentHandler, "site", site);
676                }
677            }
678
679        }
680    }
681
682    /**
683     * Get the content type's
684     * 
685     * @param request The request
686     * @return the content type's
687     */
688    protected Collection<String> getContentTypes(Request request)
689    {
690        String[] cTypes = request.getParameterValues("content-types");
691
692        if (cTypes != null && cTypes.length > 0 && !(cTypes.length == 1 && cTypes[0].equals("")))
693        {
694            if (cTypes.length == 1)
695            {
696                cTypes = cTypes[0].split(",");
697            }
698
699            return Arrays.asList(cTypes);
700        }
701        return _getAvailableContentTypes();
702    }
703
704    private Collection<String> _getAvailableContentTypes()
705    {
706        @SuppressWarnings("unchecked")
707        Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
708        if (parentContext == null)
709        {
710            parentContext = Collections.emptyMap();
711        }
712
713        String[] serviceCTypes = (String[]) parentContext.get("search-by-content-types");
714
715        String searchCTypeType = parameters.getParameter("search-by-content-types-choice", CONTENT_TYPE_CHOICE_NONE);
716        if (ArrayUtils.isNotEmpty(serviceCTypes) && StringUtils.isNotBlank(serviceCTypes[0]))
717        {
718            return Arrays.asList(serviceCTypes);
719        }
720        else if (!searchCTypeType.equals(CONTENT_TYPE_CHOICE_NONE))
721        {
722            return _getAllContentTypes();
723        }
724
725        return Collections.emptyList();
726    }
727
728    private Set<String> _getAllContentTypes()
729    {
730        Set<String> allCTypes = new HashSet<>(_cTypeExtPt.getExtensionsIds());
731        allCTypes.add("resource");
732        return allCTypes;
733    }
734
735    private void _saxContentTypeCriteria() throws SAXException
736    {
737        Collection<String> cTypes = _getAvailableContentTypes();
738
739        XMLUtils.startElement(contentHandler, "content-types");
740        for (String cTypeId : cTypes)
741        {
742            if ("resource".equals(cTypeId))
743            {
744                AttributesImpl attr = new AttributesImpl();
745                attr.addCDATAAttribute("id", cTypeId);
746                XMLUtils.startElement(contentHandler, "type", attr);
747                new I18nizableText("plugin.web", "PLUGINS_WEB_SERVICE_FRONT_SEARCH_ON_DOCUMENTS").toSAX(contentHandler);
748                XMLUtils.endElement(contentHandler, "type");
749            }
750            else if (StringUtils.isNotEmpty(cTypeId))
751            {
752                ContentType cType = _cTypeExtPt.getExtension(cTypeId);
753
754                AttributesImpl attr = new AttributesImpl();
755                attr.addCDATAAttribute("id", cTypeId);
756                XMLUtils.startElement(contentHandler, "type", attr);
757                cType.getLabel().toSAX(contentHandler);
758                XMLUtils.endElement(contentHandler, "type");
759            }
760
761        }
762        XMLUtils.endElement(contentHandler, "content-types");
763    }
764
765    private void _saxTagsCriteria(String siteName) throws SAXException
766    {
767        @SuppressWarnings("unchecked")
768        Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
769        if (parentContext == null)
770        {
771            parentContext = Collections.emptyMap();
772        }
773
774        Map<String, Object> contextParameters = new HashMap<>();
775        contextParameters.put("siteName", siteName);
776
777        String[] tagArray = (String[]) parentContext.get("search-by-tags");
778        if (ArrayUtils.isNotEmpty(tagArray))
779        {
780            XMLUtils.startElement(contentHandler, "tags");
781            for (String tagName : tagArray)
782            {
783                if (StringUtils.isNotEmpty(tagName))
784                {
785                    boolean found = false;
786                    Set<String> extensionsIds = _tagExtPt.getExtensionsIds();
787
788                    for (String id : extensionsIds)
789                    {
790                        if (found)
791                        {
792                            continue;
793                        }
794
795                        I18nizableText label = null;
796                        Map<String, Tag> tags = null;
797
798                        TagProvider tagProvider = _tagExtPt.getExtension(id);
799                        if (tagName.startsWith("provider_") && id.equals(tagName.substring("provider_".length())))
800                        {
801                            found = true;
802
803                            label = tagProvider.getLabel();
804
805                            tags = tagProvider.getTags(contextParameters);
806                        }
807                        else if (tagProvider.hasTag(tagName, contextParameters))
808                        {
809                            found = true;
810
811                            Tag tag = tagProvider.getTag(tagName, contextParameters);
812                            label = tag.getTitle();
813                            tags = tag.getTags();
814                        }
815
816                        if (found)
817                        {
818                            AttributesImpl attr = new AttributesImpl();
819                            attr.addCDATAAttribute("id", tagName);
820                            XMLUtils.startElement(contentHandler, "tag", attr);
821
822                            if (label != null)
823                            {
824                                label.toSAX(contentHandler, "title");
825                            }
826
827                            if (tags != null)
828                            {
829                                for (Tag child : tags.values())
830                                {
831                                    if (child.getTarget().getName().equals("CONTENT"))
832                                    {
833                                        _saxTag(child, true);
834                                    }
835                                }
836                            }
837
838                            XMLUtils.endElement(contentHandler, "tag");
839                        }
840                    }
841                }
842            }
843            XMLUtils.endElement(contentHandler, "tags");
844        }
845    }
846
847    private void _saxTag(Tag tag, boolean recursive) throws SAXException
848    {
849        AttributesImpl attr = new AttributesImpl();
850        attr.addCDATAAttribute("id", tag.getName());
851        XMLUtils.startElement(contentHandler, "tag", attr);
852        tag.getTitle().toSAX(contentHandler, "title");
853
854        if (recursive)
855        {
856            for (Tag child : tag.getTags().values())
857            {
858                _saxTag(child, true);
859            }
860        }
861
862        XMLUtils.endElement(contentHandler, "tag");
863    }
864
865    private void _saxSitemapCriteria() throws SAXException
866    {
867        @SuppressWarnings("unchecked")
868        Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
869        if (parentContext == null)
870        {
871            parentContext = Collections.emptyMap();
872        }
873
874        String[] pages = (String[]) parentContext.get("search-by-pages");
875        if (ArrayUtils.isNotEmpty(pages))
876        {
877            XMLUtils.startElement(contentHandler, "pages");
878
879            for (String pageID : pages)
880            {
881                if (StringUtils.isNotEmpty(pageID))
882                {
883                    Page page = _resolver.resolveById(pageID);
884                    AttributesImpl attr = new AttributesImpl();
885                    attr.addCDATAAttribute("id", pageID);
886                    attr.addCDATAAttribute("path", page.getSitemap().getName() + "/" + page.getPathInSitemap());
887                    attr.addCDATAAttribute("title", page.getTitle());
888                    attr.addCDATAAttribute("long-title", page.getLongTitle());
889                    XMLUtils.createElement(contentHandler, "page", attr);
890                }
891            }
892
893            XMLUtils.endElement(contentHandler, "pages");
894        }
895    }
896
897    /**
898     * Get the content type's filter value
899     * 
900     * @param request The request
901     * @return the content type's filter value
902     */
903    protected String getContentTypeFilterValue(Request request)
904    {
905        Enumeration<String> paramNames = request.getParameterNames();
906        while (paramNames.hasMoreElements())
907        {
908            String paramName = paramNames.nextElement();
909            if (paramName.startsWith("ctype-filter-"))
910            {
911                return paramName.substring("ctype-filter-".length());
912            }
913        }
914
915        if (request.getParameter("current-ctype-filter") != null)
916        {
917            return request.getParameter("current-ctype-filter");
918        }
919
920        return null;
921    }
922
923    // one or more of the words
924    private void _addTextFieldQuery(Collection<Query> queries, String language, Request request) throws IllegalArgumentException
925    {
926        String text = request.getParameter("textfield");
927
928        if (StringUtils.isNotBlank(text))
929        {
930            String trimText = text.trim();
931            String escapedText = _escapeQueryCharsButNotQuotes(trimText);
932
933            Query query = new FullTextQuery(escapedText, language, Operator.SEARCH);
934            Query contentQuery = new PageContentQuery(query);
935
936            queries.add(new OrQuery(query, contentQuery));
937        }
938    }
939
940    /*
941     * Do not escape double quotes for exact searching
942     */
943    private String _escapeQueryCharsButNotQuotes(String text)
944    {
945        StringBuilder sb = new StringBuilder();
946        String[] parts = StringUtils.splitPreserveAllTokens(text, '"');
947        for (int i = 0; i < parts.length; i++)
948        {
949            if (i != 0)
950            {
951                sb.append("\"");
952            }
953            String part = parts[i];
954            sb.append(ClientUtils.escapeQueryChars(part));
955        }
956
957        return sb.toString();
958    }
959
960    /**
961     * Add query for each metadata
962     * 
963     * @param queries The list of query
964     * @param language The lang
965     * @param request The request
966     * @throws IllegalArgumentException If an error occurred
967     */
968    protected void addMetadataQuery(Collection<Query> queries, String language, Request request) throws IllegalArgumentException
969    {
970        ZoneItem zoneItem = (ZoneItem) request.getAttribute(org.ametys.web.repository.page.ZoneItem.class.getName());
971        if (zoneItem.getServiceParameters().hasMetadata("search-by-metadata"))
972        {
973            String[] metadataPaths = zoneItem.getServiceParameters().getStringArray("search-by-metadata");
974            for (String metadataPath : metadataPaths)
975            {
976                String value = request.getParameter("metadata-" + metadataPath.replaceAll("/", "."));
977
978                if (StringUtils.isNotBlank(value))
979                {
980                    Collection<String> contentTypes = getContentTypes(request);
981
982                    MetadataDefinition metadataDefinition = contentTypes.isEmpty() ? null
983                            : _contentTypesHelper.getMetadataDefinition(metadataPath, (String[]) contentTypes.toArray(), null);
984                    if (metadataDefinition == null || metadataDefinition.getEnumerator() == null)
985                    {
986                        String escapedText = ClientUtils.escapeQueryChars(value).toLowerCase();
987                        if (!escapedText.startsWith("*") && !escapedText.endsWith("*"))
988                        {
989                            escapedText = "*" + escapedText + "*";
990                        }
991                        Query query = new StringQuery(metadataPath, Operator.SEARCH, escapedText, language);
992                        Query contentQuery = new PageContentQuery(query);
993
994                        queries.add(new OrQuery(query, contentQuery));
995                    }
996                    else
997                    {
998                        Query query = new StringQuery(metadataPath, Operator.EQ, value, language);
999                        Query contentQuery = new PageContentQuery(query);
1000
1001                        queries.add(new OrQuery(query, contentQuery));
1002                    }
1003                }
1004            }
1005        }
1006    }
1007
1008    // all the words
1009    private void _addAllWordsTextFieldQuery(Collection<Query> queries, String language, Request request) throws IllegalArgumentException
1010    {
1011        // TODO All words? OrQuery of AndQuery instead?
1012        String words = request.getParameter("all-words");
1013
1014        if (StringUtils.isNotBlank(words))
1015        {
1016            String escapedWords = ClientUtils.escapeQueryChars(words);
1017
1018            StringBuilder allWords = new StringBuilder();
1019            for (String word : StringUtils.split(escapedWords))
1020            {
1021                if (allWords.length() > 0)
1022                {
1023                    allWords.append(' ');
1024                }
1025                allWords.append('+').append(word);
1026            }
1027
1028            // queries.add(new FullTextQuery(allWords.toString(), language,
1029            // Operator.SEARCH));
1030
1031            Query query = new FullTextQuery(allWords.toString(), language, Operator.SEARCH);
1032            Query contentQuery = new PageContentQuery(query);
1033
1034            queries.add(new OrQuery(query, contentQuery));
1035        }
1036    }
1037
1038    // exact wording or phrase
1039    private void _addExactWordingTextFieldQuery(Collection<Query> queries, String language, Request request) throws IllegalArgumentException
1040    {
1041        String exact = request.getParameter("exact-wording");
1042
1043        if (StringUtils.isNotBlank(exact))
1044        {
1045            String escapedExact = ClientUtils.escapeQueryChars(exact);
1046
1047            Query query = new FullTextQuery(escapedExact, language, Operator.EQ);
1048            Query contentQuery = new PageContentQuery(query);
1049
1050            queries.add(new OrQuery(query, contentQuery));
1051        }
1052    }
1053
1054    // none of this word
1055    private void _addNoWordsTextFieldQuery(Collection<Query> queries, String language, Request request) throws IllegalArgumentException
1056    {
1057        String noWords = request.getParameter("no-words");
1058
1059        if (StringUtils.isNotBlank(noWords))
1060        {
1061            String escapedWords = ClientUtils.escapeQueryChars(noWords);
1062
1063            StringBuilder allWords = new StringBuilder();
1064            for (String word : StringUtils.split(escapedWords))
1065            {
1066                if (allWords.length() > 0)
1067                {
1068                    allWords.append(' ');
1069                }
1070                allWords.append('+').append(word);
1071            }
1072
1073            // queries.add(new NotQuery(new FullTextQuery(allWords.toString(),
1074            // language, Operator.SEARCH)));
1075
1076            Query query = new NotQuery(new FullTextQuery(allWords.toString(), language, Operator.SEARCH));
1077            Query contentQuery = new PageContentQuery(query);
1078
1079            queries.add(new OrQuery(query, contentQuery));
1080        }
1081    }
1082
1083    private void _addContentTypeQuery(Collection<Query> queries, Request request)
1084    {
1085        Collection<String> cTypes = new ArrayList<>(getContentTypes(request));
1086
1087        // Resource is handled in the "document types" filter.
1088        cTypes.remove("resource");
1089
1090        if (!cTypes.isEmpty())
1091        {
1092            queries.add(new PageContentQuery(new ContentTypeQuery(cTypes)));
1093        }
1094    }
1095
1096    private void _addTagsQuery(Collection<Query> queries, Request request)
1097    {
1098        String size = request.getParameter("tags-size");
1099        if (!StringUtils.isEmpty(size))
1100        {
1101            @SuppressWarnings("unchecked")
1102            Map<String, Object> parentContext = (Map<String, Object>) objectModel.get(ObjectModelHelper.PARENT_CONTEXT);
1103            if (parentContext == null)
1104            {
1105                parentContext = Collections.emptyMap();
1106            }
1107
1108            boolean isStrictSearch = parameters.getParameterAsBoolean("strict-search-on-tags", true);
1109
1110            int nbCat = Integer.parseInt(size);
1111            for (int i = 1; i < nbCat + 1; i++)
1112            {
1113                String[] tags = request.getParameterValues("tags-" + i);
1114                if (tags != null && tags.length > 0 && !(tags.length == 1 && tags[0].equals("")))
1115                {
1116                    List<Query> tagQueries = new ArrayList<>();
1117
1118                    for (String tag : tags)
1119                    {
1120                        String[] values = StringUtils.split(tag, ',');
1121
1122                        tagQueries.add(new TagQuery(Operator.EQ, !isStrictSearch, values));
1123                    }
1124
1125                    queries.add(new PageContentQuery(new OrQuery(tagQueries)));
1126                }
1127            }
1128        }
1129    }
1130
1131    private void _addPagesQuery(Collection<Query> queries, Request request)
1132    {
1133        List<Query> pageQueries = new ArrayList<>();
1134
1135        String[] pages = request.getParameterValues("pages");
1136        if (pages != null && pages.length > 0 && !(pages.length == 1 && pages[0].equals("")))
1137        {
1138            for (String pageIds : pages)
1139            {
1140                for (String pageId : StringUtils.split(pageIds, ","))
1141                {
1142                    pageQueries.add(new PageQuery(pageId, true));
1143                }
1144            }
1145        }
1146
1147        queries.add(new OrQuery(pageQueries));
1148    }
1149
1150    private void _addDateQuery(Collection<Query> queries, Request request)
1151    {
1152        String startDateId = parameters.getParameter("startDate", "");
1153        String endDateId = parameters.getParameter("endDate", "");
1154
1155        String startDateStr = request.getParameter("startDate");
1156        String endDateStr = request.getParameter("endDate");
1157
1158        // We check if startDateStr < endDateStr. If not, we swap.
1159        if (StringUtils.isNotBlank(endDateStr) && StringUtils.isNotBlank(startDateStr) && startDateStr.compareTo(endDateStr) > 0)
1160        {
1161            String dateAux = startDateStr;
1162            startDateStr = endDateStr;
1163            endDateStr = dateAux;
1164        }
1165
1166        String startPropId = startDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : startDateId;
1167        String endPropId = endDateId.equals("last-modified") ? SolrFieldNames.LAST_MODIFIED : endDateId;
1168
1169        if (StringUtils.isNotBlank(endDateStr))
1170        {
1171            Date endDate = _toDate(endDateStr);
1172            // Join for returning page documents
1173            queries.add(new PageContentQuery(new DateQuery(startPropId, Operator.LE, endDate)));
1174        }
1175
1176        if (StringUtils.isNotBlank(startDateStr))
1177        {
1178            Date startDate = _toDate(startDateStr);
1179
1180            if (startDate != null)
1181            {
1182                // Two cases are possible :
1183                // An event could have an end date (query 1)
1184                // If not, the start date is also the end date (query 2)
1185                List<Query> endDateQueries = new ArrayList<>();
1186
1187                if (StringUtils.isNotBlank(endPropId))
1188                {
1189                    endDateQueries.add(new DateQuery(endPropId, Operator.GE, startDate));
1190                }
1191                endDateQueries.add(new DateQuery(startPropId, Operator.GE, startDate));
1192
1193                // Join for returning page documents
1194                queries.add(new PageContentQuery(new OrQuery(endDateQueries)));
1195            }
1196        }
1197    }
1198
1199    private Date _toDate(String dateStr)
1200    {
1201        try
1202        {
1203            LocalDate dt = LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE);
1204            return DateUtils.asDate(dt);
1205        }
1206        catch (DateTimeParseException e)
1207        {
1208            getLogger().error("Invalid date format " + dateStr, e);
1209        }
1210        return null;
1211    }
1212
1213}