/**
* 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 org.codice.ddf.ui.searchui.query.controller.search;
import java.io.Serializable;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.codice.ddf.ui.searchui.query.controller.SearchController;
import org.codice.ddf.ui.searchui.query.model.Search;
import org.codice.ddf.ui.searchui.query.model.SearchRequest;
import org.codice.ddf.ui.searchui.query.solr.FilteringDynamicSchemaResolver;
import org.cometd.bayeux.server.ServerSession;
import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.shape.Shape;
import org.opengis.filter.Filter;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.Result;
import ddf.catalog.data.impl.MetacardImpl;
import ddf.catalog.data.impl.ResultImpl;
import ddf.catalog.federation.FederationException;
import ddf.catalog.operation.Query;
import ddf.catalog.operation.QueryRequest;
import ddf.catalog.operation.QueryResponse;
import ddf.catalog.operation.impl.ProcessingDetailsImpl;
import ddf.catalog.operation.impl.QueryRequestImpl;
import ddf.catalog.operation.impl.QueryResponseImpl;
import ddf.catalog.source.SourceUnavailableException;
import ddf.catalog.source.UnsupportedQueryException;
import ddf.catalog.util.impl.DistanceResultComparator;
import ddf.catalog.util.impl.RelevanceResultComparator;
import ddf.catalog.util.impl.TemporalResultComparator;
import ddf.security.SecurityConstants;
import ddf.security.Subject;
public abstract class QueryRunnable implements Runnable {
protected static final Logger LOGGER = LoggerFactory.getLogger(QueryRunnable.class);
protected static final Map<String, ? extends Serializable> CACHE_PROPERTIES = ImmutableMap.of(
"mode",
"cache");
protected static final Map<String, ? extends Serializable> UPDATE_PROPERTIES = ImmutableMap.of(
"mode",
"update");
protected static final JtsSpatialContextFactory JTS_SPATIAL_CONTEXT_FACTORY =
new JtsSpatialContextFactory();
static {
// permits geometry collections with intersecting polygons
JTS_SPATIAL_CONTEXT_FACTORY.allowMultiOverlap = true;
}
protected static final JtsSpatialContext SPATIAL_CONTEXT =
JTS_SPATIAL_CONTEXT_FACTORY.newSpatialContext();
protected static final int FUTURE_TIMEOUT_SECONDS = 60;
public static final int METERS_IN_KILOMETERS = 1000;
protected final SearchController searchController;
protected final SearchRequest request;
protected final Subject subject;
protected final Search search;
protected final ServerSession session;
protected final Map<String, Result> results;
public QueryRunnable(SearchController searchController, SearchRequest request, Subject subject,
Search search, ServerSession session, Map<String, Result> results) {
this.searchController = searchController;
this.request = request;
this.subject = subject;
this.search = search;
this.session = session;
this.results = results;
}
public abstract void run();
protected String extractQueryWkt(Filter filter) {
String wkt = "";
try {
wkt = searchController.getFilterAdapter()
.adapt(filter, new WktExtractionFilterDelegate());
} catch (UnsupportedQueryException e) {
LOGGER.debug("Unable to extract wkt", e);
}
return wkt;
}
protected void normalizeDistances(Filter query, Map<String, Result> results) {
String wkt = extractQueryWkt(query);
if (StringUtils.isNotBlank(wkt)) {
Shape queryShape;
try {
queryShape = getShape(wkt);
} catch (ParseException e) {
LOGGER.debug("Unable to parse query WKT to calculate distance", e);
return;
}
for (Map.Entry<String, Result> entry : results.entrySet()) {
Result result = entry.getValue();
if (result.getMetacard() != null && StringUtils.isNotBlank(result.getMetacard()
.getLocation())) {
try {
Shape locationShape = getShape(result.getMetacard()
.getLocation());
double distance = DistanceUtils.degrees2Dist(SPATIAL_CONTEXT.calcDistance(
locationShape.getCenter(),
queryShape.getCenter()), DistanceUtils.EARTH_MEAN_RADIUS_KM)
* METERS_IN_KILOMETERS;
ResultImpl updatedResult =
new ResultImpl(new MetacardImpl(result.getMetacard()));
updatedResult.setDistanceInMeters(distance);
results.put(entry.getKey(), updatedResult);
} catch (ParseException e) {
LOGGER.debug("Unable to parse metacard WKT to calculate distance", e);
}
}
}
}
}
static Shape getShape(String wkt) throws ParseException {
return SPATIAL_CONTEXT.readShapeFromWkt(wkt);
}
protected void normalizeRelevance(List<Result> indexResults, Map<String, Result> results) {
for (Result indexResult : indexResults) {
String resultKey = indexResult.getMetacard()
.getAttribute(FilteringDynamicSchemaResolver.SOURCE_ID)
.getValue() + indexResult.getMetacard()
.getId();
if (results.containsKey(resultKey)) {
MetacardImpl metacard = new MetacardImpl(results.get(resultKey)
.getMetacard());
metacard.setAttribute(Search.CACHED, null);
ResultImpl result = new ResultImpl(metacard);
result.setRelevanceScore(indexResult.getRelevanceScore());
results.put(resultKey, result);
}
}
}
protected int getMaxResults(SearchRequest request) {
return request.getQuery()
.getPageSize() > 0 ?
request.getQuery()
.getPageSize() :
Integer.MAX_VALUE;
}
protected void addResults(Collection<Result> responseResults) {
results.putAll(Maps.uniqueIndex(responseResults, new Function<Result, String>() {
@Override
public String apply(Result result) {
return getResultKey(result.getMetacard());
}
}));
}
protected String getResultKey(Metacard metacard) {
return metacard.getSourceId() + ":" + metacard.getId();
}
protected Comparator<Result> getResultComparator(Query query) {
Comparator<Result> sortComparator = new RelevanceResultComparator(SortOrder.DESCENDING);
SortBy sortBy = query.getSortBy();
if (sortBy != null && sortBy.getPropertyName() != null) {
PropertyName sortingProp = sortBy.getPropertyName();
String sortType = sortingProp.getPropertyName();
SortOrder sortOrder =
(sortBy.getSortOrder() == null) ? SortOrder.DESCENDING : sortBy.getSortOrder();
// Temporal searches are currently sorted by the effective time
if (Metacard.EFFECTIVE.equals(sortType) || Result.TEMPORAL.equals(sortType)) {
sortComparator = new TemporalResultComparator(sortOrder);
} else if (Metacard.CREATED.equals(sortType) || Metacard.MODIFIED.equals(sortType)) {
sortComparator = new TemporalResultComparator(sortOrder, sortType);
} else if (Result.DISTANCE.equals(sortType)) {
sortComparator = new DistanceResultComparator(sortOrder);
} else if (Result.RELEVANCE.equals(sortType)) {
sortComparator = new RelevanceResultComparator(sortOrder);
}
}
return sortComparator;
}
protected QueryResponse queryCatalog(String sourceId, SearchRequest searchRequest,
Subject subject, Map<String, Serializable> properties) {
Query query = searchRequest.getQuery();
QueryResponse response = getEmptyResponse(sourceId);
long startTime = System.currentTimeMillis();
try {
if (query != null) {
List<String> sourceIds;
if (sourceId == null) {
sourceIds = new ArrayList<>(searchRequest.getSourceIds());
} else {
sourceIds = Collections.singletonList(sourceId);
}
QueryRequest request = new QueryRequestImpl(query, false, sourceIds, properties);
if (subject != null) {
LOGGER.debug("Adding {} property with value {} to request.",
SecurityConstants.SECURITY_SUBJECT,
subject);
request.getProperties()
.put(SecurityConstants.SECURITY_SUBJECT, subject);
}
LOGGER.debug("Sending query: {}", query);
response = searchController.getFramework()
.query(request);
}
} catch (UnsupportedQueryException | FederationException e) {
LOGGER.info("Error executing query. {}. Set log level to DEBUG for more information",
e.getMessage());
LOGGER.debug("Error executing query", e);
response.getProcessingDetails()
.add(new ProcessingDetailsImpl(sourceId, e));
} catch (SourceUnavailableException e) {
LOGGER.info(
"Error executing query because the underlying source was unavailable. {}. Set log level to DEBUG for more information",
e.getMessage());
LOGGER.debug("Error executing query because the underlying source was unavailable.", e);
response.getProcessingDetails()
.add(new ProcessingDetailsImpl(sourceId, e));
} catch (RuntimeException e) {
// Account for any runtime exceptions and send back a server error
// this prevents full stacktraces returning to the client
// this allows for a graceful server error to be returned
LOGGER.info(
"RuntimeException on executing query. {}. Set log level to DEBUG for more information",
e.getMessage());
LOGGER.debug("RuntimeException on executing query", e);
response.getProcessingDetails()
.add(new ProcessingDetailsImpl(sourceId, e));
}
long estimatedTime = System.currentTimeMillis() - startTime;
response.getProperties()
.put("elapsed", estimatedTime);
return response;
}
private QueryResponse getEmptyResponse(String sourceId) {
// No query was specified
QueryRequest queryRequest = new QueryRequestImpl(null,
false,
Collections.singletonList(sourceId),
null);
// Create a dummy QueryResponse with zero results
return new QueryResponseImpl(queryRequest, new ArrayList<Result>(), 0);
}
}