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