org.jruby.rack.DefaultRackApplicationFactory Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jruby-rack Show documentation
Show all versions of jruby-rack Show documentation
A servlet bridge for (Ruby-based) Rack applications that allow them
to run in Java Application servers using JRuby.
/*
* 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