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