/** * 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.model; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeoutException; import org.apache.commons.collections.Bag; import org.apache.commons.collections.bag.HashBag; import org.apache.commons.lang.StringUtils; import org.codice.ddf.ui.searchui.query.model.QueryStatus.State; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableMap; import ddf.action.Action; import ddf.action.ActionRegistry; import ddf.catalog.data.Attribute; import ddf.catalog.data.AttributeDescriptor; import ddf.catalog.data.Metacard; import ddf.catalog.data.MetacardType; import ddf.catalog.data.Result; import ddf.catalog.operation.ProcessingDetails; import ddf.catalog.operation.QueryResponse; import ddf.catalog.operation.impl.ProcessingDetailsImpl; import ddf.catalog.operation.impl.QueryResponseImpl; import ddf.catalog.source.SourceUnavailableException; import ddf.catalog.source.UnsupportedQueryException; import ddf.catalog.transform.CatalogTransformerException; import ddf.catalog.transformer.metacard.geojson.GeoJsonMetacardTransformer; /** * This class represents the cached asynchronous query response from all sources. */ public class Search { private static final Map<Class, String> SAFE_ERROR_MESSAGES = ImmutableMap.of( UnsupportedQueryException.class, "Query was invalid", SourceUnavailableException.class, "Query hit a source that was unavailable", InterruptedException.class, "Query was interrupted", TimeoutException.class, "Query timed out"); private static final String REASON_INTERNAL = "Internal error"; public static final String REASONS = "reasons"; public static final String HITS = "hits"; public static final String DISTANCE = "distance"; public static final String RELEVANCE = "relevance"; public static final String METACARD = "metacard"; public static final String ACTIONS = "actions"; public static final String ACTIONS_ID = "id"; public static final String ACTIONS_TITLE = "title"; public static final String ACTIONS_DESCRIPTION = "description"; public static final String ACTIONS_URL = "url"; public static final String RESULTS = "results"; public static final String METACARD_TYPES = "metacard-types"; public static final String SUCCESSFUL = "successful"; public static final String STATUS = "status"; public static final String STATE = "state"; public static final String ID = "id"; public static final String DONE = "done"; public static final String ELAPSED = "elapsed"; public static final String CACHED = "cached"; private static final String INTERNAL_LOCAL_RESOURCE = "internal.local-resource"; private static final String IS_RESOURCE_LOCAL = "is-resource-local"; private static final Logger LOGGER = LoggerFactory.getLogger(Search.class); private static final DateTimeFormatter ISO_8601_DATE_FORMAT = DateTimeFormat.forPattern( "yyyy-MM-dd'T'HH:mm:ss.SSSZ") .withZoneUTC(); public static final String ATTRIBUTE_FORMAT = "format"; public static final String ATTRIBUTE_INDEXED = "indexed"; private static final int MAX_EXCEPTION_SCAN_DEPTH = 10; private ActionRegistry actionRegistry; private SearchRequest searchRequest; private Map<String, QueryStatus> queryStatus = new HashMap<String, QueryStatus>(); private long hits = 0; private long responseNum = 0; private List<Result> results = new ArrayList<>(); private Search() { } public Search(SearchRequest request, ActionRegistry registry) { setSearchRequest(request); actionRegistry = registry; } private void setSearchRequest(SearchRequest request) { searchRequest = request; for (String sourceId : searchRequest.getSourceIds()) { queryStatus.put(sourceId, new QueryStatus(sourceId)); } } public synchronized void update(QueryResponse queryResponse) { update(null, queryResponse); } /** * Adds a query response to the cached set of results. * * @param queryResponse - Query response to add * @throws InterruptedException */ public synchronized void update(String sourceId, QueryResponse queryResponse) { if (queryResponse != null) { results = queryResponse.getResults(); updateResultStatus(queryResponse.getResults()); updateStatus(sourceId, queryResponse); } } public synchronized void failSource(String sourceId, Exception cause) { QueryResponseImpl failedResponse = new QueryResponseImpl(null); failedResponse.closeResultQueue(); failedResponse.setHits(0); failedResponse.getProcessingDetails() .add(new ProcessingDetailsImpl(sourceId, cause)); failedResponse.getProperties() .put("elapsed", -1L); updateStatus(sourceId, failedResponse); } private void updateStatus(String sourceId, QueryResponse queryResponse) { if (StringUtils.isBlank(sourceId)) { return; } if (!queryStatus.containsKey(sourceId)) { queryStatus.put(sourceId, new QueryStatus(sourceId)); } QueryStatus status = queryStatus.get(sourceId); status.setDetails(queryResponse.getProcessingDetails()); status.setHits(queryResponse.getHits()); hits += queryResponse.getHits(); status.setElapsed((Long) queryResponse.getProperties() .get("elapsed")); status.setState((isSuccessful(queryResponse.getProcessingDetails()) ? State.SUCCEEDED : State.FAILED)); responseNum++; queryResponse.getProcessingDetails() .stream() .filter(ProcessingDetails::hasException) .forEach(details -> { status.addReason(generateSanitizedErrorMessage(details.getException(), 0)); }); } private String generateSanitizedErrorMessage(Throwable e, int depth) { if (depth > MAX_EXCEPTION_SCAN_DEPTH || e == null) { return REASON_INTERNAL; } if (SAFE_ERROR_MESSAGES.containsKey(e.getClass())) { return SAFE_ERROR_MESSAGES.get(e.getClass()); } return generateSanitizedErrorMessage(e.getCause(), depth + 1); } private boolean isSuccessful(final Set<ProcessingDetails> details) { for (ProcessingDetails detail : details) { if (detail.hasException()) { return false; } } return true; } private void updateResultStatus(List<Result> results) { Bag hitSourceCount = new HashBag(); for (QueryStatus status : queryStatus.values()) { status.setResultCount(0); } for (Result result : results) { hitSourceCount.add(result.getMetacard() .getSourceId()); } for (Object sourceId : hitSourceCount.uniqueSet()) { if (queryStatus.containsKey(sourceId)) { queryStatus.get(sourceId) .setResultCount(hitSourceCount.getCount(sourceId)); } } } public boolean isFinished() { return responseNum >= searchRequest.getSourceIds() .size(); } public SearchRequest getSearchRequest() { return searchRequest; } public Map<String, QueryStatus> getQueryStatus() { return queryStatus; } public long getHits() { return hits; } public void setHits(long hits) { this.hits = hits; } public List<Result> getResults() { return results; } private void addObject(Map<String, Object> obj, String name, Object value) { if (value instanceof Number) { if (value instanceof Double) { if (((Double) value).isInfinite()) { obj.put(name, null); } else { obj.put(name, value); } } else if (value instanceof Float) { if (((Float) value).isInfinite()) { obj.put(name, null); } else { obj.put(name, value); } } else { obj.put(name, value); } } else if (value != null) { obj.put(name, value); } } public Map<String, Object> transform(String searchRequestId) throws CatalogTransformerException { Map<String, Object> result = new HashMap<>(); addObject(result, HITS, this.getHits()); addObject(result, ID, searchRequestId); addObject(result, RESULTS, getResultList(this.getResults())); addObject(result, STATUS, getQueryStatus(this.getQueryStatus())); addObject(result, METACARD_TYPES, getMetacardTypes(this.getResults())); return result; } private List<Map<String, Object>> getQueryStatus(Map<String, QueryStatus> queryStatus) { List<Map<String, Object>> statuses = new ArrayList<>(); for (QueryStatus status : queryStatus.values()) { Map<String, Object> statusObject = new HashMap<>(); addObject(statusObject, ID, status.getSourceId()); if (status.isDone()) { addObject(statusObject, RESULTS, status.getResultCount()); addObject(statusObject, HITS, status.getHits()); addObject(statusObject, ELAPSED, status.getElapsed()); addObject(statusObject, REASONS, status.getReasons()); } addObject(statusObject, STATE, status.getState()); statuses.add(statusObject); } return statuses; } private List<Map<String, Object>> getResultList(List<Result> results) throws CatalogTransformerException { List<Map<String, Object>> resultsList = new ArrayList<>(); if (results != null) { for (Result result : results) { if (result == null) { throw new CatalogTransformerException( "Cannot transform null " + Result.class.getName()); } Map<String, Object> resultItem = getResultItem(result); if (resultItem != null) { resultsList.add(resultItem); } } } return resultsList; } private Map<String, Object> getResultItem(Result result) throws CatalogTransformerException { Map<String, Object> transformedResult = new HashMap<>(); addObject(transformedResult, DISTANCE, result.getDistanceInMeters()); addObject(transformedResult, RELEVANCE, result.getRelevanceScore()); @SuppressWarnings("unchecked") Map<String, Object> metacard = (Map<String, Object>) GeoJsonMetacardTransformer.convertToJSON(result.getMetacard()); metacard.put(ACTIONS, getActions(result.getMetacard())); Attribute cachedDate = result.getMetacard() .getAttribute(CACHED); if (cachedDate != null && cachedDate.getValue() != null) { metacard.put(CACHED, ISO_8601_DATE_FORMAT.print(new DateTime(cachedDate.getValue()))); } else { metacard.put(CACHED, ISO_8601_DATE_FORMAT.print(new DateTime())); } metacard.put(IS_RESOURCE_LOCAL, Optional.ofNullable(result.getMetacard() .getAttribute(INTERNAL_LOCAL_RESOURCE)) .map(Attribute::getValue) .orElse(Boolean.FALSE)); addObject(transformedResult, METACARD, metacard); return transformedResult; } private List<Map<String, Object>> getActions(Metacard metacard) { List<Map<String, Object>> actionsJson = new ArrayList<>(); List<Action> actions = actionRegistry.list(metacard); if (actions != null) { for (Action action : actions) { Map<String, Object> actionJson = new HashMap<>(); actionJson.put(ACTIONS_ID, action.getId() + action.getTitle()); actionJson.put(ACTIONS_TITLE, action.getTitle()); actionJson.put(ACTIONS_DESCRIPTION, action.getDescription()); //user were seeing an issue where the json url was not quoted, we were not able to //reproduce the issue but resolved it by converting the url to a string actionJson.put(ACTIONS_URL, action.getUrl() != null ? action.getUrl() .toString() : null); actionsJson.add(actionJson); } } return actionsJson; } private Map<String, Object> getMetacardTypes(List<Result> results) throws CatalogTransformerException { Map<String, Object> typesObject = new HashMap<>(); for (Result result : results) { MetacardType type = result.getMetacard() .getMetacardType(); if (type != null && !StringUtils.isBlank(type.getName()) && !typesObject.containsKey( type.getName())) { Map<String, Object> typeObj = getType(type); if (typeObj != null) { typesObject.put(type.getName(), typeObj); } } } return typesObject; } private Map<String, Object> getType(MetacardType metacardType) throws CatalogTransformerException { Map<String, Object> fields = new HashMap<>(); for (AttributeDescriptor descriptor : metacardType.getAttributeDescriptors()) { Map<String, Object> description = new HashMap<>(); description.put(ATTRIBUTE_FORMAT, descriptor.getType() .getAttributeFormat() .toString()); description.put(ATTRIBUTE_INDEXED, descriptor.isIndexed()); fields.put(descriptor.getName(), description); } return fields; } }