/**
* DataCleaner (community edition)
* Copyright (C) 2014 Neopost - Customer Information Management
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.datacleaner.connection;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.metamodel.UpdateableDataContext;
import org.apache.metamodel.jdbc.JdbcDataContext;
import org.apache.metamodel.schema.TableType;
import org.datacleaner.util.ReadObjectBuilder;
import org.datacleaner.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Datastore implementation for JDBC based connections. Connections can either
* be based on JDBC urls or JNDI urls.
*/
public class JdbcDatastore extends UsageAwareDatastore<UpdateableDataContext>
implements UpdateableDatastore, UsernameDatastore {
public static final String SYSTEM_PROPERTY_CONNECTION_POOL_MAX_SIZE = "datastore.jdbc.connection.pool.max.size";
public static final String SYSTEM_PROPERTY_CONNECTION_POOL_MIN_EVICTABLE_IDLE_TIME_MILLIS =
"datastore.jdbc.connection.pool.idle.timeout";
public static final String SYSTEM_PROPERTY_CONNECTION_POOL_TIME_BETWEEN_EVICTION_RUNS_MILLIS =
"datastore.jdbc.connection.pool.eviction.period.millis";
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(JdbcDatastore.class);
private final String _jdbcUrl;
private final String _username;
private final String _password;
private final String _driverClass;
private final boolean _multipleConnections;
private final String _datasourceJndiUrl;
private final TableType[] _tableTypes;
private final String _catalogName;
private JdbcDatastore(final String name, final String jdbcUrl, final String driverClass, final String username,
final String password, final String datasourceJndiUrl, final boolean multipleConnections,
final TableType[] tableTypes, final String catalogName) {
super(name);
_jdbcUrl = jdbcUrl;
_driverClass = driverClass;
_username = username;
_password = password;
_datasourceJndiUrl = datasourceJndiUrl;
_multipleConnections = multipleConnections;
_tableTypes = tableTypes;
_catalogName = catalogName;
}
public JdbcDatastore(final String name, final String jdbcUrl, final String driverClass) {
this(name, jdbcUrl, driverClass, null, null, true);
}
public JdbcDatastore(final String name, final String jdbcUrl, final String driverClass, final String username,
final String password, final boolean multipleConnections, final TableType[] tableTypes,
final String catalogName) {
this(name, jdbcUrl, driverClass, username, password, null, multipleConnections, tableTypes, catalogName);
}
public JdbcDatastore(final String name, final String jdbcUrl, final String driverClass, final String username,
final String password, final boolean multipleConnections) {
this(name, jdbcUrl, driverClass, username, password, multipleConnections, null, null);
}
public JdbcDatastore(final String name, final String datasourceJndiUrl) {
this(name, datasourceJndiUrl, (TableType[]) null, null);
}
public JdbcDatastore(final String name, final String datasourceJndiUrl, final TableType[] tableTypes,
final String catalogName) {
this(name, null, null, null, null, datasourceJndiUrl, false, tableTypes, catalogName);
}
/**
* Alternative constructor usable only for in-memory (ie. non-persistent)
* datastores, because the datastore will not be able to create new
* connections.
*
* @param name
* @param dc
*/
public JdbcDatastore(final String name, final UpdateableDataContext dc) {
this(name, null, null, null, null, null, false, null, null);
setDataContextProvider(new UpdateableDatastoreConnectionImpl<>(dc, this));
}
private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
ReadObjectBuilder.create(this, JdbcDatastore.class).readObject(stream);
}
@Override
public UpdateableDatastoreConnection openConnection() {
final DatastoreConnection connection = super.openConnection();
return (UpdateableDatastoreConnection) connection;
}
@Override
protected void decorateIdentity(final List<Object> identifiers) {
super.decorateIdentity(identifiers);
identifiers.add(_driverClass);
identifiers.add(_jdbcUrl);
identifiers.add(_datasourceJndiUrl);
identifiers.add(_username);
identifiers.add(_password);
identifiers.add(_multipleConnections);
identifiers.add(getTableTypes());
}
public boolean isMultipleConnections() {
return _multipleConnections;
}
public TableType[] getTableTypes() {
final TableType[] tableTypes;
if (_tableTypes == null) {
tableTypes = TableType.DEFAULT_TABLE_TYPES;
} else {
tableTypes = _tableTypes;
}
return Arrays.copyOf(tableTypes, tableTypes.length);
}
public String getCatalogName() {
return _catalogName;
}
public String getJdbcUrl() {
return _jdbcUrl;
}
@Override
public String getUsername() {
return _username;
}
public String getPassword() {
return _password;
}
public String getDriverClass() {
return _driverClass;
}
public String getDatasourceJndiUrl() {
return _datasourceJndiUrl;
}
public Connection createConnection() throws IllegalStateException {
initializeDriver();
try {
if (_username != null && _password != null) {
return DriverManager.getConnection(_jdbcUrl, _username, _password);
} else {
return DriverManager.getConnection(_jdbcUrl);
}
} catch (final SQLException e) {
throw new IllegalStateException("Could not create connection", e);
}
}
public DataSource createDataSource() {
initializeDriver();
final BasicDataSource ds = new BasicDataSource();
ds.setDefaultAutoCommit(false);
ds.setUrl(_jdbcUrl);
ds.setMaxActive(getSystemPropertyValue(SYSTEM_PROPERTY_CONNECTION_POOL_MAX_SIZE, -1));
ds.setMinEvictableIdleTimeMillis(
getSystemPropertyValue(SYSTEM_PROPERTY_CONNECTION_POOL_MIN_EVICTABLE_IDLE_TIME_MILLIS, 500));
ds.setTimeBetweenEvictionRunsMillis(
getSystemPropertyValue(SYSTEM_PROPERTY_CONNECTION_POOL_TIME_BETWEEN_EVICTION_RUNS_MILLIS, 1000));
if (_username != null && _password != null) {
ds.setUsername(_username);
ds.setPassword(_password);
}
return ds;
}
private int getSystemPropertyValue(final String property, final int defaultValue) {
final String str = System.getProperty(property);
if (str == null) {
return defaultValue;
}
try {
return Integer.parseInt(str);
} catch (final NumberFormatException e) {
logger.debug("Failed to parse system property '{}': '{}'", property, str);
return defaultValue;
}
}
private void initializeDriver() {
if (_jdbcUrl == null) {
throw new IllegalStateException("JDBC URL is null, cannot create connection!");
}
logger.debug("Determining if driver initialization is necessary");
// it's best to avoid initializing the driver, so we do this check.
// It may already have been initialized and Class.forName(...) does
// not always work if the driver is in a different classloader
boolean installDriver = true;
final Enumeration<Driver> drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
final Driver driver = drivers.nextElement();
try {
if (driver.acceptsURL(_jdbcUrl)) {
installDriver = false;
break;
}
} catch (final Exception e) {
logger.warn("Driver threw exception when acceptURL(...) was invoked", e);
}
}
if (installDriver) {
try {
Class.forName(_driverClass);
} catch (final ClassNotFoundException e) {
throw new IllegalStateException("Could not initialize JDBC driver", e);
}
}
}
@Override
protected UsageAwareDatastoreConnection<UpdateableDataContext> createDatastoreConnection() {
if (StringUtils.isNullOrEmpty(_datasourceJndiUrl)) {
if (isMultipleConnections()) {
final DataSource dataSource = createDataSource();
return new DataSourceDatastoreConnection(dataSource, getTableTypes(), _catalogName, this);
} else {
final Connection connection = createConnection();
try {
connection.setAutoCommit(false);
} catch (final SQLException e) {
logger.error("Could not set autocommit false '{}'", _datasourceJndiUrl);
throw new IllegalStateException(e);
}
final UpdateableDataContext dataContext =
new JdbcDataContext(connection, getTableTypes(), _catalogName);
return new UpdateableDatastoreConnectionImpl<>(dataContext, this);
}
} else {
try {
final Context initialContext = getJndiNamingContext();
final DataSource dataSource = (DataSource) initialContext.lookup(_datasourceJndiUrl);
return new DataSourceDatastoreConnection(dataSource, getTableTypes(), _catalogName, this);
} catch (final Exception e) {
logger.error("Could not retrieve DataSource '{}'", _datasourceJndiUrl);
throw new IllegalStateException(e);
}
}
}
protected Context getJndiNamingContext() throws NamingException {
return new InitialContext();
}
@Override
public PerformanceCharacteristics getPerformanceCharacteristics() {
return new PerformanceCharacteristicsImpl(true, false);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("JdbcDatastore[name=");
sb.append(getName());
if (_jdbcUrl != null) {
sb.append(",url=");
sb.append(_jdbcUrl);
} else {
sb.append(",jndi=");
sb.append(_datasourceJndiUrl);
}
sb.append("]");
return sb.toString();
}
}