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