001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.jsp.search.config.parser;
029
030import org.opencms.file.CmsObject;
031import org.opencms.jsp.search.config.CmsSearchConfigurationCommon;
032import org.opencms.jsp.search.config.CmsSearchConfigurationDidYouMean;
033import org.opencms.jsp.search.config.CmsSearchConfigurationFacetField;
034import org.opencms.jsp.search.config.CmsSearchConfigurationFacetQuery;
035import org.opencms.jsp.search.config.CmsSearchConfigurationFacetQuery.CmsFacetQueryItem;
036import org.opencms.jsp.search.config.CmsSearchConfigurationFacetRange;
037import org.opencms.jsp.search.config.CmsSearchConfigurationHighlighting;
038import org.opencms.jsp.search.config.CmsSearchConfigurationPagination;
039import org.opencms.jsp.search.config.CmsSearchConfigurationSortOption;
040import org.opencms.jsp.search.config.CmsSearchConfigurationSorting;
041import org.opencms.jsp.search.config.I_CmsSearchConfigurationCommon;
042import org.opencms.jsp.search.config.I_CmsSearchConfigurationDidYouMean;
043import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacet;
044import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetField;
045import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetQuery;
046import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetQuery.I_CmsFacetQueryItem;
047import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetRange;
048import org.opencms.jsp.search.config.I_CmsSearchConfigurationGeoFilter;
049import org.opencms.jsp.search.config.I_CmsSearchConfigurationHighlighting;
050import org.opencms.jsp.search.config.I_CmsSearchConfigurationPagination;
051import org.opencms.jsp.search.config.I_CmsSearchConfigurationSortOption;
052import org.opencms.jsp.search.config.I_CmsSearchConfigurationSorting;
053import org.opencms.main.CmsLog;
054import org.opencms.main.OpenCms;
055import org.opencms.search.solr.CmsSolrIndex;
056import org.opencms.xml.content.CmsXmlContent;
057import org.opencms.xml.content.CmsXmlContentValueSequence;
058import org.opencms.xml.types.I_CmsXmlContentValue;
059
060import java.util.ArrayList;
061import java.util.Arrays;
062import java.util.HashMap;
063import java.util.Iterator;
064import java.util.LinkedHashMap;
065import java.util.List;
066import java.util.Locale;
067import java.util.Map;
068import java.util.Objects;
069
070import org.apache.commons.logging.Log;
071
072/** Search configuration parser reading XML. */
073public class CmsXMLSearchConfigurationParser implements I_CmsSearchConfigurationParser {
074
075    /** Logger for the class. */
076    protected static final Log LOG = CmsLog.getLog(CmsXMLSearchConfigurationParser.class);
077
078    /** The element names of the xml content. */
079    /** Elements for common options. */
080    /** XML element name. */
081    private static final String XML_ELEMENT_QUERYPARAM = "QueryParam";
082    /** XML element name. */
083    private static final String XML_ELEMENT_LAST_QUERYPARAM = "LastQueryParam";
084    /** XML element name. */
085    private static final String XML_ELEMENT_MAX_RETURNED_RESULTS = "MaxReturnedResults";
086    /** XML element name. */
087    private static final String XML_ELEMENT_ESCAPE_QUERY_CHARACTERS = "EscapeQueryCharacters";
088    /** XML element name. */
089    private static final String XML_ELEMENT_RELOADED_PARAM = "ReloadedParam";
090    /** XML element name. */
091    private static final String XML_ELEMENT_SEARCH_FOR_EMPTY_QUERY = "SearchForEmptyQuery";
092    /** XML element name. */
093    private static final String XML_ELEMENT_IGNORE_QUERY = "IgnoreQuery";
094    /** XML element name. */
095    private static final String XML_ELEMENT_IGNORE_RELEASE_DATE = "IgnoreReleaseDate";
096    /** XML element name. */
097    private static final String XML_ELEMENT_IGNORE_EXPIRATION_DATE = "IgnoreExpirationDate";
098    /** XML element name. */
099    private static final String XML_ELEMENT_QUERY_MODIFIER = "QueryModifier";
100    /** XML element name. */
101    private static final String XML_ELEMENT_PAGEPARAM = "PageParam";
102    /** XML element name. */
103    private static final String XML_ELEMENT_INDEX = "Index";
104    /** XML element name. */
105    private static final String XML_ELEMENT_CORE = "Core";
106    /** XML element name. */
107    private static final String XML_ELEMENT_EXTRASOLRPARAMS = "ExtraSolrParams";
108    /** XML element name. */
109    private static final String XML_ELEMENT_ADDITIONAL_PARAMETERS = "AdditionalRequestParams";
110    /** XML element name. */
111    private static final String XML_ELEMENT_ADDITIONAL_PARAMETERS_PARAM = "Param";
112    /** XML element name. */
113    private static final String XML_ELEMENT_ADDITIONAL_PARAMETERS_SOLRQUERY = "SolrQuery";
114    /** XML element name. */
115    private static final String XML_ELEMENT_PAGESIZE = "PageSize";
116    /** XML element name. */
117    private static final String XML_ELEMENT_PAGENAVLENGTH = "PageNavLength";
118
119    /** XML element names for facet configuration. */
120    /** XML element name for the root element of a field facet configuration. */
121    private static final String XML_ELEMENT_FIELD_FACETS = "FieldFacet";
122    /** XML element name for the root element of the query facet configuration. */
123    private static final String XML_ELEMENT_QUERY_FACET = "QueryFacet";
124    /** XML element names for facet options. */
125    /** XML element name. */
126    private static final String XML_ELEMENT_FACET_LIMIT = "Limit";
127    /** XML element name. */
128    private static final String XML_ELEMENT_FACET_MINCOUNT = "MinCount";
129    /** XML element name. */
130    private static final String XML_ELEMENT_FACET_LABEL = "Label";
131    /** XML element name. */
132    private static final String XML_ELEMENT_FACET_FIELD = "Field";
133    /** XML element name. */
134    private static final String XML_ELEMENT_FACET_NAME = "Name";
135    /** XML element name. */
136    private static final String XML_ELEMENT_FACET_PREFIX = "Prefix";
137    /** XML element name. */
138    private static final String XML_ELEMENT_FACET_ORDER = "Order";
139    /** XML element name. */
140    private static final String XML_ELEMENT_FACET_FILTERQUERYMODIFIER = "FilterQueryModifier";
141    /** XML element name. */
142    private static final String XML_ELEMENT_FACET_ISANDFACET = "IsAndFacet";
143    /** XML element name. */
144    private static final String XML_ELEMENT_FACET_PRESELECTION = "PreSelection";
145    /** XML element name. */
146    private static final String XML_ELEMENT_FACET_IGNOREALLFACETFILTERS = "IgnoreAllFacetFilters";
147    /** XML element name. */
148    private static final String XML_ELEMENT_FACET_EXCLUDETAG = "ExcludeTag";
149    /** XML element name. */
150    private static final String XML_ELEMENT_QUERY_FACET_QUERY = "QueryItem";
151    /** XML element name. */
152    private static final String XML_ELEMENT_QUERY_FACET_QUERY_QUERY = "Query";
153    /** XML element name. */
154    private static final String XML_ELEMENT_QUERY_FACET_QUERY_LABEL = "Label";
155
156    /** XML element names for sort options. */
157    /** XML element name. */
158    private static final String XML_ELEMENT_SORTPARAM = "SortParam";
159    /** XML element name for the root element for sort options. */
160    private static final String XML_ELEMENT_DEFAULTSORTOPTION = "DefaultSortOption";
161    /** XML element name for the root element for sort options. */
162    private static final String XML_ELEMENT_SORTOPTIONS = "SortOption";
163    /** XML element names for a single search option. */
164    private static final String XML_ELEMENT_SORTOPTION_LABEL = "Label";
165    /** XML element name. */
166    private static final String XML_ELEMENT_SORTOPTION_PARAMVALUE = "ParamValue";
167    /** XML element name. */
168    private static final String XML_ELEMENT_SORTOPTION_SOLRVALUE = "SolrValue";
169    /** XML element name for the root element for the highlighting configuration. */
170    private static final String XML_ELEMENT_HIGHLIGHTER = "Highlighting";
171    /** XML elements for the highlighting configuration. */
172    /** XML element name. */
173    private static final String XML_ELEMENT_HIGHLIGHTER_FIELD = "Field";
174    /** XML element name. */
175    private static final String XML_ELEMENT_HIGHLIGHTER_SNIPPETS = "Snippets";
176    /** XML element name. */
177    private static final String XML_ELEMENT_HIGHLIGHTER_FRAGSIZE = "FragSize";
178    /** XML element name. */
179    private static final String XML_ELEMENT_HIGHLIGHTER_ALTERNATE_FIELD = "AlternateField";
180    /** XML element name. */
181    private static final String XML_ELEMENT_HIGHLIGHTER_MAX_LENGTH_ALTERNATE_FIELD = "MaxAlternateFieldLength";
182    /** XML element name. */
183    private static final String XML_ELEMENT_HIGHLIGHTER_SIMPLE_PRE = "SimplePre";
184    /** XML element name. */
185    private static final String XML_ELEMENT_HIGHLIGHTER_SIMPLE_POST = "SimplePost";
186    /** XML element name. */
187    private static final String XML_ELEMENT_HIGHLIGHTER_FORMATTER = "Formatter";
188    /** XML element name. */
189    private static final String XML_ELEMENT_HIGHLIGHTER_FRAGMENTER = "Fragmenter";
190    /** XML element name. */
191    private static final String XML_ELEMENT_HIGHLIGHTER_FASTVECTORHIGHLIGHTING = "UseFastVectorHighlighting";
192    /** XML element name. */
193    private static final String XML_ELEMENT_PARAM = "Param";
194    /** XML element name. */
195    private static final String XML_ELEMENT_PARAM_NAME = "Name";
196    /** XML element name. */
197    private static final String XML_ELEMENT_PARAM_VALUE = "Value";
198
199    /** XML element names for "Did you mean ...?". */
200    /** XML element name. */
201    private static final String XML_ELEMENT_DIDYOUMEAN = "DidYouMean";
202    /** XML elements for the "Did you mean ...?" configuration. */
203    /** XML element name. */
204    private static final String XML_ELEMENT_DIDYOUMEAN_QUERYPARAM = "QueryParam";
205    /** XML element name. */
206    private static final String XML_ELEMENT_DIDYOUMEAN_ESCAPEQUERY = "EscapeQuery";
207    /** XML element name. */
208    private static final String XML_ELEMENT_DIDYOUMEAN_COLLATE = "Collate";
209    /** XML element name. */
210    private static final String XML_ELEMENT_DIDYOUMEAN_COUNT = "Count";
211    /** XML element name. */
212    private static final String XML_ELEMENT_RANGE_FACETS = "RangeFacet";
213    /** XML element name. */
214    private static final String XML_ELEMENT_RANGE_FACET_RANGE = "Range";
215    /** XML element name. */
216    private static final String XML_ELEMENT_RANGE_FACET_START = "Start";
217    /** XML element name. */
218    private static final String XML_ELEMENT_RANGE_FACET_END = "End";
219    /** XML element name. */
220    private static final String XML_ELEMENT_RANGE_FACET_GAP = "Gap";
221    /** XML element name. */
222    private static final String XML_ELEMENT_RANGE_FACET_OTHER = "Other";
223    /** XML element name. */
224    private static final String XML_ELEMENT_RANGE_FACET_HARDEND = "HardEnd";
225    /** XML element name. */
226    private static final String XML_ELEMENT_RANGE_FACET_METHOD = "Method";
227
228    /** Default value. */
229    private static final String DEFAULT_QUERY_PARAM = "q";
230    /** Default value. */
231    private static final String DEFAULT_LAST_QUERY_PARAM = "lq";
232    /** Default value. */
233    private static final String DEFAULT_RELOADED_PARAM = "reloaded";
234
235    /** The XML content that contains the configuration. */
236    CmsXmlContent m_xml;
237    /** The locale in which the configuration should be read. */
238    Locale m_locale;
239
240    /** Constructor taking the XML content that should be read and the locale in which it should be read.
241     * @param xml The XML content that should be read for the configuration.
242     * @param locale The locale in which the content should be read.
243     */
244    public CmsXMLSearchConfigurationParser(final CmsXmlContent xml, final Locale locale) {
245
246        m_xml = xml;
247        m_locale = locale;
248    }
249
250    /**
251     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseCommon(CmsObject)
252     */
253    @Override
254    public I_CmsSearchConfigurationCommon parseCommon(CmsObject cms) {
255
256        String indexName = getIndex(cms);
257
258        return new CmsSearchConfigurationCommon(
259            getQueryParam(),
260            getLastQueryParam(),
261            parseOptionalBooleanValue(XML_ELEMENT_ESCAPE_QUERY_CHARACTERS),
262            getFirstCallParam(),
263            getSearchForEmtpyQuery(),
264            getIgnoreQuery(),
265            getQueryModifier(),
266            indexName,
267            getCore(),
268            getExtraSolrParams(),
269            getAdditionalRequestParameters(),
270            getIgnoreReleaseDate(),
271            getIgnoreExpirationDate(),
272            getMaxReturnedResults(indexName));
273    }
274
275    /**
276     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseDidYouMean()
277     */
278    @Override
279    public I_CmsSearchConfigurationDidYouMean parseDidYouMean() {
280
281        final I_CmsXmlContentValue didYouMean = m_xml.getValue(XML_ELEMENT_DIDYOUMEAN, m_locale);
282        if (didYouMean == null) {
283            return null;
284        }
285        final String pathPrefix = didYouMean.getPath() + "/";
286        String param = parseOptionalStringValue(pathPrefix + XML_ELEMENT_DIDYOUMEAN_QUERYPARAM);
287        if (null == param) {
288            param = getQueryParam();
289        }
290        Boolean escape = parseOptionalBooleanValue(pathPrefix + XML_ELEMENT_DIDYOUMEAN_ESCAPEQUERY);
291        if (null == escape) {
292            escape = parseOptionalBooleanValue(XML_ELEMENT_ESCAPE_QUERY_CHARACTERS);
293        }
294        Boolean collate = parseOptionalBooleanValue(pathPrefix + XML_ELEMENT_DIDYOUMEAN_COLLATE);
295        Integer count = parseOptionalIntValue(pathPrefix + XML_ELEMENT_DIDYOUMEAN_COUNT);
296        return new CmsSearchConfigurationDidYouMean(param, escape, collate, count);
297    }
298
299    /**
300     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseFieldFacets()
301     */
302    @Override
303    public Map<String, I_CmsSearchConfigurationFacetField> parseFieldFacets() {
304
305        final Map<String, I_CmsSearchConfigurationFacetField> facetConfigs = new LinkedHashMap<>();
306        final CmsXmlContentValueSequence fieldFacets = m_xml.getValueSequence(XML_ELEMENT_FIELD_FACETS, m_locale);
307        if (fieldFacets != null) {
308            for (int i = 0; i < fieldFacets.getElementCount(); i++) {
309
310                final I_CmsSearchConfigurationFacetField config = parseFieldFacet(
311                    fieldFacets.getValue(i).getPath() + "/");
312                if (config != null) {
313                    facetConfigs.put(config.getName(), config);
314                }
315            }
316        }
317        return facetConfigs;
318    }
319
320    /**
321     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseGeoFilter()
322     */
323    @Override
324    public I_CmsSearchConfigurationGeoFilter parseGeoFilter() {
325
326        return null;
327    }
328
329    /**
330     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseHighlighter()
331     */
332    @Override
333    public I_CmsSearchConfigurationHighlighting parseHighlighter() {
334
335        final I_CmsXmlContentValue highlighter = m_xml.getValue(XML_ELEMENT_HIGHLIGHTER, m_locale);
336        if (highlighter == null) {
337            return null;
338        }
339        try {
340            Map<String, String> hlParams = new LinkedHashMap<>();
341            final String pathPrefix = highlighter.getPath() + "/";
342            final String field = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_FIELD);
343            hlParams.put("fl", field);
344            final Integer snippets = parseOptionalIntValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_SNIPPETS);
345            if (null != snippets) {
346                hlParams.put("snippets", snippets.toString());
347            }
348            final Integer fragsize = parseOptionalIntValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_FRAGSIZE);
349            if (null != fragsize) {
350                hlParams.put("fragsize", fragsize.toString());
351            }
352            final String alternateField = parseOptionalStringValue(
353                pathPrefix + XML_ELEMENT_HIGHLIGHTER_ALTERNATE_FIELD);
354            if (null != alternateField) {
355                hlParams.put("alternateField", alternateField);
356            }
357            final Integer maxAlternateFieldLength = parseOptionalIntValue(
358                pathPrefix + XML_ELEMENT_HIGHLIGHTER_MAX_LENGTH_ALTERNATE_FIELD);
359            if (null != maxAlternateFieldLength) {
360                hlParams.put("maxAlternateFieldLength", maxAlternateFieldLength.toString());
361            }
362            final String pre = parseOptionalStringValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_SIMPLE_PRE);
363            if (null != pre) {
364                hlParams.put("simple.pre", pre);
365                hlParams.put("tag.pre", pre);
366            }
367            final String post = parseOptionalStringValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_SIMPLE_POST);
368            if (null != post) {
369                hlParams.put("simple.post", post);
370                hlParams.put("tag.post", post);
371            }
372            final String formatter = parseOptionalStringValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_FORMATTER);
373            if (null != formatter) {
374                hlParams.put("formatter", formatter);
375            }
376            final String fragmenter = parseOptionalStringValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_FRAGMENTER);
377            if (null != fragmenter) {
378                hlParams.put("fragmenter", fragmenter);
379            }
380            final Boolean useFastVectorHighlighting = parseOptionalBooleanValue(
381                pathPrefix + XML_ELEMENT_HIGHLIGHTER_FASTVECTORHIGHLIGHTING);
382            if (null != useFastVectorHighlighting) {
383                hlParams.put("method", "fastVector");
384            }
385            Map<String, String> params = parseOptionalKeyValueMap(pathPrefix + XML_ELEMENT_PARAM);
386            if (null != params) {
387                hlParams.putAll(params);
388            }
389            return new CmsSearchConfigurationHighlighting(hlParams);
390        } catch (final Exception e) {
391            LOG.error(Messages.get().getBundle().key(Messages.ERR_MANDATORY_HIGHLIGHTING_FIELD_MISSING_0), e);
392            return null;
393        }
394    }
395
396    /**
397     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parsePagination()
398     */
399    @Override
400    public I_CmsSearchConfigurationPagination parsePagination() {
401
402        return CmsSearchConfigurationPagination.create(getPageParam(), getPageSizes(), getPageNavLength());
403    }
404
405    /**
406     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseQueryFacet()
407     */
408    @Override
409    public I_CmsSearchConfigurationFacetQuery parseQueryFacet() {
410
411        final String pathPrefix = XML_ELEMENT_QUERY_FACET + "/";
412        try {
413            final List<I_CmsFacetQueryItem> queries = parseFacetQueryItems(pathPrefix + XML_ELEMENT_QUERY_FACET_QUERY);
414            final String label = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_LABEL);
415            final Boolean isAndFacet = parseOptionalBooleanValue(pathPrefix + XML_ELEMENT_FACET_ISANDFACET);
416            final List<String> preselection = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_PRESELECTION);
417            final List<String> excludeTags = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_EXCLUDETAG);
418            final Boolean ignoreAllFacetFilters = parseOptionalBooleanValue(
419                pathPrefix + XML_ELEMENT_FACET_IGNOREALLFACETFILTERS);
420            return new CmsSearchConfigurationFacetQuery(
421                queries,
422                label,
423                isAndFacet,
424                preselection,
425                ignoreAllFacetFilters,
426                excludeTags);
427        } catch (final Exception e) {
428            LOG.error(
429                Messages.get().getBundle().key(
430                    Messages.ERR_QUERY_FACET_MANDATORY_KEY_MISSING_1,
431                    XML_ELEMENT_QUERY_FACET_QUERY),
432                e);
433            return null;
434        }
435    }
436
437    /**
438     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseRangeFacets()
439     */
440    @Override
441    public Map<String, I_CmsSearchConfigurationFacetRange> parseRangeFacets() {
442
443        final Map<String, I_CmsSearchConfigurationFacetRange> facetConfigs = new LinkedHashMap<>();
444        final CmsXmlContentValueSequence rangeFacets = m_xml.getValueSequence(XML_ELEMENT_RANGE_FACETS, m_locale);
445        if (rangeFacets != null) {
446            for (int i = 0; i < rangeFacets.getElementCount(); i++) {
447
448                final I_CmsSearchConfigurationFacetRange config = parseRangeFacet(
449                    rangeFacets.getValue(i).getPath() + "/");
450                if (config != null) {
451                    facetConfigs.put(config.getName(), config);
452                }
453            }
454        }
455        return facetConfigs;
456    }
457
458    /**
459     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseSorting()
460     */
461    @Override
462    public I_CmsSearchConfigurationSorting parseSorting() {
463
464        List<I_CmsSearchConfigurationSortOption> options = getSortOptions();
465        String defaultOptionParamValue = parseOptionalStringValue(XML_ELEMENT_DEFAULTSORTOPTION);
466        I_CmsSearchConfigurationSortOption defaultSortOption = null;
467        if (null != defaultOptionParamValue) {
468            Iterator<I_CmsSearchConfigurationSortOption> optIterator = options.iterator();
469            while ((null == defaultSortOption) && optIterator.hasNext()) {
470                I_CmsSearchConfigurationSortOption opt = optIterator.next();
471                if (Objects.equals(opt.getParamValue(), defaultOptionParamValue)) {
472                    defaultSortOption = opt;
473                }
474            }
475        }
476        if ((null == defaultSortOption) && !options.isEmpty()) {
477            defaultSortOption = options.get(0);
478        }
479        return CmsSearchConfigurationSorting.create(getSortParam(), options, defaultSortOption);
480    }
481
482    /** Returns the number of maximally returned results, or <code>null</code> if the indexes default should be used.
483     * @param indexName the name of the index to search in.
484     * @return The number of maximally returned results, or <code>null</code> if the indexes default should be used.
485     */
486    protected int getMaxReturnedResults(String indexName) {
487
488        Integer maxReturnedResults = parseOptionalIntValue(XML_ELEMENT_MAX_RETURNED_RESULTS);
489        if (null != maxReturnedResults) {
490            return maxReturnedResults.intValue();
491        }
492        try {
493            CmsSolrIndex idx = OpenCms.getSearchManager().getIndexSolr(indexName);
494            if (null != idx) {
495                return idx.getMaxProcessedResults();
496            }
497        } catch (Throwable t) {
498            // This is ok, it's allowed to have an external other index here.
499            LOG.debug(
500                "Parsing JSON search configuration for none-CmsSolrIndex "
501                    + indexName
502                    + ". Setting max processed results to unlimited.");
503        }
504        return CmsSolrIndex.MAX_RESULTS_UNLIMITED;
505    }
506
507    /** Helper to read a mandatory String value list.
508     * @param path The XML path of the element to read.
509     * @return The String list stored in the XML, or <code>null</code> if the value could not be read.
510     * @throws Exception thrown if the list of String values can not be read.
511     */
512    protected List<I_CmsFacetQueryItem> parseFacetQueryItems(final String path) throws Exception {
513
514        final List<I_CmsXmlContentValue> values = m_xml.getValues(path, m_locale);
515        if (values == null) {
516            return null;
517        }
518        List<I_CmsFacetQueryItem> parsedItems = new ArrayList<>(values.size());
519        for (I_CmsXmlContentValue value : values) {
520            I_CmsFacetQueryItem item = parseFacetQueryItem(value.getPath() + "/");
521            if (null != item) {
522                parsedItems.add(item);
523            } else {
524                // TODO: log
525            }
526        }
527        return parsedItems;
528    }
529
530    /** Reads the configuration of a field facet.
531     * @param pathPrefix The XML Path that leads to the field facet configuration, or <code>null</code> if the XML was not correctly structured.
532     * @return The read configuration, or <code>null</code> if the XML was not correctly structured.
533     */
534    protected I_CmsSearchConfigurationFacetField parseFieldFacet(final String pathPrefix) {
535
536        try {
537            final String field = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_FACET_FIELD);
538            final String name = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_NAME);
539            final String label = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_LABEL);
540            final Integer minCount = parseOptionalIntValue(pathPrefix + XML_ELEMENT_FACET_MINCOUNT);
541            final Integer limit = parseOptionalIntValue(pathPrefix + XML_ELEMENT_FACET_LIMIT);
542            final String prefix = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_PREFIX);
543            final String sorder = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_ORDER);
544            I_CmsSearchConfigurationFacet.SortOrder order;
545            try {
546                order = I_CmsSearchConfigurationFacet.SortOrder.valueOf(sorder);
547            } catch (@SuppressWarnings("unused") final Exception e) {
548                order = null;
549            }
550            final String filterQueryModifier = parseOptionalStringValue(
551                pathPrefix + XML_ELEMENT_FACET_FILTERQUERYMODIFIER);
552            final Boolean isAndFacet = parseOptionalBooleanValue(pathPrefix + XML_ELEMENT_FACET_ISANDFACET);
553            final List<String> preselection = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_PRESELECTION);
554            final Boolean ignoreAllFacetFilters = parseOptionalBooleanValue(
555                pathPrefix + XML_ELEMENT_FACET_IGNOREALLFACETFILTERS);
556            final List<String> excludeTags = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_EXCLUDETAG);
557            return new CmsSearchConfigurationFacetField(
558                field,
559                name,
560                minCount,
561                limit,
562                prefix,
563                label,
564                order,
565                filterQueryModifier,
566                isAndFacet,
567                preselection,
568                ignoreAllFacetFilters,
569                excludeTags);
570        } catch (final Exception e) {
571            LOG.error(
572                Messages.get().getBundle().key(
573                    Messages.ERR_FIELD_FACET_MANDATORY_KEY_MISSING_1,
574                    XML_ELEMENT_FACET_FIELD),
575                e);
576            return null;
577        }
578    }
579
580    /** Helper to read an optional Boolean value.
581     * @param path The XML path of the element to read.
582     * @return The Boolean value stored in the XML, or <code>null</code> if the value could not be read.
583     */
584    protected Boolean parseOptionalBooleanValue(final String path) {
585
586        final I_CmsXmlContentValue value = m_xml.getValue(path, m_locale);
587        if (value == null) {
588            return null;
589        }
590        final String stringValue = value.getStringValue(null);
591        try {
592            final Boolean boolValue = Boolean.valueOf(stringValue);
593            return boolValue;
594        } catch (final NumberFormatException e) {
595            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_BOOLEAN_MISSING_1, path), e);
596            return null;
597        }
598    }
599
600    /** Helper to read an optional Integer value.
601     * @param path The XML path of the element to read.
602     * @return The Integer value stored in the XML, or <code>null</code> if the value could not be read.
603     */
604    protected Integer parseOptionalIntValue(final String path) {
605
606        final I_CmsXmlContentValue value = m_xml.getValue(path, m_locale);
607        if (value == null) {
608            return null;
609        }
610        final String stringValue = value.getStringValue(null);
611        try {
612            final Integer intValue = Integer.valueOf(stringValue);
613            return intValue;
614        } catch (final NumberFormatException e) {
615            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_INTEGER_MISSING_1, path), e);
616            return null;
617        }
618    }
619
620    /** Helper to read an optional String value.
621     * @param path The XML path of the element to read.
622     * @return The String value stored in the XML, or <code>null</code> if the value could not be read.
623     */
624    protected Map<String, String> parseOptionalKeyValueMap(final String path) {
625
626        final CmsXmlContentValueSequence entries = m_xml.getValueSequence(path, m_locale);
627        if (entries == null) {
628            return null;
629        }
630        Map<String, String> result = new LinkedHashMap<>(entries.getElementCount());
631        String prefix = path.endsWith("/") ? path : path.substring(0, path.length() - 1);
632        for (int i = 1; i <= entries.getElementCount(); i++) {
633            String entryPrefix = prefix + "[" + i + "]/";
634            String key = m_xml.getStringValue(null, entryPrefix + XML_ELEMENT_PARAM_NAME, m_locale);
635            String value = m_xml.getStringValue(null, entryPrefix + XML_ELEMENT_PARAM_VALUE, m_locale);
636            result.put(key, value);
637        }
638        return result;
639    }
640
641    /** Helper to read an optional String value.
642     * @param path The XML path of the element to read.
643     * @return The String value stored in the XML, or <code>null</code> if the value could not be read.
644     */
645    protected String parseOptionalStringValue(final String path) {
646
647        final I_CmsXmlContentValue value = m_xml.getValue(path, m_locale);
648        if (value == null) {
649            return null;
650        }
651        return value.getStringValue(null);
652    }
653
654    /** Helper to read an optional String value list.
655     * @param path The XML path of the element to read.
656     * @return The String list stored in the XML, or <code>null</code> if the value could not be read.
657     */
658    protected List<String> parseOptionalStringValues(final String path) {
659
660        final List<I_CmsXmlContentValue> values = m_xml.getValues(path, m_locale);
661        if (values == null) {
662            return null;
663        }
664        List<String> stringValues = new ArrayList<>(values.size());
665        for (I_CmsXmlContentValue value : values) {
666            stringValues.add(value.getStringValue(null));
667        }
668        return stringValues;
669    }
670
671    /** Reads the configuration of a range facet.
672     * @param pathPrefix The XML Path that leads to the range facet configuration, or <code>null</code> if the XML was not correctly structured.
673     * @return The read configuration, or <code>null</code> if the XML was not correctly structured.
674     */
675    protected I_CmsSearchConfigurationFacetRange parseRangeFacet(String pathPrefix) {
676
677        try {
678            final String range = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_RANGE_FACET_RANGE);
679            final String name = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_NAME);
680            final String label = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_LABEL);
681            final Integer minCount = parseOptionalIntValue(pathPrefix + XML_ELEMENT_FACET_MINCOUNT);
682            final String start = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_RANGE_FACET_START);
683            final String end = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_RANGE_FACET_END);
684            final String gap = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_RANGE_FACET_GAP);
685            final String sother = parseOptionalStringValue(pathPrefix + XML_ELEMENT_RANGE_FACET_OTHER);
686            List<I_CmsSearchConfigurationFacetRange.Other> other = null;
687            if (sother != null) {
688                final List<String> sothers = Arrays.asList(sother.split(","));
689                other = new ArrayList<>(sothers.size());
690                for (String so : sothers) {
691                    try {
692                        I_CmsSearchConfigurationFacetRange.Other o = I_CmsSearchConfigurationFacetRange.Other.valueOf(
693                            so);
694                        other.add(o);
695                    } catch (final Exception e) {
696                        LOG.error(Messages.get().getBundle().key(Messages.ERR_INVALID_OTHER_OPTION_1, so), e);
697                    }
698                }
699            }
700            final Boolean hardEnd = parseOptionalBooleanValue(pathPrefix + XML_ELEMENT_RANGE_FACET_HARDEND);
701            final String methodStr = parseOptionalStringValue(pathPrefix + XML_ELEMENT_RANGE_FACET_METHOD);
702            I_CmsSearchConfigurationFacetRange.Method method = null;
703            if (null != methodStr) {
704                try {
705                    method = I_CmsSearchConfigurationFacetRange.Method.valueOf(methodStr);
706                } catch (Exception e) {
707                    LOG.error(Messages.get().getBundle().key(Messages.ERR_INVALID_RANGE_METHOD_OPTION_1, methodStr), e);
708                }
709            }
710            final Boolean isAndFacet = parseOptionalBooleanValue(pathPrefix + XML_ELEMENT_FACET_ISANDFACET);
711            final List<String> preselection = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_PRESELECTION);
712            final Boolean ignoreAllFacetFilters = parseOptionalBooleanValue(
713                pathPrefix + XML_ELEMENT_FACET_IGNOREALLFACETFILTERS);
714            final List<String> excludeTags = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_EXCLUDETAG);
715            return new CmsSearchConfigurationFacetRange(
716                range,
717                start,
718                end,
719                gap,
720                other,
721                hardEnd,
722                method,
723                name,
724                minCount,
725                label,
726                isAndFacet,
727                preselection,
728                ignoreAllFacetFilters,
729                excludeTags);
730        } catch (final Exception e) {
731            LOG.error(
732                Messages.get().getBundle().key(
733                    Messages.ERR_RANGE_FACET_MANDATORY_KEY_MISSING_1,
734                    XML_ELEMENT_RANGE_FACET_RANGE
735                        + ", "
736                        + XML_ELEMENT_RANGE_FACET_START
737                        + ", "
738                        + XML_ELEMENT_RANGE_FACET_END
739                        + ", "
740                        + XML_ELEMENT_RANGE_FACET_GAP),
741                e);
742            return null;
743        }
744
745    }
746
747    /** Returns a map with additional request parameters, mapping the parameter names to Solr query parts.
748     * @return A map with additional request parameters, mapping the parameter names to Solr query parts.
749     */
750    private Map<String, String> getAdditionalRequestParameters() {
751
752        List<I_CmsXmlContentValue> parametersToParse = m_xml.getValues(XML_ELEMENT_ADDITIONAL_PARAMETERS, m_locale);
753        Map<String, String> result = new HashMap<>(parametersToParse.size());
754        for (I_CmsXmlContentValue additionalParam : parametersToParse) {
755            String param = m_xml.getValue(
756                additionalParam.getPath() + "/" + XML_ELEMENT_ADDITIONAL_PARAMETERS_PARAM,
757                m_locale).getStringValue(null);
758            String solrQuery = m_xml.hasValue(
759                additionalParam.getPath() + "/" + XML_ELEMENT_ADDITIONAL_PARAMETERS_SOLRQUERY,
760                m_locale)
761                ? m_xml.getValue(
762                    additionalParam.getPath() + "/" + XML_ELEMENT_ADDITIONAL_PARAMETERS_SOLRQUERY,
763                    m_locale).getStringValue(null)
764                : null;
765            result.put(param, solrQuery);
766        }
767        return result;
768    }
769
770    /** Returns the configured Solr core, or <code>null</code> if the core is not specified.
771     * @return The configured Solr core, or <code>null</code> if the core is not specified.
772     */
773    private String getCore() {
774
775        try {
776            return parseMandatoryStringValue(XML_ELEMENT_CORE);
777        } catch (final Exception e) {
778            LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_CORE_SPECIFIED_0), e);
779            return null;
780        }
781    }
782
783    /** Returns the extra Solr parameters specified in the configuration, or the empty string if no extra parameters are configured.
784     * @return The extra Solr parameters specified in the configuration, or the empty string if no extra parameters are configured.
785     */
786    private String getExtraSolrParams() {
787
788        return parseOptionalStringValue(XML_ELEMENT_EXTRASOLRPARAMS);
789    }
790
791    /** Returns the configured request parameter for the last query, or the default parameter if the core is not specified.
792     * @return The configured request parameter for the last query, or the default parameter if the core is not specified.
793     */
794    private String getFirstCallParam() {
795
796        final String param = parseOptionalStringValue(XML_ELEMENT_RELOADED_PARAM);
797        if (param == null) {
798            return DEFAULT_RELOADED_PARAM;
799        }
800        return param;
801    }
802
803    /** Returns a flag indicating if also expired resources should be found.
804     * @return A flag indicating if also expired resources should be found.
805     */
806    private Boolean getIgnoreExpirationDate() {
807
808        return parseOptionalBooleanValue(XML_ELEMENT_IGNORE_EXPIRATION_DATE);
809    }
810
811    /** Returns a flag, indicating if the query and lastquery parameters should be ignored. E.g., if only the additional parameters should be used for the search.
812     * @return A flag, indicating if the query and lastquery parameters should be ignored.
813     */
814    private Boolean getIgnoreQuery() {
815
816        return parseOptionalBooleanValue(XML_ELEMENT_IGNORE_QUERY);
817    }
818
819    /** Returns a flag indicating if also unreleased resources should be found.
820     * @return A flag indicating if also unreleased resources should be found.
821     */
822    private Boolean getIgnoreReleaseDate() {
823
824        return parseOptionalBooleanValue(XML_ELEMENT_IGNORE_RELEASE_DATE);
825    }
826
827    /** Returns the configured Solr index, or <code>null</code> if the core is not specified.
828     * @param cms the current context.
829     * @return The configured Solr index, or <code>null</code> if the core is not specified.
830     */
831    private String getIndex(CmsObject cms) {
832
833        String indexName = parseOptionalStringValue(XML_ELEMENT_INDEX);
834        if (null != indexName) {
835            indexName = indexName.trim();
836        }
837        return null != indexName
838        ? indexName
839        : (cms.getRequestContext().getCurrentProject().isOnlineProject()
840        ? CmsSolrIndex.DEFAULT_INDEX_NAME_ONLINE
841        : CmsSolrIndex.DEFAULT_INDEX_NAME_OFFLINE);
842    }
843
844    /** Returns the configured request parameter for the last query, or the default parameter if the core is not specified.
845     * @return The configured request parameter for the last query, or the default parameter if the core is not specified.
846     */
847    private String getLastQueryParam() {
848
849        final String param = parseOptionalStringValue(XML_ELEMENT_LAST_QUERYPARAM);
850        if (param == null) {
851            return DEFAULT_LAST_QUERY_PARAM;
852        }
853        return param;
854    }
855
856    /** Returns the configured length of the "Google"-like page navigation, or the default length if it is not configured.
857     * @return The configured length of the "Google"-like page navigation, or the default length if it is not configured.
858     */
859    private Integer getPageNavLength() {
860
861        return parseOptionalIntValue(XML_ELEMENT_PAGENAVLENGTH);
862    }
863
864    /** Returns the configured request parameter for the current page, or the default parameter if the core is not specified.
865     * @return The configured request parameter for the current page, or the default parameter if the core is not specified.
866     */
867    private String getPageParam() {
868
869        return parseOptionalStringValue(XML_ELEMENT_PAGEPARAM);
870    }
871
872    /** Returns the configured page size, or the default page size if it is not configured.
873     * @return The configured page size, or the default page size if it is not configured.
874     */
875    private List<Integer> getPageSizes() {
876
877        final String pageSizes = parseOptionalStringValue(XML_ELEMENT_PAGESIZE);
878        if (pageSizes != null) {
879            String[] pageSizesArray = pageSizes.split("-");
880            if (pageSizesArray.length > 0) {
881                try {
882                    List<Integer> result = new ArrayList<>(pageSizesArray.length);
883                    for (int i = 0; i < pageSizesArray.length; i++) {
884                        result.add(Integer.valueOf(pageSizesArray[i]));
885                    }
886                    return result;
887                } catch (NumberFormatException e) {
888                    LOG.warn(Messages.get().getBundle().key(Messages.LOG_PARSING_PAGE_SIZES_FAILED_1, pageSizes), e);
889                }
890            }
891        }
892        return null;
893    }
894
895    /** Returns the optional query modifier.
896     * @return the optional query modifier.
897     */
898    private String getQueryModifier() {
899
900        return parseOptionalStringValue(XML_ELEMENT_QUERY_MODIFIER);
901    }
902
903    /** Returns the configured request parameter for the current query string, or the default parameter if the core is not specified.
904     * @return The configured request parameter for the current query string, or the default parameter if the core is not specified.
905     */
906    private String getQueryParam() {
907
908        final String param = parseOptionalStringValue(XML_ELEMENT_QUERYPARAM);
909        if (param == null) {
910            return DEFAULT_QUERY_PARAM;
911        }
912        return param;
913    }
914
915    /** Returns a flag, indicating if search should be performed using a wildcard if the empty query is given.
916     * @return A flag, indicating if search should be performed using a wildcard if the empty query is given.
917     */
918    private Boolean getSearchForEmtpyQuery() {
919
920        return parseOptionalBooleanValue(XML_ELEMENT_SEARCH_FOR_EMPTY_QUERY);
921    }
922
923    /** Returns the configured sort options, or the empty list if no such options are configured.
924     * @return The configured sort options, or the empty list if no such options are configured.
925     */
926    private List<I_CmsSearchConfigurationSortOption> getSortOptions() {
927
928        final List<I_CmsSearchConfigurationSortOption> options = new ArrayList<>();
929        final CmsXmlContentValueSequence sortOptions = m_xml.getValueSequence(XML_ELEMENT_SORTOPTIONS, m_locale);
930        if (sortOptions == null) {
931            return null;
932        }
933        for (int i = 0; i < sortOptions.getElementCount(); i++) {
934            final I_CmsSearchConfigurationSortOption option = parseSortOption(sortOptions.getValue(i).getPath() + "/");
935            if (option != null) {
936                options.add(option);
937            }
938        }
939        return options;
940    }
941
942    /** Returns the configured request parameter for the current sort option, or the default parameter if the core is not specified.
943     * @return The configured request parameter for the current sort option, or the default parameter if the core is not specified.
944     */
945    private String getSortParam() {
946
947        return parseOptionalStringValue(XML_ELEMENT_SORTPARAM);
948    }
949
950    /** Parses a single query facet item with query and label.
951     * @param prefix path to the query facet item (with trailing '/').
952     * @return the query facet item.
953     */
954    private I_CmsFacetQueryItem parseFacetQueryItem(final String prefix) {
955
956        I_CmsXmlContentValue query = m_xml.getValue(prefix + XML_ELEMENT_QUERY_FACET_QUERY_QUERY, m_locale);
957        if (null != query) {
958            String queryString = query.getStringValue(null);
959            I_CmsXmlContentValue label = m_xml.getValue(prefix + XML_ELEMENT_QUERY_FACET_QUERY_LABEL, m_locale);
960            String labelString = null != label ? label.getStringValue(null) : null;
961            return new CmsFacetQueryItem(queryString, labelString);
962        }
963        return null;
964    }
965
966    /** Helper to read a mandatory String value.
967     * @param path The XML path of the element to read.
968     * @return The String value stored in the XML.
969     * @throws Exception thrown if the value could not be read.
970     */
971    private String parseMandatoryStringValue(final String path) throws Exception {
972
973        final String value = parseOptionalStringValue(path);
974        if (value == null) {
975            throw new Exception();
976        }
977        return value;
978    }
979
980    /** Returns the configuration of a single sort option, or <code>null</code> if the XML cannot be read.
981     * @param pathPrefix The XML path to the root node of the sort option's configuration.
982     * @return The configuration of a single sort option, or <code>null</code> if the XML cannot be read.
983     */
984    private I_CmsSearchConfigurationSortOption parseSortOption(final String pathPrefix) {
985
986        try {
987            final String solrValue = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_SORTOPTION_SOLRVALUE);
988            String paramValue = parseOptionalStringValue(pathPrefix + XML_ELEMENT_SORTOPTION_PARAMVALUE);
989            paramValue = (paramValue == null) ? solrValue : paramValue;
990            String label = parseOptionalStringValue(pathPrefix + XML_ELEMENT_SORTOPTION_LABEL);
991            label = (label == null) ? paramValue : label;
992            return new CmsSearchConfigurationSortOption(label, paramValue, solrValue);
993        } catch (final Exception e) {
994            LOG.error(
995                Messages.get().getBundle().key(
996                    Messages.ERR_SORT_OPTION_NOT_PARSABLE_1,
997                    XML_ELEMENT_SORTOPTION_SOLRVALUE),
998                e);
999            return null;
1000        }
1001    }
1002}