    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;
    025
    026/**
    027 * ManagedConnection is responsible for managing a database connection in a transactional environment
    028 * (typically called "Container Managed").  A managed connection operates like any other connection
    029 * when no global transaction (a.k.a. XA transaction or JTA Transaction) is in progress.  When a
    030 * global transaction is active a single physical connection to the database is used by all
    031 * ManagedConnections accessed in the scope of the transaction.  Connection sharing means that all
    032 * data access during a transaction has a consistent view of the database.  When the global transaction
    033 * is committed or rolled back the enlisted connections are committed or rolled back.  Typically upon
    034 * transaction completion, a connection returns to the auto commit setting in effect before being
    035 * enlisted in the transaction, but some vendors do not properly implement this.
    036 *
    037 * When enlisted in a transaction the setAutoCommit(), commit(), rollback(), and setReadOnly() methods
    038 * throw a SQLException.  This is necessary to assure that the transaction completes as a single unit.
    039 *
    040 * @param <C> the Connection type
    041 *
    042 * @author Dain Sundstrom
    043 * @since 2.0
    044 */
    045public class ManagedConnection<C extends Connection> extends DelegatingConnection<C> {
    046    private final ObjectPool<C> pool;
    047    private final TransactionRegistry transactionRegistry;
    048    private final boolean accessToUnderlyingConnectionAllowed;
    049    private TransactionContext transactionContext;
    050    private boolean isSharedConnection;
    051
    052    public ManagedConnection(final ObjectPool<C> pool,
    053            final TransactionRegistry transactionRegistry,
    054            final boolean accessToUnderlyingConnectionAllowed) throws SQLException {
    055        super(null);
    056        this.pool = pool;
    057        this.transactionRegistry = transactionRegistry;
    058        this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed;
    059        updateTransactionStatus();
    060    }
    061
    062    @Override
    063    protected void checkOpen() throws SQLException {
    064        super.checkOpen();
    065        updateTransactionStatus();
    066    }
    067
    068    private void updateTransactionStatus() throws SQLException {
    069        // if there is a is an active transaction context, assure the transaction context hasn't changed
    070        if (transactionContext != null) {
    071            if (transactionContext.isActive()) {
    072                if (transactionContext != transactionRegistry.getActiveTransactionContext()) {
    073                    throw new SQLException("Connection can not be used while enlisted in another transaction");
    074                }
    075                return;
    076            }
    077            // transaction should have been cleared up by TransactionContextListener, but in
    078            // rare cases another lister could have registered which uses the connection before
    079            // our listener is called.  In that rare case, trigger the transaction complete call now
    080            transactionComplete();
    081        }
    082
    083        // the existing transaction context ended (or we didn't have one), get the active transaction context
    084        transactionContext = transactionRegistry.getActiveTransactionContext();
    085
    086        // if there is an active transaction context and it already has a shared connection, use it
    087        if (transactionContext != null && transactionContext.getSharedConnection() != null) {
    088            // A connection for the connection factory has already been enrolled
    089            // in the transaction, replace our delegate with the enrolled connection
    090
    091            // return current connection to the pool
    092            final C connection = getDelegateInternal();
    093            setDelegate(null);
    094            if (connection != null) {
    095                try {
    096                    pool.returnObject(connection);
    097                } catch (final Exception ignored) {
    098                    // whatever... try to invalidate the connection
    099                    try {
    100                        pool.invalidateObject(connection);
    101                    } catch (final Exception ignore) {
    102                        // no big deal
    103                    }
    104                }
    105            }
    106
    107            // add a listener to the transaction context
    108            transactionContext.addTransactionContextListener(new CompletionListener());
    109
    110            // Set our delegate to the shared connection. Note that this will
    111            // always be of type C since it has been shared by another
    112            // connection from the same pool.
    113            @SuppressWarnings("unchecked")
    114            final
    115            C shared = (C) transactionContext.getSharedConnection();
    116            setDelegate(shared);
    117
    118            // remember that we are using a shared connection so it can be cleared after the
    119            // transaction completes
    120            isSharedConnection = true;
    121        } else {
    122            C connection = getDelegateInternal();
    123            // if our delegate is null, create one
    124            if (connection == null) {
    125                try {
    126                    // borrow a new connection from the pool
    127                    connection = pool.borrowObject();
    128                    setDelegate(connection);
    129                } catch (final Exception e) {
    130                    throw new SQLException("Unable to acquire a new connection from the pool", e);
    131                }
    132            }
    133
    134            // if we have a transaction, out delegate becomes the shared delegate
    135            if (transactionContext != null) {
    136                // add a listener to the transaction context
    137                transactionContext.addTransactionContextListener(new CompletionListener());
    138
    139                // register our connection as the shared connection
    140                try {
    141                    transactionContext.setSharedConnection(connection);
    142                } catch (final SQLException e) {
    143                    // transaction is hosed
    144                    transactionContext = null;
    145                    try {
    146                        pool.invalidateObject(connection);
    147                    } catch (final Exception e1) {
    148                        // we are try but no luck
    149                    }
    150                    throw e;
    151                }
    152            }
    153        }
    154        // autoCommit may have been changed directly on the underlying
    155        // connection
    156        clearCachedState();
    157    }
    158
    159    @Override
    160    public void close() throws SQLException {
    161        if (!isClosedInternal()) {
    162            try {
    163                // Don't actually close the connection if in a transaction. The
    164                // connection will be closed by the transactionComplete method.
    165                if (transactionContext == null) {
    166                    super.close();
    167                }
    168            } finally {
    169                setClosedInternal(true);
    170            }
    171        }
    172    }
    173
    174    /**
    175     * Delegates to {@link ManagedConnection#transactionComplete()}
    176     * for transaction completion events.
    177     * @since 2.0
    178     */
    179    protected class CompletionListener implements TransactionContextListener {
    180        @Override
    181        public void afterCompletion(final TransactionContext completedContext, final boolean commited) {
    182            if (completedContext == transactionContext) {
    183                transactionComplete();
    184            }
    185        }
    186    }
    187
    188    protected void transactionComplete() {
    189        transactionContext = null;
    190
    191        // If we were using a shared connection, clear the reference now that
    192        // the transaction has completed
    193        if (isSharedConnection) {
    194            setDelegate(null);
    195            isSharedConnection = false;
    196        }
    197
    198        // If this connection was closed during the transaction and there is
    199        // still a delegate present close it
    200        final Connection delegate = getDelegateInternal();
    201        if (isClosedInternal() && delegate != null) {
    202            try {
    203                setDelegate(null);
    204
    205                if (!delegate.isClosed()) {
    206                    delegate.close();
    207                }
    208            } catch (final SQLException ignored) {
    209                // Not a whole lot we can do here as connection is closed
    210                // and this is a transaction callback so there is no
    211                // way to report the error.
    212            }
    213        }
    214    }
    215
    216    //
    217    // The following methods can't be used while enlisted in a transaction
    218    //
    219
    220    @Override
    221    public void setAutoCommit(final boolean autoCommit) throws SQLException {
    222        if (transactionContext != null) {
    223            throw new SQLException("Auto-commit can not be set while enrolled in a transaction");
    224        }
    225        super.setAutoCommit(autoCommit);
    226    }
    227
    228
    229    @Override
    230    public void commit() throws SQLException {
    231        if (transactionContext != null) {
    232            throw new SQLException("Commit can not be set while enrolled in a transaction");
    233        }
    234        super.commit();
    235    }
    236
    237    @Override
    238    public void rollback() throws SQLException {
    239        if (transactionContext != null) {
    240            throw new SQLException("Commit can not be set while enrolled in a transaction");
    241        }
    242        super.rollback();
    243    }
    244
    245
    246    @Override
    247    public void setReadOnly(final boolean readOnly) throws SQLException {
    248        if (transactionContext != null) {
    249            throw new SQLException("Read-only can not be set while enrolled in a transaction");
    250        }
    251        super.setReadOnly(readOnly);
    252    }
    253
    254    //
    255    // Methods for accessing the delegate connection
    256    //
    257
    258    /**
    259     * If false, getDelegate() and getInnermostDelegate() will return null.
    260     * @return if false, getDelegate() and getInnermostDelegate() will return null
    261     */
    262    public boolean isAccessToUnderlyingConnectionAllowed() {
    263        return accessToUnderlyingConnectionAllowed;
    264    }
    265
    266    @Override
    267    public C getDelegate() {
    268        if (isAccessToUnderlyingConnectionAllowed()) {
    269            return getDelegateInternal();
    270        }
    271        return null;
    272    }
    273
    274    @Override
    275    public Connection getInnermostDelegate() {
    276        if (isAccessToUnderlyingConnectionAllowed()) {
    277            return super.getInnermostDelegateInternal();
    278        }
    279        return null;
    280    }
    281}