org.codice.ddf.opensearch.endpoint.OpenSearchEndpoint.java Source code

Java tutorial

Introduction

Here is the source code for org.codice.ddf.opensearch.endpoint.OpenSearchEndpoint.java

Source

/**
 * 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.opensearch.endpoint;

import ddf.catalog.CatalogFramework;
import ddf.catalog.Constants;
import ddf.catalog.data.BinaryContent;
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;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
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.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.codice.ddf.configuration.SystemInfo;
import org.codice.ddf.opensearch.OpenSearch;
import org.codice.ddf.opensearch.OpenSearchConstants;
import org.codice.ddf.opensearch.endpoint.query.OpenSearchQuery;
import org.parboiled.errors.ParsingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Path("/")
public class OpenSearchEndpoint implements OpenSearch {

    private static final Logger LOGGER = LoggerFactory.getLogger(OpenSearchEndpoint.class);

    private static final String UPDATE_QUERY_INTERVAL = "interval";

    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_SORT_FIELD = OpenSearchConstants.SORT_RELEVANCE;

    private static final String DEFAULT_SORT_ORDER = OpenSearchConstants.ORDER_DESCENDING;

    private static final String DEFAULT_RADIUS = "5000";

    private static final Pattern SOURCES_PATTERN = Pattern.compile(OpenSearchConstants.SOURCES_DELIMITER);

    private static final Pattern SORT_PATTERN = Pattern.compile(OpenSearchConstants.SORT_DELIMITER);

    /**
     * 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'.
     */
    private static final Pattern EXPECTED_SORT_FORMAT = Pattern
            .compile(String.format("(%1$s(%5$s(%3$s|%4$s))?)|(%2$s(%5$s%4$s)?)", OpenSearchConstants.SORT_TEMPORAL,
                    OpenSearchConstants.SORT_RELEVANCE, OpenSearchConstants.ORDER_ASCENDING,
                    OpenSearchConstants.ORDER_DESCENDING, OpenSearchConstants.SORT_DELIMITER));

    private final CatalogFramework framework;

    private final FilterBuilder filterBuilder;

    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.
     * @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 selectors 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.
     */
    @Override
    @GET
    public Response processQuery(@QueryParam(OpenSearchConstants.SEARCH_TERMS) String searchTerms,
            @QueryParam(OpenSearchConstants.MAX_RESULTS) String maxResults,
            @QueryParam(OpenSearchConstants.SOURCES) String sources,
            @QueryParam(OpenSearchConstants.MAX_TIMEOUT) String maxTimeout,
            @QueryParam(OpenSearchConstants.START_INDEX) String startIndex,
            @QueryParam(OpenSearchConstants.COUNT) String count,
            @QueryParam(OpenSearchConstants.GEOMETRY) String geometry,
            @QueryParam(OpenSearchConstants.BBOX) String bbox,
            @QueryParam(OpenSearchConstants.POLYGON) String polygon,
            @QueryParam(OpenSearchConstants.LAT) String lat, @QueryParam(OpenSearchConstants.LON) String lon,
            @QueryParam(OpenSearchConstants.RADIUS) String radius,
            @QueryParam(OpenSearchConstants.DATE_START) String dateStart,
            @QueryParam(OpenSearchConstants.DATE_END) String dateEnd,
            @QueryParam(OpenSearchConstants.DATE_OFFSET) String dateOffset,
            @QueryParam(OpenSearchConstants.SORT) String sort,
            @QueryParam(OpenSearchConstants.FORMAT) String format,
            @QueryParam(OpenSearchConstants.SELECTORS) String selectors, @Context UriInfo ui,
            @QueryParam(OpenSearchConstants.TYPE) String type,
            @QueryParam(OpenSearchConstants.VERSIONS) String versions, @Context HttpServletRequest request) {
        Response response;
        LOGGER.trace("request url: {}", ui.getRequestUri());

        try {
            OpenSearchQuery query = createNewQuery(startIndex, count, maxResults, sort, maxTimeout);

            if (StringUtils.isNotEmpty(sources)) {
                LOGGER.trace("Received site names from client.");
                final Set<String> siteSet = SOURCES_PATTERN.splitAsStream(sources).collect(Collectors.toSet());

                // This code block is for backward compatibility to support src=local.
                // Since local is a magic word, not in any specification, we need to
                // eventually remove support for it.
                if (siteSet.remove(OpenSearchConstants.LOCAL_SOURCE)) {
                    LOGGER.trace("Found 'local' alias, replacing with {}.", SystemInfo.getSiteName());
                    siteSet.add(SystemInfo.getSiteName());
                }

                if (siteSet.contains(framework.getId()) && siteSet.size() == 1) {
                    LOGGER.trace("Only local site specified, saving overhead and just performing a local query on "
                            + framework.getId() + ".");
                } else {
                    LOGGER.trace("Querying site set: {}", siteSet);
                    query.setSiteIds(siteSet);
                }

                query.setIsEnterprise(false);
            } else {
                LOGGER.trace("No sites found, defaulting to enterprise query.");
                query.setIsEnterprise(true);
            }

            // contextual
            if (StringUtils.isNotBlank(searchTerms)) {
                query.addContextualFilter(searchTerms.trim(), selectors);
            }

            if (StringUtils.isNotBlank(dateStart) || StringUtils.isNotBlank(dateEnd)) {
                // If either start date OR end date is specified and non-empty, then a temporal filter can
                // be created
                query.addStartEndTemporalFilter(dateStart, dateEnd);
            } else if (StringUtils.isNotBlank(dateOffset)) {
                query.addOffsetTemporalFilter(dateOffset);
            }

            // spatial
            // single spatial criterion per query
            addSpatialFilter(query, geometry, polygon, bbox, radius, lat, lon);

            if (StringUtils.isNotBlank(type)) {
                query.addTypeFilter(type.trim(), versions);
            }

            Map<String, Serializable> properties = new HashMap<>();
            for (Object key : request.getParameterMap().keySet()) {
                if (key instanceof String) {
                    Object value = request.getParameterMap().get(key);
                    if (value != null) {
                        properties.put((String) key, ((String[]) value)[0]);
                    }
                }
            }

            response = executeQuery(format, query, ui, properties);
        } catch (ParsingException e) {
            LOGGER.debug("Bad input found while executing a query", e);
            response = Response.status(Response.Status.BAD_REQUEST)
                    .entity(wrapStringInPreformattedTags(e.getMessage())).build();
        } catch (RuntimeException re) {
            LOGGER.debug("Exception while executing a query", re);
            response = Response.serverError()
                    .entity(wrapStringInPreformattedTags("Exception while executing a query")).build();
        }

        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.
     */
    private void addSpatialFilter(OpenSearchQuery query, String geometry, String polygon, String bbox,
            String radius, String lat, String lon) {
        if (StringUtils.isNotBlank(geometry)) {
            LOGGER.trace("Adding SpatialCriterion geometry: {}", geometry);
            query.addGeometrySpatialFilter(geometry.trim());
        }

        if (StringUtils.isNotBlank(bbox)) {
            LOGGER.trace("Adding SpatialCriterion bbox: {}", bbox);
            query.addBBoxSpatialFilter(bbox.trim());
        }

        if (StringUtils.isNotBlank(polygon)) {
            LOGGER.trace("Adding SpatialCriterion polygon: {}", polygon);
            query.addPolygonSpatialFilter(polygon.trim());
        }

        if (StringUtils.isNotBlank(lat) && StringUtils.isNotBlank(lon)) {
            if (StringUtils.isBlank(radius)) {
                LOGGER.trace("Adding default radius {}", DEFAULT_RADIUS);
                query.addPointRadiusSpatialFilter(lon.trim(), lat.trim(), DEFAULT_RADIUS);
            } else {
                LOGGER.trace("Using radius: {}", radius);
                query.addPointRadiusSpatialFilter(lon.trim(), lat.trim(), radius.trim());
            }
        }
    }

    /**
     * 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
     * @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);

        LOGGER.trace("Attempting to execute query: {}", query);
        try {
            Map<String, Serializable> arguments = new HashMap<>();
            String organization = framework.getOrganization();
            String url = ui.getRequestUri().toString();
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("organization: {}", organization);
                LOGGER.trace("url: {}", url);
            }

            arguments.put("organization", organization);
            arguments.put("url", url);

            // if subscription is specified, add to arguments as well as update
            // interval
            if (CollectionUtils.isNotEmpty(subscriptionList)) {
                String subscription = subscriptionList.get(0);
                LOGGER.trace("Subscription: {}", subscription);
                arguments.put(Constants.SUBSCRIPTION_KEY, subscription);
                List<String> intervalList = queryParams.get(UPDATE_QUERY_INTERVAL);
                if (CollectionUtils.isNotEmpty(intervalList)) {
                    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.trace("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<>(), 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.debug("Unsupported query", ce);
            response = Response.status(Response.Status.BAD_REQUEST)
                    .entity(wrapStringInPreformattedTags("Unsupported query")).build();
        } catch (CatalogTransformerException e) {
            LOGGER.debug("Error transforming response", e);
            response = Response.serverError().entity(wrapStringInPreformattedTags("Error transforming response"))
                    .build();
        } catch (FederationException e) {
            LOGGER.debug("Error executing query", e);
            response = Response.serverError().entity(wrapStringInPreformattedTags("Error executing query")).build();
        } catch (SourceUnavailableException e) {
            LOGGER.debug("Error executing query because the underlying source was unavailable.", e);
            response = Response.serverError().entity(wrapStringInPreformattedTags(
                    "Error executing query because the underlying source was unavailable.")).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.debug("RuntimeException on executing query", e);
            response = Response.serverError()
                    .entity(wrapStringInPreformattedTags("RuntimeException on executing query")).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 maxResultsStr - maximum # of results to return
     * @param sortStr - how to sort the query results
     * @param maxTimeoutStr - timeout value on the query execution
     * @return - the new query
     */
    private OpenSearchQuery createNewQuery(final String startIndexStr, final String countStr,
            final String maxResultsStr, final String sortStr, final String maxTimeoutStr) {
        final int startIndex;
        if (StringUtils.isEmpty(startIndexStr)) {
            LOGGER.trace("Empty {} query parameter. Using default {} instead.", OpenSearchConstants.START_INDEX,
                    DEFAULT_START_INDEX);
            startIndex = DEFAULT_START_INDEX;
        } else {
            final int parsedInt = Integer.parseInt(startIndexStr);

            if (parsedInt > 0) {
                startIndex = parsedInt;
            } else {
                LOGGER.debug("{} query parameter must be greater than 0 but is \"{}\". Using default {} instead.",
                        OpenSearchConstants.START_INDEX, startIndexStr, DEFAULT_START_INDEX);
                startIndex = DEFAULT_START_INDEX;
            }
        }

        final int count;
        if (StringUtils.isEmpty(countStr)) {
            LOGGER.trace("Empty {} query parameter", OpenSearchConstants.COUNT);

            if (StringUtils.isEmpty(maxResultsStr)) {
                LOGGER.trace("Empty {} query parameter. Using default {} instead", OpenSearchConstants.MAX_RESULTS,
                        DEFAULT_COUNT);
                count = DEFAULT_COUNT;
            } else {
                // honor maxResults if count is not specified
                count = Integer.parseInt(maxResultsStr);
            }
        } else {
            count = Integer.parseInt(countStr);
        }

        final String sortField;
        final String sortOrder;
        if (StringUtils.isEmpty(sortStr)) {
            LOGGER.trace("Empty {} query parameter. Using defaults (field={}, order={}) instead.",
                    OpenSearchConstants.SORT, DEFAULT_SORT_FIELD, DEFAULT_SORT_ORDER);
            sortField = DEFAULT_SORT_FIELD;
            sortOrder = DEFAULT_SORT_ORDER;
        } else {
            if (EXPECTED_SORT_FORMAT.matcher(sortStr).matches()) {
                String[] sortAry = SORT_PATTERN.split(sortStr, 2);
                sortField = sortAry[0];

                if (sortAry.length == 2) {
                    sortOrder = sortAry[1];
                } else {
                    LOGGER.trace("Empty {} order query parameter. Using default {} instead.",
                            OpenSearchConstants.SORT, DEFAULT_SORT_ORDER);
                    sortOrder = DEFAULT_SORT_ORDER;
                }
            } else {
                LOGGER.debug(
                        "Invalid {} query parameter \"{}\". See the OpenSearch Endpoint documentation for details the parameter format. Using defaults (field={}, order={}) instead.",
                        OpenSearchConstants.SORT, sortStr, DEFAULT_SORT_FIELD, DEFAULT_SORT_ORDER);
                sortField = DEFAULT_SORT_FIELD;
                sortOrder = DEFAULT_SORT_ORDER;
            }
        }

        final long maxTimeout;
        if (StringUtils.isEmpty(maxTimeoutStr)) {
            LOGGER.trace("Empty {} query parameter. Using default {} instead.", OpenSearchConstants.MAX_TIMEOUT,
                    DEFAULT_TIMEOUT);
            maxTimeout = DEFAULT_TIMEOUT;
        } else {
            maxTimeout = Long.parseLong(maxTimeoutStr);
        }

        return new OpenSearchQuery(startIndex, count, sortField, sortOrder, maxTimeout, filterBuilder);
    }

    private String wrapStringInPreformattedTags(String stringToWrap) {
        return "<pre>" + stringToWrap + "</pre>";
    }
}