/*
* 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.sql.Date;
import java.util.logging.Logger;
import org.geotools.filter.FilterType;
import org.opengis.filter.ExcludeFilter;
import org.opengis.filter.Id;
import org.opengis.filter.IncludeFilter;
import org.opengis.filter.PropertyIsBetween;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.expression.Add;
import org.opengis.filter.expression.Divide;
import org.opengis.filter.expression.Multiply;
import org.opengis.filter.expression.Subtract;
import org.opengis.filter.spatial.BBOX;
import org.opengis.filter.spatial.Contains;
import org.opengis.filter.spatial.Crosses;
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.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.io.WKTWriter;
/**
* Encodes a filter into a SQL WHERE statement for postgis. This class adds
* the ability to turn geometry filters into sql statements if they are
* bboxes.
*
* @author Chris Holmes, TOPP
*
* @task TODO: integrated with SQLEncoderPostgisGeos.java, as there no real
* reason to have two different classes. We just need to do testing to
* make sure both handle everything. At the very least have the geos
* one extend more intelligently.
* @source $URL$
*/
public class SQLEncoderPostgis extends SQLEncoder implements
org.geotools.filter.FilterVisitor {
/** Standard java logger */
private static Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.filter");
/** To write geometry so postgis can read it. */
private static WKTWriter wkt = new WKTWriter();
/**
* The srid of the schema, so the bbox conforms. Could be better to have
* it in the bbox filter itself, but this works for now.
*/
private int srid;
/** The geometry attribute to use if none is specified. */
private String defaultGeom;
/** Whether the BBOX filter should be strict (using the exact geom), or
* loose (using the envelopes) */
protected boolean looseBbox = false;
/**
* Whether the installed PostGIS has GEOS support. Default is false for
* backwards compatibility.
*/
protected boolean supportsGEOS = false;
/**
* Empty constructor TODO: rethink empty constructor, as BBOXes _need_ an
* SRID, must make client set it somehow. Maybe detect when encode is
* called?
*
*/
public SQLEncoderPostgis() {
capabilities = createFilterCapabilities();
setSqlNameEscape("\"");
}
public SQLEncoderPostgis(boolean looseBbox) {
this();
this.looseBbox = looseBbox;
}
/**
*
* @see org.geotools.filter.SQLEncoder#createFilterCapabilities()
*/
protected FilterCapabilities createFilterCapabilities() {
FilterCapabilities capabilities = new FilterCapabilities();
capabilities.addType(FilterCapabilities.NONE);
capabilities.addType(IncludeFilter.class);
capabilities.addType(FilterCapabilities.ALL);
capabilities.addType(ExcludeFilter.class);
capabilities.addType(FilterCapabilities.FID);
capabilities.addType(Id.class);
capabilities.addType(FilterCapabilities.NULL_CHECK);
capabilities.addType(PropertyIsNull.class);
capabilities.addType(FilterCapabilities.BETWEEN);
capabilities.addType(PropertyIsBetween.class);
capabilities.addType(FilterCapabilities.LOGICAL);
capabilities.addAll(FilterCapabilities.LOGICAL_OPENGIS);
capabilities.addType(FilterCapabilities.SIMPLE_ARITHMETIC);
capabilities.addType(Add.class);
capabilities.addType(Multiply.class);
capabilities.addType(Subtract.class);
capabilities.addType(Divide.class);
capabilities.addType(FilterCapabilities.SIMPLE_COMPARISONS);
capabilities.addAll(FilterCapabilities.SIMPLE_COMPARISONS_OPENGIS);
capabilities.addType(FilterCapabilities.SPATIAL_BBOX);
capabilities.addType(BBOX.class);
capabilities.addType(FilterCapabilities.LIKE);
capabilities.addType(PropertyIsLike.class);
if (supportsGEOS) {
capabilities.addType(FilterCapabilities.SPATIAL_CONTAINS);
capabilities.addType(Contains.class);
capabilities.addType(FilterCapabilities.SPATIAL_CROSSES);
capabilities.addType(Crosses.class);
capabilities.addType(FilterCapabilities.SPATIAL_DISJOINT);
capabilities.addType(Disjoint.class);
capabilities.addType(FilterCapabilities.SPATIAL_EQUALS);
capabilities.addType(Equals.class);
capabilities.addType(FilterCapabilities.SPATIAL_INTERSECT);
capabilities.addType(Intersects.class);
capabilities.addType(FilterCapabilities.SPATIAL_OVERLAPS);
capabilities.addType(Overlaps.class);
capabilities.addType(FilterCapabilities.SPATIAL_TOUCHES);
capabilities.addType(Touches.class);
capabilities.addType(FilterCapabilities.SPATIAL_WITHIN);
capabilities.addType(Within.class);
}
// TODO: add SPATIAL_BEYOND, DWITHIN to capabilities and support in
// visit(GeometryFilter)
return capabilities;
}
/**
* Constructor with srid.
*
* @param srid spatial reference id to encode geometries with.
*/
public SQLEncoderPostgis(int srid) {
this(true);
this.srid = srid;
}
/**
* Sets whether the Filter.BBOX query should be 'loose', meaning that it
* should just doing a bounding box against the envelope. If set to
* <tt>false</tt> then the BBOX query will perform a full intersects
* against the geometry, ensuring that it is exactly correct. If
* <tt>true</tt> then the query will likely perform faster, but may not
* be exactly correct.
*
* @param isLooseBbox whether the bbox should be loose or strict.
*/
public void setLooseBbox(boolean isLooseBbox) {
this.looseBbox = isLooseBbox;
}
/**
* Gets whether the Filter.BBOX query will be strict and use an intersects
* or 'loose' and just operate against the geometry envelopes.
*
* @return <tt>true</tt> if this encoder is going to do loose filtering.
*/
public boolean isLooseBbox() {
return looseBbox;
}
/**
* Sets a spatial reference system ESPG number, so that the geometry can be
* properly encoded for postgis. If geotools starts actually creating
* geometries with valid srids then this method will no longer be needed.
*
* @param srid the integer code for the EPSG spatial reference system.
*/
public void setSRID(int srid) {
this.srid = srid;
}
/**
* Sets the default geometry, so that filters with null for one of their
* expressions can assume that the default geometry is intended.
*
* @param name the name of the default geometry Attribute.
*
* @task REVISIT: pass in a featureType so that geometries can figure out
* their own default geometry?
*/
public void setDefaultGeometry(String name) {
//Do we really want clients to be using malformed filters?
//I mean, this is a useful method for unit tests, but shouldn't
//fully formed filters usually be used? Though I guess adding
//the option wouldn't hurt. -ch
this.defaultGeom = name;
}
public void setSupportsGEOS(boolean supports) {
boolean oldValue = this.supportsGEOS;
this.supportsGEOS = supports;
if (capabilities == null || supports != oldValue) {
//regenerate capabilities
capabilities = createFilterCapabilities();
}
}
public boolean getSupportsGEOS() {
return supportsGEOS;
}
private void encodeGeomFilter(GeometryFilter filter, String function, String comparison, boolean useIndex) {
//this method blindly assumes that the filter is supported
DefaultExpression left = (DefaultExpression) filter.getLeftGeometry();
DefaultExpression right = (DefaultExpression) filter.getRightGeometry();
try {
//should we use the index?
if (useIndex) {
encodeExpression(left);
out.write(" && ");
encodeExpression(right);
}
// looseBbox only applies to GEOMETRY_BBOX, so unless this is a
// BBOX, we will always generate the full SQL.
if (filter.getFilterType() != AbstractFilter.GEOMETRY_BBOX || !looseBbox) {
if (useIndex) {
out.write(" AND ");
}
out.write(function + "(");
encodeExpression(left);
out.write(", ");
encodeExpression(right);
out.write(")" + comparison);
}
} catch (java.io.IOException ioe) {
LOGGER.warning("Unable to export filter" + ioe);
}
}
private void encodeExpression(DefaultExpression expr) throws IOException {
if (expr == null) {
out.write("\"" + defaultGeom + "\"");
} else {
expr.accept(this);
}
}
/**
* Turns a geometry filter into the postgis sql bbox statement.
*
* @param filter the geometry filter to be encoded.
*
* @throws RuntimeException for IO exception (need a better error)
*/
public void visit(GeometryFilter filter) throws RuntimeException {
LOGGER.finer("exporting GeometryFilter");
short filterType = filter.getFilterType();
DefaultExpression left = (DefaultExpression) filter.getLeftGeometry();
DefaultExpression right = (DefaultExpression) filter.getRightGeometry();
//if geos is not supported, all we can use is distance = 0 for bbox
if (!supportsGEOS) {
if (filterType != AbstractFilter.GEOMETRY_BBOX) {
throw new RuntimeException(
"without GEOS support, only the BBOX function is supported; failed to encode "
+ filterType);
}
encodeGeomFilter(filter, "distance", " < 0.00001", true);
return;
}
// Figure out if we need to constrain this query with the && constraint.
int literalGeometryCount = 0;
if ((left != null)
&& (left.getType() == DefaultExpression.LITERAL_GEOMETRY)) {
literalGeometryCount++;
}
if ((right != null)
&& (right.getType() == DefaultExpression.LITERAL_GEOMETRY)) {
literalGeometryCount++;
}
boolean constrainBBOX = (literalGeometryCount == 1);
boolean onlyBbox = filterType == AbstractFilter.GEOMETRY_BBOX
&& looseBbox;
try {
// DJB: disjoint is not correctly handled in the pre-march 22/05
// version
// I changed it to not do a "&&" index search for disjoint because
// Geom1 and Geom2 can have a bbox overlap and be disjoint
// I also added test case.
// NOTE: this will not use the index, but its unlikely that using
// the index
// for a disjoint query will be the correct thing to do.
// DJB NOTE: need to check for a NOT(A intersects G) filter
// --> NOT( (A && G) AND intersects(A,G))
// and check that it does the right thing.
constrainBBOX = constrainBBOX
&& (filterType != AbstractFilter.GEOMETRY_DISJOINT);
if (constrainBBOX) {
encodeExpression(left);
out.write(" && ");
encodeExpression(right);
if (!onlyBbox) {
out.write(" AND ");
}
}
String closingParenthesis = ")";
if (!onlyBbox) {
if (filterType == AbstractFilter.GEOMETRY_EQUALS) {
out.write("equals");
} else if (filterType == AbstractFilter.GEOMETRY_DISJOINT) {
out.write("NOT (intersects");
closingParenthesis += ")";
} else if (filterType == AbstractFilter.GEOMETRY_INTERSECTS) {
out.write("intersects");
} else if (filterType == AbstractFilter.GEOMETRY_CROSSES) {
out.write("crosses");
} else if (filterType == AbstractFilter.GEOMETRY_WITHIN) {
out.write("within");
} else if (filterType == AbstractFilter.GEOMETRY_CONTAINS) {
out.write("contains");
} else if (filterType == AbstractFilter.GEOMETRY_OVERLAPS) {
out.write("overlaps");
} else if (filterType == AbstractFilter.GEOMETRY_BBOX) {
out.write("intersects");
} else if (filterType == AbstractFilter.GEOMETRY_TOUCHES) {
out.write("touches");
} else {
// this will choke on beyond and dwithin
throw new RuntimeException("does not support filter type "
+ filterType);
}
out.write("(");
encodeExpression(left);
out.write(", ");
encodeExpression(right);
out.write(closingParenthesis);
}
} catch (java.io.IOException ioe) {
LOGGER.warning("Unable to export filter" + ioe);
throw new RuntimeException("io error while writing", ioe);
}
}
/**
* Checks to see if the literal is a geometry, and encodes it if it is, if
* not just sends to the parent class.
*
* @param expression
* the expression to visit and encode.
*
* @throws IOException
* for IO exception (need a better error)
*/
public void visitLiteralGeometry(LiteralExpression expression)
throws IOException {
Geometry bbox = (Geometry) expression.evaluate( null, Geometry.class );
String geomText = null;
if ( bbox instanceof LinearRing ) {
//postgis does not handle linear rings, convert to just a line string
LineString lineString = new LineString(
((LinearRing)bbox).getCoordinateSequence(), bbox.getFactory()
);
geomText = wkt.write( lineString );
}
else {
geomText = wkt.write(bbox);
}
out.write("GeometryFromText('" + geomText + "', " + srid + ")");
}
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 = filter.isMatchingCase();
String pattern = LikeFilterImpl.convertToSQL92(esc, multi, single, matchCase,
filter.getPattern());
DefaultExpression att = (DefaultExpression) filter.getValue();
try {
out.write( " ( " );
if (!matchCase) {
out.write("UPPER( ");
}
att.accept(this);
if (!matchCase) {
out.write(" ) LIKE '");
} else {
out.write(" LIKE '");
}
out.write(pattern);
out.write("' ");
//JD: this is an ugly ugly hack!! hopefully when the new feature model is around we can
// fix this
//check for context for a date
if ( att instanceof AttributeExpression && context != null
&& java.util.Date.class.isAssignableFrom( context ) ) {
//if it is a date, add additional logic for a timestamp, or a timestamp with
// timezone
out.write( " OR " );
if (!matchCase) {
out.write("UPPER( ");
}
att.accept( this );
if (!matchCase) {
out.write(" ) LIKE '");
} else {
out.write(" LIKE '");
}
out.write(pattern + " __:__:__'" ); //timestamp
out.write( " OR " );
if (!matchCase) {
out.write("UPPER( ");
}
att.accept( this );
if (!matchCase) {
out.write(" ) LIKE '");
} else {
out.write(" LIKE '");
}
out.write(pattern + " __:__:_____'" ); //timestamp with time zone
}
out.write( " ) " );
} catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, ioe);
}
}
/**
* Checks to see if the literal is a geometry, and encodes it if it is, if
* not just sends to the parent class.
*
* @param expression the expression to visit and encode.
*
* @throws RuntimeException for IO exception (need a better error)
*/
public void visit(LiteralExpression expression) throws RuntimeException {
LOGGER.finer("exporting LiteralExpression");
try {
if (expression.getType() == DefaultExpression.LITERAL_GEOMETRY) {
visitLiteralGeometry(expression);
} else {
super.visit(expression);
}
} catch (java.io.IOException ioe) {
LOGGER.warning("Unable to export expression" + ioe);
throw new RuntimeException("io error while writing", 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 {
// a bit hacky, but what else can we really do?
if ( (right == null) && (filter.getFilterType()==FilterType.COMPARE_EQUALS ) )
{
left.accept(this);
out.write(" isnull");
}
else
{
//check for case insentivity (TODO: perhaps move this up to jdbc)
if ( !filter.isMatchingCase() ) {
//only for == or !=
if ( filter.getFilterType() == Filter.COMPARE_EQUALS ||
filter.getFilterType() == Filter.COMPARE_NOT_EQUALS ) {
//only for strings
if ( left.getType() == Expression.LITERAL_STRING
|| right.getType() == Expression.LITERAL_STRING ) {
out.write( "lower(" ); left.accept( this ); out.write( ")");
out.write( " " + type + " " );
out.write( "lower(" ); right.accept( this ); out.write( ")");
return;
}
}
}
//normal execution
left.accept(this);
out.write(" " + type + " ");
right.accept(this);
}
} catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, ioe);
}
}
}