All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jruby.rack.DefaultRackApplicationFactory Maven / Gradle / Ivy

Go to download

A servlet bridge for (Ruby-based) Rack applications that allow them to run in Java Application servers using JRuby.

There is a newer version: 1.2.2
Show newest version
/*
 * 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.
 */

package org.jruby.rack;


import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
import java.util.Set;

import org.jruby.Ruby;
import org.jruby.RubyInstanceConfig;
import org.jruby.exceptions.RaiseException;
import org.jruby.javasupport.JavaUtil;
import org.jruby.rack.servlet.ServletRackContext;
import org.jruby.rack.servlet.RewindableInputStream;
import org.jruby.rack.util.IOHelpers;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;

import static org.jruby.rack.DefaultRackConfig.isIgnoreRUBYOPT;

/**
 * Default application factory creates a new application instance on each
 * {@link #getApplication()} invocation. It does not manage applications it
 * creates (except for the error application that is assumed to be shared).
 *
 * @see SharedRackApplicationFactory
 * @see PoolingRackApplicationFactory
 *
 * @author nicksieger
 */
public class DefaultRackApplicationFactory implements RackApplicationFactory {

    private String rackupScript, rackupLocation;
    private ServletRackContext rackContext;
    private RubyInstanceConfig runtimeConfig;
    private RackApplication errorApplication;

    /**
     * Convenience helper for unwrapping a {@link RackApplicationFactoryDecorator}.
     * @param factory the (likely decorated) factory
     * @return unwrapped "real" factory (might be the same as given)
     */
    public static RackApplicationFactory getRealFactory(final RackApplicationFactory factory) {
        if ( factory instanceof RackApplicationFactory.Decorator ) {
            return getRealFactory( ((Decorator) factory).getDelegate() );
        }
        return factory;
    }

    public RackContext getRackContext() {
        return rackContext;
    }

    public String getRackupScript() {
        return rackupScript;
    }

    public void setRackupScript(String rackupScript) {
        this.rackupScript = rackupScript;
        this.rackupLocation = null;
    }

    /**
     * Initialize this factory using the given context.
     * 
* NOTE: exception handling is left to the outer factory. * @param rackContext */ public void init(final RackContext rackContext) { // NOTE: this factory is not supposed to be directly exposed // thus does not wrap exceptions into RackExceptions here ... // same applies for #newApplication() and #getApplication() this.rackContext = (ServletRackContext) rackContext; if ( getRackupScript() == null ) resolveRackupScript(); this.runtimeConfig = createRuntimeConfig(); rackContext.log(RackLogger.INFO, runtimeConfig.getVersionString()); configureDefaults(); } /** * Creates a new application instance (without initializing it). *
* NOTE: exception handling is left to the outer factory. * @return new application instance */ public RackApplication newApplication() { return createApplication(new ApplicationObjectFactory() { public IRubyObject create(Ruby runtime) { return createApplicationObject(runtime); } }); } /** * Creates a new application and initializes it. *
* NOTE: exception handling is left to the outer factory. * @return new, initialized application */ public RackApplication getApplication() { final RackApplication app = newApplication(); app.init(); return app; } /** * Destroys the application (assumably) created by this factory. *
* NOTE: exception handling is left to the outer factory. * @param app the application to "release" */ public void finishedWithApplication(final RackApplication app) { if ( app != null ) app.destroy(); } /** * @return the (default) error application */ public RackApplication getErrorApplication() { if (errorApplication == null) { synchronized(this) { if (errorApplication == null) { errorApplication = newErrorApplication(); } } } return errorApplication; } /** * Set the (default) error application to be used. * @param errorApplication */ public synchronized void setErrorApplication(RackApplication errorApplication) { this.errorApplication = errorApplication; } public void destroy() { if (errorApplication != null) { synchronized(this) { if (errorApplication != null) { errorApplication.destroy(); errorApplication = null; } } } } public IRubyObject createApplicationObject(final Ruby runtime) { if (rackupScript == null) { rackContext.log(RackLogger.WARN, "no rackup script found - starting empty Rack application!"); rackupScript = ""; } checkAndSetRackVersion(runtime); runtime.evalScriptlet("load 'jruby/rack/boot/rack.rb'"); return createRackServletWrapper(runtime, rackupScript, rackupLocation); } public IRubyObject createErrorApplicationObject(final Ruby runtime) { String errorApp = rackContext.getConfig().getProperty("jruby.rack.error.app"); String errorAppPath = ""; if (errorApp == null) { errorApp = rackContext.getConfig().getProperty("jruby.rack.error.app.path"); if (errorApp != null) { errorAppPath = rackContext.getRealPath(errorApp); try { errorApp = IOHelpers.inputStreamToString(rackContext.getResourceAsStream(errorApp)); } catch (IOException e) { rackContext.log(RackLogger.WARN, "failed to read jruby.rack.error.app.path = '" + errorApp + "' " + "will use default error application", e); errorApp = errorAppPath = null; } } } if (errorApp == null) { errorApp = "require 'jruby/rack/error_app' \n" + "use JRuby::Rack::ErrorApp::ShowStatus \n" + "run JRuby::Rack::ErrorApp.new"; } runtime.evalScriptlet("load 'jruby/rack/boot/rack.rb'"); return createRackServletWrapper(runtime, errorApp, errorAppPath); } public RackApplication newErrorApplication() { Boolean error = rackContext.getConfig().getBooleanProperty("jruby.rack.error"); if ( error != null && ! error.booleanValue() ) { // jruby.rack.error = false return new DefaultErrorApplication(rackContext); } try { RackApplication app = createErrorApplication( new ApplicationObjectFactory() { public IRubyObject create(Ruby runtime) { return createErrorApplicationObject(runtime); } } ); app.init(); return app; } catch (Exception e) { rackContext.log(RackLogger.WARN, "error application could not be initialized", e); return new DefaultErrorApplication(rackContext); // backwards compatibility } } /** * @see #createRackServletWrapper(Ruby, String, String) * @param runtime * @param rackup * @return (Ruby) built Rack Servlet handler */ protected IRubyObject createRackServletWrapper(Ruby runtime, String rackup) { return createRackServletWrapper(runtime, rackup, null); } /** * Creates the handler to bridge the Servlet and Rack worlds. * @param runtime * @param rackup * @param filename * @return (Ruby) built Rack Servlet handler */ protected IRubyObject createRackServletWrapper(Ruby runtime, String rackup, String filename) { return runtime.executeScript( "Rack::Handler::Servlet.new( " + "Rack::Builder.new { (" + rackup + "\n) }.to_app " + ")", filename ); } static interface ApplicationObjectFactory { IRubyObject create(Ruby runtime) ; } public RubyInstanceConfig createRuntimeConfig() { setupJRubyManagement(); return initRuntimeConfig(new RubyInstanceConfig()); } protected RubyInstanceConfig initRuntimeConfig(final RubyInstanceConfig config) { final RackConfig rackConfig = rackContext.getConfig(); config.setLoader(Thread.currentThread().getContextClassLoader()); // Don't affect the container and sibling web apps when ENV changes are // made inside the Ruby app ... // There are quite a such things made in a typical Bundler based app. try { // config.setUpdateNativeENVEnabled(false) using reflection : final Method setUpdateNativeENVEnabled = config.getClass().getMethod("setUpdateNativeENVEnabled", Boolean.TYPE); setUpdateNativeENVEnabled.invoke(config, false); } catch (NoSuchMethodException e) { // ignore method has been added in JRuby 1.6.7 rackContext.log(RackLogger.DEBUG, "envronment changes made inside one app " + "might affect another, consider updating JRuby if this is an issue"); } catch (IllegalAccessException e) { rackContext.log(RackLogger.WARN, "failed to disable updating native environment", e); // throw new RackException(e); } catch (InvocationTargetException e) { throw new RackException(e.getTargetException()); } final Map newEnv = rackConfig.getRuntimeEnvironment(); if ( newEnv != null ) { if ( ! newEnv.containsKey("PATH") ) { // bundler 1.1.x assumes ENV['PATH'] is a string // `ENV['PATH'].split(File::PATH_SEPARATOR)` ... newEnv.put("PATH", ""); // ENV['PATH'] = '' } // bundle exec sets RUBYOPT="-I[...]/gems/bundler/lib -rbundler/setup" if ( isIgnoreRUBYOPT(rackConfig) ) { if ( newEnv.containsKey("RUBYOPT") ) newEnv.put("RUBYOPT", ""); } else { // allow to work (backwards) "compatibly" with previous `ENV.clear` // RUBYOPT was processed since it happens on config.processArguments @SuppressWarnings("unchecked") final Map env = config.getEnvironment(); if ( env != null && env.containsKey("RUBYOPT") ) { newEnv.put( "RUBYOPT", env.get("RUBYOPT") ); } } config.setEnvironment(newEnv); } // Process arguments, namely any that might be in RUBYOPT config.processArguments(rackConfig.getRuntimeArguments()); if ( rackConfig.getCompatVersion() != null ) { config.setCompatVersion(rackConfig.getCompatVersion()); } try { // try to set jruby home to jar file path final URL resource = Ruby.class.getResource("/META-INF/jruby.home"); if ( resource != null && "jar".equals( resource.getProtocol() ) ) { String home = config.getJRubyHome(); // uri: protocol only since 9k : if ( home == null || ! home.startsWith("uri:classloader:") ) { try { home = resource.toURI().getSchemeSpecificPart(); } catch (URISyntaxException e) { home = resource.getPath(); } final int last = home.length() - 1; // trailing '/' confuses OSGi containers... if ( home.charAt(last) == '/' ) home = home.substring(0, last); config.setJRubyHome(home); } } } catch (Exception e) { rackContext.log(RackLogger.DEBUG, "won't set-up jruby.home from jar", e); } return config; } public Ruby newRuntime() throws RaiseException { final Ruby runtime = Ruby.newInstance(runtimeConfig); initRuntime(runtime); return runtime; } /** * Initializes the runtime (exports the context, boots the Rack handler). * * NOTE: (package) visible due specs * * @param runtime */ void initRuntime(final Ruby runtime) { // set $servlet_context : runtime.getGlobalVariables().set( "$servlet_context", JavaUtil.convertJavaToRuby(runtime, rackContext) ); // load our (servlet) Rack handler : runtime.evalScriptlet("require 'rack/handler/servlet'"); // NOTE: this is experimental stuff and might change in the future : String env = rackContext.getConfig().getProperty("jruby.rack.handler.env"); // currently supported "env" values are 'default' and 'servlet' if ( env != null ) { runtime.evalScriptlet("Rack::Handler::Servlet.env = '" + env + "'"); } String response = rackContext.getConfig().getProperty("jruby.rack.handler.response"); if ( response == null ) { response = rackContext.getConfig().getProperty("jruby.rack.response"); } if ( response != null ) { // JRuby::Rack::JettyResponse -> 'jruby/rack/jetty_response' runtime.evalScriptlet("Rack::Handler::Servlet.response = '" + response + "'"); } // configure (Ruby) bits and pieces : String dechunk = rackContext.getConfig().getProperty("jruby.rack.response.dechunk"); Boolean dechunkFlag = (Boolean) DefaultRackConfig.toStrictBoolean(dechunk, null); if ( dechunkFlag != null ) { runtime.evalScriptlet("JRuby::Rack::Response.dechunk = " + dechunkFlag + ""); } else { // dechunk null (default) or not a true/false value ... we're patch : runtime.evalScriptlet("JRuby::Rack::Booter.on_boot { require 'jruby/rack/chunked' }"); // `require 'jruby/rack/chunked'` that happens after Rack is loaded } String swallowAbort = rackContext.getConfig().getProperty("jruby.rack.response.swallow_client_abort"); Boolean swallowAbortFlag = (Boolean) DefaultRackConfig.toStrictBoolean(swallowAbort, null); if ( swallowAbortFlag != null ) { runtime.evalScriptlet("JRuby::Rack::Response.swallow_client_abort = " + swallowAbortFlag + ""); } } /** * Checks and sets the required Rack version (if specified as a magic comment). * * e.g. # rack.version: ~>1.3.6 * * NOTE: (package) visible due specs * * @param runtime * @return the rack version requirement */ String checkAndSetRackVersion(final Ruby runtime) { String rackVersion = null; try { rackVersion = IOHelpers.rubyMagicCommentValue(rackupScript, "rack.version:"); } catch (Exception e) { rackContext.log(RackLogger.DEBUG, "could not read 'rack.version' magic comment from rackup", e); } if ( rackVersion == null ) { // NOTE: try matching a `require 'bundler/setup'` line ... maybe not ?! } if ( rackVersion != null ) { runtime.evalScriptlet("require 'rubygems'"); if ( rackVersion.equalsIgnoreCase("bundler") ) { runtime.evalScriptlet("require 'bundler/setup'"); } else { rackContext.log(RackLogger.DEBUG, "detected 'rack.version' magic comment, " + "will use `gem 'rack', '"+ rackVersion +"'`"); runtime.evalScriptlet("gem 'rack', '"+ rackVersion +"' if defined? gem"); } } return rackVersion; } private RackApplication createApplication(final ApplicationObjectFactory appFactory) { return new RackApplicationImpl(appFactory); } /** * The application implementation this factory is producing. */ private class RackApplicationImpl extends DefaultRackApplication { protected final Ruby runtime; final ApplicationObjectFactory appFactory; RackApplicationImpl(ApplicationObjectFactory appFactory) { this.runtime = newRuntime(); this.appFactory = appFactory; } @Override public void init() { try { setApplication(appFactory.create(runtime)); } catch (RaiseException e) { captureMessage(e); throw e; } } @Override public void destroy() { runtime.tearDown(false); } } private RackApplication createErrorApplication(final ApplicationObjectFactory appFactory) { // final Ruby runtime = newRuntime(); return new ErrorApplicationImpl(appFactory); } private class ErrorApplicationImpl extends RackApplicationImpl implements ErrorApplication { ErrorApplicationImpl(ApplicationObjectFactory appFactory) { super(appFactory); } @Override public void init() { setApplication(appFactory.create(runtime)); } } private void captureMessage(final RaiseException re) { try { IRubyObject rubyException = re.getException(); ThreadContext context = rubyException.getRuntime().getCurrentContext(); // JRuby-Rack internals (@see jruby/rack/capture.rb) : rubyException.callMethod(context, "capture"); rubyException.callMethod(context, "store"); } catch (Exception e) { rackContext.log(RackLogger.INFO, "failed to capture exception message", e); // won't be able to capture anything } } private String findConfigRuPathInSubDirectories(final String path, int level) { @SuppressWarnings("unchecked") final Set entries = rackContext.getResourcePaths(path); if (entries != null) { String config_ru = path + "config.ru"; if ( entries.contains(config_ru) ) { return config_ru; } if (level > 0) { level--; for ( String subpath : entries ) { final int len = subpath.length(); if ( len > 0 && subpath.charAt(len - 1) == '/' ) { subpath = findConfigRuPathInSubDirectories(subpath, level); if ( subpath != null ) return subpath; } } } } return null; } private static String getContextLoaderScript(final String name, final boolean silent) throws IOException { try { // still try context-loader for resolving rackup : final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); InputStream is = contextLoader.getResourceAsStream(name); return IOHelpers.inputStreamToString(is); } catch (IOException e) { if ( silent ) return null; throw e; } catch (RuntimeException e) { if ( silent ) return null; throw e; } } private String resolveRackupScript() throws RackInitializationException { String rackup = rackContext.getConfig().getRackup(); if (rackup == null) { rackup = rackContext.getConfig().getRackupPath(); if (rackup == null) { rackup = findConfigRuPathInSubDirectories("/WEB-INF/", 1); } if (rackup == null) { // google-appengine gem prefers it at /config.ru // appengine misses "/" resources. Search for it directly. String rackupPath = rackContext.getRealPath("/config.ru"); if (rackupPath != null && new File(rackupPath).exists()) { rackup = "/config.ru"; } } if (rackup != null) { InputStream is; try { is = rackContext.getResourceAsStream(rackup); rackupLocation = rackContext.getRealPath(rackup); return this.rackupScript = IOHelpers.inputStreamToString(is); } catch (IOException e) { try { // last - try context-loader for resolving rackup : if ( (rackup = getContextLoaderScript(rackup, true)) != null ) { return this.rackupScript = rackup; } } catch (IOException ex) { /* won't happen */ } rackContext.log(RackLogger.ERROR, "failed to read rackup from '"+ rackup + "' (" + e + ")"); throw new RackInitializationException("failed to read rackup input", e); } } else { rackup = "config.ru"; try { rackup = getContextLoaderScript(rackup, false); rackupLocation = "uri:classloader://config.ru"; } catch (IOException e) { rackContext.log(RackLogger.ERROR, "failed to read rackup from '"+ rackup + "' (" + e + ")"); throw new RackInitializationException("failed to read rackup input", e); } } } else { rackupLocation = ""; } return this.rackupScript = rackup; } private void configureDefaults() { // configure (default) jruby.rack.request.size.[...] parameters : final RackConfig config = rackContext.getConfig(); Integer iniSize = config.getInitialMemoryBufferSize(); if (iniSize == null) iniSize = RewindableInputStream.INI_BUFFER_SIZE; Integer maxSize = config.getMaximumMemoryBufferSize(); if (maxSize == null) maxSize = RewindableInputStream.MAX_BUFFER_SIZE; if (iniSize.intValue() > maxSize.intValue()) iniSize = maxSize; RewindableInputStream.setDefaultInitialBufferSize(iniSize); RewindableInputStream.setDefaultMaximumBufferSize(maxSize); } private static void setupJRubyManagement() { final String jrubyMxEnabled = "jruby.management.enabled"; if ( ! "false".equalsIgnoreCase( System.getProperty(jrubyMxEnabled) ) ) { System.setProperty(jrubyMxEnabled, "true"); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy