com.temenos.interaction.commands.solr.SolrSearchCommand.java Source code

Java tutorial

Introduction

Here is the source code for com.temenos.interaction.commands.solr.SolrSearchCommand.java

Source

package com.temenos.interaction.commands.solr;

/*
 * The SOLR search command. Can be called with the following parameters.
 * 
 *      'core'      Name of the core to search. Defaults to the entity1 core.
 *      'q'         SOLR query term to search for.
 *      'feldname'  Name of field to search. Defaults to 'text' i.e. all fields (See schema.xml for details).
 * 
 */

/*
 * #%L
 * interaction-commands-solr
 * %%
 * Copyright (C) 2012 - 2013 Temenos Holdings N.V.
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response.Status;

import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrServer;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.temenos.interaction.authorization.command.AuthorizationAttributes;
import com.temenos.interaction.commands.solr.data.SolrConstants;
import com.temenos.interaction.core.command.InteractionCommand;
import com.temenos.interaction.core.command.InteractionContext;
import com.temenos.interaction.core.command.InteractionException;
import com.temenos.interaction.core.entity.EntityProperties;
import com.temenos.interaction.odataext.odataparser.ODataParser;
import com.temenos.interaction.odataext.odataparser.data.FieldName;
import com.temenos.interaction.odataext.odataparser.data.RowFilter;

public class SolrSearchCommand extends AbstractSolrCommand implements InteractionCommand {
    private final static Logger logger = LoggerFactory.getLogger(SolrSearchCommand.class);

    // Somewhere to store references to the embedded Solr servers used during
    // testing.
    private SolrServer testEntity1SolrServer = null;
    private SolrServer testEntity2SolrServer = null;

    // Type of entity used during testing.
    private String testEntityType;

    /**
     * Instantiates a new select command.
     * 
     * For production we pass in, and connect to, the URL of an external server
     * for each search request.
     * 
     * In future we may introduce connection pooling.
     */
    public SolrSearchCommand(String solrRootURL) {
        this.solrRootURL = solrRootURL;
    }

    /**
     * Instantiates a new select command for unittests.
     * 
     * Unit tests use an embedded Solr server. It has no URL so just pass in the
     * server references.
     * 
     * The third argument tells the test which entity type to associate witht he
     * first test server.
     * 
     * NOT TO BE USED FOR PRODUCTION CODE.
     * 
     * @param solrServer
     *            the solr server
     */
    public SolrSearchCommand(SolrServer entity1Server, SolrServer entity2Server, String entityType) {
        testEntity1SolrServer = entity1Server;
        testEntity2SolrServer = entity2Server;
        testEntityType = entityType;
    }

    @Override
    public Result execute(InteractionContext ctx) throws InteractionException {

        // Validate passed parameters
        MultivaluedMap<String, String> queryParams = ctx.getQueryParameters();

        // Dump query parameters
        Iterator<String> it = ctx.getQueryParameters().keySet().iterator();
        logger.info("SolrSearch command parameters:");
        while (it.hasNext()) {
            String theKey = (String) it.next();
            logger.info("    " + theKey + " = " + ctx.getQueryParameters().getFirst(theKey));
        }

        String coreName = queryParams.getFirst(SolrConstants.SOLR_CORE_KEY);
        String entityName = ctx.getCurrentState().getEntityName();
        if (entityName == null || entityName.isEmpty()) {
            if (testEntity1SolrServer == null) {
                // Still no luck fail fast
                logger.error(
                        "Solr search called with null Entity and Solr Core name whcih is used in resolving base Solr Core, giving up request...");
                throw new InteractionException(Status.INTERNAL_SERVER_ERROR,
                        "Solr search called with null Entity and Solr Core name whcih is used in resolving base Solr Core, giving up request...");
            } else {
                // For testing only...it should not be here though...unusual test
                entityName = testEntityType;
            }
        }
        // If core is not present, use entity name as base core name
        if (coreName == null || coreName.isEmpty()) {
            coreName = entityName;
        }

        // Set up query
        SolrQuery query = buildQuery(queryParams);
        if (null == query) {
            // Could not build a valid query.
            throw new InteractionException(Status.BAD_REQUEST, "SolrQuery is empty, please provide vali options");
        }

        // Set up a client side stub connecting to a Solr server
        SolrServer solrServer;
        if (null != testEntity1SolrServer) {
            // Use one of the test servers
            if (testEntityType == entityName) {
                solrServer = testEntity1SolrServer;
            } else {
                solrServer = testEntity2SolrServer;
            }
        } else {

            // TODO The following 4 lines, and the URL built with 'comapnyName', are a temporary work round to RTC1671119.
            // TODO Once expected behavior is understood feel free to remove this.
            String companyName = ctx.getPathParameters().getFirst("companyid");
            if (null == companyName) {
                throw new InteractionException(Status.BAD_REQUEST, "Search called with no company string.");
            }

            // Connect to an external SOLR server
            try {
                URL coreURL = new URL(solrRootURL + "/" + companyName + "_" + coreName);
                // URL coreURL = new URL(solrRootURL + "/" + coreName);

                logger.info("Connecting to external Solr server " + coreURL + ".");
                solrServer = new HttpSolrServer(coreURL.toString());
            } catch (MalformedURLException e) {
                logger.error("Malformed URL when connecting to Solr Server. " + e);
                throw new InteractionException(Status.BAD_REQUEST, "Malformed URL when connecting to Solr Server",
                        e);
            }
        }

        // Run the query
        Result res = Result.FAILURE;
        try {
            QueryResponse rsp = solrServer.query(query);
            // SolrDocumentList list = rsp.getResults();

            ctx.setResource(buildCollectionResource(entityName, rsp.getResults()));

            // Indicate that database level filtering was successful.
            ctx.setAttribute(AuthorizationAttributes.FILTER_DONE_ATTRIBUTE, Boolean.TRUE);
            ctx.setAttribute(AuthorizationAttributes.SELECT_DONE_ATTRIBUTE, Boolean.TRUE);

            res = Result.SUCCESS;
        } catch (SolrException e) {
            logger.error("An unexpected internal error occurred while querying Solr " + e);
        } catch (SolrServerException e) {
            logger.error("An unexpected error occurred while querying Solr " + e);
        }

        // If we connected to an external server disconnect.
        if (null == testEntity1SolrServer && testEntity2SolrServer == null) {
            // If we started a server connection close it down.
            solrServer.shutdown();
        }

        return (res);
    }

    // Build up a query from the parameters. Returns null on failure.
    SolrQuery buildQuery(MultivaluedMap<String, String> queryParams) {
        SolrQuery query = new SolrQuery();

        // Add Number of rows to fetch
        addNumOfRows(query, queryParams);

        // Add Shards for Distributed Query support
        addShards(query, queryParams);

        // Build the query string.
        String queryString = buildQueryString(queryParams);
        if (null != queryString) {
            query.setQuery(queryString);
        }

        // Add the filter string (like query but does hard matching).
        addFilter(query, queryParams);

        // If returned fields have been limited by authorization set them
        addSelect(query, queryParams);

        return (query);
    }

    /**
     * By default SolrQuery only returns 10 rows. This is true even if more
      * rows are available. This method will check if user has provided its preference
      * using $top, otherwise use Solr Default
     * @param query
     * @param queryParams
     */
    private void addNumOfRows(SolrQuery query, MultivaluedMap<String, String> queryParams) {
        int top = 0;
        try {
            String topStr = queryParams.getFirst("$top");
            top = topStr == null || topStr.isEmpty() ? 0 : Integer.parseInt(topStr);
        } catch (NumberFormatException nfe) {
            // Do nothing and ignore as we have default value to use

        }
        if (top > 0) {
            query.setRows(top);
        } else {
            query.setRows(MAX_ENTITIES_RETURNED);
        }
    }

    /**
     * This method will add Shards to the Query
     * @param query
     * @param queryParams
     */
    private void addShards(SolrQuery query, MultivaluedMap<String, String> queryParams) {
        String shards = queryParams.getFirst(SolrConstants.SOLR_SHARDS_KEY);
        if (shards != null && !shards.isEmpty()) {
            query.setParam("shards", shards);
            // Check if user has specified shards.tolerant, add if available
            String shardsTolerant = queryParams.getFirst(SolrConstants.SOLR_SHARDS_TOLERANT_KEY);
            if (shardsTolerant != null && !shardsTolerant.isEmpty()) {
                query.setParam(SolrConstants.SOLR_SHARDS_TOLERANT_KEY, shardsTolerant);
            }
        }
    }

    // Build Solr field list from an OData $select option.
    private void addSelect(SolrQuery query, MultivaluedMap<String, String> queryParams) {

        // If we were passed an OData $select parse it and add to the query
        String selectOption = queryParams.getFirst(ODataParser.SELECT_KEY);
        if (null != selectOption) {
            // Its a comma separated list of fields.
            Set<FieldName> fields = ODataParser.parseSelect(selectOption);

            logger.info("Adding selects:");
            for (FieldName field : fields) {
                logger.info("    " + field.getName());
                query.addField(field.getName());
            }
        }
        return;
    }

    // Build the Solr query string from passed request.
    private String buildQueryString(MultivaluedMap<String, String> queryParams) {
        String queryStr = new String();

        // If field name not passed use the 'text' field which contains all
        // other fields.
        String fieldName = queryParams.getFirst(SolrConstants.SOLR_FIELD_NAME_KEY);
        if (null == fieldName) {
            fieldName = "text";
        }

        // Add "q=" option if present.
        String query = queryParams.getFirst(SolrConstants.SOLR_QUERY_KEY);
        if (null != query) {
            queryStr = queryStr.concat(fieldName + ":" + query);
        } else {
            // If no query go with everything.
            logger.info("Search called with no query string. Searching on '*'.");
            queryStr = queryStr.concat(fieldName + ":*");
        }

        logger.info("Executing query " + queryStr + ".");

        return (queryStr);
    }

    // Build the Solr query string from passed request and any authorization
    // restrictions.
    private void addFilter(SolrQuery query, MultivaluedMap<String, String> queryParams) {

        // If we were passed an OData $filter parse it and add to the query
        String filterOption = queryParams.getFirst(ODataParser.FILTER_KEY);
        if (null != filterOption) {
            try {
                List<RowFilter> filters = ODataParser.parseFilter(filterOption);

                logger.info("Adding filters:");
                for (RowFilter filter : filters) {
                    logger.info("    " + filter.getFieldName().getName() + " "
                            + filter.getRelation().getoDataString() + " " + filter.getValue());

                    // Build filter query. Filter query (fq) syntax is non obvious. Check out on line references.

                    switch (filter.getRelation()) {
                    case EQ:
                        query.addFilterQuery(filter.getFieldName().getName() + ":\"" + filter.getValue() + "\"");
                        break;

                    case NE:
                        query.addFilterQuery(
                                "-" + filter.getFieldName().getName() + ":\"" + filter.getValue() + "\"");
                        break;

                    case LT:
                        // fq comparisons uses 'inclusive' [x TO y] syntax. To get an 'exclusive' lt use 'not gt'.
                        query.addFilterQuery(
                                "-" + filter.getFieldName().getName() + ":[\"" + filter.getValue() + "\" TO *]");
                        break;

                    case GT:
                        // fq comparisons uses 'inclusive' [x TO y] syntax. To get an 'exclusive' gt use 'not lt'.
                        query.addFilterQuery(
                                "-" + filter.getFieldName().getName() + ":[* TO \"" + filter.getValue() + "\"]");
                        break;

                    case LE:
                        query.addFilterQuery(
                                filter.getFieldName().getName() + ":[* TO \"" + filter.getValue() + "\"]");
                        break;

                    case GE:
                        query.addFilterQuery(
                                filter.getFieldName().getName() + ":[\"" + filter.getValue() + "\" TO *]");
                        break;

                    default:
                        logger.warn("Filter condition \"" + filter.getRelation()
                                + "\" not yet implemented ... ignored.");
                    }
                }
            } catch (ODataParser.UnsupportedQueryOperationException e) {
                logger.error("Could not interpret OData " + ODataParser.FILTER_KEY + " = " + filterOption, e);
                return;
            }
        }
        return;
    }

    @Override
    protected void customizeEntityProperties(SolrDocument doc, EntityProperties properties) {
        // By default nothing needs to be done
    }
}