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; 025 026/** 027 * ManagedConnection is responsible for managing a database connection in a transactional environment 028 * (typically called "Container Managed"). A managed connection operates like any other connection 029 * when no global transaction (a.k.a. XA transaction or JTA Transaction) is in progress. When a 030 * global transaction is active a single physical connection to the database is used by all 031 * ManagedConnections accessed in the scope of the transaction. Connection sharing means that all 032 * data access during a transaction has a consistent view of the database. When the global transaction 033 * is committed or rolled back the enlisted connections are committed or rolled back. Typically upon 034 * transaction completion, a connection returns to the auto commit setting in effect before being 035 * enlisted in the transaction, but some vendors do not properly implement this. 036 * 037 * When enlisted in a transaction the setAutoCommit(), commit(), rollback(), and setReadOnly() methods 038 * throw a SQLException. This is necessary to assure that the transaction completes as a single unit. 039 * 040 * @param <C> the Connection type 041 * 042 * @author Dain Sundstrom 043 * @since 2.0 044 */ 045public class ManagedConnection<C extends Connection> extends DelegatingConnection<C> { 046 private final ObjectPool<C> pool; 047 private final TransactionRegistry transactionRegistry; 048 private final boolean accessToUnderlyingConnectionAllowed; 049 private TransactionContext transactionContext; 050 private boolean isSharedConnection; 051 052 public ManagedConnection(ObjectPool<C> pool, 053 TransactionRegistry transactionRegistry, 054 boolean accessToUnderlyingConnectionAllowed) throws SQLException { 055 super(null); 056 this.pool = pool; 057 this.transactionRegistry = transactionRegistry; 058 this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed; 059 updateTransactionStatus(); 060 } 061 062 @Override 063 protected void checkOpen() throws SQLException { 064 super.checkOpen(); 065 updateTransactionStatus(); 066 } 067 068 private void updateTransactionStatus() throws SQLException { 069 // if there is a is an active transaction context, assure the transaction context hasn't changed 070 if (transactionContext != null) { 071 if (transactionContext.isActive()) { 072 if (transactionContext != transactionRegistry.getActiveTransactionContext()) { 073 throw new SQLException("Connection can not be used while enlisted in another transaction"); 074 } 075 return; 076 } 077 // transaction should have been cleared up by TransactionContextListener, but in 078 // rare cases another lister could have registered which uses the connection before 079 // our listener is called. In that rare case, trigger the transaction complete call now 080 transactionComplete(); 081 } 082 083 // the existing transaction context ended (or we didn't have one), get the active transaction context 084 transactionContext = transactionRegistry.getActiveTransactionContext(); 085 086 // if there is an active transaction context and it already has a shared connection, use it 087 if (transactionContext != null && transactionContext.getSharedConnection() != null) { 088 // A connection for the connection factory has already been enrolled 089 // in the transaction, replace our delegate with the enrolled connection 090 091 // return current connection to the pool 092 C connection = getDelegateInternal(); 093 setDelegate(null); 094 if (connection != null) { 095 try { 096 pool.returnObject(connection); 097 } catch (Exception ignored) { 098 // whatever... try to invalidate the connection 099 try { 100 pool.invalidateObject(connection); 101 } catch (Exception ignore) { 102 // no big deal 103 } 104 } 105 } 106 107 // add a listener to the transaction context 108 transactionContext.addTransactionContextListener(new CompletionListener()); 109 110 // Set our delegate to the shared connection. Note that this will 111 // always be of type C since it has been shared by another 112 // connection from the same pool. 113 @SuppressWarnings("unchecked") 114 C shared = (C) transactionContext.getSharedConnection(); 115 setDelegate(shared); 116 117 // remember that we are using a shared connection so it can be cleared after the 118 // transaction completes 119 isSharedConnection = true; 120 } else { 121 // if our delegate is null, create one 122 if (getDelegateInternal() == null) { 123 try { 124 // borrow a new connection from the pool 125 C connection = pool.borrowObject(); 126 setDelegate(connection); 127 } catch (Exception e) { 128 throw new SQLException("Unable to acquire a new connection from the pool", e); 129 } 130 } 131 132 // if we have a transaction, out delegate becomes the shared delegate 133 if (transactionContext != null) { 134 // add a listener to the transaction context 135 transactionContext.addTransactionContextListener(new CompletionListener()); 136 137 // register our connection as the shared connection 138 try { 139 transactionContext.setSharedConnection(getDelegateInternal()); 140 } catch (SQLException e) { 141 // transaction is hosed 142 transactionContext = null; 143 throw e; 144 } 145 } 146 } 147 // autoCommit may have been changed directly on the underlying 148 // connection 149 clearCachedState(); 150 } 151 152 @Override 153 public void close() throws SQLException { 154 if (!isClosedInternal()) { 155 try { 156 // Don't actually close the connection if in a transaction. The 157 // connection will be closed by the transactionComplete method. 158 if (transactionContext == null) { 159 super.close(); 160 } 161 } finally { 162 setClosedInternal(true); 163 } 164 } 165 } 166 167 /** 168 * Delegates to {@link ManagedConnection#transactionComplete()} 169 * for transaction completion events. 170 * @since 2.0 171 */ 172 protected class CompletionListener implements TransactionContextListener { 173 @Override 174 public void afterCompletion(TransactionContext completedContext, boolean commited) { 175 if (completedContext == transactionContext) { 176 transactionComplete(); 177 } 178 } 179 } 180 181 protected void transactionComplete() { 182 transactionContext = null; 183 184 // If we were using a shared connection, clear the reference now that 185 // the transaction has completed 186 if (isSharedConnection) { 187 setDelegate(null); 188 isSharedConnection = false; 189 } 190 191 // If this connection was closed during the transaction and there is 192 // still a delegate present close it 193 Connection delegate = getDelegateInternal(); 194 if (isClosedInternal() && delegate != null) { 195 try { 196 setDelegate(null); 197 198 if (!delegate.isClosed()) { 199 super.close(); 200 } 201 } catch (SQLException ignored) { 202 // Not a whole lot we can do here as connection is closed 203 // and this is a transaction callback so there is no 204 // way to report the error. 205 } 206 } 207 } 208 209 // 210 // The following methods can't be used while enlisted in a transaction 211 // 212 213 @Override 214 public void setAutoCommit(boolean autoCommit) throws SQLException { 215 if (transactionContext != null) { 216 throw new SQLException("Auto-commit can not be set while enrolled in a transaction"); 217 } 218 super.setAutoCommit(autoCommit); 219 } 220 221 222 @Override 223 public void commit() throws SQLException { 224 if (transactionContext != null) { 225 throw new SQLException("Commit can not be set while enrolled in a transaction"); 226 } 227 super.commit(); 228 } 229 230 @Override 231 public void rollback() throws SQLException { 232 if (transactionContext != null) { 233 throw new SQLException("Commit can not be set while enrolled in a transaction"); 234 } 235 super.rollback(); 236 } 237 238 239 @Override 240 public void setReadOnly(boolean readOnly) throws SQLException { 241 if (transactionContext != null) { 242 throw new SQLException("Read-only can not be set while enrolled in a transaction"); 243 } 244 super.setReadOnly(readOnly); 245 } 246 247 // 248 // Methods for accessing the delegate connection 249 // 250 251 /** 252 * If false, getDelegate() and getInnermostDelegate() will return null. 253 * @return if false, getDelegate() and getInnermostDelegate() will return null 254 */ 255 public boolean isAccessToUnderlyingConnectionAllowed() { 256 return accessToUnderlyingConnectionAllowed; 257 } 258 259 @Override 260 public C getDelegate() { 261 if (isAccessToUnderlyingConnectionAllowed()) { 262 return getDelegateInternal(); 263 } 264 return null; 265 } 266 267 @Override 268 public Connection getInnermostDelegate() { 269 if (isAccessToUnderlyingConnectionAllowed()) { 270 return super.getInnermostDelegateInternal(); 271 } 272 return null; 273 } 274}