/** * Copyright (C) 2014 Cohesive Integrations, LLC (info@cohesiveintegrations.com) * Copyright (C) 2016 Pink Summit, LLC (info@pinksummit.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.di2e.ecdr.querylanguage.basic; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.ws.rs.core.MultivaluedMap; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.opengis.filter.Filter; import org.opengis.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; import org.parboiled.Parboiled; import org.parboiled.parserunners.ParseRunner; import org.parboiled.parserunners.RecoveringParseRunner; import org.parboiled.support.ParsingResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.io.ParseException; import com.vividsolutions.jts.io.WKTReader; import com.vividsolutions.jts.io.WKTWriter; import ddf.catalog.data.Metacard; import ddf.catalog.data.Result; import ddf.catalog.filter.FilterBuilder; import ddf.catalog.filter.impl.SortByImpl; import ddf.catalog.source.UnsupportedQueryException; import net.di2e.ecdr.api.config.SortTypeConfiguration; import net.di2e.ecdr.api.query.QueryConfiguration; import net.di2e.ecdr.api.query.QueryCriteria; import net.di2e.ecdr.api.query.QueryLanguage; import net.di2e.ecdr.commons.CDRMetacard; import net.di2e.ecdr.commons.constants.SearchConstants; import net.di2e.ecdr.commons.query.CDRQueryCriteriaImpl; import net.di2e.ecdr.commons.util.DateTypeMap; import net.di2e.ecdr.commons.util.GeospatialUtils; import net.di2e.ecdr.commons.util.SearchUtils; import net.di2e.ecdr.querylanguage.basic.GeospatialCriteria.SpatialOperator; import net.di2e.ecdr.querylanguage.basic.PropertyCriteria.Operator; import net.di2e.ecdr.querylanguage.basic.keywordparser.ASTNode; import net.di2e.ecdr.querylanguage.basic.keywordparser.KeywordTextParser; public class CDRKeywordQueryLanguage implements QueryLanguage { private static final Logger LOGGER = LoggerFactory.getLogger( CDRKeywordQueryLanguage.class ); private static Map<String, String> queryParametersMap = null; private FilterBuilder filterBuilder = null; private List<SortTypeConfiguration> sortTypeConfigurationList = null; private DateTypeMap dateTypeMap = null; public CDRKeywordQueryLanguage( FilterBuilder builder, List<SortTypeConfiguration> sortTypeConfigurations, DateTypeMap dateMap ) { filterBuilder = builder; sortTypeConfigurationList = sortTypeConfigurations; dateTypeMap = dateMap; queryParametersMap = new HashMap<String, String>(); queryParametersMap.put( SearchConstants.UID_PARAMETER, "geo:uid" ); queryParametersMap.put( SearchConstants.RESOURCE_URI_PARAMETER, "ddf:resource-uri" ); queryParametersMap.put( SearchConstants.CASESENSITIVE_PARAMETER, "cdrsx:caseSensitive" ); queryParametersMap.put( SearchConstants.CONTENT_COLLECTIONS_PARAMETER, "ecdr:collections" ); queryParametersMap.put( SearchConstants.FUZZY_PARAMETER, "ecdr:fuzzy" ); queryParametersMap.put( SearchConstants.BOX_PARAMETER, "geo:box" ); queryParametersMap.put( SearchConstants.LATITUDE_PARAMETER, "geo:lat" ); queryParametersMap.put( SearchConstants.LONGITUDE_PARAMETER, "geo:lon" ); queryParametersMap.put( SearchConstants.RADIUS_PARAMETER, "geo:radius" ); queryParametersMap.put( SearchConstants.GEOMETRY_PARAMETER, "geo:geometry" ); queryParametersMap.put( SearchConstants.POLYGON_PARAMETER, "polygon" ); queryParametersMap.put( SearchConstants.GEO_RELATION_PARAMETER, "geo:relation" ); queryParametersMap.put( SearchConstants.GEO_NAME_PARAMETER, "geo:name" ); queryParametersMap.put( SearchConstants.STARTDATE_PARAMETER, "time:start" ); queryParametersMap.put( SearchConstants.ENDDATE_PARAMETER, "time:end" ); queryParametersMap.put( SearchConstants.DATETYPE_PARAMETER, "cdrsx:dateType" ); queryParametersMap.put( SearchConstants.DATE_RELATION_PARAMETER, "time:relation" ); queryParametersMap.put( SearchConstants.GEORSS_RESULT_FORMAT_PARAMETER, "ecdr:georssFormat" ); queryParametersMap.put( SearchConstants.CONTENT_TYPE_PARAMETER, "ddf:metadata-content-type" ); queryParametersMap.put( SearchConstants.TEXTPATH_PARAMETER, "ecdr:textPath" ); queryParametersMap.put( SearchConstants.SORTKEYS_PARAMETER, "sru:sortKeys" ); } @Override public String getName() { return SearchConstants.CDR_KEYWORD_QUERY_LANGUAGE; } @Override public String getUrlTemplateParameters() { StringBuilder sb = new StringBuilder(); for ( Entry<String, String> entry : queryParametersMap.entrySet() ) { sb.append( "&" + entry.getKey() + "={" + entry.getValue() + "?}" ); } return sb.toString(); } @Override public String getLanguageDescription( QueryConfiguration queryConfig ) { // @formatter:off String description = "CDR Keyword Basic Query Language" + System.lineSeparator() + "****************************" + System.lineSeparator() + "Usage: To use the CQL query language specify the '" + getName() + "' in the {cdrs:queryLanguage} parameter placeholder." + System.lineSeparator() + " The CDR Keyword Basic query language supports booleans (AND, OR, NOT) and parenthesis in the {os:searchTerms} parameter value" + System.lineSeparator() + " Additionally the parameters below can be used for temporal, geospatial, property, or enhanced keyword searches" + System.lineSeparator() + System.lineSeparator() + "The examples below are only for the keywords that can be used in the {os:searchTerms}. They can be combined with any of the " + "additional parameters defined in the sections that follow. " + System.lineSeparator() + "Examples: ballpark" + System.lineSeparator() + " ballpark AND goodyear" + System.lineSeparator() + " ballpark AND (goodyear or peoria)" + System.lineSeparator() + " " + System.lineSeparator() + " " + System.lineSeparator() + "**** ID/URI Search Parameters ****" + System.lineSeparator() + System.lineSeparator() + "geo:uid - unique identifier of the record, matches the Metacard.ID field" + System.lineSeparator() + System.lineSeparator() + "ddf:resource-uri - URL encoded resource URI value that will be directly matched on, matches the Metacard.RESOURCE_URI field" + System.lineSeparator() + System.lineSeparator() + System.lineSeparator() + "**** Contextual Search Parameters ****" + System.lineSeparator() + System.lineSeparator() + "cdrsx:caseSensitive - boolean (1 or 0) specifying whether or not the keyword search should be case sensitive" + System.lineSeparator() + " default: 0 (false - case insensitive) " + System.lineSeparator() + System.lineSeparator() + "ecdr:fuzzy - boolean (1 or 0) specifying whether or not the keyword search should be fuzzy (fuzzy allows for slight misspellings or derivations to be found)" + System.lineSeparator() + " default: ${defaultFuzzyCustom} (${defaultFuzzy}) " + System.lineSeparator() + System.lineSeparator() + System.lineSeparator() + "**** Geospatial Search Parameters ****" + System.lineSeparator() + System.lineSeparator() + "geo:box - comma delimited list of lat/lon (deg) bounding box coordinates (geo format: geo:bbox ~ west,south,east,north). " + "This is also commonly referred to by minX, minY, maxX, maxY (where longitude is the X-axis, and latitude is the Y-axis)." + System.lineSeparator() + System.lineSeparator() + "geo:lat/lon - latitude and longitude, respectively, in decimal degrees (typical GPS receiver WGS84 coordinates). Should include a 'radius' parameter " + "that specifies the search radius in meters." + System.lineSeparator() + System.lineSeparator() + "geo:radius - the radius (in meters) parameter, used with the lat and lon parameters, specifies the search distance from this point." + System.lineSeparator() + " default: ${defaultRadius}" + System.lineSeparator() + System.lineSeparator() + "geo:geometry - The geometry is defined using the Well Known Text and supports the following 2D geographic shapes: POINT, LINESTRING, POLYGON, MULTIPOINT, " + "MULTILINESTRING, MULTIPOLYGON (the Geometry shall be expressed using the EPSG:4326e)" + System.lineSeparator() + " examples: POINT(1 5)" + System.lineSeparator() + " POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2))" + System.lineSeparator() + System.lineSeparator() + "geo:polygon - (deprecated) polygon defined as comma separated latitude, longitude pairs, in clockwise order, with the last point being the same as the first " + "in order to close the polygon." + System.lineSeparator() + " example: 45.256,-110.45,46.46,-109.48,43.84,-109.86,45.256,-110.45" + System.lineSeparator() + System.lineSeparator() + "geo:relation - spatial operator for the relation to the result set " + System.lineSeparator() + " default: intersects" + System.lineSeparator() + " allowedValues: 'intersects', 'contains', 'disjoint'" + System.lineSeparator() + System.lineSeparator() + "geo:name - A string describing the location (place name) to perform the search " + System.lineSeparator() + " examples: Washington DC" + System.lineSeparator() + " Baltimore, MD" + System.lineSeparator() + System.lineSeparator() + "**** Temporal Search Parameters ****" + System.lineSeparator() + System.lineSeparator() + "time:start - replaced with a string of the beginning of the time slice of the search (RFC-3339 - Date and Time format, i.e. YYYY-MM-DDTHH:mm:ssZ). " + "Default value of \"1970-01-01T00:00:00Z\" is used when {time:end} is indicated but {time:start} is not specified." + System.lineSeparator() + System.lineSeparator() + "time:end - replaced with a string of the ending of the time slice of the search (RFC-3339 - Date and Time format, i.e. YYYY-MM-DDTHH:mm:ssZ). " + "Current GMT date/time is used when {time:start} is specified but not {time:end}." + System.lineSeparator() + System.lineSeparator() + "time:relation - temporal operation for the relation to the result set" + System.lineSeparator() + " default: intersects" + System.lineSeparator() + " allowedValues: 'intersects', 'contains', 'during', 'disjoint', 'equals'" + System.lineSeparator() + System.lineSeparator() + "cdrsx:dateType - the date type to compare against" + System.lineSeparator() + " default: ${defaultDateType}" + System.lineSeparator() + " allowedValues: ${dateTypeValues}" + System.lineSeparator() + System.lineSeparator() + System.lineSeparator() + "**** Content Collections Search Parameters ****" + System.lineSeparator() + System.lineSeparator() + "ecdr:collections - a comma separated list of content collections to search over. list of content collections can be retrieved by using the Describe spec" + System.lineSeparator() + System.lineSeparator() + System.lineSeparator() + "**** Other Parameters ****" + System.lineSeparator() + System.lineSeparator() + "ecdr:georssFormat - specifies how to return the results that include geospatial data, can be as GML or as Simple GeoRSS" + System.lineSeparator() + " allowedValues: 'simple', 'gml'" + System.lineSeparator() + System.lineSeparator() + "ddf:metadata-content-type - comma separate list that maps to the Metacard.CONTENT_TYPE attribute" + System.lineSeparator() + System.lineSeparator() + "ecdr:textPath - comma separated list of text paths (XPath-like) values to be searched over" + System.lineSeparator() + " example: /ddms:Resource/subtitle (this would return all records that contain an element of subtitle under the ddms:Resource root element" + System.lineSeparator() + System.lineSeparator() + System.lineSeparator() + "**** Sort Order ****" + System.lineSeparator() + System.lineSeparator() + "sru:sortKeys - space-separated list of sort keys, with individual sort keys comprised of a comma-separated sequence of " + "sub-parameters in the order listed below." + System.lineSeparator() + " path - Mandatory. An XPath expression for a tagpath to be used in the sort (wildcards '*' may be supported, see allowed values)" + System.lineSeparator() + " sortSchema - Optional. A short name for a URI identifying an XML schema to which the XPath expression applies" + System.lineSeparator() + " ascending - Optional. Boolean, default 'true'." + System.lineSeparator() + " caseSensitive - Optional. Boolean, default 'false'." + System.lineSeparator() + " missingValue - Optional. Default is 'highValue'." + System.lineSeparator() + " examples: Sort by relevance - score,relevance" + System.lineSeparator() + " Sort by updated time descending - entry/date,,false " + System.lineSeparator() + " Sort by distance - distance,cdrsx" + System.lineSeparator() + " 'path' allowedValues: " + SearchUtils.getAllowedSortValues( sortTypeConfigurationList ) + System.lineSeparator(); // @formatter:on boolean fuzzy = queryConfig.isDefaultFuzzySearch(); description = StringUtils.replace( description, "${defaultFuzzy}", String.valueOf( fuzzy ), 1 ); description = StringUtils.replace( description, "${defaultFuzzyCustom}", fuzzy ? SearchConstants.TRUE_STRING : SearchConstants.FALSE_STRING, 1 ); description = StringUtils.replace( description, "${defaultRadius}", String.valueOf( queryConfig.getDefaultRadius() ), 1 ); description = StringUtils.replace( description, "${defaultDateType}", queryConfig.getDefaultDateType(), 1 ); description = StringUtils.replace( description, "${dateTypeValues}", dateTypeMap.keySet().toString(), 1 ); return description; } @Override public boolean isValidQuery( MultivaluedMap<String, String> queryParameters, boolean strict ) { boolean isValid = true; //if ( strict ) { // Todo fill this out // queryParameters.get //} return isValid; } @Override public QueryCriteria getQueryCriteria( MultivaluedMap<String, String> queryParameters, QueryConfiguration queryConfig ) throws UnsupportedQueryException { try { LOGGER.debug( "Parsing query using the CDRKeywordQueryLanguage parser" ); List<Filter> filters = new ArrayList<Filter>(); SortBy sortBy = SearchUtils.getSortBy( queryParameters.getFirst( SearchConstants.SORTKEYS_PARAMETER ), sortTypeConfigurationList, true ); StringBuilder humanReadableQuery = new StringBuilder(); boolean defaultFuzzySearch = queryConfig.isDefaultFuzzySearch(); double defaultRadius = queryConfig.getDefaultRadius(); String defaultDateType = queryConfig.getDefaultDateType(); // keyword parameters TextualCriteria textualCriteria = getTextualCriteria( queryParameters, defaultFuzzySearch ); if ( textualCriteria != null ) { boolean fuzzy = textualCriteria.isFuzzy(); LOGGER.debug( "Attempting to create a Contextual filter with params keywords=[{}], isCaseSensitive=[{}], fuzzy=[{}]", textualCriteria.getKeywords(), textualCriteria.isCaseSensitive(), fuzzy ); Filter filter = getContextualFilter( textualCriteria.getKeywords(), textualCriteria.isCaseSensitive(), fuzzy, humanReadableQuery ); SearchUtils.addFilter( filters, filter ); if ( sortBy == null ) { sortBy = new SortByImpl( Result.RELEVANCE, SortOrder.DESCENDING ); } } // Geospatial query parameters GeospatialCriteria geoCriteria = createGeospatialCriteria( queryParameters.getFirst( SearchConstants.RADIUS_PARAMETER ), queryParameters.getFirst( SearchConstants.LATITUDE_PARAMETER ), queryParameters.getFirst( SearchConstants.LONGITUDE_PARAMETER ), queryParameters.getFirst( SearchConstants.BOX_PARAMETER ), queryParameters.getFirst( SearchConstants.GEOMETRY_PARAMETER ), queryParameters.getFirst( SearchConstants.POLYGON_PARAMETER ), queryParameters.getFirst( SearchConstants.GEO_RELATION_PARAMETER ), defaultRadius ); if ( geoCriteria != null ) { LOGGER.debug( "Attempting to create a Geospatial filter with params radius=[{}], latitude=[{}], longitude=[{}], geometry=[{}]", geoCriteria.getRadius(), geoCriteria.getLatitude(), geoCriteria.getLongitude(), geoCriteria.getGeometryWKT() ); Filter filter = getGeoFilter( geoCriteria.getRadius(), geoCriteria.getLatitude(), geoCriteria.getLongitude(), geoCriteria.isBBox(), geoCriteria.getGeometryWKT(), geoCriteria.getSpatialOperator(), humanReadableQuery ); SearchUtils.addFilter( filters, filter ); if ( sortBy == null ) { sortBy = new SortByImpl( Result.DISTANCE, SortOrder.ASCENDING ); } } // Temporal Criteria TemporalCriteria temporalCriteria = createTemporalCriteria( queryParameters.getFirst( SearchConstants.STARTDATE_PARAMETER ), queryParameters.getFirst( SearchConstants.ENDDATE_PARAMETER ), queryParameters.getFirst( SearchConstants.DATETYPE_PARAMETER ), humanReadableQuery, defaultDateType ); if ( temporalCriteria != null ) { LOGGER.debug( "Attempting to create a Temporal filter with params startDate=[{}], endDate=[{}], dateType=[{}]", temporalCriteria.getStartDate(), temporalCriteria.getEndDate(), temporalCriteria.getDateType() ); Filter filter = getTemporalFilter( temporalCriteria.getStartDate(), temporalCriteria.getEndDate(), temporalCriteria.getDateType(), humanReadableQuery ); SearchUtils.addFilter( filters, filter ); } // Property Criteria List<PropertyCriteria> propertyCriteriaList = getPropertyCriteria( queryParameters, queryConfig.getParameterExtensionMap() ); if ( propertyCriteriaList != null && !propertyCriteriaList.isEmpty() ) { for ( PropertyCriteria propCriteria : propertyCriteriaList ) { LOGGER.debug( "Attempting to create a Property filter with params property=[{}], value=[{}], operator=[{}]", propCriteria.getProperty(), propCriteria.getValue(), propCriteria.getOperator() ); Filter filter = getPropertyFilter( propCriteria.getProperty(), propCriteria.getValue(), propCriteria.getOperator(), humanReadableQuery ); SearchUtils.addFilter( filters, filter ); } } if ( filters.isEmpty() ) { throw new UnsupportedQueryException( "There was no valid search criteria presented from the user, cannot complete search" ); } SearchUtils.logSort( sortBy ); return new CDRQueryCriteriaImpl( filterBuilder.allOf( filters ), sortBy, humanReadableQuery.toString(), true, queryParameters, new HashMap<String, String>(), queryConfig ); } catch ( Exception e ) { LOGGER.warn( e.getMessage(), e ); if ( e instanceof UnsupportedQueryException ) { throw (UnsupportedQueryException) e; } throw new UnsupportedQueryException( "Could not create query criteria from provided query parmaeters", e ); } } protected TextualCriteria getTextualCriteria( MultivaluedMap<String, String> queryParameters, boolean defaultFuzzySearch ) throws UnsupportedQueryException { String words = queryParameters.getFirst( SearchConstants.KEYWORD_PARAMETER ); TextualCriteria textualCriteria = null; if ( StringUtils.isNotBlank( words ) ) { String stringFuzzy = queryParameters.getFirst( SearchConstants.FUZZY_PARAMETER ); LOGGER.debug( "Attempting to set 'fuzzy' value from request [{}]", stringFuzzy ); Boolean fuzzy = SearchUtils.getBoolean( stringFuzzy, null ); if ( fuzzy == null ) { LOGGER.debug( "The 'fuzzy' parameter was not specified, defaulting value to [{}]", defaultFuzzySearch ); fuzzy = defaultFuzzySearch; } String caseSensitiveString = queryParameters.getFirst( SearchConstants.CASESENSITIVE_PARAMETER ); LOGGER.debug( "Attempting to set '{}' value from request [{}], will default to false if not boolean", SearchConstants.CASESENSITIVE_PARAMETER, caseSensitiveString ); textualCriteria = new TextualCriteria( words, SearchUtils.getBoolean( caseSensitiveString, Boolean.FALSE ), fuzzy ); } return textualCriteria; } protected Filter getContextualFilter( String keywords, boolean caseSensitive, boolean fuzzy, StringBuilder humanReadableQuery ) throws UnsupportedQueryException { Filter filter = null; if ( keywords != null ) { KeywordTextParser keywordParser = Parboiled.createParser( KeywordTextParser.class ); ParseRunner<ASTNode> runner = new RecoveringParseRunner<ASTNode>( keywordParser.inputPhrase() ); ParsingResult<ASTNode> parsingResult = runner.run( keywords ); if ( !parsingResult.hasErrors() ) { try { filter = getFilterFromASTNode( parsingResult.resultValue, caseSensitive, fuzzy ); } catch ( IllegalStateException e ) { throw new UnsupportedQueryException( "searchTerms parameter [" + keywords + "] was invalid and resulted in the error: " + e.getMessage() ); } } else { throw new UnsupportedQueryException( "searchTerms parameter [" + keywords + "] was invalid and resulted in the error: " + parsingResult.parseErrors.get( 0 ).getErrorMessage() ); } humanReadableQuery.append( " " + SearchConstants.KEYWORD_PARAMETER + "=[" + keywords + "] " + SearchConstants.CASESENSITIVE_PARAMETER + "=[" + caseSensitive + "] " + SearchConstants.FUZZY_PARAMETER + "=[" + fuzzy + "]" ); } return filter; } protected Filter getFilterFromASTNode( ASTNode astNode, boolean caseSensitive, boolean fuzzy ) { if ( astNode.isKeyword() ) { String keyword = astNode.getKeyword(); // this means it is an Text Path if ( keyword.startsWith( "{" ) && keyword.contains( "}:" ) ) { int endXpath = keyword.lastIndexOf( "}:" ); String xpath = keyword.substring( 1, endXpath ); String literal = keyword.substring( endXpath + 2 ); if ( literal.trim().isEmpty() ) { return filterBuilder.xpath( xpath ).exists(); } else { if ( fuzzy ) { return filterBuilder.xpath( xpath ).like().fuzzyText( literal ); } else if ( caseSensitive ) { return filterBuilder.xpath( xpath ).like().caseSensitiveText( literal ); } else { return filterBuilder.xpath( xpath ).like().text( literal ); } } } else { if ( fuzzy ) { return filterBuilder.attribute( Metacard.ANY_TEXT ).like().fuzzyText( astNode.getKeyword() ); } else if ( caseSensitive ) { return filterBuilder.attribute( Metacard.ANY_TEXT ).like().caseSensitiveText( astNode.getKeyword() ); } else { return filterBuilder.attribute( Metacard.ANY_TEXT ).like().text( astNode.getKeyword() ); } } } else if ( astNode.isOperator() ) { switch ( astNode.getOperator() ) { case AND: return filterBuilder.allOf( getFilterFromASTNode( astNode.left(), caseSensitive, fuzzy ), getFilterFromASTNode( astNode.right(), caseSensitive, fuzzy ) ); case OR: return filterBuilder.anyOf( getFilterFromASTNode( astNode.left(), caseSensitive, fuzzy ), getFilterFromASTNode( astNode.right(), caseSensitive, fuzzy ) ); case NOT: // since NOT really means AND NOT return filterBuilder.allOf( getFilterFromASTNode( astNode.left(), caseSensitive, fuzzy ), filterBuilder.not( getFilterFromASTNode( astNode.right(), caseSensitive, fuzzy ) ) ); default: throw new IllegalStateException( "Unable to generate Filter from invalid OperatorASTNode." ); } } throw new IllegalStateException( "Unable to generate Filter from ASTNode. Found invalid ASTNode in the tree" ); } protected GeospatialCriteria createGeospatialCriteria( String rad, String lat, String lon, String box, String geom, String polygon, String geoRelation, double defaultRadius ) throws UnsupportedQueryException { GeospatialCriteria geoCriteria = null; if ( StringUtils.isNotBlank( box ) ) { try { String[] bboxArray = box.split( " |,\\p{Space}?" ); if ( bboxArray.length != 3 ) { double minX = NumberUtils.createDouble( bboxArray[0] ); double minY = NumberUtils.createDouble( bboxArray[1] ); double maxX = NumberUtils.createDouble( bboxArray[2] ); double maxY = NumberUtils.createDouble( bboxArray[3] ); geoCriteria = new GeospatialCriteria( minX, minY, maxX, maxY ); } else { throw new UnsupportedQueryException( "Invalid values found for bbox [" + box + "]" ); } } catch ( NumberFormatException e ) { LOGGER.warn( "Invalid values found for bbox [{}]. Resulted in exception: {}", box, e.getMessage() ); throw new UnsupportedQueryException( "Invalid values found for bbox [" + box + "], values must be numeric." ); } // Only check lat and lon. If Radius is blank is should be defaulted } else if ( StringUtils.isNotBlank( lat ) && StringUtils.isNotBlank( lon ) ) { try { double longitude = NumberUtils.createDouble( lon ); double latitude = NumberUtils.createDouble( lat ); double radius = StringUtils.isNotBlank( rad ) ? NumberUtils.createDouble( rad ) : defaultRadius; geoCriteria = new GeospatialCriteria( latitude, longitude, radius ); } catch ( NumberFormatException e ) { LOGGER.warn( "Invalid Number found for lat [{}], lon [{}], and/or radius [{}]. Resulted in exception: {}", lat, lon, rad, e.getMessage() ); throw new UnsupportedQueryException( "Invalid Number found for lat [" + lat + "], lon [" + lon + "], and/or radius [" + rad + "]." ); } } else if ( StringUtils.isNotBlank( geom ) ) { try { WKTReader reader = new WKTReader(); reader.read( geom ); } catch ( ParseException e ) { LOGGER.warn( "The following is not a valid WKT String: {}", geom ); throw new UnsupportedQueryException( "Invalid WKT, cannot create geospatial query." ); } geoCriteria = new GeospatialCriteria( geom ); } else if ( StringUtils.isNotBlank( polygon ) ) { String wkt = GeospatialUtils.polygonToWKT( polygon ); try { WKTReader reader = new WKTReader(); reader.read( wkt ); } catch ( ParseException e ) { LOGGER.warn( "The following is not a valid WKT String: {}", wkt ); throw new UnsupportedQueryException( "Invalid WKT, cannot create geospatial query." ); } geoCriteria = new GeospatialCriteria( wkt ); } if ( geoCriteria != null && geoRelation != null ) { SpatialOperator spatialOp = SearchUtils.enumEqualsIgnoreCase( SpatialOperator.class, geoRelation ); if ( spatialOp != null ) { geoCriteria.setSpatialOperator( spatialOp ); } } return geoCriteria; } protected Filter getGeoFilter( Double radius, Double latitude, Double longitude, boolean isBbox, String geometry, SpatialOperator operator, StringBuilder humanReadableQuery ) throws UnsupportedQueryException { Filter filter = null; if ( latitude != null && longitude != null && radius != null ) { String wkt = WKTWriter.toPoint( new Coordinate( longitude, latitude ) ); filter = filterBuilder.attribute( Metacard.ANY_GEO ).withinBuffer().wkt( wkt, radius ); humanReadableQuery.append( " " + SearchConstants.LATITUDE_PARAMETER + "=[" + latitude + "] " + SearchConstants.LONGITUDE_PARAMETER + "=[" + longitude + "] " + SearchConstants.RADIUS_PARAMETER + "=[" + radius + "]" ); } else { filter = getGeoFilter( operator, geometry, isBbox ? SearchConstants.BOX_PARAMETER : SearchConstants.GEOMETRY_PARAMETER, humanReadableQuery ); } return filter; } protected Filter getGeoFilter( SpatialOperator operator, String wkt, String geoParameter, StringBuilder humanReadableQuery ) throws UnsupportedQueryException { Filter filter = null; if ( wkt != null ) { if ( operator != null ) { switch ( operator ) { case Contains: filter = filterBuilder.attribute( Metacard.ANY_GEO ).within().wkt( wkt ); break; case Disjoint: throw new UnsupportedQueryException( "Geospatial disjoint query is not currently supported" ); case Within: filter = filterBuilder.attribute( Metacard.ANY_GEO ).containing().wkt( wkt ); break; case Overlaps: default: filter = filterBuilder.attribute( Metacard.ANY_GEO ).intersecting().wkt( wkt ); break; } humanReadableQuery.append( " " + geoParameter + "=[" + wkt + "] " + SearchConstants.GEO_RELATION_PARAMETER + "=[" + operator.toString().toLowerCase() + "]" ); } else { filter = filterBuilder.attribute( Metacard.ANY_GEO ).intersecting().wkt( wkt ); humanReadableQuery.append( " " + geoParameter + "=[" + wkt + "]" ); } } return filter; } protected TemporalCriteria createTemporalCriteria( String start, String end, String type, StringBuilder humanReadableQuery, String defaultDateType ) throws UnsupportedQueryException { TemporalCriteria temporalCriteria = null; if ( StringUtils.isNotBlank( start ) || StringUtils.isNotBlank( end ) ) { Date startDate = SearchUtils.parseDate( start ); Date endDate = SearchUtils.parseDate( end ); if ( startDate != null && endDate != null ) { if ( startDate.after( endDate ) ) { throw new UnsupportedQueryException( "Start date value [" + startDate + "] cannot be after endDate [" + endDate + "]" ); } } String dateType = null; LOGGER.debug( "Getting date type name for type [{}]", type ); if ( StringUtils.isNotBlank( type ) ) { if ( dateTypeMap.containsKey( type ) ) { dateType = dateTypeMap.getMappedValue( type ); LOGGER.debug( "Date type value received in map for request value [{}], setting internal query value to [{}]", type, dateType ); } else { String message = "Date type value not found in map for type [" + type + "], defaulting internal query value to [" + dateType + "]"; LOGGER.warn( message ); throw new UnsupportedQueryException( message ); } } else { dateType = dateTypeMap.getMappedValue( defaultDateType ); LOGGER.debug( "Date type value was not specified in request, defaulting internal query value to [{}]", dateType ); } temporalCriteria = new TemporalCriteria( startDate, endDate, dateType ); } return temporalCriteria; } protected Filter getTemporalFilter( Date startDate, Date endDate, String type, StringBuilder humanReadableQueryBuilder ) throws UnsupportedQueryException { Filter filter = null; if ( startDate != null || endDate != null ) { if ( startDate != null && endDate != null ) { if ( startDate.after( endDate ) ) { throw new UnsupportedQueryException( "Start date value [" + startDate + "] cannot be after endDate [" + endDate + "]" ); } filter = filterBuilder.attribute( type ).during().dates( startDate, endDate ); humanReadableQueryBuilder.append( " " + SearchConstants.STARTDATE_PARAMETER + "=[" + startDate + "] " + SearchConstants.ENDDATE_PARAMETER + "=[" + endDate + "] " + SearchConstants.DATETYPE_PARAMETER + "=[" + type + "]" ); } else if ( startDate != null ) { filter = filterBuilder.attribute( type ).after().date( startDate ); humanReadableQueryBuilder .append( " " + SearchConstants.STARTDATE_PARAMETER + "=[" + startDate + "] " + SearchConstants.DATETYPE_PARAMETER + "=[" + type + "]" ); } else if ( endDate != null ) { filter = filterBuilder.attribute( type ).before().date( endDate ); humanReadableQueryBuilder .append( " " + SearchConstants.ENDDATE_PARAMETER + "=[" + endDate + "] " + SearchConstants.DATETYPE_PARAMETER + "=[" + type + "]" ); } } return filter; } protected List<PropertyCriteria> getPropertyCriteria( MultivaluedMap<String, String> queryParameters, Map<String, String> parameterExtensionMap ) { List<PropertyCriteria> criteriaList = new ArrayList<PropertyCriteria>(); for ( Entry<String, List<String>> entry : queryParameters.entrySet() ) { String key = entry.getKey(); List<String> valueList = entry.getValue(); if ( CollectionUtils.isNotEmpty( valueList ) ) { String value = valueList.get( 0 ); if ( StringUtils.isNotBlank( value ) && parameterExtensionMap.containsKey( key ) ) { criteriaList.add( new PropertyCriteria( parameterExtensionMap.get( key ), value, Operator.LIKE ) ); } } } if ( queryParameters.containsKey( SearchConstants.CONTENT_COLLECTIONS_PARAMETER ) ) { String contentCollections = queryParameters.getFirst( SearchConstants.CONTENT_COLLECTIONS_PARAMETER ); if ( StringUtils.isNotEmpty( contentCollections ) ) { criteriaList.add( new PropertyCriteria( CDRMetacard.METACARD_CONTENT_COLLECTION_ATTRIBUTE, contentCollections, Operator.LIKE ) ); } } if ( queryParameters.containsKey( SearchConstants.RESOURCE_URI_PARAMETER ) ) { String uriString = queryParameters.getFirst( SearchConstants.RESOURCE_URI_PARAMETER ); if ( StringUtils.isNotEmpty( uriString ) ) { if ( uriString.startsWith( SearchConstants.DAD_SCHEME ) ) { try { String uriSubstring = uriString; StringBuilder sb = new StringBuilder( SearchConstants.DAD_SCHEME ); uriSubstring = uriSubstring.substring( SearchConstants.DAD_SCHEME.length() ); int index = uriSubstring.indexOf( '?' ); sb.append( URLEncoder.encode( uriSubstring.substring( 0, index ), "UTF-8" ) ); sb.append( "?" ); uriSubstring = uriSubstring.substring( index + 1 ); index = uriSubstring.indexOf( '#' ); sb.append( URLEncoder.encode( uriSubstring.substring( 0, index ), "UTF-8" ) ); sb.append( "#" ); uriSubstring = uriSubstring.substring( index + 1 ); sb.append( URLEncoder.encode( uriSubstring, "UTF-8" ) ); uriString = sb.toString(); } catch ( UnsupportedEncodingException | RuntimeException e ) { LOGGER.warn( "Could parse the 'resource-uri' due to exception so falling back to not parsing: " + e.getMessage() ); } } criteriaList.add( new PropertyCriteria( Metacard.RESOURCE_URI, uriString, Operator.EQUALS ) ); } } if ( queryParameters.containsKey( SearchConstants.CONTENT_TYPE_PARAMETER ) ) { String contentTypesString = queryParameters.getFirst( SearchConstants.CONTENT_TYPE_PARAMETER ); if ( StringUtils.isNotEmpty( contentTypesString ) ) { criteriaList.add( new PropertyCriteria( Metacard.CONTENT_TYPE, contentTypesString, Operator.EQUALS ) ); } } return criteriaList; } protected Filter getPropertyFilter( String property, String value, Operator operator, StringBuilder humanReadableQueryBuilder ) { Filter filter = null; if ( property != null && operator != null ) { if ( property.equals( Metacard.CONTENT_TYPE ) ) { filter = getContentTypeFilter( value ); humanReadableQueryBuilder.append( " " + property + "=like[" + value + "] " ); } else if ( property.equals( CDRMetacard.METACARD_CONTENT_COLLECTION_ATTRIBUTE ) ) { filter = getContentCollectionsFilter( property, value ); humanReadableQueryBuilder.append( " " + property + "=like[" + value + "] " ); } else { if ( Operator.EQUALS.equals( operator ) ) { filter = filterBuilder.attribute( property ).equalTo().text( value ); humanReadableQueryBuilder.append( " " + property + "=[" + value + "] " ); } else if ( Operator.LIKE.equals( operator ) ) { filter = filterBuilder.attribute( property ).like().text( value ); humanReadableQueryBuilder.append( " " + property + "=like[" + value + "] " ); } } } return filter; } protected Filter getContentCollectionsFilter( String property, String value ) { List<Filter> filterList = new ArrayList<Filter>(); String[] collections = value.split( "," ); for ( String collection : collections ) { filterList.add( filterBuilder.attribute( property ).like().text( collection ) ); } return filterBuilder.anyOf( filterList ); } protected Filter getContentTypeFilter( String value ) { List<Filter> filterList = new ArrayList<Filter>(); String[] contentTypes = value.split( "," ); for ( String contentType : contentTypes ) { String[] typeAndVersion = contentType.split( ":" ); String type = typeAndVersion[0]; if ( typeAndVersion.length == 1 ) { filterList.add( filterBuilder.attribute( Metacard.CONTENT_TYPE ).like().text( type ) ); } else { List<Filter> typeVersionPairs = new ArrayList<Filter>(); String[] versions = typeAndVersion[1].split( "\\|" ); for ( String version : versions ) { Filter typeFilter = filterBuilder.attribute( Metacard.CONTENT_TYPE ).like().text( type ); Filter versionFilter = filterBuilder.attribute( Metacard.CONTENT_TYPE_VERSION ).like().text( version ); typeVersionPairs.add( filterBuilder.allOf( typeFilter, versionFilter ) ); } // Check if we had any type/version pairs and 'OR' them together. if ( !typeVersionPairs.isEmpty() ) { filterList.add( filterBuilder.anyOf( typeVersionPairs ) ); } } } return filterBuilder.anyOf( filterList ); } }