org.opencms.search.solr.CmsSolrQuery.java Source code

Java tutorial

Introduction

Here is the source code for org.opencms.search.solr.CmsSolrQuery.java

Source

/*
 * File   : $Source$
 * Date   : $Date$
 * Version: $Revision$
 *
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (C) 2002 - 2008 Alkacon Software (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.search.solr;

import org.opencms.file.CmsObject;
import org.opencms.file.CmsPropertyDefinition;
import org.opencms.i18n.CmsEncoder;
import org.opencms.main.OpenCms;
import org.opencms.search.fields.CmsSearchField;
import org.opencms.util.CmsPair;
import org.opencms.util.CmsRequestUtil;
import org.opencms.util.CmsStringUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.common.params.CommonParams;

/**
 * A Solr search query.<p>
 */
public class CmsSolrQuery extends SolrQuery {

    /** A constant to add the score field to the result documents. */
    public static final String ALL_RETURN_FIELDS = "*,score";

    /** The default facet date gap. */
    public static final String DEFAULT_FACET_DATE_GAP = "+1DAY";

    /** The default query. */
    public static final String DEFAULT_QUERY = "*:*";

    /** The query type. */
    public static final String DEFAULT_QUERY_TYPE = "edismax";

    /** The default search result count. */
    public static final Integer DEFAULT_ROWS = new Integer(10);

    /** A constant to add the score field to the result documents. */
    public static final String MINIMUM_FIELDS = CmsSearchField.FIELD_PATH + "," + CmsSearchField.FIELD_TYPE + ","
            + CmsSearchField.FIELD_ID;

    /** A constant to add the score field to the result documents. */
    public static final String STRUCTURE_FIELDS = CmsSearchField.FIELD_PATH + "," + CmsSearchField.FIELD_TYPE + ","
            + CmsSearchField.FIELD_ID + "," + CmsSearchField.FIELD_CATEGORY + ","
            + CmsSearchField.FIELD_DATE_CONTENT + "," + CmsSearchField.FIELD_DATE_CREATED + ","
            + CmsSearchField.FIELD_DATE_EXPIRED + "," + CmsSearchField.FIELD_DATE_LASTMODIFIED + ","
            + CmsSearchField.FIELD_DATE_RELEASED + "," + CmsSearchField.FIELD_SUFFIX + ","
            + CmsSearchField.FIELD_DEPENDENCY_TYPE + "," + CmsSearchField.FIELD_DESCRIPTION + ","
            + CmsPropertyDefinition.PROPERTY_TITLE + CmsSearchField.FIELD_DYNAMIC_PROPERTIES + ","
            + CmsSearchField.FIELD_RESOURCE_LOCALES + "," + CmsSearchField.FIELD_CONTENT_LOCALES + ","
            + CmsSearchField.FIELD_SCORE + "," + CmsSearchField.FIELD_PARENT_FOLDERS;

    /** The serial version UID. */
    private static final long serialVersionUID = -2387357736597627703L;

    /** The facet date gap to use for date facets. */
    private String m_facetDateGap = DEFAULT_FACET_DATE_GAP;

    /** Ignore expiration flag. */
    private boolean m_ignoreExpiration;

    /** The parameters given by the 'query string'.  */
    private Map<String, String[]> m_queryParameters = new HashMap<String, String[]>();

    /** The search words. */
    private String m_text;

    /** The name of the field to search the text in. */
    private List<String> m_textSearchFields = new ArrayList<String>();

    /**
     * Default constructor.<p>
     */
    public CmsSolrQuery() {

        this(null, null);
    }

    /**
     * Public constructor.<p>
     *
     * @param cms the current OpenCms context
     * @param queryParams the Solr query parameters
     */
    public CmsSolrQuery(CmsObject cms, Map<String, String[]> queryParams) {

        setQuery(DEFAULT_QUERY);
        setFields(ALL_RETURN_FIELDS);
        setRequestHandler(DEFAULT_QUERY_TYPE);
        setRows(DEFAULT_ROWS);

        // set the values from the request context
        if (cms != null) {
            setLocales(Collections.singletonList(cms.getRequestContext().getLocale()));
            setSearchRoots(Collections.singletonList(cms.getRequestContext().getSiteRoot() + "/"));
        }
        if (queryParams != null) {
            m_queryParameters = queryParams;
        }
        ensureParameters();
        ensureReturnFields();
        ensureExpiration();
    }

    /**
     * Returns the resource type if only one is set as filter query.<p>
     *
     * @param fqs the field queries to check
     *
     * @return the type or <code>null</code>
     */
    public static String getResourceType(String[] fqs) {

        String ret = null;
        int count = 0;
        if (fqs != null) {
            for (String fq : fqs) {
                if (fq.startsWith(CmsSearchField.FIELD_TYPE + ":")) {
                    String val = fq.substring((CmsSearchField.FIELD_TYPE + ":").length());
                    val = val.replaceAll("\"", "");
                    if (OpenCms.getResourceManager().hasResourceType(val)) {
                        count++;
                        ret = val;
                    }
                }
            }
        }
        return (count == 1) ? ret : null;
    }

    /**
     * Creates and adds a filter query.<p>
     *
     * @param fieldName the field name to create a filter query on
     * @param vals the values that should match for the given field
     * @param all <code>true</code> to combine the given values with 'AND', <code>false</code> for 'OR'
     * @param useQuotes <code>true</code> to surround the given values with double quotes, <code>false</code> otherwise
     */
    public void addFilterQuery(String fieldName, List<String> vals, boolean all, boolean useQuotes) {

        if (getFilterQueries() != null) {
            for (String fq : getFilterQueries()) {
                if (fq.startsWith(fieldName + ":")) {
                    removeFilterQuery(fq);
                }
            }
        }
        addFilterQuery(createFilterQuery(fieldName, vals, all, useQuotes));
    }

    /**
     * Adds the given fields/orders to the existing sort fields.<p>
     *
     * @param sortFields the sortFields to set
     */
    public void addSortFieldOrders(Map<String, ORDER> sortFields) {

        if ((sortFields != null) && !sortFields.isEmpty()) {
            // add the sort fields to the query
            for (Map.Entry<String, ORDER> entry : sortFields.entrySet()) {
                addSort(entry.getKey(), entry.getValue());
            }
        }
    }

    /**
     * @see java.lang.Object#clone()
     */
    @Override
    public CmsSolrQuery clone() {

        CmsSolrQuery sq = new CmsSolrQuery(null, CmsRequestUtil.createParameterMap(toString()));
        sq.m_ignoreExpiration = m_ignoreExpiration;
        return sq;
    }

    /**
     * Ensures that the initial request parameters will overwrite the member values.<p>
     *
     * You can initialize the query with an HTTP request parameter then make some method calls
     * and finally re-ensure that the initial request parameters will overwrite the changes
     * made in the meanwhile.<p>
     */
    public void ensureParameters() {

        // overwrite already set values with values from query String
        if ((m_queryParameters != null) && !m_queryParameters.isEmpty()) {
            for (Map.Entry<String, String[]> entry : m_queryParameters.entrySet()) {
                if (!entry.getKey().equals(CommonParams.FQ)) {
                    // add or replace all parameters from the query String
                    setParam(entry.getKey(), entry.getValue());
                } else {
                    // special handling for filter queries
                    replaceFilterQueries(entry.getValue());
                }
            }
        }
    }

    /**
     * Removes the expiration flag.
     */
    public void removeExpiration() {

        if (getFilterQueries() != null) {
            for (String fq : getFilterQueries()) {
                if (fq.startsWith(CmsSearchField.FIELD_DATE_EXPIRED + ":")
                        || fq.startsWith(CmsSearchField.FIELD_DATE_RELEASED + ":")) {
                    removeFilterQuery(fq);
                }
            }
        }
        m_ignoreExpiration = true;
    }

    /**
     * Sets the categories only if not set in the query parameters.<p>
     *
     * @param categories the categories to set
     */
    public void setCategories(List<String> categories) {

        if ((categories != null) && !categories.isEmpty()) {
            addFilterQuery(CmsSearchField.FIELD_CATEGORY + CmsSearchField.FIELD_DYNAMIC_EXACT, categories, true,
                    true);
        }
    }

    /**
     * Sets the categories only if not set in the query parameters.<p>
     *
     * @param categories the categories to set
     */
    public void setCategories(String... categories) {

        setCategories(Arrays.asList(categories));
    }

    /**
     * Sets date ranges.<p>
     *
     * This call will overwrite all existing date ranges for the given keys (name of the date facet field).<p>
     *
     * The parameter Map uses as:<p>
     * <ul>
     * <li><code>keys: </code>Solr field name {@link org.opencms.search.fields.CmsSearchField} and
     * <li><code>values: </code> pairs with min date as first and max date as second {@link org.opencms.util.CmsPair}
     * </ul>
     * Alternatively you can use Solr standard query syntax like:<p>
     * <ul>
     * <li><code>+created:[* TO NOW]</code>
     * <li><code>+lastmodified:[' + date + ' TO NOW]</code>
     * </ul>
     * whereby date is Solr formated:
     * {@link org.opencms.search.solr.CmsSolrDocument#DF}
     * <p>
     *
     * @param dateRanges the ranges map with field name as key and a CmsPair with min date as first and max date as second
     */
    public void setDateRanges(Map<String, CmsPair<Date, Date>> dateRanges) {

        if ((dateRanges != null) && !dateRanges.isEmpty()) {
            // remove the date ranges
            for (Map.Entry<String, CmsPair<Date, Date>> entry : dateRanges.entrySet()) {
                removeFacetField(entry.getKey());
            }
            // add the date ranges
            for (Map.Entry<String, CmsPair<Date, Date>> entry : dateRanges.entrySet()) {
                addDateRangeFacet(entry.getKey(), entry.getValue().getFirst(), entry.getValue().getSecond(),
                        m_facetDateGap);
            }
        }
    }

    /**
     * Sets the facetDateGap.<p>
     *
     * @param facetDateGap the facetDateGap to set
     */
    public void setFacetDateGap(String facetDateGap) {

        m_facetDateGap = facetDateGap;
    }

    /**
     * Sets the highlightFields.<p>
     *
     * @param highlightFields the highlightFields to set
     */
    public void setHighlightFields(List<String> highlightFields) {

        setParam("hl.fl", CmsStringUtil.listAsString(highlightFields, ","));
    }

    /**
     * Sets the highlightFields.<p>
     *
     * @param highlightFields the highlightFields to set
     */
    public void setHighlightFields(String... highlightFields) {

        setParam("hl.fl", CmsStringUtil.arrayAsString(highlightFields, ","));
    }

    /**
     * Sets the locales only if not set in the query parameters.<p>
     *
     * @param locales the locales to set
     */
    public void setLocales(List<Locale> locales) {

        m_textSearchFields = new ArrayList<String>();
        if ((locales == null) || locales.isEmpty()) {
            m_textSearchFields.add(CmsSearchField.FIELD_TEXT);
            if (getFilterQueries() != null) {
                for (String fq : getFilterQueries()) {
                    if (fq.startsWith(CmsSearchField.FIELD_CONTENT_LOCALES + ":")) {
                        removeFilterQuery(fq);
                    }
                }
            }
        } else {
            List<String> localeStrings = new ArrayList<String>();
            for (Locale locale : locales) {
                localeStrings.add(locale.toString());
                if (!m_textSearchFields.contains("text")
                        && !OpenCms.getLocaleManager().getAvailableLocales().contains(locale)) {
                    // if the locale is not configured in the opencms-system.xml
                    // there will no localized text fields, so take the general one
                    m_textSearchFields.add("text");
                } else {
                    m_textSearchFields.add("text_" + locale);
                }
            }
            addFilterQuery(CmsSearchField.FIELD_CONTENT_LOCALES, localeStrings, false, false);
        }
        if (m_text != null) {
            setText(m_text);
        }
    }

    /**
     * Sets the locales only if not set in the query parameters.<p>
     *
     * @param locales the locales to set
     */
    public void setLocales(Locale... locales) {

        setLocales(Arrays.asList(locales));
    }

    /**
     * @see org.apache.solr.client.solrj.SolrQuery#setRequestHandler(java.lang.String)
     */
    @Override
    public SolrQuery setRequestHandler(String qt) {

        SolrQuery q = super.setRequestHandler(qt);
        if (m_text != null) {
            setText(m_text);
        }
        return q;
    }

    /**
     * Sets the resource types only if not set in the query parameters.<p>
     *
     * @param resourceTypes the resourceTypes to set
     */
    public void setResourceTypes(List<String> resourceTypes) {

        if ((resourceTypes != null) && !resourceTypes.isEmpty()) {
            addFilterQuery(CmsSearchField.FIELD_TYPE, resourceTypes, false, false);
        }
    }

    /**
     * Sets the resource types only if not set in the query parameters.<p>
     *
     * @param resourceTypes the resourceTypes to set
     */
    public void setResourceTypes(String... resourceTypes) {

        setResourceTypes(Arrays.asList(resourceTypes));
    }

    /**
     * Sets the search roots only if not set as query parameter.<p>
     *
     * @param searchRoots the searchRoots to set
     */
    public void setSearchRoots(List<String> searchRoots) {

        if ((searchRoots != null) && !searchRoots.isEmpty()) {
            addFilterQuery(CmsSearchField.FIELD_PARENT_FOLDERS, searchRoots, false, true);
        }
    }

    /**
     * Sets the search roots only if not set as query parameter.<p>
     *
     * @param searchRoots the searchRoots to set
     */
    public void setSearchRoots(String... searchRoots) {

        setSearchRoots(Arrays.asList(searchRoots));
    }

    /**
     * Sets the return fields 'fl' to a predefined set that does not contain content specific fields.<p>
     *
     * @param structureQuery the <code>true</code> to return only structural fields
     */
    public void setStructureQuery(boolean structureQuery) {

        if (structureQuery) {
            setFields(STRUCTURE_FIELDS);
        }
    }

    /**
     * Sets the text.<p>
     *
     * @param text the text to set
     */
    public void setText(String text) {

        m_text = text;
        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(text)) {
            setQuery(createTextQuery(text));
        }
    }

    /**
     * Sets the textSearchFields.<p>
     *
     * @param textSearchFields the textSearchFields to set
     */
    public void setTextSearchFields(List<String> textSearchFields) {

        m_textSearchFields = textSearchFields;
        if (m_text != null) {
            setText(m_text);
        }
    }

    /**
     * Sets the textSearchFields.<p>
     *
     * @param textSearchFields the textSearchFields to set
     */
    public void setTextSearchFields(String... textSearchFields) {

        setTextSearchFields(Arrays.asList(textSearchFields));
    }

    /**
     * @see org.apache.solr.common.params.ModifiableSolrParams#toString()
     */
    @Override
    public String toString() {

        return CmsEncoder.decode(super.toString());
    }

    /**
     * Creates a filter query on the given field name.<p>
     *
     * Creates and adds a filter query.<p>
     *
     * @param fieldName the field name to create a filter query on
     * @param vals the values that should match for the given field
     * @param all <code>true</code> to combine the given values with 'AND', <code>false</code> for 'OR'
     * @param useQuotes <code>true</code> to surround the given values with double quotes, <code>false</code> otherwise
     *
     * @return a filter query String e.g. <code>fq=fieldname:val1</code>
     */
    private String createFilterQuery(String fieldName, List<String> vals, boolean all, boolean useQuotes) {

        String filterQuery = null;
        if ((vals != null)) {
            if (vals.size() == 1) {
                if (useQuotes) {
                    filterQuery = fieldName + ":" + "\"" + vals.get(0) + "\"";
                } else {
                    filterQuery = fieldName + ":" + vals.get(0);
                }
            } else if (vals.size() > 1) {
                filterQuery = fieldName + ":(";
                for (int j = 0; j < vals.size(); j++) {
                    String val;
                    if (useQuotes) {
                        val = "\"" + vals.get(j) + "\"";
                    } else {
                        val = vals.get(j);
                    }
                    filterQuery += val;
                    if (vals.size() > (j + 1)) {
                        if (all) {
                            filterQuery += " AND ";
                        } else {
                            filterQuery += " OR ";
                        }
                    }
                }
                filterQuery += ")";
            }
        }
        return filterQuery;
    }

    /**
     * Creates a OR combined 'q' parameter.<p>
     *
     * @param text
     *
     * @return returns the 'q' parameter
     */
    private String createTextQuery(String text) {

        if (m_textSearchFields.isEmpty()) {
            m_textSearchFields.add(CmsSearchField.FIELD_TEXT);
        }
        String q = "{!q.op=OR type=" + getRequestHandler() + " qf=";
        boolean first = true;
        for (String textField : m_textSearchFields) {
            if (!first) {
                q += " ";
            }
            q += textField;
        }
        q += "}" + text;
        return q;
    }

    /**
     * Ensures that expired and not yet released resources are not returned by default.<p>
     */
    private void ensureExpiration() {

        boolean expirationDateSet = false;
        boolean releaseDateSet = false;
        if (getFilterQueries() != null) {
            for (String fq : getFilterQueries()) {
                if (fq.startsWith(CmsSearchField.FIELD_DATE_EXPIRED + ":")) {
                    expirationDateSet = true;
                }
                if (fq.startsWith(CmsSearchField.FIELD_DATE_RELEASED + ":")) {
                    releaseDateSet = true;
                }
            }
        }
        if (!expirationDateSet) {
            addFilterQuery(CmsSearchField.FIELD_DATE_EXPIRED + ":[NOW TO *]");
        }
        if (!releaseDateSet) {
            addFilterQuery(CmsSearchField.FIELD_DATE_RELEASED + ":[* TO NOW]");
        }
    }

    /**
     * Ensures that at least the 'path' and the 'type' are part of the fields returned field list.<p>
     *
     * @see CommonParams#FL
     */
    private void ensureReturnFields() {

        String[] fl = getParams(CommonParams.FL);
        if ((fl != null) && (fl.length > 0)) {
            List<String> result = new ArrayList<String>();
            for (String field : fl) {
                String commasep = field.replaceAll(" ", ",");
                List<String> list = CmsStringUtil.splitAsList(commasep, ',');
                if (!list.contains("*")) {
                    for (String reqField : CmsStringUtil.splitAsList(MINIMUM_FIELDS, ",")) {
                        if (!list.contains(reqField)) {
                            list.add(reqField);
                        }
                    }
                }
                result.addAll(list);
            }
            setParam(CommonParams.FL, CmsStringUtil.arrayAsString(result.toArray(new String[0]), ","));
        }
    }

    /**
     * Removes those filter queries that restrict the fields used in the given filter query Strings.<p>
     *
     * Searches in the given Strings for a ":", then takes the field name part
     * and removes the already set filter queries queries that are matching the same field name.<p>
     *
     * @param fqs the filter query Strings in the format <code>fq=fieldname:value</code> that should be removed
     */
    private void removeFilterQueries(String[] fqs) {

        // iterate over the given filter queries to remove
        for (String fq : fqs) {
            int idx = fq.indexOf(':');
            if (idx != -1) {
                // get the field name of the fq to remove
                String fieldName = fq.substring(0, idx);
                // iterate over the fqs of the already existing fqs from the solr query
                if (getFilterQueries() != null) {
                    for (String sfq : getFilterQueries()) {
                        if (sfq.startsWith(fieldName + ":")) {
                            // there exists a filter query for exact the same field,  remove it
                            removeFilterQuery(sfq);
                        }
                    }
                }
            }
        }
    }

    /**
     * Removes the given filter queries, if already set and then adds the filter queries again.<p>
     *
     * @param fqs the filter queries to remove
     */
    private void replaceFilterQueries(String[] fqs) {

        removeFilterQueries(fqs);
        addFilterQuery(fqs);
    }
}