/*
* 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.filter;
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.util.Converters;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.filter.ExcludeFilter;
import org.opengis.filter.IncludeFilter;
import com.vividsolutions.jts.geom.Geometry;
/**
* Encodes a filter into a SQL WHERE statement. It should hopefully be generic
* enough that any SQL database will work with it, though it has only been
* tested with MySQL and Postgis. This generic SQL encoder should eventually
* be able to encode all filters except Geometry Filters (currently
* LikeFilters are not yet fully implemented, but when they are they should be
* generic enough). This is because the OGC's SFS for SQL document specifies
* two ways of doing SQL databases, one with native geometry types and one
* without. To implement an encoder for one of the two types simply subclass
* off of this encoder and put in the proper GeometryFilter visit method. Then
* add the filter types supported to the capabilities in the static
* capabilities.addType block.
*
* @author Chris Holmes, TOPP
*
* @deprecated Please use org.geotools.data.jdbc.FilterToSQL which uses
* opengis filters instead of these geotools filters.
*
* @task TODO: Implement LikeFilter encoding, need to figure out escape chars,
* the rest of the code should work right. Once fixed be sure to add
* the LIKE type to capabilities, so others know that they can be
* encoded.
* @task REVISIT: need to figure out exceptions, we're currently eating io
* errors, which is bad. Probably need a generic visitor exception.
* @source $URL$
* @deprecated scheduled for removal in 2.7, use classes in org.geotools.jdbc
*/
public class SQLEncoder implements org.geotools.filter.FilterVisitor2 {
/** 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 comparison types to sql representation */
protected static Map comparisions = new HashMap();
/** Map of spatial types to sql representation */
private static Map spatial = new HashMap();
/** Map of logical types to sql representation */
private static Map logical = new HashMap();
/** Map of expression types to sql representation */
private static Map expressions = new HashMap();
static {
comparisions.put(new Integer(AbstractFilter.COMPARE_EQUALS), "=");
comparisions.put(new Integer(AbstractFilter.COMPARE_NOT_EQUALS), "!=");
comparisions.put(new Integer(AbstractFilter.COMPARE_GREATER_THAN), ">");
comparisions.put(new Integer(AbstractFilter.COMPARE_GREATER_THAN_EQUAL),
">=");
comparisions.put(new Integer(AbstractFilter.COMPARE_LESS_THAN), "<");
comparisions.put(new Integer(AbstractFilter.COMPARE_LESS_THAN_EQUAL),
"<=");
comparisions.put(new Integer(AbstractFilter.LIKE), "LIKE");
comparisions.put(new Integer(AbstractFilter.NULL), "IS NULL");
comparisions.put(new Integer(AbstractFilter.BETWEEN), "BETWEEN");
expressions.put(new Integer(DefaultExpression.MATH_ADD), "+");
expressions.put(new Integer(DefaultExpression.MATH_DIVIDE), "/");
expressions.put(new Integer(DefaultExpression.MATH_MULTIPLY), "*");
expressions.put(new Integer(DefaultExpression.MATH_SUBTRACT), "-");
//more to come?
spatial.put(new Integer(AbstractFilter.GEOMETRY_EQUALS), "Equals");
spatial.put(new Integer(AbstractFilter.GEOMETRY_DISJOINT), "Disjoint");
spatial.put(new Integer(AbstractFilter.GEOMETRY_INTERSECTS),
"Intersects");
spatial.put(new Integer(AbstractFilter.GEOMETRY_TOUCHES), "Touches");
spatial.put(new Integer(AbstractFilter.GEOMETRY_CROSSES), "Crosses");
spatial.put(new Integer(AbstractFilter.GEOMETRY_WITHIN), "Within");
spatial.put(new Integer(AbstractFilter.GEOMETRY_CONTAINS), "Contains");
spatial.put(new Integer(AbstractFilter.GEOMETRY_OVERLAPS), "Overlaps");
spatial.put(new Integer(AbstractFilter.GEOMETRY_BEYOND), "Beyond");
spatial.put(new Integer(AbstractFilter.GEOMETRY_BBOX), "BBOX");
logical.put(new Integer(AbstractFilter.LOGIC_AND), "AND");
logical.put(new Integer(AbstractFilter.LOGIC_OR), "OR");
logical.put(new Integer(AbstractFilter.LOGIC_NOT), "NOT");
}
//use these when Like is implemented.
//The standard SQL multicharacter wild card.
//private static String SQL_WILD_MULTI = "%";
//The standard SQL single character wild card.
//private static String SQL_WILD_SINGLE = "_";
// The escaped version of the single wildcard for the REGEXP pattern.
//private static String escapedWildcardSingle = "\\.\\?";
// The escaped version of the multiple wildcard for the REGEXP pattern.
//private static String escapedWildcardMulti = "\\.\\*";
/** Character used to escape database schema, table and column names */
private String sqlNameEscape = "";
/** 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 sql for */
protected SimpleFeatureType featureType;
/**
* A type to use as context when encoding literal.
* NOTE: when we move to geoapi filter visitor api, this will not be needed.
*/
protected Class context = null;
/**
* Empty constructor
*/
public SQLEncoder() {
}
/**
* Sets the featuretype the encoder is encoding sql for.
* <p>
* This is used for context for attribute expressions when encoding to sql.
* </p>
*
* @param featureType
*/
public void setFeatureType(SimpleFeatureType featureType) {
this.featureType = featureType;
}
/**
* Convenience constructor to perform the whole encoding process at once.
*
* @param out the writer to encode the SQL to.
* @param filter the Filter to be encoded.
*
* @throws SQLEncoderException If there were problems encoding
*/
public SQLEncoder(Writer out, Filter filter) throws SQLEncoderException {
if (getCapabilities().fullySupports(filter)) {
this.out = out;
try {
out.write("WHERE ");
filter.accept(this);
//out.write(";"); this should probably be added by client.
} catch (java.io.IOException ioe) {
throw new SQLEncoderException("Problem writing filter: ", ioe);
}
} else {
throw new SQLEncoderException("Filter type not supported");
}
}
/**
* 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;
}
public FIDMapper getFIDMapper(){
return this.mapper;
}
/**
* Sets the capabilities of this filter.
*
* @return FilterCapabilities for this Filter
*/
protected FilterCapabilities createFilterCapabilities() {
FilterCapabilities capabilities = new FilterCapabilities();
capabilities.addType(FilterCapabilities.LOGICAL);
capabilities.addType(FilterCapabilities.SIMPLE_COMPARISONS);
capabilities.addType(FilterCapabilities.NULL_CHECK);
capabilities.addType(FilterCapabilities.BETWEEN);
capabilities.addType(FilterCapabilities.FID);
capabilities.addType(FilterCapabilities.NONE);
capabilities.addType(FilterCapabilities.ALL);
return capabilities;
}
/**
* Performs the encoding, sends the encoded sql to the writer passed in.
*
* @param out the writer to encode the SQL to.
* @param filter the Filter to be encoded.
*
* @throws SQLEncoderException If filter type not supported, or if there
* were io problems.
*/
public void encode(Writer out, org.opengis.filter.Filter filter) throws SQLEncoderException {
if (getCapabilities().fullySupports(filter)) {
this.out = out;
try {
out.write("WHERE ");
Filters.accept( filter, this );
} catch (java.io.IOException ioe) {
LOGGER.warning("Unable to export filter" + ioe);
throw new SQLEncoderException("Problem writing filter: ", ioe);
}
} else {
throw new SQLEncoderException("Filter type not supported");
}
}
/**
* Performs the encoding, returns a string of the encoded SQL.
*
* @param filter the Filter to be encoded.
*
* @return the string of the SQL where statement.
*
* @throws SQLEncoderException If filter type not supported, or if there
* were io problems.
*/
public String encode(org.opengis.filter.Filter filter) throws SQLEncoderException {
StringWriter output = new StringWriter();
encode(output, filter);
return output.getBuffer().toString();
}
public void encode(Writer out, org.opengis.filter.expression.Expression expression) throws SQLEncoderException {
this.out = out;
((DefaultExpression) expression).accept(this);
}
/**
* Performs the encoding, returns a string of the encoded SQL.
*
* @param expression the expression to be encoded.
*
* @return the correspondent SQL snippet
*
* @throws SQLEncoderException If expression type not supported, or if there
* were io problems.
*/
public String encode(org.opengis.filter.expression.Expression expression) throws SQLEncoderException {
StringWriter output = new StringWriter();
encode(output, expression);
return output.getBuffer().toString();
}
/**
* Describes the capabilities of this encoder.
*
* <p>
* Performs lazy creation of capabilities.
* </p>
*
* @return The capabilities supported by this encoder.
*/
public synchronized FilterCapabilities getCapabilities() {
if (capabilities == null) {
capabilities = createFilterCapabilities();
}
return capabilities; //maybe clone? Make immutable somehow
}
/**
* This should never be called. This can only happen if a subclass of
* AbstractFilter failes to implement its own version of
* accept(FilterVisitor);
*
* @param filter The filter to visit
*
* @throws RuntimeException for IO Encoding problems.
*
* @task REVISIT: I don't think Filter.INCLUDE and Filter.EXCLUDE should be
* handled here. They should have their own methods, but they don't
* have interfaces, so I don't know if that's possible.
*/
public void visit(Filter filter) {
try {
if (filter.getFilterType() == FilterType.NONE) {
out.write("TRUE");
} else if (filter.getFilterType() == FilterType.ALL) {
out.write("FALSE");
} else {
LOGGER.warning("exporting unknown filter type:"
+ filter.toString());
//throw new RuntimeException("Do not know how to export filter:"+filter.toString() );
}
} catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, ioe);
}
}
/**
* Writes the SQL for the Between Filter.
*
* @param filter the Filter to be visited.
*
* @throws RuntimeException for io exception with writer
*/
public void visit(BetweenFilter filter) throws RuntimeException {
LOGGER.finer("exporting BetweenFilter");
DefaultExpression left = (DefaultExpression) filter.getLeftValue();
DefaultExpression right = (DefaultExpression) filter.getRightValue();
DefaultExpression mid = (DefaultExpression) filter.getMiddleValue();
LOGGER.finer("Filter type id is " + filter.getFilterType());
LOGGER.finer("Filter type text is "
+ comparisions.get(new Integer(filter.getFilterType())));
try {
mid.accept(this);
out.write(" BETWEEN ");
left.accept(this);
out.write(" AND ");
right.accept(this);
} catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, ioe);
}
}
/**
* Writes the SQL for the Like Filter. Assumes the current java
* implemented wildcards for the Like Filter: . for multi and .? for
* single. And replaces them with the SQL % and _, respectively. Currently
* does nothing, and should not be called, not included in the
* capabilities.
*
* @param filter the Like Filter to be visited.
*
* @throws UnsupportedOperationException always, as likes aren't
* implemented yet.
*
* @task REVISIT: Need to think through the escape char, so it works right
* when Java uses one, and escapes correctly with an '_'.
*/
public void visit(LikeFilter filter) throws UnsupportedOperationException
{
char esc = filter.getEscape().charAt(0);
char multi = filter.getWildcardMulti().charAt(0);
char single = filter.getWildcardSingle().charAt(0);
boolean matchCase = ((LikeFilterImpl)filter).isMatchingCase();
String pattern = LikeFilterImpl.convertToSQL92(esc, multi, single, matchCase,
filter.getPattern());
DefaultExpression att = (DefaultExpression) filter.getValue();
try {
if (!matchCase){
out.write(" UPPER(");
}
att.accept(this);
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);
}
}
/**
* Writes the SQL for the Logic Filter.
*
* @param filter the logic statement to be turned into SQL.
*
* @throws RuntimeException for io exception with writer
*/
public void visit(LogicFilter filter) throws RuntimeException {
LOGGER.finer("exporting LogicFilter");
filter.getFilterType();
String type = (String) logical.get(new Integer(filter.getFilterType()));
try {
java.util.Iterator list = filter.getFilterIterator();
if (filter.getFilterType() == AbstractFilter.LOGIC_NOT) {
out.write(" NOT (");
Filters.accept((org.opengis.filter.Filter) list.next(),this);
out.write(")");
} else { //AND or OR
out.write("(");
while (list.hasNext()) {
Filters.accept((org.opengis.filter.Filter) list.next(),this);
if (list.hasNext()) {
out.write(" " + type + " ");
}
}
out.write(")");
}
} catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, ioe);
}
}
/**
* Writes the SQL for a Compare Filter.
*
* 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; -- postgresql
* SELECT * FROM <table> WHERE isnull(<column>); -- oracle???
*
* @param filter the comparison to be turned into SQL.
*
* @throws RuntimeException for io exception with writer
*/
public void visit(CompareFilter filter) throws RuntimeException {
LOGGER.finer("exporting SQL ComparisonFilter");
DefaultExpression left = (DefaultExpression) filter.getLeftValue();
DefaultExpression right = (DefaultExpression) filter.getRightValue();
LOGGER.finer("Filter type id is " + filter.getFilterType());
LOGGER.finer("Filter type text is "
+ comparisions.get(new Integer(filter.getFilterType())));
String type = (String) comparisions.get(new Integer(
filter.getFilterType()));
try {
left.accept(this);
out.write(" " + type + " ");
right.accept(this);
} catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, ioe);
}
}
/**
* Writes the SQL for the Null Filter.
*
* @param filter the null filter to be written to SQL.
*
* @throws RuntimeException for io exception with writer
*/
public void visit(NullFilter filter) throws RuntimeException {
LOGGER.finer("exporting NullFilter");
DefaultExpression expr = (DefaultExpression) filter.getNullCheckValue();
//String type = (String) comparisions.get(new Integer(
// filter.getFilterType()));
try {
expr.accept(this);
out.write(" IS NULL ");
} catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, ioe);
}
}
/**
* Encodes an FidFilter.
*
* @param filter
*
* @throws RuntimeException DOCUMENT ME!
*
* @see org.geotools.filter.SQLEncoder#visit(org.geotools.filter.FidFilter)
*/
public void visit(FidFilter filter) {
if (mapper == null) {
throw new RuntimeException(
"Must set a fid mapper before trying to encode FIDFilters");
}
String[] fids = filter.getFids();
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]);
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);
}
}
}
/**
* Writes the SQL for the attribute Expression.
*
* @param expression the attribute to turn to SQL.
*
* @throws RuntimeException for io exception with writer
*/
public void visit(AttributeExpression expression) throws RuntimeException {
LOGGER.finer("exporting ExpressionAttribute");
try {
//JD: evaluate the expression agains the feature type to get at the attribute, then
// encode the namee
context = null;
if ( featureType != null ) {
AttributeDescriptor attributeType = (AttributeDescriptor) expression.evaluate( featureType );
if ( attributeType != null ) {
out.write( escapeName( attributeType.getLocalName() ) );
//provide context for a literal being compared to this attribute
context = attributeType.getType().getBinding();
return;
}
}
//if thigns are sane, we should get here
out.write(escapeName(expression.getAttributePath()));
} catch (java.io.IOException ioe) {
throw new RuntimeException("IO problems writing attribute exp", ioe);
}
}
/**
* Writes the SQL for the attribute Expression.
*
* @param expression the attribute to turn to SQL.
*/
public void visit(Expression expression) {
LOGGER.warning("exporting unknown (default) expression");
}
/**
* Export the contents of a Literal Expresion
*
* @param expression the Literal to export
*
* @throws RuntimeException for io exception with writer
*/
public void visit(LiteralExpression expression) throws RuntimeException {
LOGGER.finer("exporting LiteralExpression");
//type to convert the literal to
Class target = null;
if ( context != null ) {
//first try to evaluate the expression in the context of a type
target = (Class) context;
}
if ( target == null ) {
//next try and use the filter code
short type = expression.getType();
switch (type) {
case Expression.LITERAL_DOUBLE:
target = Double.class;
break;
case Expression.LITERAL_INTEGER:
target = Integer.class;
break;
case Expression.LITERAL_LONG:
target = Long.class;
break;
case Expression.LITERAL_STRING:
target = String.class;
break;
case Expression.LITERAL_GEOMETRY:
target = Geometry.class;
break;
default:
throw new RuntimeException("type: " + type + "not supported");
}
}
try {
if ( target == Geometry.class && expression.getLiteral() instanceof Geometry ) {
//call this method for backwards compatability with subclasses
visitLiteralGeometry( expression );
return;
}
else {
//convert the literal to the required type
//JD except for numerics, let the database do teh converstion
Object literal = null;
if ( Number.class.isAssignableFrom( target ) ) {
//dont convert
}
else {
//convert
literal = expression.evaluate( null, target );
}
if ( literal == null ) {
//just use string
literal = expression.getLiteral().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 sql string
String escaped = literal.toString().replaceAll("'", "''");
out.write( "'" + escaped + "'" );
return;
}
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.getLiteral().toString();
}
// sigle quotes must be escaped to have a valid sql string
out.write( "'" + encoding.replaceAll("'", "''") + "'");
}
}
} catch (IOException e) {
throw new RuntimeException("IO problems writing literal", e);
}
}
/**
* 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(LiteralExpression expression)
throws IOException {
throw new RuntimeException(
"Subclasses must implement this method in order to handle geometries");
}
/**
* @see org.geotools.filter.FilterVisitor#visit(org.geotools.filter.GeometryFilter)
*/
public void visit(GeometryFilter filter) {
throw new RuntimeException(
"Subclasses must implement this method in order to handle geometries");
}
/**
* Writes the SQL for the Math Expression.
*
* @param expression the Math phrase to be written.
*
* @throws RuntimeException for io problems
*/
public void visit(MathExpression expression) throws RuntimeException {
LOGGER.finer("exporting Expression Math");
String type = (String) expressions.get(new Integer(expression.getType()));
try {
((DefaultExpression) expression.getLeftValue()).accept(this);
out.write(" " + type + " ");
((DefaultExpression) expression.getRightValue()).accept(this);
} catch (java.io.IOException ioe) {
throw new RuntimeException("IO problems writing expression", ioe);
}
}
/**
* Writes sql for a function expression. Not currently supported.
*
* @param expression a function expression
*
* @throws UnsupportedOperationException every time, this isn't supported.
*/
public void visit(FunctionExpression expression)
throws UnsupportedOperationException {
String message = "Function expression support not yet added.";
throw new UnsupportedOperationException(message);
}
/**
* Sets the SQL name escape string.
*
* <p>
* The value of this string is prefixed and appended to table schema names,
* table names and column names in an SQL 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 SQLCODE 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 setSqlNameEscape(String escape) {
sqlNameEscape = escape;
}
/**
* Sets the escape character for the column name.
*
* @param escape The character to be used to escape database names.
*
* @deprecated Use setSqlNameEscape instead, as it is more aptly named.
*/
public void setColnameEscape(String escape) {
sqlNameEscape = escape;
}
/**
* Gets the column escape name.
*
* @return the string to be used to properly escape a db's name.
*
* @deprecated the escapeName method is preferred over this, it
* automatically returns the name properly escaped, since
* that's all getColnameEscape was being used for.
*/
protected String getColnameEscape() {
return sqlNameEscape;
}
/**
* Surrounds a name with the SQL escape character.
*
* @param name
*
* @return DOCUMENT ME!
*/
public String escapeName(String name) {
return sqlNameEscape + name + sqlNameEscape;
}
public void visit(IncludeFilter filter) {
try {
out.write("TRUE");
} catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, ioe);
}
}
public void visit(ExcludeFilter filter) {
try {
out.write("FALSE");
} catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, ioe);
}
}
}