    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.ConnectionFactory;
    021
    022import javax.transaction.TransactionManager;
    023import javax.transaction.xa.XAException;
    024import javax.transaction.xa.XAResource;
    025import javax.transaction.xa.Xid;
    026import java.sql.Connection;
    027import java.sql.SQLException;
    028import java.util.Objects;
    029
    030/**
    031 * An implementation of XAConnectionFactory which manages non-XA connections in XA transactions. A non-XA connection
    032 * commits and rolls back as part of the XA transaction, but is not recoverable since the connection does not implement
    033 * the 2-phase protocol.
    034 *
    035 * @since 2.0
    036 */
    037public class LocalXAConnectionFactory implements XAConnectionFactory {
    038    private final TransactionRegistry transactionRegistry;
    039    private final ConnectionFactory connectionFactory;
    040
    041    /**
    042     * Creates an LocalXAConnectionFactory which uses the specified connection factory to create database connections.
    043     * The connections are enlisted into transactions using the specified transaction manager.
    044     *
    045     * @param transactionManager
    046     *            the transaction manager in which connections will be enlisted
    047     * @param connectionFactory
    048     *            the connection factory from which connections will be retrieved
    049     */
    050    public LocalXAConnectionFactory(final TransactionManager transactionManager,
    051            final ConnectionFactory connectionFactory) {
    052        Objects.requireNonNull(transactionManager, "transactionManager is null");
    053        Objects.requireNonNull(connectionFactory, "connectionFactory is null");
    054        this.transactionRegistry = new TransactionRegistry(transactionManager);
    055        this.connectionFactory = connectionFactory;
    056    }
    057
    058    @Override
    059    public TransactionRegistry getTransactionRegistry() {
    060        return transactionRegistry;
    061    }
    062
    063    @Override
    064    public Connection createConnection() throws SQLException {
    065        // create a new connection
    066        final Connection connection = connectionFactory.createConnection();
    067
    068        // create a XAResource to manage the connection during XA transactions
    069        final XAResource xaResource = new LocalXAResource(connection);
    070
    071        // register the xa resource for the connection
    072        transactionRegistry.registerConnection(connection, xaResource);
    073
    074        return connection;
    075    }
    076
    077    /**
    078     * LocalXAResource is a fake XAResource for non-XA connections. When a transaction is started the connection
    079     * auto-commit is turned off. When the connection is committed or rolled back, the commit or rollback method is
    080     * called on the connection and then the original auto-commit value is restored.
    081     * <p>
    082     * The LocalXAResource also respects the connection read-only setting. If the connection is read-only the commit
    083     * method will not be called, and the prepare method returns the XA_RDONLY.
    084     * </p>
    085     * It is assumed that the wrapper around a managed connection disables the setAutoCommit(), commit(), rollback() and
    086     * setReadOnly() methods while a transaction is in progress.
    087     *
    088     * @since 2.0
    089     */
    090    protected static class LocalXAResource implements XAResource {
    091        private final Connection connection;
    092        private Xid currentXid; // @GuardedBy("this")
    093        private boolean originalAutoCommit; // @GuardedBy("this")
    094
    095        public LocalXAResource(final Connection localTransaction) {
    096            this.connection = localTransaction;
    097        }
    098
    099        /**
    100         * Gets the current xid of the transaction branch associated with this XAResource.
    101         *
    102         * @return the current xid of the transaction branch associated with this XAResource.
    103         */
    104        public synchronized Xid getXid() {
    105            return currentXid;
    106        }
    107
    108        /**
    109         * Signals that a the connection has been enrolled in a transaction. This method saves off the current auto
    110         * commit flag, and then disables auto commit. The original auto commit setting is restored when the transaction
    111         * completes.
    112         *
    113         * @param xid
    114         *            the id of the transaction branch for this connection
    115         * @param flag
    116         *            either XAResource.TMNOFLAGS or XAResource.TMRESUME
    117         * @throws XAException
    118         *             if the connection is already enlisted in another transaction, or if auto-commit could not be
    119         *             disabled
    120         */
    121        @Override
    122        public synchronized void start(final Xid xid, final int flag) throws XAException {
    123            if (flag == XAResource.TMNOFLAGS) {
    124                // first time in this transaction
    125
    126                // make sure we aren't already in another tx
    127                if (this.currentXid != null) {
    128                    throw new XAException("Already enlisted in another transaction with xid " + xid);
    129                }
    130
    131                // save off the current auto commit flag so it can be restored after the transaction completes
    132                try {
    133                    originalAutoCommit = connection.getAutoCommit();
    134                } catch (final SQLException ignored) {
    135                    // no big deal, just assume it was off
    136                    originalAutoCommit = true;
    137                }
    138
    139                // update the auto commit flag
    140                try {
    141                    connection.setAutoCommit(false);
    142                } catch (final SQLException e) {
    143                    throw (XAException) new XAException("Count not turn off auto commit for a XA transaction")
    144                            .initCause(e);
    145                }
    146
    147                this.currentXid = xid;
    148            } else if (flag == XAResource.TMRESUME) {
    149                if (!xid.equals(this.currentXid)) {
    150                    throw new XAException("Attempting to resume in different transaction: expected " + this.currentXid
    151                            + ", but was " + xid);
    152                }
    153            } else {
    154                throw new XAException("Unknown start flag " + flag);
    155            }
    156        }
    157
    158        /**
    159         * This method does nothing.
    160         *
    161         * @param xid
    162         *            the id of the transaction branch for this connection
    163         * @param flag
    164         *            ignored
    165         * @throws XAException
    166         *             if the connection is already enlisted in another transaction
    167         */
    168        @Override
    169        public synchronized void end(final Xid xid, final int flag) throws XAException {
    170            Objects.requireNonNull(xid, "xid is null");
    171            if (!this.currentXid.equals(xid)) {
    172                throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid);
    173            }
    174
    175            // This notification tells us that the application server is done using this
    176            // connection for the time being. The connection is still associated with an
    177            // open transaction, so we must still wait for the commit or rollback method
    178        }
    179
    180        /**
    181         * This method does nothing since the LocalXAConnection does not support two-phase-commit. This method will
    182         * return XAResource.XA_RDONLY if the connection isReadOnly(). This assumes that the physical connection is
    183         * wrapped with a proxy that prevents an application from changing the read-only flag while enrolled in a
    184         * transaction.
    185         *
    186         * @param xid
    187         *            the id of the transaction branch for this connection
    188         * @return XAResource.XA_RDONLY if the connection.isReadOnly(); XAResource.XA_OK otherwise
    189         */
    190        @Override
    191        public synchronized int prepare(final Xid xid) {
    192            // if the connection is read-only, then the resource is read-only
    193            // NOTE: this assumes that the outer proxy throws an exception when application code
    194            // attempts to set this in a transaction
    195            try {
    196                if (connection.isReadOnly()) {
    197                    // update the auto commit flag
    198                    connection.setAutoCommit(originalAutoCommit);
    199
    200                    // tell the transaction manager we are read only
    201                    return XAResource.XA_RDONLY;
    202                }
    203            } catch (final SQLException ignored) {
    204                // no big deal
    205            }
    206
    207            // this is a local (one phase) only connection, so we can't prepare
    208            return XAResource.XA_OK;
    209        }
    210
    211        /**
    212         * Commits the transaction and restores the original auto commit setting.
    213         *
    214         * @param xid
    215         *            the id of the transaction branch for this connection
    216         * @param flag
    217         *            ignored
    218         * @throws XAException
    219         *             if connection.commit() throws a SQLException
    220         */
    221        @Override
    222        public synchronized void commit(final Xid xid, final boolean flag) throws XAException {
    223            Objects.requireNonNull(xid, "xid is null");
    224            if (this.currentXid == null) {
    225                throw new XAException("There is no current transaction");
    226            }
    227            if (!this.currentXid.equals(xid)) {
    228                throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid);
    229            }
    230
    231            try {
    232                // make sure the connection isn't already closed
    233                if (connection.isClosed()) {
    234                    throw new XAException("Connection is closed");
    235                }
    236
    237                // A read only connection should not be committed
    238                if (!connection.isReadOnly()) {
    239                    connection.commit();
    240                }
    241            } catch (final SQLException e) {
    242                throw (XAException) new XAException().initCause(e);
    243            } finally {
    244                try {
    245                    connection.setAutoCommit(originalAutoCommit);
    246                } catch (final SQLException e) {
    247                    // ignore
    248                }
    249                this.currentXid = null;
    250            }
    251        }
    252
    253        /**
    254         * Rolls back the transaction and restores the original auto commit setting.
    255         *
    256         * @param xid
    257         *            the id of the transaction branch for this connection
    258         * @throws XAException
    259         *             if connection.rollback() throws a SQLException
    260         */
    261        @Override
    262        public synchronized void rollback(final Xid xid) throws XAException {
    263            Objects.requireNonNull(xid, "xid is null");
    264            if (!this.currentXid.equals(xid)) {
    265                throw new XAException("Invalid Xid: expected " + this.currentXid + ", but was " + xid);
    266            }
    267
    268            try {
    269                connection.rollback();
    270            } catch (final SQLException e) {
    271                throw (XAException) new XAException().initCause(e);
    272            } finally {
    273                try {
    274                    connection.setAutoCommit(originalAutoCommit);
    275                } catch (final SQLException e) {
    276                    // Ignore.
    277                }
    278                this.currentXid = null;
    279            }
    280        }
    281
    282        /**
    283         * Returns true if the specified XAResource == this XAResource.
    284         *
    285         * @param xaResource
    286         *            the XAResource to test
    287         * @return true if the specified XAResource == this XAResource; false otherwise
    288         */
    289        @Override
    290        public boolean isSameRM(final XAResource xaResource) {
    291            return this == xaResource;
    292        }
    293
    294        /**
    295         * Clears the currently associated transaction if it is the specified xid.
    296         *
    297         * @param xid
    298         *            the id of the transaction to forget
    299         */
    300        @Override
    301        public synchronized void forget(final Xid xid) {
    302            if (xid != null && xid.equals(currentXid)) {
    303                currentXid = null;
    304            }
    305        }
    306
    307        /**
    308         * Always returns a zero length Xid array. The LocalXAConnectionFactory can not support recovery, so no xids
    309         * will ever be found.
    310         *
    311         * @param flag
    312         *            ignored since recovery is not supported
    313         * @return always a zero length Xid array.
    314         */
    315        @Override
    316        public Xid[] recover(final int flag) {
    317            return new Xid[0];
    318        }
    319
    320        /**
    321         * Always returns 0 since we have no way to set a transaction timeout on a JDBC connection.
    322         *
    323         * @return always 0
    324         */
    325        @Override
    326        public int getTransactionTimeout() {
    327            return 0;
    328        }
    329
    330        /**
    331         * Always returns false since we have no way to set a transaction timeout on a JDBC connection.
    332         *
    333         * @param transactionTimeout
    334         *            ignored since we have no way to set a transaction timeout on a JDBC connection
    335         * @return always false
    336         */
    337        @Override
    338        public boolean setTransactionTimeout(final int transactionTimeout) {
    339            return false;
    340        }
    341    }
    342
    343}