/**
* Copyright (c) Codice Foundation
* <p/>
* This 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 3 of the
* License, or any later version.
* <p/>
* 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
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package ddf.catalog.source.solr;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Transformer;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.spatial4j.core.distance.DistanceUtils;
import ddf.catalog.data.AttributeType;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.MetacardCreationException;
import ddf.catalog.data.MetacardType;
import ddf.catalog.data.Result;
import ddf.catalog.data.impl.MetacardImpl;
import ddf.catalog.data.impl.ResultImpl;
import ddf.catalog.filter.FilterAdapter;
import ddf.catalog.operation.QueryRequest;
import ddf.catalog.operation.SourceResponse;
import ddf.catalog.operation.impl.QueryResponseImpl;
import ddf.catalog.operation.impl.SourceResponseImpl;
import ddf.catalog.source.UnsupportedQueryException;
import ddf.measure.Distance;
public class SolrMetacardClient {
protected static final String RELEVANCE_SORT_FIELD = "score";
private static final Logger LOGGER = LoggerFactory.getLogger(SolrMetacardClient.class);
private static final String QUOTE = "\"";
private final SolrServer server;
private final SolrFilterDelegateFactory filterDelegateFactory;
private final FilterAdapter filterAdapter;
private final DynamicSchemaResolver resolver;
public SolrMetacardClient(SolrServer solrServer, FilterAdapter catalogFilterAdapter,
SolrFilterDelegateFactory solrFilterDelegateFactory,
DynamicSchemaResolver dynamicSchemaResolver) {
server = solrServer;
filterDelegateFactory = solrFilterDelegateFactory;
filterAdapter = catalogFilterAdapter;
resolver = dynamicSchemaResolver;
}
public SourceResponse query(QueryRequest request) throws UnsupportedQueryException {
if (request == null || request.getQuery() == null) {
return new QueryResponseImpl(request, new ArrayList<Result>(), true, 0L);
}
SolrQuery query = getSolrQuery(request, filterDelegateFactory.newInstance(resolver));
String sortProperty = getSortProperty(request, query);
long totalHits;
List<Result> results = new ArrayList<>();
try {
QueryResponse solrResponse = server.query(query, SolrRequest.METHOD.POST);
totalHits = solrResponse.getResults().getNumFound();
SolrDocumentList docs = solrResponse.getResults();
for (SolrDocument doc : docs) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("SOLR DOC: {}",
doc.getFieldValue(Metacard.ID + SchemaFields.TEXT_SUFFIX));
}
ResultImpl tmpResult;
try {
tmpResult = createResult(doc, sortProperty);
// TODO: register metacard type???
} catch (MetacardCreationException e) {
LOGGER.warn("Metacard creation exception creating result", e);
throw new UnsupportedQueryException("Could not create metacard(s).");
}
results.add(tmpResult);
}
} catch (SolrServerException e) {
LOGGER.warn("Failure in Solr server query.", e);
throw new UnsupportedQueryException("Could not complete solr query.");
} catch (SolrException e) {
LOGGER.error("Could not complete solr query.", e);
throw new UnsupportedQueryException("Could not complete solr query.");
}
SourceResponseImpl sourceResponseImpl = new SourceResponseImpl(request, results);
/* Total Count */
sourceResponseImpl.setHits(totalHits);
return sourceResponseImpl;
}
public List<Metacard> query(String queryString) throws UnsupportedQueryException {
SolrQuery query = new SolrQuery();
query.setQuery(queryString);
try {
QueryResponse solrResponse = server.query(query, SolrRequest.METHOD.POST);
SolrDocumentList docs = solrResponse.getResults();
List<Metacard> results = new ArrayList<>();
for (SolrDocument doc : docs) {
try {
results.add(createMetacard(doc));
} catch (MetacardCreationException e) {
LOGGER.warn("Metacard creation exception creating result", e);
throw new UnsupportedQueryException("Could not create metacard(s).");
}
}
return results;
} catch (SolrServerException e) {
LOGGER.warn("Failure in Solr server query.", e);
throw new UnsupportedQueryException("Could not complete solr query.");
}
}
protected SolrQuery getSolrQuery(QueryRequest request, SolrFilterDelegate solrFilterDelegate)
throws UnsupportedQueryException {
solrFilterDelegate.setSortPolicy(request.getQuery().getSortBy());
SolrQuery query = filterAdapter.adapt(request.getQuery(), solrFilterDelegate);
// Solr does not support outside parenthesis in certain queries and throws EOF exception.
String queryPhrase = query.getQuery().trim();
if (queryPhrase.matches("\\(\\s*\\{!.*\\)")) {
query.setQuery(queryPhrase.replaceAll("^\\(\\s*|\\s*\\)$", ""));
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Prepared Query: {}", query.getQuery());
if (query.getFilterQueries() != null && query.getFilterQueries().length > 0) {
LOGGER.debug("Filter Queries: {}", Arrays.toString(query.getFilterQueries()));
}
}
if (request.getQuery().getPageSize() < 1) {
query.setRows(Integer.MAX_VALUE);
} else {
query.setRows(request.getQuery().getPageSize());
}
/* Start Index */
if (request.getQuery().getStartIndex() < 1) {
throw new UnsupportedQueryException("Start index must be greater than 0");
}
// Solr is 0-based
query.setStart(request.getQuery().getStartIndex() - 1);
return query;
}
protected String getSortProperty(QueryRequest request, SolrQuery query) {
SortBy sortBy = request.getQuery().getSortBy();
String sortProperty = "";
if (sortBy != null && sortBy.getPropertyName() != null) {
sortProperty = sortBy.getPropertyName().getPropertyName();
SolrQuery.ORDER order = SolrQuery.ORDER.desc;
if (sortBy.getSortOrder() == SortOrder.ASCENDING) {
order = SolrQuery.ORDER.asc;
}
if (Result.RELEVANCE.equals(sortProperty) || Result.DISTANCE.equals(sortProperty)) {
query.setFields("*", RELEVANCE_SORT_FIELD);
query.addSort(RELEVANCE_SORT_FIELD, order);
} else if (sortProperty.equals(Result.TEMPORAL)) {
query.addSort(
resolver.getField(Metacard.EFFECTIVE, AttributeType.AttributeFormat.DATE,
false), order);
} else {
List<String> resolvedProperties = resolver.getAnonymousField(sortProperty);
if (!resolvedProperties.isEmpty()) {
for (String sortField : resolvedProperties) {
query.addSort(sortField, order);
}
query.add("fl", "*," + RELEVANCE_SORT_FIELD);
} else {
LOGGER.info(
"No schema field was found for sort property [{}]. No sort field was added to the query.",
sortProperty);
}
}
}
return sortProperty;
}
private ResultImpl createResult(SolrDocument doc, String sortProperty)
throws MetacardCreationException {
ResultImpl result = new ResultImpl(createMetacard(doc));
if (doc.get(RELEVANCE_SORT_FIELD) != null) {
if (Result.RELEVANCE.equals(sortProperty)) {
result.setRelevanceScore(((Float) (doc.get(RELEVANCE_SORT_FIELD))).doubleValue());
} else if (Result.DISTANCE.equals(sortProperty)) {
Object distance = doc.getFieldValue(RELEVANCE_SORT_FIELD);
if (distance != null) {
LOGGER.debug("Distance returned from Solr [{}]", distance);
double convertedDistance = degreesToMeters(Double.valueOf(distance.toString()));
LOGGER.debug("Converted distance into meters [{}]", convertedDistance);
result.setDistanceInMeters(convertedDistance);
}
}
}
return result;
}
private Double degreesToMeters(double distance) {
return new Distance(
DistanceUtils.degrees2Dist(distance, DistanceUtils.EARTH_MEAN_RADIUS_KM),
Distance.LinearUnit.KILOMETER).getAs(Distance.LinearUnit.METER);
}
public MetacardImpl createMetacard(SolrDocument doc) throws MetacardCreationException {
MetacardType metacardType = resolver.getMetacardType(doc);
MetacardImpl metacard = new MetacardImpl(metacardType);
for (String solrFieldName : doc.getFieldNames()) {
if (!resolver.isPrivateField(solrFieldName)) {
Serializable value = resolver
.getDocValue(solrFieldName, doc.getFieldValue(solrFieldName));
metacard.setAttribute(resolver.resolveFieldName(solrFieldName), value);
}
}
return metacard;
}
public List<SolrInputDocument> add(List<Metacard> metacards, boolean forceAutoCommit)
throws IOException, SolrServerException, MetacardCreationException {
if (metacards == null || metacards.size() == 0) {
return null;
}
List<SolrInputDocument> docs = new ArrayList<>();
for (Metacard metacard : metacards) {
docs.add(getSolrInputDocument(metacard));
}
if (!forceAutoCommit) {
server.add(docs);
} else {
softCommit(docs);
}
return docs;
}
protected SolrInputDocument getSolrInputDocument(Metacard metacard)
throws MetacardCreationException {
SolrInputDocument solrInputDocument = new SolrInputDocument();
resolver.addFields(metacard, solrInputDocument);
return solrInputDocument;
}
public void deleteByIds(String fieldName, List<? extends Serializable> identifiers,
boolean forceCommit) throws IOException, SolrServerException {
if (identifiers == null || identifiers.size() == 0) {
return;
}
if (Metacard.ID.equals(fieldName)) {
CollectionUtils.transform(identifiers, new Transformer() {
@Override
public Object transform(Object o) {
return o.toString();
}
});
server.deleteById((List<String>) identifiers);
} else {
server.deleteByQuery(getIdentifierQuery(fieldName, identifiers));
}
if (forceCommit) {
server.commit();
}
}
public void deleteByQuery(String query) throws IOException, SolrServerException {
server.deleteByQuery(query);
}
public String getIdentifierQuery(String fieldName, List<? extends Serializable> identifiers) {
StringBuilder queryBuilder = new StringBuilder();
for (Serializable id : identifiers) {
if (queryBuilder.length() > 0) {
queryBuilder.append(" OR ");
}
queryBuilder.append(fieldName).append(":").append(QUOTE).append(id).append(QUOTE);
}
return queryBuilder.toString();
}
private org.apache.solr.client.solrj.response.UpdateResponse softCommit(
List<SolrInputDocument> docs) throws SolrServerException, IOException {
return new org.apache.solr.client.solrj.request.UpdateRequest().add(docs)
.setAction(AbstractUpdateRequest.ACTION.COMMIT,
/* waitForFlush */true,
/* waitToMakeVisible */true,
/* softCommit */true).process(server);
}
}