/** * 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.pubsub.internal; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.regex.Matcher; import org.geotools.filter.AttributeExpressionImpl; import org.geotools.filter.LikeFilterImpl; import org.geotools.filter.LiteralExpressionImpl; import org.geotools.filter.visitor.DefaultFilterVisitor; import org.geotools.geometry.jts.spatialschema.geometry.GeometryImpl; import org.geotools.temporal.object.DefaultPeriodDuration; import org.opengis.filter.And; import org.opengis.filter.Filter; import org.opengis.filter.IncludeFilter; import org.opengis.filter.Not; import org.opengis.filter.Or; import org.opengis.filter.PropertyIsEqualTo; import org.opengis.filter.PropertyIsLike; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; import org.opengis.filter.expression.PropertyName; import org.opengis.filter.spatial.DWithin; import org.opengis.filter.spatial.Intersects; import org.opengis.filter.spatial.Within; import org.opengis.filter.temporal.During; import org.opengis.temporal.Period; import org.opengis.temporal.PeriodDuration; import org.osgi.service.event.Event; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ddf.catalog.data.Metacard; import ddf.catalog.impl.filter.FuzzyFunction; import ddf.catalog.pubsub.EventProcessorImpl.DateType; import ddf.catalog.pubsub.criteria.geospatial.SpatialOperator; import ddf.catalog.pubsub.predicate.ContentTypePredicate; import ddf.catalog.pubsub.predicate.ContextualPredicate; import ddf.catalog.pubsub.predicate.EntryPredicate; import ddf.catalog.pubsub.predicate.GeospatialPredicate; import ddf.catalog.pubsub.predicate.Predicate; import ddf.catalog.pubsub.predicate.TemporalPredicate; public class SubscriptionFilterVisitor extends DefaultFilterVisitor { public static final double EQUATORIAL_RADIUS_IN_METERS = 6378137.0; public static final String LUCENE_ESCAPE_CHAR = "\\"; public static final String LUCENE_WILDCARD_CHAR = "*"; public static final String LUCENE_SINGLE_CHAR = "?"; private static final Logger LOGGER = LoggerFactory.getLogger(SubscriptionFilterVisitor.class); private static final String QUOTE = "\""; // private static final String FUZZY_FUNCTION_NAME = "fuzzy"; public SubscriptionFilterVisitor() { } /** * A helper method to combine multiple predicates by a logical AND */ public static Predicate and(final Predicate left, final Predicate right) { notNull(left, "left"); notNull(right, "right"); return new Predicate() { public boolean matches(Event properties) { return left.matches(properties) && right.matches(properties); } @Override public String toString() { return "(" + left + ") AND (" + right + ")"; } }; } /** * A helper method to combine multiple predicates by a logical OR */ public static Predicate or(final Predicate left, final Predicate right) { notNull(left, "left"); notNull(right, "right"); return new Predicate() { public boolean matches(Event properties) { return left.matches(properties) || right.matches(properties); } @Override public String toString() { return "(" + left + ") OR (" + right + ")"; } }; } /** * A helper method to combine multiple predicates by a logical NOT */ public static Predicate not(final Predicate predicate) { notNull(predicate, "predicate"); return new Predicate() { public boolean matches(Event properties) { return !predicate.matches(properties); } @Override public String toString() { return "(NOT (" + predicate + ")"; } }; } /** * Asserts whether the value is <b>not</b> <tt>null</tt> * * @param value * the value to test * @param name * the key that resolved the value * @throws IllegalArgumentException * is thrown if assertion fails */ public static void notNull(Object value, String name) { if (value == null) { throw new IllegalArgumentException(name + " must be specified"); } } @Override public Object visit(Not filter, Object data) { LOGGER.debug("ENTERING: NOT filter"); Predicate returnPredicate = null; Filter filterToNot = filter.getFilter(); Predicate predicateToNot = (Predicate) filterToNot.accept(this, null); returnPredicate = not(predicateToNot); LOGGER.debug("EXITING: NOT filter"); return returnPredicate; } @Override public Object visit(Or filter, Object data) { LOGGER.debug("ENTERING: OR filter"); Predicate returnPredicate = null; List<Predicate> predList = new ArrayList<Predicate>(); List<Filter> childList = filter.getChildren(); if (childList != null) { for (Filter child : childList) { if (child == null) { continue; } predList.add((Predicate) child.accept(this, data)); } } for (Predicate p : predList) { if (returnPredicate == null) { returnPredicate = p; } else { returnPredicate = or(returnPredicate, p); } } LOGGER.debug("EXITING: OR filter"); return returnPredicate; } @Override public Object visit(And filter, Object data) { LOGGER.debug("ENTERING: AND filter"); Predicate returnPredicate = null; List<Predicate> predList = new ArrayList<Predicate>(); List<Filter> childList = filter.getChildren(); if (childList != null) { for (Filter child : childList) { if (child == null) { continue; } predList.add((Predicate) child.accept(this, data)); } } ContentTypePredicate currentContentTypePred = null; LOGGER.debug("predicate list size: {}", predList.size()); for (Predicate p : predList) { if (p == null) { // filterless subscription LOGGER.debug("Found null predicate. Indicates Filterless Subscription."); } else if (p instanceof ContentTypePredicate) { LOGGER.debug("Found ContentType Predicate."); if (currentContentTypePred == null) { currentContentTypePred = (ContentTypePredicate) p; } else { ContentTypePredicate incomingContentTypePred = (ContentTypePredicate) p; String currentType = currentContentTypePred.getType(); String currentVersion = currentContentTypePred.getVersion(); String incomingType = incomingContentTypePred.getType(); String incomingVersion = incomingContentTypePred.getVersion(); // Case 1 // First ContentTypePredicate found has just a type and no version. Second // ContentTypePredicate has version and no type. // Combine the two. if (currentType != null && incomingType == null && incomingVersion != null) { currentContentTypePred.setVersion(incomingVersion); // Case 2 // First ContentTypePredicate has no type but has a version. Second // ContentTypePredicate has no version, but it has a type. } else if (currentType == null && currentVersion != null && incomingType != null) { currentContentTypePred.setType(incomingType); } } if (returnPredicate == null) { LOGGER.debug("first return predicate"); returnPredicate = currentContentTypePred; } else { LOGGER.debug("ANDing the predicates. Pred1: {} Pred2: {}", returnPredicate, currentContentTypePred); returnPredicate = and(returnPredicate, currentContentTypePred); } } else // if Spatial Predicate, Temporal Predicate, Contextual, or Entry Predicate { if (returnPredicate == null) { LOGGER.debug("first return predicate"); returnPredicate = p; } else { LOGGER.debug("ANDing the predicates. Pred1: {} Pred2: {}", returnPredicate, p); returnPredicate = and(returnPredicate, p); } } } LOGGER.debug("EXITING: AND filter"); return returnPredicate; } /** * DWithin filter maps to a Point/Radius distance Spatial search criteria. */ @Override public Object visit(DWithin filter, Object data) { LOGGER.debug("ENTERING: DWithin filter"); LOGGER.debug("Must have received point/radius query criteria."); double radius = filter.getDistance(); com.vividsolutions.jts.geom.Geometry jtsGeometry = getJtsGeometery( (LiteralExpressionImpl) filter.getExpression2()); double radiusInDegrees = (radius * 180.0) / (Math.PI * EQUATORIAL_RADIUS_IN_METERS); LOGGER.debug("radius in meters : {}", radius); LOGGER.debug("radius in degrees : {}", radiusInDegrees); Predicate predicate = new GeospatialPredicate(jtsGeometry, null, radiusInDegrees); LOGGER.debug("EXITING: DWithin filter"); return predicate; } /** * Within filter maps to a CONTAINS Spatial search criteria. */ @Override public Object visit(Within filter, Object data) { LOGGER.debug("ENTERING: Within filter"); LOGGER.debug("Must have received CONTAINS query criteria: {}", filter.getExpression2()); com.vividsolutions.jts.geom.Geometry jtsGeometry = getJtsGeometery( (LiteralExpressionImpl) filter.getExpression2()); Predicate predicate = new GeospatialPredicate(jtsGeometry, SpatialOperator.CONTAINS.name(), 0.0); LOGGER.debug("EXITING: Within filter"); return predicate; } /** * Intersects filter maps to a OVERLAPS Spatial search criteria. */ @Override public Object visit(Intersects filter, Object data) { LOGGER.debug("ENTERING: Intersects filter"); LOGGER.debug("Must have received OVERLAPS query criteria."); com.vividsolutions.jts.geom.Geometry jtsGeometry = getJtsGeometery( (LiteralExpressionImpl) filter.getExpression2()); Predicate predicate = new GeospatialPredicate(jtsGeometry, SpatialOperator.OVERLAPS.name(), 0.0); LOGGER.debug("EXITING: Intersects filter"); return predicate; } /** * During filter maps to a Temporal (Absolute and Modified) search criteria. */ @Override public Object visit(During filter, Object data) { LOGGER.debug("ENTERING: During filter"); AttributeExpressionImpl temporalTypeAttribute = (AttributeExpressionImpl) filter .getExpression1(); String temporalType = temporalTypeAttribute.getPropertyName(); LiteralExpressionImpl timePeriodLiteral = (LiteralExpressionImpl) filter.getExpression2(); Object literal = timePeriodLiteral.getValue(); Predicate returnPredicate = null; if (literal instanceof Period) { Period timePeriod = (Period) literal; // Extract the start and end dates from the OGC TOverlaps filter Date start = timePeriod.getBeginning().getPosition().getDate(); Date end = timePeriod.getEnding().getPosition().getDate(); LOGGER.debug("time period lowerBound = {}", start); LOGGER.debug("time period upperBound = {}", end); LOGGER.debug("EXITING: (temporal) filter"); returnPredicate = new TemporalPredicate(start, end, DateType.valueOf(temporalType)); // CREATE RELATIVE } else if (literal instanceof PeriodDuration) { DefaultPeriodDuration duration = (DefaultPeriodDuration) literal; long offset = duration.getTimeInMillis(); LOGGER.debug("EXITING: (temporal) filter"); returnPredicate = new TemporalPredicate(offset, DateType.valueOf(temporalType)); } LOGGER.debug("temporalType: " + temporalType); LOGGER.debug("Temporal Predicate: " + returnPredicate); LOGGER.debug("EXITING: During filter"); return returnPredicate; } /** * PropertyIsEqualTo filter maps to a Type/Version(s) and Entry search criteria. */ @Override public Object visit(PropertyIsEqualTo filter, Object data) { LOGGER.debug("ENTERING: PropertyIsEqualTo filter"); // TODO: consider if the contentType parameters are invalid (i.e. anything where type is // null) AttributeExpressionImpl exp1 = (AttributeExpressionImpl) filter.getExpression1(); String propertyName = exp1.getPropertyName(); LiteralExpressionImpl exp2 = (LiteralExpressionImpl) filter.getExpression2(); Predicate predicate = null; if (Metacard.ID.equals(propertyName)) { String entryId = (String) exp2.getValue(); LOGGER.debug("entry id for new entry predicate: {}", entryId); predicate = new EntryPredicate(entryId); } else if (Metacard.CONTENT_TYPE.equals(propertyName)) { String typeValue = (String) exp2.getValue(); predicate = new ContentTypePredicate(typeValue, null); } else if (Metacard.CONTENT_TYPE_VERSION.equals(propertyName)) { String versionValue = (String) exp2.getValue(); predicate = new ContentTypePredicate(null, versionValue); } else if (Metacard.RESOURCE_URI.equals(propertyName)) { URI productUri = null; if (exp2.getValue() instanceof URI) { productUri = (URI) exp2.getValue(); predicate = new EntryPredicate(productUri); } else { try { productUri = new URI((String) exp2.getValue()); predicate = new EntryPredicate(productUri); } catch (URISyntaxException e) { LOGGER.debug("URI Syntax exception creating EntryPredicate", e); throw new UnsupportedOperationException( "Could not create a URI object from the given ResourceURI.", e); } } } LOGGER.debug("EXITING: PropertyIsEqualTo filter"); return predicate; } /** * PropertyIsLike filter maps to a Contextual search criteria. */ @Override public Object visit(PropertyIsLike filter, Object data) { LOGGER.debug("ENTERING: PropertyIsLike filter"); String wildcard = filter.getWildCard(); String escape = filter.getEscape(); String single = filter.getSingleChar(); boolean isFuzzy = false; List<String> textPathList = null; LikeFilterImpl likeFilter = (LikeFilterImpl) filter; Expression expression = likeFilter.getExpression(); // This block handles if the PropertyIsLike filter is representing a Content Type // or Content Type Version. If that is the case, then create and return a // ContentTypePredicate if (expression instanceof PropertyName) { PropertyName propertyName = (PropertyName) expression; if (Metacard.CONTENT_TYPE.equals(propertyName.getPropertyName())) { LOGGER.debug("Expression is ContentType."); String typeValue = likeFilter.getLiteral(); ContentTypePredicate predicate = new ContentTypePredicate(typeValue, null); return predicate; } else if (Metacard.CONTENT_TYPE_VERSION.equals(propertyName.getPropertyName())) { LOGGER.debug("Expression is ContentTypeVersion."); String versionValue = likeFilter.getLiteral(); ContentTypePredicate predicate = new ContentTypePredicate(null, versionValue); return predicate; } } if (expression instanceof AttributeExpressionImpl) { AttributeExpressionImpl textPathExpression = (AttributeExpressionImpl) expression; textPathList = extractXpathSelectors(textPathExpression); } else if (expression instanceof FuzzyFunction) { FuzzyFunction fuzzyFunction = (FuzzyFunction) expression; LOGGER.debug("fuzzy search"); isFuzzy = true; List<Expression> expressions = fuzzyFunction.getParameters(); AttributeExpressionImpl firstExpression = (AttributeExpressionImpl) expressions.get(0); if (!Metacard.ANY_TEXT.equals(firstExpression.getPropertyName())) { LOGGER.debug("fuzzy search has a text path section"); textPathList = extractXpathSelectors(firstExpression); } } String searchPhrase = likeFilter.getLiteral(); LOGGER.debug("raw searchPhrase = [{}]", searchPhrase); String sterilizedSearchPhrase = sterilize(searchPhrase, wildcard, escape, single); LOGGER.debug("sterilizedSearchPhrase = [{}]", sterilizedSearchPhrase); ContextualPredicate contextPred = new ContextualPredicate(sterilizedSearchPhrase, isFuzzy, likeFilter.isMatchingCase(), textPathList); LOGGER.debug("EXITING: PropertyIsLike filter"); return contextPred; } private List<String> extractXpathSelectors(AttributeExpressionImpl textPathExpression) { List<String> textPathList = new ArrayList<String>(); String selectors; selectors = textPathExpression.getPropertyName(); LOGGER.debug("sub filter visitor selectors = {}", selectors); // Copy text paths into contextual criteria if any specified other than default "anyText" // which needs to be "removed" as it means nothing to the source if (selectors != null && !selectors.isEmpty() && !selectors.contains(Metacard.ANY_TEXT)) { String[] xpathExpressions = selectors.split(","); for (String textPath : xpathExpressions) { LOGGER.debug("adding text path to list: {}", textPath); textPathList.add(textPath); } } return textPathList; } @Override public Object visit(PropertyName expression, Object data) { LOGGER.debug("ENTERING: PropertyName expression"); // countOccurrence( expression ); LOGGER.debug("EXITING: PropertyName expression"); return data; } @Override public Object visit(Literal expression, Object data) { LOGGER.debug("ENTERING: Literal expression"); // countOccurrence( expression ); LOGGER.debug("EXITING: Literal expression"); return data; } @Override public Object visit(IncludeFilter filter, Object data) { LOGGER.debug("ENTERING: Visit Filter Includes"); LOGGER.debug("EXITING: Visit Filter Includes"); return null; } // translate filter contextual criteria to lucene syntax private String sterilize(String searchPhrase, String wildcard, String escape, String single) { // remove any spaces leading or trailing String returnSearchPhrase = searchPhrase.trim(); if (!escape.equals(LUCENE_ESCAPE_CHAR)) { returnSearchPhrase = returnSearchPhrase .replaceAll("(?<!" + "\\Q" + escape + "\\E)" + "\\Q" + escape + "\\E", Matcher.quoteReplacement(LUCENE_ESCAPE_CHAR)); } if (!wildcard.equals(LUCENE_WILDCARD_CHAR)) { // it is required to change the wildcard character of the // filter into the wildcard character of the Catalog Provider // one problem exists here is that this assumes that backslash is the escape character. returnSearchPhrase = returnSearchPhrase .replaceAll("(?<!\\\\)" + "\\Q" + wildcard + "\\E", LUCENE_WILDCARD_CHAR); } String[] splitTokens = returnSearchPhrase.split(" "); // if this is a phrase versus a single word, wrap in quotes if (splitTokens.length > 1) { returnSearchPhrase = QUOTE + returnSearchPhrase + QUOTE; } return returnSearchPhrase; } private com.vividsolutions.jts.geom.Geometry getJtsGeometery( LiteralExpressionImpl geoExpression) { com.vividsolutions.jts.geom.Geometry jtsGeometry; if (geoExpression.getValue() instanceof GeometryImpl) { GeometryImpl geo = (GeometryImpl) geoExpression.getValue(); jtsGeometry = (com.vividsolutions.jts.geom.Geometry) geo.getJTSGeometry(); } else if (geoExpression.getValue() instanceof com.vividsolutions.jts.geom.Geometry) { jtsGeometry = (com.vividsolutions.jts.geom.Geometry) geoExpression.getValue(); } else { throw new UnsupportedOperationException( "Unsupported implementation of Geometry for spatial filters."); } return jtsGeometry; } }