blob: 2ad6f401c0c8ff7cea6efb0f3c571edd3fcfb752 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.jdbc.schema;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.Locale;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.lib.conf.Configurable;
import org.apache.openjpa.lib.conf.Configuration;
import org.apache.openjpa.lib.conf.Configurations;
import org.apache.openjpa.lib.util.Closeable;
/**
* Commons DBCP basic pooling driver data source.
* The commons-dbcp packages must be on the class path for this plugin to work,
* as it WILL NOT fall back to non-DBCP mode if they are missing. For automatic
* usage of Commons DBCP when available, use AutoDriverDataSource instead.
*/
public class DBCPDriverDataSource
extends SimpleDriverDataSource implements Configurable, Closeable {
private static String DBCPPACKAGENAME = "org.apache.commons.dbcp2";
private static String DBCPBASICDATASOURCENAME = "org.apache.commons.dbcp2.BasicDataSource";
private static Class<?> _dbcpClass;
private static Boolean _dbcpAvail;
private static RuntimeException _dbcpEx;
protected JDBCConfiguration conf;
private volatile DataSource _ds;
@Override
public Connection getConnection(Properties props) throws SQLException {
return getDBCPConnection(props);
}
@Override
public void close() throws SQLException {
try {
if (_ds != null) {
if (isDBCPLoaded(getClassLoader())) {
((org.apache.commons.dbcp2.BasicDataSource)_dbcpClass.cast(_ds)).close();
}
}
} catch (Exception e) {
// no-op
} catch (Throwable t) {
// no-op
} finally {
_ds = null;
}
}
protected Connection getDBCPConnection(Properties props) throws SQLException {
Connection con = getDBCPDataSource(props).getConnection();
if (con == null) {
throw new SQLException(_eloc.get("dbcp-ds-null",
DBCPBASICDATASOURCENAME, getConnectionDriverName(), getConnectionURL()).getMessage());
}
return con;
}
protected DataSource getDBCPDataSource(Properties props) {
if (isDBCPLoaded(getClassLoader())) {
if (_ds == null) {
try {
Properties dbcpProps = updateDBCPProperties(props);
_ds = (DataSource) Configurations.newInstance(DBCPBASICDATASOURCENAME, conf,
dbcpProps, getClassLoader());
} catch (Exception e) {
_dbcpEx = new RuntimeException(_eloc.get("driver-null", DBCPBASICDATASOURCENAME).getMessage(), e);
}
return _ds;
} else {
return _ds;
}
} else {
// user choose DBCP, so fail if it isn't on the classpath
if (_dbcpEx == null)
_dbcpEx = new RuntimeException(_eloc.get("driver-null", DBCPBASICDATASOURCENAME).getMessage());
throw _dbcpEx;
}
}
/**
* This method should not throw an exception, as it is called by
* AutoDriverDataSource to determine if user already specified
* to use Commons DBCP.
* @return true if ConnectionDriverName contains org.apache.commons.dbcp2,
* otherwise false
*/
protected boolean isDBCPDataSource() {
return (getConnectionDriverName() != null &&
getConnectionDriverName().toLowerCase(Locale.ENGLISH).indexOf(DBCPPACKAGENAME) >= 0);
}
/**
* This method should not throw an exception, as it is called by
* AutoDriverDataSource to determine if it should use DBCP or not
* based on if org.apache.commons.dbcp2.BasicDataSource can be loaded.
* @return true if Commons DBCP was found on the classpath, otherwise false
*/
static protected boolean isDBCPLoaded(ClassLoader cl) {
if (Boolean.TRUE.equals(_dbcpAvail) && (_dbcpClass != null)) {
return true;
} else if (Boolean.FALSE.equals(_dbcpAvail)) {
return false;
} else {
// first time checking, so try to load it
try {
_dbcpClass = Class.forName(DBCPBASICDATASOURCENAME, true, cl);
_dbcpAvail = Boolean.TRUE;
return true;
} catch (Exception e) {
_dbcpAvail = Boolean.FALSE;
// save exception details for later instead of throwing here
_dbcpEx = new RuntimeException(_eloc.get("driver-null", DBCPBASICDATASOURCENAME).getMessage(), e);
}
return _dbcpAvail.booleanValue();
}
}
/**
* Normalize properties for Commons DBCP. This should be done for every call from DataSourceFactory,
* as we do not have a pre-configured Driver to reuse.
* @param props
* @return updated properties
*/
private Properties updateDBCPProperties(Properties props) {
Properties dbcpProps = mergeConnectionProperties(props);
// only perform the following check for the first connection attempt (_driverClassName == null),
// as multiple connections could be requested (like from SchemaTool) and isDBCPDriver() will be true
if (isDBCPDataSource()) {
String propDriver = hasProperty(dbcpProps, "driverClassName");
if (propDriver == null || propDriver.trim().isEmpty()) {
// if user specified DBCP for the connectionDriverName, then make sure they supplied a DriverClassName
throw new RuntimeException(_eloc.get("connection-property-invalid", "DriverClassName",
propDriver).getMessage());
}
propDriver = hasProperty(dbcpProps, "url");
if (propDriver == null || propDriver.trim().isEmpty()) {
// if user specified DBCP for the connectionDriverName, then make sure they supplied a Url
throw new RuntimeException(_eloc.get("connection-property-invalid", "URL",
propDriver).getMessage());
}
} else {
// set Commons DBCP expected DriverClassName to the original connection driver name
dbcpProps.setProperty(hasKey(dbcpProps, "driverClassName", "driverClassName"), getConnectionDriverName());
// set Commons DBCP expected URL property
dbcpProps.setProperty(hasKey(dbcpProps, "url", "url"), getConnectionURL());
}
// Commons DBCP requires non-Null username/password values in the connection properties
if (hasKey(dbcpProps, "username") == null) {
if (getConnectionUserName() != null)
dbcpProps.setProperty("username", getConnectionUserName());
else
dbcpProps.setProperty("username", "");
}
// Commons DBCP requires non-Null username/password values in the connection properties
if (hasKey(dbcpProps, "password") == null) {
if (getConnectionPassword() != null)
dbcpProps.setProperty("password", getConnectionPassword());
else
dbcpProps.setProperty("password", "");
}
// set some default properties for DBCP
if (hasKey(dbcpProps, "maxActive") == null) {
dbcpProps.setProperty("maxActive", "10");
}
if (hasKey(dbcpProps, "maxIdle") == null) {
// by default we set maxIdle to the same value as maxActive.
dbcpProps.setProperty("maxIdle", dbcpProps.getProperty("maxActive"));
}
if (hasKey(dbcpProps, "minIdle") == null) {
dbcpProps.setProperty("minIdle", "0");
}
return dbcpProps;
}
/**
* Merge the passed in properties with a copy of the existing _connectionProperties
* @param props
* @return Merged properties
*/
private Properties mergeConnectionProperties(final Properties props) {
Properties mergedProps = new Properties();
mergedProps.putAll(getConnectionProperties());
// need to map "user" to "username" for Commons DBCP
String uid = removeProperty(mergedProps, "user");
if (uid != null) {
mergedProps.setProperty("username", uid);
}
// now, merge in any passed in properties
if (props != null && !props.isEmpty()) {
for (Iterator<Object> itr = props.keySet().iterator(); itr.hasNext();) {
String key = (String)itr.next();
String value = props.getProperty(key);
// need to map "user" to "username" for Commons DBCP
if ("user".equalsIgnoreCase(key)) {
key = "username";
}
// case-insensitive search for existing key
String existingKey = hasKey(mergedProps, key);
if (existingKey != null) {
// update existing entry
mergedProps.setProperty(existingKey, value);
} else {
// add property to the merged set
mergedProps.setProperty(key, value);
}
}
}
return mergedProps;
}
/**
* Case-insensitive search of the given properties for the given key.
* @param props
* @param key
* @return Key name as found in properties or null if it was not found.
*/
private String hasKey(Properties props, String key) {
return hasKey(props, key, null);
}
/**
* Case-insensitive search of the given properties for the given key.
* @param props
* @param key
* @param defaultKey
* @return Key name as found in properties or the given defaultKey if it was not found.
*/
private String hasKey(Properties props, String key, String defaultKey)
{
if (props != null && key != null) {
for (Iterator<Object> itr = props.keySet().iterator(); itr.hasNext();) {
String entry = (String)itr.next();
if (key.equalsIgnoreCase(entry))
return entry;
}
}
return defaultKey;
}
private String hasProperty(Properties props, String key) {
if (props != null && key != null) {
String entry = hasKey(props, key);
if (entry != null)
return props.getProperty(entry);
}
return null;
}
private String removeProperty(Properties props, String key) {
if (props != null && key != null) {
String entry = hasKey(props, key);
if (entry != null)
return (String)props.remove(entry);
}
return null;
}
// Configurable interface methods
@Override
public void setConfiguration(Configuration conf) {
if (conf instanceof JDBCConfiguration)
this.conf = (JDBCConfiguration)conf;
}
@Override
public void startConfiguration() {
// no-op
}
@Override
public void endConfiguration() {
// no-op
}
}