/*
* GeoTools - OpenSource mapping toolkit
* http://geotools.org
* (C) 2002-2006, GeoTools Project Managment Committee (PMC)
*
* 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.util.logging.Logger;
import org.geotools.filter.FilterType;
import com.vividsolutions.jts.geom.Geometry;
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 = Logger.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(FilterCapabilities.ALL);
capabilities.addType(FilterCapabilities.FID);
capabilities.addType(FilterCapabilities.NULL_CHECK);
capabilities.addType(FilterCapabilities.BETWEEN);
capabilities.addType(FilterCapabilities.LOGICAL);
capabilities.addType(FilterCapabilities.SIMPLE_ARITHMETIC);
capabilities.addType(FilterCapabilities.SIMPLE_COMPARISONS);
capabilities.addType(FilterCapabilities.SPATIAL_BBOX);
capabilities.addType(FilterCapabilities.LIKE);
if (supportsGEOS) {
capabilities.addType(FilterCapabilities.SPATIAL_CONTAINS);
capabilities.addType(FilterCapabilities.SPATIAL_CROSSES);
capabilities.addType(FilterCapabilities.SPATIAL_DISJOINT);
capabilities.addType(FilterCapabilities.SPATIAL_EQUALS);
capabilities.addType(FilterCapabilities.SPATIAL_INTERSECT);
capabilities.addType(FilterCapabilities.SPATIAL_OVERLAPS);
capabilities.addType(FilterCapabilities.SPATIAL_TOUCHES);
capabilities.addType(FilterCapabilities.SPATIAL_WITHIN);
}
// 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
* false then the BBOX query will perform a full intersects
* against the geometry, ensuring that it is exactly correct. If
* true 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 true 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.getLiteral();
String geomText = wkt.write(bbox);
out.write("GeometryFromText('" + geomText + "', " + srid + ")");
}
/**
* 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 filters and filters
* are handled. They will come here with "property = null".
* NOTE:
* SELECT * FROM WHERE isnull; -- postgresql
* SELECT * FROM WHERE isnull(); -- 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
{
left.accept(this);
out.write(" " + type + " ");
right.accept(this);
}
} catch (java.io.IOException ioe) {
throw new RuntimeException(IO_ERROR, ioe);
}
}
}