/* * 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.data.postgis; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; import org.geotools.data.DataSourceException; import org.geotools.data.DataUtilities; import org.geotools.data.DefaultQuery; import org.geotools.data.Query; import org.geotools.data.jdbc.JDBCDataStore; import org.geotools.data.jdbc.JDBCFeatureStore; import org.geotools.data.jdbc.JDBCUtils; import org.geotools.data.jdbc.SQLBuilder; import org.geotools.data.jdbc.fidmapper.FIDMapper; import org.geotools.data.jdbc.fidmapper.NullFIDMapper; import org.geotools.factory.FactoryRegistryException; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureIterator; import org.geotools.feature.IllegalAttributeException; import org.geotools.feature.SchemaException; import org.geotools.filter.FidFilter; import org.geotools.filter.FilterFactory; import org.geotools.filter.FilterFactoryFinder; import org.geotools.filter.SQLEncoderException; import org.geotools.filter.SQLEncoderPostgis; import org.geotools.filter.SQLUnpacker; import org.geotools.geometry.jts.ReferencedEnvelope; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.filter.Filter; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.io.ParseException; import com.vividsolutions.jts.io.WKTReader; import com.vividsolutions.jts.io.WKTWriter; /** * Implementation of a Postgis specific FeatureStore. * <p> * This mostly just rips off code from PostgisDataSource * It could definitely use some nice code reuse with PostgisDataStore, as they * have a number of similar if not identical methods right now. * <p> * Approaching deadlines, however, mean that * we're sticking with the code that works, instead of getting all kinds of * nice reuse. This'll hopefully change. This bypasses the writers used in * JDBCFeatureStore, as I'm just not yet confident in them. We also should * do some solid tests to see which is actually faster. * * @author Chris Holmes, TOPP * @source $URL$ * @version $Id$ * * @task HACK: too little code reuse with PostgisDataStore. * @task TODO: make individual operations truly atomic. If the transaction is * an auto-commit one, then it should make a a new jdbc transaction that * rollsback if there are errors while performing its action. */ public class PostgisFeatureStore extends JDBCFeatureStore { /** The logger for the postgis module. */ private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.data.postgis"); /** Well Known Text writer (from JTS). */ protected static WKTWriter geometryWriter = new WKTWriter(); /** Factory for producing geometries (from JTS). */ protected static GeometryFactory geometryFactory = new GeometryFactory(); /** Well Known Text reader (from JTS). */ protected static WKTReader geometryReader = new WKTReader(geometryFactory); /** Error message prefix for sql connection errors */ protected static final String CONN_ERROR = "Some sort of database connection error: "; /** To create the sql where statement */ protected PostgisSQLBuilder sqlBuilder; protected String tableName; /** the name of the column to use for the featureId */ protected FIDMapper fidMapper; public PostgisFeatureStore(final PostgisDataStore postgisDataStore, final SimpleFeatureType featureType) throws IOException { super(postgisDataStore, featureType); tableName = featureType.getTypeName(); fidMapper = postgisDataStore.getFIDMapper(tableName); sqlBuilder = (PostgisSQLBuilder) postgisDataStore.getSqlBuilder(tableName); AttributeDescriptor geomType = featureType.getGeometryDescriptor(); /** * Override to indicate we support offset, and natural and reverse order sorting */ queryCapabilities = new JDBCQueryCapabilities(featureType){ @Override public boolean isOffsetSupported(){ return true; } @Override protected boolean supportsNaturalOrderSorting() { return true; } @Override protected boolean supportsReverseOrderSorting() { return true; } @Override public boolean isReliableFIDSupported() { //parent implementation does same thing but we already have fid mapper handy // so might as well save a lookup return !isNullFidMapper(fidMapper); } }; } protected int getSRID(String geomName) throws IOException { return getPostgisDataStore().getSRID(tableName, geomName); } /** * Adds quotes to an object for storage in postgis. The object should be a * string or a number. To perform an insert strings need quotes around * them, and numbers work fine with quotes, so this method can be called * on unknown objects. * * @param value The object to add quotes to. * * @return a string representation of the object with quotes. */ private String addQuotes(Object value) { String retString; if (value != null) { retString = "'" + value.toString() + "'"; } else { retString = "null"; } return retString; } /** * Removes the features specified by the passed filter from the PostGIS * database. * * @param filter An OpenGIS filter; specifies which features to remove. * * @throws IOException If anything goes wrong or if deleting is not * supported. * @throws DataSourceException DOCUMENT ME! */ public void removeFeatures(Filter filter) throws IOException { String sql = ""; String whereStmt = null; // check locks! // (won't do anything if we use our own // database locking) assertFilter(filter); //boolean previousAutoCommit = getAutoCommit(); //setAutoCommit(false); Filter encodableFilter = sqlBuilder.getPreQueryFilter(filter); Filter unEncodableFilter = sqlBuilder.getPostQueryFilter(filter); PreparedStatement statement = null; Connection conn = null; try { conn = getConnection(); if (encodableFilter == null && unEncodableFilter != null) { encodableFilter = getEncodableFilter(unEncodableFilter); } if (encodableFilter != null) { StringBuffer sb = new StringBuffer(); sb.append("DELETE FROM"); sb.append(sqlBuilder.encodeTableName(tableName)); sb.append(" WHERE "); sqlBuilder.encode(sb, encodableFilter); sql = sb.toString(); //do actual delete LOGGER.fine("sql statment is " + sql); DefaultQuery query=new DefaultQuery(getSchema().getTypeName(), filter); ReferencedEnvelope bounds=bounds(query); statement = conn.prepareStatement(sql); statement.executeUpdate(); if( bounds!=null && !bounds.isNull()) getJDBCDataStore().listenerManager.fireFeaturesRemoved(getSchema().getTypeName(), getTransaction(), bounds, false); } close(statement); } catch (SQLException sqle) { close(conn, getTransaction(), sqle); String message = CONN_ERROR + sqle.getMessage(); LOGGER.warning(message); throw new DataSourceException(message, sqle); } catch (SQLEncoderException ence) { String message = "error encoding sql from filter " + ence.getMessage(); LOGGER.warning(message); throw new DataSourceException(message, ence); } catch (IllegalAttributeException iae) { throw new DataSourceException("attribute problem", iae); } finally { close(statement); close(conn, getTransaction(), null); } } /** * Modifies the passed attribute types with the passed objects in all * features that correspond to the passed OGS filter. * * @param type The attributes to modify. * @param value The values to put in the attribute types. * @param filter An OGC filter to note which attributes to modify. * * @throws IOException If modificaton is not supported, if the attribute * and object arrays are not eqaul length, or if the object types * do not match the attribute types. * @throws DataSourceException DOCUMENT ME! * * @task REVISIT: validate values with types. Database does this a bit * now, but should be more fully implemented. * @task REVISIT: do some nice prepared statement stuff like oracle. */ public void modifyFeatures(AttributeDescriptor[] type, Object[] value, Filter filter) throws IOException { // check locks! // (won't do anything if we use our own // database locking) LOGGER.finer("asserting filter " + filter); assertFilter(filter); //boolean previousAutoCommit = getAutoCommit(); //setAutoCommit(false); boolean fail = false; Connection conn = null; PreparedStatement statement = null; String fid = null; //check schema with filter??? // SQLUnpacker unpacker = new SQLUnpacker(encoder.getCapabilities()); // unpacker.unPackOR(filter); String whereStmt = null; Filter encodableFilter = sqlBuilder.getPreQueryFilter(filter); Filter unEncodableFilter = sqlBuilder.getPostQueryFilter(filter); try { conn = getConnection(); if (encodableFilter == null && unEncodableFilter != null) { FidFilter fidFilter = getEncodableFilter(unEncodableFilter); encodableFilter = fidFilter; } if (encodableFilter != null) { StringBuffer sb = new StringBuffer(); sqlBuilder.encode(sb, encodableFilter); whereStmt = "WHERE " + sb; final String sql = makeModifySqlStatement(type, value, whereStmt); statement = conn.prepareStatement(sql); LOGGER.finer("encoded modify is " + sql); setModifyPreparedStatementValues(statement, type, value); LOGGER.finer("encoded modify is " + sql); DefaultQuery query=new DefaultQuery(getSchema().getTypeName(), filter); ReferencedEnvelope bounds=bounds(query); statement.executeUpdate(); if (bounds!=null && !bounds.isNull()) { ReferencedEnvelope afterBounds = bounds(query); if(afterBounds != null) bounds.expandToInclude(afterBounds); } else { bounds=bounds(query); } if (bounds!=null && !bounds.isNull()) getJDBCDataStore().listenerManager.fireFeaturesChanged(getSchema().getTypeName(), getTransaction(), bounds, false); } } catch (SQLException sqle) { fail = true; close(conn, getTransaction(), sqle); String message = CONN_ERROR + sqle.getMessage(); LOGGER.warning(message); throw new DataSourceException(message, sqle); } catch (SQLEncoderException ence) { fail = true; String message = "error encoding sql from filter " + ence.getMessage(); LOGGER.warning(message); throw new DataSourceException(message, ence); } catch (IllegalAttributeException iae) { throw new DataSourceException("attribute problem", iae); } finally { close(statement); close(conn, getTransaction(), null); } } private FidFilter getEncodableFilter(Filter unEncodableFilter) throws IOException, FactoryRegistryException, IllegalAttributeException { // this is very similar to getFidSet - the reason is so that we // don't spend time constructing geometries when we don't need // to, but we probably could get some better code reuse. DefaultQuery query = new DefaultQuery(); query.setPropertyNames(new String[0]); query.setFilter(unEncodableFilter); FeatureCollection<SimpleFeatureType, SimpleFeature> features = getFeatures(unEncodableFilter); FilterFactory ff = FilterFactoryFinder.createFilterFactory(); FidFilter fidFilter = ff.createFidFilter(); FeatureIterator<SimpleFeature> it = features.features(); try { while( it.hasNext() ) { SimpleFeature feature = (SimpleFeature) it.next(); fidFilter.addFid(feature.getID()); } } finally { features.close( it ); } return fidFilter; } /** * strips the tableName from the fid for those in the format * featureName.3534 should maybe just strip out all alpha-numeric * characters. * * @param feature The feature for which the fid number should be stripped. * * @return The fid without the leading tablename. */ private String formatFid(SimpleFeature feature) { String fid = feature.getID(); if (fid.startsWith(tableName)) { //take out the tableName and the . fid = fid.substring(tableName.length() + 1); } return addQuotes(fid); } /** * Modifies the passed attribute types with the passed objects in all * features that correspond to the passed OGS filter. A convenience * method for single attribute modifications. * * @param type The attributes to modify. * @param value The values to put in the attribute types. * @param filter An OGC filter to note which attributes to modify. * * @throws IOException If modificaton is not supported, if the object type * do not match the attribute type. */ public void modifyFeatures(AttributeDescriptor type, Object value, Filter filter) throws IOException { AttributeDescriptor[] singleType = { type }; Object[] singleVal = { value }; modifyFeatures(singleType, singleVal, filter); } /** * Builds an UPDATE sql statement suitable to be used to create a * PreparedStatement. * <p> * The form of the returned statement is like * <code>UPDATE tableName SET att = ?, att2 = ?</code>. Note the use of * the * <code>?<code> value placeholder. The number of <code>?<code> value place holders * in the returned statement will be equal to the number of <code>types</code> and <code>values</code> * passed as arguments. * </p> * * @param types * the attributes to be changed. * @param values * the values to change them to, forming a one to one mapping with <code>types</code>. * @param whereStmt * the feature to update. * * @return an update sql statement to be used in a PreparedStatement. * * @throws IOException * if the lengths of types and values don't match. * @see #setModifyPreparedStatementValues */ private String makeModifySqlStatement(AttributeDescriptor[] types, Object[] values, String whereStmt) throws IOException { final int arrLength = types.length; if (arrLength != values.length) { throw new IllegalArgumentException("length of value array is not " + "same length as type array"); } StringBuffer sqlStatement = new StringBuffer("UPDATE "); sqlStatement.append(sqlBuilder.encodeTableName(tableName) + " SET "); for (int i = 0; i < arrLength; i++) { AttributeDescriptor curType = types[i]; sqlStatement.append(sqlBuilder.encodeColumnName(curType.getLocalName())); sqlStatement.append(" = "); if (curType instanceof GeometryDescriptor) { //create a placeholder where to set the geometry as WKT int srid = getSRID(curType.getLocalName()); sqlStatement.append("GeometryFromText( ?, ").append(srid).append(")"); } else { sqlStatement.append("?"); } sqlStatement.append((i < (arrLength - 1)) ? ", " : " "); } sqlStatement.append(whereStmt); sqlStatement.append(";"); return sqlStatement.toString(); } /** * Sets the values corresponding to the PreparedStatement placeholders. * <p> * The number of <code>"?"</code> placeholders in the statement, the number of attributes * in <code>types</code> and the numbers of <code>values</code> shall be the same. * </p> * <p> * This method is intended to be used after creating a prepared statement through * {@link #makeModifySqlStatement} in order to set the statement values. * </p> * * @param types * the attribute to be changed. * @param values * the value to change it to. * @param whereStmt * the feature to update. * * @return an update sql statement. * * @throws IOException * if the lengths of types and values don't match. * @throws SQLException */ private void setModifyPreparedStatementValues(PreparedStatement statement, AttributeDescriptor[] types, Object[] values) throws IOException, SQLException { final int arrLength = types.length; if (arrLength != values.length) { throw new IllegalArgumentException("length of value array is not " + "same length as type array"); } final PostgisDataStore dataStore = getPostgisDataStore(); for (int i = 0; i < arrLength; i++) { Object newValue = values[i]; AttributeDescriptor curType = types[i]; if (curType instanceof GeometryDescriptor) { newValue = geometryWriter.write((Geometry) newValue); // prepStatement indexing starts at 1... statement.setObject(1 + i, newValue); }else{ //Get the jdbc column type final Class target = curType.getType().getBinding(); final int jdbcType = dataStore.getJdbcType(target).intValue(); // prepStatement indexing starts at 1... statement.setObject(1 + i, newValue, jdbcType); } } } /** * Performs the setFeautres operation by removing all and then adding the * full collection. This is not efficient, the add, modify and remove * operations should be used instead, this is just to follow the * interface. * * @return DOCUMENT ME! * * @task REVISIT: to abstract class, same as oracle. */ /*public void setFeatures(FeatureCollection features) throws IOException { boolean originalAutoCommit = getAutoCommit(); setAutoCommit(false); removeFeatures(null); addFeatures(features); //commit(); //setAutoCommit(originalAutoCommit); }*/ protected PostgisDataStore getPostgisDataStore() { return (PostgisDataStore) super.getJDBCDataStore(); } /** * Creates a SQL statement for the PostGIS database. * * @deprecated please use makeSql(query) * @param unpacker the object to get the encodable filter. * @param query the getFeature query - for the tableName, properties and * maxFeatures. * @throws IOException if there are problems encoding the sql. */ public String makeSql(SQLUnpacker unpacker, Query query) throws IOException { //one to one relationship for now, so typeName is not used. //String tableName = query.getTypeName(); boolean useLimit = (unpacker.getUnSupported() == null); Filter filter = unpacker.getSupported(); LOGGER.fine("Filter in making sql is " + filter); AttributeDescriptor[] attributeTypes = getAttTypes(query); int numAttributes = attributeTypes.length; StringBuffer sqlStatement = new StringBuffer("SELECT "); if (!fidMapper.returnFIDColumnsAsAttributes()) { for (int i = 0; i < fidMapper.getColumnCount(); i++) { sqlStatement.append(fidMapper.getColumnName(i)); if (numAttributes > 0 || i < (fidMapper.getColumnCount() - 1)) sqlStatement.append(", "); } } LOGGER.finer("making sql for " + numAttributes + " attributes"); for (int i = 0; i < numAttributes; i++) { String curAttName = attributeTypes[i].getLocalName(); if (Geometry.class.isAssignableFrom(attributeTypes[i].getType().getBinding())) { sqlStatement.append(", AsText(force_2d(\"" + curAttName + "\"))"); } else { sqlStatement.append(", \"" + curAttName + "\""); } } String where = ""; if (filter != null) { try { StringBuffer sb = new StringBuffer(); sqlBuilder.encode(sb, filter); where = "WHERE " + sb.toString(); } catch (SQLEncoderException sqle) { String message = "Encoder error" + sqle.getMessage(); LOGGER.warning(message); throw new DataSourceException(message, sqle); } } //int limit = HARD_MAX_FEATURES; String limit = ""; if (useLimit) { limit = " LIMIT " + query.getMaxFeatures(); } sqlStatement.append(" FROM \"" + tableName + "\" " + where + limit + ";").toString(); LOGGER.fine("sql statement is " + sqlStatement); return sqlStatement.toString(); } /** * Gets the attribute types from the query. If all are requested then * returns all attribute types of this query. If only certain * propertyNames are requested then this returns the correct attribute * types, throwing an exception is they can not be found. * * @param query contains the propertyNames. * * @return the array of attribute types to be returned by getFeature. * * @throws IOException if query contains a propertyName that is not a part * of this type's schema. */ private AttributeDescriptor[] getAttTypes(Query query) throws IOException { AttributeDescriptor[] schemaTypes = (AttributeDescriptor[]) getSchema().getAttributeDescriptors().toArray(new AttributeDescriptor[getSchema().getAttributeDescriptors().size()]); if (query.retrieveAllProperties()) { return schemaTypes; } else { List attNames = Arrays.asList(query.getPropertyNames()); AttributeDescriptor[] retAttTypes = new AttributeDescriptor[attNames.size()]; int retPos = 0; for (int i = 0, n = schemaTypes.length; i < n; i++) { String schemaTypeName = schemaTypes[i].getLocalName(); if (attNames.contains(schemaTypeName)) { retAttTypes[retPos++] = schemaTypes[i]; } } //TODO: better error reporting, and completely test this method. if (attNames.size() != retPos) { String msg = "attempted to request a property, " + attNames.get(0) + " that is not part of the schema "; throw new IOException(msg); } return retAttTypes; } } public ReferencedEnvelope getBounds() throws IOException { return getBounds(Query.ALL); } /** * Retrieve Bounds of Query results. * * <p> * Currently returns null, consider getFeatures( query ).getBounds() * instead. * </p> * * <p> * Subclasses may override this method to perform the appropriate * optimization for this result. * </p> * * @param query Query we are requesting the bounds of * * @return null representing the lack of an optimization * * @throws IOException DOCUMENT ME! */ public ReferencedEnvelope getBounds(Query query) throws IOException { return bounds(query); } // TODO: change this so that it queries for just the bbox instead of the entire sub-query schema columns! // (this is harder than you might think because of filter requirements!) // protected ReferencedEnvelope bounds(Query query) throws IOException { Filter filter = query.getFilter(); if (filter == Filter.EXCLUDE) { return new ReferencedEnvelope(new ReferencedEnvelope(), query.getCoordinateSystem()); } SimpleFeatureType schema = getSchema(); JDBCDataStore jdbc = (JDBCDataStore)getJDBCDataStore(); SQLBuilder sqlBuilder = jdbc.getSqlBuilder(schema.getTypeName()); Filter postQueryFilter = sqlBuilder.getPostQueryFilter(query.getFilter()); if (postQueryFilter != null && !postQueryFilter.equals(Filter.INCLUDE)) { // this would require postprocessing the filter // so we cannot optimize return null; } Connection conn = null; try { conn = getConnection(); ReferencedEnvelope retEnv = new ReferencedEnvelope(); Filter preFilter = sqlBuilder.getPreQueryFilter(query.getFilter()); AttributeDescriptor[] attributeTypes = (AttributeDescriptor[]) schema.getAttributeDescriptors().toArray(new AttributeDescriptor[schema.getAttributeDescriptors().size()]); SimpleFeatureType schemaNew = schema; //DJB: this should ensure that schema has a geometry in it or the bounds query has no chance of working if(!query.retrieveAllProperties()) { try { schemaNew = DataUtilities.createSubType(schema, query.getPropertyNames()); if (schemaNew.getGeometryDescriptor() == null) // does the sub-schema have a geometry in it? { //uh-oh better get one! if (schema.getGeometryDescriptor() != null) // does the entire schema have a geometry in it? { //buff-up the sub-schema so it has the default geometry in it. ArrayList al = new ArrayList (Arrays.asList(query.getPropertyNames())); al.add(schema.getGeometryDescriptor().getLocalName()); schemaNew = DataUtilities.createSubType(schema, (String[]) al.toArray(new String[1]) ); } } } catch (SchemaException e1) { throw new DataSourceException("Could not create subtype", e1); } } // at this point, the query should have a geometry in it. // BUT, if there's no geometry in the table, then the query will not (obviously) have a geometry in it. attributeTypes = (AttributeDescriptor[]) schemaNew.getAttributeDescriptors().toArray(new AttributeDescriptor[schema.getAttributeDescriptors().size()]); for (int j = 0, n = schemaNew.getAttributeCount(); j < n; j++) { if (Geometry.class.isAssignableFrom(attributeTypes[j].getType().getBinding())) // same as .isgeometry() - see new featuretype javadoc { String attName = attributeTypes[j].getLocalName(); ReferencedEnvelope curEnv = getEnvelope(conn, attName, sqlBuilder, filter); if (curEnv == null) { return null; } retEnv.expandToInclude(curEnv); } } LOGGER.finer("returning bounds " + retEnv); // handle reprojection and crs forcing CoordinateReferenceSystem base = null; if(query.getCoordinateSystem() != null) base = query.getCoordinateSystem(); else if(schemaNew.getGeometryDescriptor() != null) base = schemaNew.getGeometryDescriptor().getCoordinateReferenceSystem(); CoordinateReferenceSystem dest = query.getCoordinateSystemReproject(); ReferencedEnvelope result = new ReferencedEnvelope(retEnv, base); if(base != null && dest != null) result = result.transform(dest, true); return result; } catch (SQLException sqlException) { JDBCUtils.close(conn, transaction, sqlException); conn = null; throw new DataSourceException("Could not count " + query.getHandle(), sqlException); } catch (SQLEncoderException e) { // could not encode count // but at least we did not break the connection return null; } catch (ParseException parseE) { String message = "Could not read geometry: " + parseE.getMessage(); return null; } catch (FactoryException e) { throw new DataSourceException("Could not reproject", e); } catch (TransformException e) { throw new DataSourceException("Could not reproject", e); } finally { JDBCUtils.close(conn, transaction, null); } } //REVISIT: do we want maxFeatures here too? If we don't have maxFeatures then the answer //is still always going to be right (and guaranteed to be right, as opposed to two selects // that could be slightly different). And the performance hit shouldn't be all that much. protected ReferencedEnvelope getEnvelope( Connection conn, String geomName, SQLBuilder sqlBuilder, Filter filter) throws SQLException, SQLEncoderException, IOException, ParseException { String typeName = getSchema().getTypeName(); StringBuffer sql = new StringBuffer(); sql.append("SELECT AsText(force_2d(Envelope(" ); //check if we can apply the estimated_extent optimization boolean useEstimatedExtent = ( filter == null || filter == Filter.INCLUDE ) && ((PostgisDataStore)getDataStore()).isEstimatedExtent(); if ( useEstimatedExtent ) { sql.append("estimated_extent("); sql.append("'" + typeName + "','" + geomName + "'))));"); } else { sql.append("Extent(\"" + geomName + "\")))) " ); sqlBuilder.sqlFrom(sql, typeName); sqlBuilder.sqlWhere(sql, filter); } LOGGER.fine("SQL: " + sql); Statement statement = conn.createStatement(); ResultSet results = statement.executeQuery(sql.toString()); results.next(); String wkt = results.getString(1); ReferencedEnvelope retEnv = null; if (wkt == null) { return null; } else { retEnv = ReferencedEnvelope.reference(geometryReader.read(wkt).getEnvelopeInternal()); } results.close(); statement.close(); return retEnv; } }