/*
  Copyright (c) 2005, 2016, Oracle and/or its affiliates. All rights reserved.

  The MySQL Connector/J is licensed under the terms of the GPLv2
  <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors.
  There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
  this software, see the FOSS License Exception
  <http://www.mysql.com/about/legal/licensing/foss-exception.html>.

  This program is free software; you can redistribute it and/or modify it under the terms
  of the GNU General Public License as published by the Free Software Foundation; version 2
  of the License.

  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  See the GNU General Public License for more details.

  You should have received a copy of the GNU General Public License along with this
  program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth
  Floor, Boston, MA 02110-1301  USA

 */

package com.mysql.jdbc.jdbc2.optional;

import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sql.StatementEventListener;
import javax.sql.XAConnection;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import com.mysql.jdbc.Messages;
import com.mysql.jdbc.StringUtils;
import com.mysql.jdbc.Util;
import com.mysql.jdbc.log.Log;

/*
 * XA BEGIN <xid> [JOIN | RESUME] XA START TRANSACTION <xid> [JOIN | RESUME] XA
 * COMMIT <xid> [ONE PHASE] XA END <xid> [SUSPEND [FOR MIGRATE]] XA PREPARE
 * <xid> XA RECOVER XA ROLLBACK <xid>
 */

/**
 * An object that provides support for distributed transactions. An <code>XAConnection</code> object may be enlisted in a distributed transaction by means of
 * an <code>XAResource</code> object. A transaction manager, usually part of a middle tier server, manages an <code>XAConnection</code> object through the
 * <code>XAResource</code> object.
 * 
 * <P>
 * An application programmer does not use this interface directly; rather, it is used by a transaction manager working in the middle tier server.
 */
public class MysqlXAConnection extends MysqlPooledConnection implements XAConnection, XAResource {

    private static final int MAX_COMMAND_LENGTH = 300;

    private com.mysql.jdbc.Connection underlyingConnection;

    private final static Map<Integer, Integer> MYSQL_ERROR_CODES_TO_XA_ERROR_CODES;

    private Log log;

    protected boolean logXaCommands;

    static {
        HashMap<Integer, Integer> temp = new HashMap<Integer, Integer>();

        temp.put(1397, XAException.XAER_NOTA);
        temp.put(1398, XAException.XAER_INVAL);
        temp.put(1399, XAException.XAER_RMFAIL);
        temp.put(1400, XAException.XAER_OUTSIDE);
        temp.put(1401, XAException.XAER_RMERR);
        temp.put(1402, XAException.XA_RBROLLBACK);
        temp.put(1440, XAException.XAER_DUPID);
        temp.put(1613, XAException.XA_RBTIMEOUT);
        temp.put(1614, XAException.XA_RBDEADLOCK);

        MYSQL_ERROR_CODES_TO_XA_ERROR_CODES = Collections.unmodifiableMap(temp);
    }

    private static final Constructor<?> JDBC_4_XA_CONNECTION_WRAPPER_CTOR;

    static {
        if (Util.isJdbc4()) {
            try {
                JDBC_4_XA_CONNECTION_WRAPPER_CTOR = Class.forName("com.mysql.jdbc.jdbc2.optional.JDBC4MysqlXAConnection")
                        .getConstructor(new Class[] { com.mysql.jdbc.Connection.class, Boolean.TYPE });
            } catch (SecurityException e) {
                throw new RuntimeException(e);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        } else {
            JDBC_4_XA_CONNECTION_WRAPPER_CTOR = null;
        }
    }

    protected static MysqlXAConnection getInstance(com.mysql.jdbc.Connection mysqlConnection, boolean logXaCommands) throws SQLException {
        if (!Util.isJdbc4()) {
            return new MysqlXAConnection(mysqlConnection, logXaCommands);
        }

        return (MysqlXAConnection) Util.handleNewInstance(JDBC_4_XA_CONNECTION_WRAPPER_CTOR, new Object[] { mysqlConnection, Boolean.valueOf(logXaCommands) },
                mysqlConnection.getExceptionInterceptor());
    }

    /**
     * @param connection
     */
    public MysqlXAConnection(com.mysql.jdbc.Connection connection, boolean logXaCommands) throws SQLException {
        super(connection);
        this.underlyingConnection = connection;
        this.log = connection.getLog();
        this.logXaCommands = logXaCommands;
    }

    /**
     * Retrieves an <code>XAResource</code> object that the transaction
     * manager will use to manage this <code>XAConnection</code> object's
     * participation in a distributed transaction.
     * 
     * @return the <code>XAResource</code> object
     * @exception SQLException
     *                if a database access error occurs
     */
    public XAResource getXAResource() throws SQLException {
        return this;
    }

    /**
     * Obtains the current transaction timeout value set for this XAResource
     * instance. If XAResource.setTransactionTimeout was not used prior to
     * invoking this method, the return value is the default timeout set for the
     * resource manager; otherwise, the value used in the previous
     * setTransactionTimeout call is returned.
     * 
     * @return the transaction timeout value in seconds.
     * 
     * @throws XAException
     *             An error has occurred. Possible exception values are
     *             XAER_RMERR and XAER_RMFAIL.
     */
    public int getTransactionTimeout() throws XAException {
        return 0;
    }

    /**
     * Sets the current transaction timeout value for this XAResource instance.
     * Once set, this timeout value is effective until setTransactionTimeout is
     * invoked again with a different value.
     * 
     * To reset the timeout value to the default value used by the resource
     * manager, set the value to zero. If the timeout operation is performed
     * successfully, the method returns true; otherwise false.
     * 
     * If a resource manager does not support explicitly setting the transaction
     * timeout value, this method returns false.
     * 
     * @parameter seconds The transaction timeout value in seconds.
     * 
     * @return true if the transaction timeout value is set successfully;
     *         otherwise false.
     * 
     * @throws XAException
     *             An error has occurred. Possible exception values are
     *             XAER_RMERR, XAER_RMFAIL, or XAER_INVAL.
     */
    public boolean setTransactionTimeout(int arg0) throws XAException {
        return false;
    }

    /**
     * This method is called to determine if the resource manager instance
     * represented by the target object is the same as the resouce manager
     * instance represented by the parameter xares.
     * 
     * @parameter xares An XAResource object whose resource manager instance is
     *            to be compared with the resource manager instance of the
     *            target object.
     * 
     * @return true if it's the same RM instance; otherwise false.
     * 
     * @throws XAException
     *             An error has occurred. Possible exception values are
     *             XAER_RMERR and XAER_RMFAIL.
     */
    public boolean isSameRM(XAResource xares) throws XAException {

        if (xares instanceof MysqlXAConnection) {
            return this.underlyingConnection.isSameResource(((MysqlXAConnection) xares).underlyingConnection);
        }

        return false;
    }

    /**
     * This method is called to obtain a list of prepared transaction branches
     * from a resource manager. The transaction manager calls this method during
     * recovery to obtain the list of transaction branches that are currently in
     * prepared or heuristically completed states.
     * 
     * The flag parameter indicates where the recover scan should start or end,
     * or start and end. This method may be invoked one or more times during a
     * recovery scan. The resource manager maintains a cursor which marks the
     * current position of the prepared or heuristically completed transaction list.
     * Each invocation of the recover method moves the cursor passed the set of Xids
     * that are returned.
     * 
     * Two consecutive invocation of this method that starts from the
     * beginning of the list must return the same list of transaction branches
     * unless one of the following takes place:
     * 
     * - the transaction manager invokes the commit, forget, prepare, or rollback method for that resource
     * manager, between the two consecutive invocation of the recovery scan.
     * 
     * - the resource manager heuristically completes some transaction branches
     * between the two invocation of the recovery scan.
     * 
     * @param flag
     *            One of TMSTARTRSCAN, TMENDRSCAN, TMNOFLAGS. TMNOFLAGS must be
     *            used when no other flags are set in the parameter.
     * 
     * @returns The resource manager returns zero or more XIDs of the
     *          transaction branches that are currently in a prepared or
     *          heuristically completed state. If an error occurs during the
     *          operation, the resource manager should throw the appropriate
     *          XAException.
     * 
     * @throws XAException
     *             An error has occurred. Possible values are XAER_RMERR,
     *             XAER_RMFAIL, XAER_INVAL, and XAER_PROTO.
     */
    public Xid[] recover(int flag) throws XAException {
        return recover(this.underlyingConnection, flag);
    }

    protected static Xid[] recover(Connection c, int flag) throws XAException {
        /*
         * The XA RECOVER statement returns information for those XA transactions on the MySQL server that are in the PREPARED state. (See Section 13.4.7.2, �XA
         * Transaction States�.) The output includes a row for each such XA transaction on the server, regardless of which client started it.
         * 
         * XA RECOVER output rows look like this (for an example xid value consisting of the parts 'abc', 'def', and 7):
         * 
         * mysql> XA RECOVER;
         * +----------+--------------+--------------+--------+
         * | formatID | gtrid_length | bqual_length | data |
         * +----------+--------------+--------------+--------+
         * | 7 | 3 | 3 | abcdef |
         * +----------+--------------+--------------+--------+
         * 
         * The output columns have the following meanings:
         * 
         * formatID is the formatID part of the transaction xid
         * gtrid_length is the length in bytes of the gtrid part of the xid
         * bqual_length is the length in bytes of the bqual part of the xid
         * data is the concatenation of the gtrid and bqual parts of the xid
         */

        boolean startRscan = ((flag & TMSTARTRSCAN) > 0);
        boolean endRscan = ((flag & TMENDRSCAN) > 0);

        if (!startRscan && !endRscan && flag != TMNOFLAGS) {
            throw new MysqlXAException(XAException.XAER_INVAL, Messages.getString("MysqlXAConnection.001"), null);
        }

        //
        // We return all recovered XIDs at once, so if not  TMSTARTRSCAN, return no new XIDs
        //
        // We don't attempt to maintain state to check for TMNOFLAGS "outside" of a scan
        //

        if (!startRscan) {
            return new Xid[0];
        }

        ResultSet rs = null;
        Statement stmt = null;

        List<MysqlXid> recoveredXidList = new ArrayList<MysqlXid>();

        try {
            // TODO: Cache this for lifetime of XAConnection
            stmt = c.createStatement();

            rs = stmt.executeQuery("XA RECOVER");

            while (rs.next()) {
                final int formatId = rs.getInt(1);
                int gtridLength = rs.getInt(2);
                int bqualLength = rs.getInt(3);
                byte[] gtridAndBqual = rs.getBytes(4);

                final byte[] gtrid = new byte[gtridLength];
                final byte[] bqual = new byte[bqualLength];

                if (gtridAndBqual.length != (gtridLength + bqualLength)) {
                    throw new MysqlXAException(XAException.XA_RBPROTO, Messages.getString("MysqlXAConnection.002"), null);
                }

                System.arraycopy(gtridAndBqual, 0, gtrid, 0, gtridLength);
                System.arraycopy(gtridAndBqual, gtridLength, bqual, 0, bqualLength);

                recoveredXidList.add(new MysqlXid(gtrid, bqual, formatId));
            }
        } catch (SQLException sqlEx) {
            throw mapXAExceptionFromSQLException(sqlEx);
        } finally {
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException sqlEx) {
                    throw mapXAExceptionFromSQLException(sqlEx);
                }
            }

            if (stmt != null) {
                try {
                    stmt.close();
                } catch (SQLException sqlEx) {
                    throw mapXAExceptionFromSQLException(sqlEx);
                }
            }
        }

        int numXids = recoveredXidList.size();

        Xid[] asXids = new Xid[numXids];
        Object[] asObjects = recoveredXidList.toArray();

        for (int i = 0; i < numXids; i++) {
            asXids[i] = (Xid) asObjects[i];
        }

        return asXids;
    }

    /**
     * Asks the resource manager to prepare for a transaction commit of the
     * transaction specified in xid.
     * 
     * @parameter xid A global transaction identifier.
     * 
     * @returns A value indicating the resource manager's vote on the outcome of
     *          the transaction.
     * 
     *          The possible values are: XA_RDONLY or XA_OK. If the resource manager
     *          wants to roll back the transaction, it should do so by raising an
     *          appropriate XAException in the prepare method.
     * 
     * @throws XAException
     *             An error has occurred. Possible exception values are: XA_RB*,
     *             XAER_RMERR, XAER_RMFAIL, XAER_NOTA, XAER_INVAL, or
     *             XAER_PROTO.
     */
    public int prepare(Xid xid) throws XAException {
        StringBuilder commandBuf = new StringBuilder(MAX_COMMAND_LENGTH);
        commandBuf.append("XA PREPARE ");
        appendXid(commandBuf, xid);

        dispatchCommand(commandBuf.toString());

        return XA_OK; // TODO: Check for read-only
    }

    /**
     * Tells the resource manager to forget about a heuristically completed
     * transaction branch.
     * 
     * @parameter xid A global transaction identifier.
     * 
     * @throws XAException
     *             An error has occurred. Possible exception values are
     *             XAER_RMERR, XAER_RMFAIL, XAER_NOTA, XAER_INVAL, or
     *             XAER_PROTO.
     */
    public void forget(Xid xid) throws XAException {
        // mysql doesn't support this
    }

    /**
     * Informs the resource manager to roll back work done on behalf of a
     * transaction branch.
     * 
     * @parameter xid A global transaction identifier.
     * 
     * @throws XAException
     *             An error has occurred. Possible XAExceptions are XA_HEURHAZ,
     *             XA_HEURCOM, XA_HEURRB, XA_HEURMIX, XAER_RMERR, XAER_RMFAIL,
     *             XAER_NOTA, XAER_INVAL, or XAER_PROTO.
     * 
     *             If the transaction branch is already marked rollback-only the resource
     *             manager may throw one of the XA_RB* exceptions.
     * 
     *             Upon return, the resource manager has rolled back the branch's work and
     *             has released all held resources.
     */
    public void rollback(Xid xid) throws XAException {
        StringBuilder commandBuf = new StringBuilder(MAX_COMMAND_LENGTH);
        commandBuf.append("XA ROLLBACK ");
        appendXid(commandBuf, xid);

        try {
            dispatchCommand(commandBuf.toString());
        } finally {
            this.underlyingConnection.setInGlobalTx(false);
        }
    }

    /**
     * Ends the work performed on behalf of a transaction branch.
     * 
     * The resource manager disassociates the XA resource from the transaction
     * branch specified and lets the transaction complete.
     * 
     * If TMSUSPEND is specified in the flags, the transaction branch is
     * temporarily suspended in an incomplete state. The transaction context is
     * in a suspended state and must be resumed via the start method with
     * TMRESUME specified.
     * 
     * If TMFAIL is specified, the portion of work has failed. The resource
     * manager may mark the transaction as rollback-only
     * 
     * If TMSUCCESS is specified, the portion of work has completed
     * successfully.
     * 
     * @parameter xid A global transaction identifier that is the same as the
     *            identifier used previously in the start method.
     * 
     * @parameter flags One of TMSUCCESS, TMFAIL, or TMSUSPEND.
     * 
     * @throws XAException
     *             -
     *             An error has occurred. Possible XAException values are
     *             XAER_RMERR, XAER_RMFAIL, XAER_NOTA, XAER_INVAL, XAER_PROTO,
     *             or XA_RB*.
     */
    public void end(Xid xid, int flags) throws XAException {
        StringBuilder commandBuf = new StringBuilder(MAX_COMMAND_LENGTH);
        commandBuf.append("XA END ");
        appendXid(commandBuf, xid);

        switch (flags) {
            case TMSUCCESS:
                break; // no-op
            case TMSUSPEND:
                commandBuf.append(" SUSPEND");
                break;
            case TMFAIL:
                break; // no-op
            default:
                throw new XAException(XAException.XAER_INVAL);
        }

        dispatchCommand(commandBuf.toString());
    }

    /**
     * Starts work on behalf of a transaction branch specified in xid.
     * 
     * If TMJOIN is specified, the start applies to joining a transaction
     * previously seen by the resource manager.
     * 
     * If TMRESUME is specified, the start applies to resuming a suspended
     * transaction specified in the parameter xid.
     * 
     * If neither TMJOIN nor TMRESUME is specified and the transaction specified
     * by xid has previously been seen by the resource manager, the resource
     * manager throws the XAException exception with XAER_DUPID error code.
     * 
     * @parameter xid A global transaction identifier to be associated with the
     *            resource.
     * 
     * @parameter flags One of TMNOFLAGS, TMJOIN, or TMRESUME.
     * 
     * @throws XAException
     *             An error has occurred. Possible exceptions are XA_RB*,
     *             XAER_RMERR, XAER_RMFAIL, XAER_DUPID, XAER_OUTSIDE, XAER_NOTA,
     *             XAER_INVAL, or XAER_PROTO.
     */
    public void start(Xid xid, int flags) throws XAException {
        StringBuilder commandBuf = new StringBuilder(MAX_COMMAND_LENGTH);
        commandBuf.append("XA START ");
        appendXid(commandBuf, xid);

        switch (flags) {
            case TMJOIN:
                commandBuf.append(" JOIN");
                break;
            case TMRESUME:
                commandBuf.append(" RESUME");
                break;
            case TMNOFLAGS:
                // no-op
                break;
            default:
                throw new XAException(XAException.XAER_INVAL);
        }

        dispatchCommand(commandBuf.toString());

        this.underlyingConnection.setInGlobalTx(true);
    }

    /**
     * Commits the global transaction specified by xid.
     * 
     * @parameter xid A global transaction identifier
     * @parameter onePhase - If true, the resource manager should use a
     *            one-phase commit protocol to commit the work done on behalf of
     *            xid.
     * 
     * @throws XAException
     *             An error has occurred. Possible XAExceptions are XA_HEURHAZ,
     *             XA_HEURCOM, XA_HEURRB, XA_HEURMIX, XAER_RMERR, XAER_RMFAIL,
     *             XAER_NOTA, XAER_INVAL, or XAER_PROTO.
     * 
     *             If the resource manager did not commit the transaction and the parameter
     *             onePhase is set to true, the resource manager may throw one of the XA_RB*
     *             exceptions.
     * 
     *             Upon return, the resource manager has rolled back the branch's work and
     *             has released all held resources.
     */

    public void commit(Xid xid, boolean onePhase) throws XAException {
        StringBuilder commandBuf = new StringBuilder(MAX_COMMAND_LENGTH);
        commandBuf.append("XA COMMIT ");
        appendXid(commandBuf, xid);

        if (onePhase) {
            commandBuf.append(" ONE PHASE");
        }

        try {
            dispatchCommand(commandBuf.toString());
        } finally {
            this.underlyingConnection.setInGlobalTx(false);
        }
    }

    private ResultSet dispatchCommand(String command) throws XAException {
        Statement stmt = null;

        try {
            if (this.logXaCommands) {
                this.log.logDebug("Executing XA statement: " + command);
            }

            // TODO: Cache this for lifetime of XAConnection
            stmt = this.underlyingConnection.createStatement();

            stmt.execute(command);

            ResultSet rs = stmt.getResultSet();

            return rs;
        } catch (SQLException sqlEx) {
            throw mapXAExceptionFromSQLException(sqlEx);
        } finally {
            if (stmt != null) {
                try {
                    stmt.close();
                } catch (SQLException sqlEx) {
                }
            }
        }
    }

    protected static XAException mapXAExceptionFromSQLException(SQLException sqlEx) {
        Integer xaCode = MYSQL_ERROR_CODES_TO_XA_ERROR_CODES.get(sqlEx.getErrorCode());

        if (xaCode != null) {
            return (XAException) new MysqlXAException(xaCode.intValue(), sqlEx.getMessage(), null).initCause(sqlEx);
        }

        return (XAException) new MysqlXAException(XAException.XAER_RMFAIL, Messages.getString("MysqlXAConnection.003"), null).initCause(sqlEx);
    }

    private static void appendXid(StringBuilder builder, Xid xid) {
        byte[] gtrid = xid.getGlobalTransactionId();
        byte[] btrid = xid.getBranchQualifier();

        if (gtrid != null) {
            StringUtils.appendAsHex(builder, gtrid);
        }

        builder.append(',');
        if (btrid != null) {
            StringUtils.appendAsHex(builder, btrid);
        }

        builder.append(',');
        StringUtils.appendAsHex(builder, xid.getFormatId());
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.sql.PooledConnection#getConnection()
     */
    @Override
    public synchronized Connection getConnection() throws SQLException {
        Connection connToWrap = getConnection(false, true);

        return connToWrap;
    }

    @Override public void addStatementEventListener(StatementEventListener listener) {
        throw new UnsupportedOperationException();
    }

    @Override public void removeStatementEventListener(StatementEventListener listener) {
        throw new UnsupportedOperationException();
    }
}
