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.dbcp.managed;
19  
20  import org.apache.commons.dbcp.DelegatingConnection;
21  import org.apache.commons.pool.ObjectPool;
22  
23  import java.sql.Connection;
24  import java.sql.SQLException;
25  
26  /**
27   * ManagedConnection is responsible for managing a database connection in a transactional environment
28   * (typically called "Container Managed").  A managed connection opperates like any other connection
29   * when no gloabal transaction (a.k.a. XA transaction or JTA Transaction) is in progress.  When a
30   * global transaction is active a single physical connection to the database is used by all
31   * ManagedConnections accessed in the scope of the transaction.  Connection sharing means that all
32   * data access during a transaction has a consistent view of the database.  When the global transaction
33   * is committed or rolled back the enlisted connections are committed or rolled back.  Typically upon
34   * transaction completion, a connection returns to the auto commit setting in effect before being
35   * elisted in the transaction, but some vendors do not propertly implement this.
36   *
37   * When enslisted in a transaction the setAutoCommit(), commit(), rollback(), and setReadOnly() methods
38   * throw a SQLException.  This is necessary to assure that the transaction completes as a single unit.
39   *
40   * @author Dain Sundstrom
41   * @version $Revision$
42   */
43  public class ManagedConnection extends DelegatingConnection {
44      private final ObjectPool pool;
45      private final TransactionRegistry transactionRegistry;
46      private final boolean accessToUnderlyingConnectionAllowed;
47      private TransactionContext transactionContext;
48      private boolean isSharedConnection;
49  
50      public ManagedConnection(ObjectPool pool, TransactionRegistry transactionRegistry, boolean accessToUnderlyingConnectionAllowed) throws SQLException {
51          super(null);
52          this.pool = pool;
53          this.transactionRegistry = transactionRegistry;
54          this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed;
55          updateTransactionStatus();
56      }
57  
58      protected void checkOpen() throws SQLException {
59          super.checkOpen();
60          updateTransactionStatus();
61      }
62  
63      private void updateTransactionStatus() throws SQLException {
64          // if there is a is an active transaction context, assure the transaction context hasn't changed
65          if (transactionContext != null) {
66              if (transactionContext.isActive()) {
67                  if (transactionContext != transactionRegistry.getActiveTransactionContext()) {
68                      throw new SQLException("Connection can not be used while enlisted in another transaction");
69                  }
70                  return;
71              } else {
72                  // transaction should have been cleared up by TransactionContextListener, but in
73                  // rare cases another lister could have registered which uses the connection before
74                  // our listener is called.  In that rare case, trigger the transaction complete call now
75                  transactionComplete();
76              }
77          }
78  
79          // the existing transction context ended (or we didn't have one), get the active transaction context
80          transactionContext = transactionRegistry.getActiveTransactionContext();
81  
82          // if there is an active transaction context and it already has a shared connection, use it
83          if (transactionContext != null && transactionContext.getSharedConnection() != null) {
84              // A connection for the connection factory has already been enrolled
85              // in the transaction, replace our delegate with the enrolled connection
86  
87              // return current connection to the pool
88              Connection connection = getDelegateInternal();
89              setDelegate(null);
90              if (connection != null) {
91                  try {
92                      pool.returnObject(connection);
93                  } catch (Exception ignored) {
94                      // whatever... try to invalidat the connection
95                      try {
96                          pool.invalidateObject(connection);
97                      } catch (Exception ignore) {
98                          // no big deal
99                      }
100                 }
101             }
102 
103             // add a listener to the transaction context
104             transactionContext.addTransactionContextListener(new CompletionListener());
105 
106             // set our delegate to the shared connection
107             setDelegate(transactionContext.getSharedConnection());
108 
109             // remember that we are using a shared connection so it can be cleared after the
110             // transaction completes
111             isSharedConnection = true;
112         } else {
113             // if our delegate is null, create one
114             if (getDelegateInternal() == null) {
115                 try {
116                     // borrow a new connection from the pool
117                     Connection connection = (Connection) pool.borrowObject();
118                     setDelegate(connection);
119                 } catch (Exception e) {
120                     throw (SQLException) new SQLException("Unable to acquire a new connection from the pool").initCause(e);
121                 }
122             }
123 
124             // if we have a transaction, out delegate becomes the shared delegate
125             if (transactionContext != null) {
126                 // add a listener to the transaction context
127                 transactionContext.addTransactionContextListener(new CompletionListener());
128 
129                 // register our connection as the shared connection
130                 try {
131                     transactionContext.setSharedConnection(getDelegateInternal());
132                 } catch (SQLException e) {
133                     // transaction is hosed
134                     transactionContext = null;
135                     throw e;
136                 }
137             }
138         }
139     }
140 
141     public void close() throws SQLException {
142         if (!_closed) {
143             try {
144                 // don't actually close the connection if in a transaction
145                 // the connection will be closed by the transactionComplete method
146                 if (transactionContext == null) {
147                     getDelegateInternal().close();
148                 }
149             } finally {
150                 _closed = true;
151             }
152         }
153     }
154 
155     /**
156      * Delegates to {@link ManagedConnection#transactionComplete()} 
157      * for transaction completion events. 
158      */
159     protected class CompletionListener implements TransactionContextListener {
160         public void afterCompletion(TransactionContext completedContext, boolean commited) {
161             if (completedContext == transactionContext) {
162                 transactionComplete();
163             }
164         }
165     }
166 
167     protected void transactionComplete() {
168         transactionContext = null;
169 
170         // if we were using a shared connection, clear the reference now that the transaction has completed
171         if (isSharedConnection) {
172             // for now, just set the delegate to null, it will be created later if needed
173             setDelegate(null);
174             isSharedConnection = false;
175         }
176 
177         // if this connection was closed during the transaction and there is still a delegate present close it
178         Connection delegate = getDelegateInternal();
179         if (_closed && delegate != null) {
180             try {
181                 setDelegate(null);
182 
183                 // don't actually close the connection if in a transaction
184                 if (!delegate.isClosed()) {
185                     // don't use super.close() because it calls passivate() which marks the
186                     // the connection as cloased without returning it to the pool
187                     delegate.close();
188                 }
189             } catch (SQLException ignored) {
190                 // not a whole lot we can do here as connection is closed
191                 // and this is a transaction classback so there is no
192                 // way to report the error
193             } finally {
194                 _closed = true;
195             }
196         }
197 
198     }
199 
200     //
201     // The following methods can't be used while enlisted in a transaction
202     //
203 
204     public void setAutoCommit(boolean autoCommit) throws SQLException {
205         if (transactionContext != null) {
206             throw new SQLException("Auto-commit can not be set while enrolled in a transaction");
207         }
208         super.setAutoCommit(autoCommit);
209     }
210 
211 
212     public void commit() throws SQLException {
213         if (transactionContext != null) {
214             throw new SQLException("Commit can not be set while enrolled in a transaction");
215         }
216         super.commit();
217     }
218 
219     public void rollback() throws SQLException {
220         if (transactionContext != null) {
221             throw new SQLException("Commit can not be set while enrolled in a transaction");
222         }
223         super.rollback();
224     }
225 
226 
227     public void setReadOnly(boolean readOnly) throws SQLException {
228         if (transactionContext != null) {
229             throw new SQLException("Read-only can not be set while enrolled in a transaction");
230         }
231         super.setReadOnly(readOnly);
232     }
233 
234     //
235     // Methods for accessing the delegate connection
236     //
237 
238     /**
239      * If false, getDelegate() and getInnermostDelegate() will return null.
240      * @return if false, getDelegate() and getInnermostDelegate() will return null
241      */
242     public boolean isAccessToUnderlyingConnectionAllowed() {
243         return accessToUnderlyingConnectionAllowed;
244     }
245 
246     public Connection getDelegate() {
247         if (isAccessToUnderlyingConnectionAllowed()) {
248             return getDelegateInternal();
249         } else {
250             return null;
251         }
252     }
253 
254     public Connection getInnermostDelegate() {
255         if (isAccessToUnderlyingConnectionAllowed()) {
256             return super.getInnermostDelegateInternal();
257         } else {
258             return null;
259         }
260     }
261 }