/* * 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.Calendar; 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 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.temporal.Period; 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 (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; } /** * Checks if the comparison filter has a literal date. * @param filter * @return true if the comparison has a literal date , false in other case. */ //FIXME 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. */ // FIXME 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(PropertyIsGreaterThan filter, Object extraData) { LOGGER.finer("exporting PropertyIsGreaterThan"); return buildComparison(filter, extraData, ">", "<"); } public Object visit(PropertyIsGreaterThanOrEqualTo filter, Object extraData) { LOGGER.finer("exporting PropertyIsGreaterThanOrEqualTo"); return buildComparison(filter, extraData, ">=", "<="); } /** * Builds a comparison predicate inserting the operato1 or operator2 taking into * account the PropertyName position in the comparison filter. * * @param filter * @param extraData * @param operator1 an operator * @param operator2 the opposite to the operator1 * @return SringBuffer */ private Object buildComparison( BinaryComparisonOperator filter, Object extraData, String operator1, String operator2) { StringBuffer output = asStringBuffer(extraData); Object expr1 = filter.getExpression1(); if (expr1 instanceof PropertyName) { PropertyName propertyName = (PropertyName) filter.getExpression1(); propertyName.accept(this, output); output.append(" ").append(operator1).append(" "); filter.getExpression2().accept(this, output); } else { PropertyName propertyName = (PropertyName) filter.getExpression2(); propertyName.accept(this, output); output.append(" ").append(operator2).append(" "); filter.getExpression1().accept(this, output); } return output; } public Object visit(PropertyIsLessThan filter, Object extraData) { LOGGER.finer("exporting PropertyIsLessThan"); return buildComparison(filter, extraData, "<", ">"); } public Object visit(PropertyIsLessThanOrEqualTo filter, Object extraData) { LOGGER.finer("exporting PropertyIsLessThanOrEqualTo"); return buildComparison(filter, extraData, "<=", ">="); } 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; } public Object visit(After after, Object extraData) { StringBuffer output = asStringBuffer(extraData); PropertyName propertyName = (PropertyName) after.getExpression1(); propertyName.accept(this, output); output.append(" AFTER "); Literal expr2 = (Literal) after.getExpression2(); expr2.accept(this, output); return output; } public Object visit(Before before, Object extraData) { StringBuffer output = asStringBuffer(extraData); PropertyName propertyName = (PropertyName) before.getExpression1(); propertyName.accept(this, output); output.append(" BEFORE "); Literal expr2 = (Literal) before.getExpression2(); expr2.accept(this, output); return output; } public Object visit(During during, Object extraData) { StringBuffer output = asStringBuffer(extraData); PropertyName propertyName = (PropertyName) during.getExpression1(); propertyName.accept(this, output); output.append(" DURING "); Literal expr2 = (Literal) during.getExpression2(); Period period = (Period) expr2.getValue(); String strBeginningData = dateToCQLDate( period.getBeginning().getPosition().getDate() ); String strEndingDate = dateToCQLDate( period.getEnding().getPosition().getDate() ); output.append(strBeginningData).append("/").append(strEndingDate); return output; } private String dateToCQLDate(Date date){ StringBuffer cqlDate = new StringBuffer(); Calendar cal = Calendar.getInstance(); cal.setTime(date); // builds the string date int years = cal.get(Calendar.YEAR); String strYear = String.format("%04d", years); cqlDate.append(strYear).append("-"); int month = cal.get(Calendar.MONTH)+1; String strMonth = String.format("%02d", month); cqlDate.append(strMonth).append("-"); int day = cal.get(Calendar.DAY_OF_MONTH); String strDay = String.format("%02d", day); cqlDate.append(strDay); // builds the string time cqlDate.append("T"); int hour = cal.get(Calendar.HOUR); String strHour = String.format("%02d", hour); cqlDate.append(strHour).append(":"); int minute = cal.get(Calendar.MINUTE); String strMinute = String.format("%02d", minute); cqlDate.append(strMinute).append(":"); int second = cal.get(Calendar.SECOND); String strSecond = String.format("%02d", second); cqlDate.append(strSecond).append("Z"); // TODO it is a bug in the cql specification. Zulu zone shouldn't be only one of various possibles zone times. return cqlDate.toString(); } public Object visit(AnyInteracts anyInteracts, Object extraData) { throw new UnsupportedOperationException("Temporal filter AnyInteracts has not a CQL expression"); } public Object visit(Begins begins, Object extraData) { throw new UnsupportedOperationException("Temporal filter Begins has not a CQL expression"); } public Object visit(BegunBy begunBy, Object extraData) { throw new UnsupportedOperationException("Temporal filter BegunBy has not a CQL expression"); } public Object visit(EndedBy endedBy, Object extraData) { throw new UnsupportedOperationException("Temporal filter EndedBy has not a CQL expression"); } public Object visit(Ends ends, Object extraData) { throw new UnsupportedOperationException("Temporal filter Ends has not a CQL expression"); } public Object visit(Meets meets, Object extraData) { throw new UnsupportedOperationException("Temporal filter Meets has not a CQL expression"); } public Object visit(MetBy metBy, Object extraData) { throw new UnsupportedOperationException("Temporal filter MetBy has not a CQL expression"); } public Object visit(OverlappedBy overlappedBy, Object extraData) { throw new UnsupportedOperationException("Temporal filter OverlappedBy not implemented"); } public Object visit(TContains contains, Object extraData) { throw new UnsupportedOperationException("Temporal filter TContains has not a CQL expression"); } public Object visit(TEquals equals, Object extraData) { throw new UnsupportedOperationException("Temporal filter TEquals has not a CQL expression"); } public Object visit(TOverlaps contains, Object extraData) { throw new UnsupportedOperationException("Temporal filter TOverlaps has not a CQL expression"); } }