/*
  Copyright (c) 2005, 2013, 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 FLOSS 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 testsuite.simple;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.rmi.server.UID;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Savepoint;

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

import testsuite.BaseTestCase;

import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource;
import com.mysql.jdbc.jdbc2.optional.MysqlXid;

/**
 * Unit tests for our XA implementation.
 * 
 * @version $Id: $
 */
public class XATest extends BaseTestCase {
    MysqlXADataSource xaDs;
    
	public XATest(String name) {
		super(name);
		
		this.xaDs = new MysqlXADataSource();
		this.xaDs.setUrl(BaseTestCase.dbUrl);
		this.xaDs.setRollbackOnPooledClose(true);
	}

	/**
	 * Tests that simple distributed transaction processing works as expected.
	 * 
	 * @throws Exception
	 *             if the test fails.
	 */
	public void testCoordination() throws Exception {
		if (!versionMeetsMinimum(5, 0)) {
			return;
		}
		
		createTable("testCoordination", "(field1 int) ENGINE=InnoDB");
		
		Connection conn1 = null;
		Connection conn2 = null;
		XAConnection xaConn1 = null;
		XAConnection xaConn2 = null;
		
		try {
			xaConn1 = getXAConnection();
			XAResource xaRes1 = xaConn1.getXAResource();
			conn1 = xaConn1.getConnection();
			
			xaConn2 = getXAConnection();
			XAResource xaRes2 = xaConn2.getXAResource();
			conn2 = xaConn2.getConnection();
			
			Xid xid1 = createXid();
			Xid xid2 = createXid(xid1);
			
			xaRes1.start(xid1, XAResource.TMNOFLAGS);
			xaRes2.start(xid2, XAResource.TMNOFLAGS);
			conn1.createStatement().executeUpdate("INSERT INTO testCoordination VALUES (1)");
			conn2.createStatement().executeUpdate("INSERT INTO testCoordination VALUES (2)");
			xaRes1.end(xid1, XAResource.TMSUCCESS);
			xaRes2.end(xid2, XAResource.TMSUCCESS);
			
			xaRes1.prepare(xid1);
			xaRes2.prepare(xid2);
			
			xaRes1.commit(xid1, false);
			xaRes2.commit(xid2, false);
			
			this.rs = this.stmt.executeQuery("SELECT field1 FROM testCoordination ORDER BY field1");
			
			assertTrue(this.rs.next());
			assertEquals(1, this.rs.getInt(1));
			
			assertTrue(this.rs.next());
			assertEquals(2, this.rs.getInt(1));
			
			this.stmt.executeUpdate("TRUNCATE TABLE testCoordination");
			
			//
			// Now test rollback
			//
			
			xid1 = createXid();
			xid2 = createXid(xid1);
			
			xaRes1.start(xid1, XAResource.TMNOFLAGS);
			xaRes2.start(xid2, XAResource.TMNOFLAGS);
			conn1.createStatement().executeUpdate("INSERT INTO testCoordination VALUES (1)");
			
			// ensure visibility
			assertEquals("1", getSingleIndexedValueWithQuery(conn1, 1, "SELECT field1 FROM testCoordination WHERE field1=1").toString());
			
			conn2.createStatement().executeUpdate("INSERT INTO testCoordination VALUES (2)");
			
			// ensure visibility
			assertEquals("2", getSingleIndexedValueWithQuery(conn2, 1, "SELECT field1 FROM testCoordination WHERE field1=2").toString());
			
			xaRes1.end(xid1, XAResource.TMSUCCESS);
			xaRes2.end(xid2, XAResource.TMSUCCESS);
			
			xaRes1.prepare(xid1);
			xaRes2.prepare(xid2);
			
			xaRes1.rollback(xid1);
			xaRes2.rollback(xid2);
			
			this.rs = this.stmt.executeQuery("SELECT field1 FROM testCoordination ORDER BY field1");
			
			assertTrue(!this.rs.next());
		} finally {
			if (conn1 != null) {
				conn1.close();
			}
			
			if (conn2 != null) {
				conn2.close();
			}
			
			if (xaConn1 != null) {
				xaConn1.close();
			}
			
			if (xaConn2 != null) {
				xaConn2.close();
			}
		}
	}
	
	protected XAConnection getXAConnection() throws Exception {
		return this.xaDs.getXAConnection();
	}
	
	/**
	 * Tests that XA RECOVER works as expected.
	 * 
	 * @throws Exception
	 *             if test fails
	 */
	public void testRecover() throws Exception {
		if (!versionMeetsMinimum(5, 0)) {
			return;
		}
		
		// BUG#14670465 fix broke this functionality in 5.7.1 - 5.7.2
		if (versionMeetsMinimum(5, 7, 1) && !versionMeetsMinimum(5, 7, 3)) {
			return;
		}

		XAConnection xaConn = null, recoverConn = null;
		
		try {			
			xaConn = getXAConnection();
			
			Connection c = xaConn.getConnection();
			Xid xid = createXid();
			
			XAResource xaRes = xaConn.getXAResource();
			xaRes.start(xid, XAResource.TMNOFLAGS);
			c.createStatement().executeQuery("SELECT 1");
			xaRes.end(xid, XAResource.TMSUCCESS);
			xaRes.prepare(xid);
			
			// Now try and recover
			recoverConn = getXAConnection();
			
			XAResource recoverRes = recoverConn.getXAResource();
			
			Xid[] recoveredXids = recoverRes.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN);
			
			assertTrue(recoveredXids != null);
			assertTrue(recoveredXids.length > 0);
			
			boolean xidFound = false;
			
			for (int i = 0; i < recoveredXids.length; i++) {
				if (recoveredXids[i] != null &&
					recoveredXids[i].equals(xid)) {
					xidFound = true;
					
					break;
				}
			}
			
			assertTrue(xidFound);

			recoverRes = recoverConn.getXAResource();

			recoveredXids = recoverRes.recover(XAResource.TMSTARTRSCAN);

			assertTrue(recoveredXids != null);
			assertTrue(recoveredXids.length > 0);

			xidFound = false;

			for (int i = 0; i < recoveredXids.length; i++) {
				if (recoveredXids[i] != null &&
						recoveredXids[i].equals(xid)) {
					xidFound = true;

					break;
				}
			}

			assertTrue(xidFound);
				
			// Test flags
			recoverRes.recover(XAResource.TMSTARTRSCAN);
			recoverRes.recover(XAResource.TMENDRSCAN);
			recoverRes.recover(XAResource.TMSTARTRSCAN | XAResource.TMENDRSCAN);
			
			// This should fail
			try {
				recoverRes.recover(XAResource.TMSUCCESS);
				fail("XAException should have been thrown");
			} catch (XAException xaEx) {
				assertEquals(XAException.XAER_INVAL, xaEx.errorCode);
			}
		} finally {
			if (xaConn != null) {
				xaConn.close();
			}
			
			if (recoverConn != null) {
				recoverConn.close();
			}
		}
	}

	/**
	 * Tests operation of local transactions on XAConnections when global
	 * transactions are in or not in progress (follows from BUG#17401).
	 * 
	 * @throws Exception
	 *             if the testcase fails
	 */
	public void testLocalTransaction() throws Exception {

		if (!versionMeetsMinimum(5, 0) || isRunningOnJdk131()) {
			return;
		}

		createTable("testLocalTransaction", "(field1 int) ENGINE=InnoDB");

		Connection conn1 = null;

		XAConnection xaConn1 = null;

		try {
			xaConn1 = getXAConnection();
			XAResource xaRes1 = xaConn1.getXAResource();
			conn1 = xaConn1.getConnection();
			assertEquals(true, conn1.getAutoCommit());
			conn1.setAutoCommit(true);
			conn1.createStatement().executeUpdate(
					"INSERT INTO testLocalTransaction VALUES (1)");
			assertEquals("1", getSingleIndexedValueWithQuery(conn1, 1,
					"SELECT field1 FROM testLocalTransaction").toString());

			conn1.createStatement().executeUpdate(
					"TRUNCATE TABLE testLocalTransaction");
			conn1.setAutoCommit(false);
			conn1.createStatement().executeUpdate(
					"INSERT INTO testLocalTransaction VALUES (2)");
			assertEquals("2", getSingleIndexedValueWithQuery(conn1, 1,
					"SELECT field1 FROM testLocalTransaction").toString());
			conn1.rollback();
			assertEquals(0, getRowCount("testLocalTransaction"));

			conn1.createStatement().executeUpdate(
					"INSERT INTO testLocalTransaction VALUES (3)");
			assertEquals("3", getSingleIndexedValueWithQuery(conn1, 1,
					"SELECT field1 FROM testLocalTransaction").toString());
			conn1.commit();
			assertEquals("3", getSingleIndexedValueWithQuery(conn1, 1,
					"SELECT field1 FROM testLocalTransaction").toString());
			conn1.commit();
			
			Savepoint sp = conn1.setSavepoint();
			conn1.rollback(sp);
			sp = conn1.setSavepoint("abcd");
			conn1.rollback(sp);
			Savepoint spSaved = sp;
			
			Xid xid = createXid();
			xaRes1.start(xid, XAResource.TMNOFLAGS);

			try {
				try {
					conn1.setAutoCommit(true);
				} catch (SQLException sqlEx) {
					// we expect an exception here
					assertEquals("2D000", sqlEx.getSQLState());
				}

				try {
					conn1.commit();
				} catch (SQLException sqlEx) {
					// we expect an exception here
					assertEquals("2D000", sqlEx.getSQLState());
				}

				try {
					conn1.rollback();
				} catch (SQLException sqlEx) {
					// we expect an exception here
					assertEquals("2D000", sqlEx.getSQLState());
				}
				
				try {
					sp = conn1.setSavepoint();
				} catch (SQLException sqlEx) {
					// we expect an exception here
					assertEquals("2D000", sqlEx.getSQLState());
				}
			
				try {
					conn1.rollback(spSaved);
				} catch (SQLException sqlEx) {
					// we expect an exception here
					assertEquals("2D000", sqlEx.getSQLState());
				}
				
				try {
					sp = conn1.setSavepoint("abcd");
				} catch (SQLException sqlEx) {
					// we expect an exception here
					assertEquals("2D000", sqlEx.getSQLState());
				}
				
				try {
					conn1.rollback(spSaved);
				} catch (SQLException sqlEx) {
					// we expect an exception here
					assertEquals("2D000", sqlEx.getSQLState());
				}
			} finally {
				xaRes1.forget(xid);
			}
		} finally {
			if (xaConn1 != null) {
				try {
					xaConn1.close();
				} catch (SQLException sqlEx) {
					// this is just busted in the server right now
				}
			}
		}
	}
	
	public void testSuspendableTx() throws Exception {
		if (!versionMeetsMinimum(5, 0) || isRunningOnJdk131()) {
			return;
		}
		
		Connection conn1 = null;

		MysqlXADataSource suspXaDs = new MysqlXADataSource();
		suspXaDs.setUrl(BaseTestCase.dbUrl);
		suspXaDs.setPinGlobalTxToPhysicalConnection(true);
		suspXaDs.setRollbackOnPooledClose(true);
		
		XAConnection xaConn1 = null;
		
		Xid xid = createXid();
		
		try {
			/*
			  	-- works using RESUME
				xa start 0x123,0x456;
				select * from foo;
				xa end 0x123,0x456;
				xa start 0x123,0x456 resume;
				select * from foo;
				xa end 0x123,0x456;
				xa commit 0x123,0x456 one phase;
			 */
			
			xaConn1 = suspXaDs.getXAConnection();
			XAResource xaRes1 = xaConn1.getXAResource();
			conn1 = xaConn1.getConnection();
			xaRes1.start(xid, XAResource.TMNOFLAGS);
			conn1.createStatement().executeQuery("SELECT 1");
			xaRes1.end(xid, XAResource.TMSUCCESS);
			xaRes1.start(xid, XAResource.TMRESUME);
			conn1.createStatement().executeQuery("SELECT 1");
			xaRes1.end(xid, XAResource.TMSUCCESS);
			xaRes1.commit(xid, true);
			
			xaConn1.close();
			
			/*

				-- fails using JOIN
				xa start 0x123,0x456;
				select * from foo;
				xa end 0x123,0x456;
				xa start 0x123,0x456 join;
				select * from foo;
				xa end 0x123,0x456;
				xa commit 0x123,0x456 one phase;
				*/
		
			xaConn1 = suspXaDs.getXAConnection();
			xaRes1 = xaConn1.getXAResource();
			conn1 = xaConn1.getConnection();
			xaRes1.start(xid, XAResource.TMNOFLAGS);
			conn1.createStatement().executeQuery("SELECT 1");
			xaRes1.end(xid, XAResource.TMSUCCESS);
			xaRes1.start(xid, XAResource.TMJOIN);
			conn1.createStatement().executeQuery("SELECT 1");
			xaRes1.end(xid, XAResource.TMSUCCESS);
			xaRes1.commit(xid, true);
		} finally {
			if (xaConn1 != null) {
				xaConn1.close();
			}
		}
	}

	private Xid createXid() throws IOException {
		ByteArrayOutputStream gtridOut = new ByteArrayOutputStream();
		DataOutputStream dataOut = new DataOutputStream(gtridOut);
		new UID().write(dataOut);
		
		final byte[] gtrid = gtridOut.toByteArray();
		
		ByteArrayOutputStream bqualOut = new ByteArrayOutputStream();
		dataOut = new DataOutputStream(bqualOut);
		
		new UID().write(dataOut);
		
		final byte[] bqual = bqualOut.toByteArray();
		
		Xid xid = new MysqlXid(gtrid, bqual, 3306);
		return xid;
	}

	private Xid createXid(Xid xidToBranch) throws IOException {
		ByteArrayOutputStream bqualOut = new ByteArrayOutputStream();
		DataOutputStream dataOut = new DataOutputStream(bqualOut);
		
		new UID().write(dataOut);
		
		final byte[] bqual = bqualOut.toByteArray();
		
		Xid xid = new MysqlXid(xidToBranch.getGlobalTransactionId(), bqual, 3306);
		
		return xid;
	}
}
