/**
* 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.cache.solr.impl;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Stream;
import org.codice.ddf.platform.util.Exceptions;
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 ddf.catalog.data.Metacard;
import ddf.catalog.data.Result;
import ddf.catalog.federation.Federatable;
import ddf.catalog.operation.ProcessingDetails;
import ddf.catalog.operation.Query;
import ddf.catalog.operation.QueryRequest;
import ddf.catalog.operation.QueryResponse;
import ddf.catalog.operation.SourceResponse;
import ddf.catalog.operation.impl.ProcessingDetailsImpl;
import ddf.catalog.operation.impl.QueryResponseImpl;
import ddf.catalog.operation.impl.SourceResponseImpl;
import ddf.catalog.plugin.PluginExecutionException;
import ddf.catalog.plugin.PostFederatedQueryPlugin;
import ddf.catalog.plugin.StopProcessingException;
import ddf.catalog.util.impl.DistanceResultComparator;
import ddf.catalog.util.impl.RelevanceResultComparator;
import ddf.catalog.util.impl.TemporalResultComparator;
class SortedQueryMonitor implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(SortedQueryMonitor.class);
private final QueryRequest request;
private final CompletionService<SourceResponse> completionService;
private CachingFederationStrategy cachingFederationStrategy;
private QueryResponseImpl returnResults;
private Map<Future<SourceResponse>, QueryRequest> futures;
private List<PostFederatedQueryPlugin> postQuery;
private Query query;
private long deadline;
public SortedQueryMonitor(CachingFederationStrategy cachingFederationStrategy,
CompletionService<SourceResponse> completionService,
Map<Future<SourceResponse>, QueryRequest> futures, QueryResponseImpl returnResults,
QueryRequest request, List<PostFederatedQueryPlugin> postQuery) {
this.cachingFederationStrategy = cachingFederationStrategy;
this.completionService = completionService;
this.returnResults = returnResults;
this.request = request;
this.query = request.getQuery();
this.futures = futures;
this.postQuery = postQuery;
deadline = System.currentTimeMillis() + query.getTimeoutMillis();
}
@Override
public void run() {
SortBy sortBy = query.getSortBy();
// Prepare the Comparators that we will use
Comparator<Result> coreComparator = CachingFederationStrategy.DEFAULT_COMPARATOR;
if (sortBy != null && sortBy.getPropertyName() != null) {
PropertyName sortingProp = sortBy.getPropertyName();
String sortType = sortingProp.getPropertyName();
SortOrder sortOrder =
(sortBy.getSortOrder() == null) ? SortOrder.DESCENDING : sortBy.getSortOrder();
LOGGER.debug("Sorting type: {}", sortType);
LOGGER.debug("Sorting order: {}", sortBy.getSortOrder());
// Temporal searches are currently sorted by the effective time
if (Metacard.EFFECTIVE.equals(sortType) || Result.TEMPORAL.equals(sortType)) {
coreComparator = new TemporalResultComparator(sortOrder);
} else if (Result.DISTANCE.equals(sortType)) {
coreComparator = new DistanceResultComparator(sortOrder);
} else if (Result.RELEVANCE.equals(sortType)) {
coreComparator = new RelevanceResultComparator(sortOrder);
}
}
List<Result> resultList = new ArrayList<>();
long totalHits = 0;
Set<ProcessingDetails> processingDetails = returnResults.getProcessingDetails();
Map<String, Serializable> returnProperties = returnResults.getProperties();
HashMap<String, Long> hitsPerSource = new HashMap<>();
for (int i = futures.size(); i > 0; i--) {
String sourceId = "Unknown Source";
QueryRequest queryRequest = null;
SourceResponse sourceResponse = null;
try {
Future<SourceResponse> future;
if (query.getTimeoutMillis() < 1) {
future = completionService.take();
} else {
future = completionService.poll(getTimeRemaining(deadline),
TimeUnit.MILLISECONDS);
if (future == null) {
timeoutRemainingSources(processingDetails);
break;
}
}
queryRequest = futures.remove(future);
sourceId = getSourceIdFromRequest(queryRequest);
sourceResponse = future.get();
if (sourceResponse == null) {
LOGGER.debug("Source {} returned null response", sourceId);
executePostFederationQueryPluginsWithSourceError(queryRequest,
sourceId,
new NullPointerException(),
processingDetails);
} else {
sourceResponse = executePostFederationQueryPlugins(sourceResponse,
queryRequest);
resultList.addAll(sourceResponse.getResults());
long hits = sourceResponse.getHits();
totalHits += hits;
hitsPerSource.merge(sourceId, hits, (l1, l2) -> l1 + l2);
Map<String, Serializable> properties = sourceResponse.getProperties();
returnProperties.putAll(properties);
}
} catch (InterruptedException e) {
if (queryRequest != null) {
// First, add interrupted processing detail for this source
LOGGER.debug("Search interrupted for {}", sourceId);
executePostFederationQueryPluginsWithSourceError(queryRequest,
sourceId,
e,
processingDetails);
}
// Then add the interrupted exception for the remaining sources
interruptRemainingSources(processingDetails, e);
break;
} catch (ExecutionException e) {
LOGGER.info("Couldn't get results from completed federated query. {}, {}",
sourceId,
Exceptions.getFullMessage(e),
e);
executePostFederationQueryPluginsWithSourceError(queryRequest,
sourceId,
e,
processingDetails);
}
}
returnProperties.put("hitsPerSource", hitsPerSource);
LOGGER.debug("All sources finished returning results: {}", resultList.size());
returnResults.setHits(totalHits);
if (CachingFederationStrategy.INDEX_QUERY_MODE.equals(request.getPropertyValue(
CachingFederationStrategy.QUERY_MODE))) {
QueryResponse result = cachingFederationStrategy.queryCache(request);
returnResults.addResults(result.getResults(), true);
} else {
returnResults.addResults(sortedResults(resultList, coreComparator), true);
}
}
List<Result> sortedResults(List<Result> results, Comparator<? super Result> comparator) {
Collections.sort(results, comparator);
int maxResults = Integer.MAX_VALUE;
if (query.getPageSize() > 0) {
maxResults = query.getPageSize();
}
return results.size() > maxResults ? results.subList(0, maxResults) : results;
}
private void timeoutRemainingSources(Set<ProcessingDetails> processingDetails) {
for (QueryRequest expiredSource : futures.values()) {
if (expiredSource != null) {
String sourceId = getSourceIdFromRequest(expiredSource);
LOGGER.info("Search timed out for {}", sourceId);
processingDetails.add(new ProcessingDetailsImpl(sourceId, new TimeoutException()));
}
}
}
private void interruptRemainingSources(Set<ProcessingDetails> processingDetails,
InterruptedException interruptedException) {
for (QueryRequest interruptedSource : futures.values()) {
if (interruptedSource != null) {
String sourceId = getSourceIdFromRequest(interruptedSource);
LOGGER.info("Search interrupted for {}", sourceId);
processingDetails.add(new ProcessingDetailsImpl(sourceId, interruptedException));
}
}
}
private long getTimeRemaining(long deadline) {
long timeLeft;
if (System.currentTimeMillis() > deadline) {
timeLeft = 0;
} else {
timeLeft = deadline - System.currentTimeMillis();
}
return timeLeft;
}
private String getSourceIdFromRequest(QueryRequest queryRequest) {
String unkSource = "Unknown Source";
if (queryRequest == null) {
return unkSource;
}
return Stream.of(queryRequest)
.map(Federatable::getSourceIds)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.findFirst()
.orElse(unkSource);
}
private void executePostFederationQueryPluginsWithSourceError(QueryRequest queryRequest,
String sourceId, Exception e, Set<ProcessingDetails> processingDetails) {
ProcessingDetails processingDetail = new ProcessingDetailsImpl(sourceId, e);
SourceResponse sourceResponse = new SourceResponseImpl(queryRequest, new ArrayList<>());
sourceResponse.getProcessingErrors()
.add(processingDetail);
processingDetails.add(processingDetail);
executePostFederationQueryPlugins(sourceResponse, queryRequest);
}
private SourceResponse executePostFederationQueryPlugins(SourceResponse sourceResponse,
QueryRequest queryRequest) {
QueryResponse queryResponse = new QueryResponseImpl(queryRequest,
sourceResponse.getResults(),
true,
sourceResponse.getHits(),
queryRequest.getProperties());
try {
for (PostFederatedQueryPlugin service : postQuery) {
try {
queryResponse = service.process(queryResponse);
} catch (PluginExecutionException e) {
LOGGER.info("Error executing PostFederatedQueryPlugin", e);
}
}
} catch (StopProcessingException e) {
LOGGER.info("Plugin stopped processing", e);
}
return new SourceResponseImpl(queryRequest,
queryResponse.getResults(),
queryResponse.getHits());
}
}