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.json.JSONArray;
032import org.opencms.json.JSONException;
033import org.opencms.json.JSONObject;
034import org.opencms.jsp.search.config.CmsSearchConfigurationCommon;
035import org.opencms.jsp.search.config.CmsSearchConfigurationDidYouMean;
036import org.opencms.jsp.search.config.CmsSearchConfigurationFacetField;
037import org.opencms.jsp.search.config.CmsSearchConfigurationFacetQuery;
038import org.opencms.jsp.search.config.CmsSearchConfigurationFacetQuery.CmsFacetQueryItem;
039import org.opencms.jsp.search.config.CmsSearchConfigurationFacetRange;
040import org.opencms.jsp.search.config.CmsSearchConfigurationGeoFilter;
041import org.opencms.jsp.search.config.CmsSearchConfigurationHighlighting;
042import org.opencms.jsp.search.config.CmsSearchConfigurationPagination;
043import org.opencms.jsp.search.config.CmsSearchConfigurationSortOption;
044import org.opencms.jsp.search.config.CmsSearchConfigurationSorting;
045import org.opencms.jsp.search.config.I_CmsSearchConfiguration;
046import org.opencms.jsp.search.config.I_CmsSearchConfigurationCommon;
047import org.opencms.jsp.search.config.I_CmsSearchConfigurationDidYouMean;
048import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacet;
049import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetField;
050import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetQuery;
051import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetQuery.I_CmsFacetQueryItem;
052import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetRange;
053import org.opencms.jsp.search.config.I_CmsSearchConfigurationGeoFilter;
054import org.opencms.jsp.search.config.I_CmsSearchConfigurationHighlighting;
055import org.opencms.jsp.search.config.I_CmsSearchConfigurationPagination;
056import org.opencms.jsp.search.config.I_CmsSearchConfigurationSortOption;
057import org.opencms.jsp.search.config.I_CmsSearchConfigurationSorting;
058import org.opencms.main.CmsLog;
059import org.opencms.main.OpenCms;
060import org.opencms.search.solr.CmsSolrIndex;
061
062import java.util.ArrayList;
063import java.util.Collections;
064import java.util.HashMap;
065import java.util.Iterator;
066import java.util.LinkedHashMap;
067import java.util.LinkedList;
068import java.util.List;
069import java.util.Map;
070import java.util.Objects;
071
072import org.apache.commons.logging.Log;
073
074/** Search configuration parser reading JSON. */
075public class CmsJSONSearchConfigurationParser implements I_CmsSearchConfigurationParser {
076
077    /** Logger for the class. */
078    protected static final Log LOG = CmsLog.getLog(CmsJSONSearchConfigurationParser.class);
079
080    /** The keys that can be used in the JSON object */
081    /** JSON keys for common options. */
082    /** A JSON key. */
083    public static final String JSON_KEY_QUERYPARAM = "queryparam";
084    /** A JSON key. */
085    public static final String JSON_KEY_LAST_QUERYPARAM = "lastqueryparam";
086    /** A JSON key. */
087    public static final String JSON_KEY_ESCAPE_QUERY_CHARACTERS = "escapequerychars";
088    /** A JSON key. */
089    public static final String JSON_KEY_RELOADED_PARAM = "reloadedparam";
090    /** A JSON key. */
091    public static final String JSON_KEY_SEARCH_FOR_EMPTY_QUERY = "searchforemptyquery";
092    /** A JSON key. */
093    public static final String JSON_KEY_IGNORE_QUERY = "ignorequery";
094    /** A JSON key. */
095    public static final String JSON_KEY_IGNORE_RELEASE_DATE = "ignoreReleaseDate";
096    /** A JSON key. */
097    public static final String JSON_KEY_MAX_RETURNED_RESULTS = "maxReturnedResults";
098    /** A JSON key. */
099    public static final String JSON_KEY_IGNORE_EXPIRATION_DATE = "ignoreExpirationDate";
100    /** A JSON key. */
101    public static final String JSON_KEY_QUERY_MODIFIER = "querymodifier";
102    /** A JSON key. */
103    public static final String JSON_KEY_PAGEPARAM = "pageparam";
104    /** A JSON key. */
105    public static final String JSON_KEY_INDEX = "index";
106    /** A JSON key. */
107    public static final String JSON_KEY_CORE = "core";
108    /** A JSON key. */
109    public static final String JSON_KEY_EXTRASOLRPARAMS = "extrasolrparams";
110    /** A JSON key. */
111    public static final String JSON_KEY_ADDITIONAL_PARAMETERS = "additionalrequestparams";
112    /** A JSON key. */
113    public static final String JSON_KEY_ADDITIONAL_PARAMETERS_PARAM = "param";
114    /** A JSON key. */
115    public static final String JSON_KEY_ADDITIONAL_PARAMETERS_SOLRQUERY = "solrquery";
116    /** A JSON key. */
117    public static final String JSON_KEY_PAGESIZE = "pagesize";
118    /** A JSON key. */
119    public static final String JSON_KEY_PAGENAVLENGTH = "pagenavlength";
120    /** JSON keys for facet configuration. */
121    /** The JSON key for the sub-node with all field facet configurations. */
122    public static final String JSON_KEY_FIELD_FACETS = "fieldfacets";
123    /** The JSON key for the sub-node with all field facet configurations. */
124    public static final String JSON_KEY_RANGE_FACETS = "rangefacets";
125    /** The JSON key for the sub-node with the query facet configuration. */
126    public static final String JSON_KEY_QUERY_FACET = "queryfacet";
127    /** JSON keys for a single facet. */
128    /** A JSON key. */
129    public static final String JSON_KEY_FACET_LIMIT = "limit";
130    /** A JSON key. */
131    public static final String JSON_KEY_FACET_MINCOUNT = "mincount";
132    /** A JSON key. */
133    public static final String JSON_KEY_FACET_LABEL = "label";
134    /** A JSON key. */
135    public static final String JSON_KEY_FACET_FIELD = "field";
136    /** A JSON key. */
137    public static final String JSON_KEY_FACET_NAME = "name";
138    /** A JSON key. */
139    public static final String JSON_KEY_FACET_PREFIX = "prefix";
140    /** A JSON key. */
141    public static final String JSON_KEY_FACET_ORDER = "order";
142    /** A JSON key. */
143    public static final String JSON_KEY_FACET_FILTERQUERYMODIFIER = "filterquerymodifier";
144    /** A JSON key. */
145    public static final String JSON_KEY_FACET_ISANDFACET = "isAndFacet";
146    /** A JSON key. */
147    public static final String JSON_KEY_FACET_IGNOREALLFACETFILTERS = "ignoreAllFacetFilters";
148    /** A JSON key. */
149    public static final String JSON_KEY_FACET_EXCLUDETAGS = "excludeTags";
150    /** A JSON key. */
151    public static final String JSON_KEY_FACET_PRESELECTION = "preselection";
152    /** A JSON key. */
153    public static final String JSON_KEY_RANGE_FACET_RANGE = "range";
154    /** A JSON key. */
155    public static final String JSON_KEY_RANGE_FACET_START = "start";
156    /** A JSON key. */
157    public static final String JSON_KEY_RANGE_FACET_END = "end";
158    /** A JSON key. */
159    public static final String JSON_KEY_RANGE_FACET_GAP = "gap";
160    /** A JSON key. */
161    public static final String JSON_KEY_RANGE_FACET_OTHER = "other";
162    /** A JSON key. */
163    public static final String JSON_KEY_RANGE_FACET_HARDEND = "hardend";
164    /** A JSON key. */
165    public static final String JSON_KEY_RANGE_FACET_METHOD = "method";
166    /** A JSON key. */
167    public static final String JSON_KEY_QUERY_FACET_QUERY = "queryitems";
168    /** A JSON key. */
169    public static final String JSON_KEY_QUERY_FACET_QUERY_QUERY = "query";
170    /** A JSON key. */
171    public static final String JSON_KEY_QUERY_FACET_QUERY_LABEL = "label";
172
173    /** JSON keys for sort options. */
174    /** A JSON key. */
175    public static final String JSON_KEY_SORTPARAM = "sortby";
176    /** The JSON key for the default sort option, should hold the name paramvalue for the default option. */
177    public static final String JSON_KEY_DEFAULT_SORT_OPTION = "defaultSortOption";
178    /** The JSON key for the sub-node with all search option configurations. */
179    public static final String JSON_KEY_SORTOPTIONS = "sortoptions";
180    /** JSON keys for a single search option. */
181    /** A JSON key. */
182    public static final String JSON_KEY_SORTOPTION_LABEL = "label";
183    /** A JSON key. */
184    public static final String JSON_KEY_SORTOPTION_PARAMVALUE = "paramvalue";
185    /** A JSON key. */
186    public static final String JSON_KEY_SORTOPTION_SOLRVALUE = "solrvalue";
187    /** JSON keys for the highlighting configuration. */
188    /** The JSON key for the subnode of all highlighting configuration. */
189    public static final String JSON_KEY_HIGHLIGHTER = "highlighter";
190    /** A JSON key. */
191    public static final String JSON_KEY_HIGHLIGHTER_FIELD = "field";
192    /** A JSON key. */
193    public static final String JSON_KEY_HIGHLIGHTER_SNIPPETS = "snippets";
194    /** A JSON key. */
195    public static final String JSON_KEY_HIGHLIGHTER_FRAGSIZE = "fragsize";
196    /** A JSON key. */
197    public static final String JSON_KEY_HIGHLIGHTER_ALTERNATE_FIELD = "alternateField";
198    /** A JSON key. */
199    public static final String JSON_KEY_HIGHLIGHTER_MAX_LENGTH_ALTERNATE_FIELD = "maxAlternateFieldLength";
200    /** A JSON key. */
201    public static final String JSON_KEY_HIGHLIGHTER_SIMPLE_PRE = "simple.pre";
202    /** A JSON key. */
203    public static final String JSON_KEY_HIGHLIGHTER_SIMPLE_POST = "simple.post";
204    /** A JSON key. */
205    public static final String JSON_KEY_HIGHLIGHTER_FORMATTER = "formatter";
206    /** A JSON key. */
207    public static final String JSON_KEY_HIGHLIGHTER_FRAGMENTER = "fragmenter";
208    /** A JSON key. */
209    public static final String JSON_KEY_HIGHLIGHTER_FASTVECTORHIGHLIGHTING = "useFastVectorHighlighting";
210
211    /** JSON keys for "Did you mean ...?" */
212    /** A JSON key. */
213    public static final String JSON_KEY_DIDYOUMEAN = "didYouMean";
214    /** The JSON key for the subnode of all "Did you mean?" configuration. */
215    /** A JSON key. */
216    public static final String JSON_KEY_DIDYOUMEAN_QUERYPARAM = "didYouMeanQueryParam";
217    /** A JSON key. */
218    public static final String JSON_KEY_DIDYOUMEAN_COLLATE = "didYouMeanCollate";
219    /** A JSON key. */
220    public static final String JSON_KEY_DIDYOUMEAN_COUNT = "didYouMeanCount";
221
222    /** JSON keys for the Geo filter. */
223    public static final String JSON_KEY_GEO_FILTER = "geofilter";
224    /** A JSON key. */
225    public static final String JSON_KEY_GEO_FILTER_COORDINATES = "coordinates";
226    /** A JSON key. */
227    public static final String JSON_KEY_GEO_FILTER_COORDINATES_PARAM = "coordinatesparam";
228    /** A JSON key. */
229    public static final String JSON_KEY_GEO_FILTER_FIELD_NAME = "fieldName";
230    /** A JSON key. */
231    public static final String JSON_KEY_GEO_FILTER_RADIUS = "radius";
232    /** A JSON key. */
233    public static final String JSON_KEY_GEO_FILTER_RADIUS_PARAM = "radiusparam";
234    /** A JSON key. */
235    public static final String JSON_KEY_GEO_FILTER_UNITS = "units";
236    /** A JSON key. */
237    public static final String JSON_KEY_GEO_FILTER_UNITS_PARAM = "unitsparam";
238
239    /** The default values. */
240    /** A JSON key. */
241    public static final String DEFAULT_QUERY_PARAM = "q";
242    /** A JSON key. */
243    public static final String DEFAULT_LAST_QUERY_PARAM = "lq";
244    /** A JSON key. */
245    public static final String DEFAULT_RELOADED_PARAM = "reloaded";
246
247    /** The whole JSON file. */
248    protected JSONObject m_configObject;
249
250    /** The optional base configuration that should be changed by the JSON configuration. */
251    private I_CmsSearchConfiguration m_baseConfig;
252
253    /** Constructor taking the JSON as JSONObject.
254     * @param jsonObject The JSON that should be evaluated as JSONObject.
255     */
256    public CmsJSONSearchConfigurationParser(JSONObject jsonObject) {
257
258        m_configObject = jsonObject;
259
260    }
261
262    /** Constructor taking the JSON as String.
263     * @param json The JSON that should be parsed as String.
264     * @throws JSONException Thrown if parsing fails.
265     */
266    public CmsJSONSearchConfigurationParser(String json)
267    throws JSONException {
268
269        init(json, null);
270    }
271
272    /** Constructor taking the JSON as String.
273     * @param json The JSON that should be parsed as String.
274     * @param baseConfig A base configuration that is adjusted by the JSON configuration string.
275     * @throws JSONException Thrown if parsing fails.
276     */
277    public CmsJSONSearchConfigurationParser(String json, I_CmsSearchConfiguration baseConfig)
278    throws JSONException {
279
280        init(json, baseConfig);
281    }
282
283    /** Helper for reading a mandatory String value list - throwing an Exception if parsing fails.
284     * @param json The JSON object where the list should be read from.
285     * @param key The key of the value to read.
286     * @return The value from the JSON.
287     * @throws JSONException thrown when parsing fails.
288     */
289    protected static List<String> parseMandatoryStringValues(JSONObject json, String key) throws JSONException {
290
291        List<String> list = null;
292        JSONArray array = json.getJSONArray(key);
293        list = new ArrayList<String>(array.length());
294        for (int i = 0; i < array.length(); i++) {
295            try {
296                String entry = array.getString(i);
297                list.add(entry);
298            } catch (JSONException e) {
299                LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_STRING_ENTRY_UNPARSABLE_1, key), e);
300            }
301        }
302        return list;
303    }
304
305    /** Helper for reading an optional Boolean value - returning <code>null</code> if parsing fails.
306     * @param json The JSON object where the value should be read from.
307     * @param key The key of the value to read.
308     * @return The value from the JSON, or <code>null</code> if the value does not exist, or is no Boolean.
309     */
310    protected static Boolean parseOptionalBooleanValue(JSONObject json, String key) {
311
312        try {
313            return Boolean.valueOf(json.getBoolean(key));
314        } catch (JSONException e) {
315            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_BOOLEAN_MISSING_1, key), e);
316            return null;
317        }
318    }
319
320    /** Helper for reading an optional Integer value - returning <code>null</code> if parsing fails.
321     * @param json The JSON object where the value should be read from.
322     * @param key The key of the value to read.
323     * @return The value from the JSON, or <code>null</code> if the value does not exist, or is no Integer.
324     */
325    protected static Integer parseOptionalIntValue(JSONObject json, String key) {
326
327        try {
328            return Integer.valueOf(json.getInt(key));
329        } catch (JSONException e) {
330            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_INTEGER_MISSING_1, key), e);
331            return null;
332        }
333    }
334
335    /** Helper for reading an optional String value - returning <code>null</code> if parsing fails.
336     * @param json The JSON object where the value should be read from.
337     * @param key The key of the value to read.
338     * @return The value from the JSON, or <code>null</code> if the value does not exist.
339     */
340    protected static String parseOptionalStringValue(JSONObject json, String key) {
341
342        try {
343            return json.getString(key);
344        } catch (JSONException e) {
345            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_STRING_MISSING_1, key), e);
346            return null;
347        }
348    }
349
350    /** Helper for reading an optional String value list - returning <code>null</code> if parsing fails for the whole list, otherwise just skipping unparsable entries.
351     * @param json The JSON object where the list should be read from.
352     * @param key The key of the value to read.
353     * @return The value from the JSON, or <code>null</code> if the value does not exist.
354     */
355    protected static List<String> parseOptionalStringValues(JSONObject json, String key) {
356
357        List<String> list = null;
358        try {
359            list = parseMandatoryStringValues(json, key);
360        } catch (JSONException e) {
361            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_STRING_LIST_MISSING_1, key), e);
362            return null;
363        }
364        return list;
365    }
366
367    /**
368     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseCommon(CmsObject)
369     */
370    @Override
371    public I_CmsSearchConfigurationCommon parseCommon(CmsObject cms) {
372
373        String indexName = getIndex(cms);
374
375        return new CmsSearchConfigurationCommon(
376            getQueryParam(),
377            getLastQueryParam(),
378            getEscapeQueryChars(),
379            getFirstCallParam(),
380            getSearchForEmptyQuery(),
381            getIgnoreQuery(),
382            getQueryModifier(),
383            indexName,
384            getCore(),
385            getExtraSolrParams(),
386            getAdditionalParameters(),
387            getIgnoreReleaseDate(),
388            getIgnoreExpirationDate(),
389            getMaxReturnedResults(indexName));
390    }
391
392    /**
393     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseDidYouMean()
394     */
395    public I_CmsSearchConfigurationDidYouMean parseDidYouMean() {
396
397        try {
398            JSONObject didYouMean = m_configObject.getJSONObject(JSON_KEY_DIDYOUMEAN);
399            String param = parseOptionalStringValue(didYouMean, JSON_KEY_DIDYOUMEAN_QUERYPARAM);
400            // default to the normal query param
401            if (null == param) {
402                param = getQueryParam();
403            }
404            Boolean collate = parseOptionalBooleanValue(didYouMean, JSON_KEY_DIDYOUMEAN_COLLATE);
405            Integer count = parseOptionalIntValue(didYouMean, JSON_KEY_DIDYOUMEAN_COUNT);
406            return new CmsSearchConfigurationDidYouMean(param, collate, count);
407
408        } catch (JSONException e) {
409            if (null == m_baseConfig) {
410                if (LOG.isInfoEnabled()) {
411                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_HIGHLIGHTING_CONFIG_0), e);
412                }
413                return null;
414            } else {
415                return m_baseConfig.getDidYouMeanConfig();
416            }
417        }
418
419    }
420
421    /**
422     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseFieldFacets()
423     */
424    @Override
425    public Map<String, I_CmsSearchConfigurationFacetField> parseFieldFacets() {
426
427        Map<String, I_CmsSearchConfigurationFacetField> facetConfigs = new LinkedHashMap<String, I_CmsSearchConfigurationFacetField>();
428        try {
429            JSONArray fieldFacets = m_configObject.getJSONArray(JSON_KEY_FIELD_FACETS);
430            for (int i = 0; i < fieldFacets.length(); i++) {
431
432                I_CmsSearchConfigurationFacetField config = parseFieldFacet(fieldFacets.getJSONObject(i));
433                if (config != null) {
434                    facetConfigs.put(config.getName(), config);
435                }
436            }
437        } catch (JSONException e) {
438            if (null == m_baseConfig) {
439                if (LOG.isInfoEnabled()) {
440                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_FACET_CONFIG_0), e);
441                }
442            } else {
443                facetConfigs = m_baseConfig.getFieldFacetConfigs();
444            }
445        }
446        return facetConfigs;
447    }
448
449    /**
450     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseGeoFilter()
451     */
452    @Override
453    public I_CmsSearchConfigurationGeoFilter parseGeoFilter() {
454
455        try {
456            JSONObject geoFilter = m_configObject.getJSONObject(JSON_KEY_GEO_FILTER);
457            String coordinates = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_COORDINATES);
458            String coordinatesParam = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_COORDINATES_PARAM);
459            String fieldName = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_FIELD_NAME);
460            String radius = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_RADIUS);
461            String radiusParam = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_RADIUS_PARAM);
462            String units = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_UNITS);
463            String unitsParam = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_UNITS_PARAM);
464            return new CmsSearchConfigurationGeoFilter(
465                coordinates,
466                coordinatesParam,
467                fieldName,
468                radius,
469                radiusParam,
470                units,
471                unitsParam);
472        } catch (JSONException e) {
473            if (null == m_baseConfig) {
474                if (LOG.isInfoEnabled()) {
475                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_GEOFILTER_CONFIG_0), e);
476                }
477                return null;
478            } else {
479                return m_baseConfig.getGeoFilterConfig();
480            }
481        }
482    }
483
484    /**
485     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseHighlighter()
486     */
487    @Override
488    public I_CmsSearchConfigurationHighlighting parseHighlighter() {
489
490        try {
491            JSONObject highlighter = m_configObject.getJSONObject(JSON_KEY_HIGHLIGHTER);
492            String field = highlighter.getString(JSON_KEY_HIGHLIGHTER_FIELD);
493            Integer snippets = parseOptionalIntValue(highlighter, JSON_KEY_HIGHLIGHTER_SNIPPETS);
494            Integer fragsize = parseOptionalIntValue(highlighter, JSON_KEY_HIGHLIGHTER_FRAGSIZE);
495            String alternateField = parseOptionalStringValue(highlighter, JSON_KEY_HIGHLIGHTER_ALTERNATE_FIELD);
496            Integer maxAlternateFieldLength = parseOptionalIntValue(
497                highlighter,
498                JSON_KEY_HIGHLIGHTER_MAX_LENGTH_ALTERNATE_FIELD);
499            String pre = parseOptionalStringValue(highlighter, JSON_KEY_HIGHLIGHTER_SIMPLE_PRE);
500            String post = parseOptionalStringValue(highlighter, JSON_KEY_HIGHLIGHTER_SIMPLE_POST);
501            String formatter = parseOptionalStringValue(highlighter, JSON_KEY_HIGHLIGHTER_FORMATTER);
502            String fragmenter = parseOptionalStringValue(highlighter, JSON_KEY_HIGHLIGHTER_FRAGMENTER);
503            Boolean useFastVectorHighlighting = parseOptionalBooleanValue(
504                highlighter,
505                JSON_KEY_HIGHLIGHTER_FASTVECTORHIGHLIGHTING);
506            return new CmsSearchConfigurationHighlighting(
507                field,
508                snippets,
509                fragsize,
510                alternateField,
511                maxAlternateFieldLength,
512                pre,
513                post,
514                formatter,
515                fragmenter,
516                useFastVectorHighlighting);
517        } catch (JSONException e) {
518            if (null == m_baseConfig) {
519                if (LOG.isInfoEnabled()) {
520                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_HIGHLIGHTING_CONFIG_0), e);
521                }
522                return null;
523            } else {
524                return m_baseConfig.getHighlighterConfig();
525            }
526        }
527    }
528
529    /**
530     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parsePagination()
531     */
532    @Override
533    public I_CmsSearchConfigurationPagination parsePagination() {
534
535        return CmsSearchConfigurationPagination.create(getPageParam(), getPageSizes(), getPageNavLength());
536    }
537
538    /**
539     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseQueryFacet()
540     */
541    @Override
542    public I_CmsSearchConfigurationFacetQuery parseQueryFacet() {
543
544        try {
545            JSONObject queryFacetObject = m_configObject.getJSONObject(JSON_KEY_QUERY_FACET);
546            try {
547                List<I_CmsFacetQueryItem> queries = parseFacetQueryItems(queryFacetObject);
548                String label = parseOptionalStringValue(queryFacetObject, JSON_KEY_FACET_LABEL);
549                Boolean isAndFacet = parseOptionalBooleanValue(queryFacetObject, JSON_KEY_FACET_ISANDFACET);
550                List<String> preselection = parseOptionalStringValues(queryFacetObject, JSON_KEY_FACET_PRESELECTION);
551                Boolean ignoreAllFacetFilters = parseOptionalBooleanValue(
552                    queryFacetObject,
553                    JSON_KEY_FACET_IGNOREALLFACETFILTERS);
554                List<String> excludeTags = parseOptionalStringValues(queryFacetObject, JSON_KEY_FACET_EXCLUDETAGS);
555                return new CmsSearchConfigurationFacetQuery(
556                    queries,
557                    label,
558                    isAndFacet,
559                    preselection,
560                    ignoreAllFacetFilters,
561                    excludeTags);
562            } catch (JSONException e) {
563                LOG.error(
564                    Messages.get().getBundle().key(
565                        Messages.ERR_QUERY_FACET_MANDATORY_KEY_MISSING_1,
566                        JSON_KEY_QUERY_FACET_QUERY),
567                    e);
568                return null;
569            }
570        } catch (JSONException e) {
571            // nothing to do, configuration is optional
572            return null != m_baseConfig ? m_baseConfig.getQueryFacetConfig() : null;
573        }
574    }
575
576    /**
577     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseRangeFacets()
578     */
579    public Map<String, I_CmsSearchConfigurationFacetRange> parseRangeFacets() {
580
581        Map<String, I_CmsSearchConfigurationFacetRange> facetConfigs = new LinkedHashMap<String, I_CmsSearchConfigurationFacetRange>();
582        try {
583            JSONArray rangeFacets = m_configObject.getJSONArray(JSON_KEY_RANGE_FACETS);
584            for (int i = 0; i < rangeFacets.length(); i++) {
585
586                I_CmsSearchConfigurationFacetRange config = parseRangeFacet(rangeFacets.getJSONObject(i));
587                if (config != null) {
588                    facetConfigs.put(config.getName(), config);
589                }
590            }
591        } catch (JSONException e) {
592            if (null == m_baseConfig) {
593                if (LOG.isInfoEnabled()) {
594                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_FACET_CONFIG_0), e);
595                }
596            } else {
597                facetConfigs = m_baseConfig.getRangeFacetConfigs();
598            }
599        }
600        return facetConfigs;
601
602    }
603
604    /**
605     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseSorting()
606     */
607    @Override
608    public I_CmsSearchConfigurationSorting parseSorting() {
609
610        List<I_CmsSearchConfigurationSortOption> options = getSortOptions();
611        String defaultOptionParamValue = parseOptionalStringValue(m_configObject, JSON_KEY_DEFAULT_SORT_OPTION);
612        I_CmsSearchConfigurationSortOption defaultSortOption = null;
613        if (null != defaultOptionParamValue) {
614            Iterator<I_CmsSearchConfigurationSortOption> optIterator = options.iterator();
615            while ((null == defaultSortOption) && optIterator.hasNext()) {
616                I_CmsSearchConfigurationSortOption opt = optIterator.next();
617                if (Objects.equals(opt.getParamValue(), defaultOptionParamValue)) {
618                    defaultSortOption = opt;
619                }
620            }
621        }
622        if ((null == defaultSortOption) && !options.isEmpty()) {
623            defaultSortOption = options.get(0);
624        }
625        return CmsSearchConfigurationSorting.create(getSortParam(), options, defaultSortOption);
626    }
627
628    /** Returns a map with additional request parameters, mapping the parameter names to Solr query parts.
629     * @return A map with additional request parameters, mapping the parameter names to Solr query parts.
630     */
631    protected Map<String, String> getAdditionalParameters() {
632
633        Map<String, String> result;
634        try {
635            JSONArray additionalParams = m_configObject.getJSONArray(JSON_KEY_ADDITIONAL_PARAMETERS);
636            result = new HashMap<String, String>(additionalParams.length());
637            for (int i = 0; i < additionalParams.length(); i++) {
638                try {
639                    JSONObject currentParam = additionalParams.getJSONObject(i);
640                    String param = currentParam.getString(JSON_KEY_ADDITIONAL_PARAMETERS_PARAM);
641                    String solrQuery = parseOptionalStringValue(currentParam, JSON_KEY_ADDITIONAL_PARAMETERS_SOLRQUERY);
642                    result.put(param, solrQuery);
643                } catch (JSONException e) {
644                    LOG.error(Messages.get().getBundle().key(Messages.ERR_ADDITIONAL_PARAMETER_CONFIG_WRONG_0), e);
645                    continue;
646                }
647            }
648        } catch (JSONException e) {
649            LOG.info(Messages.get().getBundle().key(Messages.LOG_ADDITIONAL_PARAMETER_CONFIG_NOT_PARSED_0), e);
650            return null != m_baseConfig
651            ? m_baseConfig.getGeneralConfig().getAdditionalParameters()
652            : new HashMap<String, String>();
653        }
654        return result;
655    }
656
657    /** Returns the configured Solr core, or <code>null</code> if no core is configured.
658     * @return The configured Solr core, or <code>null</code> if no core is configured.
659     */
660    protected String getCore() {
661
662        try {
663            return m_configObject.getString(JSON_KEY_CORE);
664        } catch (JSONException e) {
665            if (null == m_baseConfig) {
666                if (LOG.isInfoEnabled()) {
667                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_CORE_SPECIFIED_0), e);
668                }
669                return null;
670            } else {
671                return m_baseConfig.getGeneralConfig().getSolrCore();
672            }
673        }
674    }
675
676    /**
677     * Returns the flag, indicating if the characters in the query string that are commands to Solr should be escaped.
678     * @return the flag, indicating if the characters in the query string that are commands to Solr should be escaped.
679     */
680    protected Boolean getEscapeQueryChars() {
681
682        Boolean isEscape = parseOptionalBooleanValue(m_configObject, JSON_KEY_ESCAPE_QUERY_CHARACTERS);
683        return (null == isEscape) && (m_baseConfig != null)
684        ? Boolean.valueOf(m_baseConfig.getGeneralConfig().getEscapeQueryChars())
685        : isEscape;
686    }
687
688    /** Returns the configured extra parameters that should be given to Solr, or the empty string if no parameters are configured.
689     * @return The configured extra parameters that should be given to Solr, or the empty string if no parameters are configured.
690     */
691    protected String getExtraSolrParams() {
692
693        try {
694            return m_configObject.getString(JSON_KEY_EXTRASOLRPARAMS);
695        } catch (JSONException e) {
696            if (null == m_baseConfig) {
697                if (LOG.isInfoEnabled()) {
698                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_EXTRA_PARAMETERS_0), e);
699                }
700                return "";
701            } else {
702                return m_baseConfig.getGeneralConfig().getExtraSolrParams();
703            }
704        }
705    }
706
707    /** Returns the configured request parameter for the last query, or the default parameter if no core is configured.
708    * @return The configured request parameter for the last query, or the default parameter if no core is configured.
709    */
710    protected String getFirstCallParam() {
711
712        String param = parseOptionalStringValue(m_configObject, JSON_KEY_RELOADED_PARAM);
713        if (param == null) {
714            return null != m_baseConfig ? m_baseConfig.getGeneralConfig().getReloadedParam() : DEFAULT_RELOADED_PARAM;
715        } else {
716            return param;
717        }
718    }
719
720    /** Returns a flag indicating if also expired resources should be found.
721     * @return A flag indicating if also expired resources should be found.
722     */
723    protected Boolean getIgnoreExpirationDate() {
724
725        Boolean isIgnoreExpirationDate = parseOptionalBooleanValue(m_configObject, JSON_KEY_IGNORE_EXPIRATION_DATE);
726        return (null == isIgnoreExpirationDate) && (m_baseConfig != null)
727        ? Boolean.valueOf(m_baseConfig.getGeneralConfig().getIgnoreExpirationDate())
728        : isIgnoreExpirationDate;
729    }
730
731    /** Returns a flag indicating if the query given by the parameters should be ignored.
732     * @return A flag indicating if the query given by the parameters should be ignored.
733     */
734    protected Boolean getIgnoreQuery() {
735
736        Boolean isIgnoreQuery = parseOptionalBooleanValue(m_configObject, JSON_KEY_IGNORE_QUERY);
737        return (null == isIgnoreQuery) && (m_baseConfig != null)
738        ? Boolean.valueOf(m_baseConfig.getGeneralConfig().getIgnoreQueryParam())
739        : isIgnoreQuery;
740    }
741
742    /** Returns a flag indicating if also unreleased resources should be found.
743     * @return A flag indicating if also unreleased resources should be found.
744     */
745    protected Boolean getIgnoreReleaseDate() {
746
747        Boolean isIgnoreReleaseDate = parseOptionalBooleanValue(m_configObject, JSON_KEY_IGNORE_RELEASE_DATE);
748        return (null == isIgnoreReleaseDate) && (m_baseConfig != null)
749        ? Boolean.valueOf(m_baseConfig.getGeneralConfig().getIgnoreReleaseDate())
750        : isIgnoreReleaseDate;
751    }
752
753    /** Returns the configured Solr index, or <code>null</code> if no core is configured.
754     * @param cms the current context.
755     * @return The configured Solr index, or <code>null</code> if no core is configured.
756     */
757    protected String getIndex(CmsObject cms) {
758
759        String indexName = null;
760        try {
761            indexName = m_configObject.getString(JSON_KEY_INDEX);
762        } catch (JSONException e) {
763            if (null == m_baseConfig) {
764                if (LOG.isInfoEnabled()) {
765                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_INDEX_SPECIFIED_0), e);
766                }
767            } else {
768                indexName = m_baseConfig.getGeneralConfig().getSolrIndex();
769            }
770        }
771        return null != indexName
772        ? indexName
773        : (cms.getRequestContext().getCurrentProject().isOnlineProject()
774        ? CmsSolrIndex.DEFAULT_INDEX_NAME_ONLINE
775        : CmsSolrIndex.DEFAULT_INDEX_NAME_OFFLINE);
776    }
777
778    /** Returns the configured request parameter for the last query, or the default parameter if no core is configured.
779    * @return The configured request parameter for the last query, or the default parameter if no core is configured.
780    */
781    protected String getLastQueryParam() {
782
783        String param = parseOptionalStringValue(m_configObject, JSON_KEY_LAST_QUERYPARAM);
784        if (param == null) {
785            return null != m_baseConfig
786            ? m_baseConfig.getGeneralConfig().getLastQueryParam()
787            : DEFAULT_LAST_QUERY_PARAM;
788        } else {
789            return param;
790        }
791    }
792
793    /** Returns the number of maximally returned results, or <code>null</code> if the indexes default should be used.
794     * @param indexName the name of the index to search in.
795     * @return The number of maximally returned results, or <code>null</code> if the indexes default should be used.
796     */
797    protected int getMaxReturnedResults(String indexName) {
798
799        Integer maxReturnedResults = parseOptionalIntValue(m_configObject, JSON_KEY_MAX_RETURNED_RESULTS);
800        if (null != maxReturnedResults) {
801            return maxReturnedResults.intValue();
802        } else if (m_baseConfig != null) {
803            return m_baseConfig.getGeneralConfig().getMaxReturnedResults();
804        } else {
805            try {
806                CmsSolrIndex idx = OpenCms.getSearchManager().getIndexSolr(indexName);
807                if (null != idx) {
808                    return idx.getMaxProcessedResults();
809                }
810            } catch (Throwable t) {
811                // This is ok, it's allowed to have an external other index here.
812                LOG.debug(
813                    "Parsing JSON search configuration for none-CmsSolrIndex "
814                        + indexName
815                        + ". Setting max processed results to unlimited.");
816            }
817            return CmsSolrIndex.MAX_RESULTS_UNLIMITED;
818        }
819    }
820
821    /** Returns the configured length of the "Google"-like page navigation, or the default parameter if no core is configured.
822     * @return The configured length of the "Google"-like page navigation, or the default parameter if no core is configured.
823     */
824    protected Integer getPageNavLength() {
825
826        return parseOptionalIntValue(m_configObject, JSON_KEY_PAGENAVLENGTH);
827    }
828
829    /** Returns the configured request parameter for the current page, or the default parameter if no core is configured.
830     * @return The configured request parameter for the current page, or the default parameter if no core is configured.
831     */
832    protected String getPageParam() {
833
834        return parseOptionalStringValue(m_configObject, JSON_KEY_PAGEPARAM);
835    }
836
837    /** Returns the configured page sizes, or the default page size if no core is configured.
838     * @return The configured page sizes, or the default page size if no core is configured.
839     */
840    protected List<Integer> getPageSizes() {
841
842        if (m_configObject.has(JSON_KEY_PAGESIZE)) {
843            try {
844                return Collections.singletonList(Integer.valueOf(m_configObject.getInt(JSON_KEY_PAGESIZE)));
845            } catch (JSONException e) {
846                List<Integer> result = null;
847                String pageSizesString = null;
848                try {
849                    pageSizesString = m_configObject.getString(JSON_KEY_PAGESIZE);
850                    String[] pageSizesArray = pageSizesString.split("-");
851                    if (pageSizesArray.length > 0) {
852                        result = new ArrayList<>(pageSizesArray.length);
853                        for (int i = 0; i < pageSizesArray.length; i++) {
854                            result.add(Integer.valueOf(pageSizesArray[i]));
855                        }
856                    }
857                    return result;
858                } catch (NumberFormatException | JSONException e1) {
859                    LOG.warn(
860                        Messages.get().getBundle().key(Messages.LOG_PARSING_PAGE_SIZES_FAILED_1, pageSizesString),
861                        e);
862                }
863            }
864        }
865        if (null == m_baseConfig) {
866            if (LOG.isInfoEnabled()) {
867                LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_PAGESIZE_SPECIFIED_0));
868            }
869            return null;
870        } else {
871            return m_baseConfig.getPaginationConfig().getPageSizes();
872        }
873    }
874
875    /** Returns the optional query modifier.
876     * @return the optional query modifier.
877     */
878    protected String getQueryModifier() {
879
880        String queryModifier = parseOptionalStringValue(m_configObject, JSON_KEY_QUERY_MODIFIER);
881        return (null == queryModifier) && (null != m_baseConfig)
882        ? m_baseConfig.getGeneralConfig().getQueryModifier()
883        : queryModifier;
884    }
885
886    /** Returns the configured request parameter for the query string, or the default parameter if no core is configured.
887     * @return The configured request parameter for the query string, or the default parameter if no core is configured.
888     */
889    protected String getQueryParam() {
890
891        String param = parseOptionalStringValue(m_configObject, JSON_KEY_QUERYPARAM);
892        if (param == null) {
893            return null != m_baseConfig ? m_baseConfig.getGeneralConfig().getQueryParam() : DEFAULT_QUERY_PARAM;
894        } else {
895            return param;
896        }
897    }
898
899    /** Returns a flag, indicating if search should be performed using a wildcard if the empty query is given.
900     * @return A flag, indicating if search should be performed using a wildcard if the empty query is given.
901     */
902    protected Boolean getSearchForEmptyQuery() {
903
904        Boolean isSearchForEmptyQuery = parseOptionalBooleanValue(m_configObject, JSON_KEY_SEARCH_FOR_EMPTY_QUERY);
905        return (isSearchForEmptyQuery == null) && (null != m_baseConfig)
906        ? Boolean.valueOf(m_baseConfig.getGeneralConfig().getSearchForEmptyQueryParam())
907        : isSearchForEmptyQuery;
908    }
909
910    /** Returns the list of the configured sort options, or the empty list if no sort options are configured.
911     * @return The list of the configured sort options, or the empty list if no sort options are configured.
912     */
913    protected List<I_CmsSearchConfigurationSortOption> getSortOptions() {
914
915        List<I_CmsSearchConfigurationSortOption> options = new LinkedList<I_CmsSearchConfigurationSortOption>();
916        try {
917            JSONArray sortOptions = m_configObject.getJSONArray(JSON_KEY_SORTOPTIONS);
918            for (int i = 0; i < sortOptions.length(); i++) {
919                I_CmsSearchConfigurationSortOption option = parseSortOption(sortOptions.getJSONObject(i));
920                if (option != null) {
921                    options.add(option);
922                }
923            }
924        } catch (JSONException e) {
925            if (null == m_baseConfig) {
926                if (LOG.isInfoEnabled()) {
927                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_SORT_CONFIG_0), e);
928                }
929            } else {
930                options = m_baseConfig.getSortConfig().getSortOptions();
931            }
932        }
933        return options;
934    }
935
936    /** Returns the configured request parameter for the sort option, or the default parameter if no core is configured.
937     * @return The configured request parameter for the sort option, or the default parameter if no core is configured.
938     */
939    protected String getSortParam() {
940
941        return parseOptionalStringValue(m_configObject, JSON_KEY_SORTPARAM);
942    }
943
944    /** Initialization that parses the String to a JSON object.
945     * @param configString The JSON as string.
946     * @param baseConfig The optional basic search configuration to overwrite (partly) by the JSON configuration.
947     * @throws JSONException thrown if parsing fails.
948     */
949    protected void init(String configString, I_CmsSearchConfiguration baseConfig) throws JSONException {
950
951        m_configObject = new JSONObject(configString);
952        m_baseConfig = baseConfig;
953    }
954
955    /** Parses a single query item for the query facet.
956     * @param item JSON object of the query item.
957     * @return the parsed query item, or <code>null</code> if parsing failed.
958     */
959    protected I_CmsFacetQueryItem parseFacetQueryItem(JSONObject item) {
960
961        String query;
962        try {
963            query = item.getString(JSON_KEY_QUERY_FACET_QUERY_QUERY);
964        } catch (JSONException e) {
965            // TODO: Log
966            return null;
967        }
968        String label = parseOptionalStringValue(item, JSON_KEY_QUERY_FACET_QUERY_LABEL);
969        return new CmsFacetQueryItem(query, label);
970    }
971
972    /** Parses the list of query items for the query facet.
973     * @param queryFacetObject JSON object representing the node with the query facet.
974     * @return list of query options
975     * @throws JSONException if the list cannot be parsed.
976     */
977    protected List<I_CmsFacetQueryItem> parseFacetQueryItems(JSONObject queryFacetObject) throws JSONException {
978
979        JSONArray items = queryFacetObject.getJSONArray(JSON_KEY_QUERY_FACET_QUERY);
980        List<I_CmsFacetQueryItem> result = new ArrayList<I_CmsFacetQueryItem>(items.length());
981        for (int i = 0; i < items.length(); i++) {
982            I_CmsFacetQueryItem item = parseFacetQueryItem(items.getJSONObject(i));
983            if (item != null) {
984                result.add(item);
985            }
986        }
987        return result;
988    }
989
990    /** Parses the field facet configurations.
991     * @param fieldFacetObject The JSON sub-node with the field facet configurations.
992     * @return The field facet configurations.
993     */
994    protected I_CmsSearchConfigurationFacetField parseFieldFacet(JSONObject fieldFacetObject) {
995
996        try {
997            String field = fieldFacetObject.getString(JSON_KEY_FACET_FIELD);
998            String name = parseOptionalStringValue(fieldFacetObject, JSON_KEY_FACET_NAME);
999            String label = parseOptionalStringValue(fieldFacetObject, JSON_KEY_FACET_LABEL);
1000            Integer minCount = parseOptionalIntValue(fieldFacetObject, JSON_KEY_FACET_MINCOUNT);
1001            Integer limit = parseOptionalIntValue(fieldFacetObject, JSON_KEY_FACET_LIMIT);
1002            String prefix = parseOptionalStringValue(fieldFacetObject, JSON_KEY_FACET_PREFIX);
1003            String sorder = parseOptionalStringValue(fieldFacetObject, JSON_KEY_FACET_ORDER);
1004            I_CmsSearchConfigurationFacet.SortOrder order;
1005            try {
1006                order = I_CmsSearchConfigurationFacet.SortOrder.valueOf(sorder);
1007            } catch (Exception e) {
1008                order = null;
1009            }
1010            String filterQueryModifier = parseOptionalStringValue(fieldFacetObject, JSON_KEY_FACET_FILTERQUERYMODIFIER);
1011            Boolean isAndFacet = parseOptionalBooleanValue(fieldFacetObject, JSON_KEY_FACET_ISANDFACET);
1012            List<String> preselection = parseOptionalStringValues(fieldFacetObject, JSON_KEY_FACET_PRESELECTION);
1013            Boolean ignoreFilterAllFacetFilters = parseOptionalBooleanValue(
1014                fieldFacetObject,
1015                JSON_KEY_FACET_IGNOREALLFACETFILTERS);
1016            List<String> excludeTags = parseOptionalStringValues(fieldFacetObject, JSON_KEY_FACET_EXCLUDETAGS);
1017            return new CmsSearchConfigurationFacetField(
1018                field,
1019                name,
1020                minCount,
1021                limit,
1022                prefix,
1023                label,
1024                order,
1025                filterQueryModifier,
1026                isAndFacet,
1027                preselection,
1028                ignoreFilterAllFacetFilters,
1029                excludeTags);
1030        } catch (JSONException e) {
1031            LOG.error(
1032                Messages.get().getBundle().key(Messages.ERR_FIELD_FACET_MANDATORY_KEY_MISSING_1, JSON_KEY_FACET_FIELD),
1033                e);
1034            return null;
1035        }
1036    }
1037
1038    /** Parses the query facet configurations.
1039     * @param rangeFacetObject The JSON sub-node with the query facet configurations.
1040     * @return The query facet configurations.
1041     */
1042    protected I_CmsSearchConfigurationFacetRange parseRangeFacet(JSONObject rangeFacetObject) {
1043
1044        try {
1045            String range = rangeFacetObject.getString(JSON_KEY_RANGE_FACET_RANGE);
1046            String name = parseOptionalStringValue(rangeFacetObject, JSON_KEY_FACET_NAME);
1047            String label = parseOptionalStringValue(rangeFacetObject, JSON_KEY_FACET_LABEL);
1048            Integer minCount = parseOptionalIntValue(rangeFacetObject, JSON_KEY_FACET_MINCOUNT);
1049            String start = rangeFacetObject.getString(JSON_KEY_RANGE_FACET_START);
1050            String end = rangeFacetObject.getString(JSON_KEY_RANGE_FACET_END);
1051            String gap = rangeFacetObject.getString(JSON_KEY_RANGE_FACET_GAP);
1052            List<String> sother = parseOptionalStringValues(rangeFacetObject, JSON_KEY_RANGE_FACET_OTHER);
1053            Boolean hardEnd = parseOptionalBooleanValue(rangeFacetObject, JSON_KEY_RANGE_FACET_HARDEND);
1054            I_CmsSearchConfigurationFacetRange.Method method = null;
1055            String methodStr = parseOptionalStringValue(rangeFacetObject, JSON_KEY_RANGE_FACET_METHOD);
1056            if (null != methodStr) {
1057                try {
1058                    method = I_CmsSearchConfigurationFacetRange.Method.valueOf(methodStr);
1059                } catch (Exception e) {
1060                    LOG.error(Messages.get().getBundle().key(Messages.ERR_INVALID_RANGE_METHOD_OPTION_1, methodStr), e);
1061                }
1062            }
1063            List<I_CmsSearchConfigurationFacetRange.Other> other = null;
1064            if (sother != null) {
1065                other = new ArrayList<I_CmsSearchConfigurationFacetRange.Other>(sother.size());
1066                for (String so : sother) {
1067                    try {
1068                        I_CmsSearchConfigurationFacetRange.Other o = I_CmsSearchConfigurationFacetRange.Other.valueOf(
1069                            so);
1070                        other.add(o);
1071                    } catch (Exception e) {
1072                        LOG.error(Messages.get().getBundle().key(Messages.ERR_INVALID_OTHER_OPTION_1, so), e);
1073                    }
1074                }
1075            }
1076            Boolean isAndFacet = parseOptionalBooleanValue(rangeFacetObject, JSON_KEY_FACET_ISANDFACET);
1077            List<String> preselection = parseOptionalStringValues(rangeFacetObject, JSON_KEY_FACET_PRESELECTION);
1078            Boolean ignoreAllFacetFilters = parseOptionalBooleanValue(
1079                rangeFacetObject,
1080                JSON_KEY_FACET_IGNOREALLFACETFILTERS);
1081            List<String> excludeTags = parseOptionalStringValues(rangeFacetObject, JSON_KEY_FACET_EXCLUDETAGS);
1082            return new CmsSearchConfigurationFacetRange(
1083                range,
1084                start,
1085                end,
1086                gap,
1087                other,
1088                hardEnd,
1089                method,
1090                name,
1091                minCount,
1092                label,
1093                isAndFacet,
1094                preselection,
1095                ignoreAllFacetFilters,
1096                excludeTags);
1097        } catch (JSONException e) {
1098            LOG.error(
1099                Messages.get().getBundle().key(
1100                    Messages.ERR_RANGE_FACET_MANDATORY_KEY_MISSING_1,
1101                    JSON_KEY_RANGE_FACET_RANGE
1102                        + ", "
1103                        + JSON_KEY_RANGE_FACET_START
1104                        + ", "
1105                        + JSON_KEY_RANGE_FACET_END
1106                        + ", "
1107                        + JSON_KEY_RANGE_FACET_GAP),
1108                e);
1109            return null;
1110        }
1111
1112    }
1113
1114    /** Returns a single sort option configuration as configured via the methods parameter, or null if the parameter does not specify a sort option.
1115     * @param json The JSON sort option configuration.
1116     * @return The sort option configuration, or null if the JSON could not be read.
1117     */
1118    protected I_CmsSearchConfigurationSortOption parseSortOption(JSONObject json) {
1119
1120        try {
1121            String solrValue = json.getString(JSON_KEY_SORTOPTION_SOLRVALUE);
1122            String paramValue = parseOptionalStringValue(json, JSON_KEY_SORTOPTION_PARAMVALUE);
1123            paramValue = (paramValue == null) ? solrValue : paramValue;
1124            String label = parseOptionalStringValue(json, JSON_KEY_SORTOPTION_LABEL);
1125            label = (label == null) ? paramValue : label;
1126            return new CmsSearchConfigurationSortOption(label, paramValue, solrValue);
1127        } catch (JSONException e) {
1128            LOG.error(
1129                Messages.get().getBundle().key(Messages.ERR_SORT_OPTION_NOT_PARSABLE_1, JSON_KEY_SORTOPTION_SOLRVALUE),
1130                e);
1131            return null;
1132        }
1133    }
1134}