/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-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.gui.swing.misc; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; import org.geotools.data.jdbc.fidmapper.FIDMapper; import org.geotools.filter.FilterCapabilities; import org.geotools.filter.LikeFilterImpl; import org.geotools.util.Converters; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.filter.And; import org.opengis.filter.BinaryComparisonOperator; import org.opengis.filter.BinaryLogicOperator; 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.BinaryExpression; 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.identity.FeatureId; import org.opengis.filter.spatial.BBOX; import org.opengis.filter.spatial.Beyond; import org.opengis.filter.spatial.BinarySpatialOperator; 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; /** * Encodes a filter into a CQL statement. NOT TRUSTABLE YET !!! * * * This version was ported from the original to support org.opengis.filter type * Filters. * * @author originally by Chris Holmes, TOPP * @author ported by Saul Farber, MassGIS * @author updated to CQL by Johann Sorel * * @task REVISIT: need to figure out exceptions, we're currently eating io * errors, which is bad. Probably need a generic visitor exception. * */ /* * TODO: Use the new FilterCapabilities. This may fall out of using the new * PrePostFilterSplitter code. * * TODO: Use the new Geometry classes from org.opengis. Not sure * when this will be required, but it's on the horizon here. * * Non Javadoc comments: * * Note that the old method allowed us to write WAY fewer methods, as we didn't * need to cover all the cases with exlpicit methods (as the new * org.opengis.filter.FilterVisitor and ExpressionVisitor methods require * us to do). * * The code is split into methods to support the FilterVisitor interface first * then the ExpressionVisitor methods second. * */ public class FilterToCQL implements FilterVisitor, ExpressionVisitor { /** error message for exceptions */ protected static final String IO_ERROR = "io problem writing filter"; /** The filter types that this class can encode */ protected FilterCapabilities capabilities = null; /** Standard java logger */ private static Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.filter"); /** Map of expression types to cql representation */ private static Map expressions = new HashMap(); static { expressions.put(Add.class, "+"); expressions.put(Divide.class, "/"); expressions.put(Multiply.class, "*"); expressions.put(Subtract.class, "-"); } /** Character used to escape database schema, table and column names */ private String cqlNameEscape = ""; /** where to write the constructed string from visiting the filters. */ protected Writer out; /** the fid mapper used to encode the fid filters */ protected FIDMapper mapper; /** the schmema the encoder will be used to be encode cql for */ protected SimpleFeatureType featureType; /** * Default constructor */ public FilterToCQL() { } public FilterToCQL(Writer out) { this.out = out; } /** * Performs the encoding, sends the encoded CQL to the writer passed in. * * @param filter the Filter to be encoded. * @throws org.geotools.gui.swing.misc.FilterToCQLException If filter type not supported, or if there * were io problems. * */ public void encode(Filter filter) throws FilterToCQLException { if (out == null) { throw new FilterToCQLException("Can't encode to a null writer."); } if (getCapabilities().fullySupports(filter)) { filter.accept(this, null); } else { throw new FilterToCQLException("Filter type not supported"); } } /** * purely a convenience method. * * Equivalent to: * * StringWriter out = new StringWriter(); * new FilterToCQL(out).encode(filter); * out.getBuffer().toString(); * * @param filter * @return a string representing the filter encoded to CQL. * @throws FilterToCQLException If filter type not supported, or if there * were io problems. */ public String encodeToString(Filter filter) throws FilterToCQLException { StringWriter out = new StringWriter(); this.out = out; this.encode(filter); return out.getBuffer().toString(); } /** * Sets the featuretype the encoder is encoding cql for. * <p> * This is used for context for attribute expressions when encoding to cql. * </p> * * @param featureType */ public void setFeatureType(SimpleFeatureType featureType) { this.featureType = featureType; } /** * Sets the FIDMapper that will be used in subsequente visit calls. There * must be a FIDMapper in order to invoke the FIDFilter encoder. * * @param mapper */ public void setFIDMapper(FIDMapper mapper) { this.mapper = mapper; } /** * Sets the capabilities of this filter. * * @return FilterCapabilities for this Filter */ protected FilterCapabilities createFilterCapabilities() { FilterCapabilities capabilities = new FilterCapabilities(); capabilities.addAll(FilterCapabilities.LOGICAL_OPENGIS); capabilities.addAll(FilterCapabilities.SIMPLE_COMPARISONS_OPENGIS); capabilities.addType(PropertyIsNull.class); capabilities.addType(PropertyIsBetween.class); capabilities.addType(Id.class); capabilities.addType(IncludeFilter.class); capabilities.addType(ExcludeFilter.class); return capabilities; } /** * Describes the capabilities of this encoder. * * <p> * Performs lazy creation of capabilities. * </p> * * If you're subclassing this class, override createFilterCapabilities * to declare which filtercapabilities you support. Don't use * this method. * * @return The capabilities supported by this encoder. */ public synchronized final FilterCapabilities getCapabilities() { if (capabilities == null) { capabilities = createFilterCapabilities(); } return capabilities; //maybe clone? Make immutable somehow } // BEGIN IMPLEMENTING org.opengis.filter.FilterVisitor METHODS /** * @see {@link FilterVisitor#visit(ExcludeFilter, Object)} * * not used in CQL * * @param filter the filter to be visited */ public Object visit(ExcludeFilter filter, Object extraData) { // try { // out.write("FALSE"); // } catch (IOException ioe) { // throw new RuntimeException(IO_ERROR, ioe); // } return extraData; } /** * @see {@link FilterVisitor#visit(IncludeFilter, Object)} * * not used in CQL * * @param filter the filter to be visited * */ public Object visit(IncludeFilter filter, Object extraData) { // try { // out.write("TRUE"); // } catch (IOException ioe) { // throw new RuntimeException(IO_ERROR, ioe); // } return extraData; } /** * Writes the CQL for the PropertyIsBetween Filter. * * @param filter the Filter to be visited. * * @throws RuntimeException for io exception with writer */ public Object visit(PropertyIsBetween filter, Object extraData) throws RuntimeException { LOGGER.finer("exporting PropertyIsBetween"); Expression expr = (Expression) filter.getExpression(); Expression lowerbounds = (Expression) filter.getLowerBoundary(); Expression upperbounds = (Expression) filter.getUpperBoundary(); Class context; AttributeDescriptor attType = (AttributeDescriptor) expr.evaluate(featureType); if (attType != null) { context = attType.getType().getBinding(); } else { //assume it's a string? context = String.class; } try { expr.accept(this, extraData); out.write(" BETWEEN "); lowerbounds.accept(this, context); out.write(" AND "); upperbounds.accept(this, context); } catch (java.io.IOException ioe) { throw new RuntimeException(IO_ERROR, ioe); } return extraData; } /** * Writes the CQL for the Like Filter. Assumes the current java * implemented wildcards for the Like Filter: . for multi and .? for * single. And replaces them with the CQL % and _, respectively. * * @param filter the Like Filter to be visited. * * @task REVISIT: Need to think through the escape char, so it works right * when Java uses one, and escapes correctly with an '_'. */ public Object visit(PropertyIsLike filter, Object 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()); Expression att = filter.getExpression(); try { if (!matchCase){ out.write(" UPPER("); } att.accept(this, extraData); if (!matchCase){ out.write(") LIKE '"); } else { out.write(" LIKE '"); } out.write(pattern); out.write("' "); } catch (java.io.IOException ioe) { throw new RuntimeException(IO_ERROR, ioe); } return extraData; } /** * Write the CQL for an And filter * * @param filter the filter to visit * @param extraData extra data (unused by this method) * */ public Object visit(And filter, Object extraData) { return visit((BinaryLogicOperator) filter, "AND"); } /** * Write the CQL for a Not filter * * @param filter the filter to visit * @param extraData extra data (unused by this method) * */ public Object visit(Not filter, Object extraData) { return visit((BinaryLogicOperator) filter, "NOT"); } /** * Write the CQL for an Or filter * * @param filter the filter to visit * @param extraData extra data (unused by this method) * */ public Object visit(Or filter, Object extraData) { return visit((BinaryLogicOperator) filter, "OR"); } /** * Common implementation for BinaryLogicOperator filters. This way * they're all handled centrally. * * @param filter the logic statement to be turned into CQL. * @param extraData extra filter data. Not modified directly by this method. */ protected Object visit(BinaryLogicOperator filter, Object extraData) { LOGGER.finer("exporting LogicFilter"); String type = (String) extraData; try { java.util.Iterator list = filter.getChildren().iterator(); if (filter instanceof Not) { out.write(type + " ("); ((Filter) list.next()).accept(this, extraData); out.write(")"); } else { //AND or OR out.write("("); while (list.hasNext()) { ((Filter) list.next()).accept(this, extraData); if (list.hasNext()) { out.write(" " + type + " "); } } out.write(")"); } } catch (java.io.IOException ioe) { throw new RuntimeException(IO_ERROR, ioe); } return extraData; } /** * Write the CQL for this kind of filter * * @param filter the filter to visit * @param extraData extra data (unused by this method) * */ public Object visit(PropertyIsEqualTo filter, Object extraData) { visitBinaryComparisonOperator((BinaryComparisonOperator) filter, "="); return extraData; } /** * Write the CQL for this kind of filter * * @param filter the filter to visit * @param extraData extra data (unused by this method) * */ public Object visit(PropertyIsGreaterThanOrEqualTo filter, Object extraData) { visitBinaryComparisonOperator((BinaryComparisonOperator) filter, ">="); return extraData; } /** * Write the CQL for this kind of filter * * @param filter the filter to visit * @param extraData extra data (unused by this method) * */ public Object visit(PropertyIsGreaterThan filter, Object extraData) { visitBinaryComparisonOperator((BinaryComparisonOperator) filter, ">"); return extraData; } /** * Write the CQL for this kind of filter * * @param filter the filter to visit * @param extraData extra data (unused by this method) * */ public Object visit(PropertyIsLessThan filter, Object extraData) { visitBinaryComparisonOperator((BinaryComparisonOperator) filter, "<"); return extraData; } /** * Write the CQL for this kind of filter * * @param filter the filter to visit * @param extraData extra data (unused by this method) * */ public Object visit(PropertyIsLessThanOrEqualTo filter, Object extraData) { visitBinaryComparisonOperator((BinaryComparisonOperator) filter, "<="); return extraData; } /** * Write the CQL for this kind of filter * * @param filter the filter to visit * @param extraData extra data (unused by this method) * */ public Object visit(PropertyIsNotEqualTo filter, Object extraData) { visitBinaryComparisonOperator((BinaryComparisonOperator) filter, "!="); return extraData; } /** * Common implementation for BinaryComparisonOperator filters. This way * they're all handled centrally. * * DJB: note, postgis overwrites this implementation because of the way * null is handled. This is for <PropertyIsNull> filters and <PropertyIsEqual> filters * are handled. They will come here with "property = null". * NOTE: * SELECT * FROM <table> WHERE <column> isnull; -- postgreCQL * SELECT * FROM <table> WHERE isnull(<column>); -- oracle??? * * @param filter the comparison to be turned into CQL. * * @throws RuntimeException for io exception with writer */ protected void visitBinaryComparisonOperator(BinaryComparisonOperator filter, Object extraData) throws RuntimeException { LOGGER.finer("exporting CQL ComparisonFilter"); Expression left = filter.getExpression1(); Expression right = filter.getExpression2(); Class leftContext = null, rightContext = null; if (left instanceof PropertyName) { // aha! It's a propertyname, we should get the class and pass it in // as context to the tree walker. AttributeDescriptor attType = (AttributeDescriptor) left.evaluate(featureType); if (attType != null) { rightContext = attType.getType().getBinding(); } } if (right instanceof PropertyName) { AttributeDescriptor attType = (AttributeDescriptor) right.evaluate(featureType); if (attType != null) { leftContext = attType.getType().getBinding(); } } String type = (String) extraData; try { left.accept(this, leftContext); out.write(" " + type + " "); right.accept(this, rightContext); } catch (java.io.IOException ioe) { throw new RuntimeException(IO_ERROR, ioe); } } /** * Writes the CQL for the Null Filter. * * @param filter the null filter to be written to CQL. * * @throws RuntimeException for io exception with writer */ public Object visit(PropertyIsNull filter, Object extraData) throws RuntimeException { LOGGER.finer("exporting NullFilter"); Expression expr = filter.getExpression(); try { expr.accept(this, extraData); out.write(" IS NULL "); } catch (java.io.IOException ioe) { throw new RuntimeException(IO_ERROR, ioe); } return extraData; } /** * Encodes an Id filter * * @param filter the * * @throws RuntimeException If there's a problem writing output * */ public Object visit(Id filter, Object extraData) { if (mapper == null) { throw new RuntimeException( "Must set a fid mapper before trying to encode FIDFilters"); } FeatureId[] fids = (FeatureId[]) filter.getIdentifiers().toArray(new FeatureId[filter.getIdentifiers().size()]); LOGGER.finer("Exporting FID=" + Arrays.asList(fids)); // prepare column name array String[] colNames = new String[mapper.getColumnCount()]; for (int i = 0; i < colNames.length; i++) { colNames[i] = mapper.getColumnName(i); } for (int i = 0; i < fids.length; i++) { try { Object[] attValues = mapper.getPKAttributes(fids[i].getID()); out.write("("); for (int j = 0; j < attValues.length; j++) { out.write(escapeName(colNames[j])); out.write(" = '"); out.write(attValues[j].toString()); //DJB: changed this to attValues[j] from attValues[i]. out.write("'"); if (j < (attValues.length - 1)) { out.write(" AND "); } } out.write(")"); if (i < (fids.length - 1)) { out.write(" OR "); } } catch (java.io.IOException e) { throw new RuntimeException(IO_ERROR, e); } } return extraData; } public Object visit(BBOX filter, Object extraData) { return visitBinarySpatialOperator((BinarySpatialOperator) filter, extraData); } public Object visit(Beyond filter, Object extraData) { return visitBinarySpatialOperator((BinarySpatialOperator) filter, extraData); } public Object visit(Contains filter, Object extraData) { return visitBinarySpatialOperator((BinarySpatialOperator) filter, extraData); } public Object visit(Crosses filter, Object extraData) { return visitBinarySpatialOperator((BinarySpatialOperator) filter, extraData); } public Object visit(Disjoint filter, Object extraData) { return visitBinarySpatialOperator((BinarySpatialOperator) filter, extraData); } public Object visit(DWithin filter, Object extraData) { return visitBinarySpatialOperator((BinarySpatialOperator) filter, extraData); } public Object visit(Equals filter, Object extraData) { return visitBinarySpatialOperator((BinarySpatialOperator) filter, extraData); } public Object visit(Intersects filter, Object extraData) { return visitBinarySpatialOperator((BinarySpatialOperator) filter, extraData); } public Object visit(Overlaps filter, Object extraData) { return visitBinarySpatialOperator((BinarySpatialOperator) filter, extraData); } public Object visit(Touches filter, Object extraData) { return visitBinarySpatialOperator((BinarySpatialOperator) filter, extraData); } public Object visit(Within filter, Object extraData) { return visitBinarySpatialOperator((BinarySpatialOperator) filter, extraData); } /** * @see {@link FilterVisitor#visit()} */ protected Object visitBinarySpatialOperator(BinarySpatialOperator filter, Object extraData) { throw new RuntimeException( "Subclasses must implement this method in order to handle geometries"); } /** * Encodes a null filter value. The current implementation * does exactly nothing. * @param extraData extra data to be used to evaluate the filter * @return the untouched extraData parameter */ public Object visitNullFilter(Object extraData) { return extraData; } // END IMPLEMENTING org.opengis.filter.FilterVisitor METHODS // START IMPLEMENTING org.opengis.filter.ExpressionVisitor METHODS /** * Writes the CQL for the attribute Expression. * * NOTE: This (default) implementation doesn't handle XPath at all. * Not sure exactly how to do that in a general way. How to map from the XPATH of the * property name into a column or something? Use propertyName.evaluate()? * * @param expression the attribute to turn to CQL. * * @throws RuntimeException for io exception with writer */ public Object visit(PropertyName expression, Object extraData) throws RuntimeException { LOGGER.finer("exporting PropertyName"); try { out.write(escapeName(expression.getPropertyName())); } catch (java.io.IOException ioe) { throw new RuntimeException("IO problems writing attribute exp", ioe); } return extraData; } /** * Export the contents of a Literal Expresion * * @param expression the Literal to export * * @throws RuntimeException for io exception with writer */ public Object visit(Literal expression, Object context) throws RuntimeException { LOGGER.finer("exporting LiteralExpression"); //type to convert the literal to Class target = (Class) context; try { Object literal = null; if (target == Geometry.class && expression.getValue() instanceof Geometry) { //call this method for backwards compatability with subclasses visitLiteralGeometry(expression); return context; } else if (target != null) { //convert the literal to the required type //JD except for numerics, let the database do the converstion if (Number.class.isAssignableFrom(target)) { //dont convert } else { //convert literal = expression.evaluate(null, target); } if (literal == null) { //just use string literal = expression.getValue().toString(); } //geometry hook //if ( literal instanceof Geometry ) { if (Geometry.class.isAssignableFrom(target)) { visitLiteralGeometry(expression); } //else if ( literal instanceof Number ) { else if (Number.class.isAssignableFrom(target)) { out.write(literal.toString()); } //else if ( literal instanceof String ) { else if (String.class.isAssignableFrom(target)) { // sigle quotes must be escaped to have a valid CQL string String escaped = literal.toString().replaceAll("'", "''"); out.write("'" + escaped + "'"); } } else { //convert back to a string String encoding = (String) Converters.convert(literal, String.class, null); if (encoding == null) { //could not convert back to string, use original l value encoding = expression.getValue().toString(); } // sigle quotes must be escaped to have a valid CQL string String escaped = encoding.replaceAll("'", "''"); out.write("'" + escaped + "'"); } } catch (IOException e) { throw new RuntimeException("IO problems writing literal", e); } return context; } /** * Subclasses must implement this method in order to encode geometry * filters according to the specific database implementation * * @param expression * * @throws IOException DOCUMENT ME! * @throws RuntimeException DOCUMENT ME! */ protected void visitLiteralGeometry(Literal expression) throws IOException { throw new RuntimeException( "Subclasses must implement this method in order to handle geometries"); } public Object visit(Add expression, Object extraData) { return visit((BinaryExpression) expression, extraData); } public Object visit(Divide expression, Object extraData) { return visit((BinaryExpression) expression, extraData); } public Object visit(Multiply expression, Object extraData) { return visit((BinaryExpression) expression, extraData); } public Object visit(Subtract expression, Object extraData) { return visit((BinaryExpression) expression, extraData); } /** * Writes the CQL for the Math Expression. * * @param expression the Math phrase to be written. * * @throws RuntimeException for io problems */ protected Object visit(BinaryExpression expression, Object extraData) throws RuntimeException { LOGGER.finer("exporting Expression Math"); String type = (String) expressions.get(expression.getClass()); try { expression.getExpression1().accept(this, extraData); out.write(" " + type + " "); expression.getExpression2().accept(this, extraData); } catch (java.io.IOException ioe) { throw new RuntimeException("IO problems writing expression", ioe); } return extraData; } /** * Writes CQL for a function expression. Not currently supported. * * @param expression a function expression * * @throws UnsupportedOperationException every time, this isn't supported. */ public Object visit(Function expression, Object extraData) throws UnsupportedOperationException { String message = "Function expression support not yet added."; throw new UnsupportedOperationException(message); } public Object visit(NilExpression expression, Object extraData) { try { out.write(" "); } catch (java.io.IOException ioe) { throw new RuntimeException("IO problems writing expression", ioe); } return extraData; } /** * Sets the CQL name escape string. * * <p> * The value of this string is prefixed and appended to table schema names, * table names and column names in an CQL statement to support mixed-case * and non-English names. Without this, the DBMS may assume a mixed-case * name in the query should be treated as upper-case and an CQLCODE of * -204 or 206 may result if the name is not found. * </p> * * <p> * Typically this is the double-quote character, ", but may not be for all * databases. * </p> * * <p> * For example, consider the following query: * </p> * * <p> * SELECT Geom FROM Spear.ArchSites May be interpreted by the database as: * SELECT GEOM FROM SPEAR.ARCHSITES If the column and table names were * actually created using mixed-case, the query needs to be specified as: * SELECT "Geom" from "Spear"."ArchSites" * </p> * * @param escape the character to be used to escape database names */ public void setCQLNameEscape(String escape) { cqlNameEscape = escape; } /** * Surrounds a name with the CQL escape character. * * @param name * * @return DOCUMENT ME! */ public String escapeName(String name) { return cqlNameEscape + name + cqlNameEscape; } }