/*
 * JBoss, Home of Professional Open Source
 * Copyright 2006, Red Hat Middleware LLC, 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.ejb3.client;

import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.rmi.server.RMIClassLoader;
import java.rmi.server.RMIClassLoaderSpi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Properties;

import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.jboss.beans.metadata.plugins.builder.BeanMetaDataBuilderFactory;
import org.jboss.beans.metadata.spi.BeanMetaData;
import org.jboss.beans.metadata.spi.BeanMetaDataFactory;
import org.jboss.beans.metadata.spi.ValueMetaData;
import org.jboss.beans.metadata.spi.builder.BeanMetaDataBuilder;
import org.jboss.classloader.plugins.system.DefaultClassLoaderSystem;
import org.jboss.classloader.spi.ClassLoaderDomain;
import org.jboss.classloader.spi.ClassLoaderSystem;
import org.jboss.classloader.spi.ParentPolicy;
import org.jboss.classloading.spi.dependency.ClassLoading;
import org.jboss.classloading.spi.vfs.dependency.VFSClassLoaderPolicyModule;
import org.jboss.classloading.spi.vfs.metadata.VFSClassLoaderFactory;
import org.jboss.classloading.spi.vfs.metadata.VFSClassLoaderFactory10;
import org.jboss.client.AppClientLauncher;
import org.jboss.dependency.spi.ControllerMode;
import org.jboss.dependency.spi.ControllerState;
import org.jboss.deployment.dependency.JndiDependencyMetaData;
import org.jboss.kernel.Kernel;
import org.jboss.kernel.plugins.bootstrap.AbstractBootstrap;
import org.jboss.kernel.plugins.bootstrap.basic.BasicBootstrap;
import org.jboss.kernel.plugins.deployment.AbstractKernelDeployment;
import org.jboss.kernel.plugins.deployment.xml.BasicXMLDeployer;
import org.jboss.kernel.spi.dependency.KernelController;
import org.jboss.kernel.spi.dependency.KernelControllerContext;
import org.jboss.kernel.spi.deployment.KernelDeployment;
import org.jboss.logging.Logger;
import org.jboss.metadata.client.jboss.JBossClientMetaData;
import org.jboss.remoting.Remoting;
import org.jboss.vfs.VFS;
import org.jboss.vfs.VirtualFile;
import org.jboss.vfs.util.PathTokenizer;
import org.jboss.vfs.util.automount.Automounter;
import org.jboss.vfs.util.automount.MountOption;
import org.jboss.xb.binding.JBossXBException;

/**
 * This class launches a JavaEE 5 application client.
 * 
 * The first argument is either a jar file containing the client deployment files or the application client class name.
 * The manifest file Main-Class attribute must point to the application client class.
 * It must also contain an application client deployment descriptor file (META-INF/application-client.xml).
 *
 * @author <a href="mailto:carlo.dewolf@jboss.com">Carlo de Wolf</a>
 * @author Scott.Stark@jboss.org
 * @version $Revision: 106442 $
 */
public class ClientLauncher
   implements AppClientLauncher
{
   private static final Logger log = Logger.getLogger(ClientLauncher.class);
   private static final String VERSION = "$Revision: 106442 $";
   private static Throwable exception;
   /** The kernel for the client container */
   private static Kernel kernel;

   /** The deployer */
   private static BasicXMLDeployer deployer;
   /** The mainClass used by the ClientContainer */
   private static Class<?> theMainClass;
   /** Should the remoting class loader delegate to the tcl */
   private static boolean remotingDelgatesToTCL = true;
   /** Additional classpath elements to client container classpath */
   private static List<String> extraClassPath = new ArrayList<String>();

   /**
    * Access the client main class as loaded by the MC/client container. This
    * class should be used to ensure that any static variable set in the class
    * as a by-product of invoking the client main(String[]) method are seen.
    * @return client main class object
    */
   public static Class<?> getTheMainClass()
   {
      return theMainClass;
   }

   /**
    * Convenience method to load the XML descriptor for the given applicationClientName.
    * This does a lookup against the server for the xml metadata the client
    * deployer bound.
    * 
    * @return the JBossClientMetaData
    * @throws IOException 
    * @throws JBossXBException 
    */
   public static JBossClientMetaData loadXML(String applicationClientName) throws NamingException
   {
      log.warn("FIXME: using an unsupported hack to get metadata");
      InitialContext ctx = new InitialContext();
      JBossClientMetaData metaData = (JBossClientMetaData) ctx.lookup(applicationClientName + "/metaData");
      return metaData;
   }
   public static List<String> loadClassPath(String applicationClientName)
      throws NamingException
   {
      InitialContext ctx = new InitialContext();
      List<String> cp = (List<String>) ctx.lookup(applicationClientName + "/classPathEntries");
      return cp;
   }

   /**
    * Calls {@link #launch(String, String, String[], Properties)}
    */
   public void launch(String clientClass, String clientName, String[] args)
         throws Throwable
   {
      launch(clientClass, clientName, args, null);
   }

   /**
    * Convenience method for launching a client container.
    * 
    * @param xml
    * @param mainClassName
    * @param applicationClientName
    * @param args
    * @throws Exception
    */
   public static void launch(JBossClientMetaData xml, String mainClassName, String applicationClientName, String args[])
      throws Throwable
   {
      List<String> cp = Collections.emptyList();
      launch(xml, cp, mainClassName, applicationClientName, args, null);
   }
   /**
    * The AppClientLauncher method for launching a client container.
    * 
    * @param mainClassName - the class whose main(String[]) will be invoked
    * @param clientName - the client name that maps to the server side JNDI ENC
    * @param args - the args to pass to main method
    * @param jndiEnv - jndi properties to pass to set as System properties
    * @throws Throwable
    */
   public static void launch(String mainClassName, String clientName, String args[],
         Properties jndiEnv)
      throws Throwable
   {
      // Set the RMIClassLoaderSpi implementation to JBossRMIClassLoader
      System.setProperty("java.rmi.server.RMIClassLoaderSpi", JBossRMIClassLoader.class.getName());
      // This is loaded lazily via a jndi dependency
      JBossClientMetaData xml = null;
      List<String> cp = loadClassPath(clientName);
      launch(xml, cp, mainClassName, clientName, args, jndiEnv);
   }

   /**
    * The client launcher entry point that create an mc to launch the client container.
    * @param clientClass
    * @param clientName
    * @param cp
    * @param args
    * @throws Throwable
    */
   public static void launch(JBossClientMetaData xml, List<String> classPath,
         String mainClassName, String applicationClientName, String args[],
         Properties jndiEnv)
      throws Throwable
   {
      log.info("ClientLauncher, version: "+VERSION);
      // Init the kernel and deployers
      args = init(args);

      // Pass in the jndi env properties so InitialContext() works
      if(jndiEnv != null)
      {
         for(Object key : jndiEnv.keySet())
         {
            String name = (String) key;
            System.setProperty(name, jndiEnv.getProperty(name));
         }
      }
      // Have the remoting class loader delegate to the tcl
      if(remotingDelgatesToTCL)
         System.setProperty(Remoting.CLASSLOADING_PARENT_FIRST_DELEGATION_PROP, "false");

      // Setup the 
      ArrayList<BeanMetaDataFactory> beanFactories = new ArrayList<BeanMetaDataFactory>();
      ArrayList<BeanMetaData> beans = new ArrayList<BeanMetaData>();

      // Add the common launcher beans, ClassLoaderSystem
      BeanMetaDataBuilder builder = BeanMetaDataBuilderFactory.createBuilder("ClassLoaderSystem", ClassLoaderSystem.class.getName());
      builder.setFactoryClass(ClientLauncher.class.getName());
      builder.setFactoryMethod("getClassLoaderSystem");
      BeanMetaData classLoaderSystemBMD = builder.getBeanMetaData();
      addBeanMetaData(beanFactories, beans, classLoaderSystemBMD);

      // ClassLoading
      builder = BeanMetaDataBuilderFactory.createBuilder("ClassLoading", ClassLoading.class.getName());
      builder.addMethodInstallCallback("addModule", ControllerState.CONFIGURED);
      builder.addMethodUninstallCallback("removeModule", ControllerState.CONFIGURED);
      BeanMetaData classLoadingBMD = builder.getBeanMetaData();
      addBeanMetaData(beanFactories, beans, classLoadingBMD);

      try
      {
         builder = BeanMetaDataBuilderFactory.createBuilder("ClientContainer",
               "org.jboss.ejb3.client.ClientContainer");
         VFSClassLoaderFactory factory = new VFSClassLoaderFactory("ClientLauncherClassPath");
         ArrayList<String> roots = new ArrayList<String>();
         // Create the classpath
         log.info("Setting up classpath from: ");
         for(String path : classPath)
         {
            log.info(path);
            mount(path);
            roots.add(path);
         }
         for(String path : extraClassPath)
         {
            log.info(path);
            mount(path);
            roots.add(path);
         }
         log.info("End classpath");
         factory.setRoots(roots);
         beanFactories.add(factory);
         // ClientContainer(xml, mainClass, applicationClientName, jndiEnv);
         String classLoaderName = factory.getContextName();
         if(classLoaderName == null)
            classLoaderName = factory.getName() + ":" + factory.getVersion();
         String metaDataJndiName = applicationClientName + "/metaData";
         ValueMetaData xmlMD = new JndiDependencyValueMetaData(metaDataJndiName, jndiEnv, classLoaderName);
         builder.addConstructorParameter(JBossClientMetaData.class.getName(), xmlMD);
         //builder.addConstructorParameter(JBossClientMetaData.class.getName(), xml);
         builder.addConstructorParameter(Class.class.getName(), mainClassName);
         builder.addConstructorParameter(String.class.getName(), applicationClientName);
         builder.addConstructorParameter(Properties.class.getName(), jndiEnv);
         // Use vfs class loader as the ClientContainer class loader
         ValueMetaData classLoader = builder.createInject(classLoaderName);
         builder.setClassLoader(classLoader);
         BeanMetaData clientContainerMD = builder.getBeanMetaData();

         AbstractKernelDeployment deployment = new AbstractKernelDeployment();
         deployment.setName(factory.getName() + ":" + factory.getVersion());
         addBeanMetaData(beanFactories, beans, clientContainerMD);
         deployment.setBeanFactories(beanFactories);
         if(beans.size() > 0)
            deployment.setBeans(beans);
         deploy(deployment);
         validate();

         KernelController controller = kernel.getController();
         // ClientContainer
         KernelControllerContext context = (KernelControllerContext) controller.getContext("ClientContainer", ControllerState.INSTALLED);
         if (context == null)
            throw new Exception("ClientContainer bean was not created");
         Object client = context.getTarget();
         KernelControllerContext cclContext = (KernelControllerContext) controller.getContext(classLoaderName, ControllerState.INSTALLED);
         if (cclContext == null)
            throw new Exception(classLoaderName+" bean was not created");
         ClassLoader ccLoader = (ClassLoader) cclContext.getTarget();
         if (ccLoader == null )
            throw new Exception(classLoaderName+" bean was not created");
         if (client.getClass().getClassLoader() != ccLoader)
            log.warn(client.getClass().getClassLoader()+" != "+ccLoader);
         Class<?> clientContainerClass = ccLoader.loadClass("org.jboss.ejb3.client.ClientContainer");
         if (clientContainerClass.getClassLoader() != ccLoader)
            log.warn(clientContainerClass.getClassLoader()+" != "+ccLoader);

         // Invoke main on the underlying client main class through the ClientContainer
         ClassLoader prevLoader = Thread.currentThread().getContextClassLoader();
         try
         {
            // Get the mainClass
            Class<?> empty[] = {};
            Method getMainClass = clientContainerClass.getDeclaredMethod("getMainClass", empty);
            theMainClass = (Class<?>) getMainClass.invoke(client, null);
            // Invoke main
            Thread.currentThread().setContextClassLoader(ccLoader);
            Class<?> parameterTypes[] = { args.getClass() };
            Method invokeMain = clientContainerClass.getDeclaredMethod("invokeMain", parameterTypes);
            invokeMain.invoke(client, (Object) args);
         }
         finally
         {
            Thread.currentThread().setContextClassLoader(prevLoader);
         }

         // 
         undeploy(deployment);
      }
      catch(Throwable e)
      {
         exception = e;
         throw e;
      }
   }

   /**
    * Create a ClassLoaderSystem with the default ClassLoaderDomain set to use
    * a AFTER ParentPolicy.
    *
    * @return ClassLoaderSystem instance
    */
   public static ClassLoaderSystem getClassLoaderSystem()
   {
      DefaultClassLoaderSystem system = new DefaultClassLoaderSystem();
      ClassLoaderDomain defaultDomain = system.getDefaultDomain();
      defaultDomain.setParentPolicy(ParentPolicy.AFTER);
      return system;
   }

   private static void addBeanMetaData(
         ArrayList<BeanMetaDataFactory> beanFactories,
         ArrayList<BeanMetaData> beans, BeanMetaData bmd)
   {
      // TODO Auto-generated method stub
      if(bmd instanceof BeanMetaDataFactory)
      {
         BeanMetaDataFactory bmdf = (BeanMetaDataFactory) bmd;
         beanFactories.add(bmdf);
      }
      else
      {
         // Have to use the deprecated beans
         beans.add(bmd);
      }
   }

   /**
    * Initialize the mc kernel and deployer as well as extract any client
    * launcher specific args from the input arguments.
    * 
    * @param args - the input args to both the launcher and the client main. The
    * launcher specific arguments are:
    *   -remotingDelgatesToTCL : true if remoting should first delegate to the the tcl based class loader
    *   -extraClassPath : comma separated list of vfsurls for additional classpath
    * @return the remaining arguments to pass to the client main.
    * @throws Throwable
    */
   private static String[] init(String[] args) throws Throwable
   {
      // Extract any launcher args from the input
      String[] newArgs = parseArgs(args);
      // Bootstrap the kernel
      AbstractBootstrap bootstrap = new BasicBootstrap();
      bootstrap.run();
      kernel = bootstrap.getKernel();
      
      // Create the deployer
      deployer = createDeployer();

      return newArgs;
   }
   /**
    * Extract the launcher specific arguments from the arguments array.
    * @see #init(String[])
    * @param args - the input args to both the launcher and the client main.
    * @return the remaining arguments to pass to the client main.
    */
   private static String[] parseArgs(String[] args)
   {
      ArrayList<String> tmp = new ArrayList<String>();
      for(int n = 0; n < args.length; n ++)
      {
         String arg = args[n];
         if(arg.equalsIgnoreCase("-remotingDelgatesToTCL"))
         {
            remotingDelgatesToTCL = Boolean.parseBoolean(args[++ n]);
         }
         else if(arg.equalsIgnoreCase("-extraClassPath"))
         {
            // Split classpath elements based on ','
            String cparg = args[++ n];
            String[] cp = cparg.split(",");
            for(String path : cp)
            {
               extraClassPath.add(path);
            }
            log.debug("Set extraClassPath to: "+extraClassPath);
         }
         else
         {
            tmp.add(arg);
         }
      }
      String[] newArgs = new String[tmp.size()];
      tmp.toArray(newArgs);
      return newArgs;
   }

   private static BasicXMLDeployer createDeployer()
   {
      return new BasicXMLDeployer(kernel, ControllerMode.AUTOMATIC);
   }

   /**
    * Deploy a deployment
    *
    * @param deployment the deployment
    * @throws Exception for any error  
    */
   private static void deploy(KernelDeployment deployment) throws Exception
   {
      log.debug("Deploying " + deployment);
      try
      {
         deployer.deploy(deployment);
         log.debug("Deployed " + deployment);
      }
      catch (Exception e)
      {
         throw e;
      }
      catch (Error e)
      {
         throw e;
      }
      catch (Throwable t)
      {
         throw new RuntimeException("Error deploying deployment: " + deployment, t);
      }
   }
   /**
    * Undeploy a deployment
    * 
    * @param deployment the deployment
    */
   private static void undeploy(KernelDeployment deployment)
   {
      log.debug("Undeploying " + deployment.getName());
      try
      {
         deployer.undeploy(deployment);
         log.trace("Undeployed " + deployment.getName());
      }
      catch (Throwable t)
      {
         log.warn("Error during undeployment: " + deployment.getName(), t);
      }
   }

   /**
    * Validate
    * 
    * @throws Exception for any error
    */
   private static void validate() throws Exception
   {
      try
      {
         deployer.validate();
      }
      catch (RuntimeException e)
      {
         throw e;
      }
      catch (Exception e)
      {
         throw e;
      }
      catch (Error e)
      {
         throw e;
      }
      catch (Throwable t)
      {
         throw new RuntimeException(t);
      }
   }

   public static void main(String[] args)
   {
      try
      {
         String path = "vfs:///home/carlo/work/jboss-head/build/target/jboss-6.0.0-SNAPSHOT/server/cts/tmp/jsr88/ejb3_timer_basic_mdb.ear/ejb3_timer_basic_mdb_client.jar";
         mount(path);
      }
      catch(Exception e)
      {
         e.printStackTrace();
      }
   }

   private static void mount(String path) throws IOException, URISyntaxException
   {
      URI root = new URI(path);
      VirtualFile file = VFS.getChild(root);

      mount(file);
   }

   private static void mount(VirtualFile target) throws IOException
   {
      if(target.exists())
      {
         Automounter.mount(target);
         return;
      }

      mount(target.getParent());

      Automounter.mount(target);
   }

   /**
    * RMIClassLoaderSpi that uses the thread context class loader
    * 
    * @author <a href="mailto:adrian.brock@happeningtimes.com">Adrian Brock</a>
    * @author Scott.Stark@jboss.org
    * @version $Revision: 106442 $
    */
   public static class JBossRMIClassLoader
      extends RMIClassLoaderSpi
   {
      // Attributes ----------------------------------------------------
   
      /**
       * The JVM implementation (we delegate most work to it)
       */
      RMIClassLoaderSpi delegate = RMIClassLoader.getDefaultProviderInstance();
      
      // Constructors --------------------------------------------------
   
      /**
       * Required constructor
       */
      public JBossRMIClassLoader()
      {
      }
      
      // RMIClassLoaderSpi Implementation ------------------------------
   
      /*
       * Ignore the JVM, use the thread context classloader for proxy caching
       */
      public Class<?> loadProxyClass(String codebase, String[] interfaces, ClassLoader ignored)
         throws MalformedURLException, ClassNotFoundException
      {
         ClassLoader loader = Thread.currentThread().getContextClassLoader();
         return delegate.loadProxyClass(codebase, interfaces, loader);
      }

      /*
       * Just delegate
       */
      public Class<?> loadClass(String codebase, String name, ClassLoader ignored)
         throws MalformedURLException, ClassNotFoundException
      {
         ClassLoader loader = Thread.currentThread().getContextClassLoader();
         return delegate.loadClass(codebase, name, loader);
      }

      /*
       * Just delegate
       */
      public ClassLoader getClassLoader(String codebase)
         throws MalformedURLException
      {
         return delegate.getClassLoader(codebase);
      }
   
      /*
       * Try to delegate an default to the java.rmi.server.codebase on any
       * failure.
       */
      public String getClassAnnotation(Class<?> cl)
      {
         String annotation = null;
         try
         {
            annotation = delegate.getClassAnnotation(cl);
         }
         catch(Throwable t)
         {
            // Try the java.rmi.server.codebase property
            annotation = System.getProperty("java.rmi.server.codebase");
         }
         return annotation;
      }
   }

}
