/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/

package org.jboss.cache.statetransfer;

import org.jboss.cache.Cache;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Region;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.config.Configuration.CacheMode;
import org.jboss.cache.factories.UnitTestCacheConfigurationFactory;
import org.jboss.cache.loader.AbstractCacheLoaderTestBase;
import org.jboss.cache.loader.CacheLoader;
import org.jboss.cache.marshall.SelectedClassnameClassLoader;
import org.jboss.cache.util.TestingUtil;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertFalse;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * Abstract superclass of the StateTransfer tests.
 *
 * @author <a href="mailto://brian.stansberry@jboss.com">Brian Stansberry</a>
 * @version $Revision$
 */
@Test(groups = {"functional"})
public abstract class StateTransferTestBase extends AbstractCacheLoaderTestBase
{
   protected static final int SUBTREE_SIZE = 10;

   public static final Fqn<String> A = Fqn.fromString("/a");
   public static final Fqn<String> B = Fqn.fromString("/b");
   public static final Fqn<String> C = Fqn.fromString("/c");

   protected static final String ADDRESS_CLASSNAME = "org.jboss.cache.marshall.data.Address";
   protected static final String PERSON_CLASSNAME = "org.jboss.cache.marshall.data.Person";

   public static final Fqn<String> A_B = Fqn.fromString("/a/b");
   public static final Fqn<String> A_C = Fqn.fromString("/a/c");
   public static final Fqn<String> A_D = Fqn.fromString("/a/d");

   public static final String JOE = "JOE";
   public static final String BOB = "BOB";
   public static final String JANE = "JANE";
   public static final Integer TWENTY = 20;
   public static final Integer FORTY = 40;

   protected Map<String, Cache> caches;
   private ClassLoader orig_TCL;


   protected abstract String getReplicationVersion();

   protected CacheSPI<Object, Object> createCache(String cacheID,
                                                  boolean sync,
                                                  boolean useMarshalling,
                                                  boolean useCacheLoader)
         throws Exception
   {
      return createCache(cacheID, sync, useMarshalling, useCacheLoader, false, true, true);
   }

   protected CacheSPI<Object, Object> createCache(String cacheID,
                                                  boolean sync,
                                                  boolean useMarshalling,
                                                  boolean useCacheLoader, boolean fetchPersistentState)
         throws Exception
   {
      return createCache(cacheID, sync, useMarshalling, useCacheLoader, false, true, fetchPersistentState);
   }

   protected CacheSPI<Object, Object> createCache(String cacheID,
                                                  boolean sync,
                                                  boolean useMarshalling,
                                                  boolean useCacheLoader,
                                                  boolean cacheLoaderAsync,
                                                  boolean startCache, boolean fetchPersistentState)
         throws Exception
   {
      if (useCacheLoader)
      {
         return createCache(cacheID, sync, useMarshalling, "org.jboss.cache.loader.FileCacheLoader", cacheLoaderAsync, startCache, fetchPersistentState);
      }
      else
      {
         return createCache(cacheID, sync, useMarshalling, null, cacheLoaderAsync, startCache, fetchPersistentState);
      }
   }

   protected CacheSPI<Object, Object> createCache(String cacheID, boolean sync, boolean useMarshalling, String cacheLoaderClass,
                                                  boolean cacheLoaderAsync, boolean startCache, boolean fetchPersistentState) throws Exception
   {
      if (caches.get(cacheID) != null)
      {
         throw new IllegalStateException(cacheID + " already created");
      }

      CacheMode mode = sync ? CacheMode.REPL_SYNC : CacheMode.REPL_ASYNC;
      Configuration c = UnitTestCacheConfigurationFactory.createConfiguration(mode);
      if (sync)
      {
         c.setSyncRollbackPhase(true);
         c.setSyncCommitPhase(true);
      }
      c.setClusterName("VersionedTestBase");
      c.setReplVersionString(getReplicationVersion());
      // Use a long timeout to facilitate setting debugger breakpoints
      c.setStateRetrievalTimeout(60000);
      if (useMarshalling)
      {
         c.setUseRegionBasedMarshalling(true);
         c.setInactiveOnStartup(true);
      }
      if (cacheLoaderClass != null && cacheLoaderClass.length() > 0)
      {
         configureCacheLoader(c, cacheLoaderClass, cacheID, cacheLoaderAsync, fetchPersistentState);
      }
      //      tree.setConfiguration(c);
      //c.setLockAcquisitionTimeout(60000);
      //c.setSyncReplTimeout(60000);
      CacheSPI<Object, Object> tree = (CacheSPI<Object, Object>) new DefaultCacheFactory().createCache(c, false);
      //c.setLockAcquisitionTimeout(60000); // a whole minute?!??  Lots of state to tfr?
      //c.setSyncReplTimeout(60000);
      configureMultiplexer(tree);

      // Put the cache in the map before starting, so if it fails in
      // start it can still be destroyed later
      caches.put(cacheID, tree);

      if (startCache)
      {
         tree.create();
         tree.start();
      }

      return tree;
   }

   protected void createAndActivateRegion(CacheSPI<Object, Object> c, Fqn f)
   {
      Region r = c.getRegion(f, true);
      r.registerContextClassLoader(getClass().getClassLoader());
      r.activate();
   }

   /**
    * Provides a hook for multiplexer integration. This default implementation
    * is a no-op; subclasses that test mux integration would override
    * to integrate the given cache with a multiplexer.
    * <p/>
    * param cache a cache that has been configured but not yet created.
    */
   protected void configureMultiplexer(Cache cache) throws Exception
   {
      // default does nothing
   }

   /**
    * Provides a hook to check that the cache's channel came from the
    * multiplexer, or not, as expected.  This default impl asserts that
    * the channel did not come from the multiplexer.
    *
    * @param cache a cache that has already been started
    */
   protected void validateMultiplexer(Cache cache)
   {
      assertFalse("Cache is not using multiplexer", cache.getConfiguration().isUsingMultiplexer());
   }

   protected void startCache(Cache cache) throws Exception
   {
      cache.create();
      cache.start();

      validateMultiplexer(cache);
   }

   protected void configureCacheLoader(Configuration c,
                                       String cacheID,
                                       boolean async)
         throws Exception
   {
      configureCacheLoader(c, "org.jboss.cache.loader.FileCacheLoader", cacheID, async, true);
   }

   protected void configureCacheLoader(Configuration c, String cacheloaderClass, String cacheID,
                                       boolean async, boolean fetchPersistentState) throws Exception
   {
      if (cacheloaderClass != null)
      {
         if (cacheloaderClass.equals("org.jboss.cache.loader.JDBCCacheLoader"))
         {
            Properties prop = new Properties();
            try
            {
               prop.load(this.getClass().getClassLoader().getResourceAsStream("cache-jdbc.properties"));
            }
            catch (Exception e)
            {
               System.out.println("Error loading jdbc properties ");
            }
            String props = "cache.jdbc.driver =" + prop.getProperty("cache.jdbc.driver") + "\n" + "cache.jdbc.url="
                  + prop.getProperty("cache.jdbc.url") + "\n" + "cache.jdbc.user="
                  + prop.getProperty("cache.jdbc.user") + "\n" + "cache.jdbc.password="
                  + prop.getProperty("cache.jdbc.password") + "\n" + "cache.jdbc.node.type="
                  + prop.getProperty("cache.jdbc.node.type") + "\n" + "cache.jdbc.sql-concat="
                  + prop.getProperty("cache.jdbc.sql-concat");

            c.setCacheLoaderConfig(getSingleCacheLoaderConfig("", "org.jboss.cache.loader.JDBCCacheLoader",
                  props, false, true, false));
         }
         else
         {

            String tmp_location = getTempLocation(cacheID);

            // Do cleanup in case it failed before
            File file = new File(tmp_location);
            cleanFile(file);

            file.mkdir();

            tmp_location = escapeWindowsPath(tmp_location);
            String props = "location = " + tmp_location + "\n";
            c.setCacheLoaderConfig(getSingleCacheLoaderConfig("", cacheloaderClass, props, async, fetchPersistentState, false));
         }
      }
   }

   protected void initialStateTferWithLoaderTest(String cacheLoaderClass1, String cacheLoaderClass2, boolean asyncLoader) throws Exception
   {
      CacheSPI<Object, Object> cache1 = createCache("cache1", false, false, cacheLoaderClass1, false, true, true);

      cache1.put(A_B, "name", JOE);
      cache1.put(A_B, "age", TWENTY);
      cache1.put(A_C, "name", BOB);
      cache1.put(A_C, "age", FORTY);

      CacheSPI<Object, Object> cache2 = createCache("cache2", false, false, cacheLoaderClass2, asyncLoader, false, true);

      cache2.start();

      // Pause to give caches time to see each other
      TestingUtil.blockUntilViewsReceived(new CacheSPI[]{cache1, cache2}, 60000);

      if (asyncLoader)
      {
         TestingUtil.sleepThread((long) 100);
      }

      CacheLoader loader = cache2.getCacheLoaderManager().getCacheLoader();

      assertEquals("Incorrect loader name for /a/b", JOE, loader.get(A_B).get("name"));
      assertEquals("Incorrect loader age for /a/b", TWENTY, loader.get(A_B).get("age"));
      assertEquals("Incorrect loader name for /a/c", BOB, loader.get(A_C).get("name"));
      assertEquals("Incorrect loader age for /a/c", FORTY, loader.get(A_C).get("age"));

      assertEquals("Incorrect name for /a/b", JOE, cache2.get(A_B, "name"));
      assertEquals("Incorrect age for /a/b", TWENTY, cache2.get(A_B, "age"));
      assertEquals("Incorrect name for /a/c", BOB, cache2.get(A_C, "name"));
      assertEquals("Incorrect age for /a/c", FORTY, cache2.get(A_C, "age"));
   }

   protected String getTempLocation(String cacheID)
   {
      String tmp_location = System.getProperty("java.io.tmpdir", "c:\\tmp");
      File file = new File(tmp_location);
      file = new File(file, cacheID);
      return file.getAbsolutePath();
   }

   protected String escapeWindowsPath(String path)
   {
      if ('/' == File.separatorChar)
      {
         return path;
      }

      char[] chars = path.toCharArray();
      StringBuilder sb = new StringBuilder();
      for (char aChar : chars)
      {
         if (aChar == '\\')
         {
            sb.append('\\');
         }
         sb.append(aChar);
      }
      return sb.toString();
   }

   @BeforeMethod(alwaysRun = true)
   public void setUp() throws Exception
   {
      caches = new HashMap<String, Cache>();

      // Save the TCL in case a test changes it
      orig_TCL = Thread.currentThread().getContextClassLoader();
   }

   @AfterMethod(alwaysRun = true)
   public void tearDown() throws Exception
   {

      System.out.println("*** in tearDown()");

      // Restore the TCL in case a test changed it
      Thread.currentThread().setContextClassLoader(orig_TCL);

      for (String cacheID : caches.keySet())
      {
         try
         {
            stopCache(caches.get(cacheID));
            TestingUtil.sleepThread(1500);
            File file = new File(getTempLocation(cacheID));
            cleanFile(file);
         }
         catch (Exception e)
         {
            // errors in teardown should not fail test
         }
      }
   }

   protected void stopCache(Cache cache)
   {
      if (cache != null)
      {
         try
         {
            cache.stop();
            cache.destroy();
         }
         catch (Exception e)
         {
            System.out.println("Exception stopping cache " + e.getMessage());
            e.printStackTrace(System.out);
         }
      }
   }

   protected void cleanFile(File file)
   {
      File[] children = file.listFiles();
      if (children != null)
      {
         for (File child : children)
         {
            cleanFile(child);
         }
      }

      if (file.exists())
      {
         file.delete();
      }
      if (file.exists())
      {
         file.deleteOnExit();
      }
   }

   protected ClassLoader getClassLoader() throws Exception
   {
      String[] includesClasses = {"org.jboss.cache.marshall.Person",
                                  "org.jboss.cache.marshall.Address"};
      String[] excludesClasses = {};
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return new SelectedClassnameClassLoader(includesClasses, excludesClasses, cl);
   }

   protected ClassLoader getNotFoundClassLoader() throws Exception
   {
      String[] notFoundClasses = {"org.jboss.cache.marshall.Person",
                                  "org.jboss.cache.marshall.Address"};
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return new SelectedClassnameClassLoader(null, null, notFoundClasses, cl);
   }

   protected abstract class CacheUser implements Runnable
   {
      protected Semaphore semaphore;
      protected CacheSPI<Object, Object> cache;
      protected String name;
      protected Exception exception;
      protected Thread thread;

      CacheUser()
      {
      }

      CacheUser(Semaphore semaphore,
                String name,
                boolean sync,
                boolean activateRoot)
            throws Exception
      {
         this.cache = createCache(name, sync, true, false);
         this.semaphore = semaphore;
         this.name = name;

         if (activateRoot)
         {
            cache.getRegion(Fqn.ROOT, true).activate();
         }
      }

      public void run()
      {
         boolean acquired = false;
         try
         {
            acquired = semaphore.tryAcquire(60, TimeUnit.SECONDS);
            if (!acquired)
            {
               throw new Exception(name + " cannot acquire semaphore");
            }
            //System.out.println(name + " acquired semaphore");

            useCache();

         }
         catch (Exception e)
         {
            System.out.println(name + ": " + e.getLocalizedMessage());
            e.printStackTrace(System.out);

            // Save it for the test to check
            exception = e;
         }
         finally
         {
            if (acquired)
            {
               semaphore.release();
            }
         }

      }

      abstract void useCache() throws Exception;

      public Exception getException()
      {
         return exception;
      }

      public CacheSPI<Object, Object> getCacheSPI()
      {
         return cache;
      }

      public String getName()
      {
         return name;
      }

      public void start()
      {
         thread = new Thread(this, name);
         thread.start();
      }

      public void cleanup()
      {
         if (thread != null && thread.isAlive())
         {
            thread.interrupt();
         }
      }
   }
}
