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