/** * 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.source.opensearch; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.Principal; import java.util.List; import org.apache.commons.lang.StringUtils; import org.apache.cxf.jaxrs.client.WebClient; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.ISODateTimeFormat; 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.Result; import ddf.catalog.impl.filter.SpatialDistanceFilter; import ddf.catalog.impl.filter.SpatialFilter; import ddf.catalog.impl.filter.TemporalFilter; import ddf.catalog.operation.Query; import ddf.catalog.source.UnsupportedQueryException; import ddf.security.Subject; import ddf.security.assertion.SecurityAssertion; /** * Utility helper class that performs much of the translation logic used in CddaOpenSearchSite. * */ public final class OpenSearchSiteUtil { // OpenSearch defined parameters public static final String SEARCH_TERMS = "q"; // temporal public static final String TIME_START = "dtstart"; public static final String TIME_END = "dtend"; public static final String TIME_NAME = "dateName"; // geospatial public static final String GEO_LAT = "lat"; public static final String GEO_LON = "lon"; public static final String GEO_RADIUS = "radius"; public static final String GEO_POLY = "polygon"; public static final String GEO_BBOX = "bbox"; // general options public static final String SRC = "src"; public static final String MAX_RESULTS = "mr"; public static final String COUNT = "count"; public static final String MAX_TIMEOUT = "mt"; public static final String USER_DN = "dn"; public static final String SORT = "sort"; public static final String FILTER = "filter"; // only for async searches public static final String START_INDEX = "start"; // geospatial constants public static final double LAT_DEGREE_M = 111325; public static final Integer DEFAULT_TOTAL_MAX = 1000; public static final Integer MAX_LAT = 90; public static final Integer MIN_LAT = -90; public static final Integer MAX_LON = 180; public static final Integer MIN_LON = -180; public static final Integer MAX_ROTATION = 360; public static final Integer MAX_BBOX_POINTS = 4; public static final String ORDER_ASCENDING = "asc"; public static final String ORDER_DESCENDING = "desc"; public static final String SORT_DELIMITER = ":"; public static final String SORT_RELEVANCE = "relevance"; public static final String SORT_TEMPORAL = "date"; private static final Logger LOGGER = LoggerFactory.getLogger(OpenSearchSiteUtil.class); private OpenSearchSiteUtil() { } /** * Populates general site information. * * @param client * Initial StringBuilder url that is not filled in. * @param query * @param subject */ public static void populateSearchOptions(WebClient client, Query query, Subject subject, List<String> parameters) { String maxTotalSize = null; String maxPerPage = null; String routeTo = ""; String timeout = null; String start = "1"; String dn = null; String filterStr = ""; String sortStr = null; if (query != null) { maxPerPage = String.valueOf(query.getPageSize()); if (query.getPageSize() > DEFAULT_TOTAL_MAX) { maxTotalSize = maxPerPage; } else if (query.getPageSize() <= 0) { maxTotalSize = String.valueOf(DEFAULT_TOTAL_MAX); } start = Integer.toString(query.getStartIndex()); timeout = Long.toString(query.getTimeoutMillis()); sortStr = translateToOpenSearchSort(query.getSortBy()); if (subject != null && subject.getPrincipals() != null && !subject.getPrincipals() .isEmpty()) { List principals = subject.getPrincipals().asList(); for (Object principal : principals) { if (principal instanceof SecurityAssertion) { SecurityAssertion assertion = (SecurityAssertion) principal; Principal assertionPrincipal = assertion.getPrincipal(); if (assertionPrincipal != null) { dn = assertionPrincipal.getName(); } } } } } checkAndReplace(client, start, START_INDEX, parameters); checkAndReplace(client, maxPerPage, COUNT, parameters); checkAndReplace(client, maxTotalSize, MAX_RESULTS, parameters); checkAndReplace(client, routeTo, SRC, parameters); checkAndReplace(client, timeout, MAX_TIMEOUT, parameters); checkAndReplace(client, dn, USER_DN, parameters); checkAndReplace(client, filterStr, FILTER, parameters); checkAndReplace(client, sortStr, SORT, parameters); } public static String translateToOpenSearchSort(SortBy ddfSort) { String openSearchSortStr = null; String orderType = null; if (ddfSort == null || ddfSort.getSortOrder() == null) { return openSearchSortStr; } if (ddfSort.getSortOrder().equals(SortOrder.ASCENDING)) { orderType = ORDER_ASCENDING; } else { orderType = ORDER_DESCENDING; } // QualifiedString type = ddfSort.getType(); PropertyName sortByField = ddfSort.getPropertyName(); if (Result.RELEVANCE.equals(sortByField.getPropertyName())) { // asc relevance not supported by spec openSearchSortStr = SORT_RELEVANCE + SORT_DELIMITER + ORDER_DESCENDING; } else if (Result.TEMPORAL.equals(sortByField.getPropertyName())) { openSearchSortStr = SORT_TEMPORAL + SORT_DELIMITER + orderType; } else { LOGGER.warn( "Couldn't determine sort policy, not adding sorting in request to federated site."); } return openSearchSortStr; } /** * Fills in the OpenSearch query URL with contextual information (Note: Section 2.2 - Query: The * OpenSearch specification does not define a syntax for its primary query parameter, * searchTerms, but it is generally used to support simple keyword queries.) * * @param client * @param searchPhrase */ public static void populateContextual(WebClient client, final String searchPhrase, List<String> parameters) { String queryStr = searchPhrase; if (queryStr != null) { try { queryStr = URLEncoder.encode(queryStr, "UTF-8"); } catch (UnsupportedEncodingException uee) { LOGGER.warn("Could not encode contextual string: {}", uee.getMessage()); } } checkAndReplace(client, queryStr, SEARCH_TERMS, parameters); } /** * Fills in the opensearch query URL with temporal information (Start, End, and Name). Currently * name is empty due to incompatibility with endpoints. * * @param client * OpenSearch URL to populate * @param temporal * TemporalCriteria that contains temporal data */ public static void populateTemporal(WebClient client, TemporalFilter temporal, List<String> parameters) { DateTimeFormatter fmt = ISODateTimeFormat.dateTime(); String start = ""; String end = ""; String name = ""; if (temporal != null) { long startLng = (temporal.getStartDate() != null) ? temporal.getStartDate().getTime() : 0; start = fmt.print(startLng); long endLng = (temporal.getEndDate() != null) ? temporal.getEndDate().getTime() : System.currentTimeMillis(); end = fmt.print(endLng); } checkAndReplace(client, start, TIME_START, parameters); checkAndReplace(client, end, TIME_END, parameters); checkAndReplace(client, name, TIME_NAME, parameters); } /** * Fills in the OpenSearch query URL with geospatial information (poly, lat, lon, and radius). * * @param client * OpenSearch URL to populate * @param spatial * SpatialCriteria that contains the spatial data */ public static void populateGeospatial(WebClient client, SpatialDistanceFilter spatial, boolean shouldConvertToBBox, List<String> parameters) throws UnsupportedQueryException { String lat = ""; String lon = ""; String radiusStr = ""; StringBuilder bbox = new StringBuilder(""); StringBuilder poly = new StringBuilder(""); if (spatial != null) { String wktStr = spatial.getGeometryWkt(); double radius = spatial.getDistanceInMeters(); if (wktStr.indexOf("POINT") != -1) { String[] latLon = createLatLonAryFromWKT(wktStr); lon = latLon[0]; lat = latLon[1]; radiusStr = Double.toString(radius); if (shouldConvertToBBox) { double[] bboxCoords = createBBoxFromPointRadius(Double.parseDouble(lon), Double.parseDouble(lat), radius); for (int i = 0; i < MAX_BBOX_POINTS; i++) { if (i > 0) { bbox.append(","); } bbox.append(bboxCoords[i]); } lon = ""; lat = ""; radiusStr = ""; } } else { LOGGER.warn("WKT ({}) not supported for POINT-RADIUS search, use POINT.", wktStr); } } checkAndReplace(client, lat, GEO_LAT, parameters); checkAndReplace(client, lon, GEO_LON, parameters); checkAndReplace(client, radiusStr, GEO_RADIUS, parameters); checkAndReplace(client, poly.toString(), GEO_POLY, parameters); checkAndReplace(client, bbox.toString(), GEO_BBOX, parameters); } /** * Fills in the OpenSearch query URL with geospatial information (poly, lat, lon, and radius). * * @param client * OpenSearch URL to populate * @param spatial * SpatialCriteria that contains the spatial data */ public static void populateGeospatial(WebClient client, SpatialFilter spatial, boolean shouldConvertToBBox, List<String> parameters) throws UnsupportedQueryException { String lat = ""; String lon = ""; String radiusStr = ""; StringBuilder bbox = new StringBuilder(""); StringBuilder poly = new StringBuilder(""); if (spatial != null) { String wktStr = spatial.getGeometryWkt(); if (wktStr.indexOf("POLYGON") != -1) { String[] polyAry = createPolyAryFromWKT(wktStr); if (shouldConvertToBBox) { double[] bboxCoords = createBBoxFromPolygon(polyAry); for (int i = 0; i < MAX_BBOX_POINTS; i++) { if (i > 0) { bbox.append(","); } bbox.append(bboxCoords[i]); } } else { for (int i = 0; i < polyAry.length - 1; i += 2) { if (i != 0) { poly.append(","); } poly.append(polyAry[i + 1] + "," + polyAry[i]); } } } else { LOGGER.warn("WKT ({}) not supported for SPATIAL search, use POLYGON.", wktStr); } } checkAndReplace(client, lat, GEO_LAT, parameters); checkAndReplace(client, lon, GEO_LON, parameters); checkAndReplace(client, radiusStr, GEO_RADIUS, parameters); checkAndReplace(client, poly.toString(), GEO_POLY, parameters); checkAndReplace(client, bbox.toString(), GEO_BBOX, parameters); } /** * Parses a WKT polygon string and returns a string array containing the lon and lat. * * @param wkt * WKT String in the form of POLYGON((Lon Lat, Lon Lat...)) * @return Lon on even # and Lat on odd # */ public static String[] createPolyAryFromWKT(String wkt) { String lonLat = wkt.substring(wkt.indexOf("((") + 2, wkt.indexOf("))")); return lonLat.split(" |,\\p{Space}?"); } /** * Parses a WKT Point string and returns a string array containing the lon and lat. * * @param wkt * WKT String in the form of POINT( Lon Lat) * @return Lon at position 0, Lat at position 1 */ public static String[] createLatLonAryFromWKT(String wkt) { String lonLat = wkt.substring(wkt.indexOf('(') + 1, wkt.indexOf(')')); return lonLat.split(" "); } /** * Checks the input and replaces the items inside of the url. * * @param client * The URL to do the replacement on. <b>NOTE:</b> replacement is done directly on * this object. * @param inputStr * Item to put into the URL. * @param definition * Area inside of the URL to be replaced by. */ private static void checkAndReplace(WebClient client, String inputStr, String definition, List<String> parameters) { if (hasParameter(definition, parameters)) { if (StringUtils.isNotEmpty(inputStr)) { client.replaceQueryParam(definition, inputStr); } } } private static boolean hasParameter(String parameter, List<String> parameters) { for (String param : parameters) { if (param != null && param.equalsIgnoreCase(parameter)) { return true; } } return false; } /** * Takes in a point radius search and converts it to a (rough approximation) bounding box. * * @param lon * latitude in decimal degrees (WGS-84) * @param lat * longitude in decimal degrees (WGS-84) * @param radius * radius, in meters * @return Array of bounding box coordinates in the following order: West South East North. Also * described as minX, minY, maxX, maxY (where longitude is the X-axis, and latitude is * the Y-axis). */ public static double[] createBBoxFromPointRadius(double lon, double lat, double radius) { double minX; double minY; double maxX; double maxY; double lonDifference = radius / (LAT_DEGREE_M * Math.cos(lat)); double latDifference = radius / LAT_DEGREE_M; minX = lon - lonDifference; if (minX < MIN_LON) { minX += MAX_ROTATION; } maxX = lon + lonDifference; if (maxX > MAX_LON) { maxX -= MAX_ROTATION; } minY = lat - latDifference; if (minY < MIN_LAT) { minY = Math.abs(minY + MAX_LAT) - MAX_LAT; } maxY = lat + latDifference; if (maxY > MAX_LAT) { maxY = MAX_LAT - (maxY - MAX_LAT); } return new double[] {minX, minY, maxX, maxY}; } /** * Takes in an array of coordinates and converts it to a (rough approximation) bounding box. * * Note: Searches being performed where the polygon goes through the international date line may * return a bad bounding box. * * @param polyAry * array of coordinates (lon,lat,lon,lat,lon,lat..etc) * @return Array of bounding box coordinates in the following order: West South East North. Also * described as minX, minY, maxX, maxY (where longitude is the X-axis, and latitude is * the Y-axis). */ public static double[] createBBoxFromPolygon(String[] polyAry) { double minX = Double.POSITIVE_INFINITY; double minY = Double.POSITIVE_INFINITY; double maxX = Double.NEGATIVE_INFINITY; double maxY = Double.NEGATIVE_INFINITY; double curX, curY; for (int i = 0; i < polyAry.length - 1; i += 2) { LOGGER.debug("polyToBBox: lon - {} lat - {}", polyAry[i], polyAry[i + 1]); curX = Double.parseDouble(polyAry[i]); curY = Double.parseDouble(polyAry[i + 1]); if (curX < minX) { minX = curX; } if (curX > maxX) { maxX = curX; } if (curY < minY) { minY = curY; } if (curY > maxY) { maxY = curY; } } return new double[] {minX, minY, maxX, maxY}; } }