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            // Don't actually close the connection if in a transaction. The
178            // connection will be closed by the transactionComplete method.
179            //
180            // DBCP-484 we need to make sure setClosedInternal(true) being
181            // invoked if transactionContext is not null as this value will
182            // be modified by the transactionComplete method which could run
183            // in the different thread with the transaction calling back.
184            lock.lock();
185            try {
186                if (transactionContext == null || transactionContext.isTransactionComplete()) {
187                    super.close();
188                }
189            } finally {
190                try {
191                    setClosedInternal(true);
192                } finally {
193                    lock.unlock();
194                }
195            }
196        }
197    }
198
199    /**
200     * Delegates to {@link ManagedConnection#transactionComplete()} for transaction completion events.
201     *
202     * @since 2.0
203     */
204    protected class CompletionListener implements TransactionContextListener {
205        @Override
206        public void afterCompletion(final TransactionContext completedContext, final boolean commited) {
207            if (completedContext == transactionContext) {
208                transactionComplete();
209            }
210        }
211    }
212
213    protected void transactionComplete() {
214        lock.lock();
215        try {
216            transactionContext.completeTransaction();
217        } finally {
218            lock.unlock();
219        }
220
221        // If we were using a shared connection, clear the reference now that
222        // the transaction has completed
223        if (isSharedConnection) {
224            setDelegate(null);
225            isSharedConnection = false;
226        }
227
228        // If this connection was closed during the transaction and there is
229        // still a delegate present close it
230        final Connection delegate = getDelegateInternal();
231        if (isClosedInternal() && delegate != null) {
232            try {
233                setDelegate(null);
234
235                if (!delegate.isClosed()) {
236                    delegate.close();
237                }
238            } catch (final SQLException ignored) {
239                // Not a whole lot we can do here as connection is closed
240                // and this is a transaction callback so there is no
241                // way to report the error.
242            }
243        }
244    }
245
246    //
247    // The following methods can't be used while enlisted in a transaction
248    //
249
250    @Override
251    public void setAutoCommit(final boolean autoCommit) throws SQLException {
252        if (transactionContext != null) {
253            throw new SQLException("Auto-commit can not be set while enrolled in a transaction");
254        }
255        super.setAutoCommit(autoCommit);
256    }
257
258    @Override
259    public void commit() throws SQLException {
260        if (transactionContext != null) {
261            throw new SQLException("Commit can not be set while enrolled in a transaction");
262        }
263        super.commit();
264    }
265
266    @Override
267    public void rollback() throws SQLException {
268        if (transactionContext != null) {
269            throw new SQLException("Commit can not be set while enrolled in a transaction");
270        }
271        super.rollback();
272    }
273
274    @Override
275    public void setReadOnly(final boolean readOnly) throws SQLException {
276        if (transactionContext != null) {
277            throw new SQLException("Read-only can not be set while enrolled in a transaction");
278        }
279        super.setReadOnly(readOnly);
280    }
281
282    //
283    // Methods for accessing the delegate connection
284    //
285
286    /**
287     * If false, getDelegate() and getInnermostDelegate() will return null.
288     *
289     * @return if false, getDelegate() and getInnermostDelegate() will return null
290     */
291    public boolean isAccessToUnderlyingConnectionAllowed() {
292        return accessToUnderlyingConnectionAllowed;
293    }
294
295    @Override
296    public C getDelegate() {
297        if (isAccessToUnderlyingConnectionAllowed()) {
298            return getDelegateInternal();
299        }
300        return null;
301    }
302
303    @Override
304    public Connection getInnermostDelegate() {
305        if (isAccessToUnderlyingConnectionAllowed()) {
306            return super.getInnermostDelegateInternal();
307        }
308        return null;
309    }
310}