/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2009, 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.data.postgis;
import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import org.geotools.data.jdbc.FilterToSQL;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.filter.FilterCapabilities;
import org.geotools.filter.function.FilterFunction_strConcat;
import org.geotools.filter.function.FilterFunction_strEndsWith;
import org.geotools.filter.function.FilterFunction_strEqualsIgnoreCase;
import org.geotools.filter.function.FilterFunction_strIndexOf;
import org.geotools.filter.function.FilterFunction_strLength;
import org.geotools.filter.function.FilterFunction_strReplace;
import org.geotools.filter.function.FilterFunction_strStartsWith;
import org.geotools.filter.function.FilterFunction_strSubstring;
import org.geotools.filter.function.FilterFunction_strSubstringStart;
import org.geotools.filter.function.FilterFunction_strToLowerCase;
import org.geotools.filter.function.FilterFunction_strToUpperCase;
import org.geotools.filter.function.FilterFunction_strTrim;
import org.geotools.filter.function.FilterFunction_strTrim2;
import org.geotools.filter.function.math.FilterFunction_abs;
import org.geotools.filter.function.math.FilterFunction_abs_2;
import org.geotools.filter.function.math.FilterFunction_abs_3;
import org.geotools.filter.function.math.FilterFunction_abs_4;
import org.geotools.filter.function.math.FilterFunction_ceil;
import org.geotools.filter.function.math.FilterFunction_floor;
import org.geotools.geometry.jts.JTS;
import org.geotools.jdbc.JDBCDataStore;
import org.geotools.jdbc.SQLDialect;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Function;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;
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.DistanceBufferOperator;
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.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryComponentFilter;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;
class FilterToSqlHelper {
protected static final String IO_ERROR = "io problem writing filter";
private static final Envelope WORLD = new Envelope(-180, 180, -90, 90);
FilterToSQL delegate;
Writer out;
boolean looseBBOXEnabled;
public FilterToSqlHelper(FilterToSQL delegate) {
this.delegate = delegate;
}
public static FilterCapabilities createFilterCapabilities(boolean encodeFunctions) {
FilterCapabilities caps = new FilterCapabilities();
caps.addAll(SQLDialect.BASE_DBMS_CAPABILITIES);
// adding the spatial filters support
caps.addType(BBOX.class);
caps.addType(Contains.class);
caps.addType(Crosses.class);
caps.addType(Disjoint.class);
caps.addType(Equals.class);
caps.addType(Intersects.class);
caps.addType(Overlaps.class);
caps.addType(Touches.class);
caps.addType(Within.class);
caps.addType(DWithin.class);
caps.addType(Beyond.class);
if(encodeFunctions) {
// add support for string functions
caps.addType(FilterFunction_strConcat.class);
caps.addType(FilterFunction_strEndsWith.class);
caps.addType(FilterFunction_strStartsWith.class);
caps.addType(FilterFunction_strEqualsIgnoreCase.class);
caps.addType(FilterFunction_strIndexOf.class);
caps.addType(FilterFunction_strLength.class);
caps.addType(FilterFunction_strToLowerCase.class);
caps.addType(FilterFunction_strToUpperCase.class);
caps.addType(FilterFunction_strReplace.class);
caps.addType(FilterFunction_strSubstring.class);
caps.addType(FilterFunction_strSubstringStart.class);
caps.addType(FilterFunction_strTrim.class);
caps.addType(FilterFunction_strTrim2.class);
// add support for math functions
caps.addType(FilterFunction_abs.class);
caps.addType(FilterFunction_abs_2.class);
caps.addType(FilterFunction_abs_3.class);
caps.addType(FilterFunction_abs_4.class);
caps.addType(FilterFunction_ceil.class);
caps.addType(FilterFunction_floor.class);
}
return caps;
}
protected Object visitBinarySpatialOperator(BinarySpatialOperator filter,
PropertyName property, Literal geometry, boolean swapped,
Object extraData) {
try {
if (filter instanceof DistanceBufferOperator) {
visitDistanceSpatialOperator((DistanceBufferOperator) filter,
property, geometry, swapped, extraData);
} else {
visitComparisonSpatialOperator(filter, property, geometry,
swapped, extraData);
}
} catch (IOException e) {
throw new RuntimeException(IO_ERROR, e);
}
return extraData;
}
void visitDistanceSpatialOperator(DistanceBufferOperator filter,
PropertyName property, Literal geometry, boolean swapped,
Object extraData) throws IOException {
if ((filter instanceof DWithin && !swapped)
|| (filter instanceof Beyond && swapped)) {
out.write("ST_DWithin(");
property.accept(delegate, extraData);
out.write(",");
geometry.accept(delegate, extraData);
out.write(",");
out.write(Double.toString(filter.getDistance()));
out.write(")");
}
if ((filter instanceof DWithin && swapped)
|| (filter instanceof Beyond && !swapped)) {
out.write("ST_Distance(");
property.accept(delegate, extraData);
out.write(",");
geometry.accept(delegate, extraData);
out.write(") > ");
out.write(Double.toString(filter.getDistance()));
}
}
void visitComparisonSpatialOperator(BinarySpatialOperator filter,
PropertyName property, Literal geometry, boolean swapped, Object extraData)
throws IOException {
// if geography case, sanitize geometry first
if(isCurrentGeography()) {
geometry = clipToWorld(geometry);
if(isWorld(geometry)) {
// nothing to filter in this case
out.write(" TRUE ");
return;
} else if(isEmpty(geometry)) {
if(!(filter instanceof Disjoint)) {
out.write(" FALSE ");
} else {
out.write(" TRUE ");
}
return;
}
}
// add && filter if possible
if(!(filter instanceof Disjoint)) {
property.accept(delegate, extraData);
out.write(" && ");
geometry.accept(delegate, extraData);
// if we're just encoding a bbox in loose mode, we're done
if(filter instanceof BBOX && looseBBOXEnabled)
return;
out.write(" AND ");
}
String closingParenthesis = ")";
if (filter instanceof Equals) {
out.write("ST_Equals");
} else if (filter instanceof Disjoint) {
out.write("NOT (ST_Intersects");
closingParenthesis += ")";
} else if (filter instanceof Intersects || filter instanceof BBOX) {
out.write("ST_Intersects");
} else if (filter instanceof Crosses) {
out.write("ST_Crosses");
} else if (filter instanceof Within) {
if(swapped)
out.write("ST_Contains");
else
out.write("ST_Within");
} else if (filter instanceof Contains) {
if(swapped)
out.write("ST_Within");
else
out.write("ST_Contains");
} else if (filter instanceof Overlaps) {
out.write("ST_Overlaps");
} else if (filter instanceof Touches) {
out.write("ST_Touches");
} else {
throw new RuntimeException("Unsupported filter type " + filter.getClass());
}
out.write("(");
property.accept(delegate, extraData);
out.write(", ");
geometry.accept(delegate, extraData);
out.write(closingParenthesis);
}
boolean isCurrentGeography() {
AttributeDescriptor geom = null;
if(delegate instanceof PostgisPSFilterToSql) {
geom = ((PostgisPSFilterToSql) delegate).getCurrentGeometry();
} else if(delegate instanceof PostgisFilterToSQL) {
geom = ((PostgisFilterToSQL) delegate).getCurrentGeometry();
}
return geom != null &&
"geography".equals(geom.getUserData().get(JDBCDataStore.JDBC_NATIVE_TYPENAME));
}
private Literal clipToWorld(Literal geometry) {
if(geometry != null) {
Geometry g = geometry.evaluate(null, Geometry.class);
if(g != null) {
Envelope env = g.getEnvelopeInternal();
// first, limit to world
if(!WORLD.contains(env)) {
g = sanitizePolygons(g.intersection(JTS.toGeometry(WORLD)));
}
// second, postgis will always use the shortest distance between two
// points, if an arc is longer than 180 degrees the opposite will
// be used instead, so we have to slice the geometry in parts
env = g.getEnvelopeInternal();
if(Math.sqrt(env.getWidth() * env.getWidth() + env.getHeight() * env.getHeight()) >= 180) {
// slice in 90x90 degrees quadrants, none of them has a diagonal longer than 180
final List<Polygon> polygons = new ArrayList<Polygon>();
for(double lon = Math.floor(env.getMinX()); lon < env.getMaxX(); lon+= 90) {
for (double lat = Math.floor(env.getMinY()); lat < env.getMaxY(); lat += 90) {
Geometry quadrant = JTS.toGeometry(new Envelope(lon, lon + 90, lat, lat + 90));
Geometry cut = sanitizePolygons(g.intersection(quadrant));
if(!cut.isEmpty()) {
if(cut instanceof Polygon) {
polygons.add((Polygon) cut);
} else {
for (int i = 0; i < cut.getNumGeometries(); i++) {
polygons.add((Polygon) cut.getGeometryN(i));
}
}
}
}
}
g = toPolygon(g.getFactory(), polygons);
}
geometry = CommonFactoryFinder.getFilterFactory(null).literal(g);
}
}
return geometry;
}
/**
* Given a geometry that might contain heterogeneous components extracts only the polygonal ones
* @param geometry
* @return
*/
private Geometry sanitizePolygons(Geometry geometry) {
// already sane?
if(geometry == null || geometry instanceof Polygon || geometry instanceof MultiPolygon) {
return geometry;
}
// filter out only polygonal parts
final List<Polygon> polygons = new ArrayList<Polygon>();
geometry.apply(new GeometryComponentFilter() {
public void filter(Geometry geom) {
if(geom instanceof Polygon) {
polygons.add((Polygon) geom);
}
}
});
// turn filtered selection into a geometry
return toPolygon(geometry.getFactory(), polygons);
}
private Geometry toPolygon(GeometryFactory gf, final List<Polygon> polygons) {
if(polygons.size() == 0) {
return gf.createGeometryCollection(null);
} else if(polygons.size() == 1) {
return polygons.get(0);
} else {
return gf.createMultiPolygon((Polygon[]) polygons.toArray(new Polygon[polygons.size()]));
}
}
/**
* Returns true if the geometry covers the entire world
* @param geometry
* @return
*/
private boolean isWorld(Literal geometry) {
if(geometry != null) {
Geometry g = geometry.evaluate(null, Geometry.class);
if(g != null) {
return JTS.toGeometry(WORLD).equalsTopo(g.union());
}
}
return false;
}
/**
* Returns true if the geometry is fully empty
* @param geometry
* @return
*/
private boolean isEmpty(Literal geometry) {
if(geometry != null) {
Geometry g = geometry.evaluate(null, Geometry.class);
return g == null || g.isEmpty();
}
return false;
}
/**
* Maps a function to its native db equivalent
*
* @param function
* @return
*/
public String getFunctionName(Function function) {
if(function instanceof FilterFunction_strLength) {
return "char_length";
} else if(function instanceof FilterFunction_strToLowerCase) {
return "lower";
} else if(function instanceof FilterFunction_strToUpperCase) {
return "upper";
} else if(function instanceof FilterFunction_abs ||
function instanceof FilterFunction_abs_2 ||
function instanceof FilterFunction_abs_3 ||
function instanceof FilterFunction_abs_4) {
return "abs";
}
return function.getName();
}
/**
* Performs custom visits for functions that cannot be encoded
* as <code>functionName(p1, p2, ... pN).</code>
* @param function
* @param extraData
* @return
*/
public boolean visitFunction(Function function, Object extraData) throws IOException {
if(function instanceof FilterFunction_strConcat) {
Expression s1 = getParameter(function, 0, true);
Expression s2 = getParameter(function, 1, true);
out.write("(");
s1.accept(delegate, String.class);
out.write(" || ");
s2.accept(delegate, String.class);
out.write(")");
} else if(function instanceof FilterFunction_strEndsWith) {
Expression str = getParameter(function, 0, true);
Expression end = getParameter(function, 1, true);
out.write("(");
str.accept(delegate, String.class);
out.write(" LIKE ");
if(end instanceof Literal) {
out.write("'%" + end.evaluate(null, String.class) + "'");
} else {
out.write("('%' || ");
end.accept(delegate, String.class);
out.write(")");
}
out.write(")");
} else if(function instanceof FilterFunction_strStartsWith) {
Expression str = getParameter(function, 0, true);
Expression start = getParameter(function, 1, true);
out.write("(");
str.accept(delegate, String.class);
out.write(" LIKE ");
if(start instanceof Literal) {
out.write("'" + start.evaluate(null, String.class) + "%'");
} else {
out.write("(");
start.accept(delegate, String.class);
out.write(" || '%')");
}
out.write(")");
} else if(function instanceof FilterFunction_strEqualsIgnoreCase) {
Expression first = getParameter(function, 0, true);
Expression second = getParameter(function, 1, true);
out.write("(lower(");
first.accept(delegate, String.class);
out.write(") = lower(");
second.accept(delegate, String.class);
out.write("::text))");
} else if(function instanceof FilterFunction_strIndexOf) {
Expression first = getParameter(function, 0, true);
Expression second = getParameter(function, 1, true);
// would be a simple call, but strIndexOf returns zero based indices
out.write("(strpos(");
first.accept(delegate, String.class);
out.write(", ");
second.accept(delegate, String.class);
out.write(") - 1)");
} else if(function instanceof FilterFunction_strSubstring) {
Expression string = getParameter(function, 0, true);
Expression start = getParameter(function, 1, true);
Expression end = getParameter(function, 2, true);
// postgres does sub(string, start, count)... count instead of end, and 1 based indices
out.write("substr(");
string.accept(delegate, String.class);
out.write(", ");
start.accept(delegate, Integer.class);
out.write(" + 1, (");
end.accept(delegate, Integer.class);
out.write(" - ");
start.accept(delegate, Integer.class);
out.write("))");
} else if(function instanceof FilterFunction_strSubstringStart) {
Expression string = getParameter(function, 0, true);
Expression start = getParameter(function, 1, true);
// postgres does sub(string, start, count)... count instead of end, and 1 based indices
out.write("substr(");
string.accept(delegate, String.class);
out.write(", ");
start.accept(delegate, Integer.class);
out.write(" + 1)");
} else if(function instanceof FilterFunction_strTrim) {
Expression string = getParameter(function, 0, true);
out.write("trim(both ' ' from ");
string.accept(delegate, String.class);
out.write(")");
} else {
// function not supported
return false;
}
return true;
}
Expression getParameter(Function function, int idx, boolean mandatory) {
final List<Expression> params = function.getParameters();
if(params == null || params.size() <= idx) {
if(mandatory) {
throw new IllegalArgumentException("Missing parameter number " + (idx + 1)
+ "for function " + function.getName() + ", cannot encode in SQL");
}
}
return params.get(idx);
}
public String cast(String property, Class target) {
if(String.class.equals(target)) {
return property + "::text";
} else if(Short.class.equals(target) || Byte.class.equals(target)){
return property + "::smallint";
} else if(Integer.class.equals(target)){
return property + "::integer";
} else if(Long.class.equals(target)){
return property + "::bigint";
} else if(Float.class.equals(target)) {
return property + "::real";
} else if(Double.class.equals(target)) {
return property + "::float8";
} else if(BigInteger.class.equals(target)) {
return property + "::numeric";
} else if(BigDecimal.class.equals(target)) {
return property + "::decimal";
} else if(Double.class.equals(target)) {
return property + "::float8";
} else if(Time.class.isAssignableFrom(target)) {
return property + "::time";
} else if(Timestamp.class.isAssignableFrom(target)) {
return property + "::timestamp";
} else if(Date.class.isAssignableFrom(target)) {
return property + "::date";
} else if(java.util.Date.class.isAssignableFrom(target)) {
return property + "::timesamp";
} else {
// dunno how to cast, leave as is
return property;
}
}
}