    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}