org.jruby.runtime.load.LoadService Maven / Gradle / Ivy
/***** BEGIN LICENSE BLOCK *****
* Version: CPL 1.0/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Common Public
* License Version 1.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of
* the License at http://www.eclipse.org/legal/cpl-v10.html
*
* Software distributed under the License is distributed on an "AS
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
* implied. See the License for the specific language governing
* rights and limitations under the License.
*
* Copyright (C) 2002-2011 JRuby Community
* Copyright (C) 2002-2004 Anders Bengtsson
* Copyright (C) 2002-2004 Jan Arne Petersen
* Copyright (C) 2004 Thomas E Enebo
* Copyright (C) 2004-2005 Charles O Nutter
* Copyright (C) 2004 Stefan Matthias Aust
* Copyright (C) 2006 Ola Bini
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the CPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the CPL, the GPL or the LGPL.
***** END LICENSE BLOCK *****/
package org.jruby.runtime.load;
import org.jruby.util.collections.StringArraySet;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipException;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyFile;
import org.jruby.RubyHash;
import org.jruby.RubyInstanceConfig;
import org.jruby.RubyString;
import org.jruby.ast.executable.Script;
import org.jruby.exceptions.MainExitException;
import org.jruby.exceptions.RaiseException;
import org.jruby.ext.rbconfig.RbConfigLibrary;
import org.jruby.platform.Platform;
import org.jruby.runtime.Block;
import org.jruby.runtime.Constants;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.JRubyFile;
import org.jruby.util.log.Logger;
import org.jruby.util.log.LoggerFactory;
import static org.jruby.util.URLUtil.getPath;
/**
* How require works in JRuby
*
* When requiring a name from Ruby, JRuby will first remove any file
* extension it knows about, thereby making it possible to use this string
* to see if JRuby has already loaded the name in question. If a .rb
* extension is specified, JRuby will only try those extensions when
* searching. If a .so, .o, .dll, or .jar extension is specified, JRuby will
* only try .so or .jar when searching. Otherwise, JRuby goes through the
* known suffixes (.rb, .rb.ast.ser, .so, and .jar) and tries to find a
* library with this name. The process for finding a library follows this
* order for all searchable extensions:
*
*
* - First, check if the name starts with 'jar:', then the path points to
* a jar-file resource which is returned.
* - Second, try searching for the file in the current dir
* - Then JRuby looks through the load path trying these variants:
*
* - See if the current load path entry starts with 'jar:', if so
* check if this jar-file contains the name
* - Otherwise JRuby tries to construct a path by combining the entry
* and the current working directy, and then see if a file with the
* correct name can be reached from this point.
*
*
* - If all these fail, try to load the name as a resource from
* classloader resources, using the bare name as well as the load path
* entries
* - When we get to this state, the normal JRuby loading has failed. At
* this stage JRuby tries to load Java native extensions, by following this
* process:
*
* - First it checks that we haven't already found a library. If we
* found a library of type JarredScript, the method continues.
*
* - The first step is translating the name given into a valid Java
* Extension class name. First it splits the string into each path
* segment, and then makes all but the last downcased. After this it
* takes the last entry, removes all underscores and capitalizes each
* part separated by underscores. It then joins everything together and
* tacks on a 'Service' at the end. Lastly, it removes all leading dots,
* to make it a valid Java FWCN.
*
* - If the previous library was of type JarredScript, we try to add
* the jar-file to the classpath
*
* - Now JRuby tries to instantiate the class with the name
* constructed. If this works, we return a ClassExtensionLibrary.
* Otherwise, the old library is put back in place, if there was one.
*
*
* - When all separate methods have been tried and there was no result, a
* LoadError will be raised.
* - Otherwise, the name will be added to the loaded features, and the
* library loaded
*
*
* How to make a class that can get required by JRuby
*
* First, decide on what name should be used to require the extension. In
* this purely hypothetical example, this name will be
* 'active_record/connection_adapters/jdbc_adapter'. Then create the class
* name for this require-name, by looking at the guidelines above. Our class
* should be named active_record.connection_adapters.JdbcAdapterService, and
* implement one of the library-interfaces. The easiest one is
* BasicLibraryService, where you define the basicLoad-method, which will
* get called when your library should be loaded.
*
* The next step is to either put your compiled class on JRuby's
* classpath, or package the class/es inside a jar-file. To package into a
* jar-file, we first create the file, then rename it to jdbc_adapter.jar.
* Then we put this jar-file in the directory
* active_record/connection_adapters somewhere in JRuby's load path. For
* example, copying jdbc_adapter.jar into
* JRUBY_HOME/lib/ruby/site_ruby/1.8/active_record/connection_adapters will
* make everything work. If you've packaged your extension inside a RubyGem,
* write a setub.rb-script that copies the jar-file to this place.
*
* @author jpetersen
*/
public class LoadService {
private static final Logger LOG = LoggerFactory.getLogger("LoadService");
private final LoadTimer loadTimer;
public enum SuffixType {
Source, Extension, Both, Neither;
private static final String[] emptySuffixes = { "" };
// NOTE: always search .rb first for speed
public static final String[] sourceSuffixes = { ".rb", ".class" };
public static final String[] extensionSuffixes;
private static final String[] allSuffixes;
static { // compute based on platform
extensionSuffixes = new String[3];
extensionSuffixes[0] = ".jar";
if (Platform.IS_WINDOWS) {
extensionSuffixes[1] = ".dll";
} else if (Platform.IS_MAC) { // TODO: BSD also?
extensionSuffixes[1] = ".bundle";
} else {
extensionSuffixes[1] = ".so";
}
extensionSuffixes[2] = ".jar.rb";
allSuffixes = new String[sourceSuffixes.length + extensionSuffixes.length];
System.arraycopy(sourceSuffixes, 0, allSuffixes, 0, sourceSuffixes.length);
System.arraycopy(extensionSuffixes, 0, allSuffixes, sourceSuffixes.length, extensionSuffixes.length);
}
public String[] getSuffixes() {
switch (this) {
case Source:
return sourceSuffixes;
case Extension:
return extensionSuffixes;
case Both:
return allSuffixes;
case Neither:
return emptySuffixes;
}
throw new RuntimeException("Unknown SuffixType: " + this);
}
}
protected static final Pattern sourcePattern = Pattern.compile("\\.(?:rb)$");
protected static final Pattern extensionPattern = Pattern.compile("\\.(?:so|o|dll|bundle|jar)$");
protected RubyArray loadPath;
protected StringArraySet loadedFeatures;
protected final Map builtinLibraries = new HashMap();
protected final Map jarFiles = new HashMap();
protected final Ruby runtime;
public LoadService(Ruby runtime) {
this.runtime = runtime;
if (RubyInstanceConfig.DEBUG_LOAD_TIMINGS) {
loadTimer = new TracingLoadTimer();
} else {
loadTimer = new LoadTimer();
}
}
/**
* Called to initialize the load path with a set of optional prepended
* directories and then the standard set of dirs.
*
* This should only be called once, at load time, since it wipes out loaded
* features.
*
* @param prependDirectories
*/
public void init(List prependDirectories) {
loadPath = RubyArray.newArray(runtime);
String jrubyHome = runtime.getJRubyHome();
loadedFeatures = new StringArraySet(runtime);
// add all startup load paths to the list first
addPaths(prependDirectories);
// add $RUBYLIB paths
RubyHash env = (RubyHash) runtime.getObject().getConstant("ENV");
RubyString env_rubylib = runtime.newString("RUBYLIB");
if (env.has_key_p(env_rubylib).isTrue()) {
String rubylib = env.op_aref(runtime.getCurrentContext(), env_rubylib).toString();
String[] paths = rubylib.split(File.pathSeparator);
addPaths(paths);
}
// wrap in try/catch for security exceptions in an applet
try {
if (jrubyHome != null) {
// siteDir has to come first, because rubygems insert paths after it
// and we must to prefer Gems to rubyLibDir/rubySharedLibDir (same as MRI)
addPath(RbConfigLibrary.getSiteDir(runtime));
// if vendorDirGeneral is different than siteDirGeneral,
// add vendorDir, too
// adding {vendor,site}{Lib,Arch}Dir dirs is not necessary,
// since they should be the same as {vendor,site}Dir
if (!RbConfigLibrary.isSiteVendorSame(runtime)) {
addPath(RbConfigLibrary.getVendorDir(runtime));
}
String rubygemsDir = RbConfigLibrary.getRubygemsDir(runtime);
if (rubygemsDir != null) {
addPath(rubygemsDir);
}
addPath(RbConfigLibrary.getRubySharedLibDir(runtime));
addPath(RbConfigLibrary.getRubyLibDir(runtime));
}
} catch(SecurityException ignore) {}
// "." dir is used for relative path loads from a given file, as in require '../foo/bar'
if (!runtime.is1_9()) {
addPath(".");
}
}
/**
* Add additional directories to the load path.
*
* @param additionalDirectories a List of additional dirs to append to the load path
*/
public void addPaths(List additionalDirectories) {
for (String dir : additionalDirectories) {
addPath(dir);
}
}
/**
* Add additional directories to the load path.
*
* @param additionalDirectories an array of additional dirs to append to the load path
*/
public void addPaths(String... additionalDirectories) {
for (String dir : additionalDirectories) {
addPath(dir);
}
}
protected void addLoadedFeature(String name) {
loadedFeatures.append(RubyString.newString(runtime, name));
}
protected void addPath(String path) {
// Empty paths do not need to be added
if (path == null || path.length() == 0) return;
synchronized(loadPath) {
loadPath.append(runtime.newString(path.replace('\\', '/')));
}
}
public void load(String file, boolean wrap) {
if(!runtime.getProfile().allowLoad(file)) {
throw runtime.newLoadError("no such file to load -- " + file);
}
SearchState state = new SearchState(file);
state.prepareLoadSearch(file);
Library library = findBuiltinLibrary(state, state.searchFile, state.suffixType);
if (library == null) library = findLibraryWithoutCWD(state, state.searchFile, state.suffixType);
if (library == null) {
library = findLibraryWithClassloaders(state, state.searchFile, state.suffixType);
if (library == null) {
throw runtime.newLoadError("no such file to load -- " + file);
}
}
try {
library.load(runtime, wrap);
} catch (IOException e) {
if (runtime.getDebug().isTrue()) e.printStackTrace(runtime.getErr());
throw newLoadErrorFromThrowable(runtime, file, e);
}
}
public void loadFromClassLoader(ClassLoader classLoader, String file, boolean wrap) {
SearchState state = new SearchState(file);
state.prepareLoadSearch(file);
Library library = null;
LoadServiceResource resource = getClassPathResource(classLoader, file);
if (resource != null) {
state.loadName = resolveLoadName(resource, file);
library = createLibrary(state, resource);
}
if (library == null) {
throw runtime.newLoadError("no such file to load -- " + file);
}
try {
library.load(runtime, wrap);
} catch (IOException e) {
if (runtime.getDebug().isTrue()) e.printStackTrace(runtime.getErr());
throw newLoadErrorFromThrowable(runtime, file, e);
}
}
public SearchState findFileForLoad(String file) {
if (Platform.IS_WINDOWS) {
file = file.replace('\\', '/');
}
// Even if we don't support .so, some stdlib require .so directly.
// Replace it with .jar to look for a java extension
// JRUBY-5033: The ExtensionSearcher will locate C exts, too, this way.
if (file.endsWith(".so")) {
file = file.replaceAll(".so$", ".jar");
}
SearchState state = new SearchState(file);
state.prepareRequireSearch(file);
for (LoadSearcher searcher : searchers) {
if (searcher.shouldTrySearch(state)) {
if (!searcher.trySearch(state)) {
return null;
}
}
}
return state;
}
public boolean require(String requireName) {
return requireCommon(requireName, true) == RequireState.LOADED;
}
public boolean autoloadRequire(String requireName) {
return requireCommon(requireName, false) != RequireState.CIRCULAR;
}
private enum RequireState {
LOADED, ALREADY_LOADED, CIRCULAR
};
private RequireState requireCommon(String requireName, boolean circularRequireWarning) {
// check for requiredName without extension.
if (featureAlreadyLoaded(requireName)) {
return RequireState.ALREADY_LOADED;
}
if (!requireLocks.lock(requireName)) {
if (circularRequireWarning && runtime.isVerbose() && runtime.is1_9()) {
warnCircularRequire(requireName);
}
return RequireState.CIRCULAR;
}
try {
if (!runtime.getProfile().allowRequire(requireName)) {
throw runtime.newLoadError("no such file to load -- " + requireName);
}
// check for requiredName again now that we're locked
if (featureAlreadyLoaded(requireName)) {
return RequireState.ALREADY_LOADED;
}
// numbers from loadTimer does not include lock waiting time.
long startTime = loadTimer.startLoad(requireName);
try {
boolean loaded = smartLoadInternal(requireName);
return loaded ? RequireState.LOADED : RequireState.ALREADY_LOADED;
} finally {
loadTimer.endLoad(requireName, startTime);
}
} finally {
requireLocks.unlock(requireName);
}
}
protected final RequireLocks requireLocks = new RequireLocks();
private class RequireLocks {
private final Map pool;
// global lock for require must be fair
private final ReentrantLock globalLock;
private RequireLocks() {
this.pool = new HashMap();
this.globalLock = new ReentrantLock(true);
}
/**
* Get exclusive lock for the specified requireName. Acquire sync object
* for the requireName from the pool, then try to lock it. NOTE: This
* lock is not fair for now.
*
* @param requireName
* just a name for the lock.
* @return If the sync object already locked by current thread, it just
* returns false without getting a lock. Otherwise true.
*/
private boolean lock(String requireName) {
ReentrantLock lock;
while (true) {
synchronized (pool) {
lock = pool.get(requireName);
if (lock == null) {
if (runtime.getInstanceConfig().isGlobalRequireLock()) {
lock = globalLock;
} else {
lock = new ReentrantLock();
}
pool.put(requireName, lock);
} else if (lock.isHeldByCurrentThread()) {
return false;
}
}
lock.lock();
// repeat until locked object still in requireLocks.
synchronized (pool) {
if (pool.get(requireName) == lock) {
// the object is locked && the lock is in the pool
return true;
}
// go next try
lock.unlock();
}
}
}
/**
* Unlock the lock for the specified requireName.
*
* @param requireName
* name of the lock to be unlocked.
*/
private void unlock(String requireName) {
synchronized (pool) {
ReentrantLock lock = pool.get(requireName);
if (lock != null) {
assert lock.isHeldByCurrentThread();
lock.unlock();
pool.remove(requireName);
}
}
}
}
protected void warnCircularRequire(String requireName) {
runtime.getWarnings().warn("loading in progress, circular require considered harmful - " + requireName);
// it's a hack for c:rb_backtrace impl.
// We should introduce new method to Ruby.TraceType when rb_backtrace is widely used not only for this purpose.
RaiseException ex = new RaiseException(runtime, runtime.getRuntimeError(), null, false);
String trace = runtime.getInstanceConfig().getTraceType().printBacktrace(ex.getException(), runtime.getPosix().isatty(FileDescriptor.err));
// rb_backtrace dumps to stderr directly.
System.err.print(trace.replaceFirst("[^\n]*\n", ""));
}
/**
* This method did require the specified file without getting a lock.
* Now we offer safe version only. Use {@link LoadService#require(String)} instead.
*/
@Deprecated
public boolean smartLoad(String file) {
return require(file);
}
private boolean smartLoadInternal(String file) {
checkEmptyLoad(file);
SearchState state = findFileForLoad(file);
if (state == null) {
return false;
}
if (state.library == null) {
throw runtime.newLoadError("no such file to load -- " + state.searchFile);
}
// check with long name
if (featureAlreadyLoaded(state.loadName)) {
return false;
}
boolean loaded = tryLoadingLibraryOrScript(runtime, state);
if (loaded) {
addLoadedFeature(state.loadName);
}
return loaded;
}
private static class LoadTimer {
public long startLoad(String file) { return 0L; }
public void endLoad(String file, long startTime) {}
}
private static class TracingLoadTimer extends LoadTimer {
private final AtomicInteger indent = new AtomicInteger(0);
private String getIndentString() {
StringBuilder buf = new StringBuilder();
int i = indent.get();
for (int j = 0; j < i; j++) {
buf.append(" ");
}
return buf.toString();
}
@Override
public long startLoad(String file) {
indent.incrementAndGet();
LOG.info(getIndentString() + "-> " + file);
return System.currentTimeMillis();
}
@Override
public void endLoad(String file, long startTime) {
LOG.info(getIndentString() + "<- " + file + " - "
+ (System.currentTimeMillis() - startTime) + "ms");
indent.decrementAndGet();
}
}
/**
* Load the org.jruby.runtime.load.Library implementation specified by
* className. The purpose of using this method is to avoid having static
* references to the given library class, thereby avoiding the additional
* classloading when the library is not in use.
*
* @param runtime The runtime in which to load
* @param libraryName The name of the library, to use for error messages
* @param className The class of the library
* @param classLoader The classloader to use to load it
* @param wrap Whether to wrap top-level in an anonymous module
*/
public static void reflectedLoad(Ruby runtime, String libraryName, String className, ClassLoader classLoader, boolean wrap) {
try {
if (classLoader == null && Ruby.isSecurityRestricted()) {
classLoader = runtime.getInstanceConfig().getLoader();
}
Object libObject = classLoader.loadClass(className).newInstance();
if (libObject instanceof Library) {
Library library = (Library)libObject;
library.load(runtime, false);
} else if (libObject instanceof BasicLibraryService) {
BasicLibraryService service = (BasicLibraryService)libObject;
service.basicLoad(runtime);
} else {
// invalid type of library, raise error
throw runtime.newLoadError("library `" + libraryName + "' is not of type Library or BasicLibraryService");
}
} catch (RaiseException re) {
throw re;
} catch (Throwable e) {
if (runtime.getDebug().isTrue()) e.printStackTrace();
throw runtime.newLoadError("library `" + libraryName + "' could not be loaded: " + e);
}
}
public IRubyObject getLoadPath() {
return loadPath;
}
public IRubyObject getLoadedFeatures() {
return loadedFeatures;
}
public void addBuiltinLibrary(String name, Library library) {
builtinLibraries.put(name, library);
}
public void removeBuiltinLibrary(String name) {
builtinLibraries.remove(name);
}
public void removeInternalLoadedFeature(String name) {
RubyString nameRubyString = runtime.newString(name);
loadedFeatures.delete(runtime.getCurrentContext(), nameRubyString, Block.NULL_BLOCK);
}
protected boolean featureAlreadyLoaded(String name) {
return loadedFeatures.containsString(name);
}
protected boolean isJarfileLibrary(SearchState state, final String file) {
return state.library instanceof JarredScript && file.endsWith(".jar");
}
protected void reraiseRaiseExceptions(Throwable e) throws RaiseException {
if (e instanceof RaiseException) {
throw (RaiseException) e;
}
}
public interface LoadSearcher {
/**
* @param state
* @return true if trySearch should be called.
*/
public boolean shouldTrySearch(SearchState state);
/**
* @param state
* @return false if loadSearch must be bail-out.
*/
public boolean trySearch(SearchState state);
}
public class BailoutSearcher implements LoadSearcher {
public boolean shouldTrySearch(SearchState state) {
return state.library == null;
}
protected boolean trySearch(String file, SuffixType suffixType) {
for (String suffix : suffixType.getSuffixes()) {
String searchName = file + suffix;
if (featureAlreadyLoaded(searchName)) {
return false;
}
}
return true;
}
public boolean trySearch(SearchState state) {
return trySearch(state.searchFile, state.suffixType);
}
}
public class SourceBailoutSearcher extends BailoutSearcher {
public boolean shouldTrySearch(SearchState state) {
// JRUBY-5032: Load extension files if they are required
// explicitly, and even if an rb file of the same name
// has already been loaded (effectively skipping the search for a source file).
return !extensionPattern.matcher(state.loadName).find();
}
// According to Rubyspec, source files should be loaded even if an equally named
// extension is loaded already. So we use the bailout search twice, once only
// for source files and once for whatever suffix type the state determines
public boolean trySearch(SearchState state) {
return super.trySearch(state.searchFile, SuffixType.Source);
}
}
public class NormalSearcher implements LoadSearcher {
public boolean shouldTrySearch(SearchState state) {
return state.library == null;
}
public boolean trySearch(SearchState state) {
state.library = findLibraryWithoutCWD(state, state.searchFile, state.suffixType);
return true;
}
}
public class ClassLoaderSearcher implements LoadSearcher {
public boolean shouldTrySearch(SearchState state) {
return state.library == null;
}
public boolean trySearch(SearchState state) {
state.library = findLibraryWithClassloaders(state, state.searchFile, state.suffixType);
return true;
}
}
public class ExtensionSearcher implements LoadSearcher {
public boolean shouldTrySearch(SearchState state) {
return (state.library == null || state.library instanceof JarredScript) && !state.searchFile.equalsIgnoreCase("");
}
public boolean trySearch(SearchState state) {
// This code exploits the fact that all .jar files will be found for the JarredScript feature.
// This is where the basic extension mechanism gets fixed
Library oldLibrary = state.library;
// Create package name, by splitting on / and joining all but the last elements with a ".", and downcasing them.
String[] all = state.searchFile.split("/");
StringBuilder finName = new StringBuilder();
for(int i=0, j=(all.length-1); i -1 && lastSlashIndex < className.length() - 1 && !Character.isJavaIdentifierStart(className.charAt(lastSlashIndex + 1))) {
if (lastSlashIndex == -1) {
className = "_" + className;
} else {
className = className.substring(0, lastSlashIndex + 1) + "_" + className.substring(lastSlashIndex + 1);
}
}
className = className.replace('/', '.');
try {
Class scriptClass = Class.forName(className);
script = (Script) scriptClass.newInstance();
} catch (Exception cnfe) {
return true;
}
state.library = new ScriptClassLibrary(script);
return true;
}
}
public static class SearchState {
public Library library;
public String loadName;
public SuffixType suffixType;
public String searchFile;
public SearchState(String file) {
loadName = file;
}
public void prepareRequireSearch(final String file) {
// if an extension is specified, try more targetted searches
if (file.lastIndexOf('.') > file.lastIndexOf('/')) {
Matcher matcher = null;
if ((matcher = sourcePattern.matcher(file)).find()) {
// source extensions
suffixType = SuffixType.Source;
// trim extension to try other options
searchFile = file.substring(0, matcher.start());
} else if ((matcher = extensionPattern.matcher(file)).find()) {
// extension extensions
suffixType = SuffixType.Extension;
// trim extension to try other options
searchFile = file.substring(0, matcher.start());
} else if (file.endsWith(".class")) {
// For JRUBY-6731, treat require 'foo.class' as no other filename than 'foo.class'.
suffixType = SuffixType.Neither;
searchFile = file;
} else {
// unknown extension, fall back to search with extensions
suffixType = SuffixType.Both;
searchFile = file;
}
} else {
// try all extensions
suffixType = SuffixType.Both;
searchFile = file;
}
}
public void prepareLoadSearch(final String file) {
// if a source extension is specified, try all source extensions
if (file.lastIndexOf('.') > file.lastIndexOf('/')) {
Matcher matcher = null;
if ((matcher = sourcePattern.matcher(file)).find()) {
// source extensions
suffixType = SuffixType.Source;
// trim extension to try other options
searchFile = file.substring(0, matcher.start());
} else {
// unknown extension, fall back to exact search
suffixType = SuffixType.Neither;
searchFile = file;
}
} else {
// try only literal search
suffixType = SuffixType.Neither;
searchFile = file;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getName()).append(": ");
sb.append("library=").append(library.toString());
sb.append(", loadName=").append(loadName);
sb.append(", suffixType=").append(suffixType.toString());
sb.append(", searchFile=").append(searchFile);
return sb.toString();
}
}
protected boolean tryLoadingLibraryOrScript(Ruby runtime, SearchState state) {
// attempt to load the found library
try {
state.library.load(runtime, false);
return true;
} catch (MainExitException mee) {
// allow MainExitException to propagate out for exec and friends
throw mee;
} catch (Throwable e) {
if(isJarfileLibrary(state, state.searchFile)) {
return true;
}
reraiseRaiseExceptions(e);
if(runtime.getDebug().isTrue()) e.printStackTrace(runtime.getErr());
RaiseException re = newLoadErrorFromThrowable(runtime, state.searchFile, e);
re.initCause(e);
throw re;
}
}
private static RaiseException newLoadErrorFromThrowable(Ruby runtime, String file, Throwable t) {
if (RubyInstanceConfig.DEBUG_PARSER) t.printStackTrace();
return runtime.newLoadError(String.format("load error: %s -- %s: %s", file, t.getClass().getName(), t.getMessage()));
}
// Using the BailoutSearch twice, once only for source files and once for state suffixes,
// in order to adhere to Rubyspec
protected final List searchers = new ArrayList();
{
searchers.add(new SourceBailoutSearcher());
searchers.add(new NormalSearcher());
searchers.add(new ClassLoaderSearcher());
searchers.add(new BailoutSearcher());
searchers.add(new ExtensionSearcher());
searchers.add(new ScriptClassSearcher());
}
protected String buildClassName(String className) {
// Remove any relative prefix, e.g. "./foo/bar" becomes "foo/bar".
className = className.replaceFirst("^\\.\\/", "");
if (className.lastIndexOf(".") != -1) {
className = className.substring(0, className.lastIndexOf("."));
}
className = className.replace("-", "_minus_").replace('.', '_');
return className;
}
protected void checkEmptyLoad(String file) throws RaiseException {
if (file.equals("")) {
throw runtime.newLoadError("no such file to load -- " + file);
}
}
protected void debugLogTry(String what, String msg) {
if (RubyInstanceConfig.DEBUG_LOAD_SERVICE) {
LOG.info( "LoadService: trying " + what + ": " + msg );
}
}
protected void debugLogFound(String what, String msg) {
if (RubyInstanceConfig.DEBUG_LOAD_SERVICE) {
LOG.info( "LoadService: found " + what + ": " + msg );
}
}
protected void debugLogFound( LoadServiceResource resource ) {
if (RubyInstanceConfig.DEBUG_LOAD_SERVICE) {
String resourceUrl;
try {
resourceUrl = resource.getURL().toString();
} catch (IOException e) {
resourceUrl = e.getMessage();
}
LOG.info( "LoadService: found: " + resourceUrl );
}
}
protected Library findBuiltinLibrary(SearchState state, String baseName, SuffixType suffixType) {
for (String suffix : suffixType.getSuffixes()) {
String namePlusSuffix = baseName + suffix;
debugLogTry( "builtinLib", namePlusSuffix );
if (builtinLibraries.containsKey(namePlusSuffix)) {
state.loadName = namePlusSuffix;
Library lib = builtinLibraries.get(namePlusSuffix);
debugLogFound( "builtinLib", namePlusSuffix );
return lib;
}
}
return null;
}
protected Library findLibraryWithoutCWD(SearchState state, String baseName, SuffixType suffixType) {
Library library = null;
switch (suffixType) {
case Both:
library = findBuiltinLibrary(state, baseName, SuffixType.Source);
if (library == null) library = createLibrary(state, tryResourceFromJarURL(state, baseName, SuffixType.Source));
if (library == null) library = createLibrary(state, tryResourceFromLoadPathOrURL(state, baseName, SuffixType.Source));
// If we fail to find as a normal Ruby script, we try to find as an extension,
// checking for a builtin first.
if (library == null) library = findBuiltinLibrary(state, baseName, SuffixType.Extension);
if (library == null) library = createLibrary(state, tryResourceFromJarURL(state, baseName, SuffixType.Extension));
if (library == null) library = createLibrary(state, tryResourceFromLoadPathOrURL(state, baseName, SuffixType.Extension));
break;
case Source:
case Extension:
// Check for a builtin first.
library = findBuiltinLibrary(state, baseName, suffixType);
if (library == null) library = createLibrary(state, tryResourceFromJarURL(state, baseName, suffixType));
if (library == null) library = createLibrary(state, tryResourceFromLoadPathOrURL(state, baseName, suffixType));
break;
case Neither:
library = createLibrary(state, tryResourceFromJarURL(state, baseName, SuffixType.Neither));
if (library == null) library = createLibrary(state, tryResourceFromLoadPathOrURL(state, baseName, SuffixType.Neither));
break;
}
return library;
}
protected Library findLibraryWithClassloaders(SearchState state, String baseName, SuffixType suffixType) {
for (String suffix : suffixType.getSuffixes()) {
String file = baseName + suffix;
LoadServiceResource resource = findFileInClasspath(file);
if (resource != null) {
state.loadName = resolveLoadName(resource, file);
return createLibrary(state, resource);
}
}
return null;
}
protected Library createLibrary(SearchState state, LoadServiceResource resource) {
if (resource == null) {
return null;
}
String file = state.loadName;
if (file.endsWith(".so") || file.endsWith(".dll") || file.endsWith(".bundle")) {
if (runtime.getInstanceConfig().isCextEnabled()) {
return new CExtension(resource);
} else {
throw runtime.newLoadError("C extensions are disabled, can't load `" + resource.getName() + "'");
}
} else if (file.endsWith(".jar")) {
return new JarredScript(resource);
} else if (file.endsWith(".class")) {
return new JavaCompiledScript(resource);
} else {
return new ExternalScript(resource, file);
}
}
protected LoadServiceResource tryResourceFromCWD(SearchState state, String baseName,SuffixType suffixType) throws RaiseException {
LoadServiceResource foundResource = null;
for (String suffix : suffixType.getSuffixes()) {
String namePlusSuffix = baseName + suffix;
// check current directory; if file exists, retrieve URL and return resource
try {
JRubyFile file = JRubyFile.create(runtime.getCurrentDirectory(), RubyFile.expandUserPath(runtime.getCurrentContext(), namePlusSuffix));
debugLogTry("resourceFromCWD", file.toString());
if (file.isFile() && file.isAbsolute() && file.canRead()) {
boolean absolute = true;
foundResource = new LoadServiceResource(file, getFileName(file, namePlusSuffix), absolute);
debugLogFound(foundResource);
state.loadName = resolveLoadName(foundResource, namePlusSuffix);
break;
}
} catch (IllegalArgumentException illArgEx) {
} catch (SecurityException secEx) {
}
}
return foundResource;
}
protected LoadServiceResource tryResourceFromHome(SearchState state, String baseName, SuffixType suffixType) throws RaiseException {
LoadServiceResource foundResource = null;
RubyHash env = (RubyHash) runtime.getObject().getConstant("ENV");
RubyString env_home = runtime.newString("HOME");
if (env.has_key_p(env_home).isFalse()) {
return null;
}
String home = env.op_aref(runtime.getCurrentContext(), env_home).toString();
String path = baseName.substring(2);
for (String suffix : suffixType.getSuffixes()) {
String namePlusSuffix = path + suffix;
// check home directory; if file exists, retrieve URL and return resource
try {
JRubyFile file = JRubyFile.create(home, RubyFile.expandUserPath(runtime.getCurrentContext(), namePlusSuffix));
debugLogTry("resourceFromHome", file.toString());
if (file.isFile() && file.isAbsolute() && file.canRead()) {
boolean absolute = true;
state.loadName = file.getPath();
foundResource = new LoadServiceResource(file, state.loadName, absolute);
debugLogFound(foundResource);
break;
}
} catch (IllegalArgumentException illArgEx) {
} catch (SecurityException secEx) {
}
}
return foundResource;
}
protected LoadServiceResource tryResourceFromJarURL(SearchState state, String baseName, SuffixType suffixType) {
// if a jar or file URL, return load service resource directly without further searching
LoadServiceResource foundResource = null;
if (baseName.startsWith("jar:")) {
for (String suffix : suffixType.getSuffixes()) {
String namePlusSuffix = baseName + suffix;
try {
URI resourceUri = new URI("jar", namePlusSuffix.substring(4), null);
URL url = resourceUri.toURL();
debugLogTry("resourceFromJarURL", url.toString());
if (url.openStream() != null) {
foundResource = new LoadServiceResource(url, namePlusSuffix);
debugLogFound(foundResource);
}
} catch (FileNotFoundException e) {
} catch (URISyntaxException e) {
throw runtime.newIOError(e.getMessage());
} catch (MalformedURLException e) {
throw runtime.newIOErrorFromException(e);
} catch (IOException e) {
throw runtime.newIOErrorFromException(e);
}
if (foundResource != null) {
state.loadName = resolveLoadName(foundResource, namePlusSuffix);
break; // end suffix iteration
}
}
} else if(baseName.startsWith("file:") && baseName.indexOf("!/") != -1) {
for (String suffix : suffixType.getSuffixes()) {
String namePlusSuffix = baseName + suffix;
try {
String jarFile = namePlusSuffix.substring(5, namePlusSuffix.indexOf("!/"));
JarFile file = new JarFile(jarFile);
String expandedFilename = expandRelativeJarPath(namePlusSuffix.substring(namePlusSuffix.indexOf("!/") + 2));
debugLogTry("resourceFromJarURL", expandedFilename.toString());
if(file.getJarEntry(expandedFilename) != null) {
URI resourceUri = new URI("jar", "file:" + jarFile + "!/" + expandedFilename, null);
foundResource = new LoadServiceResource(resourceUri.toURL(), namePlusSuffix);
debugLogFound(foundResource);
}
} catch (URISyntaxException e) {
throw runtime.newIOError(e.getMessage());
} catch (MalformedURLException e) {
throw runtime.newIOErrorFromException(e);
} catch(Exception e) {}
if (foundResource != null) {
state.loadName = resolveLoadName(foundResource, namePlusSuffix);
break; // end suffix iteration
}
}
}
return foundResource;
}
protected LoadServiceResource tryResourceFromLoadPathOrURL(SearchState state, String baseName, SuffixType suffixType) {
LoadServiceResource foundResource = null;
// if it's a ./ baseName, use CWD logic
if (baseName.startsWith("./")) {
foundResource = tryResourceFromCWD(state, baseName, suffixType);
if (foundResource != null) {
state.loadName = resolveLoadName(foundResource, foundResource.getName());
}
// not found, don't bother with load path
return foundResource;
}
// if it's a ~/ baseName use HOME logic
if (baseName.startsWith("~/")) {
foundResource = tryResourceFromHome(state, baseName, suffixType);
if (foundResource != null) {
state.loadName = resolveLoadName(foundResource, foundResource.getName());
}
// not found, don't bother with load path
return foundResource;
}
// if given path is absolute, just try it as-is (with extensions) and no load path
if (new File(baseName).isAbsolute() || baseName.startsWith("../")) {
for (String suffix : suffixType.getSuffixes()) {
String namePlusSuffix = baseName + suffix;
foundResource = tryResourceAsIs(namePlusSuffix);
if (foundResource != null) {
state.loadName = resolveLoadName(foundResource, namePlusSuffix);
return foundResource;
}
}
return null;
}
Outer: for (int i = 0; i < loadPath.size(); i++) {
// TODO this is really inefficient, and potentially a problem everytime anyone require's something.
// we should try to make LoadPath a special array object.
String loadPathEntry = getLoadPathEntry(loadPath.eltInternal(i));
if (loadPathEntry.equals(".") || loadPathEntry.equals("")) {
foundResource = tryResourceFromCWD(state, baseName, suffixType);
if (foundResource != null) {
String ss = foundResource.getName();
if(ss.startsWith("./")) {
ss = ss.substring(2);
}
state.loadName = resolveLoadName(foundResource, ss);
break Outer;
}
} else {
boolean looksLikeJarURL = loadPathLooksLikeJarURL(loadPathEntry);
boolean looksLikeClasspathURL = loadPathLooksLikeClasspathURL(loadPathEntry);
for (String suffix : suffixType.getSuffixes()) {
String namePlusSuffix = baseName + suffix;
if (looksLikeJarURL) {
foundResource = tryResourceFromJarURLWithLoadPath(namePlusSuffix, loadPathEntry);
} else if (looksLikeClasspathURL) {
foundResource = findFileInClasspath(loadPathEntry + "/" + namePlusSuffix);
} else {
foundResource = tryResourceFromLoadPath(namePlusSuffix, loadPathEntry);
}
if (foundResource != null) {
String ss = namePlusSuffix;
if(ss.startsWith("./")) {
ss = ss.substring(2);
}
state.loadName = resolveLoadName(foundResource, ss);
break Outer; // end suffix iteration
}
}
}
}
return foundResource;
}
protected String getLoadPathEntry(IRubyObject entry) {
RubyString entryString = entry.convertToString();
return entryString.asJavaString();
}
protected LoadServiceResource tryResourceFromJarURLWithLoadPath(String namePlusSuffix, String loadPathEntry) {
LoadServiceResource foundResource = null;
String[] urlParts = splitJarUrl(loadPathEntry);
String jarFileName = urlParts[0];
String entryPath = urlParts[1];
JarFile current = getJarFile(jarFileName);
if (current != null ) {
String canonicalEntry = (entryPath.length() > 0 ? entryPath + "/" : "") + namePlusSuffix;
debugLogTry("resourceFromJarURLWithLoadPath", current.getName() + "!/" + canonicalEntry);
if (current.getJarEntry(canonicalEntry) != null) {
try {
URI resourceUri = new URI("jar", "file:" + jarFileName + "!/" + canonicalEntry, null);
foundResource = new LoadServiceResource(resourceUri.toURL(), resourceUri.toString());
debugLogFound(foundResource);
} catch (URISyntaxException e) {
throw runtime.newIOError(e.getMessage());
} catch (MalformedURLException e) {
throw runtime.newIOErrorFromException(e);
}
}
}
return foundResource;
}
public JarFile getJarFile(String jarFileName) {
JarFile jarFile = jarFiles.get(jarFileName);
if(null == jarFile) {
try {
jarFile = new JarFile(jarFileName);
jarFiles.put(jarFileName, jarFile);
} catch (ZipException ignored) {
if (runtime.getInstanceConfig().isDebug()) {
LOG.info("ZipException trying to access " + jarFileName + ", stack trace follows:");
ignored.printStackTrace(runtime.getErr());
}
} catch (FileNotFoundException ignored) {
} catch (IOException e) {
throw runtime.newIOErrorFromException(e);
}
}
return jarFile;
}
protected boolean loadPathLooksLikeJarURL(String loadPathEntry) {
return loadPathEntry.startsWith("jar:") || loadPathEntry.endsWith(".jar") || (loadPathEntry.startsWith("file:") && loadPathEntry.indexOf("!") != -1);
}
protected boolean loadPathLooksLikeClasspathURL(String loadPathEntry) {
return loadPathEntry.startsWith("classpath:");
}
private String[] splitJarUrl(String loadPathEntry) {
int idx = loadPathEntry.indexOf("!");
if (idx == -1) {
return new String[]{loadPathEntry, ""};
}
String filename = loadPathEntry.substring(0, idx);
String entry = idx + 2 < loadPathEntry.length() ? loadPathEntry.substring(idx + 2) : "";
if(filename.startsWith("jar:")) {
filename = filename.substring(4);
}
if(filename.startsWith("file:")) {
filename = filename.substring(5);
}
return new String[]{filename, entry};
}
protected LoadServiceResource tryResourceFromLoadPath( String namePlusSuffix,String loadPathEntry) throws RaiseException {
LoadServiceResource foundResource = null;
try {
if (!Ruby.isSecurityRestricted()) {
String reportedPath = loadPathEntry + "/" + namePlusSuffix;
boolean absolute = true;
// we check length == 0 for 'load', which does not use load path
if (!new File(reportedPath).isAbsolute()) {
absolute = false;
// prepend ./ if . is not already there, since we're loading based on CWD
if (reportedPath.charAt(0) != '.') {
reportedPath = "./" + reportedPath;
}
loadPathEntry = JRubyFile.create(runtime.getCurrentDirectory(), loadPathEntry).getAbsolutePath();
}
JRubyFile actualPath = JRubyFile.create(loadPathEntry, RubyFile.expandUserPath(runtime.getCurrentContext(), namePlusSuffix));
if (RubyInstanceConfig.DEBUG_LOAD_SERVICE) {
debugLogTry("resourceFromLoadPath", "'" + actualPath.toString() + "' " + actualPath.isFile() + " " + actualPath.canRead());
}
if (actualPath.isFile() && actualPath.canRead()) {
foundResource = new LoadServiceResource(actualPath, reportedPath, absolute);
debugLogFound(foundResource);
}
}
} catch (SecurityException secEx) {
}
return foundResource;
}
protected LoadServiceResource tryResourceAsIs(String namePlusSuffix) throws RaiseException {
LoadServiceResource foundResource = null;
try {
if (!Ruby.isSecurityRestricted()) {
String reportedPath = namePlusSuffix;
File actualPath;
// we check length == 0 for 'load', which does not use load path
if (new File(reportedPath).isAbsolute()) {
// it's an absolute path, use it as-is
actualPath = new File(RubyFile.expandUserPath(runtime.getCurrentContext(), namePlusSuffix));
} else {
// prepend ./ if . is not already there, since we're loading based on CWD
if (reportedPath.charAt(0) == '.' && reportedPath.charAt(1) == '/') {
reportedPath = reportedPath.replaceFirst("\\./", runtime.getCurrentDirectory());
}
actualPath = JRubyFile.create(runtime.getCurrentDirectory(), RubyFile.expandUserPath(runtime.getCurrentContext(), namePlusSuffix));
}
debugLogTry("resourceAsIs", actualPath.toString());
if (actualPath.isFile() && actualPath.canRead()) {
foundResource = new LoadServiceResource(actualPath, reportedPath);
debugLogFound(foundResource);
}
}
} catch (SecurityException secEx) {
}
return foundResource;
}
/**
* this method uses the appropriate lookup strategy to find a file.
* It is used by Kernel#require.
*
* @mri rb_find_file
* @param name the file to find, this is a path name
* @return the correct file
*/
protected LoadServiceResource findFileInClasspath(String name) {
// Look in classpath next (we do not use File as a test since UNC names will match)
// Note: Jar resources must NEVER begin with an '/'. (previous code said "always begin with a /")
ClassLoader classLoader = runtime.getJRubyClassLoader();
// handle security-sensitive case
if (Ruby.isSecurityRestricted() && classLoader == null) {
classLoader = runtime.getInstanceConfig().getLoader();
}
// absolute classpath URI, no need to iterate over loadpaths
if (name.startsWith("classpath:/")) {
LoadServiceResource foundResource = getClassPathResource(classLoader, name);
if (foundResource != null) {
return foundResource;
}
} else if (name.startsWith("classpath:")) {
// "relative" classpath URI
name = name.substring("classpath:".length());
}
for (int i = 0; i < loadPath.size(); i++) {
// TODO this is really inefficient, and potentially a problem everytime anyone require's something.
// we should try to make LoadPath a special array object.
String entry = getLoadPathEntry(loadPath.eltInternal(i));
// if entry is an empty string, skip it
if (entry.length() == 0) continue;
// if entry starts with a slash, skip it since classloader resources never start with a /
if (entry.charAt(0) == '/' || (entry.length() > 1 && entry.charAt(1) == ':')) continue;
if (entry.startsWith("classpath:/")) {
entry = entry.substring("classpath:/".length());
} else if (entry.startsWith("classpath:")) {
entry = entry.substring("classpath:".length());
}
String entryName;
if (name.startsWith(entry)) {
entryName = name.substring(entry.length());
} else {
entryName = name;
}
// otherwise, try to load from classpath (Note: Jar resources always uses '/')
LoadServiceResource foundResource = getClassPathResource(classLoader, entry + "/" + entryName);
if (foundResource != null) {
return foundResource;
}
}
// if name starts with a / we're done (classloader resources won't load with an initial /)
if (name.charAt(0) == '/' || (name.length() > 1 && name.charAt(1) == ':')) return null;
// Try to load from classpath without prefix. "A/b.rb" will not load as
// "./A/b.rb" in a jar file.
LoadServiceResource foundResource = getClassPathResource(classLoader, name);
if (foundResource != null) {
return foundResource;
}
return null;
}
/* Directories and unavailable resources are not able to open a stream. */
protected boolean isRequireable(URL loc) {
if (loc != null) {
if (loc.getProtocol().equals("file") && new java.io.File(getPath(loc)).isDirectory()) {
return false;
}
try {
loc.openConnection();
return true;
} catch (Exception e) {}
}
return false;
}
protected LoadServiceResource getClassPathResource(ClassLoader classLoader, String name) {
boolean isClasspathScheme = false;
// strip the classpath scheme first
if (name.startsWith("classpath:/")) {
isClasspathScheme = true;
name = name.substring("classpath:/".length());
} else if (name.startsWith("classpath:")) {
isClasspathScheme = true;
name = name.substring("classpath:".length());
} else if(name.startsWith("file:") && name.indexOf("!/") != -1) {
name = name.substring(name.indexOf("!/") + 2);
}
debugLogTry("fileInClasspath", name);
URL loc = classLoader.getResource(name);
if (loc != null) { // got it
String path = "classpath:/" + name;
// special case for typical jar:file URLs, but only if the name didn't have
// the classpath scheme explicitly
if (!isClasspathScheme &&
(loc.getProtocol().equals("jar") || loc.getProtocol().equals("file"))
&& isRequireable(loc)) {
path = getPath(loc);
// On windows file: urls converted to names will return /C:/foo from
// getPath versus C:/foo. Since getPath is used in a million places
// putting the newFile.getPath broke some file with-in Jar loading.
// So I moved it to only this site.
if (Platform.IS_WINDOWS && loc.getProtocol().equals("file")) {
path = new File(path).getPath();
}
}
LoadServiceResource foundResource = new LoadServiceResource(loc, path);
debugLogFound(foundResource);
return foundResource;
}
return null;
}
private String expandRelativeJarPath(String path) {
return path.replaceAll("/[^/]+/\\.\\.|[^/]+/\\.\\./|\\./","").replace("^\\\\","/");
}
protected String resolveLoadName(LoadServiceResource foundResource, String previousPath) {
return previousPath;
}
protected String getFileName(JRubyFile file, String namePlusSuffix) {
String s = namePlusSuffix;
if(!namePlusSuffix.startsWith("./")) {
s = "./" + s;
}
return s;
}
}