/*
 *
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */

package org.jboss.cache.transaction;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.jboss.cache.CacheException;
import org.jboss.cache.DataNode;
import org.jboss.cache.Fqn;
import org.jboss.cache.GlobalTransaction;
import org.jboss.cache.TreeCache;
import org.jboss.cache.lock.IdentityLock;
import org.jboss.cache.lock.IsolationLevel;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.UserTransaction;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * Tests transactional access to a local TreeCache.
 * Note: we use DummpyTranasctionManager to replace jta
 *
 * @version $Id: TransactionTest.java 3628 2007-03-05 19:15:56Z msurtani $
 */
public class TransactionTest extends TestCase {
   TreeCache cache=null;
   UserTransaction tx=null;
   Properties p=null;
   String old_factory=null;
   final String FACTORY="org.jboss.cache.transaction.DummyContextFactory";
   Exception thread_ex;


   public TransactionTest(String name) {
      super(name);
   }

   public void setUp() throws Exception {
      super.setUp();
      old_factory=System.getProperty(Context.INITIAL_CONTEXT_FACTORY);
      System.setProperty(Context.INITIAL_CONTEXT_FACTORY, FACTORY);
      DummyTransactionManager.getInstance();
      if(p == null) {
         p=new Properties();
         p.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.cache.transaction.DummyContextFactory");
      }

      tx=(UserTransaction)new InitialContext(p).lookup("UserTransaction");
      cache=new TreeCache("test", null, 10000);
      cache.setTransactionManagerLookupClass("org.jboss.cache.DummyTransactionManagerLookup");
      cache.setIsolationLevel(IsolationLevel.SERIALIZABLE);

      cache.setLockParentForChildInsertRemove(true); // these tests are written with this assumption in mind
      cache.createService();
      cache.startService();
      thread_ex=null;
   }

   public void tearDown() throws Exception {
      super.tearDown();
      if(cache != null)
         cache.stopService();

      // BW. kind of a hack to destroy jndi binding and thread local tx before next run.
      DummyTransactionManager.destroy();
      if(old_factory != null) {
         System.setProperty(Context.INITIAL_CONTEXT_FACTORY, old_factory);
         old_factory=null;
      }

      if(tx != null) {
         try {
            tx.rollback();
         }
         catch(Throwable t) {
         }
         tx=null;
      }
   }


   public void testPutTx() {
      try {
         tx.begin();
         cache.put("/a/b/c", "age", new Integer(38));
         // the tx interceptor should know that we're in the same tx.
         assertEquals(new Integer(38), cache.get("/a/b/c", "age"));

         cache.put("/a/b/c", "age", new Integer(39));
         tx.commit();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertEquals(new Integer(39), cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         fail(t.toString());
      }
   }


   public void testRollbackTx1() {
      try {
         tx.begin();
         cache.put("/a/b/c", "age", new Integer(38));
         cache.put("/a/b/c", "age", new Integer(39));
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testGetAfterRemovalRollback() throws Exception
   {
      cache.put("/a/b", null);
      assertTrue(cache.exists("/a/b"));
      tx.begin();
      cache.remove("/a/b");
      assertFalse(cache.exists("/a/b"));
      tx.rollback();
      assertTrue(cache.exists("/a/b"));
      assertEquals(0, cache.getNumberOfLocksHeld());
      // new tx in new thread
      Thread th = new Thread()
      {
         public void run()
         {
            try
            {
               cache.getTransactionManager().begin();
               assertNotNull(cache.get("/a/b"));
               cache.getTransactionManager().rollback();
            }
            catch (Exception e)
            {
               e.printStackTrace();
               fail("Caught exception");
            }
         }
      };

      th.start();
      th.join();

      assertEquals(0, cache.getNumberOfLocksHeld());
   }

   public void testRollbackTx2() {
      try {
         tx.begin();
         cache.put("/a/b/c", "age", new Integer(38));
         cache.remove("/a/b/c", "age");
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testRollbackTx2a() {
      try {
          System.out.println("locks " + cache.printLockInfo());
         cache.put("/a/b/c", "age", new Integer(38));
          System.out.println("locks " + cache.printLockInfo());
         tx.begin();
         cache.remove("/a/b/c", "age");
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertEquals(new Integer(38), cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testRollbackTx3() {
      try {
         java.util.Map map1=new java.util.HashMap();
         map1.put("age", new Integer(38));
         java.util.Map map2=new java.util.HashMap();
         map2.put("age", new Integer(39));
         tx.begin();
         cache.put("/a/b/c", map1);
         cache.put("/a/b/c", map2);
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testRollbackTx4() {
      try {
         Map map=new HashMap();
         map.put("age", new Integer(38));
         tx.begin();
         cache.put("/a/b/c", map);
         cache.remove("/a/b/c");
         tx.rollback();

         // This test is done outside the TX, it wouldn't work if someone else
         // modified "age". This works because we're the only TX running.
         assertNull(cache.get("/a/b/c", "age"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeCreationRollback() {
      try {
         tx.begin();
         System.out.println("initial state:\n" + cache);
         cache.put("/bela/ban", null);
         System.out.println("after put():\n" + cache);
         tx.rollback();
         System.out.println("after rollback():\n" + cache);

         assertNull("node should be not existent", cache.get("/bela/ban"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeCreationRollback2() {
      try {
         cache.put("/bela/ban", null);
         tx.begin();
         cache.put("/bela/ban/michelle", null);
         tx.rollback();
         assertNotNull("node should be not null", cache.get("/bela/ban"));
         assertNull("node should be not existent", cache.get("/bela/ban/michelle"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeDeletionRollback() {
      try {
         cache.put("/a/b/c", null);
         tx.begin();
         cache.remove("/a/b/c");
         assertNull(cache.get("/a/b/c"));
         cache.remove("/a/b");
         assertNull(cache.get("/a/b"));
         cache.remove("/a");
         assertNull(cache.get("/a"));
         tx.rollback();
         assertNotNull(cache.get("/a/b/c"));
         assertNotNull(cache.get("/a/b"));
         assertNotNull(cache.get("/a"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testNodeDeletionRollback2() {
      try {
         cache.put("/a/b/c", null);
         cache.put("/a/b/c1", null);
         cache.put("/a/b/c2", null);
         tx.begin();
         cache.remove("/a");
         assertNull(cache.get("/a/b/c"));
         assertNull(cache.get("/a/b/c1"));
         assertNull(cache.get("/a/b/c2"));
         assertNull(cache.get("/a/b"));
         assertNull(cache.get("/a"));
         Set children=cache.getChildrenNames("/a/b");
         assertNull(children);
         children=cache.getChildrenNames("/a");
         assertNull(children);
         tx.rollback();
         assertNotNull(cache.get("/a"));
         assertNotNull(cache.get("/a/b"));
         assertNotNull(cache.get("/a/b/c"));
         assertNotNull(cache.get("/a/b/c1"));
         assertNotNull(cache.get("/a/b/c2"));
         children=cache.getChildrenNames("/a/b");
         assertEquals(3, children.size());
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }



    public void testNodeCreation() throws Exception {
      GlobalTransaction gtx;
      cache.put("/a/b", null);
      tx.begin();
      gtx=cache.getCurrentTransaction();
      cache.put("/a/b/c", null);
      assertLocked(gtx, "/a", false);
      assertLocked(gtx, "/a/b", true);
      assertLocked(gtx, "/a/b/c", true);
      System.out.println("locks: " + cache.printLockInfo());
   }


    public void testNodeCreation2() throws Exception {
      GlobalTransaction gtx;
      tx.begin();
      gtx=cache.getCurrentTransaction();
      cache.put("/a/b/c", null);
      assertLocked(gtx, "/a", true);
      assertLocked(gtx, "/a/b", true);
      assertLocked(gtx, "/a/b/c", true);
      System.out.println("locks: " + cache.printLockInfo());
   }


    public void testNodeRemoval() {
      GlobalTransaction gtx;
      try {
         cache.put("/a/b/c", null);
         tx.begin();
         gtx=cache.getCurrentTransaction();
         cache.remove("/a/b/c"); // need to remove the node, not just the data in the node.
         assertLocked(gtx, "/a", false);
         assertLocked(gtx, "/a/b", true);
         assertLocked(gtx, "/a/b/c", true);
         System.out.println("locks: " + cache.printLockInfo());
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testNodeRemoval2() {
     GlobalTransaction gtx;
     try {
        cache.put("/a/b/c", null);
        tx.begin();
        gtx=cache.getCurrentTransaction();
        cache.remove("/a/b"); // need to remove the node, not just the data in the node.
        assertLocked(gtx, "/a", true);
        assertLocked(gtx, "/a/b", true);
        assertLocked(gtx, "/a/b/c", true);
        System.out.println("locks: " + cache.printLockInfo());
     }
     catch(Throwable t) {
        t.printStackTrace();
        fail(t.toString());
     }
  }

  public void testIntermediateNodeCreationOnWrite() throws Exception
  {
     cache.put("/a", null);
     tx.begin();
     cache.put("/a/b/c", null);
     // expecting WLs on /a, /a/b and /a/b/c.
     GlobalTransaction gtx=cache.getCurrentTransaction();
     assertLocked(gtx, "/a", true);
     assertLocked(gtx, "/a/b", true);
     assertLocked(gtx, "/a/b/c", true);
     tx.rollback();

  }

  public void testIntermediateNodeCreationOnRead() throws Exception
  {
     cache.put("/a", null);
     tx.begin();
     cache.get("/a/b/c");

     // expecting RLs on /, /a
     // /a/b, /a/b/c should NOT be created!
     GlobalTransaction gtx=cache.getCurrentTransaction();
     assertLocked(gtx, "/", false);
     assertLocked(gtx, "/a", false);
     assertNull("/a/b should not exist", cache.peek(Fqn.fromString("/a/b")));
     assertNull("/a/b/c should not exist", cache.peek(Fqn.fromString("/a/b/c")));
     tx.rollback();
     assertNull("/a/b should not exist", cache.peek(Fqn.fromString("/a/b")));
     assertNull("/a/b/c should not exist", cache.peek(Fqn.fromString("/a/b/c")));

  }


   public void testIntermediateNodeCreationOnRemove() throws Exception
   {
      cache.put("/a", null);
      tx.begin();
      cache.remove("/a/b/c");

      // expecting RLs on /, /a
      // /a/b, /a/b/c should NOT be created!
      GlobalTransaction gtx=cache.getCurrentTransaction();
      assertLocked(gtx, "/", false);
      assertLocked(gtx, "/a", false);
      assertNull("/a/b should not exist", cache.peek(Fqn.fromString("/a/b")));
      assertNull("/a/b/c should not exist", cache.peek(Fqn.fromString("/a/b/c")));
      tx.rollback();
      assertNull("/a/b should not exist", cache.peek(Fqn.fromString("/a/b")));
      assertNull("/a/b/c should not exist", cache.peek(Fqn.fromString("/a/b/c")));

   }


   public void testNodeDeletionRollback3() throws Exception{
      GlobalTransaction gtx;
      cache.put("/a/b/c1", null);

      tx.begin();
      gtx=cache.getCurrentTransaction();
      cache.put("/a/b/c1", null);
      assertLocked(gtx, "/a", false);
      assertLocked(gtx, "/a/b", false);
      assertLocked(gtx, "/a/b/c1", true);

      cache.put("/a/b/c2", null);
      assertLocked(gtx, "/a/b", true);
      assertLocked(gtx, "/a/b/c2", true);

      cache.put("/a/b/c3", null);
      cache.put("/a/b/c1/one", null);
      assertLocked(gtx, "/a/b/c1", true);
      assertLocked(gtx, "/a/b/c1/one", true);

      cache.put("/a/b/c1/two", null);
      cache.put("/a/b/c1/one/1", null);
      assertLocked(gtx, "/a/b/c1", true);
      assertLocked(gtx, "/a/b/c1/one", true);
      assertLocked(gtx, "/a/b/c1/one/1", true);

      cache.put("/a/b/c1/two/2/3/4", null);
      assertLocked(gtx, "/a/b/c1", true);
      assertLocked(gtx, "/a/b/c1/two", true);
      assertLocked(gtx, "/a/b/c1/two/2", true);
      assertLocked(gtx, "/a/b/c1/two/2/3", true);
      assertLocked(gtx, "/a/b/c1/two/2/3/4", true);

      System.out.println("locks: " + cache.printLockInfo());

      cache.remove("/a/b");
      tx.rollback();
      assertTrue(cache.getChildrenNames("/a/b/c1").isEmpty());
      Set cn = cache.getChildrenNames("/a/b");
      assertEquals(1, cn.size());
      assertEquals("c1", cn.iterator().next());
   }

   public void testDoubleLocks() throws Exception{
      tx.begin();
      GlobalTransaction gtx = cache.getCurrentTransaction();
      cache.put("/a/b/c", null);
      cache.put("/a/b/c", null);

      DataNode n=cache.get("/a");
      IdentityLock lock=n.getLock();
      int num=lock.getReaderOwners().size();
      assertEquals(0, num);
      // make sure this is write locked.
      assertLocked(gtx, "/a", true);

      n=cache.get("/a/b");
      lock=n.getLock();
      num=lock.getReaderOwners().size();
      assertEquals(0, num);
      // make sure this is write locked.
      assertLocked(gtx, "/a/b", true);

      n=cache.get("/a/b/c");
      lock=n.getLock();
      num=lock.getReaderOwners().size();
      assertEquals(0, num);
      // make sure this is write locked.
      assertLocked(gtx, "/a/b/c", true);

      tx.rollback();
      assertEquals(0, cache.getNumberOfLocksHeld());
   }

   private void assertLocked(Object owner, String fqn, boolean write_locked) throws Exception{
      DataNode n=cache.peek(Fqn.fromString(fqn));
      IdentityLock lock=n.getLock();
      if(owner == null)
         owner=Thread.currentThread();
      assertTrue("node " + fqn + " is not locked", lock.isLocked());
      if(write_locked) {
         assertTrue("node " + fqn + " is not write-locked"  + (lock.isReadLocked() ? " but is read-locked instead!" : "!"), lock.isWriteLocked());
      }
      else {
         assertTrue("node " + fqn + " is not read-locked" + (lock.isWriteLocked() ? " but is write-locked instead!" : "!"), lock.isReadLocked());
      }
      assertTrue("owner " + owner + "is not owner", lock.isOwner(owner));
   }

   public void testConcurrentNodeAccessOnRemovalWithTx() throws Exception
   {
      cache.put("/a/b/c", null);
      tx.begin();
      cache.remove("/a/b/c");
      // this node should now be locked.
      Transaction t = cache.getTransactionManager().suspend();
      Transaction t2 = null;
      try
      {
         System.out.println(cache.printLockInfo());
         // start a new tx
         cache.getTransactionManager().begin();
         t2 = cache.getTransactionManager().getTransaction();
         cache.get("/a/b/c"); // should fail
         t2.commit();
         fail("Should not be able to get a hold of /a/b/c until the deleting tx completes");
      }
      catch (Exception e)
      {
         // expected
         t2.commit();
      }

      cache.getTransactionManager().resume(t);
      tx.rollback();

      assertNotNull(cache.get("/a/b/c"));
      assertEquals(0, cache.getNumberOfLocksHeld());
   }

   public void testConcurrentNodeAccessOnRemovalWithoutTx() throws Exception
   {
      cache.put("/a/b/c", null);
      tx.begin();
      cache.remove("/a/b/c");
      // this node should now be locked.
      Transaction t = cache.getTransactionManager().suspend();
      Thread th = new Thread(){
         public void run(){
            try
            {
               System.out.println(cache.printLockInfo());
               cache.get("/a/b/c"); // should fail

               fail("Should not be able to get a hold of /a/b/c until the deleting tx completes");
            }
            catch (Exception e)
            {
               // expected
            }
         }
      };

      th.start();
      th.join();

      cache.getTransactionManager().resume(t);
      tx.rollback();

      assertNotNull(cache.get("/a/b/c"));
      assertEquals(0, cache.getNumberOfLocksHeld());
   }



   public void testRemove() throws CacheException, SystemException, NotSupportedException, HeuristicMixedException, HeuristicRollbackException, RollbackException {
      cache.put("/a/b/c", null);
      cache.put("/a/b/c/1", null);
      cache.put("/a/b/c/2", null);
      cache.put("/a/b/c/3", null);
      cache.put("/a/b/c/3/a/b/c", null);

      assertEquals(0, cache.getNumberOfLocksHeld());
      assertEquals(0, cache.getLockTable().size());

      tx.begin();
      cache.remove("/a/b/c");
      System.out.println("locks held (after removing /a/b/c): \n" + cache.printLockInfo());
      // this used to test for 2 locks held.  After the fixes for JBCACHE-875 however, 2 more locks are acquired - for the root node as well as the deleted node.
      // and since we would lock all children of the deleted node as well, we have 10 locks here.
      assertEquals(10, cache.getNumberOfLocksHeld());
      tx.commit();
      System.out.println("locks held (after committing /a/b/c): \n" + cache.printLockInfo());
      assertEquals(0, cache.getNumberOfLocksHeld());
   }


   public void testRemoveAndRollback() throws CacheException, SystemException, NotSupportedException, HeuristicMixedException, HeuristicRollbackException, RollbackException {
      cache.put("/a/b/c", null);
      cache.put("/a/b/c/1", null);
      cache.put("/a/b/c/2", null);
      cache.put("/a/b/c/3", null);
      cache.put("/a/b/c/3/a/b/c", null);

      assertEquals(0, cache.getNumberOfLocksHeld());
      assertEquals(0, cache.getLockTable().size());

      tx.begin();
      System.out.println("locks held (before removing /a/b/c): \n" + cache.printLockInfo());
      cache.remove("/a/b/c");
      System.out.println("locks held (after removing /a/b/c): \n" + cache.printLockInfo());
      assertEquals(10, cache.getNumberOfLocksHeld());
      tx.rollback();
      System.out.println("locks held (after rollback): \n" + cache.printLockInfo());
      assertEquals(0, cache.getNumberOfLocksHeld());

      assertTrue(cache.exists("/a/b/c"));
      assertTrue(cache.exists("/a/b/c/1"));
      assertTrue(cache.exists("/a/b/c/2"));
      assertTrue(cache.exists("/a/b/c/3"));
      assertTrue(cache.exists("/a/b/c/3/a"));
      assertTrue(cache.exists("/a/b/c/3/a/b"));
      assertTrue(cache.exists("/a/b/c/3/a/b/c"));
   }


   public void testRemoveKeyRollback() throws CacheException, SystemException, NotSupportedException {
      cache.put("/bela/ban", "name", "Bela");
      tx.begin();
      cache.remove("/bela/ban", "name");
      assertNull(cache.get("/bela/ban", "name"));
      tx.rollback();
      assertEquals("Bela", cache.get("/bela/ban", "name"));
   }


   public void testRemoveKeyRollback2() {
      try {
         Map m=new HashMap();
         m.put("name", "Bela");
         m.put("id", new Integer(322649));
         cache.put("/bela/ban", m);
         tx.begin();
         cache.remove("/bela/ban", "name");
         assertNull(cache.get("/bela/ban", "name"));
         tx.rollback();
         assertEquals("Bela", cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   public void testRemoveKeyRollback3() {
      try {
         cache.put("/bela/ban", "name", "Bela");
         tx.begin();
         cache.put("/bela/ban", "name", "Michelle");
         cache.remove("/bela/ban", "name");
         assertNull(cache.get("/bela/ban", "name"));
         tx.rollback();
         assertEquals("Bela", cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }




   public void testDoubleRemovalOfSameData() {
      try {
         tx.begin();
         cache.put("/foo/1", "item", new Integer(1));
         assertEquals(cache.get("/foo/1", "item"), new Integer(1));
         cache.remove("/foo/1");
         assertNull(cache.get("/foo/1", "item"));
         cache.remove("/foo/1");
         assertNull(cache.get("/foo/1", "item"));
         tx.rollback();
         assertFalse(cache.exists("/foo/1"));
         assertNull(cache.get("/foo/1", "item"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   /**
    * put(Fqn, Map) with a previous null map
    */
   public void testPutDataRollback1() {
      try {
         cache.put("/bela/ban", null); // create a node /bela/ban with a null map
         tx.begin();
         Map m=new HashMap();
         m.put("name", "Bela");
         m.put("id", new Integer(322649));
         cache.put("/bela/ban", m);
         tx.rollback();

         DataNode n=cache.get("/bela/ban");
         if(n.getData() == null) return;
         assertEquals("map should be empty", 0, n.getData().size());
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }

   /**
    * put(Fqn, Map) with a previous non-null map
    */
   public void testputDataRollback2() {
      Map m1, m2;
      m1=new HashMap();
      m1.put("name", "Bela");
      m1.put("id", new Integer(322649));
      m2=new HashMap();
      m2.put("other", "bla");
      m2.put("name", "Michelle");

      try {
         cache.put("/bela/ban", m1);
         tx.begin();

         cache.put("/bela/ban", m2);
         Map tmp=cache.get("/bela/ban").getData();
         assertEquals(3, tmp.size());
         assertEquals("Michelle", tmp.get("name"));
         assertEquals(tmp.get("id"), new Integer(322649));
         assertEquals("bla", tmp.get("other"));
         tx.rollback();

         tmp=cache.get("/bela/ban").getData();
         assertEquals(2, tmp.size());
         assertEquals("Bela", tmp.get("name"));
         assertEquals(tmp.get("id"), new Integer(322649));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testPutRollback() {
      try {
         cache.put("/bela/ban", null); // /bela/ban needs to exist
         tx.begin();
         cache.put("/bela/ban", "name", "Bela");
         assertEquals("Bela", cache.get("/bela/ban", "name"));
         tx.rollback();
         assertNull(cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testPutRollback2() {
      try {
         cache.put("/bela/ban", "name", "Bela"); // /bela/ban needs to exist
         tx.begin();
         cache.put("/bela/ban", "name", "Michelle");
         assertEquals("Michelle", cache.get("/bela/ban", "name"));
         tx.rollback();
         assertEquals("Bela", cache.get("/bela/ban", "name"));
      }
      catch(Throwable t) {
         t.printStackTrace();
         fail(t.toString());
      }
   }


   public void testSimpleRollbackTransactions() throws Exception {
      final Fqn FQN=Fqn.fromString("/a/b/c");
      tx.begin();
      cache.put(FQN, "entry", "commit");
      tx.commit();

      tx.begin();
      cache.put(FQN, "entry", "rollback");
      cache.remove(FQN);
      tx.rollback();
      assertEquals("Node should keep the commited value", "commit", cache.get(FQN).get("entry"));

      tx.begin();
      cache.remove(FQN);
      cache.put(FQN, "entry", "rollback");
      tx.rollback();
      assertEquals("Node should keep the commited value", "commit", cache.get(FQN).get("entry"));
   }


   private Transaction startTransaction() throws Exception {
      DummyTransactionManager mgr=DummyTransactionManager.getInstance();
      mgr.begin();
      return mgr.getTransaction();
   }


   public void testConcurrentReadAndWriteAccess() throws Exception {
      cache.setIsolationLevel(IsolationLevel.REPEATABLE_READ);

      cache.put("/1/2/3/4", "foo", "bar"); // no TX, no locks held after put() returns


      class Reader extends Thread {
         Transaction thread_tx;

         public Reader() {
            super("Reader");
         }

         public void run() {
            try {
               thread_tx=startTransaction();
               log("acquiring RL");
               cache.get("/1/2/3", "foo"); // acquires RLs on all 3 nodes
               log("RL acquired successfully");
               sleep(2000);
               log("committing TX");
               thread_tx.commit(); // releases RLs
               log("committed TX");
            }
            catch(Exception e) {
               thread_ex=e;
            }
         }
      }

      class Writer extends Thread {
         Transaction thread_tx;

         public Writer() {
            super("Writer");
         }

         public void run() {
            try {
               sleep(500); // give the Reader a chance to acquire the RLs
               thread_tx=startTransaction();
               log("acquiring WL");
               cache.put("/1", "foo", "bar2"); // needs to acquired a WL on /1
               log("acquired WL successfully");
               log("committing TX");
               thread_tx.commit();
               log("committed TX");
            }
            catch(Exception e) {
               thread_ex=e;
            }
         }
      }

      Reader reader=new Reader();
      Writer writer=new Writer();
      reader.start();
      writer.start();
      reader.join();
      writer.join();
      if(thread_ex != null)
         throw thread_ex;
   }

   public void testRemoveAndGetInTx() throws Exception
   {
      Fqn A_B = Fqn.fromString("/a/b");
      Fqn A = Fqn.fromString("/a");

      cache.put(A_B, "k", "v");

      assertTrue(cache.exists(A_B));
      assertTrue(cache.exists(A));

      cache.getTransactionManager().begin();
      cache.remove(A);
      cache.get(A_B, "k");
      cache.getTransactionManager().commit();
   }

   public void testRemoveAndPutInTx() throws Exception
   {
      Fqn A_B = Fqn.fromString("/a/b");
      Fqn A = Fqn.fromString("/a");

      cache.put(A_B, "k", "v");

      assertTrue(cache.exists(A_B));
      assertTrue(cache.exists(A));

      cache.getTransactionManager().begin();
      cache.remove(A_B);
      cache.put(A_B, "k", "v2");
      cache.getTransactionManager().commit();

      assertTrue(cache.exists(A_B));
      assertTrue(cache.exists(A));

      assertEquals("v2", cache.get(A_B, "k"));
   }

   public void testRemoveParentAndPutInTx() throws Exception
   {
      Fqn A_B = Fqn.fromString("/a/b");
      Fqn A = Fqn.fromString("/a");

      cache.put(A_B, "k", "v");

      assertTrue(cache.exists(A_B));
      assertTrue(cache.exists(A));

      cache.getTransactionManager().begin();
      cache.remove(A);
      cache.put(A_B, "k", "v2");
      cache.getTransactionManager().commit();

      assertTrue(cache.exists(A_B));
      assertTrue(cache.exists(A));

      assertEquals("v2", cache.get(A_B, "k"));
   }

   public void testRemoveGrandParentAndPutInTx() throws Exception
   {
      Fqn A_B_C = Fqn.fromString("/a/b/c");
      Fqn A = Fqn.fromString("/a");

      cache.put(A_B_C, "k", "v");

      assertTrue(cache.exists(A_B_C));
      assertTrue(cache.exists(A));

      cache.getTransactionManager().begin();
      cache.remove(A);
      cache.put(A_B_C, "k", "v2");
      cache.getTransactionManager().commit();

      assertTrue(cache.exists(A_B_C));
      assertTrue(cache.exists(A));

      assertEquals("v2", cache.get(A_B_C, "k"));
   }


   // thanks to msteiner at gazeta.pl for this test and spotting JBCACHE-999
    public void testRootNodeRemoval() throws Exception
    {
      Fqn root = Fqn.ROOT;
      Fqn fqn = new Fqn(root, new Long(1));
      //put first time
      tx.begin();
      this.cache.put(fqn, "k", "v");
      tx.commit();

      //get works fine
      tx.begin();
      assertEquals("v", this.cache.get(fqn, "k"));
      tx.commit();

      //remove all
      tx.begin();
      this.cache.remove(root);
      tx.commit();

      //get returns null - ok
      //put - endless loop
      tx.begin();
      assertNull(this.cache.get(fqn, "k"));
      this.cache.put(fqn, "k", "v");
      tx.commit();
	}

   // thanks to msteiner at gazeta.pl for this test and spotting JBCACHE-999
   public void testNodeAdditionAfterRemoval() throws Exception
    {
      Fqn fqn = Fqn.fromString("/1/2/3/4");
      //put first time
      tx.begin();
      this.cache.put(fqn, "k", "v");
      tx.commit();

      //get works fine
      tx.begin();
      assertEquals("v", this.cache.get(fqn, "k"));
      tx.commit();

      //remove all
      tx.begin();
      this.cache.remove(Fqn.ROOT);
      tx.commit();

      //get returns null - ok
      //put - endless loop
      tx.begin();
      assertNull(this.cache.get(fqn, "k"));
      this.cache.put(fqn, "k", "v");
      tx.commit();
	}

   // thanks to msteiner at gazeta.pl for this test and spotting JBCACHE-999
   public void testRootNodeRemovalRollback() throws Exception
    {

      Fqn root = Fqn.ROOT;
      Fqn fqn = new Fqn(root, new Long(1));
      //put first time
      tx.begin();
      this.cache.put(fqn, "k", "v");
      tx.commit();

      //get works fine
      tx.begin();
      assertEquals("v", this.cache.get(fqn, "k"));
      tx.commit();

      //remove all
      tx.begin();
      this.cache.remove(root);
      tx.rollback();

       assertEquals("v", this.cache.get(fqn, "k"));
   }


 /*  public void testConcurrentReadAccess() throws Exception {
      cache.setIsolationLevel(IsolationLevel.REPEATABLE_READ);
      cache.setUseInterceptorMbeans(false);

      cache.put("/1/2/3/4", "foo", "bar"); // no TX, no locks held after put() returns


      class Reader extends Thread {
         Transaction thread_tx;

         public Reader(String name) {
            super(name);
         }

         public void run() {
            try {
               thread_tx=startTransaction();
               log("acquiring RL");
               cache.get("/1/2/3", "foo"); // acquires RLs on all 3 nodes
               log("RL acquired successfully");
               sleep(200000);
               log("committing TX");
               thread_tx.commit(); // releases RLs
               log("committed TX");
            }
            catch(Exception e) {
               thread_ex=e;
            }
         }
      }


      Reader reader=new Reader("R1");
      Reader reader2=new Reader("R2");
      reader.start();
      reader2.start();
      reader.join();
      reader2.join();
      if(thread_ex != null)
         throw thread_ex;
   }

*/
   private static void log(String msg) {
      System.out.println(Thread.currentThread().getName() + ": " + msg);
   }

//   public void testRaceConditionOnNotInCacheCondition() throws Exception {
//      cache.setIsolationLevel(IsolationLevel.SERIALIZABLE);
//
//      tx.begin();
//      // we now read the null entry, and decide that we need to go do something.
//
//      Object cachedObject=cache.get("/SecurityInfo/", Integer.toString(23));
//      assertNull(cachedObject); // we expect this in this test
//
//      /**
//       * now start another Thread to go do the same action, looking for the value, but it SHOULD
//       * see the result of the main thread put once it commits.
//       */
//      Thread thread=new Thread(new Runnable() {
//         UserTransaction tx2=(UserTransaction)new InitialContext(p).lookup("UserTransaction");
//         public void run() {
//            try {
//               tx2.begin();
//               log("OtherThread: inspecting the cache");
//               Object cachedObject=cache.get("/SecurityInfo", Integer.toString(23));
//
//               log("OtherThread: read from cache: " + cachedObject);
//               Thread.sleep(3000);
//
//               cachedObject=cache.get("/SecurityInfo", Integer.toString(23));
//               log("OtherThread: read(second time) from cache:" + cachedObject);
//
//               /**
//                * This should really fail because the other thread should actually have put something else there.
//                */
//               cache.put("/SecurityInfo", Integer.toString(23), "HelloWorldDIRTY!");
//
//               log("OtherThread: Has put something in the cache tha shouldn't be there");
//            }
//            catch(Exception e) {
//               e.printStackTrace();
//            }
//            finally {
//               if(tx2 != null)
//                  try {tx2.commit();} catch(Exception e) {e.printStackTrace();}
//            }
//            log("OthreThread: exiting");
//
//         }
//      });
//
//      thread.start();
//      log("MainThread is now waiting a little bit");
//      Thread.sleep(2000); // wait long enough for the other thread to block a bit. Simulate a DB read
//      log("MainThread is now putting something in the cache");
//      cache.put("/SecurityInfo", Integer.toString(23), "HelloWorld");
//      Thread.sleep(2000); // wait long enough for the other thread to block a bit. Simulate a DB read
//      tx.commit();
//      log("MainThread: committed");
//      thread.join(30000);
//
//      log(cache.get("/SecurityInfo", Integer.toString(23)).toString());
//   }


//   public void testConcurrentReadsWithSerializableIsolationLevel() throws CacheException, InterruptedException {
//      final long TIMEOUT=5000;
//      cache.setIsolationLevel(IsolationLevel.SERIALIZABLE);
//      cache.put("/testfqn", "testkey", "testvalue"); // add initial value, to be read by threads
//      Reader r1, r2;
//      r1=new Reader("reader1", 3000);
//      r2=new Reader("reader2", 0);
//      r1.start();
//      pause(100); // make sure thread1 starts and acquires the lock before thread2
//      r2.start();
//      r1.join(TIMEOUT);
//      r2.join(TIMEOUT);
//   }
//
//   class Reader extends Thread {
//      long timeout;
//
//      public Reader(String name, long timeout) {
//         super(name);
//         this.timeout=timeout;
//      }
//
//      public void run() {
//         UserTransaction trans=null;
//         try {
//            trans=(UserTransaction)new InitialContext(p).lookup("UserTransaction");
//            trans.begin();
//            Object retval=null;
//            log2("accessing tree");
//            retval=cache.get("testfqn", "testkey");
//            log2("retval: " + retval);
//            if(timeout > 0) {
//               log2("sleeping for " + timeout + " ms");
//               pause(timeout);
//            }
//         }
//         catch(Exception e) {
//            e.printStackTrace();
//         }
//         finally {
//            try {trans.commit();} catch(Throwable t) {}
//            log2("done");
//         }
//      }
//   }

//   private void pause(long timeout) {
//      try {
//         Thread.sleep(timeout);
//      }
//      catch(InterruptedException e) {
//      }
//   }
//
//   private void log(String msg) {
//      System.out.println("-- [" + Thread.currentThread() + "]: " + msg);
//   }
//
//   private void log2(String msg) {
//      System.out.println("-- [" + System.currentTimeMillis() + " " + Thread.currentThread() + "]: " + msg);
//   }


   public static Test suite() throws Exception {
      return new TestSuite(TransactionTest.class);
   }

   public static void main(String[] args) throws Exception {
      junit.textui.TestRunner.run(suite());
   }


}
