View Javadoc
1   /*
2   
3     Licensed to the Apache Software Foundation (ASF) under one or more
4     contributor license agreements.  See the NOTICE file distributed with
5     this work for additional information regarding copyright ownership.
6     The ASF licenses this file to You under the Apache License, Version 2.0
7     (the "License"); you may not use this file except in compliance with
8     the License.  You may obtain a copy of the License at
9   
10        http://www.apache.org/licenses/LICENSE-2.0
11  
12     Unless required by applicable law or agreed to in writing, software
13     distributed under the License is distributed on an "AS IS" BASIS,
14     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15     See the License for the specific language governing permissions and
16     limitations under the License.
17   */
18  package org.apache.commons.dbcp2.managed;
19  
20  import java.sql.Connection;
21  import java.sql.SQLException;
22  import java.util.concurrent.locks.Lock;
23  import java.util.concurrent.locks.ReentrantLock;
24  
25  import org.apache.commons.dbcp2.DelegatingConnection;
26  import org.apache.commons.pool2.ObjectPool;
27  
28  /**
29   * ManagedConnection is responsible for managing a database connection in a transactional environment (typically called
30   * "Container Managed"). A managed connection operates like any other connection when no global transaction (a.k.a. XA
31   * transaction or JTA Transaction) is in progress. When a global transaction is active a single physical connection to
32   * the database is used by all ManagedConnections accessed in the scope of the transaction. Connection sharing means
33   * that all data access during a transaction has a consistent view of the database. When the global transaction is
34   * committed or rolled back the enlisted connections are committed or rolled back. Typically upon transaction
35   * completion, a connection returns to the auto commit setting in effect before being enlisted in the transaction, but
36   * some vendors do not properly implement this.
37   * <p>
38   * When enlisted in a transaction the setAutoCommit(), commit(), rollback(), and setReadOnly() methods throw a
39   * SQLException. This is necessary to assure that the transaction completes as a single unit.
40   * </p>
41   *
42   * @param <C>
43   *            the Connection type
44   *
45   * @since 2.0
46   */
47  public class ManagedConnection<C extends Connection> extends DelegatingConnection<C> {
48  
49      /**
50       * Delegates to {@link ManagedConnection#transactionComplete()} for transaction completion events.
51       *
52       * @since 2.0
53       */
54      protected class CompletionListener implements TransactionContextListener {
55          @Override
56          public void afterCompletion(final TransactionContext completedContext, final boolean committed) {
57              if (completedContext == transactionContext) {
58                  transactionComplete();
59              }
60          }
61      }
62      private final ObjectPool<C> pool;
63      private final TransactionRegistry transactionRegistry;
64      private final boolean accessToUnderlyingConnectionAllowed;
65      private TransactionContext transactionContext;
66      private boolean isSharedConnection;
67  
68      private final Lock lock;
69  
70      /**
71       * Constructs a new instance responsible for managing a database connection in a transactional environment.
72       *
73       * @param pool
74       *            The connection pool.
75       * @param transactionRegistry
76       *            The transaction registry.
77       * @param accessToUnderlyingConnectionAllowed
78       *            Whether or not to allow access to the underlying Connection.
79       * @throws SQLException
80       *             Thrown when there is problem managing transactions.
81       */
82      public ManagedConnection(final ObjectPool<C> pool, final TransactionRegistry transactionRegistry,
83              final boolean accessToUnderlyingConnectionAllowed) throws SQLException {
84          super(null);
85          this.pool = pool;
86          this.transactionRegistry = transactionRegistry;
87          this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed;
88          this.lock = new ReentrantLock();
89          updateTransactionStatus();
90      }
91  
92      @Override
93      protected void checkOpen() throws SQLException {
94          super.checkOpen();
95          updateTransactionStatus();
96      }
97  
98      @Override
99      public void close() throws SQLException {
100         if (!isClosedInternal()) {
101             // Don't actually close the connection if in a transaction. The
102             // connection will be closed by the transactionComplete method.
103             //
104             // DBCP-484 we need to make sure setClosedInternal(true) being
105             // invoked if transactionContext is not null as this value will
106             // be modified by the transactionComplete method which could run
107             // in the different thread with the transaction calling back.
108             lock.lock();
109             try {
110                 if (transactionContext == null || transactionContext.isTransactionComplete()) {
111                     super.close();
112                 }
113             } finally {
114                 try {
115                     setClosedInternal(true);
116                 } finally {
117                     lock.unlock();
118                 }
119             }
120         }
121     }
122 
123     @Override
124     public void commit() throws SQLException {
125         if (transactionContext != null) {
126             throw new SQLException("Commit can not be set while enrolled in a transaction");
127         }
128         super.commit();
129     }
130 
131     @Override
132     public C getDelegate() {
133         if (isAccessToUnderlyingConnectionAllowed()) {
134             return getDelegateInternal();
135         }
136         return null;
137     }
138 
139     //
140     // The following methods can't be used while enlisted in a transaction
141     //
142 
143     @Override
144     public Connection getInnermostDelegate() {
145         if (isAccessToUnderlyingConnectionAllowed()) {
146             return super.getInnermostDelegateInternal();
147         }
148         return null;
149     }
150 
151     /**
152      * @return The transaction context.
153      * @since 2.6.0
154      */
155     public TransactionContext getTransactionContext() {
156         return transactionContext;
157     }
158 
159     /**
160      * @return The transaction registry.
161      * @since 2.6.0
162      */
163     public TransactionRegistry getTransactionRegistry() {
164         return transactionRegistry;
165     }
166 
167     /**
168      * If false, getDelegate() and getInnermostDelegate() will return null.
169      *
170      * @return if false, getDelegate() and getInnermostDelegate() will return null
171      */
172     public boolean isAccessToUnderlyingConnectionAllowed() {
173         return accessToUnderlyingConnectionAllowed;
174     }
175 
176     //
177     // Methods for accessing the delegate connection
178     //
179 
180     @Override
181     public void rollback() throws SQLException {
182         if (transactionContext != null) {
183             throw new SQLException("Commit can not be set while enrolled in a transaction");
184         }
185         super.rollback();
186     }
187 
188     @Override
189     public void setAutoCommit(final boolean autoCommit) throws SQLException {
190         if (transactionContext != null) {
191             throw new SQLException("Auto-commit can not be set while enrolled in a transaction");
192         }
193         super.setAutoCommit(autoCommit);
194     }
195 
196     @Override
197     public void setReadOnly(final boolean readOnly) throws SQLException {
198         if (transactionContext != null) {
199             throw new SQLException("Read-only can not be set while enrolled in a transaction");
200         }
201         super.setReadOnly(readOnly);
202     }
203 
204     /**
205      * Completes the transaction.
206      */
207     protected void transactionComplete() {
208         lock.lock();
209         try {
210             transactionContext.completeTransaction();
211         } finally {
212             lock.unlock();
213         }
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         // autoCommit may have been changed directly on the underlying connection
223         clearCachedState();
224 
225         // If this connection was closed during the transaction and there is
226         // still a delegate present close it
227         final Connection delegate = getDelegateInternal();
228         if (isClosedInternal() && delegate != null) {
229             try {
230                 setDelegate(null);
231 
232                 if (!delegate.isClosed()) {
233                     delegate.close();
234                 }
235             } catch (final SQLException ignored) {
236                 // Not a whole lot we can do here as connection is closed
237                 // and this is a transaction callback so there is no
238                 // way to report the error.
239             }
240         }
241     }
242 
243     private void updateTransactionStatus() throws SQLException {
244         // if there is a is an active transaction context, assure the transaction context hasn't changed
245         if (transactionContext != null && !transactionContext.isTransactionComplete()) {
246             if (transactionContext.isActive()) {
247                 if (transactionContext != transactionRegistry.getActiveTransactionContext()) {
248                     throw new SQLException("Connection can not be used while enlisted in another transaction");
249                 }
250                 return;
251             }
252             // transaction should have been cleared up by TransactionContextListener, but in
253             // rare cases another lister could have registered which uses the connection before
254             // our listener is called. In that rare case, trigger the transaction complete call now
255             transactionComplete();
256         }
257 
258         // the existing transaction context ended (or we didn't have one), get the active transaction context
259         transactionContext = transactionRegistry.getActiveTransactionContext();
260 
261         // if there is an active transaction context and it already has a shared connection, use it
262         if (transactionContext != null && transactionContext.getSharedConnection() != null) {
263             // A connection for the connection factory has already been enrolled
264             // in the transaction, replace our delegate with the enrolled connection
265 
266             // return current connection to the pool
267             @SuppressWarnings("resource")
268             final C connection = getDelegateInternal();
269             setDelegate(null);
270             if (connection != null && transactionContext.getSharedConnection() != connection) {
271                 try {
272                     pool.returnObject(connection);
273                 } catch (final Exception ignored) {
274                     // whatever... try to invalidate the connection
275                     try {
276                         pool.invalidateObject(connection);
277                     } catch (final Exception ignore) {
278                         // no big deal
279                     }
280                 }
281             }
282 
283             // add a listener to the transaction context
284             transactionContext.addTransactionContextListener(new CompletionListener());
285 
286             // Set our delegate to the shared connection. Note that this will
287             // always be of type C since it has been shared by another
288             // connection from the same pool.
289             @SuppressWarnings("unchecked")
290             final C shared = (C) transactionContext.getSharedConnection();
291             setDelegate(shared);
292 
293             // remember that we are using a shared connection so it can be cleared after the
294             // transaction completes
295             isSharedConnection = true;
296         } else {
297             C connection = getDelegateInternal();
298             // if our delegate is null, create one
299             if (connection == null) {
300                 try {
301                     // borrow a new connection from the pool
302                     connection = pool.borrowObject();
303                     setDelegate(connection);
304                 } catch (final Exception e) {
305                     throw new SQLException("Unable to acquire a new connection from the pool", e);
306                 }
307             }
308 
309             // if we have a transaction, out delegate becomes the shared delegate
310             if (transactionContext != null) {
311                 // add a listener to the transaction context
312                 transactionContext.addTransactionContextListener(new CompletionListener());
313 
314                 // register our connection as the shared connection
315                 try {
316                     transactionContext.setSharedConnection(connection);
317                 } catch (final SQLException e) {
318                     // transaction is hosed
319                     transactionContext = null;
320                     try {
321                         pool.invalidateObject(connection);
322                     } catch (final Exception e1) {
323                         // we are try but no luck
324                     }
325                     throw e;
326                 }
327             }
328         }
329         // autoCommit may have been changed directly on the underlying
330         // connection
331         clearCachedState();
332     }
333 }