001/** 002 * 003 * Licensed to the Apache Software Foundation (ASF) under one or more 004 * contributor license agreements. See the NOTICE file distributed with 005 * this work for additional information regarding copyright ownership. 006 * The ASF licenses this file to You under the Apache License, Version 2.0 007 * (the "License"); you may not use this file except in compliance with 008 * the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.commons.dbcp2.managed; 019 020import org.apache.commons.dbcp2.DelegatingConnection; 021import org.apache.commons.pool2.ObjectPool; 022 023import java.sql.Connection; 024import java.sql.SQLException; 025import java.util.concurrent.locks.Lock; 026import java.util.concurrent.locks.ReentrantLock; 027 028/** 029 * ManagedConnection is responsible for managing a database connection in a transactional environment (typically called 030 * "Container Managed"). A managed connection operates like any other connection when no global transaction (a.k.a. XA 031 * transaction or JTA Transaction) is in progress. When a global transaction is active a single physical connection to 032 * the database is used by all ManagedConnections accessed in the scope of the transaction. Connection sharing means 033 * that all data access during a transaction has a consistent view of the database. When the global transaction is 034 * committed or rolled back the enlisted connections are committed or rolled back. Typically upon transaction 035 * completion, a connection returns to the auto commit setting in effect before being enlisted in the transaction, but 036 * some vendors do not properly implement this. 037 * <p> 038 * When enlisted in a transaction the setAutoCommit(), commit(), rollback(), and setReadOnly() methods throw a 039 * SQLException. This is necessary to assure that the transaction completes as a single unit. 040 * </p> 041 * 042 * @param <C> 043 * the Connection type 044 * 045 * @since 2.0 046 */ 047public class ManagedConnection<C extends Connection> extends DelegatingConnection<C> { 048 049 private final ObjectPool<C> pool; 050 private final TransactionRegistry transactionRegistry; 051 private final boolean accessToUnderlyingConnectionAllowed; 052 private TransactionContext transactionContext; 053 private boolean isSharedConnection; 054 private final Lock lock; 055 056 /** 057 * Constructs a new instance responsible for managing a database connection in a transactional environment. 058 * 059 * @param pool 060 * The connection pool. 061 * @param transactionRegistry 062 * The transaction registry. 063 * @param accessToUnderlyingConnectionAllowed 064 * Whether or not to allow access to the underlying Connection. 065 * @throws SQLException 066 * Thrown when there is problem managing transactions. 067 */ 068 public ManagedConnection(final ObjectPool<C> pool, final TransactionRegistry transactionRegistry, 069 final boolean accessToUnderlyingConnectionAllowed) throws SQLException { 070 super(null); 071 this.pool = pool; 072 this.transactionRegistry = transactionRegistry; 073 this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed; 074 this.lock = new ReentrantLock(); 075 updateTransactionStatus(); 076 } 077 078 @Override 079 protected void checkOpen() throws SQLException { 080 super.checkOpen(); 081 updateTransactionStatus(); 082 } 083 084 private void updateTransactionStatus() throws SQLException { 085 // if there is a is an active transaction context, assure the transaction context hasn't changed 086 if (transactionContext != null && !transactionContext.isTransactionComplete()) { 087 if (transactionContext.isActive()) { 088 if (transactionContext != transactionRegistry.getActiveTransactionContext()) { 089 throw new SQLException("Connection can not be used while enlisted in another transaction"); 090 } 091 return; 092 } 093 // transaction should have been cleared up by TransactionContextListener, but in 094 // rare cases another lister could have registered which uses the connection before 095 // our listener is called. In that rare case, trigger the transaction complete call now 096 transactionComplete(); 097 } 098 099 // the existing transaction context ended (or we didn't have one), get the active transaction context 100 transactionContext = transactionRegistry.getActiveTransactionContext(); 101 102 // if there is an active transaction context and it already has a shared connection, use it 103 if (transactionContext != null && transactionContext.getSharedConnection() != null) { 104 // A connection for the connection factory has already been enrolled 105 // in the transaction, replace our delegate with the enrolled connection 106 107 // return current connection to the pool 108 final C connection = getDelegateInternal(); 109 setDelegate(null); 110 if (connection != null) { 111 try { 112 pool.returnObject(connection); 113 } catch (final Exception ignored) { 114 // whatever... try to invalidate the connection 115 try { 116 pool.invalidateObject(connection); 117 } catch (final Exception ignore) { 118 // no big deal 119 } 120 } 121 } 122 123 // add a listener to the transaction context 124 transactionContext.addTransactionContextListener(new CompletionListener()); 125 126 // Set our delegate to the shared connection. Note that this will 127 // always be of type C since it has been shared by another 128 // connection from the same pool. 129 @SuppressWarnings("unchecked") 130 final C shared = (C) transactionContext.getSharedConnection(); 131 setDelegate(shared); 132 133 // remember that we are using a shared connection so it can be cleared after the 134 // transaction completes 135 isSharedConnection = true; 136 } else { 137 C connection = getDelegateInternal(); 138 // if our delegate is null, create one 139 if (connection == null) { 140 try { 141 // borrow a new connection from the pool 142 connection = pool.borrowObject(); 143 setDelegate(connection); 144 } catch (final Exception e) { 145 throw new SQLException("Unable to acquire a new connection from the pool", e); 146 } 147 } 148 149 // if we have a transaction, out delegate becomes the shared delegate 150 if (transactionContext != null) { 151 // add a listener to the transaction context 152 transactionContext.addTransactionContextListener(new CompletionListener()); 153 154 // register our connection as the shared connection 155 try { 156 transactionContext.setSharedConnection(connection); 157 } catch (final SQLException e) { 158 // transaction is hosed 159 transactionContext = null; 160 try { 161 pool.invalidateObject(connection); 162 } catch (final Exception e1) { 163 // we are try but no luck 164 } 165 throw e; 166 } 167 } 168 } 169 // autoCommit may have been changed directly on the underlying 170 // connection 171 clearCachedState(); 172 } 173 174 @Override 175 public void close() throws SQLException { 176 if (!isClosedInternal()) { 177 try { 178 // Don't actually close the connection if in a transaction. The 179 // connection will be closed by the transactionComplete method. 180 // 181 // DBCP-484 we need to make sure setClosedInternal(true) being 182 // invoked if transactionContext is not null as this value will 183 // be modified by the transactionComplete method which could run 184 // in the different thread with the transaction calling back. 185 lock.lock(); 186 if (transactionContext == null || transactionContext.isTransactionComplete()) { 187 super.close(); 188 } 189 } finally { 190 setClosedInternal(true); 191 lock.unlock(); 192 } 193 } 194 } 195 196 /** 197 * Delegates to {@link ManagedConnection#transactionComplete()} for transaction completion events. 198 * 199 * @since 2.0 200 */ 201 protected class CompletionListener implements TransactionContextListener { 202 @Override 203 public void afterCompletion(final TransactionContext completedContext, final boolean commited) { 204 if (completedContext == transactionContext) { 205 transactionComplete(); 206 } 207 } 208 } 209 210 protected void transactionComplete() { 211 lock.lock(); 212 transactionContext.completeTransaction(); 213 lock.unlock(); 214 215 // If we were using a shared connection, clear the reference now that 216 // the transaction has completed 217 if (isSharedConnection) { 218 setDelegate(null); 219 isSharedConnection = false; 220 } 221 222 // If this connection was closed during the transaction and there is 223 // still a delegate present close it 224 final Connection delegate = getDelegateInternal(); 225 if (isClosedInternal() && delegate != null) { 226 try { 227 setDelegate(null); 228 229 if (!delegate.isClosed()) { 230 delegate.close(); 231 } 232 } catch (final SQLException ignored) { 233 // Not a whole lot we can do here as connection is closed 234 // and this is a transaction callback so there is no 235 // way to report the error. 236 } 237 } 238 } 239 240 // 241 // The following methods can't be used while enlisted in a transaction 242 // 243 244 @Override 245 public void setAutoCommit(final boolean autoCommit) throws SQLException { 246 if (transactionContext != null) { 247 throw new SQLException("Auto-commit can not be set while enrolled in a transaction"); 248 } 249 super.setAutoCommit(autoCommit); 250 } 251 252 @Override 253 public void commit() throws SQLException { 254 if (transactionContext != null) { 255 throw new SQLException("Commit can not be set while enrolled in a transaction"); 256 } 257 super.commit(); 258 } 259 260 @Override 261 public void rollback() throws SQLException { 262 if (transactionContext != null) { 263 throw new SQLException("Commit can not be set while enrolled in a transaction"); 264 } 265 super.rollback(); 266 } 267 268 @Override 269 public void setReadOnly(final boolean readOnly) throws SQLException { 270 if (transactionContext != null) { 271 throw new SQLException("Read-only can not be set while enrolled in a transaction"); 272 } 273 super.setReadOnly(readOnly); 274 } 275 276 // 277 // Methods for accessing the delegate connection 278 // 279 280 /** 281 * If false, getDelegate() and getInnermostDelegate() will return null. 282 * 283 * @return if false, getDelegate() and getInnermostDelegate() will return null 284 */ 285 public boolean isAccessToUnderlyingConnectionAllowed() { 286 return accessToUnderlyingConnectionAllowed; 287 } 288 289 @Override 290 public C getDelegate() { 291 if (isAccessToUnderlyingConnectionAllowed()) { 292 return getDelegateInternal(); 293 } 294 return null; 295 } 296 297 @Override 298 public Connection getInnermostDelegate() { 299 if (isAccessToUnderlyingConnectionAllowed()) { 300 return super.getInnermostDelegateInternal(); 301 } 302 return null; 303 } 304}