001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.jsp.search.config.parser;
029
030import org.opencms.file.CmsObject;
031import org.opencms.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_ESCAPE_QUERY = "escapeDidYouMeanQuery";
219    /** A JSON key. */
220    public static final String JSON_KEY_DIDYOUMEAN_COLLATE = "didYouMeanCollate";
221    /** A JSON key. */
222    public static final String JSON_KEY_DIDYOUMEAN_COUNT = "didYouMeanCount";
223
224    /** JSON keys for the Geo filter. */
225    public static final String JSON_KEY_GEO_FILTER = "geofilter";
226    /** A JSON key. */
227    public static final String JSON_KEY_GEO_FILTER_COORDINATES = "coordinates";
228    /** A JSON key. */
229    public static final String JSON_KEY_GEO_FILTER_COORDINATES_PARAM = "coordinatesparam";
230    /** A JSON key. */
231    public static final String JSON_KEY_GEO_FILTER_FIELD_NAME = "fieldName";
232    /** A JSON key. */
233    public static final String JSON_KEY_GEO_FILTER_RADIUS = "radius";
234    /** A JSON key. */
235    public static final String JSON_KEY_GEO_FILTER_RADIUS_PARAM = "radiusparam";
236    /** A JSON key. */
237    public static final String JSON_KEY_GEO_FILTER_UNITS = "units";
238    /** A JSON key. */
239    public static final String JSON_KEY_GEO_FILTER_UNITS_PARAM = "unitsparam";
240
241    /** The default values. */
242    /** A JSON key. */
243    public static final String DEFAULT_QUERY_PARAM = "q";
244    /** A JSON key. */
245    public static final String DEFAULT_LAST_QUERY_PARAM = "lq";
246    /** A JSON key. */
247    public static final String DEFAULT_RELOADED_PARAM = "reloaded";
248
249    /** The whole JSON file. */
250    protected JSONObject m_configObject;
251
252    /** The optional base configuration that should be changed by the JSON configuration. */
253    private I_CmsSearchConfiguration m_baseConfig;
254
255    /** Constructor taking the JSON as JSONObject.
256     * @param jsonObject The JSON that should be evaluated as JSONObject.
257     */
258    public CmsJSONSearchConfigurationParser(JSONObject jsonObject) {
259
260        m_configObject = jsonObject;
261
262    }
263
264    /** Constructor taking the JSON as String.
265     * @param json The JSON that should be parsed as String.
266     * @throws JSONException Thrown if parsing fails.
267     */
268    public CmsJSONSearchConfigurationParser(String json)
269    throws JSONException {
270
271        init(json, null);
272    }
273
274    /** Constructor taking the JSON as String.
275     * @param json The JSON that should be parsed as String.
276     * @param baseConfig A base configuration that is adjusted by the JSON configuration string.
277     * @throws JSONException Thrown if parsing fails.
278     */
279    public CmsJSONSearchConfigurationParser(String json, I_CmsSearchConfiguration baseConfig)
280    throws JSONException {
281
282        init(json, baseConfig);
283    }
284
285    /** Helper for reading a mandatory String value list - throwing an Exception if parsing fails.
286     * @param json The JSON object where the list should be read from.
287     * @param key The key of the value to read.
288     * @return The value from the JSON.
289     * @throws JSONException thrown when parsing fails.
290     */
291    protected static List<String> parseMandatoryStringValues(JSONObject json, String key) throws JSONException {
292
293        List<String> list = null;
294        JSONArray array = json.getJSONArray(key);
295        list = new ArrayList<>(array.length());
296        for (int i = 0; i < array.length(); i++) {
297            try {
298                String entry = array.getString(i);
299                list.add(entry);
300            } catch (JSONException e) {
301                LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_STRING_ENTRY_UNPARSABLE_1, key), e);
302            }
303        }
304        return list;
305    }
306
307    /** Helper for reading an optional JSON value of type String, boolean or number. The result is always returned as String.
308     * @param json The JSON object where the list should be read from.
309     * @param key The key of the value to read.
310     * @return The value from the JSON.
311     */
312    protected static String parseOptionalAtomicValue(JSONObject json, String key) {
313
314        String result = parseOptionalStringValue(json, key);
315        if (null == result) {
316            Boolean b = parseOptionalBooleanValue(json, key);
317            if (null != b) {
318                result = b.booleanValue() ? "true" : "false";
319            }
320        }
321        if (null == result) {
322            Integer i = parseOptionalIntValue(json, key);
323            if (null != i) {
324                result = String.valueOf(i);
325            }
326        }
327        if (null == result) {
328            Double d = parseOptionalDoubleValue(json, key);
329            if (null != d) {
330                result = String.valueOf(d);
331            }
332        }
333        return result;
334    }
335
336    /** Helper for reading an optional Boolean value - returning <code>null</code> if parsing fails.
337     * @param json The JSON object where the value should be read from.
338     * @param key The key of the value to read.
339     * @return The value from the JSON, or <code>null</code> if the value does not exist, or is no Boolean.
340     */
341    protected static Boolean parseOptionalBooleanValue(JSONObject json, String key) {
342
343        try {
344            return Boolean.valueOf(json.getBoolean(key));
345        } catch (JSONException e) {
346            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_BOOLEAN_MISSING_1, key), e);
347            return null;
348        }
349    }
350
351    /** Helper for reading an optional Double value - returning <code>null</code> if parsing fails.
352     * @param json The JSON object where the value should be read from.
353     * @param key The key of the value to read.
354     * @return The value from the JSON, or <code>null</code> if the value does not exist, or is no Double.
355     */
356    protected static Double parseOptionalDoubleValue(JSONObject json, String key) {
357
358        try {
359            return Double.valueOf(json.getDouble(key));
360        } catch (JSONException e) {
361            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_DOUBLE_MISSING_1, key), e);
362            return null;
363        }
364    }
365
366    /** Helper for reading an optional Integer value - returning <code>null</code> if parsing fails.
367     * @param json The JSON object where the value should be read from.
368     * @param key The key of the value to read.
369     * @return The value from the JSON, or <code>null</code> if the value does not exist, or is no Integer.
370     */
371    protected static Integer parseOptionalIntValue(JSONObject json, String key) {
372
373        try {
374            return Integer.valueOf(json.getInt(key));
375        } catch (JSONException e) {
376            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_INTEGER_MISSING_1, key), e);
377            return null;
378        }
379    }
380
381    /** Helper for reading an optional JSON object - returning <code>null</code> if parsing fails.
382     * @param json The JSON object where the value should be read from.
383     * @param key The key of the JSON object to read.
384     * @return The value from the JSON, or <code>null</code> if the value does not exist, or is no JSON object.
385     */
386    protected static JSONObject parseOptionalJSONObject(JSONObject json, String key) {
387
388        try {
389            return json.getJSONObject(key);
390        } catch (JSONException e) {
391            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_JSON_OBJECT_MISSING_1, key), e);
392            return null;
393        }
394    }
395
396    /** Helper for reading an optional String value - returning <code>null</code> if parsing fails.
397     * @param json The JSON object where the value should be read from.
398     * @param key The key of the value to read.
399     * @return The value from the JSON, or <code>null</code> if the value does not exist.
400     */
401    protected static String parseOptionalStringValue(JSONObject json, String key) {
402
403        try {
404            return json.getString(key);
405        } catch (JSONException e) {
406            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_STRING_MISSING_1, key), e);
407            return null;
408        }
409    }
410
411    /** Helper for reading an optional String value list - returning <code>null</code> if parsing fails for the whole list, otherwise just skipping unparsable entries.
412     * @param json The JSON object where the list should be read from.
413     * @param key The key of the value to read.
414     * @return The value from the JSON, or <code>null</code> if the value does not exist.
415     */
416    protected static List<String> parseOptionalStringValues(JSONObject json, String key) {
417
418        List<String> list = null;
419        try {
420            list = parseMandatoryStringValues(json, key);
421        } catch (JSONException e) {
422            LOG.info(Messages.get().getBundle().key(Messages.LOG_OPTIONAL_STRING_LIST_MISSING_1, key), e);
423            return null;
424        }
425        return list;
426    }
427
428    /**
429     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseCommon(CmsObject)
430     */
431    @Override
432    public I_CmsSearchConfigurationCommon parseCommon(CmsObject cms) {
433
434        String indexName = getIndex(cms);
435
436        return new CmsSearchConfigurationCommon(
437            getQueryParam(),
438            getLastQueryParam(),
439            getEscapeQueryChars(),
440            getFirstCallParam(),
441            getSearchForEmptyQuery(),
442            getIgnoreQuery(),
443            getQueryModifier(),
444            indexName,
445            getCore(),
446            getExtraSolrParams(),
447            getAdditionalParameters(),
448            getIgnoreReleaseDate(),
449            getIgnoreExpirationDate(),
450            getMaxReturnedResults(indexName));
451    }
452
453    /**
454     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseDidYouMean()
455     */
456    @Override
457    public I_CmsSearchConfigurationDidYouMean parseDidYouMean() {
458
459        try {
460            JSONObject didYouMean = m_configObject.getJSONObject(JSON_KEY_DIDYOUMEAN);
461            String param = parseOptionalStringValue(didYouMean, JSON_KEY_DIDYOUMEAN_QUERYPARAM);
462            // default to the normal query param
463            if (null == param) {
464                param = getQueryParam();
465            }
466            Boolean escape = parseOptionalBooleanValue(didYouMean, JSON_KEY_DIDYOUMEAN_ESCAPE_QUERY);
467            if (null == escape) {
468                escape = getEscapeQueryChars();
469            }
470            Boolean collate = parseOptionalBooleanValue(didYouMean, JSON_KEY_DIDYOUMEAN_COLLATE);
471            Integer count = parseOptionalIntValue(didYouMean, JSON_KEY_DIDYOUMEAN_COUNT);
472            return new CmsSearchConfigurationDidYouMean(param, escape, collate, count);
473
474        } catch (JSONException e) {
475            if (null == m_baseConfig) {
476                if (LOG.isInfoEnabled()) {
477                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_HIGHLIGHTING_CONFIG_0), e);
478                }
479                return null;
480            }
481            return m_baseConfig.getDidYouMeanConfig();
482        }
483
484    }
485
486    /**
487     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseFieldFacets()
488     */
489    @Override
490    public Map<String, I_CmsSearchConfigurationFacetField> parseFieldFacets() {
491
492        Map<String, I_CmsSearchConfigurationFacetField> facetConfigs = new LinkedHashMap<>();
493        try {
494            JSONArray fieldFacets = m_configObject.getJSONArray(JSON_KEY_FIELD_FACETS);
495            for (int i = 0; i < fieldFacets.length(); i++) {
496
497                I_CmsSearchConfigurationFacetField config = parseFieldFacet(fieldFacets.getJSONObject(i));
498                if (config != null) {
499                    facetConfigs.put(config.getName(), config);
500                }
501            }
502        } catch (JSONException e) {
503            if (null == m_baseConfig) {
504                if (LOG.isInfoEnabled()) {
505                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_FACET_CONFIG_0), e);
506                }
507            } else {
508                facetConfigs = m_baseConfig.getFieldFacetConfigs();
509            }
510        }
511        return facetConfigs;
512    }
513
514    /**
515     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseGeoFilter()
516     */
517    @Override
518    public I_CmsSearchConfigurationGeoFilter parseGeoFilter() {
519
520        try {
521            JSONObject geoFilter = m_configObject.getJSONObject(JSON_KEY_GEO_FILTER);
522            String coordinates = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_COORDINATES);
523            String coordinatesParam = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_COORDINATES_PARAM);
524            String fieldName = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_FIELD_NAME);
525            String radius = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_RADIUS);
526            String radiusParam = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_RADIUS_PARAM);
527            String units = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_UNITS);
528            String unitsParam = parseOptionalStringValue(geoFilter, JSON_KEY_GEO_FILTER_UNITS_PARAM);
529            return new CmsSearchConfigurationGeoFilter(
530                coordinates,
531                coordinatesParam,
532                fieldName,
533                radius,
534                radiusParam,
535                units,
536                unitsParam);
537        } catch (JSONException e) {
538            if (null == m_baseConfig) {
539                if (LOG.isInfoEnabled()) {
540                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_GEOFILTER_CONFIG_0), e);
541                }
542                return null;
543            }
544            return m_baseConfig.getGeoFilterConfig();
545        }
546    }
547
548    /**
549     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseHighlighter()
550     */
551    @Override
552    public I_CmsSearchConfigurationHighlighting parseHighlighter() {
553
554        try {
555            Map<String, String> hlParams = new LinkedHashMap<>();
556            JSONObject highlighter = m_configObject.getJSONObject(JSON_KEY_HIGHLIGHTER);
557            String field = highlighter.getString(JSON_KEY_HIGHLIGHTER_FIELD);
558            hlParams.put("fl", field);
559            Integer snippets = parseOptionalIntValue(highlighter, JSON_KEY_HIGHLIGHTER_SNIPPETS);
560            if (null != snippets) {
561                hlParams.put("snippets", snippets.toString());
562            }
563            Integer fragsize = parseOptionalIntValue(highlighter, JSON_KEY_HIGHLIGHTER_FRAGSIZE);
564            if (null != fragsize) {
565                hlParams.put("fragsize", fragsize.toString());
566            }
567            String alternateField = parseOptionalStringValue(highlighter, JSON_KEY_HIGHLIGHTER_ALTERNATE_FIELD);
568            if (null != alternateField) {
569                hlParams.put("alternateField", alternateField);
570            }
571            Integer maxAlternateFieldLength = parseOptionalIntValue(
572                highlighter,
573                JSON_KEY_HIGHLIGHTER_MAX_LENGTH_ALTERNATE_FIELD);
574            if (null != maxAlternateFieldLength) {
575                hlParams.put("maxAlternateFieldLength", maxAlternateFieldLength.toString());
576            }
577            String pre = parseOptionalStringValue(highlighter, JSON_KEY_HIGHLIGHTER_SIMPLE_PRE);
578            if (null != pre) {
579                hlParams.put("simple.pre", pre);
580                hlParams.put("tag.pre", pre);
581            }
582            String post = parseOptionalStringValue(highlighter, JSON_KEY_HIGHLIGHTER_SIMPLE_POST);
583            if (null != post) {
584                hlParams.put("simple.post", post);
585                hlParams.put("tag.post", post);
586            }
587            String formatter = parseOptionalStringValue(highlighter, JSON_KEY_HIGHLIGHTER_FORMATTER);
588            if (null != formatter) {
589                hlParams.put("formatter", formatter);
590            }
591            String fragmenter = parseOptionalStringValue(highlighter, JSON_KEY_HIGHLIGHTER_FRAGMENTER);
592            if (null != fragmenter) {
593                hlParams.put("fragmenter", fragmenter);
594            }
595            Boolean useFastVectorHighlighting = parseOptionalBooleanValue(
596                highlighter,
597                JSON_KEY_HIGHLIGHTER_FASTVECTORHIGHLIGHTING);
598            if (null != useFastVectorHighlighting) {
599                hlParams.put("method", "fastVector");
600            }
601            JSONObject params = parseOptionalJSONObject(highlighter, "params");
602            if (null != params) {
603                Iterator<String> it = params.keys();
604                while (it.hasNext()) {
605                    String key = it.next();
606                    String value = parseOptionalAtomicValue(params, key);
607                    if (value != null) {
608                        hlParams.put(key, value);
609                    } else {
610                        LOG.warn("Invalid highlight option " + key + "=" + value + " will be ignored.");
611                    }
612                }
613            }
614            return new CmsSearchConfigurationHighlighting(hlParams);
615        } catch (JSONException e) {
616            if (null == m_baseConfig) {
617                if (LOG.isInfoEnabled()) {
618                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_HIGHLIGHTING_CONFIG_0), e);
619                }
620                return null;
621            }
622            return m_baseConfig.getHighlighterConfig();
623        }
624    }
625
626    /**
627     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parsePagination()
628     */
629    @Override
630    public I_CmsSearchConfigurationPagination parsePagination() {
631
632        return CmsSearchConfigurationPagination.create(getPageParam(), getPageSizes(), getPageNavLength());
633    }
634
635    /**
636     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseQueryFacet()
637     */
638    @Override
639    public I_CmsSearchConfigurationFacetQuery parseQueryFacet() {
640
641        try {
642            JSONObject queryFacetObject = m_configObject.getJSONObject(JSON_KEY_QUERY_FACET);
643            try {
644                List<I_CmsFacetQueryItem> queries = parseFacetQueryItems(queryFacetObject);
645                String label = parseOptionalStringValue(queryFacetObject, JSON_KEY_FACET_LABEL);
646                Boolean isAndFacet = parseOptionalBooleanValue(queryFacetObject, JSON_KEY_FACET_ISANDFACET);
647                List<String> preselection = parseOptionalStringValues(queryFacetObject, JSON_KEY_FACET_PRESELECTION);
648                Boolean ignoreAllFacetFilters = parseOptionalBooleanValue(
649                    queryFacetObject,
650                    JSON_KEY_FACET_IGNOREALLFACETFILTERS);
651                List<String> excludeTags = parseOptionalStringValues(queryFacetObject, JSON_KEY_FACET_EXCLUDETAGS);
652                return new CmsSearchConfigurationFacetQuery(
653                    queries,
654                    label,
655                    isAndFacet,
656                    preselection,
657                    ignoreAllFacetFilters,
658                    excludeTags);
659            } catch (JSONException e) {
660                LOG.error(
661                    Messages.get().getBundle().key(
662                        Messages.ERR_QUERY_FACET_MANDATORY_KEY_MISSING_1,
663                        JSON_KEY_QUERY_FACET_QUERY),
664                    e);
665                return null;
666            }
667        } catch (JSONException e) {
668            // nothing to do, configuration is optional
669            return null != m_baseConfig ? m_baseConfig.getQueryFacetConfig() : null;
670        }
671    }
672
673    /**
674     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseRangeFacets()
675     */
676    @Override
677    public Map<String, I_CmsSearchConfigurationFacetRange> parseRangeFacets() {
678
679        Map<String, I_CmsSearchConfigurationFacetRange> facetConfigs = new LinkedHashMap<>();
680        try {
681            JSONArray rangeFacets = m_configObject.getJSONArray(JSON_KEY_RANGE_FACETS);
682            for (int i = 0; i < rangeFacets.length(); i++) {
683
684                I_CmsSearchConfigurationFacetRange config = parseRangeFacet(rangeFacets.getJSONObject(i));
685                if (config != null) {
686                    facetConfigs.put(config.getName(), config);
687                }
688            }
689        } catch (JSONException e) {
690            if (null == m_baseConfig) {
691                if (LOG.isInfoEnabled()) {
692                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_FACET_CONFIG_0), e);
693                }
694            } else {
695                facetConfigs = m_baseConfig.getRangeFacetConfigs();
696            }
697        }
698        return facetConfigs;
699
700    }
701
702    /**
703     * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseSorting()
704     */
705    @Override
706    public I_CmsSearchConfigurationSorting parseSorting() {
707
708        List<I_CmsSearchConfigurationSortOption> options = getSortOptions();
709        String defaultOptionParamValue = parseOptionalStringValue(m_configObject, JSON_KEY_DEFAULT_SORT_OPTION);
710        I_CmsSearchConfigurationSortOption defaultSortOption = null;
711        if (null != defaultOptionParamValue) {
712            Iterator<I_CmsSearchConfigurationSortOption> optIterator = options.iterator();
713            while ((null == defaultSortOption) && optIterator.hasNext()) {
714                I_CmsSearchConfigurationSortOption opt = optIterator.next();
715                if (Objects.equals(opt.getParamValue(), defaultOptionParamValue)) {
716                    defaultSortOption = opt;
717                }
718            }
719        }
720        if ((null == defaultSortOption) && !options.isEmpty()) {
721            defaultSortOption = options.get(0);
722        }
723        return CmsSearchConfigurationSorting.create(getSortParam(), options, defaultSortOption);
724    }
725
726    /** Returns a map with additional request parameters, mapping the parameter names to Solr query parts.
727     * @return A map with additional request parameters, mapping the parameter names to Solr query parts.
728     */
729    protected Map<String, String> getAdditionalParameters() {
730
731        Map<String, String> result;
732        try {
733            JSONArray additionalParams = m_configObject.getJSONArray(JSON_KEY_ADDITIONAL_PARAMETERS);
734            result = new HashMap<>(additionalParams.length());
735            for (int i = 0; i < additionalParams.length(); i++) {
736                try {
737                    JSONObject currentParam = additionalParams.getJSONObject(i);
738                    String param = currentParam.getString(JSON_KEY_ADDITIONAL_PARAMETERS_PARAM);
739                    String solrQuery = parseOptionalStringValue(currentParam, JSON_KEY_ADDITIONAL_PARAMETERS_SOLRQUERY);
740                    result.put(param, solrQuery);
741                } catch (JSONException e) {
742                    LOG.error(Messages.get().getBundle().key(Messages.ERR_ADDITIONAL_PARAMETER_CONFIG_WRONG_0), e);
743                    continue;
744                }
745            }
746        } catch (JSONException e) {
747            LOG.info(Messages.get().getBundle().key(Messages.LOG_ADDITIONAL_PARAMETER_CONFIG_NOT_PARSED_0), e);
748            return null != m_baseConfig ? m_baseConfig.getGeneralConfig().getAdditionalParameters() : new HashMap<>();
749        }
750        return result;
751    }
752
753    /** Returns the configured Solr core, or <code>null</code> if no core is configured.
754     * @return The configured Solr core, or <code>null</code> if no core is configured.
755     */
756    protected String getCore() {
757
758        try {
759            return m_configObject.getString(JSON_KEY_CORE);
760        } catch (JSONException e) {
761            if (null == m_baseConfig) {
762                if (LOG.isInfoEnabled()) {
763                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_CORE_SPECIFIED_0), e);
764                }
765                return null;
766            }
767            return m_baseConfig.getGeneralConfig().getSolrCore();
768        }
769    }
770
771    /**
772     * Returns the flag, indicating if the characters in the query string that are commands to Solr should be escaped.
773     * @return the flag, indicating if the characters in the query string that are commands to Solr should be escaped.
774     */
775    protected Boolean getEscapeQueryChars() {
776
777        Boolean isEscape = parseOptionalBooleanValue(m_configObject, JSON_KEY_ESCAPE_QUERY_CHARACTERS);
778        return (null == isEscape) && (m_baseConfig != null)
779        ? Boolean.valueOf(m_baseConfig.getGeneralConfig().getEscapeQueryChars())
780        : isEscape;
781    }
782
783    /** Returns the configured extra parameters that should be given to Solr, or the empty string if no parameters are configured.
784     * @return The configured extra parameters that should be given to Solr, or the empty string if no parameters are configured.
785     */
786    protected String getExtraSolrParams() {
787
788        try {
789            return m_configObject.getString(JSON_KEY_EXTRASOLRPARAMS);
790        } catch (JSONException e) {
791            if (null == m_baseConfig) {
792                if (LOG.isInfoEnabled()) {
793                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_EXTRA_PARAMETERS_0), e);
794                }
795                return "";
796            }
797            return m_baseConfig.getGeneralConfig().getExtraSolrParams();
798        }
799    }
800
801    /** Returns the configured request parameter for the last query, or the default parameter if no core is configured.
802    * @return The configured request parameter for the last query, or the default parameter if no core is configured.
803    */
804    protected String getFirstCallParam() {
805
806        String param = parseOptionalStringValue(m_configObject, JSON_KEY_RELOADED_PARAM);
807        if (param == null) {
808            return null != m_baseConfig ? m_baseConfig.getGeneralConfig().getReloadedParam() : DEFAULT_RELOADED_PARAM;
809        }
810        return param;
811    }
812
813    /** Returns a flag indicating if also expired resources should be found.
814     * @return A flag indicating if also expired resources should be found.
815     */
816    protected Boolean getIgnoreExpirationDate() {
817
818        Boolean isIgnoreExpirationDate = parseOptionalBooleanValue(m_configObject, JSON_KEY_IGNORE_EXPIRATION_DATE);
819        return (null == isIgnoreExpirationDate) && (m_baseConfig != null)
820        ? Boolean.valueOf(m_baseConfig.getGeneralConfig().getIgnoreExpirationDate())
821        : isIgnoreExpirationDate;
822    }
823
824    /** Returns a flag indicating if the query given by the parameters should be ignored.
825     * @return A flag indicating if the query given by the parameters should be ignored.
826     */
827    protected Boolean getIgnoreQuery() {
828
829        Boolean isIgnoreQuery = parseOptionalBooleanValue(m_configObject, JSON_KEY_IGNORE_QUERY);
830        return (null == isIgnoreQuery) && (m_baseConfig != null)
831        ? Boolean.valueOf(m_baseConfig.getGeneralConfig().getIgnoreQueryParam())
832        : isIgnoreQuery;
833    }
834
835    /** Returns a flag indicating if also unreleased resources should be found.
836     * @return A flag indicating if also unreleased resources should be found.
837     */
838    protected Boolean getIgnoreReleaseDate() {
839
840        Boolean isIgnoreReleaseDate = parseOptionalBooleanValue(m_configObject, JSON_KEY_IGNORE_RELEASE_DATE);
841        return (null == isIgnoreReleaseDate) && (m_baseConfig != null)
842        ? Boolean.valueOf(m_baseConfig.getGeneralConfig().getIgnoreReleaseDate())
843        : isIgnoreReleaseDate;
844    }
845
846    /** Returns the configured Solr index, or <code>null</code> if no core is configured.
847     * @param cms the current context.
848     * @return The configured Solr index, or <code>null</code> if no core is configured.
849     */
850    protected String getIndex(CmsObject cms) {
851
852        String indexName = null;
853        try {
854            indexName = m_configObject.getString(JSON_KEY_INDEX);
855        } catch (JSONException e) {
856            if (null == m_baseConfig) {
857                if (LOG.isInfoEnabled()) {
858                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_INDEX_SPECIFIED_0), e);
859                }
860            } else {
861                indexName = m_baseConfig.getGeneralConfig().getSolrIndex();
862            }
863        }
864        return null != indexName
865        ? indexName
866        : (cms.getRequestContext().getCurrentProject().isOnlineProject()
867        ? CmsSolrIndex.DEFAULT_INDEX_NAME_ONLINE
868        : CmsSolrIndex.DEFAULT_INDEX_NAME_OFFLINE);
869    }
870
871    /** Returns the configured request parameter for the last query, or the default parameter if no core is configured.
872    * @return The configured request parameter for the last query, or the default parameter if no core is configured.
873    */
874    protected String getLastQueryParam() {
875
876        String param = parseOptionalStringValue(m_configObject, JSON_KEY_LAST_QUERYPARAM);
877        if (param == null) {
878            return null != m_baseConfig
879            ? m_baseConfig.getGeneralConfig().getLastQueryParam()
880            : DEFAULT_LAST_QUERY_PARAM;
881        }
882        return param;
883    }
884
885    /** Returns the number of maximally returned results, or <code>null</code> if the indexes default should be used.
886     * @param indexName the name of the index to search in.
887     * @return The number of maximally returned results, or <code>null</code> if the indexes default should be used.
888     */
889    protected int getMaxReturnedResults(String indexName) {
890
891        Integer maxReturnedResults = parseOptionalIntValue(m_configObject, JSON_KEY_MAX_RETURNED_RESULTS);
892        if (null != maxReturnedResults) {
893            return maxReturnedResults.intValue();
894        } else if (m_baseConfig != null) {
895            return m_baseConfig.getGeneralConfig().getMaxReturnedResults();
896        } else {
897            try {
898                CmsSolrIndex idx = OpenCms.getSearchManager().getIndexSolr(indexName);
899                if (null != idx) {
900                    return idx.getMaxProcessedResults();
901                }
902            } catch (Throwable t) {
903                // This is ok, it's allowed to have an external other index here.
904                LOG.debug(
905                    "Parsing JSON search configuration for none-CmsSolrIndex "
906                        + indexName
907                        + ". Setting max processed results to unlimited.");
908            }
909            return CmsSolrIndex.MAX_RESULTS_UNLIMITED;
910        }
911    }
912
913    /** Returns the configured length of the "Google"-like page navigation, or the default parameter if no core is configured.
914     * @return The configured length of the "Google"-like page navigation, or the default parameter if no core is configured.
915     */
916    protected Integer getPageNavLength() {
917
918        return parseOptionalIntValue(m_configObject, JSON_KEY_PAGENAVLENGTH);
919    }
920
921    /** Returns the configured request parameter for the current page, or the default parameter if no core is configured.
922     * @return The configured request parameter for the current page, or the default parameter if no core is configured.
923     */
924    protected String getPageParam() {
925
926        return parseOptionalStringValue(m_configObject, JSON_KEY_PAGEPARAM);
927    }
928
929    /** Returns the configured page sizes, or the default page size if no core is configured.
930     * @return The configured page sizes, or the default page size if no core is configured.
931     */
932    protected List<Integer> getPageSizes() {
933
934        if (m_configObject.has(JSON_KEY_PAGESIZE)) {
935            try {
936                return Collections.singletonList(Integer.valueOf(m_configObject.getInt(JSON_KEY_PAGESIZE)));
937            } catch (JSONException e) {
938                List<Integer> result = null;
939                String pageSizesString = null;
940                try {
941                    pageSizesString = m_configObject.getString(JSON_KEY_PAGESIZE);
942                    String[] pageSizesArray = pageSizesString.split("-");
943                    if (pageSizesArray.length > 0) {
944                        result = new ArrayList<>(pageSizesArray.length);
945                        for (int i = 0; i < pageSizesArray.length; i++) {
946                            result.add(Integer.valueOf(pageSizesArray[i]));
947                        }
948                    }
949                    return result;
950                } catch (NumberFormatException | JSONException e1) {
951                    LOG.warn(
952                        Messages.get().getBundle().key(Messages.LOG_PARSING_PAGE_SIZES_FAILED_1, pageSizesString),
953                        e);
954                }
955            }
956        }
957        if (null == m_baseConfig) {
958            if (LOG.isInfoEnabled()) {
959                LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_PAGESIZE_SPECIFIED_0));
960            }
961            return null;
962        }
963        return m_baseConfig.getPaginationConfig().getPageSizes();
964    }
965
966    /** Returns the optional query modifier.
967     * @return the optional query modifier.
968     */
969    protected String getQueryModifier() {
970
971        String queryModifier = parseOptionalStringValue(m_configObject, JSON_KEY_QUERY_MODIFIER);
972        return (null == queryModifier) && (null != m_baseConfig)
973        ? m_baseConfig.getGeneralConfig().getQueryModifier()
974        : queryModifier;
975    }
976
977    /** Returns the configured request parameter for the query string, or the default parameter if no core is configured.
978     * @return The configured request parameter for the query string, or the default parameter if no core is configured.
979     */
980    protected String getQueryParam() {
981
982        String param = parseOptionalStringValue(m_configObject, JSON_KEY_QUERYPARAM);
983        if (param == null) {
984            return null != m_baseConfig ? m_baseConfig.getGeneralConfig().getQueryParam() : DEFAULT_QUERY_PARAM;
985        }
986        return param;
987    }
988
989    /** Returns a flag, indicating if search should be performed using a wildcard if the empty query is given.
990     * @return A flag, indicating if search should be performed using a wildcard if the empty query is given.
991     */
992    protected Boolean getSearchForEmptyQuery() {
993
994        Boolean isSearchForEmptyQuery = parseOptionalBooleanValue(m_configObject, JSON_KEY_SEARCH_FOR_EMPTY_QUERY);
995        return (isSearchForEmptyQuery == null) && (null != m_baseConfig)
996        ? Boolean.valueOf(m_baseConfig.getGeneralConfig().getSearchForEmptyQueryParam())
997        : isSearchForEmptyQuery;
998    }
999
1000    /** Returns the list of the configured sort options, or the empty list if no sort options are configured.
1001     * @return The list of the configured sort options, or the empty list if no sort options are configured.
1002     */
1003    protected List<I_CmsSearchConfigurationSortOption> getSortOptions() {
1004
1005        List<I_CmsSearchConfigurationSortOption> options = new LinkedList<>();
1006        try {
1007            JSONArray sortOptions = m_configObject.getJSONArray(JSON_KEY_SORTOPTIONS);
1008            for (int i = 0; i < sortOptions.length(); i++) {
1009                I_CmsSearchConfigurationSortOption option = parseSortOption(sortOptions.getJSONObject(i));
1010                if (option != null) {
1011                    options.add(option);
1012                }
1013            }
1014        } catch (JSONException e) {
1015            if (null == m_baseConfig) {
1016                if (LOG.isInfoEnabled()) {
1017                    LOG.info(Messages.get().getBundle().key(Messages.LOG_NO_SORT_CONFIG_0), e);
1018                }
1019            } else {
1020                options = m_baseConfig.getSortConfig().getSortOptions();
1021            }
1022        }
1023        return options;
1024    }
1025
1026    /** Returns the configured request parameter for the sort option, or the default parameter if no core is configured.
1027     * @return The configured request parameter for the sort option, or the default parameter if no core is configured.
1028     */
1029    protected String getSortParam() {
1030
1031        return parseOptionalStringValue(m_configObject, JSON_KEY_SORTPARAM);
1032    }
1033
1034    /** Initialization that parses the String to a JSON object.
1035     * @param configString The JSON as string.
1036     * @param baseConfig The optional basic search configuration to overwrite (partly) by the JSON configuration.
1037     * @throws JSONException thrown if parsing fails.
1038     */
1039    protected void init(String configString, I_CmsSearchConfiguration baseConfig) throws JSONException {
1040
1041        m_configObject = new JSONObject(configString);
1042        m_baseConfig = baseConfig;
1043    }
1044
1045    /** Parses a single query item for the query facet.
1046     * @param item JSON object of the query item.
1047     * @return the parsed query item, or <code>null</code> if parsing failed.
1048     */
1049    protected I_CmsFacetQueryItem parseFacetQueryItem(JSONObject item) {
1050
1051        String query;
1052        try {
1053            query = item.getString(JSON_KEY_QUERY_FACET_QUERY_QUERY);
1054        } catch (JSONException e) {
1055            // TODO: Log
1056            return null;
1057        }
1058        String label = parseOptionalStringValue(item, JSON_KEY_QUERY_FACET_QUERY_LABEL);
1059        return new CmsFacetQueryItem(query, label);
1060    }
1061
1062    /** Parses the list of query items for the query facet.
1063     * @param queryFacetObject JSON object representing the node with the query facet.
1064     * @return list of query options
1065     * @throws JSONException if the list cannot be parsed.
1066     */
1067    protected List<I_CmsFacetQueryItem> parseFacetQueryItems(JSONObject queryFacetObject) throws JSONException {
1068
1069        JSONArray items = queryFacetObject.getJSONArray(JSON_KEY_QUERY_FACET_QUERY);
1070        List<I_CmsFacetQueryItem> result = new ArrayList<>(items.length());
1071        for (int i = 0; i < items.length(); i++) {
1072            I_CmsFacetQueryItem item = parseFacetQueryItem(items.getJSONObject(i));
1073            if (item != null) {
1074                result.add(item);
1075            }
1076        }
1077        return result;
1078    }
1079
1080    /** Parses the field facet configurations.
1081     * @param fieldFacetObject The JSON sub-node with the field facet configurations.
1082     * @return The field facet configurations.
1083     */
1084    protected I_CmsSearchConfigurationFacetField parseFieldFacet(JSONObject fieldFacetObject) {
1085
1086        try {
1087            String field = fieldFacetObject.getString(JSON_KEY_FACET_FIELD);
1088            String name = parseOptionalStringValue(fieldFacetObject, JSON_KEY_FACET_NAME);
1089            String label = parseOptionalStringValue(fieldFacetObject, JSON_KEY_FACET_LABEL);
1090            Integer minCount = parseOptionalIntValue(fieldFacetObject, JSON_KEY_FACET_MINCOUNT);
1091            Integer limit = parseOptionalIntValue(fieldFacetObject, JSON_KEY_FACET_LIMIT);
1092            String prefix = parseOptionalStringValue(fieldFacetObject, JSON_KEY_FACET_PREFIX);
1093            String sorder = parseOptionalStringValue(fieldFacetObject, JSON_KEY_FACET_ORDER);
1094            I_CmsSearchConfigurationFacet.SortOrder order;
1095            try {
1096                order = I_CmsSearchConfigurationFacet.SortOrder.valueOf(sorder);
1097            } catch (Exception e) {
1098                order = null;
1099            }
1100            String filterQueryModifier = parseOptionalStringValue(fieldFacetObject, JSON_KEY_FACET_FILTERQUERYMODIFIER);
1101            Boolean isAndFacet = parseOptionalBooleanValue(fieldFacetObject, JSON_KEY_FACET_ISANDFACET);
1102            List<String> preselection = parseOptionalStringValues(fieldFacetObject, JSON_KEY_FACET_PRESELECTION);
1103            Boolean ignoreFilterAllFacetFilters = parseOptionalBooleanValue(
1104                fieldFacetObject,
1105                JSON_KEY_FACET_IGNOREALLFACETFILTERS);
1106            List<String> excludeTags = parseOptionalStringValues(fieldFacetObject, JSON_KEY_FACET_EXCLUDETAGS);
1107            return new CmsSearchConfigurationFacetField(
1108                field,
1109                name,
1110                minCount,
1111                limit,
1112                prefix,
1113                label,
1114                order,
1115                filterQueryModifier,
1116                isAndFacet,
1117                preselection,
1118                ignoreFilterAllFacetFilters,
1119                excludeTags);
1120        } catch (JSONException e) {
1121            LOG.error(
1122                Messages.get().getBundle().key(Messages.ERR_FIELD_FACET_MANDATORY_KEY_MISSING_1, JSON_KEY_FACET_FIELD),
1123                e);
1124            return null;
1125        }
1126    }
1127
1128    /** Parses the query facet configurations.
1129     * @param rangeFacetObject The JSON sub-node with the query facet configurations.
1130     * @return The query facet configurations.
1131     */
1132    protected I_CmsSearchConfigurationFacetRange parseRangeFacet(JSONObject rangeFacetObject) {
1133
1134        try {
1135            String range = rangeFacetObject.getString(JSON_KEY_RANGE_FACET_RANGE);
1136            String name = parseOptionalStringValue(rangeFacetObject, JSON_KEY_FACET_NAME);
1137            String label = parseOptionalStringValue(rangeFacetObject, JSON_KEY_FACET_LABEL);
1138            Integer minCount = parseOptionalIntValue(rangeFacetObject, JSON_KEY_FACET_MINCOUNT);
1139            String start = rangeFacetObject.getString(JSON_KEY_RANGE_FACET_START);
1140            String end = rangeFacetObject.getString(JSON_KEY_RANGE_FACET_END);
1141            String gap = rangeFacetObject.getString(JSON_KEY_RANGE_FACET_GAP);
1142            List<String> sother = parseOptionalStringValues(rangeFacetObject, JSON_KEY_RANGE_FACET_OTHER);
1143            Boolean hardEnd = parseOptionalBooleanValue(rangeFacetObject, JSON_KEY_RANGE_FACET_HARDEND);
1144            I_CmsSearchConfigurationFacetRange.Method method = null;
1145            String methodStr = parseOptionalStringValue(rangeFacetObject, JSON_KEY_RANGE_FACET_METHOD);
1146            if (null != methodStr) {
1147                try {
1148                    method = I_CmsSearchConfigurationFacetRange.Method.valueOf(methodStr);
1149                } catch (Exception e) {
1150                    LOG.error(Messages.get().getBundle().key(Messages.ERR_INVALID_RANGE_METHOD_OPTION_1, methodStr), e);
1151                }
1152            }
1153            List<I_CmsSearchConfigurationFacetRange.Other> other = null;
1154            if (sother != null) {
1155                other = new ArrayList<>(sother.size());
1156                for (String so : sother) {
1157                    try {
1158                        I_CmsSearchConfigurationFacetRange.Other o = I_CmsSearchConfigurationFacetRange.Other.valueOf(
1159                            so);
1160                        other.add(o);
1161                    } catch (Exception e) {
1162                        LOG.error(Messages.get().getBundle().key(Messages.ERR_INVALID_OTHER_OPTION_1, so), e);
1163                    }
1164                }
1165            }
1166            Boolean isAndFacet = parseOptionalBooleanValue(rangeFacetObject, JSON_KEY_FACET_ISANDFACET);
1167            List<String> preselection = parseOptionalStringValues(rangeFacetObject, JSON_KEY_FACET_PRESELECTION);
1168            Boolean ignoreAllFacetFilters = parseOptionalBooleanValue(
1169                rangeFacetObject,
1170                JSON_KEY_FACET_IGNOREALLFACETFILTERS);
1171            List<String> excludeTags = parseOptionalStringValues(rangeFacetObject, JSON_KEY_FACET_EXCLUDETAGS);
1172            return new CmsSearchConfigurationFacetRange(
1173                range,
1174                start,
1175                end,
1176                gap,
1177                other,
1178                hardEnd,
1179                method,
1180                name,
1181                minCount,
1182                label,
1183                isAndFacet,
1184                preselection,
1185                ignoreAllFacetFilters,
1186                excludeTags);
1187        } catch (JSONException e) {
1188            LOG.error(
1189                Messages.get().getBundle().key(
1190                    Messages.ERR_RANGE_FACET_MANDATORY_KEY_MISSING_1,
1191                    JSON_KEY_RANGE_FACET_RANGE
1192                        + ", "
1193                        + JSON_KEY_RANGE_FACET_START
1194                        + ", "
1195                        + JSON_KEY_RANGE_FACET_END
1196                        + ", "
1197                        + JSON_KEY_RANGE_FACET_GAP),
1198                e);
1199            return null;
1200        }
1201
1202    }
1203
1204    /** Returns a single sort option configuration as configured via the methods parameter, or null if the parameter does not specify a sort option.
1205     * @param json The JSON sort option configuration.
1206     * @return The sort option configuration, or null if the JSON could not be read.
1207     */
1208    protected I_CmsSearchConfigurationSortOption parseSortOption(JSONObject json) {
1209
1210        try {
1211            String solrValue = json.getString(JSON_KEY_SORTOPTION_SOLRVALUE);
1212            String paramValue = parseOptionalStringValue(json, JSON_KEY_SORTOPTION_PARAMVALUE);
1213            paramValue = (paramValue == null) ? solrValue : paramValue;
1214            String label = parseOptionalStringValue(json, JSON_KEY_SORTOPTION_LABEL);
1215            label = (label == null) ? paramValue : label;
1216            return new CmsSearchConfigurationSortOption(label, paramValue, solrValue);
1217        } catch (JSONException e) {
1218            LOG.error(
1219                Messages.get().getBundle().key(Messages.ERR_SORT_OPTION_NOT_PARSABLE_1, JSON_KEY_SORTOPTION_SOLRVALUE),
1220                e);
1221            return null;
1222        }
1223    }
1224}