/*
 *
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache.transaction;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.CacheFactory;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.factories.XmlConfigurationParser;
import org.jboss.cache.factories.UnitTestCacheConfigurationFactory;
import org.jboss.cache.lock.IsolationLevel;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.fail;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import javax.transaction.UserTransaction;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * Unit test for local CacheSPI. Use locking and multiple threads to test
 * concurrent access to the tree.
 *
 * @version $Id: ConcurrentTransactionalTest.java 5238 2008-01-25 12:47:36Z mircea.markus $
 */
@Test(groups = {"functional", "transaction"})
public class ConcurrentTransactionalTest
{
   private CacheSPI<Integer, String> cache;
   private Log logger_ = LogFactory.getLog(ConcurrentTransactionalTest.class);
   private static Throwable thread_ex = null;
   private static final int NUM = 10000;

   @BeforeMethod(alwaysRun = true)
   public void setUp() throws Exception
   {
   }

   private void createCache(IsolationLevel level)
   {
      CacheFactory<Integer, String> factory = new DefaultCacheFactory();
      Configuration conf = UnitTestCacheConfigurationFactory.createConfiguration(Configuration.CacheMode.LOCAL, true);
      conf.setCacheMode(Configuration.CacheMode.LOCAL);
      conf.setIsolationLevel(level);
      conf.setTransactionManagerLookupClass(TransactionSetup.getManagerLookup());
      cache = (CacheSPI) new DefaultCacheFactory().createCache(conf);
      cache.put("/a/b/c", null);
   }

   @AfterMethod(alwaysRun = true)
   public void tearDown() throws Exception
   {
      cache.stop();
      thread_ex = null;
      TransactionSetup.cleanup();
   }

   public void testConcurrentAccessWithRWLock() throws Throwable
   {
      createCache(IsolationLevel.REPEATABLE_READ);
      work_();
   }

   public void testConcurrentAccessWithExclusiveLock() throws Throwable
   {
      createCache(IsolationLevel.SERIALIZABLE);
      work_();
   }

   private void work_() throws Throwable
   {
      Updater one, two;
      try
      {
         one = new Updater("Thread one");
         two = new Updater("Thread two");
         long current = System.currentTimeMillis();
         one.start();
         two.start();
         one.join();
         two.join();
         if (thread_ex != null)
         {
            throw thread_ex;
         }

         long now = System.currentTimeMillis();
         log("*** Time elapsed: " + (now - current));

         System.out.println("cache content: " + cache.toString());
         Set<Integer> keys = cache.getNode(Fqn.fromString("/a/b/c")).getKeys();
         System.out.println("number of keys=" + keys.size());

         if (keys.size() != NUM)
         {
            scanForNullValues(keys);

            try
            {
               System.out.println("size=" + keys.size());
               List<Integer> l = new LinkedList<Integer>(keys);
               Collections.sort(l);
               System.out.println("keys: " + l);
               for (int i = 0; i < NUM; i++)
               {
                  if (!l.contains(new Integer(i)))
                  {
                     System.out.println("missing: " + i);
                  }
               }

               LinkedList<Integer> duplicates = new LinkedList<Integer>();
               for (Integer integer : l)
               {
                  if (duplicates.contains(integer))
                  {
                     System.out.println(integer + " is a duplicate");
                  }
                  else
                  {
                     duplicates.add(integer);
                  }
               }
            }
            catch (Exception e1)
            {
               e1.printStackTrace();
            }
         }

         assertEquals(NUM, keys.size());
      }
      catch (Exception e)
      {
         e.printStackTrace();
         fail(e.toString());
      }
   }

   private void scanForNullValues(Set<Integer> keys)
   {
      for (Object o : keys)
      {
         if (o == null)
         {
            System.err.println("found a null value in keys");
         }
      }
   }

   private void log(String msg)
   {
      logger_.debug(" [" + Thread.currentThread() + "]: " + msg);
   }

   private class Updater extends Thread
   {
      private String val = null;
      private UserTransaction tx;

      public Updater(String name)
      {
         this.val = name;
      }

      public void run()
      {
         try
         {
            log("adding data");
            tx = TransactionSetup.getUserTransaction();
            for (int i = 0; i < NUM; i++)
            {
               log("adding data i=" + i);
               tx.begin();
               cache.put("/a/b/c", i, val);
               tx.commit();
               yield();
            }
         }
         catch (Throwable t)
         {
            t.printStackTrace();
            thread_ex = t;
         }
      }
   }

}
