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