Package

Source Code of WarMain

/**
* Copyright (c) 2010-2012 Engine Yard, Inc.
* Copyright (c) 2007-2009 Sun Microsystems, Inc.
* This source code is available under the MIT license.
* See the file LICENSE.txt for details.
*/

import java.lang.reflect.Method;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.SequenceInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.net.URI;
import java.net.URLClassLoader;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.Set;

/**
* Used as a Main-Class in the manifest for a .war file, so that you can run
* a .war file with <tt>java -jar</tt>.
*
* WarMain can be used with different web server libraries. WarMain expects
* to have two files present in the .war file,
* <tt>WEB-INF/webserver.properties</tt> and <tt>WEB-INF/webserver.jar</tt>.
*
* When WarMain starts up, it extracts the webserver jar to a temporary
* directory, and creates a temporary work directory for the webapp. Both
* are deleted on exit.
*
* It then reads webserver.properties into a java.util.Properties object,
* creates a URL classloader holding the jar, and loads and invokes the
* <tt>main</tt> method of the main class mentioned in the properties.
*
* An example webserver.properties follows for Winstone. The <tt>args</tt>
* property indicates the names and ordering of other properties to be used
* as command-line arguments. The special tokens <tt>{{warfile}}</tt> and
* <tt>{{webroot}}</tt> are substituted with the location of the .war file
* being run and the temporary work directory, respectively.
* <pre>
* mainclass = winstone.Launcher
* args = args0,args1,args2
* args0 = --warfile={{warfile}}
* args1 = --webroot={{webroot}}
* args2 = --directoryListings=false
* </pre>
*
* System properties can also be set via webserver.properties. For example,
* the following entries set <tt>jetty.home</tt> before launching the server.
* <pre>
* props = jetty.home
* jetty.home = {{webroot}}
* </pre>
*/
public class WarMain extends JarMain {
   
    static final String MAIN = "/" + WarMain.class.getName().replace('.', '/') + ".class";
    static final String WEBSERVER_PROPERTIES = "/WEB-INF/webserver.properties";
    static final String WEBSERVER_JAR = "/WEB-INF/webserver.jar";

    /**
     *  jruby arguments, consider the following command :
     *    `java -jar rails.war --1.9 -S rake db:migrate`
     *   arguments == [ "--1.9" ]
     *   executable == "rake"
     *   executableArgv == [ "db:migrate" ]
     */
    private final String[] arguments;

    /**
     * null to launch webserver or != null to run a executable e.g. rake
     */
    private final String executable;
    private final String[] executableArgv;
           
    private File webroot;

    WarMain(final String[] args) {
        super(args);
        final List<String> argsList = Arrays.asList(args);
        final int sIndex = argsList.indexOf("-S");
        if ( sIndex == -1 ) {
            executable = null; executableArgv = null; arguments = null;
        }
        else {
            if ( args.length == sIndex + 1 || args[sIndex + 1].isEmpty() ) {
                throw new IllegalArgumentException("missing executable after -S");
            }
            arguments = argsList.subList(0, sIndex).toArray(new String[0]);
            String execArg = argsList.get(sIndex + 1);
            executableArgv = argsList.subList(sIndex + 2, argsList.size()).toArray(new String[0]);

            if (execArg.equals("bundle") && executableArgv.length > 0 && executableArgv[0].equals("exec")) {
                warn("`bundle exec' may drop out of the Warbler environment and into the system environment");
            } else if (execArg.equals("rails")) {
                // The rails executable doesn't play well with ScriptingContainer, so we've packaged the
                // same script that would have been generated by `rake rails:update:bin`
                execArg = "./META-INF/rails.rb";
            }

            executable = execArg;
        }
    }
   
    private URL extractWebserver() throws Exception {
        this.webroot = File.createTempFile("warbler", "webroot");
        this.webroot.delete();
        this.webroot.mkdirs();
        this.webroot = new File(this.webroot, new File(archive).getName());
        debug("webroot directory is " + this.webroot.getPath());
        InputStream jarStream = new URI("jar", entryPath(WEBSERVER_JAR), null).toURL().openStream();
        File jarFile = File.createTempFile("webserver", ".jar");
        jarFile.deleteOnExit();
        FileOutputStream outStream = new FileOutputStream(jarFile);
        try {
            byte[] buf = new byte[4096];
            int bytesRead = 0;
            while ((bytesRead = jarStream.read(buf)) != -1) {
                outStream.write(buf, 0, bytesRead);
            }
        } finally {
            jarStream.close();
            outStream.close();
        }
        debug("webserver.jar extracted to " + jarFile.getPath());
        return jarFile.toURI().toURL();
    }

    private Properties getWebserverProperties() throws Exception {
        Properties props = new Properties();
        try {
            InputStream is = getClass().getResourceAsStream(WEBSERVER_PROPERTIES);
            if ( is != null ) props.load(is);
        } catch (Exception e) { }

        for (Map.Entry entry : props.entrySet()) {
            String val = (String) entry.getValue();
            val = val.replace("{{warfile}}", archive).replace("{{webroot}}", webroot.getAbsolutePath());
            entry.setValue(val);
        }

        if (props.getProperty("props") != null) {
            String[] propsToSet = props.getProperty("props").split(",");
            for (String key : propsToSet) {
                System.setProperty(key, props.getProperty(key));
            }
        }

        return props;
    }

    private void launchWebServer(URL jar) throws Exception {
        URLClassLoader loader = new URLClassLoader(new URL[] {jar});
        Thread.currentThread().setContextClassLoader(loader);
        Properties props = getWebserverProperties();
        String mainClass = props.getProperty("mainclass");
        if (mainClass == null) {
            throw new IllegalArgumentException("unknown webserver main class ("
                                               + WEBSERVER_PROPERTIES
                                               + " is missing 'mainclass' property)");
        }
        Class klass = Class.forName(mainClass, true, loader);
        Method main = klass.getDeclaredMethod("main", new Class[] { String[].class });
        String[] newArgs = launchWebServerArguments(props);
        debug("invoking webserver with: " + Arrays.deepToString(newArgs));
        main.invoke(null, new Object[] { newArgs });

        // the following code is specific to winstone. but a whole winstone module like the jetty module seemed
        // excessive. if running under jetty (or anything other than wintstone) this will effectively do nothing.
        Set<Thread> threads = Thread.getAllStackTraces().keySet();
        for (Thread thread : threads) {
            String name = thread.getName();
            if (name.startsWith("LauncherControlThread")) {
                debug("joining thread: " + name);
                thread.join();
            }
        }
    }

    private String[] launchWebServerArguments(Properties props) {
        String[] newArgs = args;

        if (props.getProperty("args") != null) {
            String[] insertArgs = props.getProperty("args").split(",");
            newArgs = new String[args.length + insertArgs.length];
            for (int i = 0; i < insertArgs.length; i++) {
                newArgs[i] = props.getProperty(insertArgs[i], "");
            }
            System.arraycopy(args, 0, newArgs, insertArgs.length, args.length);
        }

        return newArgs;
    }

    // JarMain overrides to make WarMain "launchable"
    // e.g. java -jar rails.war -S rake db:migrate
   
    @Override
    protected String getExtractEntryPath(final JarEntry entry) {
        final String name = entry.getName();
        final String start = "WEB-INF";
        if ( name.startsWith(start) ) {
            // WEB-INF/app/controllers/application_controller.rb ->
            // app/controllers/application_controller.rb
            return name.substring(start.length());
        }
        if ( name.indexOf('/') == -1 ) {
            // 404.html -> public/404.html
            return "/public/" + name;
        }
        return "/" + name;
    }
   
    @Override
    protected URL extractEntry(final JarEntry entry, final String path) throws Exception {
        // always extract but only return class-path entry URLs :
        final URL entryURL = super.extractEntry(entry, path);
        return path.endsWith(".jar") ? entryURL : null;
    }
   
    @Override
    protected int launchJRuby(final URL[] jars) throws Exception {
        final Object scriptingContainer = newScriptingContainer(jars);

        invokeMethod(scriptingContainer, "setArgv", (Object) executableArgv);
        invokeMethod(scriptingContainer, "setCurrentDirectory", extractRoot.getAbsolutePath());
        initJRubyScriptingEnv(scriptingContainer, jars);

        final Object provider = invokeMethod(scriptingContainer, "getProvider");
        final Object rubyInstanceConfig = invokeMethod(provider, "getRubyInstanceConfig");
       
        invokeMethod(rubyInstanceConfig, "setUpdateNativeENVEnabled", new Class[] { Boolean.TYPE }, false);
       
        final String executablePath = locateExecutable(scriptingContainer);
        if ( executablePath == null ) {
            throw new IllegalStateException("failed to locate gem executable: '" + executable + "'");
        }
        invokeMethod(scriptingContainer, "setScriptFilename", executablePath);
       
        invokeMethod(rubyInstanceConfig, "processArguments", (Object) arguments);
       
        Object runtime = invokeMethod(scriptingContainer, "getRuntime");
        Object executableInput =
            new SequenceInputStream(new ByteArrayInputStream(executableScriptEnvPrefix().getBytes()),
                                    (InputStream) invokeMethod(rubyInstanceConfig, "getScriptSource"));
       
        debug("invoking " + executablePath + " with: " + Arrays.toString(executableArgv));
       
        Object outcome = invokeMethod(runtime, "runFromMain",
                new Class[] { InputStream.class, String.class },
                executableInput, executablePath
        );
        return ( outcome instanceof Number ) ? ( (Number) outcome ).intValue() : 0;
    }
   
    protected String locateExecutable(final Object scriptingContainer) throws Exception {
        if ( executable == null ) {
            throw new IllegalStateException("no executable");
        }
        final File exec = new File(extractRoot, executable);
        if ( exec.exists() ) {
            return exec.getAbsolutePath();
        }
        else {
            final String script = locateExecutableScript(executable);
            return (String) invokeMethod(scriptingContainer, "runScriptlet", script);
        }
    }
    protected String executableScriptEnvPrefix() {
        final String gemsDir = new File(extractRoot, "gems").getAbsolutePath();
        final String gemfile = new File(extractRoot, "Gemfile").getAbsolutePath();
        debug("setting GEM_HOME to " + gemsDir);
        debug("... and BUNDLE_GEMFILE to " + gemfile);

        // ideally this would look up the config.override_gem_home setting
        return "ENV['GEM_HOME'] = ENV['GEM_PATH'] = '"+ gemsDir +"' \n" +
        "ENV['BUNDLE_GEMFILE'] ||= '"+ gemfile +"' \n" +
        "require 'META-INF/init.rb' \n";
    }

    protected String locateExecutableScript(final String executable) {
        return executableScriptEnvPrefix() +
        "begin\n" +
        // locate the executable within gemspecs :
        "  require 'rubygems' \n" +
            "  begin\n" +
            // add bundler gems to load path:
            "    require 'bundler' \n" +
            // TODO: environment from web.xml. Any others?
            "    Bundler.setup(:default, *ENV.values_at('RACK_ENV', 'RAILS_ENV').compact)\n" +
            "  rescue LoadError\n" +
            // bundler not used
            "  end\n" +
        "  exec = '"+ executable +"' \n" +
        "  spec = Gem::Specification.find { |s| s.executables.include?(exec) } \n" +
        "  spec ? spec.bin_file(exec) : nil \n" +
        // returns the full path to the executable
        "rescue SystemExit => e\n" +
        "  e.status\n" +
        "end";
    }

    protected void initJRubyScriptingEnv(Object scriptingContainer, final URL[] jars) throws Exception {
        String jrubyStdlibJar = "";
        String bcpkixJar = "";
        String bcprovJar = "";
        for (URL url : jars) {
            if (url.toString().matches("file:/.*jruby-stdlib-.*jar")) {
                jrubyStdlibJar = url.toString();
                debug("using jruby-stdlib: " + jrubyStdlibJar);
            } else if (url.toString().matches("file:/.*bcpkix-jdk15on-.*jar")) {
                bcpkixJar = url.toString();
                debug("using bcpkix: " + bcpkixJar);
            } else if (url.toString().matches("file:/.*bcprov-jdk15on-.*jar")) {
                bcprovJar = url.toString();
                debug("using bcprov: " + bcprovJar);
            }
        }

        invokeMethod(scriptingContainer, "runScriptlet", "" +
            "ruby = RUBY_VERSION.match(/^\\d\\.\\d/)[0] \n" +
            "jruby_major_version = JRUBY_VERSION.match(/^\\d\\.\\d/)[0].to_f \n" +
            "jruby_minor_version = JRUBY_VERSION.split('.')[2].to_i\n" +
            "$: << \"" + jrubyStdlibJar + "!/META-INF/jruby.home/lib/ruby/#{ruby}/site_ruby\"\n" +
            "$: << \"" + jrubyStdlibJar + "!/META-INF/jruby.home/lib/ruby/shared\"\n" +
            "$: << \"" + jrubyStdlibJar + "!/META-INF/jruby.home/lib/ruby/#{ruby}\"\n" +
            "if jruby_major_version >= 1.7 and jruby_minor_version < 13\n" +
            "  require \"" + bcpkixJar + "\".gsub('file:', '') unless \"" + bcpkixJar + "\".empty?\n" +
            "  require \"" + bcprovJar + "\".gsub('file:', '') unless \"" + bcprovJar + "\".empty?\n" +
            "end");

        invokeMethod(scriptingContainer, "setHomeDirectory", "classpath:/META-INF/jruby.home");
    }

    @Override
    protected int start() throws Exception {
        if ( executable == null ) {
            try {
                URL server = extractWebserver();
                launchWebServer(server);
            }
            catch (FileNotFoundException e) {
                if ( e.getMessage().indexOf("WEB-INF/webserver.jar") > -1 ) {
                    System.out.println("specify the -S argument followed by the bin file to run e.g. `java -jar rails.war -S rake -T` ...");
                    System.out.println("(or if you'd like your .war file to start a web server package it using `warbler executable war`)");
                }
                throw e;
            }
            return 0;
        }
        else {
            return super.start();
        }
    }

    @Override
    public void run() {
        super.run();
        if ( webroot != null ) delete(webroot.getParentFile());
    }

    public static void main(String[] args) {
        doStart(new WarMain(args));
    }

}
TOP

Related Classes of WarMain

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.