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}