/*
 * Copyright 2004-2010 H2 Group. Multiple-Licensed under the H2 License,
 * Version 1.0, and under the Eclipse Public License, Version 1.0
 * (http://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.jdbcx;

//## Java 1.4 begin ##
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Properties;
import javax.sql.ConnectionEvent;
import javax.sql.ConnectionEventListener;
import javax.sql.XAConnection;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import org.h2.Driver;
import org.h2.constant.ErrorCode;
import org.h2.constant.SysProperties;
import org.h2.jdbc.JdbcConnection;
import org.h2.util.JdbcUtils;
import org.h2.util.New;
import org.h2.util.StringUtils;
//## Java 1.4 end ##

import org.h2.message.DbException;
import org.h2.message.Trace;
import org.h2.message.TraceObject;

//## Java 1.6 begin ##
import javax.sql.StatementEventListener;
//## Java 1.6 end ##

/**
 * This class provides support for distributed transactions.
 * An application developer usually does not use this interface.
 * It is used by the transaction manager internally.
 */
public class JdbcXAConnection extends TraceObject
//## Java 1.4 begin ##
implements XAConnection, XAResource
//## Java 1.4 end ##
{

//## Java 1.4 begin ##
    private static int nextTransactionId;

    private JdbcDataSourceFactory factory;
    private String url, user;

    // This connection is kept open as long as the XAConnection is alive
    private JdbcConnection physicalConn;

    // This connection is replaced whenever getConnection is called
    private volatile PooledJdbcConnection handleConn;
    private ArrayList<ConnectionEventListener> listeners = New.arrayList();
    private Xid currentTransaction;
    private int currentTransactionId;

    static {
        org.h2.Driver.load();
    }

    JdbcXAConnection(JdbcDataSourceFactory factory, int id, String url, String user, char[] password) throws SQLException {
        this.factory = factory;
        setTrace(factory.getTrace(), TraceObject.XA_DATA_SOURCE, id);
        this.url = url;
        this.user = user;
        Properties info = new Properties();
        info.setProperty("user", user);
        info.put("password", StringUtils.cloneCharArray(password));
        physicalConn = (JdbcConnection) Driver.load().connect(url, info);
    }
//## Java 1.4 end ##

    /**
     * Get the XAResource object.
     *
     * @return itself
     */
//## Java 1.4 begin ##
    public XAResource getXAResource() {
        debugCodeCall("getXAResource");
        return this;
    }
//## Java 1.4 end ##

    /**
     * Close the physical connection.
     * This method is usually called by the connection pool.
     *
     * @throws SQLException
     */
//## Java 1.4 begin ##
    public void close() throws SQLException {
        debugCodeCall("close");
        PooledJdbcConnection lastHandle = handleConn;
        if (lastHandle != null) {
            listeners.clear();
            lastHandle.close();
        }
        if (physicalConn != null) {
            try {
                physicalConn.close();
            } finally {
                physicalConn = null;
            }
        }
    }
//## Java 1.4 end ##

    /**
     * Get a connection that is a handle to the physical connection. This method
     * is usually called by the connection pool. This method closes the last
     * connection handle if one exists.
     *
     * @return the connection
     */
//## Java 1.4 begin ##
    public Connection getConnection() throws SQLException {
        debugCodeCall("getConnection");
        PooledJdbcConnection lastHandle = handleConn;
        if (lastHandle != null) {
            lastHandle.close();
        }
        handleConn = new PooledJdbcConnection(physicalConn);
        return handleConn;
    }
//## Java 1.4 end ##

    /**
     * Register a new listener for the connection.
     *
     * @param listener the event listener
     */
//## Java 1.4 begin ##
    public void addConnectionEventListener(ConnectionEventListener listener) {
        debugCode("addConnectionEventListener(listener);");
        listeners.add(listener);
    }
//## Java 1.4 end ##

    /**
     * Remove the event listener.
     *
     * @param listener the event listener
     */
//## Java 1.4 begin ##
    public void removeConnectionEventListener(ConnectionEventListener listener) {
        debugCode("removeConnectionEventListener(listener);");
        listeners.remove(listener);
    }
//## Java 1.4 end ##

    /**
     * INTERNAL
     */
//## Java 1.4 begin ##
    void closedHandle() {
        debugCode("closedHandle();");
        for (ConnectionEventListener listener : New.arrayList(listeners)) {
            ConnectionEvent event = new ConnectionEvent(this);
            listener.connectionClosed(event);
        }
        handleConn = null;
    }
//## Java 1.4 end ##

    /**
     * Get the transaction timeout.
     *
     * @return 0
     */
//## Java 1.4 begin ##
    public int getTransactionTimeout() {
        debugCodeCall("getTransactionTimeout");
        return 0;
    }
//## Java 1.4 end ##

    /**
     * Set the transaction timeout.
     *
     * @param seconds ignored
     * @return false
     */
//## Java 1.4 begin ##
    public boolean setTransactionTimeout(int seconds) {
        debugCodeCall("setTransactionTimeout", seconds);
        return false;
    }
//## Java 1.4 end ##

    /**
     * Checks if this is the same XAResource.
     *
     * @param xares the other object
     * @return true if this is the same object
     */
//## Java 1.4 begin ##
    public boolean isSameRM(XAResource xares) {
        debugCode("isSameRM(xares);");
        return xares == this;
    }
//## Java 1.4 end ##

    /**
     * Get the list of prepared transaction branches.
     * This method is called by the transaction manager during recovery.
     *
     * @param flag TMSTARTRSCAN, TMENDRSCAN, or TMNOFLAGS. If no other flags are set,
     *  TMNOFLAGS must be used.
     *  @return zero or more Xid objects
     * @throws XAException
     */
//## Java 1.4 begin ##
    public Xid[] recover(int flag) throws XAException {
        debugCodeCall("recover", quoteFlags(flag));
        checkOpen();
        Statement stat = null;
        try {
            stat = physicalConn.createStatement();
            ResultSet rs = stat.executeQuery("SELECT * FROM INFORMATION_SCHEMA.IN_DOUBT ORDER BY TRANSACTION");
            ArrayList<Xid> list = New.arrayList();
            while (rs.next()) {
                String tid = rs.getString("TRANSACTION");
                int id = getNextId(XID);
                Xid xid = new JdbcXid(factory, id, tid);
                list.add(xid);
            }
            rs.close();
            Xid[] result = new Xid[list.size()];
            list.toArray(result);
            return result;
        } catch (SQLException e) {
            XAException xa = new XAException(XAException.XAER_RMERR);
            xa.initCause(e);
            throw xa;
        } finally {
            JdbcUtils.closeSilently(stat);
        }
    }
//## Java 1.4 end ##

    /**
     * Prepare a transaction.
     *
     * @param xid the transaction id
     * @return XA_OK
     * @throws XAException
     */
//## Java 1.4 begin ##
    public int prepare(Xid xid) throws XAException {
        if (isDebugEnabled()) {
            debugCode("prepare("+quoteXid(xid)+");");
        }
        checkOpen();
        if (!currentTransaction.equals(xid)) {
            throw new XAException(XAException.XAER_INVAL);
        }
        Statement stat = null;
        try {
            stat = physicalConn.createStatement();
            currentTransactionId = nextTransactionId++;
            stat.execute("PREPARE COMMIT TX_" + currentTransactionId);
        } catch (SQLException e) {
            throw convertException(e);
        } finally {
            JdbcUtils.closeSilently(stat);
        }
        return XA_OK;
    }
//## Java 1.4 end ##

    /**
     * Forget a transaction.
     * This method does not have an effect for this database.
     *
     * @param xid the transaction id
     */
//## Java 1.4 begin ##
    public void forget(Xid xid) {
        if (isDebugEnabled()) {
            debugCode("forget("+quoteXid(xid)+");");
        }
    }
//## Java 1.4 end ##

    /**
     * Roll back a transaction.
     *
     * @param xid the transaction id
     * @throws XAException
     */
//## Java 1.4 begin ##
    public void rollback(Xid xid) throws XAException {
        if (isDebugEnabled()) {
            debugCode("rollback("+quoteXid(xid)+");");
        }
        try {
            physicalConn.rollback();
            physicalConn.setAutoCommit(true);
        } catch (SQLException e) {
            throw convertException(e);
        }
        currentTransaction = null;
    }
//## Java 1.4 end ##

    /**
     * End a transaction.
     *
     * @param xid the transaction id
     * @param flags TMSUCCESS, TMFAIL, or TMSUSPEND
     * @throws XAException
     */
//## Java 1.4 begin ##
    public void end(Xid xid, int flags) throws XAException {
        if (isDebugEnabled()) {
            debugCode("end("+quoteXid(xid)+", "+quoteFlags(flags)+");");
        }
        // TODO transaction end: implement this method
        if (flags == TMSUSPEND) {
            return;
        }
        if (!currentTransaction.equals(xid)) {
            throw new XAException(XAException.XAER_OUTSIDE);
        }
    }
//## Java 1.4 end ##

    /**
     * Start or continue to work on a transaction.
     *
     * @param xid the transaction id
     * @param flags TMNOFLAGS, TMJOIN, or TMRESUME
     * @throws XAException
     */
//## Java 1.4 begin ##
    public void start(Xid xid, int flags) throws XAException {
        if (isDebugEnabled()) {
            debugCode("start("+quoteXid(xid)+", "+quoteFlags(flags)+");");
        }
        if (flags == TMRESUME) {
            return;
        }
        if (flags == TMJOIN) {
            if (currentTransaction != null && !currentTransaction.equals(xid)) {
                throw new XAException(XAException.XAER_RMERR);
            }
        } else if (currentTransaction != null) {
            throw new XAException(XAException.XAER_NOTA);
        }
        try {
            physicalConn.setAutoCommit(false);
        } catch (SQLException e) {
            throw convertException(e);
        }
        currentTransaction = xid;
    }
//## Java 1.4 end ##

    /**
     * Commit a transaction.
     *
     * @param xid the transaction id
     * @param onePhase use a one-phase protocol if true
     * @throws XAException
     */
//## Java 1.4 begin ##
    public void commit(Xid xid, boolean onePhase) throws XAException {
        if (isDebugEnabled()) {
            debugCode("commit("+quoteXid(xid)+", "+onePhase+");");
        }
        Statement stat = null;
        try {
            if (onePhase) {
                physicalConn.commit();
            } else {
                stat = physicalConn.createStatement();
                stat.execute("COMMIT TRANSACTION TX_" + currentTransactionId);
            }
            physicalConn.setAutoCommit(true);
        } catch (SQLException e) {
            throw convertException(e);
        } finally {
            JdbcUtils.closeSilently(stat);
        }
        currentTransaction = null;
    }
//## Java 1.4 end ##

    /**
     * [Not supported] Add a statement event listener.
     *
     * @param listener the new statement event listener
     */
//## Java 1.6 begin ##
    public void addStatementEventListener(StatementEventListener listener) {
        throw new UnsupportedOperationException();
    }
//## Java 1.6 end ##

    /**
     * [Not supported] Remove a statement event listener.
     *
     * @param listener the statement event listener
     */
//## Java 1.6 begin ##
    public void removeStatementEventListener(StatementEventListener listener) {
        throw new UnsupportedOperationException();
    }
//## Java 1.6 end ##

    /**
     * INTERNAL
     */
//## Java 1.4 begin ##
    public String toString() {
        return getTraceObjectName() + ": url=" + url + " user=" + user;
    }

    private XAException convertException(SQLException e) {
        XAException xa = new XAException(e.getMessage());
        xa.initCause(e);
        return xa;
    }

    private String quoteXid(Xid xid) {
        StringBuilder buff = new StringBuilder();
        buff.append("\"f:").
            append(xid.getFormatId()).
            append(",bq:").
            append(StringUtils.convertBytesToString(xid.getBranchQualifier())).
            append(",gx:").
            append(StringUtils.convertBytesToString(xid.getGlobalTransactionId())).
            append(",c:").
            append(xid.getClass().getName()).
            append("\"");
        return buff.toString();
    }

    private String quoteFlags(int flags) {
        StringBuilder buff = new StringBuilder();
        if ((flags & XAResource.TMENDRSCAN) != 0) {
            buff.append("|XAResource.TMENDRSCAN");
        }
        if ((flags & XAResource.TMFAIL) != 0) {
            buff.append("|XAResource.TMFAIL");
        }
        if ((flags & XAResource.TMJOIN) != 0) {
            buff.append("|XAResource.TMJOIN");
        }
        if ((flags & XAResource.TMONEPHASE) != 0) {
            buff.append("|XAResource.TMONEPHASE");
        }
        if ((flags & XAResource.TMRESUME) != 0) {
            buff.append("|XAResource.TMRESUME");
        }
        if ((flags & XAResource.TMSTARTRSCAN) != 0) {
            buff.append("|XAResource.TMSTARTRSCAN");
        }
        if ((flags & XAResource.TMSUCCESS) != 0) {
            buff.append("|XAResource.TMSUCCESS");
        }
        if ((flags & XAResource.TMSUSPEND) != 0) {
            buff.append("|XAResource.TMSUSPEND");
        }
        if ((flags & XAResource.XA_RDONLY) != 0) {
            buff.append("|XAResource.XA_RDONLY");
        }
        if (buff.length() == 0) {
            buff.append("|XAResource.TMNOFLAGS");
        }
        return buff.toString().substring(1);
    }

    private void checkOpen() throws XAException {
        if (physicalConn == null) {
            throw new XAException(XAException.XAER_RMERR);
        }
    }
//## Java 1.4 end ##

    /**
     * A pooled connection.
     */
    class PooledJdbcConnection extends JdbcConnection {

        private boolean isClosed;

        public PooledJdbcConnection(JdbcConnection conn) {
            super(conn);
            openStackTrace = new Exception("Stack Trace");
        }

        public synchronized void close() throws SQLException {
            if (!isClosed) {
                rollback();
                setAutoCommit(true);
                closedHandle();
                isClosed = true;
            }
        }

        public synchronized boolean isClosed() throws SQLException {
            return isClosed || super.isClosed();
        }

        protected synchronized void checkClosed(boolean write) throws SQLException {
            if (isClosed) {
                throw DbException.get(ErrorCode.OBJECT_CLOSED);
            }
            super.checkClosed(write);
        }

        protected void finalize() {
            // don't call super.finalize because
            // this should print its own stack trace,
            // and would close the connection
            if (!SysProperties.runFinalize) {
                return;
            }
            Trace trace = getTrace();
            try {
                if (!isClosed()) {
                    trace.error("Pooled connection not closed", openStackTrace);
                    JdbcXAConnection.this.close();
                }
            } catch (SQLException e) {
                trace.debug("finalize", e);
            }
        }

    }

}
