
org.noorm.jdbc.DataSourceProvider Maven / Gradle / Ivy
package org.noorm.jdbc;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.Properties;
import oracle.jdbc.OracleCallableStatement;
import oracle.jdbc.OracleConnection;
import oracle.jdbc.pool.OracleDataSource;
//import oracle.ucp.jdbc.PoolDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
/**
* The DataSourceProvider organizes data source access and transaction control for NoORM.
* Access to the JDBC connection objects is discouraged outside of NoORM. The only reason
* to use DataSourceProvider directly in an application is for fine grained transaction
* control. DataSourceProvider provides the required methods begin(), commit() and rollback()
* to control a transaction in an application (direct access to the underlying JDBC
* connection is possible for special purposes like integration with Oracle AQ).
* When these methods for explicit transaction control are not used, DataSourceProvider will
* manage transactions automatically by issuing an auto-commit for every database access.
* Issuing a commit even for read-only database access imposes a little performance overhead.
* For most usage scenarios, this overhead is not significant. However, when an application
* performs a large number of read only operation it it worth comparing performance with
* explicit transaction handling.
* Explicit transaction handling (using begin(), commit(), rollback()) is managed by
* maintaining the JDBC connection in a ThreadLocal variable. This removes the burden of
* connection control from the application programmer, since DataSourceProvider will keep
* track of the current transaction beyond class and method boundaries in the application.
* However, using explicit transaction control requires the application programmer to control
* transaction termination properly, i.e., the application must guarantee that a transaction
* started with begin() will always be terminated with commit() or rollback().
*
* @author Ulf Pietruschka / [email protected]
*/
public class DataSourceProvider {
private static final Logger log = LoggerFactory.getLogger(DataSourceProvider.class);
private static DataSourceProvider dataSourceFactory;
private DataSource dataSource;
private static final String NOORM_PROPERTIES_FILENAME = "/META-INF/noorm.properties";
private static final String NOORM_XML_FILENAME = "/META-INF/noorm.xml";
private static final String DATABASE_JNDINAME = "database.jndiname";
private static final String DATABASE_PASSWORD = "database.password";
private static final String DATABASE_URL = "database.url";
private static final String DATABASE_USERNAME = "database.username";
private static final String DATABASE_BATCH_UPDATE_SIZE = "database.batch_update_size";
private static final String DEBUG_MODE = "debug.mode";
private static final String DEBUG_JDWP_HOST = "debug.host";
private static final String DEBUG_JDWP_PORT = "debug.port";
private static final ThreadLocal conThreadLocal = new ThreadLocal();
private static final ThreadLocal tsStackThreadLocal = new ThreadLocal();
private int batchUpdateSize = 100;
private boolean debugMode = false;
private String debugJDWPPort = "4000";
private String debugJDWPHost = "localhost";
private final Properties noormProperties = new Properties();
private DataSourceProvider() {
}
/**
* Initializes and instantiates the DataSourceProvider with the given, pre-configured DataSource.
* This step is optionally but strictly recommend for production usage. Alternatively, the datasource
* could be configured using the noorm.properties file, but the latter is only suitable for a simple
* setup with username, password and URL. To apply advanced settings to the DataSource, this
* pre-configuration method should be used.
*
* @param pDataSource the pre-configured Oracle datasource.
*/
public static void setDataSource(final DataSource pDataSource) {
if (dataSourceFactory == null) {
log.debug("Instantiating DataSourceProvider.");
dataSourceFactory = new DataSourceProvider();
dataSourceFactory.loadNoormProperties();
dataSourceFactory.dataSource = pDataSource;
dataSourceFactory.initNoormProperties();
validateDataSource();
}
}
/**
* Validate the data source. Either for the data source submitted using setDataSource or for the
* data source constructed using the NoORM properties, a connection is established to validate
* the data source.
*/
private static void validateDataSource() {
final StringBuilder validationInfo = new StringBuilder();
validationInfo.append("Validating data source. ");
try {
// To provide information for a failed data source validation in case of an UCP
// (Oracle Universal Connection Pool) PoolDataSource, we need to include UCP into
// the list of dependencies for NoORM as well as for the projects using NoORM.
// The typically undesired dependency for a specific connection pool implementation
// outweighs the advantage of a proper reporting for a mis-configured data source.
// Thus, UCP is no longer considered within NoORM.
//if (dataSourceFactory.dataSource instanceof PoolDataSource) {
// validationInfo.append("Connection parameters: ");
// validationInfo.append(";URL: ");
// validationInfo.append(((PoolDataSource) dataSourceFactory.dataSource).getURL());
// validationInfo.append(";Username: ");
// validationInfo.append(((PoolDataSource) dataSourceFactory.dataSource).getUser());
//} else {
if (dataSourceFactory.dataSource instanceof OracleDataSource) {
validationInfo.append("Connection parameters: ");
validationInfo.append(";URL: ");
validationInfo.append(((OracleDataSource) dataSourceFactory.dataSource).getURL());
validationInfo.append(";Username: ");
validationInfo.append(((OracleDataSource) dataSourceFactory.dataSource).getUser());
} else {
validationInfo.append("Unable to retrieve connection parameters from data source. [");
validationInfo.append(dataSourceFactory.dataSource.getClass().getName());
validationInfo.append("]");
}
//}
log.info(validationInfo.toString());
dataSourceFactory.dataSource.getConnection();
} catch (Exception e) {
throw new DataAccessException(DataAccessException.Type.COULD_NOT_ESTABLISH_CONNECTION, e);
}
}
private static DataSource getDataSource() throws SQLException {
if (dataSourceFactory == null) {
log.debug("Instantiating DataSourceProvider.");
dataSourceFactory = new DataSourceProvider();
dataSourceFactory.loadNoormProperties();
dataSourceFactory.initDataSource();
dataSourceFactory.initNoormProperties();
}
return dataSourceFactory.dataSource;
}
private void loadNoormProperties() {
log.info("Trying to load configuration file ".concat(NOORM_XML_FILENAME));
InputStream is = dataSourceFactory.getClass().getResourceAsStream(NOORM_XML_FILENAME);
try {
if (is != null) {
noormProperties.loadFromXML(is);
log.info("Configuration file ".concat(NOORM_XML_FILENAME).concat(" loaded."));
} else {
log.info("Configuration file ".concat(NOORM_XML_FILENAME).concat(" not found."));
log.info("Trying to load configuration file ".concat(NOORM_PROPERTIES_FILENAME));
is = dataSourceFactory.getClass().getResourceAsStream(NOORM_PROPERTIES_FILENAME);
if (is != null) {
noormProperties.load(is);
log.info("Configuration file ".concat(NOORM_PROPERTIES_FILENAME).concat(" loaded."));
} else {
log.info("Configuration file ".concat(NOORM_PROPERTIES_FILENAME).concat(" not found."));
throw new FileNotFoundException();
}
}
} catch (FileNotFoundException ex) {
// File noorm.properties is optional, failing to load the properties is considered to be
// an exception only when the reason for the failure is not a missing file.
} catch (IOException ex) {
throw new DataAccessException(DataAccessException.Type.INITIALIZATION_FAILURE,
"Loading of noorm.properties failed.");
}
}
private void initNoormProperties() {
final String batchUpdateSizeProp = noormProperties.getProperty(DATABASE_BATCH_UPDATE_SIZE);
if (batchUpdateSizeProp != null && !batchUpdateSizeProp.isEmpty()) {
try {
batchUpdateSize = Integer.parseInt(batchUpdateSizeProp);
log.info("Setting ".concat(DATABASE_BATCH_UPDATE_SIZE).concat(" = ").concat(batchUpdateSizeProp));
} catch (NumberFormatException ex) {
throw new DataAccessException(DataAccessException.Type.INITIALIZATION_FAILURE, ex);
}
} else {
final String batchUpdateSizeS = (new Integer(batchUpdateSize).toString());
log.info("Setting (default) ".concat(DATABASE_BATCH_UPDATE_SIZE).concat(" = ").concat(batchUpdateSizeS));
}
final String debugModeProp = noormProperties.getProperty(DEBUG_MODE);
if (debugModeProp != null && debugModeProp.toLowerCase().equals("true")) {
debugMode = true;
log.info("Setting ".concat(DEBUG_MODE).concat(" = true"));
} else {
log.info("Setting (default) ".concat(DEBUG_MODE).concat(" = false"));
}
final String debugHostProp = noormProperties.getProperty(DEBUG_JDWP_HOST);
if (debugHostProp != null && !debugHostProp.isEmpty()) {
debugJDWPHost = debugHostProp;
log.info("Setting ".concat(DEBUG_JDWP_HOST).concat(" = ").concat(debugHostProp));
} else {
log.info("Setting (default) ".concat(DEBUG_JDWP_HOST).concat(" = ").concat(debugJDWPHost));
}
final String debugPortProp = noormProperties.getProperty(DEBUG_JDWP_PORT);
if (debugPortProp != null && !debugPortProp.isEmpty()) {
debugJDWPPort = debugPortProp;
log.info("Setting ".concat(DEBUG_JDWP_PORT).concat(" = ").concat(debugPortProp));
} else {
log.info("Setting (default) ".concat(DEBUG_JDWP_PORT).concat(" = ").concat(debugJDWPPort));
}
}
/*
* The data source parameters in the NoORM properties can either be specified using JNDI or directly
* by providing the required parameters for setting up an OracleDataSource. When both types of
* configuration settings are available, the JNDI configuration has precedence.
*/
private void initDataSource() throws SQLException {
final String jndiName = noormProperties.getProperty(DATABASE_JNDINAME);
if (jndiName != null) {
try {
log.info("Trying to establish data source using JDNI name ".concat(jndiName));
final Context initCtx = new InitialContext();
dataSource = (DataSource) initCtx.lookup(jndiName);
log.info("JNDI data source lookup successful.");
} catch (NamingException e) {
throw new DataAccessException("JNDI data source lookup failed.", e);
}
} else {
final String url = noormProperties.getProperty(DATABASE_URL);
if (url == null) {
throw new DataAccessException(DataAccessException.Type.INITIALIZATION_FAILURE,
"Data source parameter 'database.url' not properly configured.");
} else {
log.info("Setting ".concat(DATABASE_URL).concat(" = ").concat(url));
}
final String username = noormProperties.getProperty(DATABASE_USERNAME);
if (username == null) {
throw new DataAccessException(DataAccessException.Type.INITIALIZATION_FAILURE,
"Data source parameter 'database.username' not properly configured.");
} else {
log.info("Setting ".concat(DATABASE_USERNAME).concat(" = ").concat(username));
}
final String password = noormProperties.getProperty(DATABASE_PASSWORD);
if (password == null) {
throw new DataAccessException(DataAccessException.Type.INITIALIZATION_FAILURE,
"Data source parameter 'database.password' not properly configured.");
} else {
log.info("Setting ".concat(DATABASE_PASSWORD).concat(" = ").concat(password));
}
OracleDataSource oracleDataSource = new OracleDataSource();
oracleDataSource.setURL(url);
oracleDataSource.setUser(username);
oracleDataSource.setPassword(password);
dataSource = oracleDataSource;
}
validateDataSource();
}
static void returnConnection(final OracleConnection pCon, final boolean pSuccess) {
OracleConnection con = conThreadLocal.get();
try {
if (con == null || con.isClosed()) {
if (log.isDebugEnabled()) {
log.debug("Returning connection to connection pool.");
}
// When closing the connection (resp. returning it to the connection pool, we
// always issue a commit, since in any case, there is no meaningful transaction
// scope beyond this point in time.
// Committing even in case of a read only transaction may incur some additional
// cost, since from a technical perspective, the commit is not necessary. On
// the other hand, Oracle reduces the cost in this case to a minimum, so that
// the advantage of this way of transaction automation outweighs a possible
// minimal performance reduction.
if (pSuccess) {
pCon.commit();
} else {
pCon.rollback();
}
pCon.close();
}
} catch (SQLException e) {
throw new DataAccessException(DataAccessException.Type.CONNECTION_ACCESS_FAILURE, e);
}
}
/**
* Returns the Oracle database connection currently managed by the DataSourceProvider.
* Note that the life-cycle of the connection is managed by the DataSourceProvider, i.e. acquiring a
* connection, closing a connection, resp. returning a connection to the connection pool and managing
* transaction boundaries SHOULD NOT BE DONE in the application code. Providing the database connection
* directly by this method has the sole purpose to support additional Oracle Java client software, which
* requires access to the connection (e.g. Oracle Streams Advanced Queueing, which requires the Oracle
* connection to create an AQSession).
*
* @return the Oracle database connection currently managed by the DataSourceProvider.
* @throws SQLException
*/
public static OracleConnection getConnection() throws SQLException {
return getConnection(false);
}
/**
* Retaining the connection is only required when starting a new transaction
* explicitly (indicating that the transaction is beyond the scope of a single
* PL/SQL call or insert or update operation). Having already a retained connection
* for this thread indicates that this transaction is beyond the scope of the
* caller.
*
* @param pRetain indicates, if the connection should be retained.
* @return the acquired connection.
* @throws SQLException
*/
private static OracleConnection getConnection(final boolean pRetain) throws SQLException {
OracleConnection con = conThreadLocal.get();
if (con == null || con.isClosed()) {
if (log.isDebugEnabled()) {
if (pRetain) {
log.debug("Acquiring retainable connection from connection pool.");
} else {
log.debug("Acquiring new connection from connection pool.");
}
}
// JDBC resources provided by an application server may wrap the native connection with some
// proprietary connection implementation. This prevents the usage of vendor specific features,
// in particular Oracle specific features used here. Thus, the provided connection is checked
// for a wrapped native connection and this connection is then unwrapped and used instead.
java.sql.Connection dataSourceConn = getDataSource().getConnection();
if (dataSourceConn.isWrapperFor(oracle.jdbc.OracleConnection.class)) {
con = dataSourceConn.unwrap(oracle.jdbc.OracleConnection.class);
} else {
con = (OracleConnection) dataSourceConn;
}
con.setAutoCommit(false);
if (dataSourceFactory.debugMode) {
enableDebugMode(con);
}
if (pRetain) {
conThreadLocal.set(con);
}
}
if (pRetain) {
Long tsStackThreadLocal0 = tsStackThreadLocal.get();
if (tsStackThreadLocal0 == null) {
tsStackThreadLocal.set(1L);
} else {
tsStackThreadLocal.set(tsStackThreadLocal0 + 1L);
}
if (log.isDebugEnabled()) {
log.debug("Setting transaction call stack level to ".concat(tsStackThreadLocal.get().toString()));
}
}
return con;
}
/**
* To control transactions in the calling application, the connection in use must not
* turned back to the connection pool, but should be preserved in a ThreadLocal variable.
* This method is used to indicate that the calling application intends to broaden the
* transaction boundaries beyond the scope of a single PL/SQL or DML call. Oracle does not know
* about the classical "begin" call, since an Oracle session has always an active
* transaction (after issuing a "commit" or "rollback", a new transaction is started
* implicitly).
*/
public static void begin() {
try {
getConnection(true);
} catch (Exception e) {
throw new DataAccessException(DataAccessException.Type.CONNECTION_ACCESS_FAILURE, e);
}
}
/**
* Commits a user managed transaction.
* Connections are maintained in the connection pool.
* The connection object itself is not exploited to accessing classes outside of the package, but
* for controlling transactions from outside, using methods commit() and rollback() provides the
* required functionality.
* Note that using this method requires that a user managed transaction has been started using
* method begin(), other a DataAccessException is thrown.
* Transactions, which are not user managed are always committed automatically, when the connection
* is returned to the connection pool (restricting automated transaction management to transactions
* with a scope limited to a single call to PL/SQL, an insert, update or an delete operation.
*/
public static void commit() {
OracleConnection con = null;
try {
con = conThreadLocal.get();
if (con == null || con.isClosed()) {
throw new DataAccessException(DataAccessException.Type.STALE_TRANSACTION);
}
// When the transaction stack counter indicates that the transaction is beyond the scope of
// the caller, reduce the transactions stack counter and remain the connection open.
Long tsStackThreadLocal0 = tsStackThreadLocal.get();
if (tsStackThreadLocal0 == null) {
throw new DataAccessException(DataAccessException.Type.STALE_TRANSACTION);
} else {
tsStackThreadLocal.set(tsStackThreadLocal0 - 1L);
}
if (log.isDebugEnabled()) {
log.debug("Setting transaction call stack level to ".concat(tsStackThreadLocal.get().toString()));
}
if (tsStackThreadLocal.get() == 0L) {
if (log.isDebugEnabled()) {
log.debug("Committing transaction");
}
con.commit();
} else {
if (log.isDebugEnabled()) {
log.debug("Delegating transaction termination to caller.");
}
}
} catch (SQLException e) {
throw new DataAccessException(DataAccessException.Type.CONNECTION_ACCESS_FAILURE, e);
} finally {
try {
if (con != null && !con.isClosed()) {
// Although the connection may get reused in the current thread, we do not know
// here, if the thread will be closed. In the latter case we cannot influence,
// when the connection is returned to the pool, so we do it here.
if (tsStackThreadLocal.get() == 0L) {
if (log.isDebugEnabled()) {
log.debug("Returning retained connection to connection pool.");
}
con.close();
}
}
} catch (SQLException ignored) {
} // Nothing to do
}
}
/**
* Issues a rollback on a user managed transaction.
* See DataSourceProvider.commit();
*/
public static void rollback() {
OracleConnection con = null;
try {
con = conThreadLocal.get();
if (con == null || con.isClosed()) {
throw new DataAccessException(DataAccessException.Type.STALE_TRANSACTION);
}
// When the transaction stack counter indicates that the transaction is beyond the scope of
// the caller, reduce the transactions stack counter and remain the connection open.
Long tsStackThreadLocal0 = tsStackThreadLocal.get();
if (tsStackThreadLocal0 == null) {
throw new DataAccessException(DataAccessException.Type.STALE_TRANSACTION);
} else {
tsStackThreadLocal.set(tsStackThreadLocal0 - 1L);
}
if (log.isDebugEnabled()) {
log.debug("Setting transaction call stack level to ".concat(tsStackThreadLocal.get().toString()));
}
if (tsStackThreadLocal.get() == 0L) {
if (log.isDebugEnabled()) {
log.debug("Rolling back transaction");
}
con.rollback();
} else {
if (log.isDebugEnabled()) {
log.debug("Delegating transaction termination to caller.");
}
}
} catch (SQLException e) {
throw new DataAccessException(DataAccessException.Type.CONNECTION_ACCESS_FAILURE, e);
} finally {
try {
if (con != null && !con.isClosed()) {
// Although the connection may get reused in the current thread, we do not know
// here, if the thread will be closed. In the latter case we cannot influence,
// when the connection is returned to the pool, so we do it here.
if (tsStackThreadLocal.get() == 0L) {
if (log.isDebugEnabled()) {
log.debug("Returning retained connection to connection pool.");
}
con.close();
}
}
} catch (SQLException ignored) {
} // Nothing to do
}
}
/**
* Returns the currently size of JDBC update and insert batches. The default is 100.
*
* @return the batch size.
*/
public static int getBatchUpdateSize() {
return dataSourceFactory.batchUpdateSize;
}
/**
* Sets the size of JDBC update and insert batches. The default is 100.
*
* @param pBatchUpdateSize
*/
public static void setBatchUpdateSize(final int pBatchUpdateSize) {
dataSourceFactory.batchUpdateSize = pBatchUpdateSize;
}
/**
* Shows whether debug mode has been enabled.
*
* @return true, when debug mode has been enabled, false otherwise.
*/
public boolean isDebugOn() {
return debugMode;
}
/**
* Debug mode is enabled by establishing a JPDA/JDWP connection to a listening source code debugger,
* which listens on the configured host and port for database session debugging.
* DO NOT enable debugging without an active listening source code debugger.
*
* @param pDebugMode debug mode toggle.
*/
public static void setDebugMode(final boolean pDebugMode) {
dataSourceFactory.debugMode = pDebugMode;
}
// There is no easy configurable way to control a clean disconnect for the established debug
// connection, thus clean-up of the associated socket is left the Connection, resp. Pool.
static private void enableDebugMode(final OracleConnection pCon) throws SQLException {
StringBuilder logMessage = new StringBuilder();
logMessage.append("Enabling PL/SQL debugging. Connecting to host : ");
logMessage.append(dataSourceFactory.debugJDWPHost);
logMessage.append(", port : ");
logMessage.append(dataSourceFactory.debugJDWPPort);
log.debug(logMessage.toString());
final String plSQLCall = "{ call dbms_debug_jdwp.connect_tcp(host => :host, port => :port)";
final OracleCallableStatement cstmt = (OracleCallableStatement) pCon.prepareCall(plSQLCall);
cstmt.setString("host", dataSourceFactory.debugJDWPHost);
cstmt.setString("port", dataSourceFactory.debugJDWPPort);
cstmt.execute();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy