    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 *      http://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        @Override
    055        public void afterCompletion(final TransactionContext completedContext, final boolean committed) {
    056            if (completedContext == transactionContext) {
    057                transactionComplete();
    058            }
    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    private final Lock lock;
    068
    069    /**
    070     * Constructs a new instance responsible for managing a database connection in a transactional environment.
    071     *
    072     * @param pool
    073     *            The connection pool.
    074     * @param transactionRegistry
    075     *            The transaction registry.
    076     * @param accessToUnderlyingConnectionAllowed
    077     *            Whether or not to allow access to the underlying Connection.
    078     * @throws SQLException
    079     *             Thrown when there is problem managing transactions.
    080     */
    081    public ManagedConnection(final ObjectPool<C> pool, final TransactionRegistry transactionRegistry,
    082            final boolean accessToUnderlyingConnectionAllowed) throws SQLException {
    083        super(null);
    084        this.pool = pool;
    085        this.transactionRegistry = transactionRegistry;
    086        this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed;
    087        this.lock = new ReentrantLock();
    088        updateTransactionStatus();
    089    }
    090
    091    @Override
    092    protected void checkOpen() throws SQLException {
    093        super.checkOpen();
    094        updateTransactionStatus();
    095    }
    096
    097    @Override
    098    public void close() throws SQLException {
    099        if (!isClosedInternal()) {
    100            // Don't actually close the connection if in a transaction. The
    101            // connection will be closed by the transactionComplete method.
    102            //
    103            // DBCP-484 we need to make sure setClosedInternal(true) being
    104            // invoked if transactionContext is not null as this value will
    105            // be modified by the transactionComplete method which could run
    106            // in the different thread with the transaction calling back.
    107            lock.lock();
    108            try {
    109                if (transactionContext == null || transactionContext.isTransactionComplete()) {
    110                    super.close();
    111                }
    112            } finally {
    113                try {
    114                    setClosedInternal(true);
    115                } finally {
    116                    lock.unlock();
    117                }
    118            }
    119        }
    120    }
    121
    122    @Override
    123    public void commit() throws SQLException {
    124        if (transactionContext != null) {
    125            throw new SQLException("Commit cannot be set while enrolled in a transaction");
    126        }
    127        super.commit();
    128    }
    129
    130    @Override
    131    public C getDelegate() {
    132        if (isAccessToUnderlyingConnectionAllowed()) {
    133            return getDelegateInternal();
    134        }
    135        return null;
    136    }
    137
    138    //
    139    // The following methods can't be used while enlisted in a transaction
    140    //
    141
    142    @Override
    143    public Connection getInnermostDelegate() {
    144        if (isAccessToUnderlyingConnectionAllowed()) {
    145            return super.getInnermostDelegateInternal();
    146        }
    147        return null;
    148    }
    149
    150    /**
    151     * Gets the transaction context.
    152     *
    153     * @return The transaction context.
    154     * @since 2.6.0
    155     */
    156    public TransactionContext getTransactionContext() {
    157        return transactionContext;
    158    }
    159
    160    /**
    161     * Gets the transaction registry.
    162     *
    163     * @return The transaction registry.
    164     * @since 2.6.0
    165     */
    166    public TransactionRegistry getTransactionRegistry() {
    167        return transactionRegistry;
    168    }
    169
    170    /**
    171     * If false, getDelegate() and getInnermostDelegate() will return null.
    172     *
    173     * @return if false, getDelegate() and getInnermostDelegate() will return null
    174     */
    175    public boolean isAccessToUnderlyingConnectionAllowed() {
    176        return accessToUnderlyingConnectionAllowed;
    177    }
    178
    179    @Override
    180    public void rollback() throws SQLException {
    181        if (transactionContext != null) {
    182            throw new SQLException("Commit cannot be set while enrolled in a transaction");
    183        }
    184        super.rollback();
    185    }
    186
    187    @Override
    188    public void setAutoCommit(final boolean autoCommit) throws SQLException {
    189        if (transactionContext != null) {
    190            throw new SQLException("Auto-commit cannot be set while enrolled in a transaction");
    191        }
    192        super.setAutoCommit(autoCommit);
    193    }
    194
    195    @Override
    196    public void setReadOnly(final boolean readOnly) throws SQLException {
    197        if (transactionContext != null) {
    198            throw new SQLException("Read-only cannot be set while enrolled in a transaction");
    199        }
    200        super.setReadOnly(readOnly);
    201    }
    202
    203    /**
    204     * Completes the transaction.
    205     */
    206    protected void transactionComplete() {
    207        lock.lock();
    208        try {
    209            transactionContext.completeTransaction();
    210        } finally {
    211            lock.unlock();
    212        }
    213
    214        // If we were using a shared connection, clear the reference now that
    215        // the transaction has completed
    216        if (isSharedConnection) {
    217            setDelegate(null);
    218            isSharedConnection = false;
    219        }
    220
    221        // autoCommit may have been changed directly on the underlying connection
    222        clearCachedState();
    223
    224        // If this connection was closed during the transaction and there is
    225        // still a delegate present close it
    226        final Connection delegate = getDelegateInternal();
    227        if (isClosedInternal() && delegate != null) {
    228            try {
    229                setDelegate(null);
    230
    231                if (!delegate.isClosed()) {
    232                    delegate.close();
    233                }
    234            } catch (final SQLException ignored) {
    235                // Not a whole lot we can do here as connection is closed
    236                // and this is a transaction callback so there is no
    237                // way to report the error.
    238            }
    239        }
    240    }
    241
    242    private void updateTransactionStatus() throws SQLException {
    243        // if there is an active transaction context, assure the transaction context hasn't changed
    244        if (transactionContext != null && !transactionContext.isTransactionComplete()) {
    245            if (transactionContext.isActive()) {
    246                if (transactionContext != transactionRegistry.getActiveTransactionContext()) {
    247                    throw new SQLException("Connection cannot be used while enlisted in another transaction");
    248                }
    249                return;
    250            }
    251            // transaction should have been cleared up by TransactionContextListener, but in
    252            // rare cases another lister could have registered which uses the connection before
    253            // our listener is called. In that rare case, trigger the transaction complete call now
    254            transactionComplete();
    255        }
    256
    257        // the existing transaction context ended (or we didn't have one), get the active transaction context
    258        transactionContext = transactionRegistry.getActiveTransactionContext();
    259
    260        // if there is an active transaction context, and it already has a shared connection, use it
    261        if (transactionContext != null && transactionContext.getSharedConnection() != null) {
    262            // A connection for the connection factory has already been enrolled
    263            // in the transaction, replace our delegate with the enrolled connection
    264
    265            // return current connection to the pool
    266            @SuppressWarnings("resource")
    267            final C connection = getDelegateInternal();
    268            setDelegate(null);
    269            if (connection != null && transactionContext.getSharedConnection() != connection) {
    270                try {
    271                    pool.returnObject(connection);
    272                } catch (final Exception e) {
    273                    // whatever... try to invalidate the connection
    274                    try {
    275                        pool.invalidateObject(connection);
    276                    } catch (final Exception ignored) {
    277                        // no big deal
    278                    }
    279                }
    280            }
    281
    282            // add a listener to the transaction context
    283            transactionContext.addTransactionContextListener(new CompletionListener());
    284
    285            // Set our delegate to the shared connection. Note that this will
    286            // always be of type C since it has been shared by another
    287            // connection from the same pool.
    288            @SuppressWarnings("unchecked")
    289            final C shared = (C) transactionContext.getSharedConnection();
    290            setDelegate(shared);
    291
    292            // remember that we are using a shared connection, so it can be cleared after the
    293            // transaction completes
    294            isSharedConnection = true;
    295        } else {
    296            C connection = getDelegateInternal();
    297            // if our delegate is null, create one
    298            if (connection == null) {
    299                try {
    300                    // borrow a new connection from the pool
    301                    connection = pool.borrowObject();
    302                    setDelegate(connection);
    303                } catch (final Exception e) {
    304                    throw new SQLException("Unable to acquire a new connection from the pool", e);
    305                }
    306            }
    307
    308            // if we have a transaction, out delegate becomes the shared delegate
    309            if (transactionContext != null) {
    310                // add a listener to the transaction context
    311                transactionContext.addTransactionContextListener(new CompletionListener());
    312
    313                // register our connection as the shared connection
    314                try {
    315                    transactionContext.setSharedConnection(connection);
    316                } catch (final SQLException e) {
    317                    // transaction is hosed
    318                    transactionContext = null;
    319                    try {
    320                        pool.invalidateObject(connection);
    321                    } catch (final Exception ignored) {
    322                        // we are try but no luck
    323                    }
    324                    throw e;
    325                }
    326            }
    327        }
    328        // autoCommit may have been changed directly on the underlying
    329        // connection
    330        clearCachedState();
    331    }
    332}