/** * 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.endpoints; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import org.apache.commons.lang.StringUtils; import org.codice.ddf.configuration.ConfigurationManager; import org.codice.ddf.configuration.ConfigurationWatcher; import org.codice.ddf.opensearch.query.OpenSearchQuery; import org.parboiled.errors.ParsingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.CatalogFramework; import ddf.catalog.Constants; import ddf.catalog.data.BinaryContent; import ddf.catalog.data.Result; import ddf.catalog.federation.FederationException; import ddf.catalog.filter.FilterBuilder; import ddf.catalog.operation.QueryRequest; import ddf.catalog.operation.QueryResponse; 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.transform.CatalogTransformerException; @Path("/") public class OpenSearchEndpoint implements ConfigurationWatcher, OpenSearch { private static final String UPDATE_QUERY_INTERVAL = "interval"; private static final Logger LOGGER = LoggerFactory.getLogger(OpenSearchEndpoint.class); private static final String DEFAULT_SORT_FIELD = "relevance"; private static final String DEFAULT_SORT_ORDER = "desc"; private static final String DEFAULT_FORMAT = "atom"; private static final long DEFAULT_TIMEOUT = 300000; private static final int DEFAULT_COUNT = 10; private static final int DEFAULT_START_INDEX = 1; private static final String DEFAULT_RADIUS = "5000"; private static final String LOCAL = "local"; private final CatalogFramework framework; private final FilterBuilder filterBuilder; // DDF Site Name private String localSiteName = null; public OpenSearchEndpoint(CatalogFramework framework, FilterBuilder filterBuilder) { this.framework = framework; this.filterBuilder = filterBuilder; } /** * * @param searchTerms * Space delimited list of search terms. * @param maxResults * Maximum # of results to return. If count is also specified, the count value will * take precedence over the maxResults value * @param sources * Comma delimited list of data sources to query (default: default sources selected). * @param maxTimeout * Maximum timeout (msec) for query to respond (default: mt=30000). * @param startIndex * Index of first result to return. Integer >= 0 (default: start=1). * @param count * Number of results to retrieve per page (default: count=10). * @param geometry * WKT Geometries (Support POINT and POLYGON). * @param bbox * Comma delimited list of lat/lon (deg) bounding box coordinates (geo format: * geo:bbox ~ West,South,East,North). * @param polygon * Comma delimited list of lat/lon (deg) pairs, in clockwise order around the * polygon, with the last point being the same as the first in order to close the * polygon. * @param lat * Latitude in decimal degrees (typical GPS receiver WGS84 coordinates). * @param lon * Longitude in decimal degrees (typical GPS receiver WGS84 coordinates). * @param radius * The radius (m) parameter, used with the lat and lon parameters, specifies the * search distance from this point (default: radius=5000). * @param dateStart * Specifies the beginning of the time slice of the search on the modified time field * (RFC-3339 - Date and Time format, i.e. YYYY-MM-DDTHH:mm:ssZ). Default value of * "1970-01-01T00:00:00Z" is used when dtend is indicated but dtstart is not * specified * @param dateEnd * Specifies the ending of the time slice of the search on the modified time field * (RFC-3339 - Date and Time format, i.e. YYYY-MM-DDTHH:mm:ssZ). Current GMT * date/time is used when dtstart is specified but not dtend. * @param dateOffset * Specifies an offset, backwards from the current time, to search on the modified * time field for entries. Defined in milliseconds. * @param sort * Specifies sort by field as sort=<sbfield>:<sborder>, where <sbfield> may be 'date' * or 'relevance' (default is 'relevance'). The conditional param <sborder> is * optional but has a value of 'asc' or 'desc' (default is 'desc'). When <sbfield> is * 'relevance', <sborder> must be 'desc'. * @param format * Defines the format that the return type should be in. (example:atom, html) * @param selector * Defines a comma delimited list of XPath selectors to narrow the query. * @param type * Specifies the type of data to search for. (example: nitf) * @param versions * Specifies the versions in a comma delimited list. * @return */ @GET public Response processQuery(@QueryParam(PHRASE) String searchTerms, @QueryParam(MAX_RESULTS) String maxResults, @QueryParam(SOURCES) String sources, @QueryParam(MAX_TIMEOUT) String maxTimeout, @QueryParam(START_INDEX) String startIndex, @QueryParam(COUNT) String count, @QueryParam(GEOMETRY) String geometry, @QueryParam(BBOX) String bbox, @QueryParam(POLYGON) String polygon, @QueryParam(LAT) String lat, @QueryParam(LON) String lon, @QueryParam(RADIUS) String radius, @QueryParam(DATE_START) String dateStart, @QueryParam(DATE_END) String dateEnd, @QueryParam(DATE_OFFSET) String dateOffset, @QueryParam(SORT) String sort, @QueryParam(FORMAT) String format, @QueryParam(SELECTOR) String selector, @Context UriInfo ui, @QueryParam(TYPE) String type, @QueryParam(VERSION) String versions, @Context HttpServletRequest request) { final String methodName = "processQuery"; LOGGER.trace("ENTERING: " + methodName); Response response; String localCount = count; LOGGER.debug("request url: " + ui.getRequestUri()); // honor maxResults if count is not specified if ((StringUtils.isEmpty(localCount)) && (!(StringUtils.isEmpty(maxResults)))) { LOGGER.debug("setting count to: " + maxResults); localCount = maxResults; } try { String queryFormat = format; OpenSearchQuery query = createNewQuery(startIndex, localCount, sort, maxTimeout); if (!(StringUtils.isEmpty(sources))) { LOGGER.debug("Received site names from client."); Set<String> siteSet = new HashSet<String>( Arrays.asList(StringUtils.stripAll(sources.split(",")))); // This code block is for backward compatibility to support src=local. // Since local is a magic work, not in any specification, weneed to // eventually remove support for it. if (siteSet.remove(LOCAL)) { LOGGER.debug("Found 'local' alias, replacing with " + localSiteName + "."); siteSet.add(localSiteName); } if (siteSet.contains(framework.getId()) && siteSet.size() == 1) { LOGGER.debug( "Only local site specified, saving overhead and just performing a local query on " + framework.getId() + "."); } else { LOGGER.debug("Querying site set: " + siteSet); query.setSiteIds(siteSet); } query.setIsEnterprise(false); } else { LOGGER.debug("No sites found, defaulting to enterprise query."); query.setIsEnterprise(true); } // contextual if (searchTerms != null && !searchTerms.trim().isEmpty()) { try { query.addContextualFilter(searchTerms, selector); } catch (ParsingException e) { throw new IllegalArgumentException(e.getMessage()); } } // temporal // single temporal criterion per query if ((dateStart != null && !dateStart.trim().isEmpty()) || (dateEnd != null && !dateEnd .trim().isEmpty()) || (dateOffset != null && !dateOffset.trim().isEmpty())) { query.addTemporalFilter(dateStart, dateEnd, dateOffset); } // spatial // single spatial criterion per query addSpatialFilter(query, geometry, polygon, bbox, radius, lat, lon); if (type != null && !type.trim().isEmpty()) { query.addTypeFilter(type, versions); } Map<String, Serializable> properties = new HashMap<String, Serializable>(); for (Object key : request.getParameterMap().keySet()) { if (key instanceof String) { Object value = request.getParameterMap().get(key); if (value instanceof Serializable) { properties.put((String) key, ((String[]) value)[0]); } } } response = executeQuery(queryFormat, query, ui, properties); } catch (IllegalArgumentException iae) { LOGGER.warn("Bad input found while executing a query", iae); response = Response.status(Response.Status.BAD_REQUEST) .entity(wrapStringInPreformattedTags(iae.getMessage())).build(); } catch (RuntimeException re) { LOGGER.warn("Exception while executing a query", re); response = Response.serverError().entity(wrapStringInPreformattedTags(re.getMessage())) .build(); } LOGGER.trace("EXITING: " + methodName); return response; } /** * Creates SpatialCriterion based on the input parameters, any null values will be ignored * * @param geometry * - the geo to search over * @param polygon * - the polygon to search over * @param bbox * - the bounding box to search over * @param radius * - the radius for a point radius search * @param lat * - the latitude of the point. * @param lon * - the longitude of the point. * @return - the spatialCriterion created, can be null */ private void addSpatialFilter(OpenSearchQuery query, String geometry, String polygon, String bbox, String radius, String lat, String lon) { if (geometry != null && !geometry.trim().isEmpty()) { LOGGER.debug("Adding SpatialCriterion geometry: " + geometry); query.addGeometrySpatialFilter(geometry); } else if (bbox != null && !bbox.trim().isEmpty()) { LOGGER.debug("Adding SpatialCriterion bbox: " + bbox); query.addBBoxSpatialFilter(bbox); } else if (polygon != null && !polygon.trim().isEmpty()) { LOGGER.debug("Adding SpatialCriterion polygon: " + polygon); query.addPolygonSpatialFilter(polygon); } else if (lat != null && !lat.trim().isEmpty() && lon != null && !lon.trim().isEmpty()) { if (radius == null || radius.trim().isEmpty()) { LOGGER.debug("Adding default radius"); query.addSpatialDistanceFilter(lon, lat, DEFAULT_RADIUS); } else { LOGGER.debug("Using radius: " + radius); query.addSpatialDistanceFilter(lon, lat, radius); } } } /** * Executes the OpenSearchQuery and formulates the response * * @param format * - of the results in the response * * @param query * - the query to execute * * @param ui * -the ui information to use to format the results * * @param properties * @return the response on the query */ private Response executeQuery(String format, OpenSearchQuery query, UriInfo ui, Map<String, Serializable> properties) { Response response = null; String queryFormat = format; MultivaluedMap<String, String> queryParams = ui.getQueryParameters(); List<String> subscriptionList = queryParams.get(Constants.SUBSCRIPTION_KEY); try { Map<String, Serializable> arguments = new HashMap<String, Serializable>(); String organization = framework.getOrganization(); String url = ui.getRequestUri().toString(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("organization: " + organization); LOGGER.debug("url: " + url); } arguments.put("organization", organization); arguments.put("url", url); // if subscription is specified, add to arguments as well as update // interval if (subscriptionList != null && !subscriptionList.isEmpty()) { String subscription = subscriptionList.get(0); LOGGER.debug("Subscription: " + subscription); arguments.put(Constants.SUBSCRIPTION_KEY, subscription); List<String> intervalList = queryParams.get(UPDATE_QUERY_INTERVAL); if (intervalList != null && !intervalList.isEmpty()) { arguments.put(UPDATE_QUERY_INTERVAL, intervalList.get(0)); } } if (StringUtils.isEmpty(queryFormat)) { queryFormat = DEFAULT_FORMAT; } if (query.getFilter() != null) { QueryRequest queryRequest = new QueryRequestImpl(query, query.isEnterprise(), query.getSiteIds(), properties); QueryResponse queryResponse; LOGGER.debug("Sending query"); queryResponse = framework.query(queryRequest); // pass in the format for the transform BinaryContent content = framework.transform(queryResponse, queryFormat, arguments); response = Response.ok(content.getInputStream(), content.getMimeTypeValue()) .build(); } else { // No query was specified QueryRequest queryRequest = new QueryRequestImpl(query, query.isEnterprise(), query.getSiteIds(), null); // Create a dummy QueryResponse with zero results QueryResponseImpl queryResponseQueue = new QueryResponseImpl(queryRequest, new ArrayList<Result>(), 0); // pass in the format for the transform BinaryContent content = framework .transform(queryResponseQueue, queryFormat, arguments); if (null != content) { response = Response.ok(content.getInputStream(), content.getMimeTypeValue()) .build(); } } } catch (UnsupportedQueryException ce) { LOGGER.warn("Error executing query", ce); response = Response.serverError().entity(wrapStringInPreformattedTags(ce.getMessage())) .build(); } catch (CatalogTransformerException e) { LOGGER.warn("Error tranforming response", e); response = Response.serverError().entity(wrapStringInPreformattedTags(e.getMessage())) .build(); } catch (FederationException e) { LOGGER.warn("Error executing query", e); response = Response.serverError().entity(wrapStringInPreformattedTags(e.getMessage())) .build(); } catch (SourceUnavailableException e) { LOGGER.warn("Error executing query because the underlying source was unavailable.", e); response = Response.serverError().entity(wrapStringInPreformattedTags(e.getMessage())) .build(); } 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.warn("RuntimeException on executing query", e); response = Response.serverError().entity(wrapStringInPreformattedTags(e.getMessage())) .build(); } return response; } /** * Creates a new query from the incoming parameters * * @param startIndexStr * - Start index for the query * @param countStr * - number of results for the query * @param sortStr * - How to sort the query results * @param maxTimeoutStr * - timeout value on the query execution * @return - the new query */ private OpenSearchQuery createNewQuery(String startIndexStr, String countStr, String sortStr, String maxTimeoutStr) { // default values String sortField = DEFAULT_SORT_FIELD; String sortOrder = DEFAULT_SORT_ORDER; Integer startIndex = DEFAULT_START_INDEX; Integer count = DEFAULT_COUNT; long maxTimeout = DEFAULT_TIMEOUT; // Updated to use the passed in index if valid (=> 1) // and to use the default if no value, or an invalid value (< 1) // is specified if (!(StringUtils.isEmpty(startIndexStr)) && (Integer.parseInt(startIndexStr) > 0)) { startIndex = Integer.parseInt(startIndexStr); } if (!(StringUtils.isEmpty(countStr))) { count = Integer.parseInt(countStr); } if (!(StringUtils.isEmpty(sortStr))) { String[] sortAry = sortStr.split(":"); if (sortAry.length > 1) { sortField = sortAry[0]; sortOrder = sortAry[1]; } } if (!(StringUtils.isEmpty(maxTimeoutStr))) { maxTimeout = Long.parseLong(maxTimeoutStr); } LOGGER.debug("Retrieved query settings: \n" + "sortField:" + sortField + "\nsortOrder:" + sortOrder); return new OpenSearchQuery(null, startIndex, count, sortField, sortOrder, maxTimeout, filterBuilder); } @Override public void configurationUpdateCallback(Map<String, String> ddfProperties) { String methodName = "configurationUpdateCallback"; LOGGER.trace("ENTERING: " + methodName); // Need the id aka sitename property for the query if (ddfProperties != null && !ddfProperties.isEmpty()) { String siteName = ddfProperties.get(ConfigurationManager.SITE_NAME); if (StringUtils.isNotBlank(siteName)) { this.localSiteName = siteName; } } else { LOGGER.debug("properties are NULL or empty"); } LOGGER.trace("EXITING: " + methodName); } private String wrapStringInPreformattedTags(String stringToWrap) { return "<pre>" + stringToWrap + "</pre>"; } }