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