    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 can not 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     * @return The transaction context.
    152     * @since 2.6.0
    153     */
    154    public TransactionContext getTransactionContext() {
    155        return transactionContext;
    156    }
    157
    158    /**
    159     * @return The transaction registry.
    160     * @since 2.6.0
    161     */
    162    public TransactionRegistry getTransactionRegistry() {
    163        return transactionRegistry;
    164    }
    165
    166    /**
    167     * If false, getDelegate() and getInnermostDelegate() will return null.
    168     *
    169     * @return if false, getDelegate() and getInnermostDelegate() will return null
    170     */
    171    public boolean isAccessToUnderlyingConnectionAllowed() {
    172        return accessToUnderlyingConnectionAllowed;
    173    }
    174
    175    @Override
    176    public void rollback() throws SQLException {
    177        if (transactionContext != null) {
    178            throw new SQLException("Commit can not be set while enrolled in a transaction");
    179        }
    180        super.rollback();
    181    }
    182
    183    @Override
    184    public void setAutoCommit(final boolean autoCommit) throws SQLException {
    185        if (transactionContext != null) {
    186            throw new SQLException("Auto-commit can not be set while enrolled in a transaction");
    187        }
    188        super.setAutoCommit(autoCommit);
    189    }
    190
    191    @Override
    192    public void setReadOnly(final boolean readOnly) throws SQLException {
    193        if (transactionContext != null) {
    194            throw new SQLException("Read-only can not be set while enrolled in a transaction");
    195        }
    196        super.setReadOnly(readOnly);
    197    }
    198
    199    /**
    200     * Completes the transaction.
    201     */
    202    protected void transactionComplete() {
    203        lock.lock();
    204        try {
    205            transactionContext.completeTransaction();
    206        } finally {
    207            lock.unlock();
    208        }
    209
    210        // If we were using a shared connection, clear the reference now that
    211        // the transaction has completed
    212        if (isSharedConnection) {
    213            setDelegate(null);
    214            isSharedConnection = false;
    215        }
    216
    217        // autoCommit may have been changed directly on the underlying connection
    218        clearCachedState();
    219
    220        // If this connection was closed during the transaction and there is
    221        // still a delegate present close it
    222        final Connection delegate = getDelegateInternal();
    223        if (isClosedInternal() && delegate != null) {
    224            try {
    225                setDelegate(null);
    226
    227                if (!delegate.isClosed()) {
    228                    delegate.close();
    229                }
    230            } catch (final SQLException ignored) {
    231                // Not a whole lot we can do here as connection is closed
    232                // and this is a transaction callback so there is no
    233                // way to report the error.
    234            }
    235        }
    236    }
    237
    238    private void updateTransactionStatus() throws SQLException {
    239        // if there is an active transaction context, assure the transaction context hasn't changed
    240        if (transactionContext != null && !transactionContext.isTransactionComplete()) {
    241            if (transactionContext.isActive()) {
    242                if (transactionContext != transactionRegistry.getActiveTransactionContext()) {
    243                    throw new SQLException("Connection can not be used while enlisted in another transaction");
    244                }
    245                return;
    246            }
    247            // transaction should have been cleared up by TransactionContextListener, but in
    248            // rare cases another lister could have registered which uses the connection before
    249            // our listener is called. In that rare case, trigger the transaction complete call now
    250            transactionComplete();
    251        }
    252
    253        // the existing transaction context ended (or we didn't have one), get the active transaction context
    254        transactionContext = transactionRegistry.getActiveTransactionContext();
    255
    256        // if there is an active transaction context, and it already has a shared connection, use it
    257        if (transactionContext != null && transactionContext.getSharedConnection() != null) {
    258            // A connection for the connection factory has already been enrolled
    259            // in the transaction, replace our delegate with the enrolled connection
    260
    261            // return current connection to the pool
    262            @SuppressWarnings("resource")
    263            final C connection = getDelegateInternal();
    264            setDelegate(null);
    265            if (connection != null && transactionContext.getSharedConnection() != connection) {
    266                try {
    267                    pool.returnObject(connection);
    268                } catch (final Exception e) {
    269                    // whatever... try to invalidate the connection
    270                    try {
    271                        pool.invalidateObject(connection);
    272                    } catch (final Exception ignored) {
    273                        // no big deal
    274                    }
    275                }
    276            }
    277
    278            // add a listener to the transaction context
    279            transactionContext.addTransactionContextListener(new CompletionListener());
    280
    281            // Set our delegate to the shared connection. Note that this will
    282            // always be of type C since it has been shared by another
    283            // connection from the same pool.
    284            @SuppressWarnings("unchecked")
    285            final C shared = (C) transactionContext.getSharedConnection();
    286            setDelegate(shared);
    287
    288            // remember that we are using a shared connection, so it can be cleared after the
    289            // transaction completes
    290            isSharedConnection = true;
    291        } else {
    292            C connection = getDelegateInternal();
    293            // if our delegate is null, create one
    294            if (connection == null) {
    295                try {
    296                    // borrow a new connection from the pool
    297                    connection = pool.borrowObject();
    298                    setDelegate(connection);
    299                } catch (final Exception e) {
    300                    throw new SQLException("Unable to acquire a new connection from the pool", e);
    301                }
    302            }
    303
    304            // if we have a transaction, out delegate becomes the shared delegate
    305            if (transactionContext != null) {
    306                // add a listener to the transaction context
    307                transactionContext.addTransactionContextListener(new CompletionListener());
    308
    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}