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    /**
050     * Delegates to {@link ManagedConnection#transactionComplete()} for transaction completion events.
051     *
052     * @since 2.0
053     */
054    protected class CompletionListener implements TransactionContextListener {
055        @Override
056        public void afterCompletion(final TransactionContext completedContext, final boolean committed) {
057            if (completedContext == transactionContext) {
058                transactionComplete();
059            }
060        }
061    }
062    private final ObjectPool<C> pool;
063    private final TransactionRegistry transactionRegistry;
064    private final boolean accessToUnderlyingConnectionAllowed;
065    private TransactionContext transactionContext;
066    private boolean isSharedConnection;
067
068    private final Lock lock;
069
070    /**
071     * Constructs a new instance responsible for managing a database connection in a transactional environment.
072     *
073     * @param pool
074     *            The connection pool.
075     * @param transactionRegistry
076     *            The transaction registry.
077     * @param accessToUnderlyingConnectionAllowed
078     *            Whether or not to allow access to the underlying Connection.
079     * @throws SQLException
080     *             Thrown when there is problem managing transactions.
081     */
082    public ManagedConnection(final ObjectPool<C> pool, final TransactionRegistry transactionRegistry,
083            final boolean accessToUnderlyingConnectionAllowed) throws SQLException {
084        super(null);
085        this.pool = pool;
086        this.transactionRegistry = transactionRegistry;
087        this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed;
088        this.lock = new ReentrantLock();
089        updateTransactionStatus();
090    }
091
092    @Override
093    protected void checkOpen() throws SQLException {
094        super.checkOpen();
095        updateTransactionStatus();
096    }
097
098    @Override
099    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    protected void transactionComplete() {
205        lock.lock();
206        try {
207            transactionContext.completeTransaction();
208        } finally {
209            lock.unlock();
210        }
211
212        // If we were using a shared connection, clear the reference now that
213        // the transaction has completed
214        if (isSharedConnection) {
215            setDelegate(null);
216            isSharedConnection = false;
217        }
218
219        // If this connection was closed during the transaction and there is
220        // still a delegate present close it
221        final Connection delegate = getDelegateInternal();
222        if (isClosedInternal() && delegate != null) {
223            try {
224                setDelegate(null);
225
226                if (!delegate.isClosed()) {
227                    delegate.close();
228                }
229            } catch (final SQLException ignored) {
230                // Not a whole lot we can do here as connection is closed
231                // and this is a transaction callback so there is no
232                // way to report the error.
233            }
234        }
235    }
236
237    private void updateTransactionStatus() throws SQLException {
238        // if there is a is an active transaction context, assure the transaction context hasn't changed
239        if (transactionContext != null && !transactionContext.isTransactionComplete()) {
240            if (transactionContext.isActive()) {
241                if (transactionContext != transactionRegistry.getActiveTransactionContext()) {
242                    throw new SQLException("Connection can not be used while enlisted in another transaction");
243                }
244                return;
245            }
246            // transaction should have been cleared up by TransactionContextListener, but in
247            // rare cases another lister could have registered which uses the connection before
248            // our listener is called. In that rare case, trigger the transaction complete call now
249            transactionComplete();
250        }
251
252        // the existing transaction context ended (or we didn't have one), get the active transaction context
253        transactionContext = transactionRegistry.getActiveTransactionContext();
254
255        // if there is an active transaction context and it already has a shared connection, use it
256        if (transactionContext != null && transactionContext.getSharedConnection() != null) {
257            // A connection for the connection factory has already been enrolled
258            // in the transaction, replace our delegate with the enrolled connection
259
260            // return current connection to the pool
261            @SuppressWarnings("resource")
262            final C connection = getDelegateInternal();
263            setDelegate(null);
264            if (connection != null && transactionContext.getSharedConnection() != connection) {
265                try {
266                    pool.returnObject(connection);
267                } catch (final Exception ignored) {
268                    // whatever... try to invalidate the connection
269                    try {
270                        pool.invalidateObject(connection);
271                    } catch (final Exception ignore) {
272                        // no big deal
273                    }
274                }
275            }
276
277            // add a listener to the transaction context
278            transactionContext.addTransactionContextListener(new CompletionListener());
279
280            // Set our delegate to the shared connection. Note that this will
281            // always be of type C since it has been shared by another
282            // connection from the same pool.
283            @SuppressWarnings("unchecked")
284            final C shared = (C) transactionContext.getSharedConnection();
285            setDelegate(shared);
286
287            // remember that we are using a shared connection so it can be cleared after the
288            // transaction completes
289            isSharedConnection = true;
290        } else {
291            C connection = getDelegateInternal();
292            // if our delegate is null, create one
293            if (connection == null) {
294                try {
295                    // borrow a new connection from the pool
296                    connection = pool.borrowObject();
297                    setDelegate(connection);
298                } catch (final Exception e) {
299                    throw new SQLException("Unable to acquire a new connection from the pool", e);
300                }
301            }
302
303            // if we have a transaction, out delegate becomes the shared delegate
304            if (transactionContext != null) {
305                // add a listener to the transaction context
306                transactionContext.addTransactionContextListener(new CompletionListener());
307
308                // register our connection as the shared connection
309                try {
310                    transactionContext.setSharedConnection(connection);
311                } catch (final SQLException e) {
312                    // transaction is hosed
313                    transactionContext = null;
314                    try {
315                        pool.invalidateObject(connection);
316                    } catch (final Exception e1) {
317                        // we are try but no luck
318                    }
319                    throw e;
320                }
321            }
322        }
323        // autoCommit may have been changed directly on the underlying
324        // connection
325        clearCachedState();
326    }
327}