package org.geoserver.jdbcloader;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.geotools.factory.GeoTools;
import org.geotools.util.Converters;
import org.geotools.util.logging.Logging;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import com.google.common.base.Optional;
public class DataSourceFactoryBean implements FactoryBean<DataSource>, DisposableBean {
private static final Logger LOGGER = Logging.getLogger(DataSourceFactoryBean.class);
JDBCLoaderProperties config;
Context jndiCtx;
DataSource dataSource;
private static Context getJNDI(JDBCLoaderProperties config) {
if(config.isEnabled() && config.getJndiName().isPresent()) {
try {
return GeoTools.getInitialContext(GeoTools.getDefaultHints());
} catch (NamingException ex) {
LOGGER.log(Level.WARNING, "Could not get JNDI Context, will not use JNDI to locate DataSource", ex);
return null;
}
} else {
// Don't bother trying to get a JNDI context if JNDI lookup isn't needed.
return null;
}
}
public DataSourceFactoryBean(JDBCLoaderProperties config) {
this(config, getJNDI(config));
}
public DataSourceFactoryBean(JDBCLoaderProperties config, Context jndiCtx) {
this.config = config;
this.jndiCtx = jndiCtx;
}
@Override
public DataSource getObject() throws Exception {
if (dataSource == null) {
if (!config.isEnabled()) {
//hack, create a stub database so that other beans in the context like the
// transaction manager can function, despite the fact that the plugin is
// disabled
dataSource = createDataSourceStub();
}
else {
dataSource = lookupOrCreateDataSource();
}
}
return dataSource;
}
protected DataSource createDataSourceStub() {
return (DataSource) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class[]{DataSource.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
return null;
}
});
}
/**
* Look up or create a DataSource
*
*/
protected DataSource lookupOrCreateDataSource() throws Exception {
DataSource ds = null;
boolean jndi = true;
ds = getJNDIDataSource(config.getJndiName()).orNull();
if(ds==null) {
jndi=false;
ds = createDataSource();
}
// Open and close a connection to verify the datasource works.
try {
ds.getConnection().close();
} catch (Exception ex) {
// Provide a useful error message that won't get lost in the stack trace.
if(jndi) {
LOGGER.severe("Error connecting to JDBC database. Verify the settings of your JNDI data source and that the database is available.");
} else {
LOGGER.severe("Error connecting to JDBC database. Verify the settings in your properties file and that the database is available.");
}
throw ex;
}
return ds;
}
/**
* Get an unconfigured BasicDataSource to set up
*
*/
protected BasicDataSource createBasicDataSource() {
return new BasicDataSource();
}
/**
* Try to lookup a configured DataSource using JNDI.
*
* @throws NamingException
*/
protected Optional<DataSource> getJNDIDataSource(Optional<String> name) {
if(jndiCtx==null) return Optional.absent();
if(name.isPresent()) {
try {
Optional<DataSource> ds = Optional.of((DataSource)jndiCtx.lookup(name.get()));
if(LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, "JDBCLoader using JNDI DataSource {0}", name.get());
}
config.setDatasourceId(name.get());
return ds;
} catch (NamingException ex) {
if(LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, "Could not resolve JNDI name "+name.get()+" for JDBCLoader Database", ex);
}
return Optional.absent();
}
} else {
if(LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("No JNDI name given for JDBCLoader DB.");
}
return Optional.absent();
}
}
/**
* Create and configure a DataSource based on the JDBCLoaderProperties
*
*/
protected DataSource createDataSource() throws Exception {
BasicDataSource dataSource = createBasicDataSource();
dataSource.setUrl(config.getJdbcUrl().get());
Optional<String> driverClassName = get(config, "driverClassName", String.class, true);
try {
Class.forName(driverClassName.get());
}
catch(Exception e) {
throw new RuntimeException("Error loading jdbc driver class: " + driverClassName, e);
}
dataSource.setDriverClassName(driverClassName.get());
dataSource.setUsername(get(config, "username", String.class, false).orNull());
dataSource.setPassword(get(config, "password", String.class, false).orNull());
dataSource.setMinIdle(get(config, "pool.minIdle", Integer.class, false).or(1));
dataSource.setMaxActive(get(config, "pool.maxActive", Integer.class, false).or(10));
dataSource.setPoolPreparedStatements(
get(config, "pool.poolPreparedStatements", Boolean.class, false).or(true));
dataSource.setMaxOpenPreparedStatements(
get(config, "pool.maxOpenPreparedStatements", Integer.class, false).or(50));
boolean testOnBorrow = get(config, "pool.testOnBorrow", Boolean.class, false).or(false);
if (testOnBorrow) {
String validateQuery = get(config, "pool.validationQuery", String.class, true).get();
dataSource.setTestOnBorrow(true);
dataSource.setValidationQuery(validateQuery);
}
if(LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, "JDBCConfig using JDBC DataSource {0}", config.getJdbcUrl());
}
//TODO: more connection parameters
config.setDatasourceId(config.getJdbcUrl().get());
return dataSource;
}
<T> Optional<T> get(Properties props, String key, Class<T> clazz, boolean mandatory) {
String raw = props.getProperty(key);
if (raw == null && mandatory) {
throw new IllegalStateException(key + " property is mandatory but not found");
}
return Optional.fromNullable(Converters.convert(raw, clazz));
}
@Override
public Class<?> getObjectType() {
return DataSource.class;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public void destroy() throws Exception {
if (dataSource != null && dataSource instanceof BasicDataSource) {
((BasicDataSource)dataSource).close();
}
dataSource = null;
}
}