/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2006-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library 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;
* version 2.1 of the License.
*
* This library 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.
*/
package org.geotools.filter.text.cql2;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;
import org.geotools.filter.LikeFilterImpl;
import org.opengis.filter.And;
import org.opengis.filter.BinaryComparisonOperator;
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.PropertyIsNotEqualTo;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.expression.Add;
import org.opengis.filter.expression.Divide;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.ExpressionVisitor;
import org.opengis.filter.expression.Function;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.Multiply;
import org.opengis.filter.expression.NilExpression;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.expression.Subtract;
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 com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.WKTWriter;
/**
* This is a utility class used by CQL.encode( Filter ) method to do the
* hard work.
* <p>
* Please note that this encoder is a bit more strict than you may be used to
* (the Common Query Language for example demands Equals.getExpression1() is a
* PropertyName). If you used FilterFactory to produce your filter you should be
* okay (as it only provides methods to make a valid Filter); if not please
* expect ClassCastExceptions.
* <p>
* This visitor will return a StringBuffer; you can also provide a StringBuffer
* as the data parameter in order to cut down on the number of objects
* created during encoding.<pre><code>
* FilterToCQL toCQL = new FilterToCQL();
* StringBuffer output = filter.accepts( toCQL, new StringBuffer() );
* String cql = output.toString();
* </code></pre>
* @author Johann Sorel
*/
class FilterToCQL implements FilterVisitor, ExpressionVisitor {
/** Standard java logger */
private static Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.filter");
private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
/**
* Process the possibly user supplied extraData parameter into a StringBuffer.
* @param extraData
* @return
*/
protected StringBuffer asStringBuffer( Object extraData){
if( extraData instanceof StringBuffer){
return (StringBuffer) extraData;
}
return new StringBuffer();
}
/**
* Exclude everything; using an old SQL trick of 1=0.
*/
public Object visit(ExcludeFilter filter, Object extraData) {
StringBuffer output = asStringBuffer(extraData);
output.append("1 = 1");
return output;
}
/**
* Include everything; using an old SQL trick of 1=1.
*/
public Object visit(IncludeFilter filter, Object extraData) {
StringBuffer output = asStringBuffer(extraData);
output.append("1 = 1");
return output;
}
public Object visit(And filter, Object extraData) {
LOGGER.finer("exporting And filter");
StringBuffer output = asStringBuffer(extraData);
List<Filter> children = filter.getChildren();
if( children != null ){
output.append("(");
for( Iterator<Filter> i=children.iterator(); i.hasNext(); ){
Filter child = i.next();
child.accept(this, output);
/*if(comparisonHasDate(child) ){
// FIXME during should be built
throw new UnsupportedOperationException("work in progress: DURING requires implementation!");
} else {*/
if (i.hasNext()) {
output.append(" AND ");
}
// }
}
output.append(")");
}
return output;
}
/**
* Encoding an Id filter is not supported by CQL.
* <p>
* This is because in the Catalog specification retreiving an object
* by an id is a distinct operation seperate from a filter based query.
*/
public Object visit(Id filter, Object extraData) {
throw new IllegalStateException("Cannot encode an Id as legal CQL");
}
public Object visit(Not filter, Object extraData) {
LOGGER.finer("exporting Not filter");
StringBuffer output = asStringBuffer(extraData);
output.append( "NOT (");
filter.getFilter().accept(this, output );
output.append( ")");
return output;
}
public Object visit(Or filter, Object extraData) {
LOGGER.finer("exporting Or filter");
StringBuffer output = asStringBuffer(extraData);
List<Filter> children = filter.getChildren();
if( children != null ){
output.append("(");
for( Iterator<Filter> i=children.iterator(); i.hasNext(); ){
Filter child = i.next();
child.accept(this, output);
if (i.hasNext()) {
output.append(" OR ");
}
}
output.append(")");
}
return output;
}
public Object visit(PropertyIsBetween filter, Object extraData) {
LOGGER.finer("exporting PropertyIsBetween");
StringBuffer output = asStringBuffer(extraData);
PropertyName propertyName = (PropertyName) filter.getExpression();
propertyName.accept(this, output);
output.append(" BETWEEN ");
filter.getLowerBoundary().accept(this, output);
output.append(" AND ");
filter.getUpperBoundary().accept(this, output);
return output;
}
public Object visit(PropertyIsEqualTo filter, Object extraData) {
LOGGER.finer("exporting PropertyIsEqualTo");
StringBuffer output = asStringBuffer(extraData);
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(" = ");
filter.getExpression2().accept(this, output);
return output;
}
public Object visit(PropertyIsNotEqualTo filter, Object extraData) {
LOGGER.finer("exporting PropertyIsNotEqualTo");
StringBuffer output = asStringBuffer(extraData);
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(" != ");
filter.getExpression2().accept(this, output);
return output;
}
public Object visit(PropertyIsGreaterThan filter, Object extraData) {
StringBuffer output = asStringBuffer(extraData);
if( comparisonHasDate( filter )){
return after( filter, output);
}
LOGGER.finer("exporting PropertyIsGreaterThan");
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(" > ");
filter.getExpression2().accept(this, output);
return output;
}
/**
* Checks if the comparison filter has a literal date.
* @param filter
* @return true if the comparison has a literal date , false in other case.
*/
private boolean comparisonHasDate( Filter filter) {
if(!(filter instanceof BinaryComparisonOperator)){
return false;
}
BinaryComparisonOperator comparison = (BinaryComparisonOperator) filter;
boolean bool;
if( comparison.getExpression2() instanceof Literal){
Literal literal = (Literal) comparison.getExpression2();
bool = literal.getValue() instanceof Date;
} else {
Literal literal = (Literal) comparison.getExpression1();
bool = literal.getValue() instanceof Date;
}
return bool;
}
/**
* This is where it would be noice to know if we are working on a Date.
* <p>
* I am tempted to do the SimpleFeature look aisde in order to guess
* what kind of type I am working with.
*/
private StringBuffer after( PropertyIsGreaterThan filter, StringBuffer output ){
LOGGER.finer("exporting AFTER");
Object expr1 = filter.getExpression1();
if( expr1 instanceof PropertyName){
PropertyName propertyName = (PropertyName) expr1;
propertyName.accept(this, output);
output.append(" AFTER ");
filter.getExpression2().accept(this, output);
}else {
PropertyName propertyName = (PropertyName) filter.getExpression2();
propertyName.accept(this, output);
output.append(" BEFORE ");
filter.getExpression1().accept(this, output);
}
return output;
}
public Object visit(PropertyIsGreaterThanOrEqualTo filter, Object extraData) {
LOGGER.finer("exporting PropertyIsGreaterThanOrEqualTo");
StringBuffer output = asStringBuffer(extraData);
Object expr1 = filter.getExpression1();
if( expr1 instanceof PropertyName){
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(" >= ");
filter.getExpression2().accept(this, output);
} else {
PropertyName propertyName = (PropertyName) filter.getExpression2();
propertyName.accept(this, output);
output.append(" <= ");
filter.getExpression1().accept(this, output);
}
return output;
}
public Object visit(PropertyIsLessThan filter, Object extraData) {
LOGGER.finer("exporting PropertyIsLessThan");
StringBuffer output = asStringBuffer(extraData);
Object expr1 = filter.getExpression1();
if( expr1 instanceof PropertyName){
PropertyName propertyName = (PropertyName) expr1;
propertyName.accept(this, output);
Literal expression2 = (Literal)filter.getExpression2();
Object literalValue = expression2.getValue();
if( literalValue instanceof Date){
output.append(" BEFORE ");
filter.getExpression2().accept(this, output);
} else {
output.append(" < ");
filter.getExpression2().accept(this, output);
}
} else {
PropertyName propertyName = (PropertyName) filter.getExpression2();
propertyName.accept(this, output);
Literal expression2 = (Literal)expr1;
Object literalValue = expression2.getValue();
if( literalValue instanceof Date){
output.append(" AFTER ");
filter.getExpression1().accept(this, output);
} else {
output.append(" > ");
filter.getExpression1().accept(this, output);
}
}
return output;
}
public Object visit(PropertyIsLessThanOrEqualTo filter, Object extraData) {
LOGGER.finer("exporting PropertyIsLessThanOrEqualTo");
StringBuffer output = asStringBuffer(extraData);
Object expr1 = filter.getExpression1();
if( expr1 instanceof PropertyName){
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(" <= ");
filter.getExpression2().accept(this, output);
} else {
PropertyName propertyName = (PropertyName) filter.getExpression2();
propertyName.accept(this, output);
output.append(" >= ");
filter.getExpression1().accept(this, output);
}
return output;
}
public Object visit(PropertyIsLike filter, Object extraData) {
StringBuffer output = asStringBuffer(extraData);
char esc = filter.getEscape().charAt(0);
char multi = filter.getWildCard().charAt(0);
char single = filter.getSingleChar().charAt(0);
boolean matchCase = filter.isMatchingCase();
String pattern = LikeFilterImpl.convertToSQL92(esc, multi, single, matchCase,
filter.getLiteral());
PropertyName propertyName = (PropertyName) filter.getExpression();
propertyName.accept(this, output);
output.append(" LIKE '");
output.append(pattern);
output.append("'");
return output;
}
public Object visit(PropertyIsNull filter, Object extraData) {
StringBuffer output = asStringBuffer(extraData);
PropertyName propertyName = (PropertyName) filter.getExpression();
propertyName.accept(this, output);
output.append(" IS NULL");
return output;
}
public Object visit(BBOX filter, Object extraData) {
StringBuffer output = asStringBuffer(extraData);
output.append( "BBOX(");
output.append( filter.getPropertyName() );
output.append( ", ");
output.append( filter.getMinX() );
output.append( ",");
output.append( filter.getMinY() );
output.append( ",");
output.append( filter.getMaxX() );
output.append( ",");
output.append( filter.getMaxY() );
output.append( ")");
return output;
}
public Object visit(Beyond filter, Object extraData) {
LOGGER.finer("exporting Beyond");
StringBuffer output = asStringBuffer(extraData);
output.append("BEYOND(");
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(", ");
filter.getExpression2().accept(this, output);
output.append(")");
return output;
}
public Object visit(Contains filter, Object extraData) {
LOGGER.finer("exporting Contains");
StringBuffer output = asStringBuffer(extraData);
output.append("CONTAINS(");
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(", ");
filter.getExpression2().accept(this, output);
output.append(")");
return output;
}
public Object visit(Crosses filter, Object extraData) {
LOGGER.finer("exporting Crosses");
StringBuffer output = asStringBuffer(extraData);
output.append("CROSS(");
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(", ");
filter.getExpression2().accept(this, output);
output.append(")");
return output;
}
public Object visit(Disjoint filter, Object extraData) {
LOGGER.finer("exporting Crosses");
StringBuffer output = asStringBuffer(extraData);
output.append("DISJOINT(");
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(", ");
filter.getExpression2().accept(this, output);
output.append(")");
return output;
}
public Object visit(DWithin filter, Object extraData) {
LOGGER.finer("exporting Crosses");
StringBuffer output = asStringBuffer(extraData);
output.append("DWITHIN(");
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(", ");
filter.getExpression2().accept(this, output);
output.append(", ");
output.append( filter.getDistance() );
output.append(", ");
output.append( filter.getDistanceUnits() );
output.append(")");
return output;
}
public Object visit(Equals filter, Object extraData) {
LOGGER.finer("exporting Equals");
StringBuffer output = asStringBuffer(extraData);
output.append("EQUALS(");
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(", ");
filter.getExpression2().accept(this, output);
output.append(")");
return output;
}
public Object visit(Intersects filter, Object extraData) {
LOGGER.finer("exporting Intersects");
StringBuffer output = asStringBuffer(extraData);
output.append("INTERSECT(");
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(", ");
filter.getExpression2().accept(this, output);
output.append(")");
return output;
}
public Object visit(Overlaps filter, Object extraData) {
LOGGER.finer("exporting Overlaps");
StringBuffer output = asStringBuffer(extraData);
output.append("OVERLAP(");
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(", ");
filter.getExpression2().accept(this, output);
output.append(")");
return output;
}
public Object visit(Touches filter, Object extraData) {
LOGGER.finer("exporting Touches");
StringBuffer output = asStringBuffer(extraData);
output.append("TOUCH(");
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(", ");
filter.getExpression2().accept(this, output);
output.append(")");
return output;
}
public Object visit(Within filter, Object extraData) {
LOGGER.finer("exporting Within");
StringBuffer output = asStringBuffer(extraData);
output.append("WITHIN(");
PropertyName propertyName = (PropertyName) filter.getExpression1();
propertyName.accept(this, output);
output.append(", ");
filter.getExpression2().accept(this, output);
output.append(")");
return output;
}
/**
* A filter has not been provided.
* <p>
* In general this is a bad situtation which we ask people to
* represent with Filter.INCLUDES or Filter.EXCLUDES depending
* on what behaviour they want to see happen - in this case
* literally <code>null</code> was provided.
* <p>
*/
public Object visitNullFilter(Object extraData) {
throw new NullPointerException("Cannot encode null as a Filter");
}
/**
* Not sure how to record an unset expression in CQL; going
* to use an emptry string for now.
*/
public Object visit(NilExpression expression, Object extraData) {
LOGGER.finer("exporting Expression Nil");
StringBuffer output = asStringBuffer(extraData);
output.append( "\"\"" );
return output;
}
public Object visit(Add expression, Object extraData) {
LOGGER.finer("exporting Expression Add");
StringBuffer output = asStringBuffer(extraData);
expression.getExpression1().accept(this, output );
output.append( " + " );
expression.getExpression2().accept(this, output );
return output;
}
public Object visit(Divide expression, Object extraData) {
LOGGER.finer("exporting Expression Divide");
StringBuffer output = asStringBuffer(extraData);
expression.getExpression1().accept(this, output );
output.append( " - " );
expression.getExpression2().accept(this, output );
return output;
}
public Object visit(Function function, Object extraData) {
LOGGER.finer("exporting Function");
StringBuffer output = asStringBuffer(extraData);
output.append( function.getName() );
output.append( "(" );
List<Expression> parameters = function.getParameters();
if( parameters != null ){
for( Iterator<Expression> i=parameters.iterator(); i.hasNext(); ){
Expression argument = i.next();
argument.accept(this, output );
if( i.hasNext() ){
output.append(",");
}
}
}
output.append( ")" );
return output;
}
public Object visit(Literal expression, Object extraData) {
LOGGER.finer("exporting LiteralExpression");
StringBuffer output = asStringBuffer(extraData);
Object literal = expression.getValue();
if (literal instanceof Geometry) {
Geometry geometry = (Geometry) literal;
WKTWriter writer = new WKTWriter();
String wkt = writer.write( geometry );
output.append( wkt );
}
else if( literal instanceof Number ){
// don't convert to string
output.append( literal );
}
else if (literal instanceof Date ){
return date( (Date) literal, output );
}
else {
String escaped = literal.toString().replaceAll("'", "''");
output.append("'" + escaped + "'");
}
return output;
}
/**
* Uses the format <code>yyyy-MM-dd'T'HH:mm:ss'Z'</code> for
* output the provided date.
* @param date
* @param output
* @return output
*/
public StringBuffer date( Date date, StringBuffer output ){
DateFormat dateFormatter = new SimpleDateFormat(DATE_FORMAT);
String text = dateFormatter.format( date );
output.append( text );
return output;
}
public Object visit(Multiply expression, Object extraData) {
LOGGER.finer("exporting Expression Multiply");
StringBuffer output = asStringBuffer(extraData);
expression.getExpression1().accept(this, output );
output.append( " * " );
expression.getExpression2().accept(this, output );
return output;
}
public Object visit(PropertyName expression, Object extraData) {
LOGGER.finer("exporting PropertyName");
StringBuffer output = asStringBuffer(extraData);
output.append( expression.getPropertyName() );
return output;
}
public Object visit(Subtract expression, Object extraData) {
LOGGER.finer("exporting Expression Subtract");
StringBuffer output = asStringBuffer(extraData);
expression.getExpression1().accept(this, output );
output.append( " - " );
expression.getExpression2().accept(this, output );
return output;
}
}