/* * 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. *
* This is used for context for attribute expressions when encoding to cql. *
* * @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. * ** Performs lazy creation of capabilities. *
* * 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