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