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.file.CmsPropertyDefinition;
032import org.opencms.json.JSONException;
033import org.opencms.jsp.search.config.CmsSearchConfigurationFacetField;
034import org.opencms.jsp.search.config.CmsSearchConfigurationFacetRange;
035import org.opencms.jsp.search.config.CmsSearchConfigurationSortOption;
036import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacet.SortOrder;
037import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetField;
038import org.opencms.jsp.search.config.I_CmsSearchConfigurationFacetRange;
039import org.opencms.jsp.search.config.I_CmsSearchConfigurationPagination;
040import org.opencms.jsp.search.config.I_CmsSearchConfigurationSortOption;
041import org.opencms.main.CmsException;
042import org.opencms.relations.CmsCategoryService;
043import org.opencms.search.fields.CmsSearchField;
044import org.opencms.search.solr.CmsSolrQuery;
045import org.opencms.ui.apps.lists.CmsListManager;
046import org.opencms.ui.apps.lists.CmsListManager.ListConfigurationBean;
047import org.opencms.ui.apps.lists.daterestrictions.I_CmsListDateRestriction;
048import org.opencms.util.CmsStringUtil;
049import org.opencms.util.CmsUUID;
050import org.opencms.xml.types.CmsXmlDisplayFormatterValue;
051
052import java.util.Collections;
053import java.util.HashMap;
054import java.util.LinkedList;
055import java.util.List;
056import java.util.Locale;
057import java.util.Map;
058import java.util.Objects;
059
060import org.apache.solr.common.params.CommonParams;
061
062import com.google.common.collect.Lists;
063
064/**
065 * Search configuration parser using a list configuration file as the base configuration with additional JSON.<p>
066 */
067public class CmsSimpleSearchConfigurationParser extends CmsJSONSearchConfigurationParser {
068
069    /** Sort options that are available by default. */
070    public static enum SortOption {
071        /** Sort by date ascending. */
072        DATE_ASC,
073        /** Sort by date descending. */
074        DATE_DESC,
075        /** Sort by title ascending. */
076        TITLE_ASC,
077        /** Sort by title descending. */
078        TITLE_DESC,
079        /** Sort by order ascending. */
080        ORDER_ASC,
081        /** Sort by order descending. */
082        ORDER_DESC;
083
084        /**
085         * Generates the suitable {@link I_CmsSearchConfigurationSortOption} for the option.
086         * @param l the locale for which the option should be created
087         * @return the created {@link I_CmsSearchConfigurationSortOption}
088         */
089        public I_CmsSearchConfigurationSortOption getOption(Locale l) {
090
091            switch (this) {
092                case DATE_ASC:
093                    return new CmsSearchConfigurationSortOption("date.asc", "date_asc", getSortDateField(l) + " asc");
094                case DATE_DESC:
095                    return new CmsSearchConfigurationSortOption(
096                        "date.desc",
097                        "date_desc",
098                        getSortDateField(l) + " desc");
099                case TITLE_ASC:
100                    return new CmsSearchConfigurationSortOption(
101                        "title.asc",
102                        "title_asc",
103                        getSortTitleField(l) + " asc");
104                case TITLE_DESC:
105                    return new CmsSearchConfigurationSortOption(
106                        "title.desc",
107                        "title_desc",
108                        getSortTitleField(l) + " desc");
109                case ORDER_ASC:
110                    return new CmsSearchConfigurationSortOption(
111                        "order.asc",
112                        "order_asc",
113                        getSortOrderField(l) + " asc");
114                case ORDER_DESC:
115                    return new CmsSearchConfigurationSortOption(
116                        "order.desc",
117                        "order_desc",
118                        getSortOrderField(l) + " desc");
119                default:
120                    throw new IllegalArgumentException();
121            }
122        }
123
124        /**
125         * Returns the locale specific date field to use for sorting.
126         * @param l the locale to use, can be <code>null</code>
127         * @return the locale specific date field to use for sorting.
128         */
129        protected String getSortDateField(Locale l) {
130
131            return CmsSearchField.FIELD_INSTANCEDATE
132                + (null != l ? "_" + l.toString() : "")
133                + CmsSearchField.FIELD_POSTFIX_DATE;
134        }
135
136        /**
137         * Returns the locale specific order field to use for sorting.
138         * @param l the locale to use, can be <code>null</code>
139         * @return the locale specific order field to use for sorting.
140         */
141        protected String getSortOrderField(Locale l) {
142
143            return CmsSearchField.FIELD_DISPORDER
144                + (null != l ? "_" + l.toString() : "")
145                + CmsSearchField.FIELD_POSTFIX_INT;
146        }
147
148        /**
149         * Returns the locale specific title field to use for sorting.
150         * @param l the locale to use, can be <code>null</code>
151         * @return the locale specific title field to use for sorting.
152         */
153        protected String getSortTitleField(Locale l) {
154
155            return CmsSearchField.FIELD_DISPTITLE
156                + (null != l ? "_" + l.toString() : "")
157                + CmsSearchField.FIELD_POSTFIX_SORT;
158        }
159    }
160
161    /** Pagination which may override the default pagination. */
162    private I_CmsSearchConfigurationPagination m_pagination;
163
164    /** The current cms context. */
165    private CmsObject m_cms;
166
167    /** The list configuration bean. */
168    private ListConfigurationBean m_config;
169
170    /** The (mutable) search locale. */
171    private Locale m_searchLocale;
172
173    /** The (mutable) sort order. */
174    private CmsSimpleSearchConfigurationParser.SortOption m_sortOrder;
175
176    /** Flag which, if true, causes the search to ignore the blacklist. */
177    private boolean m_ignoreBlacklist;
178
179    /**
180     * Constructor.<p>
181     *
182     * @param cms the cms context
183     * @param config the list configuration
184     * @param additionalParamJSON the additional JSON configuration
185     *
186     * @throws JSONException in case parsing the JSON fails
187     */
188    public CmsSimpleSearchConfigurationParser(
189        CmsObject cms,
190        CmsListManager.ListConfigurationBean config,
191        String additionalParamJSON)
192    throws JSONException {
193
194        super(CmsStringUtil.isEmptyOrWhitespaceOnly(additionalParamJSON) ? "{}" : additionalParamJSON);
195        m_cms = cms;
196        m_config = config;
197    }
198
199    /**
200     * Creates an instance for an empty JSON configuration.<p>
201     *
202     * The point of this is that we know that passing an empty configuration makes it impossible
203     * for a JSONException to thrown.
204     *
205     * @param cms the current CMS context
206     * @param config  the search configuration
207     *
208     * @return the search config parser
209     */
210    public static CmsSimpleSearchConfigurationParser createInstanceWithNoJsonConfig(
211        CmsObject cms,
212        CmsListManager.ListConfigurationBean config) {
213
214        try {
215            return new CmsSimpleSearchConfigurationParser(cms, config, null);
216
217        } catch (JSONException e) {
218            return null;
219        }
220    }
221
222    /**
223     * Returns the initial SOLR query.<p>
224     *
225     * @return the SOLR query
226     */
227    public CmsSolrQuery getInitialQuery() {
228
229        Map<String, String[]> queryParams = new HashMap<String, String[]>();
230        if (!m_cms.getRequestContext().getCurrentProject().isOnlineProject() && m_config.isShowExpired()) {
231            queryParams.put("fq", new String[] {"released:[* TO *]", "expired:[* TO *]"});
232        }
233        return new CmsSolrQuery(null, queryParams);
234    }
235
236    /**
237     * Gets the search locale.<p>
238     *
239     * @return the search locale
240     */
241    public Locale getSearchLocale() {
242
243        if (m_searchLocale != null) {
244            return m_searchLocale;
245        }
246        return m_cms.getRequestContext().getLocale();
247    }
248
249    /**
250    * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseFieldFacets()
251    */
252    @Override
253    public Map<String, I_CmsSearchConfigurationFacetField> parseFieldFacets() {
254
255        if (m_configObject.has(JSON_KEY_FIELD_FACETS)) {
256            return super.parseFieldFacets();
257        } else {
258            return getDefaultFieldFacets(true);
259        }
260    }
261
262    /**
263     * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#parsePagination()
264     */
265    @Override
266    public I_CmsSearchConfigurationPagination parsePagination() {
267
268        if (m_pagination != null) {
269            return m_pagination;
270        }
271        return super.parsePagination();
272    }
273
274    /**
275    * @see org.opencms.jsp.search.config.parser.I_CmsSearchConfigurationParser#parseRangeFacets()
276    */
277    @Override
278    public Map<String, I_CmsSearchConfigurationFacetRange> parseRangeFacets() {
279
280        if (m_configObject.has(JSON_KEY_RANGE_FACETS)) {
281            return super.parseRangeFacets();
282        } else {
283            Map<String, I_CmsSearchConfigurationFacetRange> rangeFacets = new HashMap<String, I_CmsSearchConfigurationFacetRange>();
284            I_CmsSearchConfigurationFacetRange rangeFacet = new CmsSearchConfigurationFacetRange(
285                String.format(CmsListManager.FIELD_DATE, getSearchLocale().toString()),
286                "NOW/YEAR-20YEARS",
287                "NOW/MONTH+2YEARS",
288                "+1MONTHS",
289                null,
290                Boolean.FALSE,
291                CmsListManager.FIELD_DATE_FACET_NAME,
292                Integer.valueOf(1),
293                "Date",
294                Boolean.FALSE,
295                null,
296                Boolean.TRUE);
297
298            rangeFacets.put(rangeFacet.getName(), rangeFacet);
299            return rangeFacets;
300        }
301    }
302
303    /**
304     * Sets the 'ignore blacklist' flag.<p>
305     *
306     * If set, the search will ignore the blacklist from the list configuration.<p>
307     *
308     * @param ignoreBlacklist true if the blacklist should be ignored
309     */
310    public void setIgnoreBlacklist(boolean ignoreBlacklist) {
311
312        m_ignoreBlacklist = ignoreBlacklist;
313    }
314
315    /**
316     * Sets the pagination.<p>
317     *
318     * If this is set, parsePagination will always return the set value instead of using the default way to compute the pagination
319     *
320     * @param pagination the pagination
321     */
322    public void setPagination(I_CmsSearchConfigurationPagination pagination) {
323
324        m_pagination = pagination;
325    }
326
327    /**
328     * Sets the search locale.<p>
329     *
330     * @param locale the search locale
331     */
332    public void setSearchLocale(Locale locale) {
333
334        m_searchLocale = locale;
335    }
336
337    /**
338     * Sets the sort option.<p>
339     *
340     * @param sortOption the sort option
341     */
342    public void setSortOption(String sortOption) {
343
344        if (null != sortOption) {
345            try {
346                m_sortOrder = CmsSimpleSearchConfigurationParser.SortOption.valueOf(sortOption);
347            } catch (IllegalArgumentException e) {
348                m_sortOrder = null;
349                LOG.warn(
350                    "Setting illegal default sort option " + sortOption + " failed. Using Solr's default sort option.");
351            }
352        } else {
353            m_sortOrder = null;
354        }
355    }
356
357    /**
358     * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getEscapeQueryChars()
359     */
360    @Override
361    protected Boolean getEscapeQueryChars() {
362
363        if (m_configObject.has(JSON_KEY_ESCAPE_QUERY_CHARACTERS)) {
364            return super.getEscapeQueryChars();
365        } else {
366            return Boolean.TRUE;
367        }
368    }
369
370    /**
371     * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getExtraSolrParams()
372     */
373    @Override
374    protected String getExtraSolrParams() {
375
376        String params = super.getExtraSolrParams();
377        if (CmsStringUtil.isEmptyOrWhitespaceOnly(params)) {
378            params = getFolderFilter()
379                + getResourceTypeFilter()
380                + getCategoryFilter()
381                + getFilterQuery()
382                + getBlacklistFilter();
383        }
384        return params;
385    }
386
387    /**
388     * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getIgnoreExpirationDate()
389     */
390    @Override
391    protected Boolean getIgnoreExpirationDate() {
392
393        return getIgnoreReleaseAndExpiration();
394
395    }
396
397    /**
398     * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getIgnoreReleaseDate()
399     */
400    @Override
401    protected Boolean getIgnoreReleaseDate() {
402
403        return getIgnoreReleaseAndExpiration();
404    }
405
406    /**
407     * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getQueryModifier()
408     */
409    @Override
410    protected String getQueryModifier() {
411
412        String modifier = super.getQueryModifier();
413        if (CmsStringUtil.isEmptyOrWhitespaceOnly(modifier)) {
414            modifier = "{!type=edismax qf=\""
415                + CmsSearchField.FIELD_CONTENT
416                + "_"
417                + getSearchLocale().toString()
418                + " "
419                + CmsPropertyDefinition.PROPERTY_TITLE
420                + CmsSearchField.FIELD_DYNAMIC_PROPERTIES
421                + " "
422                + CmsPropertyDefinition.PROPERTY_DESCRIPTION
423                + CmsSearchField.FIELD_DYNAMIC_PROPERTIES_DIRECT
424                + " "
425                + CmsPropertyDefinition.PROPERTY_DESCRIPTION_HTML
426                + CmsSearchField.FIELD_DYNAMIC_PROPERTIES_DIRECT
427                + " "
428                + CmsSearchField.FIELD_SPELL
429                + "\"}%(query)";
430        }
431        return modifier;
432    }
433
434    /**
435     * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getSearchForEmptyQuery()
436     */
437    @Override
438    protected Boolean getSearchForEmptyQuery() {
439
440        if (m_configObject.has(JSON_KEY_SEARCH_FOR_EMPTY_QUERY)) {
441            return super.getSearchForEmptyQuery();
442        } else {
443            return Boolean.TRUE;
444        }
445    }
446
447    /**
448     * @see org.opencms.jsp.search.config.parser.CmsJSONSearchConfigurationParser#getSortOptions()
449     */
450    @Override
451    protected List<I_CmsSearchConfigurationSortOption> getSortOptions() {
452
453        if (m_configObject.has(JSON_KEY_SORTOPTIONS)) {
454            return super.getSortOptions();
455        } else {
456            List<I_CmsSearchConfigurationSortOption> options = new LinkedList<I_CmsSearchConfigurationSortOption>();
457
458            CmsSimpleSearchConfigurationParser.SortOption currentOption = CmsSimpleSearchConfigurationParser.SortOption.valueOf(
459                m_config.getSortOrder());
460            if (m_sortOrder != null) {
461                currentOption = m_sortOrder;
462            }
463            Locale locale = getSearchLocale();
464            options.add(currentOption.getOption(locale));
465            CmsSimpleSearchConfigurationParser.SortOption[] sortOptions = CmsSimpleSearchConfigurationParser.SortOption.values();
466            for (int i = 0; i < sortOptions.length; i++) {
467                CmsSimpleSearchConfigurationParser.SortOption option = sortOptions[i];
468                if (!Objects.equals(currentOption, option)) {
469                    options.add(option.getOption(locale));
470                }
471            }
472            return options;
473        }
474    }
475
476    /**
477     * Returns the blacklist filter.<p>
478     *
479     * @return the blacklist filter
480     */
481    String getBlacklistFilter() {
482
483        if (m_ignoreBlacklist) {
484            return "";
485        }
486        String result = "";
487        List<CmsUUID> blacklist = m_config.getBlacklist();
488        List<String> blacklistStrings = Lists.newArrayList();
489        for (CmsUUID id : blacklist) {
490            blacklistStrings.add("\"" + id.toString() + "\"");
491        }
492        if (!blacklistStrings.isEmpty()) {
493            result = "&fq=-id:(" + CmsStringUtil.listAsString(blacklistStrings, " OR ") + ")";
494        }
495        return result;
496    }
497
498    /**
499     * Returns the category filter string.<p>
500     *
501     * @return the category filter
502     */
503    String getCategoryFilter() {
504
505        String result = "";
506        if (!m_config.getCategories().isEmpty()) {
507            List<String> categoryVals = Lists.newArrayList();
508            for (String path : m_config.getCategories()) {
509                try {
510                    path = CmsCategoryService.getInstance().getCategory(
511                        m_cms,
512                        m_cms.getRequestContext().addSiteRoot(path)).getPath();
513                    categoryVals.add("\"" + path + "\"");
514                } catch (CmsException e) {
515                    LOG.warn(e.getLocalizedMessage(), e);
516                }
517            }
518            if (!categoryVals.isEmpty()) {
519                String operator = " " + m_config.getCategoryMode() + " ";
520                String valueExpression = CmsStringUtil.listAsString(categoryVals, operator);
521                result = "&fq=category_exact:(" + valueExpression + ")";
522
523            }
524        }
525        return result;
526    }
527
528    /**
529     * The fields returned by default. Typically the output is done via display formatters and hence nearly no
530     * field is necessary. Returning all fields might cause performance problems.
531     *
532     * @return the default return fields.
533     */
534    String getDefaultReturnFields() {
535
536        StringBuffer fields = new StringBuffer("");
537        fields.append(CmsSearchField.FIELD_PATH);
538        fields.append(',');
539        fields.append(CmsSearchField.FIELD_INSTANCEDATE).append('_').append(getSearchLocale().toString()).append("_dt");
540        fields.append(',');
541        fields.append(CmsSearchField.FIELD_INSTANCEDATE_END).append('_').append(getSearchLocale().toString()).append(
542            "_dt");
543        fields.append(',');
544        fields.append(CmsSearchField.FIELD_INSTANCEDATE_CURRENT_TILL).append('_').append(
545            getSearchLocale().toString()).append("_dt");
546        fields.append(',');
547        fields.append(CmsSearchField.FIELD_ID);
548        fields.append(',');
549        fields.append(CmsSearchField.FIELD_SOLR_ID);
550        fields.append(',');
551        fields.append(CmsSearchField.FIELD_DISPTITLE).append('_').append(getSearchLocale().toString()).append("_sort");
552        fields.append(',');
553        fields.append(CmsSearchField.FIELD_LINK);
554        return fields.toString();
555    }
556
557    /**
558     * Returns the filter query string.<p>
559     *
560     * @return the filter query
561     */
562    String getFilterQuery() {
563
564        String result = m_config.getFilterQuery();
565        if (result == null) {
566            result = "";
567        }
568        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(result) && !result.startsWith("&")) {
569            result = "&" + result;
570        }
571        if (!result.contains(CommonParams.FL + "=")) {
572            result += "&" + CommonParams.FL + "=" + getDefaultReturnFields();
573        }
574        I_CmsListDateRestriction dateRestriction = m_config.getDateRestriction();
575        if (dateRestriction != null) {
576            result += "&fq="
577                + CmsSearchField.FIELD_INSTANCEDATE_CURRENT_TILL
578                + "_"
579                + getSearchLocale().toString()
580                + "_dt:"
581                + dateRestriction.getRange();
582
583        }
584        return result;
585    }
586
587    /**
588     * Returns the folder filter string.<p>
589     *
590     * @return the folder filter
591     */
592    String getFolderFilter() {
593
594        String result = "";
595        List<String> parentFolderVals = Lists.newArrayList();
596        if (!m_config.getFolders().isEmpty()) {
597            for (String value : m_config.getFolders()) {
598                parentFolderVals.add("\"" + value + "\"");
599            }
600        }
601        if (parentFolderVals.isEmpty()) {
602            result = "fq=parent-folders:(\"/\")";
603        } else {
604            result = "fq=parent-folders:(" + CmsStringUtil.listAsString(parentFolderVals, " OR ") + ")";
605        }
606        return result;
607    }
608
609    /**
610     * Returns the resource type filter string.<p>
611     *
612     * @return the folder filter
613     */
614    String getResourceTypeFilter() {
615
616        String result = "";
617        List<String> typeVals = Lists.newArrayList();
618        if (!m_config.getDisplayTypes().isEmpty()) {
619            for (String displayType : m_config.getDisplayTypes()) {
620                if (displayType.contains(CmsXmlDisplayFormatterValue.SEPARATOR)) {
621                    displayType = displayType.substring(0, displayType.indexOf(CmsXmlDisplayFormatterValue.SEPARATOR));
622                }
623                typeVals.add("\"" + displayType + "\"");
624            }
625        }
626        if (!typeVals.isEmpty()) {
627            result = "&fq=type:(" + CmsStringUtil.listAsString(typeVals, " OR ") + ")";
628        }
629        return result;
630    }
631
632    /** The default field facets.
633     *
634     * @param categoryConjunction flag, indicating if category selections in the facet should be "AND" combined.
635     * @return the default field facets.
636     */
637    private Map<String, I_CmsSearchConfigurationFacetField> getDefaultFieldFacets(boolean categoryConjunction) {
638
639        Map<String, I_CmsSearchConfigurationFacetField> fieldFacets = new HashMap<String, I_CmsSearchConfigurationFacetField>();
640        fieldFacets.put(
641            CmsListManager.FIELD_CATEGORIES,
642            new CmsSearchConfigurationFacetField(
643                CmsListManager.FIELD_CATEGORIES,
644                null,
645                Integer.valueOf(1),
646                Integer.valueOf(200),
647                null,
648                "Category",
649                SortOrder.index,
650                null,
651                Boolean.valueOf(categoryConjunction),
652                null,
653                Boolean.TRUE));
654        fieldFacets.put(
655            CmsListManager.FIELD_PARENT_FOLDERS,
656            new CmsSearchConfigurationFacetField(
657                CmsListManager.FIELD_PARENT_FOLDERS,
658                null,
659                Integer.valueOf(1),
660                Integer.valueOf(200),
661                null,
662                "Folders",
663                SortOrder.index,
664                null,
665                Boolean.FALSE,
666                null,
667                Boolean.TRUE));
668        return Collections.unmodifiableMap(fieldFacets);
669
670    }
671
672    /**
673     * Returns a flag, indicating if the release and expiration date should be ignored.
674     * @return a flag, indicating if the release and expiration date should be ignored.
675     */
676    private Boolean getIgnoreReleaseAndExpiration() {
677
678        return Boolean.valueOf(m_config.isShowExpired());
679    }
680}