/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
* (C) Copyright IBM Corporation, 2005-2007. All rights reserved.
*
* 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.db2;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
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.Query;
import org.geotools.data.ReTypeFeatureReader;
import org.geotools.data.Transaction;
import org.geotools.data.db2.filter.SQLEncoderDB2;
import org.geotools.data.jdbc.FeatureTypeHandler;
import org.geotools.data.jdbc.FeatureTypeInfo;
import org.geotools.data.jdbc.FilterToSQL;
import org.geotools.data.jdbc.JDBCDataStore;
import org.geotools.data.jdbc.JDBCDataStoreConfig;
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.fidmapper.FIDMapper;
import org.geotools.data.jdbc.fidmapper.FIDMapperFactory;
import org.geotools.feature.AttributeTypeBuilder;
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.crs.CoordinateReferenceSystem;
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;
/**
* DB2 DataStore implementation.
*
* <p>
* Instances of this class should only be obtained via
* DB2DataStoreFactory.createDataStore or DataStoreFinder.getDataStore.
* </p>
*
* @author David Adler - IBM Corporation
* @source $URL$
*/
public class DB2DataStore extends JDBCDataStore implements DataStore{
private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(
"org.geotools.data.db2");
/**
* List of all the DB2 geometry type names and the corresponding JTS class
*/
private static final Map<String, Class<?>> DB2_GEOM_TYPE_MAPPING = new HashMap<String, Class<?>>();
/**
* Populate list of geometry classes supported by DB2
*/
static {
DB2_GEOM_TYPE_MAPPING.put("ST_POINT", Point.class);
DB2_GEOM_TYPE_MAPPING.put("ST_LINESTRING", LineString.class);
DB2_GEOM_TYPE_MAPPING.put("ST_POLYGON", Polygon.class);
DB2_GEOM_TYPE_MAPPING.put("ST_MULTIPOINT", MultiPoint.class);
DB2_GEOM_TYPE_MAPPING.put("ST_MULTILINESTRING", MultiLineString.class);
DB2_GEOM_TYPE_MAPPING.put("ST_MULTIPOLYGON", MultiPolygon.class);
}
/** Reference to DB2 in-memory spatial catalog */
private DB2SpatialCatalog catalog;
/** The URL for this particular data store */
private String dbURL = null;
/** Time since catalog was last reloaded */
private long lastTypeNameRequestTime = 0;
/**
* The only supported constructor for a DB2DataStore. This constructor is
* mainly intended to be called from DB2DataStoreFactory.
*
* @param connectionPool the initialized DB2 connection pool
* @param config the JDBCDataStoreConfiguration
* @param dbURL the database URL of the form
* <code>jdbc:db2://hostname:hostport/dbname </code>
*
* @throws IOException
*/
public DB2DataStore(DataSource dataSource,
JDBCDataStoreConfig config, String dbURL) throws IOException {
super(dataSource, config);
if (dataSource == null) {
throw new IOException("DataSource pool is null");
}
this.dbURL = dbURL;
// Get an instance of the DB2 spatial catalog for this database.
// We need to provide a database connection in case the spatial catalog instance
// does not already exist and we need to issue SQL against the database.
Connection conn = this.getConnection(Transaction.AUTO_COMMIT);
try {
this.catalog = DB2SpatialCatalog.getInstance(dbURL,
config.getDatabaseSchemaName(), conn);
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, null);
} catch (SQLException e) {
LOGGER.info("DB2SpatialCatalog create failed: " + e);
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, e);
throw new IOException("DB2SpatialCatalog create failed");
}
}
public DataSource getDataSource() {
return dataSource;
}
/**
* Handles DB2-specific geometry types. If it isn't one, just let the
* parent method handle it.
*
* @param rs The ResultSet containing the result of a
* DatabaseMetaData.getColumns call.
*
* @return The AttributeType built from the ResultSet.
*
* @throws IOException If an error occurs processing the ResultSet.
*/
protected AttributeDescriptor buildAttributeType(ResultSet rs)
throws IOException {
try {
String spatialTypePrefix = "\"DB2GSE\".\"ST_";
int dataType = rs.getInt("DATA_TYPE");
String typeName = rs.getString("TYPE_NAME");
String tableSchema = rs.getString("TABLE_SCHEM");
String tableName = rs.getString("TABLE_NAME");
String columnName = rs.getString("COLUMN_NAME");
if (!typeName.startsWith(spatialTypePrefix)) {
return super.buildAttributeType(rs);
}
String geomTypeName = this.catalog.getDB2GeometryTypeName(tableSchema,
tableName, columnName);
// Look up the geometry type we just got back and create the
// corresponding attribute type for the geometry class, if it was found.
Class geomClass = (Class) DB2_GEOM_TYPE_MAPPING.get(geomTypeName);
if (geomClass != null) {
CoordinateReferenceSystem crs;
try {
crs = this.catalog.getCRS(tableSchema, tableName, columnName);
} catch (Exception e) {
throw new IOException("Exception: " + e.getMessage());
}
AttributeTypeBuilder atb = new AttributeTypeBuilder();
atb.setName(columnName);
atb.setBinding(geomClass);
atb.setNillable(true);
atb.setLength(0);
atb.setDefaultValue(null);
atb.setCRS(crs);
GeometryDescriptor geometryAttribute = (GeometryDescriptor) atb
.buildDescriptor(columnName);
return geometryAttribute;
}
// It is some other unrecognized structured type - log what we found and return null
LOGGER.fine("Type '" + geomTypeName + "' is not recognized");
return null;
} catch (SQLException e) {
throw new IOException("SQL exception occurred: " + e.getMessage());
}
}
/**
* Creates a DB2-specific FIDMapperFactory.
*
* @param config not used.
*
* @return a DB2FIDMapperFactory
*/
protected FIDMapperFactory buildFIDMapperFactory(JDBCDataStoreConfig config) {
return new DB2FIDMapperFactory(config.getDatabaseSchemaName());
}
/**
* Get the SRID associated with a geometry column.
*
* <p>
* The value returned is the EPSG coordinate system identifier, not the DB2
* srs_id.
* </p>
*
* @param tableName The name of the table to get the SRID for.
* @param geometryColumnName The name of the geometry column within the
* table to get SRID for.
*
* @return The SRID for the geometry column or -1.
*
* @throws IOException
*/
protected int determineSRID(String tableName, String geometryColumnName)
throws IOException {
int srid = -1;
// Not sure that this is actually the right value to use
String tableSchema = this.config.getDatabaseSchemaName();
srid = this.catalog.getCsId(tableSchema, tableName, geometryColumnName);
LOGGER.fine(DB2SpatialCatalog.geomID(tableSchema, tableName,
geometryColumnName) + " srid=" + srid);
return srid;
}
/**
* Gets the database URL.
*
* @return the database URL.
*/
String getDbURL() {
return this.dbURL;
}
/**
* Create a DB2-specific FeatureTypeHandler.
*
* @param config a JDBCDataStoreConfig.
*
* @return a DB2FeatureTypeHandler.
*
* @throws IOException if the feature type handler could not be created.
*/
protected FeatureTypeHandler getFeatureTypeHandler(
JDBCDataStoreConfig config) throws IOException {
return new DB2FeatureTypeHandler(this, buildFIDMapperFactory(config),
config.getTypeHandlerTimeout());
}
/**
* Gets the handler to convert a geometry database value to a JTS geometry.
*
* @param type not used.
* @param queryData not used.
*
* @return AttributIO
*/
protected AttributeIO getGeometryAttributeIO(AttributeDescriptor type,
QueryData queryData) {
return new DB2WKBAttributeIO();
}
/**
* Gets the instance of the DB2SpatialCatalog associated with this data
* store.
*
* @return a DB2SpatialCatalog
*/
DB2SpatialCatalog getSpatialCatalog() {
return this.catalog;
}
/**
* Gets the DB2-specific SQL builder object.
*
* @param typeName Name of the type to build the SQL for.
*
* @return DB2SQLBuilder
*
* @throws IOException
*/
public SQLBuilder getSqlBuilder(String typeName) throws IOException {
FeatureTypeInfo info = this.typeHandler.getFeatureTypeInfo(typeName);
int srid = 0;
SQLEncoderDB2 encoder = new SQLEncoderDB2();
encoder.setSqlNameEscape("\"");
FIDMapper mapper = getFIDMapper(typeName);
encoder.setFIDMapper(mapper);
if (info.getSchema().getGeometryDescriptor() != null) {
String geom = info.getSchema().getGeometryDescriptor().getLocalName();
srid = this.catalog.getSRID(getTableSchema(), typeName, geom);
}
encoder.setSRID(srid);
// We should probably get the table schema name from the feature type
// information - not sure that it exists there.
return new DB2SQLBuilder((FilterToSQL) encoder, getTableSchema(), info.getSchema(), mapper);
}
/**
* Gets the names of tables (types) that contain a spatial column. Note
* that there is still an issue concerning the ambiguity of spatial tables
* that have the same table name but different table schema names.
*
* @return Array of type names as Strings.
*
* @throws IOException if the spatial catalog can not be accessed.
*/
public String[] getTypeNames() throws IOException {
long lastTime = lastTypeNameRequestTime;
long now = System.currentTimeMillis();
lastTypeNameRequestTime = now;
if (lastTime < (now - config.getTypeHandlerTimeout())) {
refreshCatalog();
}
return getSpatialCatalog().getTypeNames();
}
/**
* Reloads the spatial catalog from the database.
* <p>
* This is useful in case anything changed after the datastore was initialized.
* @throws IOException
* @throws SQLException
*/
public void refreshCatalog() throws IOException {
try {
Connection conn = getConnection(Transaction.AUTO_COMMIT);
getSpatialCatalog().loadCatalog(conn,getTableSchema());
lastTypeNameRequestTime = System.currentTimeMillis();
JDBCUtils.close(conn, Transaction.AUTO_COMMIT, null);
LOGGER.fine("Refreshing spatial catalog");
} catch (SQLException e) {
throw(new IOException(e.getLocalizedMessage()));
}
}
/**
* Gets the table schema associated with this data store.
*
* <p>
* At some point this may change if multiple schemas are supported by a
* data store.
* </p>
*
* @return the schema name that will prefix table names.
*/
public String getTableSchema() {
return this.config.getDatabaseSchemaName();
}
/**
* Gets a DB2-specific feature source.
*
* @param typeName
*
* @return a DB2Feature Source, Store or Locking
*
* @throws IOException if the feature source could not be created.
*
* @see org.geotools.data.DataStore#getFeatureSource(java.lang.String)
*/
public FeatureSource<SimpleFeatureType, SimpleFeature> getFeatureSource(String typeName)
throws IOException {
if (!this.typeHandler.getFIDMapper(typeName).isVolatile()
|| this.allowWriteOnVolatileFIDs) {
if (getLockingManager() != null) {
// Use default JDBCFeatureLocking that delegates all locking
// the getLockingManager
//
return new DB2FeatureLocking(this, getSchema(typeName));
} else {
// subclass should provide a FeatureLocking implementation
// but for now we will simply forgo all locking
return new DB2FeatureStore(this, getSchema(typeName));
}
} else {
return new DB2FeatureSource(this, getSchema(typeName));
}
}
/**
* Overrides the method in JDBCDataStore because it includes
* PostGIS-specific handling to setAutoCommit(false) which causes problems
* for DB2 because the transaction is still uncommitted when the
* connection is closed.
*
* @param featureTypeInfo
* @param tableName
* @param sqlQuery The SQL query to execute.
* @param transaction The Transaction is included here for handling
* transaction connections at a later stage. It is not currently
* used.
* @param forWrite
*
* @return The QueryData object that contains the resources for the query.
*
* @throws IOException
* @throws DataSourceException If an error occurs performing the query.
*/
protected QueryData executeQuery(FeatureTypeInfo featureTypeInfo,
String tableName, String sqlQuery, Transaction transaction,
boolean forWrite) throws IOException {
LOGGER.fine("About to execute query: " + sqlQuery);
Connection conn = null;
Statement statement = null;
ResultSet rs = null;
try {
conn = getConnection(transaction);
statement = conn.createStatement(getResultSetType(forWrite),
getConcurrency(forWrite));
statement.setFetchSize(200);
int rsc1 = statement.getResultSetConcurrency();
rs = statement.executeQuery(sqlQuery);
int rsc2 = statement.getResultSetConcurrency();
int c = rs.getConcurrency();
int update = ResultSet.CONCUR_UPDATABLE;
int read = ResultSet.CONCUR_READ_ONLY;
return new QueryData(featureTypeInfo, this, conn, statement, rs,
transaction);
} catch (SQLException e) {
// if an error occurred we close the resources
String msg = "Error Performing SQL query: " + sqlQuery;
LOGGER.log(Level.SEVERE, msg, e);
JDBCUtils.close(rs);
JDBCUtils.close(statement);
JDBCUtils.close(conn, transaction, e);
throw new DataSourceException(msg, e);
}
}
/**
* Overrides the method in JDBCDataStore so that a DB2FeatureWriter is
* created.
*
* @param featureReader
* @param queryData
*
* @return The DB2FeatureWriter
*
* @throws IOException
*
* @see org.geotools.data.jdbc.JDBCDataStore#createFeatureWriter(org.geotools.data.FeatureReader,
* org.geotools.data.jdbc.QueryData)
*/
protected JDBCFeatureWriter createFeatureWriter(
FeatureReader featureReader, QueryData queryData)
throws IOException {
String featureName = queryData.getFeatureType().getTypeName();
return new DB2FeatureWriter(featureReader, queryData,
(DB2SQLBuilder) getSqlBuilder(featureName));
}
/**
* 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 the is 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,
* 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);
LOGGER.fine("requestType: " + requestType);
LOGGER.fine("schemaType: " + schemaType);
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.INCLUDE) || filter.equals(Filter.INCLUDE)) {
return new EmptyFeatureReader(requestType);
}
FeatureReader<SimpleFeatureType, SimpleFeature> reader = getFeatureReader(query, transaction);
if (compare == 1) {
reader = new ReTypeFeatureReader(reader, requestType);
}
return reader;
}
}