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;
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);
private static final String TEXT = "text";
private static final String COLON = ":";
private static final String STAR = "*";
/**
* 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;
}
protected SolrSearchCommand() {}
@Override
public Result execute(InteractionContext ctx) throws InteractionException {
try {
URL coreURL = new URL(solrRootURL + "/" + getCompanyId(ctx) + "_" + getCoreName(ctx));
// URL coreURL = new URL(solrRootURL + "/" + coreName);
logger.info("Connecting to external Solr server " + coreURL + ".");
return execute(ctx, 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);
}
}
protected Result execute(InteractionContext ctx, SolrServer solrServer) throws InteractionException {
logQueryParameters(ctx.getQueryParameters());
// Set up query
SolrQuery query = buildQuery(ctx.getQueryParameters());
if (null == query) {
// Could not build a valid query.
throw new InteractionException(Status.BAD_REQUEST, "Search query is empty, please provide valid options");
}
// 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.
// Run the query
Result res = Result.FAILURE;
try {
QueryResponse rsp = solrServer.query(query);
// SolrDocumentList list = rsp.getResults();
ctx.setResource(buildCollectionResource(getEntityName(ctx), 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);
}
solrServer.shutdown();
return res;
}
private void logQueryParameters(MultivaluedMap<String, String> queryParams) {
Iterator<String> it = queryParams.keySet().iterator();
logger.info("SolrSearch command parameters:");
while (it.hasNext()) {
String theKey = (String) it.next();
logger.info(" " + theKey + " = " + queryParams.getFirst(theKey));
}
}
private String getCoreName(InteractionContext ctx) throws InteractionException {
if (ctx.getQueryParameters().containsKey(SolrConstants.SOLR_CORE_KEY)) {
return ctx.getQueryParameters().getFirst(SolrConstants.SOLR_CORE_KEY);
}
return getEntityName(ctx);
}
private String getCompanyId(InteractionContext ctx) throws InteractionException {
String companyName = ctx.getPathParameters().getFirst("companyid");
if (null == companyName) {
throw new InteractionException(Status.BAD_REQUEST, "Missing company id");
}
return companyName;
}
private String getEntityName(InteractionContext ctx) throws InteractionException {
String entityName = ctx.getCurrentState().getEntityName();
if (entityName == null || entityName.isEmpty()) {
throw new InteractionException(Status.INTERNAL_SERVER_ERROR, "Missing entity name");
}
return entityName;
}
private 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);
// Add 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(SolrConstants.SOLR_SHARDS_KEY, 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 query = queryParams.getFirst(SolrConstants.SOLR_QUERY_KEY);
if (null == query || query.isEmpty()) {
return TEXT + COLON + STAR;
}
query = query.trim();
if (!query.contains(COLON)) {
return TEXT + COLON + query;
}
while (query.startsWith(STAR)) {
query = query.substring(1, query.length());
}
if (query.startsWith(COLON)) {
return TEXT + query;
}
return query;
}
// 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
}
}