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