/* * Copyright (c) OSGi Alliance (2013). All Rights Reserved. * * 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 osgi.jpa.managed.support; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.Connection; import java.sql.SQLException; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.sql.DataSource; import javax.sql.XAConnection; import javax.sql.XADataSource; import javax.transaction.Synchronization; import javax.transaction.Transaction; import javax.transaction.TransactionManager; /** * The intent of this class is wrap an XA Data Source in transactional or * non-transactional mode. If used in transactional mode it enlists any * connections that are obtained through this object and ensure that non of the * transaction methods are called on the connection in that case. */ class DataSourceWrapper implements InvocationHandler { final XADataSource xaDataSource; final TransactionManager transactionManager; final boolean transactionMode; final Map<Transaction, TransactionSession> xaConnections = new ConcurrentHashMap<Transaction, TransactionSession>(); final JPABridgeLogMessages msgs; final Set<Connection> connections = Collections .synchronizedSet(new HashSet<Connection>()); private DataSource datasource; /** * A TransactionSession is created when a XAConnection is used in a * Transaction, it then lives until the transactions is ended. This class * ensures enlisting/delisting and closes all used connections in this * transaction. */ class TransactionSession { final XAConnection xaConnection; final Set<Connection> connections = new HashSet<Connection>(); final Transaction transaction; TransactionSession(Transaction transaction, XAConnection xaConnection) throws Exception { this.transaction = transaction; this.xaConnection = xaConnection; transaction.enlistResource(xaConnection.getXAResource()); } @SuppressWarnings("unused") public Connection getConnection() throws SQLException { Connection connection = xaConnection.getConnection(); connections.add(connection); // // The connection is under control of a transaction // so make sure it is not used wrongly. We wrap // it and throw exceptions when then try to call // transaction control methods. Also ignore // close since we will close the connection at the end // of the commit // return new ConnectionWrapper(connection) { public void setAutoCommit(boolean autoCommit) throws SQLException { throw new UnsupportedOperationException("jta transaction"); } public boolean getAutoCommit() throws SQLException { throw new UnsupportedOperationException("jta transaction"); } public void commit() throws SQLException { throw new UnsupportedOperationException("jta transaction"); } public void rollback() throws SQLException { throw new UnsupportedOperationException("jta transaction"); } public void close() throws SQLException { msgs.step("close on connection due to jta ignored " + delegate); // Do not close the connection until the end of // of the commit. } }.getConnection(); } public void close(int status) throws Exception { for (Connection c : connections) { c.close(); } } } public DataSourceWrapper(TransactionManager tm, XADataSource ds, boolean transactionMode, JPABridgeLogMessages msgs) { this.transactionManager = tm; this.xaDataSource = ds; this.transactionMode = transactionMode; this.msgs = msgs; this.datasource = (DataSource) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { DataSource.class }, this); } @Override public Object invoke(Object target, Method method, Object[] args) throws Throwable { try { if (method.getName().equals("getConnection")) // getConnection()/getConnection(user,password) return getLocalConnection(); if (method.getName().equals("close") && args.length == 0) close(); return method.invoke(target, args); } catch (InvocationTargetException ite) { throw ite.getTargetException(); } } public DataSource getDataSource() { return datasource; } // // Close any connections outside a transaction // public void close() { System.out.println("Close all connections " + connections); for (Connection c : connections) { try { c.close(); } catch (SQLException e) { // ignore } } } public Connection getLocalConnection() throws SQLException { // // If we are not transaction, just return a connection // and leave it up to the caller to close it. // if (!transactionMode) { return add(xaDataSource.getXAConnection().getConnection()); } try { // // We're now in transaction mode. So we need to enlist // ourselves with the current transaction. So we // require a real transaction // final Transaction transaction = transactionManager.getTransaction(); if (transaction == null) { // // SPEC: Hibernate was calling a Jta Data Source without a // transaction so we return a non-transaction data source. // not sure what the spec says about this. The right // thing to do is throw an exception I would say. // throw new // SQLException("A JTA Data source requires a transaction and there is none"); return xaDataSource.getXAConnection().getConnection(); } // // SPEC: Initially got this wrong, it seems you can enlist a // XAConnection only once per transaction. So we now use a // session to control the life cycle: Transaction -> XAConnection // -> * Connection // msgs.step("get jta Connection " + transaction); TransactionSession session = xaConnections.get(transaction); if (session != null) // // The session already exists, so just another // connection is required. Enlisting is already done // return add(session.getConnection()); // // First time a connections is required for this // transaction. Create a session to track the // connections created in this transaction // session = new TransactionSession(transaction, xaDataSource.getXAConnection()); xaConnections.put(transaction, session); // // Just to make it visible to the inner class // final TransactionSession session0 = session; // // Ensure we close the connection at the end of the transaction // by registering a callback with the transaction. // transactionManager.getTransaction().registerSynchronization(new Synchronization() { @Override public void beforeCompletion() { } @Override public void afterCompletion(int status) { try { msgs.step("close transaction " + transaction + " " + status + " " + Thread.currentThread()); session0.close(status); } catch (Exception e) { // Ignore since these errors // should have been handled during // the commit phase of the transaction msgs.failed("closing connection after transaction close", e); } } }); return session0.getConnection(); } catch (SQLException e) { throw e; } catch (Exception e) { throw new SQLException("DataSourceWrapper:getConnection", e); } } @SuppressWarnings("unused") private Connection add(final Connection connection) { connections.add(connection); return new ConnectionWrapper(connection) { public void close() { System.out.println("close " + connection); connections.remove(connection); } }.getConnection(); } /** * Just show that we're a proxy on another XA Data Source */ @Override public String toString() { return xaDataSource + "'"; } // // Full Connection delegator kept separate to increase readability // of main code. // static class ConnectionWrapper implements InvocationHandler { protected Connection delegate; private Connection proxy; ConnectionWrapper(Connection delegate) { this.delegate = delegate; this.proxy = (Connection) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { Connection.class }, this); } @Override public Object invoke(Object target, Method method, Object[] args) throws Throwable { try { // TODO Bit inefficient, should work for now Method m = getClass().getMethod(method.getName(), method.getParameterTypes()); return m.invoke(this, args); } catch (NoSuchMethodException e) { try { return method.invoke(delegate, args); } catch (InvocationTargetException t) { throw t.getTargetException(); } } catch (InvocationTargetException ite) { throw ite.getTargetException(); } } public Connection getConnection() { return proxy; } } }