/*
* 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.mysql;
import java.io.IOException;
import java.sql.*;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.*;
import java.util.logging.Logger;
import javax.sql.DataSource;
import org.geotools.data.jdbc.JDBCUtils;
import org.geotools.data.DataSourceException;
import org.geotools.data.FeatureReader;
import org.geotools.data.FeatureWriter;
import org.geotools.data.Transaction;
import org.geotools.data.jdbc.JDBCDataStore;
import org.geotools.data.jdbc.JDBCDataStoreConfig;
import org.geotools.data.jdbc.JDBCFeatureWriter;
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.WKBAttributeIO;
import org.geotools.data.jdbc.attributeio.WKTAttributeIO;
import org.geotools.data.jdbc.datasource.DataSourceUtil;
import org.geotools.factory.Hints;
import org.geotools.feature.AttributeTypeBuilder;
import org.opengis.filter.Filter;
import org.geotools.filter.SQLEncoderMySQL;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.geotools.referencing.CRS;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
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;
/**
* An implementation of the GeoTools Data Store API for the MySQL database platform.
* The plan is to support traditional MySQL datatypes, as well as the new geometric
* datatypes provided with MySQL 4.1 and higher.<br>
* <br>
* TODO: MySQLDataStore is not yet tested for MySQL 4.1's geometric datatypes.<br>
* <br>
* Please see {@link org.geotools.data.jdbc.JDBCDataStore class JDBCDataStore} and
* {@link org.geotools.data.DataStore interface DataStore} for DataStore usage details.
* @author Gary Sheppard garysheppard@psu.edu
* @author Andrea Aime aaime@users.sourceforge.net
* @author Debasish Sahu debasish.sahu@rmsi.com
* @source $URL$
*/
public class MySQLDataStore extends JDBCDataStore {
/** The logger for the mysql module. */
private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.data.mysql");
/**
* When true wkb encoding will be used to transfer geometries over the wire
*/
protected boolean wkbEnabled;
/**
* Basic constructor for MySQLDataStore.
* be done similar to the following:<br>
* @param dataSource A source of connections for this datastore
* @throws IOException if the database cannot be properly accessed
* @see javax.sql.DataSource
* @see org.geotools.data.mysql.MySQLConnectionFactory
*/
public MySQLDataStore(DataSource dataSource) throws IOException {
this(dataSource, null);
}
/**
* Constructor for MySQLDataStore where the database schema name is provided.
* @param dataSource A source of connections for this datastore
* @param databaseSchemaName the database schema. Can be null. See the comments for the parameter schemaPattern in {@link java.sql.DatabaseMetaData#getTables(String, String, String, String[]) DatabaseMetaData.getTables}, because databaseSchemaName behaves in the same way.
* @throws IOException if the database cannot be properly accessed
*/
public MySQLDataStore(DataSource dataSource, String databaseSchemaName)
throws IOException {
this(dataSource, databaseSchemaName, null);
}
/**
* Constructor for MySQLDataStore where the database schema name is provided.
* @param dataSource A source of connections for this datastore
* @param databaseSchemaName the database schema. Can be null. See the comments for the parameter schemaPattern in {@link java.sql.DatabaseMetaData#getTables(String, String, String, String[]) DatabaseMetaData.getTables}, because databaseSchemaName behaves in the same way.
* @param namespace the namespace for this data store. Can be null, in which case the namespace will simply be the schema name.
* @throws IOException if the database cannot be properly accessed
*/
public MySQLDataStore(DataSource dataSource, String databaseSchemaName, String namespace)
throws IOException {
super(dataSource,
JDBCDataStoreConfig.createWithNameSpaceAndSchemaName(namespace, databaseSchemaName));
}
/**
* A utility method for creating a MySQLDataStore from database connection parameters,
* using the default port (3306) for MySQL.
* @param host the host name or IP address of the database server
* @param schema the name of the database instance
* @param username the database username
* @param password the password corresponding to <code>username</code>
* @return a MySQLDataStore for the specified parameters
*/
public static MySQLDataStore getInstance(String host, String schema, String username,
String password) throws IOException, SQLException {
return getInstance(host, 3306, schema, username, password);
}
/**
* Utility method for creating a MySQLDataStore from database connection parameters.
* @param host the host name or IP address of the database server
* @param port the port number of the database
* @param schema the name of the database instance
* @param username the database username
* @param password the password corresponding to <code>username</code>
* @throws IOException if the MySQLDataStore cannot be created because the database cannot be properly accessed
* @throws SQLException if a MySQL connection pool cannot be established
*/
public static MySQLDataStore getInstance(String host, int port, String schema, String username,
String password) throws IOException, SQLException {
String url = "jdbc:mysql://" + host + ":" + port + "/" + schema;
String driver = "com.mysql.jdbc.Driver";
return new MySQLDataStore(DataSourceUtil.buildDefaultDataSource(url, driver, username,
password, "select version()"));
}
/**
* Utility method for getting a FeatureWriter for modifying existing features,
* using no feature filtering and auto-committing. Not used for adding new
* features.
* @param typeName the feature type name (the table name)
* @return a FeatureWriter for modifying existing features
* @throws IOException if the database cannot be properly accessed
*/
public FeatureWriter<SimpleFeatureType, SimpleFeature> getFeatureWriter(String typeName)
throws IOException {
return getFeatureWriter(typeName, Filter.INCLUDE, Transaction.AUTO_COMMIT);
}
/**
* Utility method for getting a FeatureWriter for adding new features, using
* auto-committing. Not used for modifying existing features.
* @param typeName the feature type name (the table name)
* @return a FeatureWriter for adding new features
* @throws IOException if the database cannot be properly accessed
*/
public FeatureWriter<SimpleFeatureType, SimpleFeature> getFeatureWriterAppend(String typeName)
throws IOException {
return getFeatureWriterAppend(typeName, Transaction.AUTO_COMMIT);
}
/**
* Constructs an AttributeType 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>
* In addition to standard SQL types, this method identifies MySQL 4.1's geometric
* datatypes and creates attribute types accordingly. This happens when the
* datatype, identified by column 5 of the ResultSet parameter, is equal to
* java.sql.Types.OTHER. If a Types.OTHER ends up not being geometric, this
* method simply calls the parent class's buildAttributeType method to do something
* with it.
* </p>
*
* <p>
* Note: Overriding methods must never move the current row pointer in the
* result set.
* </p>
*
* @param rs The ResultSet containing the result of a
* DatabaseMetaData.getColumns call.
*
* @return The AttributeType built from the ResultSet.
*
* @throws SQLException If an error occurs processing the ResultSet.
* @throws DataSourceException Provided for overriding classes to wrap
* exceptions caused by other operations they may perform to
* determine additional types. This will only be thrown by the
* default implementation if a type is present that is not present
* in the TYPE_MAPPINGS.
* NOTE: the srid of the features stored is used to build an CRS assuming
* that it an ESPG srid
*/
protected AttributeDescriptor buildAttributeType(ResultSet rs)
throws IOException {
// these come from the Interface DatabaseMetaData in java.sql
final int TABLE_NAME = 3;
final int COLUMN_NAME = 4;
final int DATA_TYPE = 5;
final int TYPE_NAME = 6;
try {
int dataType = rs.getInt(DATA_TYPE);
if (dataType == Types.OTHER || dataType == Types.BINARY) {
//this is MySQL-specific; handle it
String typeName = rs.getString(TYPE_NAME);
AttributeTypeBuilder builder = new AttributeTypeBuilder();
Class geom = getGeomClass(typeName);
if ( geom == null) {
//nothing else we can do
return super.buildAttributeType(rs);
}
builder.setBinding(geom);
//get CRS we are making the assumtion that the srid coresponds
// to the EPSG srid. Which may not be true.
int srid = determineSRID(rs.getString(TABLE_NAME), rs.getString(COLUMN_NAME) );
CoordinateReferenceSystem crs = null;
try{
crs = CRS.decode("EPSG:"+ srid);
} catch (Exception e) {
crs = null;
}
builder.setCRS(crs);
// set some other stuff like name and return
builder.setNillable(true);
builder.setName(rs.getString(COLUMN_NAME));
return builder.buildDescriptor(rs.getString(COLUMN_NAME));
} else {
return super.buildAttributeType(rs);
}
} catch (SQLException e) {
throw new IOException("SQL exception occurred: " + e.getMessage());
}
}
/** Map of mysql geometries to jts geometries */
private static Map<String,Class> GEOM_TYPE_MAP = new HashMap<String,Class>();
static {
GEOM_TYPE_MAP.put("GEOMETRY", Geometry.class);
GEOM_TYPE_MAP.put("POINT", Point.class);
GEOM_TYPE_MAP.put("LINESTRING", LineString.class);
GEOM_TYPE_MAP.put("POLYGON", Polygon.class);
GEOM_TYPE_MAP.put("MULTIPOINT", MultiPoint.class);
GEOM_TYPE_MAP.put("MULTILINESTRING", MultiLineString.class);
GEOM_TYPE_MAP.put("MULTIPOLYGON", MultiPolygon.class);
GEOM_TYPE_MAP.put("GEOMETRYCOLLECTION", GeometryCollection.class);
}
/** Returns the geomoty class associated with the string passed in or null
*/
static public Class getGeomClass(String type)
{
return GEOM_TYPE_MAP.get(type.toUpperCase());
}
/**
* @see org.geotools.data.jdbc.JDBCDataStore#getSqlBuilder(java.lang.String)
*/
public SQLBuilder getSqlBuilder(String typeName) throws IOException {
//SQLEncoder encoder = new SQLEncoderMySQL(); replace with this once
//it is fully tested, the test cases work, but I don't have a live
//mysql database. -ch
//SQLEncoder encoder = new SQLEncoder();
SQLEncoderMySQL encoder = new SQLEncoderMySQL();
encoder.setFIDMapper(getFIDMapper(typeName));
MySQLSQLBuilder builder = new MySQLSQLBuilder(encoder, getSchema(typeName));
builder.setWKBEnabled(wkbEnabled);
return builder;
}
/**
* @see org.geotools.data.jdbc.JDBCDataStore#getGeometryAttributeIO(org.geotools.feature.AttributeType)
*/
protected AttributeIO getGeometryAttributeIO(AttributeDescriptor type, QueryData queryData) {
if(wkbEnabled)
return new WKBAttributeIO(queryData.getHints());
else
return new WKTAttributeIO();
}
protected JDBCFeatureWriter createFeatureWriter(FeatureReader <SimpleFeatureType, SimpleFeature> reader, QueryData queryData)
throws IOException {
LOGGER.fine("returning jdbc feature writer");
return new MySQLFeatureWriter(reader, queryData);
}
private static final Set MYSQL_HINTS = Collections.unmodifiableSet(
new HashSet(Arrays.asList(new Object[] {
Hints.FEATURE_DETACHED, Hints.JTS_COORDINATE_SEQUENCE_FACTORY,
Hints.JTS_GEOMETRY_FACTORY})));
@Override
public Set getSupportedHints() {
return MYSQL_HINTS;
}
@Override
protected void setAutoCommit(boolean forWrite, Connection conn) throws SQLException {
// override, do nothing
}
/**
* 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;
}
protected int determineSRID(String table, String column)
throws IOException{
Connection con = null;
try {
// Gets a connection for the stored JDBCDataStore and uses it to make a statament
con = getConnection(Transaction.AUTO_COMMIT);
Statement stmt = con.createStatement();
ResultSet rslt = stmt.executeQuery("SELECT SRID(" + column + ") FROM " + table + " LIMIT 1;");
if ( rslt.next() ) {
int srid = rslt.getInt(1);
JDBCUtils.close(stmt);
return srid;
}
throw new DataSourceException( "In table: " + table + ", geometry column: " + column + " was not able to return a valid srid.");
} catch (SQLException sqle) {
String message = sqle.getMessage();
throw new DataSourceException(message, sqle);
} finally {
JDBCUtils.close(con, Transaction.AUTO_COMMIT, null);
}
}
}