/**
* Copyright 2010 JBoss Inc
*
* Licensed 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 bitronix.tm.resource.jdbc;
import bitronix.tm.utils.*;
import bitronix.tm.internal.*;
import bitronix.tm.resource.common.*;
import bitronix.tm.resource.jdbc.lrc.LrcXADataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.XAConnection;
import javax.transaction.SystemException;
import javax.transaction.xa.XAResource;
import java.sql.*;
import java.util.*;
import java.util.Date;
/**
* Implementation of a JDBC pooled connection wrapping vendor's {@link XAConnection} implementation.
* <p>© <a href="http://www.bitronix.be">Bitronix Software</a></p>
*
* @author lorban
*/
public class JdbcPooledConnection extends AbstractXAResourceHolder implements StateChangeListener, JdbcPooledConnectionMBean {
private final static Logger log = LoggerFactory.getLogger(JdbcPooledConnection.class);
private XAConnection xaConnection;
private Connection connection;
private XAResource xaResource;
private PoolingDataSource poolingDataSource;
private LruMap statementsCache;
private List uncachedStatements = new ArrayList();
/* management */
private String jmxName;
private Date acquisitionDate;
private Date lastReleaseDate;
public JdbcPooledConnection(PoolingDataSource poolingDataSource, XAConnection xaConnection) throws SQLException {
this.poolingDataSource = poolingDataSource;
this.xaConnection = xaConnection;
this.xaResource = xaConnection.getXAResource();
this.statementsCache = new LruMap(poolingDataSource.getPreparedStatementCacheSize());
statementsCache.addEvictionListener(new LruEvictionListener() {
public void onEviction(Object value) {
PreparedStatement stmt = (PreparedStatement) value;
try {
if (log.isDebugEnabled()) log.debug("closing evicted statement " + stmt);
stmt.close();
} catch (SQLException ex) {
log.warn("error closing evicted statement", ex);
}
}
});
connection = xaConnection.getConnection();
addStateChangeEventListener(this);
if (poolingDataSource.getClassName().equals(LrcXADataSource.class.getName())) {
if (log.isDebugEnabled()) log.debug("emulating XA for resource " + poolingDataSource.getUniqueName() + " - changing twoPcOrderingPosition to " + Scheduler.ALWAYS_LAST_POSITION);
poolingDataSource.setTwoPcOrderingPosition(Scheduler.ALWAYS_LAST_POSITION);
if (log.isDebugEnabled()) log.debug("emulating XA for resource " + poolingDataSource.getUniqueName() + " - changing deferConnectionRelease to true");
poolingDataSource.setDeferConnectionRelease(true);
if (log.isDebugEnabled()) log.debug("emulating XA for resource " + poolingDataSource.getUniqueName() + " - changing useTmJoin to true");
poolingDataSource.setUseTmJoin(true);
}
this.jmxName = "bitronix.tm:type=JdbcPooledConnection,UniqueName=" + ManagementRegistrar.makeValidName(poolingDataSource.getUniqueName()) + ",Id=" + poolingDataSource.incCreatedResourcesCounter();
ManagementRegistrar.register(jmxName, this);
}
private void applyIsolationLevel() throws SQLException {
String isolationLevel = getPoolingDataSource().getIsolationLevel();
if (isolationLevel != null) {
int level = translateIsolationLevel(isolationLevel);
if (level < 0) {
log.warn("invalid transaction isolation level '" + isolationLevel + "' configured, keeping the default isolation level.");
}
else {
if (log.isDebugEnabled()) log.debug("setting connection's isolation level to " + isolationLevel);
connection.setTransactionIsolation(level);
}
}
}
private static int translateIsolationLevel(String isolationLevelGuarantee) {
if ("READ_COMMITTED".equals(isolationLevelGuarantee)) return Connection.TRANSACTION_READ_COMMITTED;
if ("READ_UNCOMMITTED".equals(isolationLevelGuarantee)) return Connection.TRANSACTION_READ_UNCOMMITTED;
if ("REPEATABLE_READ".equals(isolationLevelGuarantee)) return Connection.TRANSACTION_REPEATABLE_READ;
if ("SERIALIZABLE".equals(isolationLevelGuarantee)) return Connection.TRANSACTION_SERIALIZABLE;
return -1;
}
public void close() throws SQLException {
setState(STATE_CLOSED);
// cleanup of pooled resources
Iterator it = statementsCache.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
PreparedStatement stmt = (PreparedStatement) entry.getValue();
stmt.close();
}
ManagementRegistrar.unregister(jmxName);
connection.close();
xaConnection.close();
}
public RecoveryXAResourceHolder createRecoveryXAResourceHolder() {
return new RecoveryXAResourceHolder(this);
}
private void testConnection(Connection connection) throws SQLException {
String query = poolingDataSource.getTestQuery();
if (query == null) {
if (log.isDebugEnabled()) log.debug("no query to test connection of " + this + ", skipping test");
return;
}
if (log.isDebugEnabled()) log.debug("testing with query '" + query + "' connection of " + this);
PreparedStatement stmt = connection.prepareStatement(query);
ResultSet rs = stmt.executeQuery();
rs.close();
stmt.close();
if (log.isDebugEnabled()) log.debug("successfully tested connection of " + this);
}
protected void release() throws SQLException {
if (log.isDebugEnabled()) log.debug("releasing to pool " + this);
//TODO: even if delisting fails, requeuing should be done or we'll have a connection leak here
// delisting
try {
TransactionContextHelper.delistFromCurrentTransaction(this, poolingDataSource);
} catch (BitronixRollbackSystemException ex) {
throw (SQLException) new SQLException("unilateral rollback of " + this).initCause(ex);
} catch (SystemException ex) {
throw (SQLException) new SQLException("error delisting " + this).initCause(ex);
}
// requeuing
try {
TransactionContextHelper.requeue(this, poolingDataSource);
} catch (BitronixSystemException ex) {
throw (SQLException) new SQLException("error requeueing " + this).initCause(ex);
}
if (log.isDebugEnabled()) log.debug("released to pool " + this);
}
public XAResource getXAResource() {
return xaResource;
}
public PoolingDataSource getPoolingDataSource() {
return poolingDataSource;
}
public List getXAResourceHolders() {
List xaResourceHolders = new ArrayList();
xaResourceHolders.add(this);
return xaResourceHolders;
}
public Object getConnectionHandle() throws Exception {
if (log.isDebugEnabled()) log.debug("getting connection handle from " + this);
int oldState = getState();
setState(STATE_ACCESSIBLE);
if (oldState == STATE_IN_POOL) {
if (log.isDebugEnabled()) log.debug("connection " + xaConnection + " was in state IN_POOL, testing it");
testConnection(connection);
applyIsolationLevel();
}
else {
if (log.isDebugEnabled()) log.debug("connection " + xaConnection + " was in state " + Decoder.decodeXAStatefulHolderState(oldState) + ", no need to test it");
}
if (log.isDebugEnabled()) log.debug("got connection handle from " + this);
return new JdbcConnectionHandle(this, connection);
}
public void stateChanged(XAStatefulHolder source, int oldState, int newState) {
if (newState == STATE_IN_POOL) {
if (log.isDebugEnabled()) log.debug("requeued JDBC connection of " + poolingDataSource);
lastReleaseDate = new Date();
}
if (oldState == STATE_IN_POOL && newState == STATE_ACCESSIBLE) {
acquisitionDate = new Date();
}
if (oldState == STATE_NOT_ACCESSIBLE && newState == STATE_ACCESSIBLE) {
TransactionContextHelper.recycle(this);
}
}
public void stateChanging(XAStatefulHolder source, int currentState, int futureState) {
if (futureState == STATE_IN_POOL) {
// close all uncached statements
if (log.isDebugEnabled()) log.debug("closing " + uncachedStatements.size() + " uncached statement(s)");
for (int i = 0; i < uncachedStatements.size(); i++) {
Statement statement = (Statement) uncachedStatements.get(i);
try {
statement.close();
} catch (SQLException ex) {
if (log.isDebugEnabled()) log.debug("error trying to close uncached statement " + statement, ex);
}
}
uncachedStatements.clear();
// clear SQL warnings
try {
if (log.isDebugEnabled()) log.debug("clearing warnings of " + connection);
connection.clearWarnings();
} catch (SQLException ex) {
if (log.isDebugEnabled()) log.debug("error cleaning warnings of " + connection, ex);
}
}
}
/**
* Get a PreparedStatement from cache.
* @param sql the key that has been used to cache the statement.
* @return the cached statement corresponding to the key or null if no statement is cached under that key.
*/
protected PreparedStatement getCachedStatement(String sql) {
PreparedStatement stmt = (PreparedStatement) statementsCache.get(sql);
if (log.isDebugEnabled()) log.debug("statement cache lookup of <" + sql + "> in " + this + ": " + stmt);
return stmt;
}
/**
* Put a PreparedStatement in the cache.
* @param sql the key that is used to cache the statement.
* @param stmt the statement to cache.
* @return the cached statement.
*/
protected PreparedStatement putCachedStatement(String sql, PreparedStatement stmt) {
if (log.isDebugEnabled()) log.debug("caching statement <" + sql + "> in " + this);
return (PreparedStatement) statementsCache.put(sql, stmt);
}
/**
* Register uncached statement so that it can be closed when the connection is put back in the pool.
* @param stmt the statement to register.
* @return the registered statement.
*/
protected Statement registerUncachedStatement(Statement stmt) {
uncachedStatements.add(stmt);
return stmt;
}
public String toString() {
return "a JdbcPooledConnection from datasource " + poolingDataSource.getUniqueName() + " in state " + Decoder.decodeXAStatefulHolderState(getState()) + " wrapping " + xaConnection;
}
/* management */
public String getStateDescription() {
return Decoder.decodeXAStatefulHolderState(getState());
}
public Date getAcquisitionDate() {
return acquisitionDate;
}
public Date getLastReleaseDate() {
return lastReleaseDate;
}
public String getTransactionGtridCurrentlyHoldingThis() {
return getXAResourceHolderState().getXid().getGlobalTransactionIdUid().toString();
}
}