/*
* 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.math.BigDecimal;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.DataSource;
import org.geotools.data.DataSourceException;
import org.geotools.data.DataStore;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultQuery;
import org.geotools.data.EmptyFeatureReader;
import org.geotools.data.FeatureReader;
import org.geotools.data.FeatureSource;
import org.geotools.data.FeatureWriter;
import org.geotools.data.InProcessLockingManager;
import org.geotools.data.LockingManager;
import org.geotools.data.Query;
import org.geotools.data.ReTypeFeatureReader;
import org.geotools.data.Transaction;
import org.geotools.data.jdbc.FeatureTypeInfo;
import org.geotools.data.jdbc.JDBCDataStore;
import org.geotools.data.jdbc.JDBCDataStoreConfig;
import org.geotools.data.jdbc.JDBCFeatureLocking;
import org.geotools.data.jdbc.JDBCFeatureSource;
import org.geotools.data.jdbc.JDBCFeatureStore;
import org.geotools.data.jdbc.JDBCFeatureWriter;
import org.geotools.data.jdbc.JDBCUtils;
import org.geotools.data.jdbc.QueryData;
import org.geotools.data.jdbc.SQLBuilder;
import org.geotools.data.jdbc.attributeio.AttributeIO;
import org.geotools.data.jdbc.attributeio.WKTAttributeIO;
import org.geotools.data.jdbc.fidmapper.FIDMapper;
import org.geotools.data.jdbc.fidmapper.FIDMapperFactory;
import org.geotools.data.postgis.attributeio.EWKTAttributeIO;
import org.geotools.data.postgis.attributeio.PgWKBAttributeIO;
import org.geotools.data.postgis.fidmapper.PostgisFIDMapperFactory;
import org.geotools.data.postgis.referencing.PostgisAuthorityFactory;
import org.geotools.factory.GeoTools;
import org.geotools.factory.Hints;
import org.geotools.feature.AttributeTypeBuilder;
import org.geotools.feature.FeatureTypes;
import org.geotools.filter.SQLEncoderPostgis;
import org.geotools.referencing.NamedIdentifier;
import org.geotools.referencing.crs.DefaultGeographicCRS;
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 com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.io.WKTReader;
/**
* Postgis DataStore implementation.
*
* <p>
* This datastore by default will read/write geometries in WKT format.<br>
* Optionally use of WKB can be turned on, in which case you may want to turn
* on also the use of the bytea function, that fasten the data trasfer, but
* that it's available only from version 0.7.2 onwards.
* </p>
*
* @author Chris Holmes, TOPP
* @author Andrea Aime
* @author Paolo Rizzi
* @source $URL$
* @version $Id$
*
* @task REVISIT: So Paolo Rizzi has a number of improvements in
* http://jira.codehuas.org/browse/GEOT-379 I rolled in a few of them,
* but some beg more fundamental questions - like the use of primary
* keys - in the geotools model. See the issue for a bit more
* discussion, and I will attempt to write my thoughts up on wiki soon.
* -ch
*/
public class PostgisDataStore extends JDBCDataStore implements DataStore {
/** The logger for the postgis module. */
protected static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(
"org.geotools.data.postgis");
/** 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);
/** Map of postgis geometries to jts geometries */
private static Map GEOM_TYPE_MAP = new HashMap();
static {
GEOM_TYPE_MAP.put("GEOMETRY", Geometry.class);
GEOM_TYPE_MAP.put("POINT", Point.class);
GEOM_TYPE_MAP.put("POINTM", Point.class);
GEOM_TYPE_MAP.put("LINESTRING", LineString.class);
GEOM_TYPE_MAP.put("LINESTRINGM", LineString.class);
GEOM_TYPE_MAP.put("POLYGON", Polygon.class);
GEOM_TYPE_MAP.put("POLYGONM", Polygon.class);
GEOM_TYPE_MAP.put("MULTIPOINT", MultiPoint.class);
GEOM_TYPE_MAP.put("MULTIPOINTM", MultiPoint.class);
GEOM_TYPE_MAP.put("MULTILINESTRING", MultiLineString.class);
GEOM_TYPE_MAP.put("MULTILINESTRINGM", MultiLineString.class);
GEOM_TYPE_MAP.put("MULTIPOLYGON", MultiPolygon.class);
GEOM_TYPE_MAP.put("MULTIPOLYGONM", MultiPolygon.class);
GEOM_TYPE_MAP.put("GEOMETRYCOLLECTION", GeometryCollection.class);
GEOM_TYPE_MAP.put("GEOMETRYCOLLECTIONM", GeometryCollection.class);
// SQL MM "Curve" extensions
GEOM_TYPE_MAP.put("CIRCULARSTRING", LineString.class);
GEOM_TYPE_MAP.put("COMPOUNDCURVE", LineString.class);
GEOM_TYPE_MAP.put("CURVEPOLYGON", Polygon.class);
}
private static Map CLASS_MAPPINGS = new HashMap();
static {
CLASS_MAPPINGS.put(String.class, "VARCHAR");
CLASS_MAPPINGS.put(Boolean.class, "BOOLEAN");
CLASS_MAPPINGS.put(Integer.class, "INTEGER");
CLASS_MAPPINGS.put(Long.class, "BIGINT");
CLASS_MAPPINGS.put(Float.class, "REAL");
CLASS_MAPPINGS.put(Double.class, "DOUBLE PRECISION");
CLASS_MAPPINGS.put(BigDecimal.class, "DECIMAL");
CLASS_MAPPINGS.put(java.sql.Date.class, "DATE");
CLASS_MAPPINGS.put(java.util.Date.class, "DATE");
CLASS_MAPPINGS.put(java.sql.Time.class, "TIME");
CLASS_MAPPINGS.put(java.sql.Timestamp.class, "TIMESTAMP");
}
private static Map GEOM_CLASS_MAPPINGS = new HashMap();
//why don't we just stick this in with the non-geom class mappings?
static {
// init the inverse map
Set keys = GEOM_TYPE_MAP.keySet();
for (Iterator it = keys.iterator(); it.hasNext();) {
String name = (String) it.next();
Class geomClass = (Class) GEOM_TYPE_MAP.get(name);
GEOM_CLASS_MAPPINGS.put(geomClass, name);
}
}
/** OPTIMIZE_MODE constants */
public static final int OPTIMIZE_SAFE = 0;
public static final int OPTIMIZE_SQL = 1;
/** Maximum string size for postgres */
private static final int MAX_ALLOWED_VALUE = 10485760;
//JD: GEOT-723, keeping this reference static allows the authority factory
// to hold onto a stale connection pool when a new datastore is created.
//private static PostgisAuthorityFactory paf = null;
private PostgisAuthorityFactory paf = null;
/** PostGIS version information (persisted here so we don't have to keep asking the database what version it is, in perpituity. */
protected PostgisDBInfo dbInfo;
/** Enables the use of geos operators */
protected boolean useGeos;
/**
* Current optimize mode
* @deprecated Dot not use this directly, use {@link #getOptimizeMode()}.
*/
public int OPTIMIZE_MODE;
/** If true, WKB format is used instead of WKT */
protected boolean WKBEnabled = false;
/**
* If true, the bytea function will be used to optimize even further data
* loading when using WKB format
*/
protected boolean byteaEnabled = false;
/**
* postgis 1.0 changed the way WKB is handled, this needs to be
* set if version >1.
* (it affects the way you send WKB to the database)
*/
protected boolean byteaWKB = false;
/**
* If true then the bounding box filters will use the && postgis operator,
* which uses the spatial index and performs against the envelope of the
* geom, leading to greater speed and slightly less accuracy.
*/
protected boolean looseBbox;
/**
* set to true if the bounds for a table should be computed using the
* estimated_extent' function, but beware that this function is less accurate
* and in some cases *far* less accurate if the data within the actual bounds
* does not follow a uniform distribution.
*/
protected boolean estimatedExtent;
/** Flag indicating whether schema support **/
protected boolean schemaEnabled = true;
protected PostgisDataStore(DataSource dataSource)
throws IOException {
this(dataSource, (String) null);
}
protected PostgisDataStore(DataSource dataSource, String namespace)
throws IOException {
this(dataSource, schema(null), namespace);
}
protected PostgisDataStore(DataSource dataSource, String schema,
String namespace) throws IOException {
this(dataSource,
new JDBCDataStoreConfig(namespace, schema(schema), new HashMap(),
new HashMap()), OPTIMIZE_SQL);
}
protected PostgisDataStore(DataSource dataSource, String schema,
String namespace, int optimizeMode) throws IOException {
this(dataSource,
new JDBCDataStoreConfig(namespace, schema(schema), new HashMap(),
new HashMap()), optimizeMode);
}
/**
* Simple helper method to ensure that a schema is always set.
*/
protected static String schema(String schema) {
if (schema != null && !"".equals(schema))
return schema;
return (String) PostgisDataStoreFactory.SCHEMA.sample;
}
public PostgisDataStore(DataSource dataSource,
JDBCDataStoreConfig config, int optimizeMode) throws IOException {
super(dataSource, config);
guessDataStoreOptions();
OPTIMIZE_MODE = optimizeMode;
// use the specific postgis fid mapper factory
setFIDMapperFactory( buildFIDMapperFactory( config ) );
}
/**
* Allows subclass to create LockingManager to support their needs.
*
*/
protected LockingManager createLockingManager() {
return new InProcessLockingManager();
}
/**
* Creates a new sql builder for encoding raw sql statements;
*
*/
protected PostgisSQLBuilder createSQLBuilder() {
PostgisSQLBuilder builder = new PostgisSQLBuilder(new SQLEncoderPostgis(), config);
initBuilder(builder);
return builder;
}
/**
* Attempts to figure out some optimization options, based on some postgis
* metadata. If the version is later than 0.7.2 then bytea will be used
* to read geometries if WKB is enabled. And it will read if GEOS is
* enabled from the version string as well.
*
* @throws IOException
*/
protected void guessDataStoreOptions() throws IOException {
PostgisDBInfo dbInfo = getDBInfo();
if (dbInfo == null) { //assume
LOGGER.severe("Could not obtain PostgisDBInfo");
byteaEnabled = true;
byteaWKB = false;
useGeos = true;
schemaEnabled = true;
} else {
byteaEnabled = dbInfo.isByteaEnabled();
if (dbInfo.getMajorVersion() >= 1) {
byteaWKB = true; // force ew wkb writing format
}
useGeos = dbInfo.isGeosEnabled();
schemaEnabled = dbInfo.isSchemaEnabled();
}
}
/*
* (non-Javadoc)
*
* @see org.geotools.data.DataStore#getTypeNames()
*/
public String[] getTypeNames() throws IOException {
final int TABLE_NAME_COL = 3;
Connection conn = null;
List list = new ArrayList();
try {
conn = getConnection(Transaction.AUTO_COMMIT);
DatabaseMetaData meta = conn.getMetaData(); // DB: shouldnt this be done by looking at geometry_columns? or are you trying to allow non-spatial tables in as well?
String[] tableType = { "TABLE" , "VIEW"};
ResultSet tables = meta.getTables(null,
config.getDatabaseSchemaName(), "%", tableType);
while (tables.next()) {
String tableName = tables.getString(TABLE_NAME_COL);
if (allowTable(tableName)) {
list.add(tableName);
}
}
tables.close();
return (String[]) list.toArray(new String[list.size()]);
} catch (SQLException sqlException) {
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, sqlException);
conn = null;
String message = "Error querying database for list of tables:"
+ sqlException.getMessage();
throw new DataSourceException(message, sqlException);
} finally {
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, null);
}
/*
//Justin's patch from uDig, should be faster, but untested.
Connection conn = null;
String namespace = config.getNamespace();
try {
conn = getConnection(Transaction.AUTO_COMMIT);
PreparedStatement st = null;
if (namespace != null && !namespace.trim().equals("")) { //$NON-NLS-1$
st = conn.prepareStatement(
"SELECT distinct a.relname " //$NON-NLS-1$
+ "FROM pg_class a, pg_attribute b, pg_namespace c, pg_type d " //$NON-NLS-1$
+ "WHERE a.oid = b.attrelid " //$NON-NLS-1$
+ "AND b.atttypid = d.oid " //$NON-NLS-1$
+ "AND a.relnamespace = c.oid " //$NON-NLS-1$
+ "AND c.nspname = ? " //$NON-NLS-1$
+ "AND d.typname = ? " //$NON-NLS-1$
+ "AND a.relname in (SELECT f_table_name FROM geometry_columns)" //$NON-NLS-1$
);
st.setString(1, namespace);
st.setString(2, "geometry"); //$NON-NLS-1$
}
else {
st = conn.prepareStatement(
"SELECT distinct a.relname " //$NON-NLS-1$
+ "FROM pg_class a, pg_attribute b, pg_type d " //$NON-NLS-1$
+ "WHERE a.oid = b.attrelid " //$NON-NLS-1$
+ "AND b.atttypid = d.oid " //$NON-NLS-1$
+ "AND d.typname = ? " //$NON-NLS-1$
+ "AND a.relname in (SELECT f_table_name FROM geometry_columns)" //$NON-NLS-1$
);
st.setString(1, "geometry"); //$NON-NLS-1$
}
ResultSet rs = st.executeQuery();
ArrayList names = new ArrayList();
while(rs.next()) {
String table = rs.getString(1);
if (allowTable(table)){
names.add(table);
}
}
return (String[])names.toArray(new String[names.size()]);
}
catch (SQLException sqlException) {
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, sqlException);
conn = null;
throw new DataSourceException( sqlException );
}
finally {
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, null);
}
*/
}
/**
* Retrieve approx bounds of all Features.
* <p>
* This result is suitable for a quick map display, illustrating the data.
* This value is often stored as metadata in databases such as oraclespatial.
* </p>
* @return null as a generic implementation is not provided.
*/
public Envelope getEnvelope( String typeName ){
Connection conn = null;
try {
conn = createConnection();
Statement st = null;
ResultSet rs = null;
Envelope envelope = null;
SimpleFeatureType schema = getSchema(typeName);
String geomName = schema.getGeometryDescriptor().getLocalName();
// optimization, postgis version >= 1.0 contains estimated_extent
// function to query the stats of the table to determine the bbox,
// however, it may return null
if (getDBInfo().getMajorVersion() >= 1) {
//try the estimated_extent([schema], table, geocolumn) function
String q;
String dbSchema = config.getDatabaseSchemaName();
if (!schemaEnabled || dbSchema == null || "".equals(dbSchema)) {
q = "SELECT AsText(force_2d(envelope(estimated_extent('"+typeName+"','"+geomName+"'))))";
} else {
q = "SELECT AsText(force_2d(envelope(estimated_extent('"+dbSchema+"','"+typeName+"','"+geomName+"'))))";
}
st = conn.createStatement();
rs = st.executeQuery(q);
if (rs.next()) {
//parse return value
String wkt = rs.getString(1);
if (wkt != null && !wkt.trim().equals("")) { //$NON-NLS-1$
envelope = geometryReader.read(wkt).getEnvelopeInternal();
// expand the bounds by 20% (10% in each direction)
// Works whether or not the bounds are at the origin
double minX = envelope.getMinX();
double minY = envelope.getMinY();
double maxX = envelope.getMaxX();
double maxY = envelope.getMaxY();
double deltaX = (maxX - minX)*0.1;
double deltaY = (maxY - minY)*0.1;
envelope.expandToInclude(minX - deltaX, minY - deltaY);
envelope.expandToInclude(maxX + deltaX, maxY + deltaY);
} else {
LOGGER.warning("PostGIS estimated_extent function did not return a result." +
"\nPerhaps 'ANALYZE "+typeName+";' needs to be run or the table is empty?");
}
}
rs.close();
st.close();
}
if (envelope == null) {
//try to generate an approximation
envelope = new Envelope();
//this is an attempt to grab a handful of envelopes without counting the features
final int blockSize = 10; //how many features to grab on each postgis hit
final int fetchAllLimit = 99; //if we don't exceed this value, just fetch all features
//final int upperLimit = 1000000; //aim for this many features
//final int nBlocks = 7; //number of times to hit postgis
//int[] offset = new int[nBlocks];
//automatic range calculation (for tweaking)
//once we hit 100,000 features in our scan, things get really slow
//therefore we'll stop around 50k
//offset[0] = 1;
//double magicNumber = Math.pow(upperLimit, 1.0 / (nBlocks - 1));
//for (int i = 1; i < nBlocks; i++) {
// offset[i] = (int) Math.ceil(offset[i-1] * magicNumber);
// System.out.println(offset[i]);
//}
//offset[0] = 0;
int[] offset = new int[] {0,10,100,1000,10000,20000,40000};
int hits = 0;
int misses = 0;
for (int i = 0; i < offset.length && misses < 4; i++) {
String limit = " LIMIT " + blockSize + " OFFSET " + offset[i];;
if (i+1 < offset.length && offset[i+1]-offset[i] <= blockSize) {
limit = " LIMIT " + blockSize * 2 + " OFFSET " + offset[i];
offset[i+1] = offset[i] + blockSize;
i++;
}
String q = "SELECT AsText(force_2d(envelope(" + geomName + "))) FROM " + typeName;
if (offset[i] > -1) {
q = q + limit;
}
st = conn.createStatement();
rs = st.executeQuery(q);
boolean gotEnvelope = false;
while (rs.next()) {
gotEnvelope = true;
String wkt = rs.getString(1);
if (wkt != null && !wkt.trim().equals("")) { //$NON-NLS-1$
Envelope e = geometryReader.read(wkt)
.getEnvelopeInternal();
if (envelope.isNull())
envelope.init(e);
else
envelope.expandToInclude(e);
}
}
if (gotEnvelope) {
hits++;
} else {
misses++;
if (hits == 0) { //there are no features!
rs.close();
st.close();
return new Envelope();
}
if (offset[i-1] < fetchAllLimit) { //just fetch everything
offset[i] = -1;
} else {
//went beyond the last feature
//on our first miss, we move back 50% and try again
//on our second miss, we stop guessing and look between last 2 hits
int min = offset[i-1];
int max = offset[i];
if (misses == 2) {
min = offset[i-2];
max = offset[i-1];
}
if (misses < 3) {
offset[i] = (int) ((min + max) / 2.0);
int width = (int) ((max - min) / (double) (offset.length - i));
for (int j = i+1; j < offset.length; j++) {
offset[j] = min + (width * (j-i));
}
} else {
rs.close();
st.close();
break;
}
}
i--;
}
rs.close();
st.close();
if (offset[i] == -1)
break;
}
// expand since this is an approximation
// Works whether or not the bounds are at the origin
double minX = envelope.getMinX();
double minY = envelope.getMinY();
double maxX = envelope.getMaxX();
double maxY = envelope.getMaxY();
double deltaX = (maxX - minX)*1.0;
double deltaY = (maxY - minY)*1.0;
envelope.expandToInclude(minX - deltaX, minY - deltaY);
envelope.expandToInclude(maxX + deltaX, maxY + deltaY);
}
return envelope;
} catch (Exception ignore) {
return null;
} finally {
if( conn != null ){
try {
conn.close();
} catch (SQLException e) {
// I give up
}
}
}
}
protected boolean allowTable(String tablename) {
if (tablename.equals("geometry_columns")) {
return false;
} else if (tablename.startsWith("spatial_ref_sys")) {
return false;
}
//others?
return true;
}
/**
* Override this method to perform a few permission checks before the super
* class has a chance to do its thing.
*/
protected SimpleFeatureType buildSchema(String typeName, FIDMapper mapper) throws IOException {
//be sure we can query the necessary tables
//TODO: should spatial_ref_sys be in here?
Connection conn = getConnection(Transaction.AUTO_COMMIT);
try {
Statement st = conn.createStatement();
try {
st.execute("SELECT * FROM geometry_columns LIMIT 0;");
}
catch (Throwable t) {
String msg = "Error querying relation: geometry_columns." +
" Possible cause:" + t.getLocalizedMessage();
throw new DataSourceException(msg,t);
}
try {
SQLEncoderPostgis encoder = new SQLEncoderPostgis(-1);
encoder.setSupportsGEOS(useGeos);
PostgisSQLBuilder builder = new PostgisSQLBuilder(encoder,
config);
initBuilder(builder);
st.execute("SELECT * FROM " + builder.encodeTableName(typeName)
+ " LIMIT 0;");
}
catch (Throwable t) {
String msg = "Error querying relation:" + typeName + "." +
" Possible cause:" + t.getLocalizedMessage();
throw new DataSourceException(msg,t);
}
st.close();
}
catch (SQLException e) {
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, e);
throw new DataSourceException(e);
}
finally {
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, null);
}
//everything is cool, keep going
return super.buildSchema(typeName, mapper);
}
/**
* This is a public entry point to the DataStore.
*
* <p>
* We have given some though to changing this api to be based on query.
* </p>
*
* <p>
* Currently this is the only way to retype your features to different
* name spaces.
* </p>
* (non-Javadoc)
*
* @see org.geotools.data.DataStore#getFeatureReader(org.geotools.feature.FeatureType,
* org.geotools.filter.Filter, org.geotools.data.Transaction)
*/
public FeatureReader<SimpleFeatureType, SimpleFeature> getFeatureReader(final SimpleFeatureType requestType,
final Filter filter, final Transaction transaction)
throws IOException {
String typeName = requestType.getTypeName();
SimpleFeatureType schemaType = getSchema(typeName);
int compare = DataUtilities.compare(requestType, schemaType);
Query query;
if (compare == 0) {
// they are the same type
//
query = new DefaultQuery(typeName, filter);
} else if (compare == 1) {
// featureType is a proper subset and will require reTyping
//
String[] names = attributeNames(requestType, filter);
query = new DefaultQuery(typeName, filter, Query.DEFAULT_MAX,
names, "getFeatureReader");
} else {
// featureType is not compatiable
//
throw new IOException("Type " + typeName + " does match request");
}
if ((filter == Filter.EXCLUDE) || filter.equals(Filter.EXCLUDE)) {
return new EmptyFeatureReader<SimpleFeatureType, SimpleFeature>(requestType);
}
FeatureReader<SimpleFeatureType, SimpleFeature> reader = getFeatureReader(query, transaction);
if (compare == 1) {
reader = new ReTypeFeatureReader(reader, requestType, false);
}
return reader;
}
/**
* Gets the list of attribute names required for both featureType and
* filter
*
* @param featureType The FeatureType to get attribute names for.
* @param filter The filter which needs attributes to filter.
*
* @return The list of attribute names required by a filter.
*
* @throws IOException If we can't get the schema.
*/
protected String[] attributeNames(SimpleFeatureType featureType, Filter filter)
throws IOException {
String typeName = featureType.getTypeName();
SimpleFeatureType original = getSchema(typeName);
SQLBuilder sqlBuilder = getSqlBuilder(typeName);
if (featureType.getAttributeCount() == original.getAttributeCount()) {
// featureType is complete (so filter must require subset
return DataUtilities.attributeNames(featureType);
}
String[] typeAttributes = DataUtilities.attributeNames(featureType);
String[] filterAttributes = DataUtilities.attributeNames(sqlBuilder
.getPostQueryFilter(filter));
if ((filterAttributes == null) || (filterAttributes.length == 0)) {
// no filter attributes required
return typeAttributes;
}
Set set = new HashSet();
set.addAll(Arrays.asList(typeAttributes));
set.addAll(Arrays.asList(filterAttributes));
if (set.size() == typeAttributes.length) {
// filter required a subset of featureType attributes
return typeAttributes;
} else {
return (String[]) set.toArray(new String[set.size()]);
}
}
/**
* DOCUMENT ME!
*
* @param typeName
*
* @return DOCUMENT ME!
*
* @throws IOException DOCUMENT ME!
*/
public SQLBuilder getSqlBuilder(String typeName) throws IOException {
FeatureTypeInfo info = typeHandler.getFeatureTypeInfo(typeName);
int srid = -1;
SQLEncoderPostgis encoder = new SQLEncoderPostgis();
encoder.setSupportsGEOS(useGeos);
encoder.setFIDMapper(typeHandler.getFIDMapper(typeName));
if (info.getSchema().getGeometryDescriptor() != null) {
String geom = info.getSchema().getGeometryDescriptor().getLocalName();
srid = info.getSRID(geom);
encoder.setDefaultGeometry(geom);
}
encoder.setFeatureType( info.getSchema() );
encoder.setSRID(srid);
encoder.setLooseBbox(looseBbox);
PostgisSQLBuilder builder = new PostgisSQLBuilder(encoder,config,info.getSchema());
initBuilder(builder);
return builder;
}
protected void initBuilder(PostgisSQLBuilder builder) {
builder.setWKBEnabled(WKBEnabled);
builder.setByteaEnabled(byteaEnabled);
builder.setSchemaEnabled(schemaEnabled);
}
/**
* DOCUMENT ME!
*
* @param tableName
* @param geometryColumnName
*
*
* @throws IOException DOCUMENT ME!
* @throws DataSourceException DOCUMENT ME!
*/
protected int determineSRID(String tableName, String geometryColumnName)
throws IOException {
Connection dbConnection = null;
try {
String dbSchema = config.getDatabaseSchemaName();
StringBuffer sql = new StringBuffer();
sql.append("SELECT srid FROM geometry_columns WHERE ");
if (schemaEnabled && dbSchema != null && dbSchema.length() > 0) {
sql.append("f_table_schema='");
sql.append(dbSchema);
sql.append("' AND ");
}
sql.append("f_table_name='");
sql.append(tableName);
sql.append("' AND f_geometry_column='");
sql.append(geometryColumnName);
sql.append("';");
String sqlStatement = sql.toString();
LOGGER.fine("srid statement is " + sqlStatement);
dbConnection = getConnection(Transaction.AUTO_COMMIT);
Statement statement = dbConnection.createStatement();
ResultSet result = statement.executeQuery(sqlStatement);
if (result.next()) {
int retSrid = result.getInt("srid");
JDBCUtils.close(statement);
return retSrid;
}
result.close();
//try asking the first feature for its srid
sql = new StringBuffer();
sql.append("SELECT SRID(\"");
sql.append(geometryColumnName);
sql.append("\") FROM \"");
if (schemaEnabled && dbSchema != null && dbSchema.length() > 0) {
sql.append(dbSchema);
sql.append("\".\"");
}
sql.append(tableName);
sql.append("\" LIMIT 1");
sqlStatement = sql.toString();
result = statement.executeQuery(sqlStatement);
if (result.next()) {
int retSrid = result.getInt(1);
JDBCUtils.close(statement);
return retSrid;
}
String mesg = "No geometry column row for srid in table: "
+ tableName + ", geometry column " + geometryColumnName;
throw new DataSourceException(mesg);
} catch (SQLException sqle) {
String message = sqle.getMessage();
throw new DataSourceException(message, sqle);
} finally {
JDBCUtils.close(dbConnection, Transaction.AUTO_COMMIT, null);
}
}
/**
* Provides the default implementation of determining the FID column.
*
* <p>
* The default implementation of determining the FID column name is to use
* the primary key as the FID column. If no primary key is present, null
* will be returned. Sub classes can override this behaviour to define
* primary keys for vendor specific cases.
* </p>
*
* <p>
* There is an unresolved issue as to what to do when there are multiple
* primary keys. Maybe a restriction that table much have a single column
* primary key is appropriate.
* </p>
*
* <p>
* This should not be called by subclasses to retreive the FID column name.
* Instead, subclasses should call getFeatureTypeInfo(String) to get the
* FeatureTypeInfo for a feature type and get the fidColumn name from the
* fidColumn name memeber.
* </p>
*
* @param array DOCUMENT ME!
* @param value DOCUMENT ME!
*
* @return The name of the primay key column or null if one does not exist.
*/
// protected String determineFidColumnName(String typeName)
// throws IOException {
// String fidColumn = super.determineFidColumnName(typeName);
//
// if(fidColumn == null)
// fidColumn = DEFAULT_FID_COLUMN;
//
// return fidColumn;
// }
/*
private static boolean isPresent(String[] array, String value) {
if (array != null) {
for (int i = 0; i < array.length; i++) {
if ((array[i] != null) && (array[i].equals(value))) {
return (true);
}
}
}
return (false);
}
*/
/**
* Constructs an AttributeDescriptor from a row in a ResultSet. The ResultSet
* contains the information retrieved by a call to getColumns() on the
* DatabaseMetaData object. This information can be used to construct an
* Attribute Type.
*
* <p>
* This implementation construct an AttributeDescriptor using the default JDBC
* type mappings defined in JDBCDataStore. These type mappings only handle
* native Java classes and SQL standard column types. If a geometry type
* is found then getGeometryAttribute is called.
* </p>
*
* <p>
* Note: Overriding methods must never move the current row pointer in the
* result set.
* </p>
*
* @param metadataRs The ResultSet containing the result of a
* DatabaseMetaData.getColumns call.
*
* @return The AttributeDescriptor built from the ResultSet.
*
* @throws IOException If an error occurs processing the ResultSet.
*/
protected AttributeDescriptor buildAttributeType(ResultSet metadataRs)
throws IOException {
try {
final int TABLE_NAME = 3;
final int COLUMN_NAME = 4;
final int TYPE_NAME = 6;
final int NULLABLE = 11;
String typeName = metadataRs.getString(TYPE_NAME);
if (typeName.equals("geometry")) {
String tableName = metadataRs.getString(TABLE_NAME);
String columnName = metadataRs.getString(COLUMN_NAME);
// check for nullability
int nullCode = metadataRs.getInt( NULLABLE );
boolean nillable = isNullable(nullCode);
return getGeometryAttribute(tableName, columnName, nillable);
} else if("uuid".equals(typeName)) {
String tableName = metadataRs.getString(TABLE_NAME);
String columnName = metadataRs.getString(COLUMN_NAME);
// check for nullability
int nullCode = metadataRs.getInt( NULLABLE );
boolean nillable = isNullable(nullCode);
AttributeTypeBuilder atb = new AttributeTypeBuilder();
atb.setName(columnName);
atb.setBinding(String.class);
atb.setMinOccurs(nillable ? 0 : 1);
atb.setMaxOccurs(1);
return atb.buildDescriptor(columnName);
} else {
return super.buildAttributeType(metadataRs);
}
} catch (SQLException e) {
throw new IOException("Sql error occurred: " + e.getMessage());
}
}
private boolean isNullable(int nullCode) {
boolean nillable = true;
switch( nullCode ) {
case DatabaseMetaData.columnNoNulls:
nillable = false;
break;
case DatabaseMetaData.columnNullable:
nillable = true;
break;
case DatabaseMetaData.columnNullableUnknown:
nillable = true;
break;
}
return nillable;
}
/**
* @see org.geotools.data.jdbc.JDBCDataStore#buildFIDMapperFactory(org.geotools.data.jdbc.JDBCDataStoreConfig)
*/
protected FIDMapperFactory buildFIDMapperFactory(JDBCDataStoreConfig config) {
return new PostgisFIDMapperFactory( config );
}
protected FIDMapper buildFIDMapper( String typeName, FIDMapperFactory factory ) throws IOException {
Connection conn = null;
try {
conn = getConnection(Transaction.AUTO_COMMIT);
String dbSchema = config.getDatabaseSchemaName();
FIDMapper mapper = factory.getMapper(null, dbSchema, typeName, conn);
return mapper;
} finally {
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, null);
}
}
/**
* Returns an attribute type for a geometry column in a feature table.
*
* @param tableName The feature table name.
* @param columnName The geometry column name.
* @param nillable
*
* @return Geometric attribute.
*
* @throws IOException DOCUMENT ME!
*
* @task REVISIT: combine with querySRID, as they use the same select
* statement.
* @task This should probably take a Transaction, so if things mess up then
* we can rollback.
*/
AttributeDescriptor getGeometryAttribute(String tableName, String columnName, boolean nillable)
throws IOException {
Connection dbConnection = null;
Class type = null;
int srid = 0;
int dimension = 2;
try {
dbConnection = getConnection(Transaction.AUTO_COMMIT);
StringBuffer sql = new StringBuffer();
sql.append("SELECT type, coord_dimension FROM geometry_columns WHERE ");
String dbSchema = config.getDatabaseSchemaName();
if (schemaEnabled && dbSchema != null && dbSchema.length() > 0) {
sql.append("f_table_schema='");
sql.append(dbSchema);
sql.append("' AND ");
}
sql.append("f_table_name='");
sql.append(tableName);
sql.append("' AND f_geometry_column='");
sql.append(columnName);
sql.append("';");
String sqlStatement = sql.toString();
LOGGER.fine("geometry type sql statement is " + sqlStatement);
String geometryType = null;
// retrieve the result set from the JDBC driver
Statement statement = dbConnection.createStatement();
ResultSet result = statement.executeQuery(sqlStatement);
if (result.next()) {
geometryType = result.getString("type");
dimension = result.getInt("coord_dimension");
LOGGER.fine("geometry type is: " + geometryType);
if(dimension < 2) {
dimension = 2;
LOGGER.warning("Geometry dimension " + dimension + " + is invalid, assuming 2");
}
}
result.close();
if (geometryType == null) {
//no geometry_columns entry, try grabbing a feature
sql = new StringBuffer();
if (WKBEnabled) {
sql.append("SELECT encode(AsBinary(force_2d(\"");
sql.append(columnName);
sql.append("\"), 'XDR'),'base64') FROM \"");
} else {
sql.append("SELECT AsText(\"");
sql.append(columnName);
sql.append("\") FROM \"");
}
if (schemaEnabled && dbSchema != null && dbSchema.length() > 0) {
sql.append(dbSchema);
sql.append("\".\"");
}
sql.append(tableName);
sql.append("\" LIMIT 1");
sqlStatement = sql.toString();
result = statement.executeQuery(sqlStatement);
if (result.next()) {
AttributeIO attrIO = getGeometryAttributeIO(null, null);
Object object = attrIO.read(result, 1);
if (object instanceof Geometry) {
Geometry geom = (Geometry) object;
geometryType = geom.getGeometryType().toUpperCase();
type = geom.getClass();
srid = geom.getSRID(); //will return 0 unless we support EWKB
}
}
result.close();
}
statement.close();
if (geometryType == null) {
String msg = " no geometry found in the GEOMETRY_COLUMNS table"
+ " for " + tableName + " of the postgis install. A row"
+ " for " + columnName + " is required"
+ " for geotools to work correctly";
throw new DataSourceException(msg);
}
if (type == null) {
type = (Class) GEOM_TYPE_MAP.get(geometryType);
}
} catch (SQLException sqe) {
throw new IOException("An SQL exception occurred: "
+ sqe.getMessage());
} finally {
JDBCUtils.close(dbConnection, Transaction.AUTO_COMMIT, null);
}
if (srid < 1) {
//try again
srid = determineSRID(tableName, columnName);
}
CoordinateReferenceSystem crs = null;
try {
crs = getPostgisAuthorityFactory().createCRS(srid);
} catch (FactoryException e) {
crs = null;
}
return new AttributeTypeBuilder().name(columnName).binding(type)
.nillable(nillable).crs(crs).userData(Hints.COORDINATE_DIMENSION, dimension).buildDescriptor(columnName);
}
private PostgisAuthorityFactory getPostgisAuthorityFactory() {
if (paf == null) {
paf = new PostgisAuthorityFactory(dataSource);
}
return paf;
}
/**
* Gets the sql geometry column name for this type.
*
* @param type DOCUMENT ME!
*
* @return DOCUMENT ME!
*
* @throws RuntimeException DOCUMENT ME!
*
* @task TODO: test this, I can just make sure it compiles.
*/
private String getGeometrySQLTypeName(Class type) {
String res = (String) GEOM_CLASS_MAPPINGS.get(type);
if (res == null) {
throw new RuntimeException("Unknown type name for class " + type
+ " please update GEOMETRY_MAPPINGS");
}
return res;
}
/**
* Creates a FeatureType in this instance of the PostgisDataStore. Since we
* don't yet know which attribute in the FeatureType is the primary key, we
* will create our own called "fid_tablename", which has its own sequence
* called "tablename_fid_seq". The user should not interact with this
* column, although its value will be the FID. This method currently assumes
* there are only 2 dimensions.
*
* @throws IOException
* if something goes horribly wrong or the table already exists
* @see org.geotools.data.DataStore#createSchema(org.geotools.feature.FeatureType)
*/
public void createSchema(SimpleFeatureType featureType) throws IOException {
String tableName = featureType.getTypeName();
String lcTableName = tableName.toLowerCase();
AttributeDescriptor[] attributeType = (AttributeDescriptor[]) featureType.getAttributeDescriptors().toArray(new AttributeDescriptor[featureType.getAttributeDescriptors().size()]);
String dbSchema = config.getDatabaseSchemaName();
PostgisSQLBuilder sqlb = createSQLBuilder();
//the featureType won't tell us who the primary key is, so we'll create
//our own "fid_tablename". Later when we load the featureType, we will
//pretend we didn't see fid_tablename when we return the attributes.
String fidColumn = lcTableName + "_fid";
//make sure the fid column doesn't already exist
for (int i = 0; i < attributeType.length; i++) {
if (attributeType[i].getLocalName().equalsIgnoreCase(fidColumn)) {
String message = "The featuretype cannot contain the column "
+ fidColumn + ", since this is used as the hidden FID column";
throw new IOException(message);
}
}
Connection con = this.getConnection(Transaction.AUTO_COMMIT);
Statement st = null;
boolean shouldExecute = !tablePresent(tableName, con);
try {
con.setAutoCommit(false);
st = con.createStatement();
StringBuffer sql = new StringBuffer("CREATE TABLE ");
sql.append(sqlb.encodeTableName(tableName));
sql.append(" (");
sql.append(sqlb.encodeColumnName(fidColumn));
sql.append(" serial PRIMARY KEY,");
sql.append(makeSqlCreate(attributeType));
sql.append(");");
String sqlStr = sql.toString();
LOGGER.info(sqlStr);
if (shouldExecute) {
st.execute(sqlStr);
}
//fix from pr: it may be that table existed and then was dropped
//without removing its geometry info from GEOMETRY_COLUMNS.
//To support this, try to delete before inserting.
//Preserving case for table names gives problems,
//so convert to lower case
sql = new StringBuffer("DELETE FROM GEOMETRY_COLUMNS WHERE f_table_catalog=''");
sql.append(" AND f_table_schema = '");
sql.append(dbSchema);
sql.append("'");
sql.append("AND f_table_name = '");
sql.append(tableName);
sql.append("';");
//prints statement for later reuse
sqlStr = sql.toString();
LOGGER.info(sqlStr);
if (shouldExecute) {
st.execute(sqlStr);
}
//Ok, so Paolo Rizzi suggested that we get rid of our hand-adding
//of geometry column information and use AddGeometryColumn instead
//as it is better (this is in GEOT-379, he attached an extended
//datastore that does postgis fixes). But I am pretty positive
//the reason we are doing things this way is to preserve the order
//of FeatureTypes. I know this is fairly silly, from most
//information perspectives, but from another perspective it seems
//to make sense - if you were transfering a featureType from one
//data store to another then it should have the same order, right?
//And order is important in WFS. There are a few caveats though
//for one I don't even know if things work right. I imagine the
//proper constraints that a AddGeometryColumn operation does are
//not set in our hand version, for one. I would feel better about
//ignoring the order and just doing things as we like if we had
//views in place, if users could add the schema, and then be able
//to get it back in exactly the order they wanted. So for now
//let's leave things as is, and maybe talk about it in an irc. -ch
for (int i = 0; i < attributeType.length; i++) {
if (!(attributeType[i] instanceof GeometryDescriptor)) {
continue;
}
GeometryDescriptor geomAttribute = (GeometryDescriptor) attributeType[i];
String columnName = attributeType[i].getLocalName();
CoordinateReferenceSystem refSys = geomAttribute.getCoordinateReferenceSystem();
int SRID;
if (refSys != null) {
try {
Set ident = refSys.getIdentifiers();
if ((ident == null || ident.isEmpty()) && refSys == DefaultGeographicCRS.WGS84) {
SRID = 4326;
} else {
String code = ((NamedIdentifier) ident.toArray()[0]).getCode();
SRID = Integer.parseInt(code);
}
} catch (Exception e) {
LOGGER.warning("SRID could not be determined");
SRID = -1;
}
} else {
SRID = -1;
}
// DatabaseMetaData metaData = con.getMetaData();
// ResultSet rs = metaData.getCatalogs();
// rs.next();
//
// //String dbName = rs.getString(1);
// rs.close();
String typeName = null;
//this construct seems unnecessary, since we already would
//pass over if this wasn't a geometry...
Class type = geomAttribute.getType().getBinding();
int dimension = 2;
typeName = getGeometrySQLTypeName(type);
GeometryDescriptor gd = (GeometryDescriptor) geomAttribute;
if(gd.getUserData().get(Hints.COORDINATE_DIMENSION) instanceof Integer) {
dimension = (Integer) gd.getUserData().get(Hints.COORDINATE_DIMENSION);
}
if (typeName != null) {
// statementSQL = new StringBuffer(
// "SELECT AddGeometryColumn('" + dbSchema + "','"
// + tableName + "','" + attributeType[i].getName()
// + "','" + SRID + "','" + typeName + "',2);"
// ); //assumes 2-D
//add a row to the geometry_columns table
sql = new StringBuffer("INSERT INTO GEOMETRY_COLUMNS VALUES (");
sql.append("'','");
sql.append(dbSchema);
sql.append("','");
sql.append(tableName);
sql.append("','");
sql.append(columnName);
sql.append("',");
sql.append(dimension);
sql.append(",");
sql.append(SRID);
sql.append(",'");
sql.append(typeName);
sql.append("');");
sqlStr = sql.toString();
LOGGER.info(sqlStr);
if (shouldExecute) {
st.execute(sqlStr);
}
//add geometry constaints to the table
if (SRID > -1) {
sql = new StringBuffer("ALTER TABLE ");
sql.append(sqlb.encodeTableName(tableName));
sql.append(" ADD CONSTRAINT enforce_srid_");
sql.append( columnName );
sql.append(" CHECK (SRID(");
sql.append(sqlb.encodeColumnName(columnName));
sql.append(") = ");
sql.append(SRID);
sql.append(");");
sqlStr = sql.toString();
LOGGER.info(sqlStr);
if (shouldExecute) {
st.execute(sqlStr);
}
}
sql = new StringBuffer("ALTER TABLE ");
sql.append(sqlb.encodeTableName(tableName));
sql.append(" ADD CONSTRAINT enforce_dims_");
sql.append(columnName);
sql.append(" CHECK (ndims(");
sql.append(sqlb.encodeColumnName(columnName));
sql.append(") = ");
sql.append(dimension);
sql.append(");");
sqlStr = sql.toString();
LOGGER.info(sqlStr);
if (shouldExecute) {
st.execute(sqlStr);
}
if (!typeName.equals("GEOMETRY")) {
sql = new StringBuffer("ALTER TABLE ");
sql.append(sqlb.encodeTableName(tableName));
sql.append(" ADD CONSTRAINT enforce_geotype_");
sql.append(columnName);
sql.append(" CHECK (geometrytype(");
sql.append(sqlb.encodeColumnName(columnName));
sql.append(") = '");
sql.append(typeName);
sql.append("'::text OR ");
sql.append(sqlb.encodeColumnName(columnName));
sql.append(" IS NULL);");
sqlStr = sql.toString();
LOGGER.info(sqlStr);
if (shouldExecute) {
st.execute(sqlStr);
}
}
} else {
LOGGER.warning("Error: " + geomAttribute.getLocalName()+ " unknown type!!!");
}
//also build a spatial index on each geometry column.
sql = new StringBuffer("CREATE INDEX spatial_");
sql.append(tableName);
sql.append("_");
sql.append(attributeType[i].getLocalName().toLowerCase());
sql.append(" ON ");
sql.append(sqlb.encodeTableName(tableName));
sql.append(" USING GIST (");
sql.append(sqlb.encodeColumnName(attributeType[i].getLocalName()));
sql.append(");");
sqlStr = sql.toString();
LOGGER.info(sqlStr);
if (shouldExecute) {
st.execute(sqlStr);
}
}
con.commit();
} catch (SQLException e) {
try {
if (con != null) {
con.rollback();
}
} catch (SQLException sqle) {
throw new IOException(sqle.getMessage());
}
throw (IOException) new IOException(e.getMessage()).initCause( e );
} finally {
try {
if (st != null) {
st.close();
}
} catch (SQLException e) {
throw new IOException(e.getMessage());
} finally {
try {
if (con != null) {
con.setAutoCommit(true);
con.close();
}
} catch (SQLException e) {
throw new IOException(e.getMessage());
}
}
}
if (!shouldExecute) {
throw new IOException("The table " + tableName + " already exists.");
}
}
// /**
// * Returns the sql type name given the SQL type code
// *
// * @param typeCode
// *
// *
// * @throws RuntimeException DOCUMENT ME!
// */
// private String getSQLTypeName(int typeCode) {
// Class typeClass = (Class) TYPE_MAPPINGS.get(new Integer(typeCode));
//
// if (typeClass == null) {
// throw new RuntimeException("Unknown type " + typeCode
// + " please update TYPE_MAPPINGS");
// }
//
// String typeName = (String) CLASS_MAPPINGS.get(typeClass);
//
// if (typeName == null) {
// throw new RuntimeException("Unknown type name for class "
// + typeClass.getName() + " please update CLASS_MAPPINGS");
// }
//
// return typeName;
// }
private StringBuffer makeSqlCreate(AttributeDescriptor[] attributeType)
throws IOException {
StringBuffer buf = new StringBuffer("");
for (int i = 0; i < attributeType.length; i++) {
String typeName = null;
typeName = (String) CLASS_MAPPINGS.get(attributeType[i].getType().getBinding());
if (typeName == null)
typeName = (String) GEOM_CLASS_MAPPINGS.get(attributeType[i].getType().getBinding());
if (typeName != null) {
if (attributeType[i] instanceof GeometryDescriptor) {
typeName = "GEOMETRY";
} else if (typeName.equals("VARCHAR")) {
int length = FeatureTypes.getFieldLength(attributeType[i]);
if (length < 1) {
LOGGER.warning("FeatureType did not specify string length; defaulted to 256");
length = 256;
}
else if (length > MAX_ALLOWED_VALUE)
{
length = MAX_ALLOWED_VALUE;
}
typeName = typeName + "(" + length + ")";
}
if (!attributeType[i].isNillable()) {
typeName = typeName + " NOT NULL";
}
//TODO review!!! Is toString() always OK???
Object defaultValue = attributeType[i].getDefaultValue();
if (defaultValue != null) {
typeName = typeName + " DEFAULT '"
+ defaultValue.toString() + "'";
}
buf.append(" \"" + attributeType[i].getLocalName() + "\" " + typeName + ",");
} else {
String msg;
if (attributeType[i] == null) {
msg = "AttributeDescriptor was null!";
} else {
msg = "Type '" + attributeType[i].getType().getBinding() + "' not supported!";
}
throw (new IOException(msg));
}
}
return buf.deleteCharAt(buf.length()-1);
}
/**
* DOCUMENT ME!
*
* @param table
* @param con
*
*
* @throws IOException DOCUMENT ME!
* @throws DataSourceException DOCUMENT ME!
*/
private boolean tablePresent(String table, Connection con)
throws IOException {
final int TABLE_NAME_COL = 3;
Connection conn = null;
//List list = new ArrayList();
try {
conn = getConnection(Transaction.AUTO_COMMIT);
DatabaseMetaData meta = conn.getMetaData();
String[] tableType = { "TABLE" };
ResultSet tables = meta.getTables(null,
config.getDatabaseSchemaName(), "%", tableType);
while (tables.next()) {
String tableName = tables.getString(TABLE_NAME_COL);
if (allowTable(tableName) && (tableName != null)
&& (tableName.equalsIgnoreCase(table))) {
return (true);
}
}
return false;
} catch (SQLException sqlException) {
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, sqlException);
conn = null;
String message = "Error querying database for list of tables:"
+ sqlException.getMessage();
throw new DataSourceException(message, sqlException);
} finally {
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, null);
}
}
/**
* DOCUMENT ME!
*
* @param query
*
*
* @throws IOException DOCUMENT ME!
*/
/**
* Get propertyNames in a safe manner.
*
* <p>
* Method wil figure out names from the schema for query.getTypeName(), if
* query getPropertyNames() is <code>null</code>, or
* query.retrieveAllProperties is <code>true</code>.
* </p>
*
* @param query
*
*
* @throws IOException
*/
/*
private String[] propertyNames(Query query) throws IOException {
String[] names = query.getPropertyNames();
if ((names == null) || query.retrieveAllProperties()) {
String typeName = query.getTypeName();
FeatureType schema = getSchema(typeName);
names = new String[schema.getAttributeCount()];
for (int i = 0; i < schema.getAttributeCount(); i++) {
names[i] = schema.getAttributeType(i).getName();
}
}
return names;
}
*/
/**
* @see org.geotools.data.DataStore#updateSchema(java.lang.String,
* org.geotools.feature.FeatureType)
*/
public void updateSchema(String typeName, SimpleFeatureType featureType)
throws IOException {
throw new IOException("PostgisDataStore.updateSchema not yet implemented");
//TODO: implement updateSchema
}
/**
* Default implementation based on getFeatureReader and getFeatureWriter.
*
* <p>
* We should be able to optimize this to only get the RowSet once
* </p>
*
* @see org.geotools.data.DataStore#getFeatureSource(java.lang.String)
*/
public FeatureSource<SimpleFeatureType, SimpleFeature> getFeatureSource(String typeName)
throws IOException {
if (!typeHandler.getFIDMapper(typeName).isVolatile()
|| allowWriteOnVolatileFIDs) {
LOGGER.fine("get Feature source called on " + typeName);
if (OPTIMIZE_MODE == OPTIMIZE_SQL) {
LOGGER.fine("returning pg feature locking");
return createFeatureLockingInternal(this, getSchema(typeName));
}
// default
if (getLockingManager() != null) {
// Use default JDBCFeatureLocking that delegates all locking
// the getLockingManager
LOGGER.fine("returning jdbc feature locking");
return new JDBCFeatureLocking(this, getSchema(typeName));
} else {
LOGGER.fine(
"returning jdbc feature store (lock manager is null)");
// subclass should provide a FeatureLocking implementation
// but for now we will simply forgo all locking
return new JDBCFeatureStore(this, getSchema(typeName));
}
} else {
return new JDBCFeatureSource(this, getSchema(typeName));
}
}
public PostgisFeatureLocking createFeatureLockingInternal(
PostgisDataStore ds, SimpleFeatureType type
) throws IOException {
return new PostgisFeatureLocking(ds,type);
}
/**
* DOCUMENT ME!
*
* @param fReader
* @param queryData
*
*
* @throws IOException DOCUMENT ME!
*/
protected JDBCFeatureWriter createFeatureWriter(FeatureReader <SimpleFeatureType, SimpleFeature> fReader,
QueryData queryData) throws IOException {
PostgisSQLBuilder sqlBuilder =
(PostgisSQLBuilder) getSqlBuilder(fReader.getFeatureType().getTypeName());
PostgisFeatureWriter postgisFeatureWriter = new PostgisFeatureWriter(fReader, queryData, WKBEnabled,byteaWKB,sqlBuilder);
return postgisFeatureWriter;
}
/**
* Retrieve a FeatureWriter over entire dataset.
*
* <p>
* Quick notes: This FeatureWriter is often used to add new content, or
* perform summary calculations over the entire dataset.
* </p>
*
* <p>
* Subclass may wish to implement an optimized featureWriter for these
* operations.
* </p>
*
* <p>
* It should provide Feature for next() even when hasNext() is
* <code>false</code>.
* </p>
*
* <p>
* Subclasses are responsible for checking with the lockingManger unless
* they are providing their own locking support.
* </p>
*
* @param typeName
* @param transaction
*
*
* @throws IOException
*
* @see org.geotools.data.DataStore#getFeatureWriter(java.lang.String,
* boolean, org.geotools.data.Transaction)
*/
public FeatureWriter<SimpleFeatureType, SimpleFeature> getFeatureWriter(String typeName,
Transaction transaction) throws IOException {
return getFeatureWriter(typeName, Filter.INCLUDE, transaction);
}
/*
* (non-Javadoc)
*
* @see org.geotools.data.DataStore#getFeatureWriterAppend(java.lang.String,
* org.geotools.data.Transaction)
*/
/**
* Retrieve a FeatureWriter for creating new content.
*
* <p>
* Subclass may wish to implement an optimized featureWriter for this
* operation. One based on prepared statements is a possibility, as we do
* not require a ResultSet.
* </p>
*
* <p>
* To allow new content the FeatureWriter should provide Feature for next()
* even when hasNext() is <code>false</code>.
* </p>
*
* <p>
* Subclasses are responsible for checking with the lockingManger unless
* they are providing their own locking support.
* </p>
*
* @param typeName
* @param transaction
*
*
* @throws IOException
*
* @see org.geotools.data.DataStore#getFeatureWriter(java.lang.String,
* boolean, org.geotools.data.Transaction)
*/
public FeatureWriter<SimpleFeatureType, SimpleFeature> getFeatureWriterAppend(String typeName,
Transaction transaction) throws IOException {
FeatureWriter<SimpleFeatureType, SimpleFeature> writer = getFeatureWriter(typeName, Filter.EXCLUDE,
transaction);
while (writer.hasNext()) {
writer.next(); // this would be a use for skip then :-)
}
return writer;
}
int getSRID(String typeName, String geomColName) throws IOException {
return typeHandler.getFeatureTypeInfo(typeName).getSRID(geomColName);
}
/**
* @see org.geotools.data.jdbc.JDBCDataStore#getGeometryAttributeIO(org.geotools.feature.AttributeDescriptor)
*/
protected AttributeIO getGeometryAttributeIO(AttributeDescriptor type,
QueryData queryData) {
// grab the crs if available
GeometryDescriptor geometryType = (GeometryDescriptor) type;
CoordinateReferenceSystem crs = null;
if(geometryType != null)
crs = geometryType.getCoordinateReferenceSystem();
Hints hints = queryData != null ? queryData.getHints() : GeoTools.getDefaultHints();
if(geometryType != null && geometryType.getUserData().get(Hints.COORDINATE_DIMENSION) instanceof Integer) {
hints.put(Hints.COORDINATE_DIMENSION, geometryType.getUserData().get(Hints.COORDINATE_DIMENSION));
}
int D = (crs == null || Boolean.TRUE.equals( queryData.getHints().get( Hints.FEATURE_2D )))
? 2 : crs.getCoordinateSystem().getDimension();
if (WKBEnabled) {
return new PgWKBAttributeIO(isByteaEnabled(), hints);
} else {
if( D == 3 ){
return new EWKTAttributeIO();
}
else {
return new WKTAttributeIO();
}
}
}
protected int getResultSetType(boolean forWrite) {
return ResultSet.TYPE_FORWARD_ONLY;
}
protected int getConcurrency(boolean forWrite) {
return ResultSet.CONCUR_READ_ONLY;
}
/**
* Returns true if the WKB format is used to transfer geometries, false
* otherwise
*
*/
public boolean isWKBEnabled() {
return WKBEnabled;
}
/**
* If turned on, WKB will be used to transfer geometry data instead of WKT
*
* @param enabled
*/
public void setWKBEnabled(boolean enabled) {
WKBEnabled = enabled;
}
/**
* Sets this postgis instance to use a less strict but faster bounding box
* query. Setting this to <tt>true</tt> will have PostGIS issue bounding
* box queries against the envelope of the geometry, so some may be
* <i>slighty</i> wrong, but will perform much faster. The intersects
* function can still be used to obtain the exact query.
*
* @param isLooseBbox <tt>true</tt> if this should have a loose Bbox.
*/
public void setLooseBbox(boolean isLooseBbox) {
this.looseBbox = isLooseBbox;
}
/**
* Whether the bounding boxes issued against this postgis datastore are on
* the envelope of the geometry or the actual geometry.
*
* @return <tt>true</tt> if the bounding box is 'loose', against the
* envelope instead of the actual geometry.
*/
public boolean isLooseBbox() {
return looseBbox;
}
/**
* Returns true if the data store is using the bytea function to fasten WKB
* data transfer, false otherwise
*
*/
public boolean isByteaEnabled() {
return byteaEnabled;
}
public void setByteaWKB(boolean byteaWKB) {
this.byteaWKB = byteaWKB;
}
public boolean isByteaWKB()
{
return byteaWKB;
}
/**
* Enables the use of bytea function for WKB data transfer (will improve
* performance). Note this function need not be set by the programmer, as
* the datastore will use it to optimize performance whenever it can (when
* postGIS is 0.7.2 or later)
*
* @param byteaEnabled
*/
public void setByteaEnabled(boolean byteaEnabled) {
this.byteaEnabled = byteaEnabled;
}
/**
* Enables the use of the 'estimated_extent' function for bounds computation.
* <p>
* Beware that this function is an approximation and is dependent on the
* degree to with the data in the actual bounds follows a uniform distribution.
* </p>
*/
public void setEstimatedExtent(boolean estimatedExtent) {
this.estimatedExtent = estimatedExtent;
//also make sure optimize mode is set properly
if ( estimatedExtent ) {
LOGGER.info( "Setting OPTIMIZE_MODE to 'SQL'" );
setOptimizeMode( OPTIMIZE_SQL );
}
}
/**
* @see {@link #setEstimatedExtent(boolean)}.
*/
public boolean isEstimatedExtent() {
return estimatedExtent;
}
/**
* Sets the optimization mode for the datastore.
*
* @param mode One of {@link #OPTIMIZE_SAFE},{@link #OPTIMIZE_SQL}.
*/
public void setOptimizeMode( int mode ) {
OPTIMIZE_MODE = mode;
}
public int getOptimizeMode() {
return OPTIMIZE_MODE;
}
public SimpleFeatureType getSchema(String arg0) throws IOException {
return super.getSchema(arg0);
}
/**
* Obtains the postgis datastore connection pool.
*
* @return ConnectionPool
*/
public DataSource getDataSource() {
return dataSource;
}
/**
* Obtains database specific information, such as version, supported
* functions, etc.
*/
public PostgisDBInfo getDBInfo() {
if (dbInfo == null) {
Connection conn;
try {
conn = getConnection(Transaction.AUTO_COMMIT);
dbInfo = new PostgisDBInfo(conn);
} catch (IOException e1) {
LOGGER.log(Level.SEVERE, "Could not obtain DBInfo object", e1);
}
}
return dbInfo;
}
/**
* Returns the JDBC type constant (as in {@link Types}) that maps to the given
* Java class binding when constructing attribute types, or null if no such mapping exist.
*
* @param attributeTypeBinding
* @return
*/
public Integer getJdbcType(Class attributeTypeBinding){
Map.Entry entry;
Integer jdbcType = null;
Class binding;
for(Iterator it = TYPE_MAPPINGS.entrySet().iterator(); it.hasNext();){
entry = (Map.Entry) it.next();
binding = (Class) entry.getValue();
if(binding.equals(attributeTypeBinding)){
jdbcType = (Integer)entry.getKey();
break;
}
}
return jdbcType;
}
/**
* The hints supported by this datastore depending on the configuration
*/
private static final Set BASE_HINTS = Collections.unmodifiableSet(
new HashSet(Arrays.asList(new Object[] {
Hints.FEATURE_DETACHED})));
private static final Set WKB_HINTS = Collections.unmodifiableSet(
new HashSet(Arrays.asList(new Object[] {
Hints.FEATURE_DETACHED,
Hints.JTS_COORDINATE_SEQUENCE_FACTORY,
Hints.JTS_GEOMETRY_FACTORY})));
public Set getSupportedHints() {
if(isWKBEnabled()) {
return WKB_HINTS;
} else {
return BASE_HINTS;
}
}
}