001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://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: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://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
193    /** XML element names for "Did you mean ...?". */
194    /** XML element name. */
195    private static final String XML_ELEMENT_DIDYOUMEAN = "DidYouMean";
196    /** XML elements for the "Did you mean ...?" configuration. */
197    /** XML element name. */
198    private static final String XML_ELEMENT_DIDYOUMEAN_QUERYPARAM = "QueryParam";
199    /** XML element name. */
200    private static final String XML_ELEMENT_DIDYOUMEAN_COLLATE = "Collate";
201    /** XML element name. */
202    private static final String XML_ELEMENT_DIDYOUMEAN_COUNT = "Count";
203    /** XML element name. */
204    private static final String XML_ELEMENT_RANGE_FACETS = "RangeFacet";
205    /** XML element name. */
206    private static final String XML_ELEMENT_RANGE_FACET_RANGE = "Range";
207    /** XML element name. */
208    private static final String XML_ELEMENT_RANGE_FACET_START = "Start";
209    /** XML element name. */
210    private static final String XML_ELEMENT_RANGE_FACET_END = "End";
211    /** XML element name. */
212    private static final String XML_ELEMENT_RANGE_FACET_GAP = "Gap";
213    /** XML element name. */
214    private static final String XML_ELEMENT_RANGE_FACET_OTHER = "Other";
215    /** XML element name. */
216    private static final String XML_ELEMENT_RANGE_FACET_HARDEND = "HardEnd";
217
218    /** Default value. */
219    private static final String DEFAULT_QUERY_PARAM = "q";
220    /** Default value. */
221    private static final String DEFAULT_LAST_QUERY_PARAM = "lq";
222    /** Default value. */
223    private static final String DEFAULT_RELOADED_PARAM = "reloaded";
224
225    /** The XML content that contains the configuration. */
226    CmsXmlContent m_xml;
227    /** The locale in which the configuration should be read. */
228    Locale m_locale;
229
230    /** Constructor taking the XML content that should be read and the locale in which it should be read.
231     * @param xml The XML content that should be read for the configuration.
232     * @param locale The locale in which the content should be read.
233     */
234    public CmsXMLSearchConfigurationParser(final CmsXmlContent xml, final Locale locale) {
235
236        m_xml = xml;
237        m_locale = locale;
238    }
239
240    /**
241     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseCommon(CmsObject)
242     */
243    @Override
244    public I_CmsSearchConfigurationCommon parseCommon(CmsObject cms) {
245
246        String indexName = getIndex(cms);
247
248        return new CmsSearchConfigurationCommon(
249            getQueryParam(),
250            getLastQueryParam(),
251            parseOptionalBooleanValue(XML_ELEMENT_ESCAPE_QUERY_CHARACTERS),
252            getFirstCallParam(),
253            getSearchForEmtpyQuery(),
254            getIgnoreQuery(),
255            getQueryModifier(),
256            indexName,
257            getCore(),
258            getExtraSolrParams(),
259            getAdditionalRequestParameters(),
260            getIgnoreReleaseDate(),
261            getIgnoreExpirationDate(),
262            getMaxReturnedResults(indexName));
263    }
264
265    /**
266     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseDidYouMean()
267     */
268    public I_CmsSearchConfigurationDidYouMean parseDidYouMean() {
269
270        final I_CmsXmlContentValue didYouMean = m_xml.getValue(XML_ELEMENT_DIDYOUMEAN, m_locale);
271        if (didYouMean == null) {
272            return null;
273        } else {
274            final String pathPrefix = didYouMean.getPath() + "/";
275            String param = parseOptionalStringValue(pathPrefix + XML_ELEMENT_DIDYOUMEAN_QUERYPARAM);
276            if (null == param) {
277                param = getQueryParam();
278            }
279            Boolean collate = parseOptionalBooleanValue(pathPrefix + XML_ELEMENT_DIDYOUMEAN_COLLATE);
280            Integer count = parseOptionalIntValue(pathPrefix + XML_ELEMENT_DIDYOUMEAN_COUNT);
281            return new CmsSearchConfigurationDidYouMean(param, collate, count);
282        }
283    }
284
285    /**
286     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseFieldFacets()
287     */
288    @Override
289    public Map<String, I_CmsSearchConfigurationFacetField> parseFieldFacets() {
290
291        final Map<String, I_CmsSearchConfigurationFacetField> facetConfigs = new LinkedHashMap<String, I_CmsSearchConfigurationFacetField>();
292        final CmsXmlContentValueSequence fieldFacets = m_xml.getValueSequence(XML_ELEMENT_FIELD_FACETS, m_locale);
293        if (fieldFacets != null) {
294            for (int i = 0; i < fieldFacets.getElementCount(); i++) {
295
296                final I_CmsSearchConfigurationFacetField config = parseFieldFacet(
297                    fieldFacets.getValue(i).getPath() + "/");
298                if (config != null) {
299                    facetConfigs.put(config.getName(), config);
300                }
301            }
302        }
303        return facetConfigs;
304    }
305
306    /**
307     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseGeoFilter()
308     */
309    @Override
310    public I_CmsSearchConfigurationGeoFilter parseGeoFilter() {
311
312        return null;
313    }
314
315    /**
316     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseHighlighter()
317     */
318    @Override
319    public I_CmsSearchConfigurationHighlighting parseHighlighter() {
320
321        final I_CmsXmlContentValue highlighter = m_xml.getValue(XML_ELEMENT_HIGHLIGHTER, m_locale);
322        if (highlighter == null) {
323            return null;
324        } else {
325            try {
326                final String pathPrefix = highlighter.getPath() + "/";
327                final String field = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_FIELD);
328                final Integer snippets = parseOptionalIntValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_SNIPPETS);
329                final Integer fragsize = parseOptionalIntValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_FRAGSIZE);
330                final String alternateField = parseOptionalStringValue(
331                    pathPrefix + XML_ELEMENT_HIGHLIGHTER_ALTERNATE_FIELD);
332                final Integer maxAlternateFieldLength = parseOptionalIntValue(
333                    pathPrefix + XML_ELEMENT_HIGHLIGHTER_MAX_LENGTH_ALTERNATE_FIELD);
334                final String pre = parseOptionalStringValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_SIMPLE_PRE);
335                final String post = parseOptionalStringValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_SIMPLE_POST);
336                final String formatter = parseOptionalStringValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_FORMATTER);
337                final String fragmenter = parseOptionalStringValue(pathPrefix + XML_ELEMENT_HIGHLIGHTER_FRAGMENTER);
338                final Boolean useFastVectorHighlighting = parseOptionalBooleanValue(
339                    pathPrefix + XML_ELEMENT_HIGHLIGHTER_FASTVECTORHIGHLIGHTING);
340                return new CmsSearchConfigurationHighlighting(
341                    field,
342                    snippets,
343                    fragsize,
344                    alternateField,
345                    maxAlternateFieldLength,
346                    pre,
347                    post,
348                    formatter,
349                    fragmenter,
350                    useFastVectorHighlighting);
351            } catch (final Exception e) {
352                LOG.error(Messages.get().getBundle().key(Messages.ERR_MANDATORY_HIGHLIGHTING_FIELD_MISSING_0), e);
353                return null;
354            }
355        }
356    }
357
358    /**
359     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parsePagination()
360     */
361    @Override
362    public I_CmsSearchConfigurationPagination parsePagination() {
363
364        return CmsSearchConfigurationPagination.create(getPageParam(), getPageSizes(), getPageNavLength());
365    }
366
367    /**
368     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseQueryFacet()
369     */
370    @Override
371    public I_CmsSearchConfigurationFacetQuery parseQueryFacet() {
372
373        final String pathPrefix = XML_ELEMENT_QUERY_FACET + "/";
374        try {
375            final List<I_CmsFacetQueryItem> queries = parseFacetQueryItems(pathPrefix + XML_ELEMENT_QUERY_FACET_QUERY);
376            final String label = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_LABEL);
377            final Boolean isAndFacet = parseOptionalBooleanValue(pathPrefix + XML_ELEMENT_FACET_ISANDFACET);
378            final List<String> preselection = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_PRESELECTION);
379            final List<String> excludeTags = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_EXCLUDETAG);
380            final Boolean ignoreAllFacetFilters = parseOptionalBooleanValue(
381                pathPrefix + XML_ELEMENT_FACET_IGNOREALLFACETFILTERS);
382            return new CmsSearchConfigurationFacetQuery(
383                queries,
384                label,
385                isAndFacet,
386                preselection,
387                ignoreAllFacetFilters,
388                excludeTags);
389        } catch (final Exception e) {
390            LOG.error(
391                Messages.get().getBundle().key(
392                    Messages.ERR_QUERY_FACET_MANDATORY_KEY_MISSING_1,
393                    XML_ELEMENT_QUERY_FACET_QUERY),
394                e);
395            return null;
396        }
397    }
398
399    /**
400     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseRangeFacets()
401     */
402    public Map<String, I_CmsSearchConfigurationFacetRange> parseRangeFacets() {
403
404        final Map<String, I_CmsSearchConfigurationFacetRange> facetConfigs = new LinkedHashMap<String, I_CmsSearchConfigurationFacetRange>();
405        final CmsXmlContentValueSequence rangeFacets = m_xml.getValueSequence(XML_ELEMENT_RANGE_FACETS, m_locale);
406        if (rangeFacets != null) {
407            for (int i = 0; i < rangeFacets.getElementCount(); i++) {
408
409                final I_CmsSearchConfigurationFacetRange config = parseRangeFacet(
410                    rangeFacets.getValue(i).getPath() + "/");
411                if (config != null) {
412                    facetConfigs.put(config.getName(), config);
413                }
414            }
415        }
416        return facetConfigs;
417    }
418
419    /**
420     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseSorting()
421     */
422    @Override
423    public I_CmsSearchConfigurationSorting parseSorting() {
424
425        List<I_CmsSearchConfigurationSortOption> options = getSortOptions();
426        String defaultOptionParamValue = parseOptionalStringValue(XML_ELEMENT_DEFAULTSORTOPTION);
427        I_CmsSearchConfigurationSortOption defaultSortOption = null;
428        if (null != defaultOptionParamValue) {
429            Iterator<I_CmsSearchConfigurationSortOption> optIterator = options.iterator();
430            while ((null == defaultSortOption) && optIterator.hasNext()) {
431                I_CmsSearchConfigurationSortOption opt = optIterator.next();
432                if (Objects.equals(opt.getParamValue(), defaultOptionParamValue)) {
433                    defaultSortOption = opt;
434                }
435            }
436        }
437        if ((null == defaultSortOption) && !options.isEmpty()) {
438            defaultSortOption = options.get(0);
439        }
440        return CmsSearchConfigurationSorting.create(getSortParam(), options, defaultSortOption);
441    }
442
443    /** Returns the number of maximally returned results, or <code>null</code> if the indexes default should be used.
444     * @param indexName the name of the index to search in.
445     * @return The number of maximally returned results, or <code>null</code> if the indexes default should be used.
446     */
447    protected int getMaxReturnedResults(String indexName) {
448
449        Integer maxReturnedResults = parseOptionalIntValue(XML_ELEMENT_MAX_RETURNED_RESULTS);
450        if (null != maxReturnedResults) {
451            return maxReturnedResults.intValue();
452        }
453        try {
454            CmsSolrIndex idx = OpenCms.getSearchManager().getIndexSolr(indexName);
455            if (null != idx) {
456                return idx.getMaxProcessedResults();
457            }
458        } catch (Throwable t) {
459            // This is ok, it's allowed to have an external other index here.
460            LOG.debug(
461                "Parsing JSON search configuration for none-CmsSolrIndex "
462                    + indexName
463                    + ". Setting max processed results to unlimited.");
464        }
465        return CmsSolrIndex.MAX_RESULTS_UNLIMITED;
466    }
467
468    /** Helper to read a mandatory String value list.
469     * @param path The XML path of the element to read.
470     * @return The String list stored in the XML, or <code>null</code> if the value could not be read.
471     * @throws Exception thrown if the list of String values can not be read.
472     */
473    protected List<I_CmsFacetQueryItem> parseFacetQueryItems(final String path) throws Exception {
474
475        final List<I_CmsXmlContentValue> values = m_xml.getValues(path, m_locale);
476        if (values == null) {
477            return null;
478        } else {
479            List<I_CmsFacetQueryItem> parsedItems = new ArrayList<I_CmsFacetQueryItem>(values.size());
480            for (I_CmsXmlContentValue value : values) {
481                I_CmsFacetQueryItem item = parseFacetQueryItem(value.getPath() + "/");
482                if (null != item) {
483                    parsedItems.add(item);
484                } else {
485                    // TODO: log
486                }
487            }
488            return parsedItems;
489        }
490    }
491
492    /** Reads the configuration of a field facet.
493     * @param pathPrefix The XML Path that leads to the field facet configuration, or <code>null</code> if the XML was not correctly structured.
494     * @return The read configuration, or <code>null</code> if the XML was not correctly structured.
495     */
496    protected I_CmsSearchConfigurationFacetField parseFieldFacet(final String pathPrefix) {
497
498        try {
499            final String field = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_FACET_FIELD);
500            final String name = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_NAME);
501            final String label = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_LABEL);
502            final Integer minCount = parseOptionalIntValue(pathPrefix + XML_ELEMENT_FACET_MINCOUNT);
503            final Integer limit = parseOptionalIntValue(pathPrefix + XML_ELEMENT_FACET_LIMIT);
504            final String prefix = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_PREFIX);
505            final String sorder = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_ORDER);
506            I_CmsSearchConfigurationFacet.SortOrder order;
507            try {
508                order = I_CmsSearchConfigurationFacet.SortOrder.valueOf(sorder);
509            } catch (@SuppressWarnings("unused") final Exception e) {
510                order = null;
511            }
512            final String filterQueryModifier = parseOptionalStringValue(
513                pathPrefix + XML_ELEMENT_FACET_FILTERQUERYMODIFIER);
514            final Boolean isAndFacet = parseOptionalBooleanValue(pathPrefix + XML_ELEMENT_FACET_ISANDFACET);
515            final List<String> preselection = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_PRESELECTION);
516            final Boolean ignoreAllFacetFilters = parseOptionalBooleanValue(
517                pathPrefix + XML_ELEMENT_FACET_IGNOREALLFACETFILTERS);
518            final List<String> excludeTags = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_EXCLUDETAG);
519            return new CmsSearchConfigurationFacetField(
520                field,
521                name,
522                minCount,
523                limit,
524                prefix,
525                label,
526                order,
527                filterQueryModifier,
528                isAndFacet,
529                preselection,
530                ignoreAllFacetFilters,
531                excludeTags);
532        } catch (final Exception e) {
533            LOG.error(
534                Messages.get().getBundle().key(
535                    Messages.ERR_FIELD_FACET_MANDATORY_KEY_MISSING_1,
536                    XML_ELEMENT_FACET_FIELD),
537                e);
538            return null;
539        }
540    }
541
542    /** Helper to read an optional Boolean value.
543     * @param path The XML path of the element to read.
544     * @return The Boolean value stored in the XML, or <code>null</code> if the value could not be read.
545     */
546    protected Boolean parseOptionalBooleanValue(final String path) {
547
548        final I_CmsXmlContentValue value = m_xml.getValue(path, m_locale);
549        if (value == null) {
550            return null;
551        } else {
552            final String stringValue = value.getStringValue(null);
553            try {
554                final Boolean boolValue = Boolean.valueOf(stringValue);
555                return boolValue;
556            } catch (final NumberFormatException e) {
557                LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_BOOLEAN_MISSING_1, path), e);
558                return null;
559            }
560        }
561    }
562
563    /** Helper to read an optional Integer value.
564     * @param path The XML path of the element to read.
565     * @return The Integer value stored in the XML, or <code>null</code> if the value could not be read.
566     */
567    protected Integer parseOptionalIntValue(final String path) {
568
569        final I_CmsXmlContentValue value = m_xml.getValue(path, m_locale);
570        if (value == null) {
571            return null;
572        } else {
573            final String stringValue = value.getStringValue(null);
574            try {
575                final Integer intValue = Integer.valueOf(stringValue);
576                return intValue;
577            } catch (final NumberFormatException e) {
578                LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_INTEGER_MISSING_1, path), e);
579                return null;
580            }
581        }
582    }
583
584    /** Helper to read an optional String value.
585     * @param path The XML path of the element to read.
586     * @return The String value stored in the XML, or <code>null</code> if the value could not be read.
587     */
588    protected String parseOptionalStringValue(final String path) {
589
590        final I_CmsXmlContentValue value = m_xml.getValue(path, m_locale);
591        if (value == null) {
592            return null;
593        } else {
594            return value.getStringValue(null);
595        }
596    }
597
598    /** Helper to read an optional String value list.
599     * @param path The XML path of the element to read.
600     * @return The String list stored in the XML, or <code>null</code> if the value could not be read.
601     */
602    protected List<String> parseOptionalStringValues(final String path) {
603
604        final List<I_CmsXmlContentValue> values = m_xml.getValues(path, m_locale);
605        if (values == null) {
606            return null;
607        } else {
608            List<String> stringValues = new ArrayList<String>(values.size());
609            for (I_CmsXmlContentValue value : values) {
610                stringValues.add(value.getStringValue(null));
611            }
612            return stringValues;
613        }
614    }
615
616    /** Reads the configuration of a range facet.
617     * @param pathPrefix The XML Path that leads to the range facet configuration, or <code>null</code> if the XML was not correctly structured.
618     * @return The read configuration, or <code>null</code> if the XML was not correctly structured.
619     */
620    protected I_CmsSearchConfigurationFacetRange parseRangeFacet(String pathPrefix) {
621
622        try {
623            final String range = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_RANGE_FACET_RANGE);
624            final String name = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_NAME);
625            final String label = parseOptionalStringValue(pathPrefix + XML_ELEMENT_FACET_LABEL);
626            final Integer minCount = parseOptionalIntValue(pathPrefix + XML_ELEMENT_FACET_MINCOUNT);
627            final String start = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_RANGE_FACET_START);
628            final String end = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_RANGE_FACET_END);
629            final String gap = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_RANGE_FACET_GAP);
630            final String sother = parseOptionalStringValue(pathPrefix + XML_ELEMENT_RANGE_FACET_OTHER);
631            List<I_CmsSearchConfigurationFacetRange.Other> other = null;
632            if (sother != null) {
633                final List<String> sothers = Arrays.asList(sother.split(","));
634                other = new ArrayList<I_CmsSearchConfigurationFacetRange.Other>(sothers.size());
635                for (String so : sothers) {
636                    try {
637                        I_CmsSearchConfigurationFacetRange.Other o = I_CmsSearchConfigurationFacetRange.Other.valueOf(
638                            so);
639                        other.add(o);
640                    } catch (final Exception e) {
641                        LOG.error(Messages.get().getBundle().key(Messages.ERR_INVALID_OTHER_OPTION_1, so), e);
642                    }
643                }
644            }
645            final Boolean hardEnd = parseOptionalBooleanValue(pathPrefix + XML_ELEMENT_RANGE_FACET_HARDEND);
646            final Boolean isAndFacet = parseOptionalBooleanValue(pathPrefix + XML_ELEMENT_FACET_ISANDFACET);
647            final List<String> preselection = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_PRESELECTION);
648            final Boolean ignoreAllFacetFilters = parseOptionalBooleanValue(
649                pathPrefix + XML_ELEMENT_FACET_IGNOREALLFACETFILTERS);
650            final List<String> excludeTags = parseOptionalStringValues(pathPrefix + XML_ELEMENT_FACET_EXCLUDETAG);
651            return new CmsSearchConfigurationFacetRange(
652                range,
653                start,
654                end,
655                gap,
656                other,
657                hardEnd,
658                name,
659                minCount,
660                label,
661                isAndFacet,
662                preselection,
663                ignoreAllFacetFilters,
664                excludeTags);
665        } catch (final Exception e) {
666            LOG.error(
667                Messages.get().getBundle().key(
668                    Messages.ERR_RANGE_FACET_MANDATORY_KEY_MISSING_1,
669                    XML_ELEMENT_RANGE_FACET_RANGE
670                        + ", "
671                        + XML_ELEMENT_RANGE_FACET_START
672                        + ", "
673                        + XML_ELEMENT_RANGE_FACET_END
674                        + ", "
675                        + XML_ELEMENT_RANGE_FACET_GAP),
676                e);
677            return null;
678        }
679
680    }
681
682    /** Returns a map with additional request parameters, mapping the parameter names to Solr query parts.
683     * @return A map with additional request parameters, mapping the parameter names to Solr query parts.
684     */
685    private Map<String, String> getAdditionalRequestParameters() {
686
687        List<I_CmsXmlContentValue> parametersToParse = m_xml.getValues(XML_ELEMENT_ADDITIONAL_PARAMETERS, m_locale);
688        Map<String, String> result = new HashMap<String, String>(parametersToParse.size());
689        for (I_CmsXmlContentValue additionalParam : parametersToParse) {
690            String param = m_xml.getValue(
691                additionalParam.getPath() + "/" + XML_ELEMENT_ADDITIONAL_PARAMETERS_PARAM,
692                m_locale).getStringValue(null);
693            String solrQuery = m_xml.hasValue(
694                additionalParam.getPath() + "/" + XML_ELEMENT_ADDITIONAL_PARAMETERS_SOLRQUERY,
695                m_locale)
696                ? m_xml.getValue(
697                    additionalParam.getPath() + "/" + XML_ELEMENT_ADDITIONAL_PARAMETERS_SOLRQUERY,
698                    m_locale).getStringValue(null)
699                : null;
700            result.put(param, solrQuery);
701        }
702        return result;
703    }
704
705    /** Returns the configured Solr core, or <code>null</code> if the core is not specified.
706     * @return The configured Solr core, or <code>null</code> if the core is not specified.
707     */
708    private String getCore() {
709
710        try {
711            return parseMandatoryStringValue(XML_ELEMENT_CORE);
712        } catch (final Exception e) {
713            LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_CORE_SPECIFIED_0), e);
714            return null;
715        }
716    }
717
718    /** Returns the extra Solr parameters specified in the configuration, or the empty string if no extra parameters are configured.
719     * @return The extra Solr parameters specified in the configuration, or the empty string if no extra parameters are configured.
720     */
721    private String getExtraSolrParams() {
722
723        return parseOptionalStringValue(XML_ELEMENT_EXTRASOLRPARAMS);
724    }
725
726    /** Returns the configured request parameter for the last query, or the default parameter if the core is not specified.
727     * @return The configured request parameter for the last query, or the default parameter if the core is not specified.
728     */
729    private String getFirstCallParam() {
730
731        final String param = parseOptionalStringValue(XML_ELEMENT_RELOADED_PARAM);
732        if (param == null) {
733            return DEFAULT_RELOADED_PARAM;
734        } else {
735            return param;
736        }
737    }
738
739    /** Returns a flag indicating if also expired resources should be found.
740     * @return A flag indicating if also expired resources should be found.
741     */
742    private Boolean getIgnoreExpirationDate() {
743
744        return parseOptionalBooleanValue(XML_ELEMENT_IGNORE_EXPIRATION_DATE);
745    }
746
747    /** 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.
748     * @return A flag, indicating if the query and lastquery parameters should be ignored.
749     */
750    private Boolean getIgnoreQuery() {
751
752        return parseOptionalBooleanValue(XML_ELEMENT_IGNORE_QUERY);
753    }
754
755    /** Returns a flag indicating if also unreleased resources should be found.
756     * @return A flag indicating if also unreleased resources should be found.
757     */
758    private Boolean getIgnoreReleaseDate() {
759
760        return parseOptionalBooleanValue(XML_ELEMENT_IGNORE_RELEASE_DATE);
761    }
762
763    /** Returns the configured Solr index, or <code>null</code> if the core is not specified.
764     * @param cms the current context.
765     * @return The configured Solr index, or <code>null</code> if the core is not specified.
766     */
767    private String getIndex(CmsObject cms) {
768
769        String indexName = parseOptionalStringValue(XML_ELEMENT_INDEX);
770        if (null != indexName) {
771            indexName = indexName.trim();
772        }
773        return null != indexName
774        ? indexName
775        : (cms.getRequestContext().getCurrentProject().isOnlineProject()
776        ? CmsSolrIndex.DEFAULT_INDEX_NAME_ONLINE
777        : CmsSolrIndex.DEFAULT_INDEX_NAME_OFFLINE);
778    }
779
780    /** Returns the configured request parameter for the last query, or the default parameter if the core is not specified.
781     * @return The configured request parameter for the last query, or the default parameter if the core is not specified.
782     */
783    private String getLastQueryParam() {
784
785        final String param = parseOptionalStringValue(XML_ELEMENT_LAST_QUERYPARAM);
786        if (param == null) {
787            return DEFAULT_LAST_QUERY_PARAM;
788        } else {
789            return param;
790        }
791    }
792
793    /** Returns the configured length of the "Google"-like page navigation, or the default length if it is not configured.
794     * @return The configured length of the "Google"-like page navigation, or the default length if it is not configured.
795     */
796    private Integer getPageNavLength() {
797
798        return parseOptionalIntValue(XML_ELEMENT_PAGENAVLENGTH);
799    }
800
801    /** Returns the configured request parameter for the current page, or the default parameter if the core is not specified.
802     * @return The configured request parameter for the current page, or the default parameter if the core is not specified.
803     */
804    private String getPageParam() {
805
806        return parseOptionalStringValue(XML_ELEMENT_PAGEPARAM);
807    }
808
809    /** Returns the configured page size, or the default page size if it is not configured.
810     * @return The configured page size, or the default page size if it is not configured.
811     */
812    private List<Integer> getPageSizes() {
813
814        final String pageSizes = parseOptionalStringValue(XML_ELEMENT_PAGESIZE);
815        if (pageSizes != null) {
816            String[] pageSizesArray = pageSizes.split("-");
817            if (pageSizesArray.length > 0) {
818                try {
819                    List<Integer> result = new ArrayList<>(pageSizesArray.length);
820                    for (int i = 0; i < pageSizesArray.length; i++) {
821                        result.add(Integer.valueOf(pageSizesArray[i]));
822                    }
823                    return result;
824                } catch (NumberFormatException e) {
825                    LOG.warn(Messages.get().getBundle().key(Messages.LOG_PARSING_PAGE_SIZES_FAILED_1, pageSizes), e);
826                }
827            }
828        }
829        return null;
830    }
831
832    /** Returns the optional query modifier.
833     * @return the optional query modifier.
834     */
835    private String getQueryModifier() {
836
837        return parseOptionalStringValue(XML_ELEMENT_QUERY_MODIFIER);
838    }
839
840    /** Returns the configured request parameter for the current query string, or the default parameter if the core is not specified.
841     * @return The configured request parameter for the current query string, or the default parameter if the core is not specified.
842     */
843    private String getQueryParam() {
844
845        final String param = parseOptionalStringValue(XML_ELEMENT_QUERYPARAM);
846        if (param == null) {
847            return DEFAULT_QUERY_PARAM;
848        } else {
849            return param;
850        }
851    }
852
853    /** Returns a flag, indicating if search should be performed using a wildcard if the empty query is given.
854     * @return A flag, indicating if search should be performed using a wildcard if the empty query is given.
855     */
856    private Boolean getSearchForEmtpyQuery() {
857
858        return parseOptionalBooleanValue(XML_ELEMENT_SEARCH_FOR_EMPTY_QUERY);
859    }
860
861    /** Returns the configured sort options, or the empty list if no such options are configured.
862     * @return The configured sort options, or the empty list if no such options are configured.
863     */
864    private List<I_CmsSearchConfigurationSortOption> getSortOptions() {
865
866        final List<I_CmsSearchConfigurationSortOption> options = new ArrayList<I_CmsSearchConfigurationSortOption>();
867        final CmsXmlContentValueSequence sortOptions = m_xml.getValueSequence(XML_ELEMENT_SORTOPTIONS, m_locale);
868        if (sortOptions == null) {
869            return null;
870        } else {
871            for (int i = 0; i < sortOptions.getElementCount(); i++) {
872                final I_CmsSearchConfigurationSortOption option = parseSortOption(
873                    sortOptions.getValue(i).getPath() + "/");
874                if (option != null) {
875                    options.add(option);
876                }
877            }
878            return options;
879        }
880    }
881
882    /** Returns the configured request parameter for the current sort option, or the default parameter if the core is not specified.
883     * @return The configured request parameter for the current sort option, or the default parameter if the core is not specified.
884     */
885    private String getSortParam() {
886
887        return parseOptionalStringValue(XML_ELEMENT_SORTPARAM);
888    }
889
890    /** Parses a single query facet item with query and label.
891     * @param prefix path to the query facet item (with trailing '/').
892     * @return the query facet item.
893     */
894    private I_CmsFacetQueryItem parseFacetQueryItem(final String prefix) {
895
896        I_CmsXmlContentValue query = m_xml.getValue(prefix + XML_ELEMENT_QUERY_FACET_QUERY_QUERY, m_locale);
897        if (null != query) {
898            String queryString = query.getStringValue(null);
899            I_CmsXmlContentValue label = m_xml.getValue(prefix + XML_ELEMENT_QUERY_FACET_QUERY_LABEL, m_locale);
900            String labelString = null != label ? label.getStringValue(null) : null;
901            return new CmsFacetQueryItem(queryString, labelString);
902        } else {
903            return null;
904        }
905    }
906
907    /** Helper to read a mandatory String value.
908     * @param path The XML path of the element to read.
909     * @return The String value stored in the XML.
910     * @throws Exception thrown if the value could not be read.
911     */
912    private String parseMandatoryStringValue(final String path) throws Exception {
913
914        final String value = parseOptionalStringValue(path);
915        if (value == null) {
916            throw new Exception();
917        }
918        return value;
919    }
920
921    /** Returns the configuration of a single sort option, or <code>null</code> if the XML cannot be read.
922     * @param pathPrefix The XML path to the root node of the sort option's configuration.
923     * @return The configuration of a single sort option, or <code>null</code> if the XML cannot be read.
924     */
925    private I_CmsSearchConfigurationSortOption parseSortOption(final String pathPrefix) {
926
927        try {
928            final String solrValue = parseMandatoryStringValue(pathPrefix + XML_ELEMENT_SORTOPTION_SOLRVALUE);
929            String paramValue = parseOptionalStringValue(pathPrefix + XML_ELEMENT_SORTOPTION_PARAMVALUE);
930            paramValue = (paramValue == null) ? solrValue : paramValue;
931            String label = parseOptionalStringValue(pathPrefix + XML_ELEMENT_SORTOPTION_LABEL);
932            label = (label == null) ? paramValue : label;
933            return new CmsSearchConfigurationSortOption(label, paramValue, solrValue);
934        } catch (final Exception e) {
935            LOG.error(
936                Messages.get().getBundle().key(
937                    Messages.ERR_SORT_OPTION_NOT_PARSABLE_1,
938                    XML_ELEMENT_SORTOPTION_SOLRVALUE),
939                e);
940            return null;
941        }
942    }
943}