001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.dbcp2.managed;
018
019import java.sql.Connection;
020import java.sql.SQLException;
021import java.util.concurrent.locks.Lock;
022import java.util.concurrent.locks.ReentrantLock;
023
024import org.apache.commons.dbcp2.DelegatingConnection;
025import org.apache.commons.pool2.ObjectPool;
026
027/**
028 * ManagedConnection is responsible for managing a database connection in a transactional environment (typically called
029 * "Container Managed"). A managed connection operates like any other connection when no global transaction (a.k.a. XA
030 * transaction or JTA Transaction) is in progress. When a global transaction is active a single physical connection to
031 * the database is used by all ManagedConnections accessed in the scope of the transaction. Connection sharing means
032 * that all data access during a transaction has a consistent view of the database. When the global transaction is
033 * committed or rolled back the enlisted connections are committed or rolled back. Typically, upon transaction
034 * completion, a connection returns to the auto commit setting in effect before being enlisted in the transaction, but
035 * some vendors do not properly implement this.
036 * <p>
037 * When enlisted in a transaction the setAutoCommit(), commit(), rollback(), and setReadOnly() methods throw a
038 * SQLException. This is necessary to assure that the transaction completes as a single unit.
039 * </p>
040 *
041 * @param <C>
042 *            the Connection type
043 *
044 * @since 2.0
045 */
046public class ManagedConnection<C extends Connection> extends DelegatingConnection<C> {
047
048    /**
049     * Delegates to {@link ManagedConnection#transactionComplete()} for transaction completion events.
050     *
051     * @since 2.0
052     */
053    protected class CompletionListener implements TransactionContextListener {
054
055        /**
056         * Constructs a new instance.
057         */
058        public CompletionListener() {
059            // empty
060        }
061
062        @Override
063        public void afterCompletion(final TransactionContext completedContext, final boolean committed) {
064            if (completedContext == transactionContext) {
065                transactionComplete();
066            }
067        }
068    }
069
070    private final ObjectPool<C> pool;
071    private final TransactionRegistry transactionRegistry;
072    private final boolean accessToUnderlyingConnectionAllowed;
073    private TransactionContext transactionContext;
074    private boolean isSharedConnection;
075    private final Lock lock;
076
077    /**
078     * Constructs a new instance responsible for managing a database connection in a transactional environment.
079     *
080     * @param pool
081     *            The connection pool.
082     * @param transactionRegistry
083     *            The transaction registry.
084     * @param accessToUnderlyingConnectionAllowed
085     *            Whether or not to allow access to the underlying Connection.
086     * @throws SQLException
087     *             Thrown when there is problem managing transactions.
088     */
089    public ManagedConnection(final ObjectPool<C> pool, final TransactionRegistry transactionRegistry,
090            final boolean accessToUnderlyingConnectionAllowed) throws SQLException {
091        super(null);
092        this.pool = pool;
093        this.transactionRegistry = transactionRegistry;
094        this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed;
095        this.lock = new ReentrantLock();
096        updateTransactionStatus();
097    }
098
099    @Override
100    protected void checkOpen() throws SQLException {
101        super.checkOpen();
102        updateTransactionStatus();
103    }
104
105    @Override
106    public void close() throws SQLException {
107        if (!isClosedInternal()) {
108            // Don't actually close the connection if in a transaction. The
109            // connection will be closed by the transactionComplete method.
110            //
111            // DBCP-484 we need to make sure setClosedInternal(true) being
112            // invoked if transactionContext is not null as this value will
113            // be modified by the transactionComplete method which could run
114            // in the different thread with the transaction calling back.
115            lock.lock();
116            try {
117                if (transactionContext == null || transactionContext.isTransactionComplete()) {
118                    super.close();
119                }
120            } finally {
121                try {
122                    setClosedInternal(true);
123                } finally {
124                    lock.unlock();
125                }
126            }
127        }
128    }
129
130    @Override
131    public void commit() throws SQLException {
132        if (transactionContext != null) {
133            throw new SQLException("Commit cannot be set while enrolled in a transaction");
134        }
135        super.commit();
136    }
137
138    @Override
139    public C getDelegate() {
140        if (isAccessToUnderlyingConnectionAllowed()) {
141            return getDelegateInternal();
142        }
143        return null;
144    }
145
146    //
147    // The following methods can't be used while enlisted in a transaction
148    //
149
150    @Override
151    public Connection getInnermostDelegate() {
152        if (isAccessToUnderlyingConnectionAllowed()) {
153            return super.getInnermostDelegateInternal();
154        }
155        return null;
156    }
157
158    /**
159     * Gets the transaction context.
160     *
161     * @return The transaction context.
162     * @since 2.6.0
163     */
164    public TransactionContext getTransactionContext() {
165        return transactionContext;
166    }
167
168    /**
169     * Gets the transaction registry.
170     *
171     * @return The transaction registry.
172     * @since 2.6.0
173     */
174    public TransactionRegistry getTransactionRegistry() {
175        return transactionRegistry;
176    }
177
178    /**
179     * If false, getDelegate() and getInnermostDelegate() will return null.
180     *
181     * @return if false, getDelegate() and getInnermostDelegate() will return null
182     */
183    public boolean isAccessToUnderlyingConnectionAllowed() {
184        return accessToUnderlyingConnectionAllowed;
185    }
186
187    @Override
188    public void rollback() throws SQLException {
189        if (transactionContext != null) {
190            throw new SQLException("Commit cannot be set while enrolled in a transaction");
191        }
192        super.rollback();
193    }
194
195    @Override
196    public void setAutoCommit(final boolean autoCommit) throws SQLException {
197        if (transactionContext != null) {
198            throw new SQLException("Auto-commit cannot be set while enrolled in a transaction");
199        }
200        super.setAutoCommit(autoCommit);
201    }
202
203    @Override
204    public void setReadOnly(final boolean readOnly) throws SQLException {
205        if (transactionContext != null) {
206            throw new SQLException("Read-only cannot be set while enrolled in a transaction");
207        }
208        super.setReadOnly(readOnly);
209    }
210
211    /**
212     * Completes the transaction.
213     */
214    protected void transactionComplete() {
215        lock.lock();
216        try {
217            transactionContext.completeTransaction();
218            // If we were using a shared connection, clear the reference now that
219            // the transaction has completed
220            if (isSharedConnection) {
221                setDelegate(null);
222                isSharedConnection = false;
223            }
224        } finally {
225            lock.unlock();
226        }
227        // autoCommit may have been changed directly on the underlying connection
228        clearCachedState();
229        // If this connection was closed during the transaction and there is
230        // still a delegate present close it
231        final Connection delegate = getDelegateInternal();
232        if (isClosedInternal() && delegate != null) {
233            try {
234                setDelegate(null);
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    private void updateTransactionStatus() throws SQLException {
247        // if there is an active transaction context, assure the transaction context hasn't changed
248        if (transactionContext != null && !transactionContext.isTransactionComplete()) {
249            if (transactionContext.isActive()) {
250                if (transactionContext != transactionRegistry.getActiveTransactionContext()) {
251                    throw new SQLException("Connection cannot be used while enlisted in another transaction");
252                }
253                return;
254            }
255            // transaction should have been cleared up by TransactionContextListener, but in
256            // rare cases another lister could have registered which uses the connection before
257            // our listener is called. In that rare case, trigger the transaction complete call now
258            transactionComplete();
259        }
260        // the existing transaction context ended (or we didn't have one), get the active transaction context
261        transactionContext = transactionRegistry.getActiveTransactionContext();
262        // if there is an active transaction context, and it already has a shared connection, use it
263        if (transactionContext != null && transactionContext.getSharedConnection() != null) {
264            // A connection for the connection factory has already been enrolled
265            // in the transaction, replace our delegate with the enrolled connection
266            // return current connection to the pool
267            @SuppressWarnings("resource")
268            final C connection = getDelegateInternal();
269            setDelegate(null);
270            if (connection != null && transactionContext.getSharedConnection() != connection) {
271                try {
272                    pool.returnObject(connection);
273                } catch (final Exception e) {
274                    // whatever... try to invalidate the connection
275                    try {
276                        pool.invalidateObject(connection);
277                    } catch (final Exception ignored) {
278                        // no big deal
279                    }
280                }
281            }
282            // add a listener to the transaction context
283            transactionContext.addTransactionContextListener(new CompletionListener());
284            // Set our delegate to the shared connection. Note that this will
285            // always be of type C since it has been shared by another
286            // connection from the same pool.
287            @SuppressWarnings("unchecked")
288            final C shared = (C) transactionContext.getSharedConnection();
289            setDelegate(shared);
290            // remember that we are using a shared connection, so it can be cleared after the
291            // transaction completes
292            isSharedConnection = true;
293        } else {
294            C connection = getDelegateInternal();
295            // if our delegate is null, create one
296            if (connection == null) {
297                try {
298                    // borrow a new connection from the pool
299                    connection = pool.borrowObject();
300                    setDelegate(connection);
301                } catch (final Exception e) {
302                    throw new SQLException("Unable to acquire a new connection from the pool", e);
303                }
304            }
305            // if we have a transaction, out delegate becomes the shared delegate
306            if (transactionContext != null) {
307                // add a listener to the transaction context
308                transactionContext.addTransactionContextListener(new CompletionListener());
309                // register our connection as the shared connection
310                try {
311                    transactionContext.setSharedConnection(connection);
312                } catch (final SQLException e) {
313                    // transaction is hosed
314                    transactionContext = null;
315                    try {
316                        pool.invalidateObject(connection);
317                    } catch (final Exception ignored) {
318                        // we are try but no luck
319                    }
320                    throw e;
321                }
322            }
323        }
324        // autoCommit may have been changed directly on the underlying
325        // connection
326        clearCachedState();
327    }
328}