/*

   Derby - Class org.apache.derby.impl.drda.DRDAXAProtocol.java

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */

package org.apache.derby.impl.drda;

import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import org.apache.derby.iapi.jdbc.ResourceAdapter;
import org.apache.derby.iapi.services.monitor.Monitor;
import org.apache.derby.shared.common.sanity.SanityManager;
import org.apache.derby.iapi.store.access.xa.XAXactId;
import org.apache.derby.shared.common.reference.MessageId;

/**
 * This class translates DRDA XA protocol from an application requester to XA
 * calls for Derby and then translates the results from Derby to DRDA
 * for return to the application requester.
 * This class requires the use of javax.transaction.xa classes from j2ee,
 * so is separated from DRDAConnThread, because of the additional
 * library requirements
 */

class DRDAXAProtocol {

    private DRDAConnThread connThread;
    private DDMReader reader;
    private DDMWriter writer;
    /** Holds the Xid of the global transaction associated with
      * the corresponding DRDAConnThread (and connection itself). */
    private Xid xid;


    DRDAXAProtocol(DRDAConnThread connThread)
    {
        this.connThread = connThread;
        reader = connThread.getReader();
        writer = connThread.getWriter();
        xid = null;
    }



    /**
     * Parse SYNCCTL - Parse SYNCCTL command for XAMGR lvl 7
     *
     */
    protected void parseSYNCCTL() throws DRDAProtocolException
    {
        
        reader.markCollection();
        
        int codePoint = reader.getCodePoint(CodePoint.SYNCTYPE);
        int syncType = parseSYNCTYPE();
        
        int xaflags = 0;
        boolean readXAFlags = false;
        Xid xid = null;
        // The value -1 means no value of timeout received
        long xaTimeout = -1;
        boolean readXATimeout = false;

        codePoint = reader.getCodePoint();
        while (codePoint != -1)
        {
            switch(codePoint)
            {
                case CodePoint.XID:
                    xid = parseXID();
                    break;
                case CodePoint.XAFLAGS:
                    xaflags = parseXAFlags();
                    readXAFlags =true;
                    break;
                case CodePoint.TIMEOUT:
                    xaTimeout = parseXATimeout();
                    readXATimeout = true;
                    break;
                case CodePoint.RLSCONV:
                    connThread.codePointNotSupported(codePoint);      
                default:
                    connThread.invalidCodePoint(codePoint);
            }

            codePoint = reader.getCodePoint();
        }


        {
            connThread.trace("syncType = " + syncTypeToString(syncType));
            connThread.trace("xid = " + xid);
            connThread.trace("xaflags =" + xaflagsToString(xaflags));
        }

        if (syncType != CodePoint.SYNCTYPE_INDOUBT)
        {
            if (xid == null)
                connThread.missingCodePoint(CodePoint.XID);
            
            // All but Recover and forget require xaFlags
            if (syncType != CodePoint.SYNCTYPE_REQ_FORGET && 
                ! readXAFlags)
                if (SanityManager.DEBUG)
                    connThread.missingCodePoint(CodePoint.XAFLAGS);
        }

        switch (syncType)
        {  
            case CodePoint.SYNCTYPE_NEW_UOW:
                // new unit of work for XA
                // formatId -1 is just a local connection
                startXATransaction(xid, xaflags, xaTimeout);
                break;
            case CodePoint.SYNCTYPE_END_UOW:
                // End unit of work
                endXA(xid,xaflags);
                break;
            case CodePoint.SYNCTYPE_PREPARE:
                prepareXATransaction(xid);
                // Prepare to commit 
                break;
            case CodePoint.SYNCTYPE_MIGRATE:
                // migrate to resync server sync type
                connThread.codePointNotSupported(codePoint);                
                break;
            case CodePoint.SYNCTYPE_REQ_COMMIT:
                // request to commit sync type
                commitTransaction(xid,xaflags);
                break;
            case CodePoint.SYNCTYPE_COMMITTED:
                // commit  sync type
                commitTransaction(xid, xaflags);
                break;
            case CodePoint.SYNCTYPE_REQ_FORGET:
                // request to forget sync type
                forgetXATransaction(xid);
                break;
            case CodePoint.SYNCTYPE_ROLLBACK:
                //rollback sync type
                rollbackTransaction(xid, true);
                break;
            case CodePoint.SYNCTYPE_INDOUBT:
                //recover sync type
                if (readXAFlags)
                    recoverXA(xaflags);
                else
                    recoverXA();
                break;
            default:
                connThread.invalidCodePoint(codePoint);
        }

    }

    /** 
     * parse SYNCTYPE for XAMGR lvl 7
     * return synctype value 
     *   CodePoint.SYNCTYPE_NEW_UOW -&gt; XAResource.start()
     *   CodePoint.SYNCTYPE_END_UOW -&gt; XAResource.end()
     *   CodePoint.SYNCTYPE_PREPARE -&gt; XAResource.prepare()
     *   CodePoint.SYNCTYPE_MIGRATE -&gt; not supported  //SYNCPT MGR LEVEL 5
     *   CodePoint.SYNCTYPE_REQ_COMMIT -&gt; not supported //SYNCPT MGR LEVEL 5
     *   CodePoint.SYNCTYPE_COMMITTED -&gt; XAResource.commit()  
     *                                   or local commit for null XID
     *   CodePoint.SYNCTYPE_REQ_LOG -&gt;  not supported
     *   CodePoint.SYNCTYPE_REQ_FORGET -&gt; XAResource.forget()
     *   CodePoint.SYNCTYPE_ROLLBACK -&gt; XAResource.rollback()
     *   CodePoint.SYNCTYPE_MIGRATED -&gt; not supported
     *   CodePoint.SYNCTYPE_INDOUBT   -&gt; XAResource.recover();
     * 
     */
    protected int  parseSYNCTYPE() throws DRDAProtocolException
    {
        return reader.readUnsignedByte();
        
    }
    

    /** Parse XID
     *  formatId -1 translates into a null XID and a local transaction
     */
    private  Xid parseXID () throws DRDAProtocolException
    {
        int formatId = reader.readNetworkInt();
        byte[] gtrid = null;
        byte[] bqual = null;
        if (formatId != -1)
        {
            int gtridLen = reader.readNetworkInt();
            int bqualLen = reader.readNetworkInt();
            
            gtrid = reader.readBytes(gtridLen);
            bqual = reader.readBytes(bqualLen);
        }
        return new DRDAXid(formatId, gtrid, bqual);
    }

    /** 
     *  parse XAFlags 
     *
     * @return XAFlags value
     * @throws DRDAProtocolException
     */
    private int parseXAFlags() throws DRDAProtocolException
    {
        return reader.readNetworkInt();
    }

    /**
     * Parses a XA transaction timout value.
     *
     * @return A timeout value.
     * @throws DRDAProtocolException
     */
    private long parseXATimeout() throws DRDAProtocolException
    {
        return reader.readNetworkLong();
    }


    /**
     *  Start the xa transaction. Send SYNCRRD response
     * 
     *  @param xid - XID (formatId = -1 for local transaction)
     *  @param xaflags - xaflags
     *  @param xaTimeout - The timeout for the global transaction in millis
     *                     (or -1 if not specified)
     *  @throws DRDAProtocolException
     */
    private void startXATransaction(Xid xid, int xaflags, long xaTimeout)
                                                throws DRDAProtocolException
    {
        XAResource xaResource = getXAResource();
        int xaRetVal = XAResource.XA_OK;

        try {
            if (xid.getFormatId() == -1 && xaTimeout != -1) {
                // The value of timeout might be specified only for global transactions
                throw new XAException(XAException.XAER_PROTO);
            } else if (xaTimeout != -1 && xaflags != XAResource.TMNOFLAGS) {
                // According the DRDA spec if the value of timeout was specified
                // a TMNOFLAGS have to be used
                throw new XAException(XAException.XAER_PROTO);
            } else {
                if (xaTimeout == 0) {
                    // According the DRDA specification
                    // value 0 means the unlimited timeout
                    // Integer.MAX_VALUE is used in derby
                    // to set up the infinite timeout.
                    // In JDBC spec the value 0 means the resource
                    // manager's default value.
                    xaResource.setTransactionTimeout(Integer.MAX_VALUE);
                } else if (xaTimeout == -1) {
                    // The timeout value was not specified, so use the default
                    // timeout - see javadoc for XAResource.setTransactionTimeout
                    xaResource.setTransactionTimeout(0);
                } else {
                    // The value of timeout was specified
                    xaResource.setTransactionTimeout((int) (xaTimeout/1000));
                }
                if (xid.getFormatId() != -1) {
                    xaResource.start(xid,xaflags);
                }
                this.xid = xid;
            }
        } catch (XAException xe)
        {
            xaRetVal = processXAException(xe);
        }
        writeSYNCCRD(CodePoint.SYNCTYPE_NEW_UOW, 
                     xaRetVal, null);
        
    }
    

    /**
     *  Commit  the xa transaction. Send SYNCCRD response
     * 
     *  @param xid - XID (formatId = -1 for local transaction)
     *  @param xaflags - xaflags
     *  @throws DRDAProtocolException
     */
    private void commitTransaction(Xid xid, int xaflags) throws DRDAProtocolException
    {
        boolean local  = ( xid.getFormatId() == -1);
        if (local)
            commitLocalTransaction();
        else
            commitXATransaction(xid, xaflags);
    }

    /**
     *  Commit local transaction. Send SYNCCRD response.
     * 
     *  @throws DRDAProtocolException
     */
    private void commitLocalTransaction() throws DRDAProtocolException
    {
        int xaRetVal = XAResource.XA_OK;
        try {
            connThread.getDatabase().commit();
        }
        catch  (Exception e)
        {
            xaRetVal = XAException.XAER_RMFAIL;
            if (SanityManager.DEBUG)
            {
                connThread.getServer().consoleExceptionPrint(e);
            }

        }
        writeSYNCCRD(CodePoint.SYNCTYPE_COMMITTED, 
                     xaRetVal, null);

    }

    
    /**
     *  Commit  the xa transaction. Send SYNCCRD response.
     * 
     *  @param xid - XID 
     *  @param xaflags - xaflags
     *  @throws DRDAProtocolException
     */
    private void commitXATransaction(Xid xid, int xaflags) throws DRDAProtocolException
    {
        XAResource xaResource = getXAResource();
        int xaRetVal = XAResource.XA_OK;
        // check this
        boolean isOnePhase = (xaflags & XAResource.TMONEPHASE) != 0;
        try {
            xaResource.commit(xid, isOnePhase);
            if (SanityManager.DEBUG)
                connThread.trace("committed XA transaction: xaRetVal=" + xaRetVal);

        } catch (XAException xe)
        {
            xaRetVal = processXAException(xe);
        }
        writeSYNCCRD(CodePoint.SYNCTYPE_COMMITTED, 
                     xaRetVal, null);
        
    }

    /**
     * Rollback transaction. Optionally send SYNCCRD response.
     * @param xid  Xid for rollback for global transaction.
     *             If xid formatid is -1 it represents a local transaction
     * @param sendSYNCCRD Indicates whether the function should
     *                    send a SYNCCRD response
     */
    private void rollbackTransaction(Xid xid, boolean sendSYNCCRD) throws DRDAProtocolException
    {
        boolean local  = ( xid.getFormatId() == -1);
        if (local)
            rollbackLocalTransaction(sendSYNCCRD);
        else
            rollbackXATransaction(xid, sendSYNCCRD);
    }
    
    /**
     * Rollback a local transaction. Optionally send SYNCCRD response.
     *
     * @param sendSYNCCRD Indicates whether the function should
     *                    send a SYNCCRD response
     * @throws DRDAProtocolException
     */
    private void rollbackLocalTransaction(boolean sendSYNCCRD) throws DRDAProtocolException
    {
        int xaRetVal = XAResource.XA_OK;
        try {
            connThread.getDatabase().rollback();
        }
        catch  (Exception e)
        {
            xaRetVal = XAException.XAER_RMFAIL;
            if (SanityManager.DEBUG)
            {
                connThread.getServer().consoleExceptionPrint(e);
            }
            
        }
        if (sendSYNCCRD) {
            writeSYNCCRD(CodePoint.SYNCTYPE_COMMITTED,
                         xaRetVal, null);
        }
    }

    /**
     *  Rollback the xa transaction. Optionally send SYNCCRD response.
     * 
     *  @param xid - XID 
     *  @param sendSYNCCRD Indicates whether the function should
     *                     send a SYNCCRD response
     *  @throws DRDAProtocolException
     */
    private void rollbackXATransaction(Xid xid, boolean sendSYNCCRD) throws DRDAProtocolException
    {
        XAResource xaResource = getXAResource();
        int xaRetVal = XAResource.XA_OK;

        try {
            xaResource.rollback(xid);
            if (SanityManager.DEBUG)
            {
                connThread.trace("rollback  XA transaction: xaRetVal=" + xaRetVal); 
            }
        } catch (XAException xe)
        {
            xaRetVal = processXAException(xe);
        }
        if (sendSYNCCRD) {
            writeSYNCCRD(CodePoint.SYNCTYPE_ROLLBACK,
                         xaRetVal, null);
        }
    }

    /**
     *  End  the xa transaction. Send SYNCRRD response
     * 
     *  @param xid - XID 
     *  @param xaflags - xaflags
     *  @throws DRDAProtocolException
     */
    private void endXA(Xid xid, int xaflags) throws DRDAProtocolException
    {
        XAResource xaResource = getXAResource();        
        int xaRetVal = XAResource.XA_OK;

        try {
            xaResource.end(xid,xaflags);
            this.xid = null;
            if (SanityManager.DEBUG)
            {
                connThread.trace("ended XA transaction. xid =  " + xid +
                               " xaflags =" + xaflags + 
                                 "xaRetVal=" + xaRetVal); 
            }
        } catch (XAException xe)
        {
            xaRetVal = processXAException(xe);
        }
        writeSYNCCRD(CodePoint.SYNCTYPE_END_UOW,
                     xaRetVal, null);
    }


    /**
     *  Prepare the xa transaction. Send SYNCCRD response.
     * 
     *  @param xid - XID 
     *  @throws DRDAProtocolException
     */
    private void prepareXATransaction(Xid xid) throws DRDAProtocolException
    {
        XAResource xaResource = getXAResource();
        int xaRetVal;

        try {
            xaRetVal = xaResource.prepare(xid);
            if (SanityManager.DEBUG)
            {
                connThread.trace("prepared xa transaction: xaRetVal=" +
                                xaRetVal); 
            }
        } catch (XAException xe)
        {
            xaRetVal = processXAException(xe);
        }
        writeSYNCCRD(CodePoint.SYNCTYPE_PREPARE,
                     xaRetVal, null);
    }

    /**
     *  Forget the xa transaction. Send SYNCCRD response.
     * 
     *  @param xid - XID 
     *  @throws DRDAProtocolException
     */
    private void forgetXATransaction(Xid xid) throws DRDAProtocolException
    {
        XAResource xaResource = getXAResource();
        int xaRetVal = XAResource.XA_OK;

        try {
            xaResource.forget(xid);
            if (SanityManager.DEBUG)
            {
                connThread.trace("forgot xa transaction: xaRetVal=" + xaRetVal);
            }
        } catch (XAException xe)
        {
            xaRetVal = processXAException(xe);
        }
        writeSYNCCRD(CodePoint.SYNCTYPE_REQ_FORGET,
                     xaRetVal, null);
    }

    // JCC doesn't send xaflags but always wants TMSTARTRSCAN.  
    //So default to that if we got no xaflags
    private void recoverXA() throws DRDAProtocolException
    {
        recoverXA(XAResource.TMSTARTRSCAN);
    }

    /**
     * Call recover. Send SYNCCRD response with indoubt list
     * 
     *  @throws DRDAProtocolException
     */
    private void recoverXA(int xaflags) throws DRDAProtocolException
    {
        XAResource xaResource = getXAResource();
        int xaRetVal = XAResource.XA_OK;
        Xid[] indoubtXids = null;
        try {
            indoubtXids = xaResource.recover(xaflags);
        } catch (XAException xe)
        {
            xaRetVal = processXAException(xe);
        }
        writeSYNCCRD(CodePoint.SYNCTYPE_INDOUBT,
                     xaRetVal, indoubtXids);        
    }

    /** Write SYNCCRD (SYNCCTL response)
     * @param synctype - XA Command to send response for see  parseSYNCTYPE
     * @param xaRetVal - return value from XA command
     * @param xids - list of xids to return for recover. 
     *               null for other commands
     * @throws DRDAProtocolException
     */
    private void writeSYNCCRD (int synctype, int xaRetVal, Xid[] xids) throws DRDAProtocolException
    {
        writer.createDssReply();
        writer.startDdm(CodePoint.SYNCCRD);
        writer.startDdm(CodePoint.XARETVAL);
        writer.writeInt(xaRetVal);
        writer.endDdm();
        if (xids != null)
            writePRPHRCLST(xids);
        writer.endDdmAndDss();
    }

    /** write PRPHRCLST (indoubt list)
     * 
     * @param xids - list of indoubt xa transactions obtained from recover
     * @throws DRDAProtocolException
     */
    private void writePRPHRCLST(Xid[] xids) throws DRDAProtocolException
    {
        int xidcnt = (xids == null ? 0 : xids.length);
        writer.startDdm(CodePoint.PRPHRCLST);
        writer.writeScalar2Bytes(CodePoint.XIDCNT, xidcnt);
        for (int i = 0; i < xidcnt; i++)
            writeXID(xids[i]);
        writer.endDdm();
    }

    /** write XID
     * 
     * @param xid - XID to write
     * @throws DRDAProtocolException
     */
    
    private void writeXID(Xid xid) throws DRDAProtocolException
    {
        writer.startDdm(CodePoint.XID);
        int formatId = xid.getFormatId();
        byte[] gtrid = xid.getGlobalTransactionId();
        byte[] bqual = xid.getBranchQualifier();
        
        writer.writeInt(formatId);
        writer.writeInt(gtrid.length);
        writer.writeInt(bqual.length);
        writer.writeBytes(gtrid);
        writer.writeBytes(bqual);
        writer.endDdm();
    }
    

    /** get XAResource for the connection
     *
     * @return XAResource
     */
    private XAResource getXAResource()
    {
        return ((XADatabase) connThread.getDatabase()).getXAResource();
        
    }
    
    /** printable syncType for debug output
     * @param syncType
     * @return - sync type meaning
     */
    private String syncTypeToString(int syncType)
    {
        switch (syncType)
        {  
            case CodePoint.SYNCTYPE_NEW_UOW:
                return "SYNCTYPE_NEW_UOW";
                
            case CodePoint.SYNCTYPE_END_UOW:
                return "SYNCTYPE_END_UOW";
                
            case CodePoint.SYNCTYPE_PREPARE:
                return "SYNCTYPE_PREPARE";
                
            case CodePoint.SYNCTYPE_MIGRATE:
                return "SYNCTYPE_MIGRATE";
                            
            case CodePoint.SYNCTYPE_REQ_COMMIT:
                return "SYNCTYPE_REQ_COMMIT";
                
            case CodePoint.SYNCTYPE_COMMITTED:
                return "SYNCTYPE_COMMITTED";
                
            case CodePoint.SYNCTYPE_REQ_FORGET:
                return "SYNCTYPE_FORGET";
                
            case CodePoint.SYNCTYPE_ROLLBACK:
                return "SYNCTYPE_ROLLBACK";
                
            case CodePoint.SYNCTYPE_REQ_LOG:
                return "SYNCTYPE_REQ_LOG";
                
            case   CodePoint.SYNCTYPE_MIGRATED:
                return "SYNCTYPE_MIGRATED";
                
            case CodePoint.SYNCTYPE_INDOUBT:
                return "SYNCTYPE_INDOUBT";
                
            default:
                return "UNKNOWN SYNCTYPE";
        }
    }

    /** 
     * printable xaflags
     * @param xaflags
     * @return printable xaflags for debug output
     */
    private String xaflagsToString(int xaflags)
    {
        switch (xaflags)
        {
            case XAResource.TMENDRSCAN :
                return "XAResource.TMENDRSCAN";
                
            case XAResource.TMFAIL:
                return "XAResource.TMFAIL";
                
            case XAResource.TMNOFLAGS:
                return "XAResource.TMNOFLAGS";
                
            case XAResource.TMJOIN:
                return "XAResource.TMJOIN";
                
            case XAResource.TMONEPHASE:
                return "XAResource.TMONEPHASE";
                
            case XAResource.TMRESUME:
                return "XAResource.TMRESUME";
                
            case XAResource.TMSTARTRSCAN:
                return "XAResource.TMSTARTRSCAN";
                
            case XAResource.TMSUCCESS:
                return "XAResource.TMSUCCESS";
                
            case XAResource.TMSUSPEND:
                return "XAResource.TMSUSPEND";
                
            default:
                return "UNRECOGNIZED flags:" + xaflags;
                
        }
    }

    /** 
     * return xa exception errorCode.
     * print to console for debug output.
     * @param xe - XA Exception
     */
    private int processXAException(XAException xe)
    {
        int xaRetVal = xe.errorCode;
        if (SanityManager.DEBUG)
        {
            connThread.getServer().consoleExceptionPrint(xe);
        }
        return xaRetVal;
    }

    /**
     * @return The ResourceAdapter instance for
     *         the underlying database.
     */
    ResourceAdapter getResourceAdapter()
    {
        return ((XADatabase)connThread.getDatabase()).getResourceAdapter();
    }

    /**
     * This function rollbacks the current global transaction associated
     * with the XAResource or a local transaction. The function should
     * be called only in exceptional cases - like client socket
     * is closed. */
    void rollbackCurrentTransaction()
    {
        if (xid != null) {
            boolean local  = ( xid.getFormatId() == -1);
            if (!local) {
                try {
                    XAXactId xid_im = new XAXactId(xid);
                    getResourceAdapter().cancelXATransaction(
                        xid_im,
                        MessageId.CONN_CLOSE_XA_TRANSACTION_ROLLED_BACK
                    );
                } catch (XAException e) {
                    Monitor.logThrowable(e);
                }
            } else {
                try {
                    rollbackTransaction(xid, false);
                } catch  (DRDAProtocolException e) {
                    // because we do not dump any DRDA stuff to the socket
                    // the exception can not be thrown in this case
                    // However, we will log the exception to the monitor
                    Monitor.logThrowable(e);
                }
            }
            xid = null;
        }
    }

}

