package mil.nga.giat.geowave.adapter.vector.query.cql;
/**
* A modified copy of org.geotools.filter.text.cql2.FilterToCQL from GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org (C) 2006-2008, Open Source Geospatial Foundation (OSGeo).
*
* Fixes unsupported negated equavalence (<> instead of !=).
* Fixes construction of the'in' ID expression where an id contains non-alpha characters '-','+', etc.
*/
import java.util.Iterator;
import java.util.List;
import mil.nga.giat.geowave.adapter.vector.plugin.GeoWaveGTDataStore;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.geotools.filter.LiteralExpressionImpl;
import org.geotools.filter.spatial.IntersectsImpl;
import org.geotools.filter.text.commons.ExpressionToText;
import org.geotools.filter.text.commons.FilterToTextUtil;
import org.opengis.filter.And;
import org.opengis.filter.ExcludeFilter;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterVisitor;
import org.opengis.filter.Id;
import org.opengis.filter.IncludeFilter;
import org.opengis.filter.Not;
import org.opengis.filter.Or;
import org.opengis.filter.PropertyIsBetween;
import org.opengis.filter.PropertyIsEqualTo;
import org.opengis.filter.PropertyIsGreaterThan;
import org.opengis.filter.PropertyIsGreaterThanOrEqualTo;
import org.opengis.filter.PropertyIsLessThan;
import org.opengis.filter.PropertyIsLessThanOrEqualTo;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.PropertyIsNil;
import org.opengis.filter.PropertyIsNotEqualTo;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Function;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.identity.Identifier;
import org.opengis.filter.spatial.BBOX;
import org.opengis.filter.spatial.Beyond;
import org.opengis.filter.spatial.Contains;
import org.opengis.filter.spatial.Crosses;
import org.opengis.filter.spatial.DWithin;
import org.opengis.filter.spatial.Disjoint;
import org.opengis.filter.spatial.Equals;
import org.opengis.filter.spatial.Intersects;
import org.opengis.filter.spatial.Overlaps;
import org.opengis.filter.spatial.Touches;
import org.opengis.filter.spatial.Within;
import org.opengis.filter.temporal.After;
import org.opengis.filter.temporal.AnyInteracts;
import org.opengis.filter.temporal.Before;
import org.opengis.filter.temporal.Begins;
import org.opengis.filter.temporal.BegunBy;
import org.opengis.filter.temporal.During;
import org.opengis.filter.temporal.EndedBy;
import org.opengis.filter.temporal.Ends;
import org.opengis.filter.temporal.Meets;
import org.opengis.filter.temporal.MetBy;
import org.opengis.filter.temporal.OverlappedBy;
import org.opengis.filter.temporal.TContains;
import org.opengis.filter.temporal.TEquals;
import org.opengis.filter.temporal.TOverlaps;
import org.opengis.referencing.operation.TransformException;
import com.vividsolutions.jts.geom.Geometry;
public class FilterToECQLExtension implements
FilterVisitor
{
private static Logger LOGGER = LoggerFactory.getLogger(FilterToECQLExtension.class);
ExpressionToText expressionVisitor = new ExpressionToText();
@Override
public Object visitNullFilter(
final Object extraData ) {
throw new NullPointerException(
"Cannot encode null as a Filter");
}
@Override
public Object visit(
final ExcludeFilter filter,
final Object extraData ) {
return FilterToTextUtil.buildExclude(extraData);
}
@Override
public Object visit(
final IncludeFilter filter,
final Object extraData ) {
return FilterToTextUtil.buildInclude(extraData);
}
@Override
public Object visit(
final And filter,
final Object extraData ) {
return FilterToTextUtil.buildBinaryLogicalOperator(
"AND",
this,
filter,
extraData);
}
/**
* builds a ecql id expression: in (id1, id2, ...)
*/
@Override
public Object visit(
final Id filter,
final Object extraData ) {
final StringBuilder ecql = FilterToTextUtil.asStringBuilder(extraData);
ecql.append("IN (");
final Iterator<Identifier> iter = filter.getIdentifiers().iterator();
while (iter.hasNext()) {
final Identifier identifier = iter.next();
final String id = identifier.toString();
// CHANGE FROM GEOTOOLS: look for identifiers with non-alphanumeric
// characters
final boolean needsQuotes = id.matches(".*(\\.|\\-|\\+|\\*|\\/).*");
if (needsQuotes) {
ecql.append('\'');
}
ecql.append(identifier);
if (needsQuotes) {
ecql.append('\'');
}
if (iter.hasNext()) {
ecql.append(",");
}
}
ecql.append(")");
return ecql;
}
/**
* builds the Not logical operator
*/
@Override
public Object visit(
final Not filter,
final Object extraData ) {
return FilterToTextUtil.buildNot(
this,
filter,
extraData);
}
/**
* Builds the OR logical operator.
*
* This visitor checks for {@link #isInFilter(Or)} and is willing to output
* ECQL of the form <code>left IN (right, right, right)</code>.
*/
@Override
public Object visit(
final Or filter,
final Object extraData ) {
if (isInFilter(filter)) {
return buildIN(
filter,
extraData);
}
// default to normal OR output
return FilterToTextUtil.buildBinaryLogicalOperator(
"OR",
this,
filter,
extraData);
}
/** Check if this is an encoding of ECQL IN */
private boolean isInFilter(
final Or filter ) {
if (filter.getChildren() == null) {
return false;
}
Expression left = null;
for (final Filter child : filter.getChildren()) {
if (child instanceof PropertyIsEqualTo) {
final PropertyIsEqualTo equal = (PropertyIsEqualTo) child;
if (left == null) {
left = equal.getExpression1();
}
else if (!left.equals(equal.getExpression1())) {
return false; // not IN
}
}
else {
return false; // not IN
}
}
return true;
}
private Object buildIN(
final Or filter,
final Object extraData ) {
final StringBuilder output = FilterToTextUtil.asStringBuilder(extraData);
final List<Filter> children = filter.getChildren();
final PropertyIsEqualTo first = (PropertyIsEqualTo) filter.getChildren().get(
0);
final Expression left = first.getExpression1();
left.accept(
expressionVisitor,
output);
output.append(" IN (");
for (final Iterator<Filter> i = children.iterator(); i.hasNext();) {
final PropertyIsEqualTo child = (PropertyIsEqualTo) i.next();
final Expression right = child.getExpression2();
right.accept(
expressionVisitor,
output);
if (i.hasNext()) {
output.append(",");
}
}
output.append(")");
return output;
}
/**
* builds the BETWEEN predicate
*/
@Override
public Object visit(
final PropertyIsBetween filter,
final Object extraData ) {
return FilterToTextUtil.buildBetween(
filter,
extraData);
}
/**
* Output EQUAL filter (will checks for ECQL geospatial operations).
*/
@Override
public Object visit(
final PropertyIsEqualTo filter,
final Object extraData ) {
final StringBuilder output = FilterToTextUtil.asStringBuilder(extraData);
if (isRelateOperation(filter)) {
return buildRelate(
filter,
output);
}
else if (isFunctionTrue(
filter,
"PropertyExists",
1)) {
return buildExists(
filter,
output);
}
return FilterToTextUtil.buildComparison(
filter,
output,
"=");
}
/** Check if this is an encoding of ECQL geospatial operation */
private boolean isFunctionTrue(
final PropertyIsEqualTo filter,
final String operation,
final int numberOfArguments ) {
if (filter.getExpression1() instanceof Function) {
final Function function = (Function) filter.getExpression1();
final List<Expression> parameters = function.getParameters();
if (parameters == null) {
return false;
}
final String name = function.getName();
if (!operation.equalsIgnoreCase(name) || (parameters.size() != numberOfArguments)) {
return false;
}
}
else {
return false;
}
if (filter.getExpression2() instanceof Literal) {
final Literal literal = (Literal) filter.getExpression2();
final Boolean value = literal.evaluate(
null,
Boolean.class);
if ((value == null) || (value == false)) {
return false;
}
}
else {
return false;
}
return true;
}
private Object buildExists(
final PropertyIsEqualTo filter,
final StringBuilder output ) {
final Function function = (Function) filter.getExpression1();
final List<Expression> parameters = function.getParameters();
final Literal arg = (Literal) parameters.get(0);
output.append(arg.getValue());
output.append(" EXISTS");
return output;
}
/** Check if this is an encoding of ECQL geospatial operation */
private boolean isRelateOperation(
final PropertyIsEqualTo filter ) {
if (isFunctionTrue(
filter,
"relatePattern",
3)) {
final Function function = (Function) filter.getExpression1();
final List<Expression> parameters = function.getParameters();
final Expression param3 = parameters.get(2);
if (param3 instanceof Literal) {
final Literal literal = (Literal) param3;
final Object value = literal.getValue();
if (!(value instanceof String)) {
return false; // not a relate
}
}
}
else {
return false;
}
return true;
}
private Object buildRelate(
final PropertyIsEqualTo filter,
final StringBuilder output ) {
final Function operation = (Function) filter.getExpression1();
output.append("RELATE(");
final List<Expression> parameters = operation.getParameters();
final Expression arg1 = parameters.get(0);
final Expression arg2 = parameters.get(1);
final Literal arg3 = (Literal) parameters.get(2);
arg1.accept(
expressionVisitor,
output);
output.append(",");
arg2.accept(
expressionVisitor,
output);
output.append(",");
output.append(arg3.getValue());
output.append(")");
return output;
}
@Override
public Object visit(
final PropertyIsNotEqualTo filter,
final Object extraData ) {
// CHANGE FROM GEOTOOLS: <> instead of !=
return FilterToTextUtil.buildComparison(
filter,
extraData,
"<>");
}
@Override
public Object visit(
final PropertyIsGreaterThan filter,
final Object extraData ) {
return FilterToTextUtil.buildComparison(
filter,
extraData,
">");
}
@Override
public Object visit(
final PropertyIsGreaterThanOrEqualTo filter,
final Object extraData ) {
return FilterToTextUtil.buildComparison(
filter,
extraData,
">=");
}
@Override
public Object visit(
final PropertyIsLessThan filter,
final Object extraData ) {
return FilterToTextUtil.buildComparison(
filter,
extraData,
"<");
}
@Override
public Object visit(
final PropertyIsLessThanOrEqualTo filter,
final Object extraData ) {
return FilterToTextUtil.buildComparison(
filter,
extraData,
"<=");
}
@Override
public Object visit(
final PropertyIsLike filter,
final Object extraData ) {
return FilterToTextUtil.buildIsLike(
filter,
extraData);
}
@Override
public Object visit(
final PropertyIsNull filter,
final Object extraData ) {
return FilterToTextUtil.buildIsNull(
filter,
extraData);
}
@Override
public Object visit(
final PropertyIsNil filter,
final Object extraData ) {
throw new UnsupportedOperationException(
"PropertyIsNil not supported");
}
@Override
public Object visit(
final BBOX filter,
final Object extraData ) {
return FilterToTextUtil.buildBBOX(
filter,
extraData);
}
@Override
public Object visit(
final Beyond filter,
final Object extraData ) {
return FilterToTextUtil.buildDistanceBufferOperation(
"BEYOND",
filter,
extraData);
}
@Override
public Object visit(
final Contains filter,
final Object extraData ) {
return FilterToTextUtil.buildBinarySpatialOperator(
"CONTAINS",
filter,
extraData);
}
@Override
public Object visit(
final Crosses filter,
final Object extraData ) {
return FilterToTextUtil.buildBinarySpatialOperator(
"CROSSES",
filter,
extraData);
}
@Override
public Object visit(
final Disjoint filter,
final Object extraData ) {
return FilterToTextUtil.buildBinarySpatialOperator(
"DISJOINT",
filter,
extraData);
}
/**
* DWithin spatial operator will find out if a feature in a datalayer is
* within X meters of a point, line, or polygon.
*/
@Override
public Object visit(
final DWithin filter,
final Object extraData ) {
IntersectsImpl newWithImpl = null;
try {
if ((filter.getExpression1() instanceof PropertyName) && (filter.getExpression2() instanceof Literal)) {
Pair<Geometry, Double> geometryAndDegrees;
geometryAndDegrees = mil.nga.giat.geowave.adapter.vector.utils.GeometryUtils.buffer(
GeoWaveGTDataStore.DEFAULT_CRS,
filter.getExpression2().evaluate(
extraData,
Geometry.class),
filter.getDistanceUnits(),
filter.getDistance());
newWithImpl = new IntersectsImpl(
filter.getExpression1(),
new LiteralExpressionImpl(
geometryAndDegrees.getLeft()));
}
else if ((filter.getExpression2() instanceof PropertyName) && (filter.getExpression1() instanceof Literal)) {
final Pair<Geometry, Double> geometryAndDegrees = mil.nga.giat.geowave.adapter.vector.utils.GeometryUtils
.buffer(
GeoWaveGTDataStore.DEFAULT_CRS,
filter.getExpression1().evaluate(
extraData,
Geometry.class),
filter.getDistanceUnits(),
filter.getDistance());
newWithImpl = new IntersectsImpl(
new LiteralExpressionImpl(
geometryAndDegrees.getLeft()),
filter.getExpression2());
}
}
catch (TransformException e) {
LOGGER.error(
"Cannot transform geoemetry to support provide distance",
e);
return FilterToTextUtil.buildDWithin(
filter,
extraData);
}
return FilterToTextUtil.buildBinarySpatialOperator(
"INTERSECTS",
newWithImpl,
extraData);
}
@Override
public Object visit(
final Equals filter,
final Object extraData ) {
return FilterToTextUtil.buildBinarySpatialOperator(
"EQUALS",
filter,
extraData);
}
@Override
public Object visit(
final Intersects filter,
final Object extraData ) {
return FilterToTextUtil.buildBinarySpatialOperator(
"INTERSECTS",
filter,
extraData);
}
@Override
public Object visit(
final Overlaps filter,
final Object extraData ) {
return FilterToTextUtil.buildBinarySpatialOperator(
"OVERLAPS",
filter,
extraData);
}
@Override
public Object visit(
final Touches filter,
final Object extraData ) {
return FilterToTextUtil.buildBinarySpatialOperator(
"TOUCHES",
filter,
extraData);
}
@Override
public Object visit(
final Within filter,
final Object extraData ) {
return FilterToTextUtil.buildBinarySpatialOperator(
"WITHIN",
filter,
extraData);
}
@Override
public Object visit(
final After after,
final Object extraData ) {
return FilterToTextUtil.buildBinaryTemporalOperator(
"AFTER",
after,
extraData);
}
@Override
public Object visit(
final Before before,
final Object extraData ) {
return FilterToTextUtil.buildBinaryTemporalOperator(
"BEFORE",
before,
extraData);
}
@Override
public Object visit(
final AnyInteracts anyInteracts,
final Object extraData ) {
throw ecqlUnsupported("AnyInteracts");
}
@Override
public Object visit(
final Begins begins,
final Object extraData ) {
throw ecqlUnsupported("Begins");
}
@Override
public Object visit(
final BegunBy begunBy,
final Object extraData ) {
throw ecqlUnsupported("BegunBy");
}
/**
* New instance of unsupported exception with the name of filter
*
* @param filterName
* filter unsupported
* @return UnsupportedOperationException
*/
static private UnsupportedOperationException ecqlUnsupported(
final String filterName ) {
return new UnsupportedOperationException(
"The" + filterName + " has not an ECQL expression");
}
@Override
public Object visit(
final During during,
final Object extraData ) {
return FilterToTextUtil.buildDuring(
during,
extraData);
}
@Override
public Object visit(
final EndedBy endedBy,
final Object extraData ) {
throw ecqlUnsupported("EndedBy");
}
@Override
public Object visit(
final Ends ends,
final Object extraData ) {
throw ecqlUnsupported("EndedBy");
}
@Override
public Object visit(
final Meets meets,
final Object extraData ) {
throw ecqlUnsupported("Meets");
}
@Override
public Object visit(
final MetBy metBy,
final Object extraData ) {
throw ecqlUnsupported("MetBy");
}
@Override
public Object visit(
final OverlappedBy overlappedBy,
final Object extraData ) {
throw ecqlUnsupported("OverlappedBy");
}
@Override
public Object visit(
final TContains contains,
final Object extraData ) {
throw ecqlUnsupported("TContains");
}
@Override
public Object visit(
final TEquals equals,
final Object extraData ) {
throw ecqlUnsupported("TContains");
}
@Override
public Object visit(
final TOverlaps contains,
final Object extraData ) {
throw ecqlUnsupported("TContains");
}
}