/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, 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;

import gnu.getopt.Getopt;
import gnu.getopt.LongOpt;

import java.io.File;
import java.io.FilenameFilter;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

import org.jboss.system.server.Server;
import org.jboss.system.server.ServerConfig;
import org.jboss.system.server.ServerConfigUtil;
import org.jboss.system.server.ServerLoader;

/**
 * Provides a command line interface to start the JBoss server.
 * <p>
 * To enable debug or trace messages durring boot change the Log4j
 * configuration to use either <tt>log4j-debug.properties</tt>
 * <tt>log4j-trace.properties</tt> by setting the system property
 * <tt>log4j.configuration</tt>:
 *
 * <pre>
 *   ./run.sh -Dlog4j.configuration=log4j-debug.properties
 * </pre>
 *
 * @author <a href="mailto:marc.fleury@jboss.org">Marc Fleury</a>
 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
 * @author <a href="mailto:adrian.brock@happeningtimes.com">Adrian Brock</a>
 * @author <a href="mailto:scott.stark@jboss.org">Scott Stark</a>
 * @version $Revision: 1.17.2.20 $
 */
public class Main
{
   /** The JMX library to use. */
   private String jmxLibs = "jboss-jmx.jar,dom4j.jar,jaxen.jar,gnu-regexp.jar";

   /** EDU.oswego.cs.dl.util.concurrent */
   private String concurrentLib = "concurrent.jar";

   /** A URL for obtaining microkernel patches */
   private URL bootURL;
   
   /** Extra jars from the /lib location that are added to the start of the boot
    classpath. This can be used to override jboss /lib boot classes.
    */
   private List bootLibraries = new LinkedList();

   /** Extra libraries to load the server with .*/
   private List extraLibraries = new LinkedList();

   /** Extra classpath URLS to load the server with .*/
   private List extraClasspath = new LinkedList();

   /**
    * Server properties.  This object holds all of the required
    * information to get the server up and running. Use System
    * properties for defaults.
    */
   private Properties props = new Properties(System.getProperties());

   /**
    * Explicit constructor.
    */
   public Main()
   {
      super();
   }

   /**
    * Boot up JBoss.
    *
    * @param args   The command line arguments.
    *
    * @throws Exception    Failed to boot.
    */
   public void boot(final String[] args) throws Exception
   {
      // First process the command line to pickup custom props/settings
      processCommandLine(args);

      // Auto set HOME_DIR to ../bin/run.jar if not set
      String homeDir = props.getProperty(ServerConfig.HOME_DIR);
      if (homeDir == null)
      {
         String path = Main.class.getProtectionDomain().getCodeSource().getLocation().getFile();
         /* The 1.4 JDK munges the code source file with URL encoding so run
          * this path through the decoder so that is JBoss starts in a path with
          * spaces we don't come crashing down.
         */
         path = urlDecode(path); 
         File runJar = new File(path);
         File homeFile = runJar.getParentFile().getParentFile();
         homeDir = homeFile.getCanonicalPath();
      }
      props.setProperty(ServerConfig.HOME_DIR, homeDir);

      // Setup HOME_URL too, ServerLoader needs this
      String homeURL = props.getProperty(ServerConfig.HOME_URL);
      if (homeURL == null)
      {
         File file = new File(homeDir);
         homeURL = file.toURL().toString();
         props.setProperty(ServerConfig.HOME_URL, homeURL);
      }

      // Load the server instance
      ServerLoader loader = new ServerLoader(props);

      /* If there is a patch dir specified make it the first element of the
      loader bootstrap classpath. If its a file url pointing to a dir, then
      add the dir and its contents.
      */
      if (bootURL != null)
      {
         if (bootURL.getProtocol().equals("file"))
         {
            File dir = new File(bootURL.getFile());
            if (dir.exists())
            {
               // Add the local file patch directory
               loader.addURL(dir.toURL());

               // Add the contents of the directory too
               File[] jars = dir.listFiles(new JarFilter());

               for (int j = 0; jars != null && j < jars.length; j++)
               {
                  loader.addURL(jars[j].getCanonicalFile().toURL());
               }
            }
         }
         else
         {
            loader.addURL(bootURL);
         }
      }

      // Add any extra libraries ahead of the default boot libs
      for (int i = 0; i < bootLibraries.size(); i++)
      {
         loader.addLibrary((String)bootLibraries.get(i));
      }

      // Add the jars from the endorsed dir
      loader.addEndorsedJars();

      // Add jmx libs
      loader.addLibraries(jmxLibs);
      
      // jmx UnifiedLoaderRepository needs a concurrent class...
      loader.addLibrary(concurrentLib);

      // Add any extra libraries after the boot libs
      for (int i = 0; i < extraLibraries.size(); i++)
      {
         loader.addLibrary((String)extraLibraries.get(i));
      }

      // Add any extra classapth URLs
      for (int i = 0; i < extraClasspath.size(); i++)
      {
         loader.addURL((URL)extraClasspath.get(i));
      }

      // Load the server
      ClassLoader parentCL = Thread.currentThread().getContextClassLoader();
      Server server = loader.load(parentCL);

      // Initialize the server
      server.init(props);

      // Start 'er up mate!
      server.start();
   }

   private URL makeURL(String urlspec) throws MalformedURLException
   {
      urlspec = urlspec.trim();

      URL url;

      try
      {
         url = new URL(urlspec);
         if (url.getProtocol().equals("file"))
         {
            // make sure the file is absolute & canonical file url
            File file = new File(url.getFile()).getCanonicalFile();
            url = file.toURL();
         }
      }
      catch (Exception e)
      {
         // make sure we have a absolute & canonical file url
         try
         {
            File file = new File(urlspec).getCanonicalFile();
            url = file.toURL();
         }
         catch (Exception n)
         {
            throw new MalformedURLException(n.toString());
         }
      }

      return url;
   }

   /** Process the command line... */
   private void processCommandLine(final String[] args) throws Exception
   {
      // set this from a system property or default to jboss
      String programName = System.getProperty("program.name", "jboss");
      String sopts = "-:hD:d:p:n:c:Vj::B:L:C:P:b:";
      LongOpt[] lopts =
      {
         new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h'),
         new LongOpt("bootdir", LongOpt.REQUIRED_ARGUMENT, null, 'd'),
         new LongOpt("patchdir", LongOpt.REQUIRED_ARGUMENT, null, 'p'),
         new LongOpt("netboot", LongOpt.REQUIRED_ARGUMENT, null, 'n'),
         new LongOpt("configuration", LongOpt.REQUIRED_ARGUMENT, null, 'c'),
         new LongOpt("version", LongOpt.NO_ARGUMENT, null, 'V'),
         new LongOpt("jaxp", LongOpt.REQUIRED_ARGUMENT, null, 'j'),         
         new LongOpt("bootlib", LongOpt.REQUIRED_ARGUMENT, null, 'B'),
         new LongOpt("library", LongOpt.REQUIRED_ARGUMENT, null, 'L'),
         new LongOpt("classpath", LongOpt.REQUIRED_ARGUMENT, null, 'C'),
         new LongOpt("properties", LongOpt.REQUIRED_ARGUMENT, null, 'P'),
         new LongOpt("host", LongOpt.REQUIRED_ARGUMENT, null, 'b'),
      };

      Getopt getopt = new Getopt(programName, args, sopts, lopts);
      int code;
      String arg;
      props.setProperty(ServerConfig.SERVER_BIND_ADDRESS, "0.0.0.0");
      System.setProperty(ServerConfig.SERVER_BIND_ADDRESS, "0.0.0.0");
      while ((code = getopt.getopt()) != -1)
      {
         switch (code)
         {
            case ':':
            case '?':
               // for now both of these should exit with error status
               System.exit(1);
               break; // for completeness

            case 1:
               // this will catch non-option arguments
               // (which we don't currently care about)
               System.err.println(programName +
                                  ": unused non-option argument: " +
                                  getopt.getOptarg());
               break; // for completeness

            case 'h':
               // show command line help
               System.out.println("usage: " + programName + " [options]");
               System.out.println();
               System.out.println("options:");
               System.out.println("    -h, --help                    Show this help message");
               System.out.println("    -V, --version                 Show version information");
               System.out.println("    --                            Stop processing options");
               System.out.println("    -D<name>[=<value>]            Set a system property");
               System.out.println("    -d, --bootdir=<dir>           Set the boot patch directory; Must be absolute or url");
               System.out.println("    -p, --patchdir=<dir>          Set the patch directory; Must be absolute or url");
               System.out.println("    -n, --netboot=<url>           Boot from net with the given url as base");
               System.out.println("    -c, --configuration=<name>    Set the server configuration name");
               System.out.println("    -B, --bootlib=<filename>      Add an extra library to the front bootclasspath");
               System.out.println("    -L, --library=<filename>      Add an extra library to the loaders classpath");
               System.out.println("    -C, --classpath=<url>         Add an extra url to the loaders classpath");
               System.out.println("    -P, --properties=<url>        Load system properties from the given url");
               System.out.println("    -b, --host=<host or ip>       Bind address for all JBoss services");
               System.out.println();
               System.exit(0);
               break; // for completeness

            case 'D':
            {
               // set a system property
               arg = getopt.getOptarg();
               String name, value;
               int i = arg.indexOf("=");
               if (i == -1)
               {
                  name = arg;
                  value = "true";
               }
               else
               {
                  name = arg.substring(0, i);
                  value = arg.substring(i + 1, arg.length());
               }
               System.setProperty(name, value);
               break;
            }

            case 'd':
               // set the boot patch URL
               bootURL = makeURL(getopt.getOptarg());
               break;

            case 'p':
            {
               // set the patch URL
               URL patchURL = makeURL(getopt.getOptarg());
               props.put(ServerConfig.PATCH_URL, patchURL.toString());

               break;
            }

            case 'n':
               // set the net boot url
               arg = getopt.getOptarg();

               // make sure there is a trailing '/'
               if (!arg.endsWith("/")) arg += "/";

               props.put(ServerConfig.HOME_URL, new URL(arg).toString());
               break;

            case 'c':
               // set the server name
               arg = getopt.getOptarg();
               props.put(ServerConfig.SERVER_NAME, arg);
               break;

            case 'V':
            {
               // Package information for org.jboss
               Package jbossPackage = Package.getPackage("org.jboss");

               // show version information
               System.out.println("JBoss " + jbossPackage.getImplementationVersion());
               System.out.println();
               System.out.println("Distributable under LGPL license.");
               System.out.println("See terms of license at gnu.org.");
               System.out.println();
               System.exit(0);
               break; // for completness
            }

            case 'j':
               // Show an error and exit
               System.err.println(programName + ": option '-j, --jaxp' no longer supported");
               System.exit(1);
               break; // for completness

            case 'B':
               arg = getopt.getOptarg();
               bootLibraries.add(arg);
               break;

            case 'L':
               arg = getopt.getOptarg();
               extraLibraries.add(arg);
               break;

            case 'C':
            {
               URL url = makeURL(getopt.getOptarg());
               extraClasspath.add(url);
               break;
            }

            case 'P':
            {
               // Set system properties from url/file
               URL url = makeURL(getopt.getOptarg());
               Properties props = System.getProperties();
               props.load(url.openConnection().getInputStream());
               break;
            }
            case 'b':
               arg = getopt.getOptarg();
               props.put(ServerConfig.SERVER_BIND_ADDRESS, arg);
               System.setProperty(ServerConfig.SERVER_BIND_ADDRESS, arg);
               // used by JGroups
               System.setProperty("bind.address", arg);
               // Set the java.rmi.server.hostname if not set
               String rmiHost = System.getProperty("java.rmi.server.hostname");
               if( rmiHost == null )
               {
                  rmiHost = ServerConfigUtil.fixRemoteAddress(arg);
                  System.setProperty("java.rmi.server.hostname", rmiHost);
               }
               break;
            default:
               // this should not happen,
               // if it does throw an error so we know about it
               throw new Error("unhandled option code: " + code);
         }
      }
   }

   /**
    * Decode the path depending upon whether we have java1.4 or java1.3 installed
    *
    * @param path the path to decode
    * @return the decoded path
    * @throws Exception for any error
    */
   private String urlDecode(String path) throws Exception
   {
      Method decode;
      try
      {
         decode = URLDecoder.class.getMethod("decode", new Class[] { String.class, String.class });
         return (String) decode.invoke(null, new Object[] { path, "UTF-8" });
      }
      catch (NoSuchMethodException e)
      {
         return URLDecoder.decode(path);
      }
   }

   /**
    * This is where the magic begins.
    *
    * <P>Starts up inside of a "jboss" thread group to allow better
    *    identification of JBoss threads.
    *
    * @param args    The command line arguments.
    */
   public static void main(final String[] args) throws Exception
   {
      Runnable worker = new Runnable() {
            public void run()
            {
               try
               {
                  Main main = new Main();
                  main.boot(args);
               }
               catch (Exception e)
               {
                  System.err.println("Failed to boot JBoss:");
                  e.printStackTrace();
               }
            }

         };

      ThreadGroup threads = new ThreadGroup("jboss");
      new Thread(threads, worker, "main").start();
   }

   /**
    * This method is here so that if JBoss is running under
    * Alexandria (An NT Service Installer), Alexandria can shutdown
    * the system down correctly.
    */
   public static void systemExit(String argv[])
   {
      System.exit(0);
   }

   static class JarFilter implements FilenameFilter
   {
      public boolean accept(File dir, String name)
      {
         return name.endsWith(".jar");
      }
   }
}
