/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2012-2013, Geomatys
*
* 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.geotoolkit.db;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.geotoolkit.data.AbstractFeatureStoreFactory;
import org.geotoolkit.db.dialect.SQLDialect;
import org.geotoolkit.jdbc.DBCPDataSource;
import org.apache.sis.metadata.iso.quality.DefaultConformanceResult;
import org.apache.sis.parameter.ParameterBuilder;
import org.apache.sis.storage.DataStoreException;
import org.geotoolkit.storage.DataStore;
import org.opengis.metadata.quality.ConformanceResult;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValueGroup;
/**
* Abstract FeatureStoreFactory for databases.
*
* @author Johann Sorel (Geomatys)
* @module
*/
public abstract class AbstractJDBCFeatureStoreFactory extends AbstractFeatureStoreFactory {
/** parameter for database host */
public static final ParameterDescriptor<String> HOST = new ParameterBuilder()
.addName("host")
.addName(Bundle.formatInternational(Bundle.Keys.host))
.setRemarks(Bundle.formatInternational(Bundle.Keys.host_remarks))
.setRequired(true)
.create(String.class, "localhost");
/** parameter for database port */
public static final ParameterDescriptor<Integer> PORT = new ParameterBuilder()
.addName("port")
.addName(Bundle.formatInternational(Bundle.Keys.port))
.setRemarks(Bundle.formatInternational(Bundle.Keys.port_remarks))
.setRequired(true)
.create(Integer.class, null);
/** parameter for database instance */
public static final ParameterDescriptor<String> DATABASE = new ParameterBuilder()
.addName("database")
.addName(Bundle.formatInternational(Bundle.Keys.database))
.setRemarks(Bundle.formatInternational(Bundle.Keys.database_remarks))
.setRequired(false)
.create(String.class, null);
/** parameter for database schema */
public static final ParameterDescriptor<String> SCHEMA = new ParameterBuilder()
.addName("schema")
.addName(Bundle.formatInternational(Bundle.Keys.schema))
.setRemarks(Bundle.formatInternational(Bundle.Keys.schema_remarks))
.setRequired(false)
.create(String.class, null);
/** parameter for database user */
public static final ParameterDescriptor<String> USER = new ParameterBuilder()
.addName("user")
.addName(Bundle.formatInternational(Bundle.Keys.user))
.setRemarks(Bundle.formatInternational(Bundle.Keys.user_remarks))
.setRequired(true)
.create(String.class, null);
/** parameter for database password */
public static final ParameterDescriptor<String> PASSWORD = new ParameterBuilder()
.addName("password")
.addName(Bundle.formatInternational(Bundle.Keys.password))
.setRemarks(Bundle.formatInternational(Bundle.Keys.password_remarks))
.setRequired(true)
.create(String.class, null);
/** parameter for data source */
public static final ParameterDescriptor<DataSource> DATASOURCE = new ParameterBuilder()
.addName("Data Source")
.addName(Bundle.formatInternational(Bundle.Keys.datasource))
.setRemarks(Bundle.formatInternational(Bundle.Keys.datasource_remarks))
.setRequired(false)
.create(DataSource.class, null);
/** Set to true to have only simple feature types.
* relations won't be rebuilded as complexe features.
* Default is true.
*/
public static final ParameterDescriptor<Boolean> SIMPLETYPE = new ParameterBuilder()
.addName("simple types")
.addName(Bundle.formatInternational(Bundle.Keys.simpletype))
.setRemarks(Bundle.formatInternational(Bundle.Keys.simpletype_remarks))
.setRequired(true)
.create(Boolean.class, Boolean.TRUE);
/** Maximum number of connections in the connection pool */
public static final ParameterDescriptor<Integer> MAXCONN = new ParameterBuilder()
.addName("max connections")
.addName(Bundle.formatInternational(Bundle.Keys.max_connections))
.setRemarks(Bundle.formatInternational(Bundle.Keys.max_connections_remarks))
.setRequired(false)
.create(Integer.class, 10);
/** Minimum number of connections in the connection pool */
public static final ParameterDescriptor<Integer> MINCONN = new ParameterBuilder()
.addName("min connections")
.addName(Bundle.formatInternational(Bundle.Keys.min_connections))
.setRemarks(Bundle.formatInternational(Bundle.Keys.min_connections_remarks))
.setRequired(false)
.create(Integer.class, 1);
/** If connections should be validated before using them */
public static final ParameterDescriptor<Boolean> VALIDATECONN = new ParameterBuilder()
.addName("validate connections")
.addName(Bundle.formatInternational(Bundle.Keys.validate_connections))
.setRemarks(Bundle.formatInternational(Bundle.Keys.validate_connections_remarks))
.setRequired(false)
.create(Boolean.class, Boolean.FALSE);
/** If connections should be validated before using them */
public static final ParameterDescriptor<Integer> FETCHSIZE = new ParameterBuilder()
.addName("fetch size")
.addName(Bundle.formatInternational(Bundle.Keys.fetch_size))
.setRemarks(Bundle.formatInternational(Bundle.Keys.fetch_size_remarks))
.setRequired(false)
.create(Integer.class, 1000);
/** Maximum amount of time the pool will wait when trying to grab a new connection **/
public static final ParameterDescriptor<Integer> MAXWAIT = new ParameterBuilder()
.addName("Connection timeout")
.addName(Bundle.formatInternational(Bundle.Keys.timeout))
.setRemarks(Bundle.formatInternational(Bundle.Keys.timeout_remarks))
.setRequired(false)
.create(Integer.class, 20);
/** parameter for table to load **/
public static final ParameterDescriptor<String> TABLE = new ParameterBuilder()
.addName("Table Name")
.addName(Bundle.formatInternational(Bundle.Keys.table))
.setRemarks(Bundle.formatInternational(Bundle.Keys.table_remarks))
.setRequired(false)
.create(String.class, null);
/**
* Create the database port descriptor, and set default parameter value.
* @return a databse port descriptor.
*/
public static ParameterDescriptor<Integer> createFixedPort(Integer value) {
return new ParameterBuilder()
.addName(PORT.getName().getCode())
.addName(PORT.getAlias().iterator().next())
.setRemarks(PORT.getRemarks())
.setRequired(true)
.create(Integer.class, value);
}
@Override
public boolean canProcess(final ParameterValueGroup params) {
final boolean valid = super.canProcess(params);
if(!valid){
//check if the datasource is set
try{
final DataSource ds = (DataSource) params.parameter(DATASOURCE.getName().toString()).getValue();
if(ds == null){
return false;
}
}catch(ParameterNotFoundException ex){
//parameter does not exist
return false;
}
}
return valid;
}
@Override
public JDBCFeatureStore open(final ParameterValueGroup params) throws DataStoreException {
ensureCanProcess(params);
final DefaultJDBCFeatureStore featureStore = toFeatureStore(params,
getIdentification().getCitation().getIdentifiers().iterator().next().getCode());
prepareStore(featureStore, params);
return featureStore;
}
/**
* Configure feature store datasource and dialect.
*
* @param featureStore
* @param params
* @throws DataStoreException
*/
public void prepareStore(DefaultJDBCFeatureStore featureStore, final ParameterValueGroup params) throws DataStoreException{
// datasource
final DataSource ds = (DataSource) params.parameter(DATASOURCE.getName().getCode()).getValue();
try {
featureStore.setDataSource((ds != null) ? ds : createDataSource(params));
} catch (IOException ex) {
throw new DataStoreException(ex);
}
// dialect
final SQLDialect dialect = createSQLDialect(featureStore);
featureStore.setDialect(dialect);
}
protected DefaultJDBCFeatureStore toFeatureStore(final ParameterValueGroup params,String factoryId){
return new DefaultJDBCFeatureStore(params,factoryId);
}
/**
* Create a JDBC database : this will not create a database but just
* create the schema if possible.
*
* @param params
* @return
* @throws DataStoreException
*/
@Override
public DataStore create(final ParameterValueGroup params) throws DataStoreException {
JDBCFeatureStore store = null;
Connection cnx = null;
Statement stmt = null;
ResultSet rs = null;
try {
store = open(params);
cnx = store.getDataSource().getConnection();
rs = cnx.getMetaData().getSchemas();
final String schema = store.getDatabaseSchema();
while (rs.next()) {
final String currentSchema = rs.getString(1);
if (currentSchema.equals(schema)) {
//shema already exist, might be normal, like 'public' on postgres and H2.
return store;
//throw new DataStoreException("Schema " + schema+ " already exist. Unable to create DataStore.");
}
}
stmt = cnx.createStatement();
if (schema != null && !schema.isEmpty()) {
//create schema
stmt.executeUpdate("CREATE SCHEMA \"" + schema + "\";");
}
return store;
} catch (SQLException ex) {
if (store != null) {
store.close();
}
throw new DataStoreException(ex);
} finally {
if (store != null) {
JDBCFeatureStoreUtilities.closeSafe(store.getLogger(),cnx, stmt, rs);
}
}
}
/**
* {@inheritDoc }
*/
@Override
public ConformanceResult availability() {
final DefaultConformanceResult result = (DefaultConformanceResult)super.availability();
if(Boolean.FALSE.equals(result.pass())) return result;
try {
//check jdbc driver
Class.forName(getDriverClassName());
} catch (ClassNotFoundException e) {
result.setPass(false);
}
return result;
}
/**
* @return String , name used in the construction of the JDBC url.
*/
protected abstract String getJDBCURLDatabaseName();
/**
* @return String , complete JDBC driver class name.
*/
protected abstract String getDriverClassName();
/**
* Creates the dialect that the featurestore uses for communication with the
* underlying database.
*
* @param featureStore The featurestore.
*/
protected abstract SQLDialect createSQLDialect(final JDBCFeatureStore featureStore);
/**
* Create a datasource using given parameters.
*/
protected DataSource createDataSource(final ParameterValueGroup params) throws IOException {
//create a datasource
final BasicDataSource dataSource = new BasicDataSource();
// some default data source behaviour
dataSource.setPoolPreparedStatements(false);
// driver
dataSource.setDriverClassName(getDriverClassName());
// url
dataSource.setUrl(getJDBCUrl(params));
// username
final String user = (String) params.parameter(USER.getName().toString()).getValue();
dataSource.setUsername(user);
// password
final String passwd = (String) params.parameter(PASSWORD.getName().toString()).getValue();
if (passwd != null) {
dataSource.setPassword(passwd);
}
// max wait
final Integer maxWait = (Integer) params.parameter(MAXWAIT.getName().toString()).getValue();
if (maxWait != null && maxWait != -1) {
dataSource.setMaxWait(maxWait * 1000);
}
// connection pooling options
final Integer minConn = (Integer) params.parameter(MINCONN.getName().toString()).getValue();
if ( minConn != null ) {
dataSource.setMinIdle(minConn);
}
final Integer maxConn = (Integer) params.parameter(MAXCONN.getName().toString()).getValue();
if ( maxConn != null ) {
dataSource.setMaxActive(maxConn);
}
final Boolean validate = (Boolean) params.parameter(VALIDATECONN.getName().toString()).getValue();
if(validate != null && validate && getValidationQuery() != null) {
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery(getValidationQuery());
}
// allow manipulating connections for possible tuning.
dataSource.setAccessToUnderlyingConnectionAllowed(true);
return new DBCPDataSource(dataSource);
}
/**
* @return String : a fast query which can be send to the server to ensure
* the connextion is still valid.
*/
protected abstract String getValidationQuery();
/**
* Build JDBC url string = jdbc:<database>://<host>:<port>/<dbname>
*/
protected String getJDBCUrl(final ParameterValueGroup params) throws IOException {
final String host = (String) params.parameter(HOST.getName().toString()).getValue();
final Integer port = (Integer) params.parameter(PORT.getName().toString()).getValue();
final String db = (String) params.parameter(DATABASE.getName().toString()).getValue();
final StringBuilder sb = new StringBuilder("jdbc:");
sb.append(getJDBCURLDatabaseName());
sb.append("://");
sb.append(host);
if(port != null){
sb.append(':').append(port);
}
if(db != null){
sb.append('/').append(db);
}
return sb.toString();
}
}