/** * 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.query; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.security.auth.Subject; import org.codice.ddf.endpoints.ASTNode; import org.codice.ddf.endpoints.KeywordFilterGenerator; import org.codice.ddf.endpoints.KeywordTextParser; import org.codice.ddf.opensearch.query.filter.BBoxSpatialFilter; import org.codice.ddf.opensearch.query.filter.PolygonSpatialFilter; import org.geotools.filter.FilterFactoryImpl; import org.geotools.styling.UomOgcMapping; import org.geotools.temporal.object.DefaultInstant; import org.geotools.temporal.object.DefaultPeriod; import org.geotools.temporal.object.DefaultPosition; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.filter.FilterVisitor; import org.opengis.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; import org.opengis.geometry.Geometry; import org.opengis.temporal.Instant; import org.opengis.temporal.Period; import org.parboiled.Parboiled; import org.parboiled.buffers.InputBuffer; import org.parboiled.errors.InvalidInputError; import org.parboiled.errors.ParseError; import org.parboiled.errors.ParsingException; import org.parboiled.parserunners.RecoveringParseRunner; import org.parboiled.support.ParsingResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.data.Metacard; import ddf.catalog.data.Result; import ddf.catalog.federation.FederationStrategy; import ddf.catalog.filter.FilterBuilder; import ddf.catalog.impl.filter.SpatialDistanceFilter; import ddf.catalog.impl.filter.SpatialFilter; import ddf.catalog.impl.filter.TemporalFilter; import ddf.catalog.operation.Query; public class OpenSearchQuery implements Query { public static final String CARET = "^"; // TODO remove this and only use filterbuilder public static final FilterFactory FILTER_FACTORY = new FilterFactoryImpl(); private static final Logger LOGGER = LoggerFactory.getLogger(OpenSearchQuery.class); private final FilterBuilder filterBuilder; private Subject user; private Integer startIndex; private Integer count; private long maxTimeout; private boolean isEnterprise; private Set<String> siteIds; private SortBy sortBy; private List<Filter> filters; /** * Creates an Implementation of a DDF Query interface. This object is passed from the endpoint * to DDF and will be used by sites to perform queries on their respective systems. * * @param user * Credentials of the user performing the query * @param startIndex * Offset of the returned results. * @param count * Number of results to return. * @param sortField * Area that the results should be sorted by. Possible values: 'date' and 'relevance' * @param sortOrderIn * Order of the results. Possible values: 'asc', 'desc' * @param maxTimeout * Maximum amount of time for a query to respond. * @param filterBuilder * FilterBuilder object to use for filter creation. */ public OpenSearchQuery(Subject user, Integer startIndex, Integer count, String sortField, String sortOrderIn, long maxTimeout, FilterBuilder filterBuilder) { String methodName = "OpenSearchQuery constructor"; this.user = user; this.startIndex = startIndex; this.count = count; this.filterBuilder = filterBuilder; SortOrder sortOrder; // Query must specify a valid sort order if a sort field was specified, i.e., query // cannot specify just "date:", must specify "date:asc" if ("asc".equalsIgnoreCase(sortOrderIn)) { sortOrder = SortOrder.ASCENDING; } else if ("desc".equalsIgnoreCase(sortOrderIn)) { sortOrder = SortOrder.DESCENDING; } else { throw new IllegalArgumentException( "Incorrect sort order received, must be 'asc' or 'desc'"); } if (sortField.equalsIgnoreCase("relevance")) { // this.sortPolicy = new SortPolicyImpl( true, Constants.DDF_SORT_QUALIFIER, // Constants.SORT_POLICY_VALUE_FULLTEXT, this.sortOrder ); this.sortBy = FILTER_FACTORY.sort(sortField.toUpperCase(), sortOrder); } else if (sortField.equalsIgnoreCase("date")) { // this.sortPolicy = new SortPolicyImpl( true, Constants.DDF_SORT_QUALIFIER, // Constants.SORT_POLICY_VALUE_TEMPORAL, this.sortOrder ); this.sortBy = FILTER_FACTORY.sort(Result.TEMPORAL, sortOrder); } else { throw new IllegalArgumentException( "Incorrect sort field received, must be 'relevance' or 'date'"); } this.maxTimeout = maxTimeout; this.filters = new ArrayList<Filter>(); this.siteIds = new HashSet<String>(); } public void addContextualFilter(String searchTerm, String selectors) throws ParsingException { String methodName = "addContextualFilter"; Filter filter = null; KeywordFilterGenerator keywordFilterGenerator = new KeywordFilterGenerator(filterBuilder); KeywordTextParser parser = Parboiled.createParser(KeywordTextParser.class); // translate the search terms into an abstract syntax tree ParsingResult<ASTNode> result = new RecoveringParseRunner(parser.inputPhrase()).run( searchTerm); // make sure it's a good result before using it if (result.matched && !result.hasErrors()) { filter = generateContextualFilter(selectors, keywordFilterGenerator, result); } else if (result.hasErrors()) { throw new ParsingException( "Unable to parse keyword search phrase. " + generateParsingError(result)); } if (filter != null) { filters.add(filter); } } private String generateParsingError(ParsingResult<ASTNode> result) { StringBuilder parsingErrorBuilder = new StringBuilder( "Parsing error" + ((result.parseErrors.size() > 1) ? "s" : "") + ": \n"); InputBuffer inputBuffer = result.inputBuffer; String parsedLine = inputBuffer.extract(0, Integer.MAX_VALUE); StringBuilder invalidInputLineBuilder = null; for (ParseError parseError : result.parseErrors) { StringBuilder otherErrorLineBuilder = getCaratLineStringBuilder(parsedLine); // NOTE for some reason, these indexes start at 1, not 0 int originalEndIndex = inputBuffer.getOriginalIndex(parseError.getEndIndex()) - 1; int originalStartIndex = inputBuffer.getOriginalIndex(parseError.getStartIndex()) - 1; if (parseError.getClass() .isAssignableFrom(InvalidInputError.class)) { // Combine all InvalidInputError's if (invalidInputLineBuilder == null) { invalidInputLineBuilder = getCaratLineStringBuilder(parsedLine); } addCaretsToStringBuilder(invalidInputLineBuilder, originalEndIndex, originalStartIndex); } else { // output other types of errors separately parsingErrorBuilder.append("\nError found in: \n"); addCaretsToStringBuilder(otherErrorLineBuilder, originalEndIndex, originalStartIndex); parsingErrorBuilder.append("\n\t"); parsingErrorBuilder.append(parsedLine); parsingErrorBuilder.append("\n\t"); parsingErrorBuilder.append(otherErrorLineBuilder); } } if (invalidInputLineBuilder != null) { // if the first and last occurrence of CARET aren't the same, there are more than one in // the string parsingErrorBuilder.append("\nInvalid character" + ((invalidInputLineBuilder.indexOf( CARET) != invalidInputLineBuilder.lastIndexOf(CARET)) ? "s" : "") + " found in: \n"); parsingErrorBuilder.append("\n\t"); parsingErrorBuilder.append(parsedLine); parsingErrorBuilder.append("\n\t"); parsingErrorBuilder.append(invalidInputLineBuilder); } return parsingErrorBuilder.toString(); } private Filter generateContextualFilter(String selectors, KeywordFilterGenerator keywordFilterGenerator, ParsingResult<ASTNode> result) throws ParsingException { Filter filter = null; try { if (selectors != null) { // generate a filter for each selector for (String selector : selectors.split(",")) { if (filter == null) { filter = keywordFilterGenerator.getFilterFromASTNode(result.resultValue, selector); } else { filter = filterBuilder.anyOf(filter, keywordFilterGenerator.getFilterFromASTNode(result.resultValue, selector)); } } } else { filter = keywordFilterGenerator.getFilterFromASTNode(result.resultValue); } } catch (IllegalStateException e) { throw new ParsingException("Unable to parse keyword search phrase. ", e); } return filter; } private void addCaretsToStringBuilder(StringBuilder stringBuilder, int endIndex, int startIndex) { for (int insertCaretIndex = startIndex + 1; insertCaretIndex <= endIndex; insertCaretIndex++) { stringBuilder.replace(insertCaretIndex, insertCaretIndex + 1, CARET); } } private StringBuilder getCaratLineStringBuilder(String parsedLine) { StringBuilder caratLineBuilder = new StringBuilder(); for (int index = 0; index < parsedLine.length(); index++) { caratLineBuilder.append(" "); } return caratLineBuilder; } public void addTemporalFilter(String dateStart, String dateEnd, String dateOffset) { String methodName = "addTemporalFilter"; TemporalFilter temporalFilter = null; // If either start date OR end date is specified and non-empty, then // a temporal filter can be created if ((dateStart != null && !dateStart.trim() .isEmpty()) || (dateEnd != null && !dateEnd.trim() .isEmpty())) { temporalFilter = new TemporalFilter(dateStart, dateEnd); } else if (dateOffset != null && !dateOffset.trim() .isEmpty()) { temporalFilter = new TemporalFilter(Long.parseLong(dateOffset)); } addTemporalFilter(temporalFilter); } public void addTemporalFilter(TemporalFilter temporalFilter) { String methodName = "addTemporalFilter"; if (temporalFilter != null) { // t1.start < timeType instance < t1.end Instant startInstant = new DefaultInstant(new DefaultPosition(temporalFilter.getStartDate())); Instant endInstant = new DefaultInstant(new DefaultPosition(temporalFilter.getEndDate())); Period period = new DefaultPeriod(startInstant, endInstant); Filter filter = FILTER_FACTORY.during(FILTER_FACTORY.property(Metacard.MODIFIED), FILTER_FACTORY.literal(period)); LOGGER.debug("Adding temporal filter"); filters.add(filter); } } public void addGeometrySpatialFilter(String geometryWkt) { SpatialFilter spatialFilter = new SpatialFilter(geometryWkt); addSpatialFilter(spatialFilter); } public void addBBoxSpatialFilter(String bbox) { BBoxSpatialFilter bboxFilter = new BBoxSpatialFilter(bbox); addSpatialFilter(bboxFilter); } public void addPolygonSpatialFilter(String polygon) { PolygonSpatialFilter polygonFilter = new PolygonSpatialFilter(polygon); addSpatialFilter(polygonFilter); } public void addSpatialDistanceFilter(String lon, String lat, String radius) { SpatialDistanceFilter distanceFilter = new SpatialDistanceFilter(lon, lat, radius); Geometry geometry = distanceFilter.getGeometry(); if (geometry != null) { Filter filter = FILTER_FACTORY.dwithin(Metacard.ANY_GEO, geometry, Double.parseDouble(radius), UomOgcMapping.METRE.name()); LOGGER.debug("Adding spatial filter"); filters.add(filter); } } private void addSpatialFilter(SpatialFilter spatialFilter) { Geometry geometry = spatialFilter.getGeometry(); if (geometry != null) { Filter filter = FILTER_FACTORY.intersects(Metacard.ANY_GEO, geometry); LOGGER.debug("Adding spatial filter"); filters.add(filter); } } public void addTypeFilter(String type, String versions) { Filter filter; Filter typeFilter = null; if (type.contains("*")) { typeFilter = FILTER_FACTORY.like(FILTER_FACTORY.property(Metacard.CONTENT_TYPE), type); } else { typeFilter = FILTER_FACTORY.equals(FILTER_FACTORY.property(Metacard.CONTENT_TYPE), FILTER_FACTORY.literal(type)); } if (versions != null && !versions.isEmpty()) { LOGGER.debug("Received versions from client."); String[] typeVersions = versions.split(","); List<Filter> typeVersionPairsFilters = new ArrayList<Filter>(); for (String version : typeVersions) { Filter versionFilter = null; if (version.contains("*")) { versionFilter = FILTER_FACTORY.like(FILTER_FACTORY.property(Metacard.CONTENT_TYPE_VERSION), version); } else { versionFilter = FILTER_FACTORY.equals(FILTER_FACTORY.property(Metacard.CONTENT_TYPE_VERSION), FILTER_FACTORY.literal(version)); } typeVersionPairsFilters.add(FILTER_FACTORY.and(typeFilter, versionFilter)); } if (!typeVersionPairsFilters.isEmpty()) { filter = FILTER_FACTORY.or(typeVersionPairsFilters); } else { filter = typeFilter; } } else { filter = typeFilter; } if (filter != null) { LOGGER.debug("Adding type filter"); filters.add(filter); } } @Override public Object accept(FilterVisitor visitor, Object obj) { Filter filter = getFilter(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("filter being visited: {}", filter); } if (filter != null) { return filter.accept(visitor, obj); } return null; } @Override public boolean evaluate(Object object) { Filter filter = getFilter(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("filter being evaluated: {}", filter); } if (filter != null) { return filter.evaluate(object); } return false; } @Override public int getStartIndex() { return startIndex; } @Override public int getPageSize() { return count; } @Override public boolean requestsTotalResultsCount() { // always send back total count return true; } @Override public long getTimeoutMillis() { return maxTimeout; } public Set<String> getSiteIds() { return this.siteIds; } public void setSiteIds(Set<String> siteIds) { this.siteIds = siteIds; } public boolean isEnterprise() { return this.isEnterprise; } public void setIsEnterprise(boolean isEnterprise) { this.isEnterprise = isEnterprise; } public FederationStrategy getStrategy() { return null; } @Override public SortBy getSortBy() { return sortBy; } public Filter getFilter() { if (filters.size() > 1) { // If multiple filters, then AND them all together return FILTER_FACTORY.and(filters); } else if (filters.size() == 1) { // If only one filter, then just return it // (AND'ing it would create an erroneous </ogc:and> closing tag) return filters.get(0); } else { // Otherwise, no filters return null; } } @Override public String toString() { Filter queryFilter = getFilter(); if (queryFilter == null) { return "OpenSearchQuery: FILTERS:{ NULL }"; } else { return "OpenSearchQuery: " + "FILTERS:{" + queryFilter.toString() + "}"; } } }