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

org.jruby.runtime.load.LoadService Maven / Gradle / Ivy

There is a newer version: 9.4.7.0
Show newest version
/***** 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-2010 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 java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
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.platform.Platform;
import org.jruby.runtime.Constants;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.JRubyFile;

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:
 * 
    *
  1. First, check if the name starts with 'jar:', then the path points to a jar-file resource which is returned.
  2. *
  3. Second, try searching for the file in the current dir
  4. *
  5. Then JRuby looks through the load path trying these variants: *
      *
    1. See if the current load path entry starts with 'jar:', if so check if this jar-file contains the name
    2. *
    3. 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.
    4. *
    *
  6. *
  7. 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
  8. *
  9. 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: *
      *
    1. First it checks that we haven't already found a library. If we found a library of type JarredScript, the method continues.
    2. *
    3. 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.
    4. *
    5. If the previous library was of type JarredScript, we try to add the jar-file to the classpath
    6. *
    7. 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. *
    *
  10. *
  11. When all separate methods have been tried and there was no result, a LoadError will be raised.
  12. *
  13. Otherwise, the name will be added to the loaded features, and the library loaded
  14. *
* * 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.

*

If you don't want to have the name of your extension-class to be prescribed, you can also put a file called * jruby-ext.properties in your jar-files META-INF directory, where you can use the key .impl * to make the extension library load the correct class. An example for the above would have a jruby-ext.properties * that contained a ruby like: "active_record/connection_adapters/jdbc_adapter=org.jruby.ar.JdbcAdapter". (NOTE: THIS * FEATURE IS NOT IMPLEMENTED YET.)

* * @author jpetersen */ public class LoadService { private final LoadTimer loadTimer; public enum SuffixType { Source, Extension, Both, Neither; public static final String[] sourceSuffixes = { ".class", ".rb" }; public static final String[] extensionSuffixes = { ".jar", ".so", ".bundle", ".dll" }; private static final String[] allSuffixes = { ".class", ".rb", ".jar", ".so", ".bundle", ".dll" }; private static final String[] emptySuffixes = { "" }; 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 RubyArray loadedFeatures; protected List loadedFeaturesInternal; protected final Map builtinLibraries = new HashMap(); protected final Map jarFiles = new HashMap(); protected final Ruby runtime; protected boolean caseInsensitiveFS = false; public LoadService(Ruby runtime) { this.runtime = runtime; if (RubyInstanceConfig.DEBUG_LOAD_TIMINGS) { loadTimer = new TracingLoadTimer(); } else { loadTimer = new LoadTimer(); } } public void init(List additionalDirectories) { loadPath = RubyArray.newArray(runtime); loadedFeatures = RubyArray.newArray(runtime); loadedFeaturesInternal = Collections.synchronizedList(loadedFeatures); // add all startup load paths to the list first for (Iterator iter = additionalDirectories.iterator(); iter.hasNext();) { addPath((String) iter.next()); } // add $RUBYLIB paths RubyHash env = (RubyHash) runtime.getObject().fastGetConstant("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); for (int i = 0; i < paths.length; i++) { addPath(paths[i]); } } // wrap in try/catch for security exceptions in an applet try { String jrubyHome = runtime.getJRubyHome(); if (jrubyHome != null) { char sep = '/'; String rubyDir = jrubyHome + sep + "lib" + sep + "ruby" + sep; // If we're running in 1.9 compat mode, add Ruby 1.9 libs to path before 1.8 libs if (runtime.is1_9()) { addPath(rubyDir + "site_ruby" + sep + Constants.RUBY1_9_MAJOR_VERSION); addPath(rubyDir + "site_ruby" + sep + "shared"); addPath(rubyDir + "site_ruby" + sep + Constants.RUBY_MAJOR_VERSION); addPath(rubyDir + Constants.RUBY1_9_MAJOR_VERSION); } else { // Add 1.8 libs addPath(rubyDir + "site_ruby" + sep + Constants.RUBY_MAJOR_VERSION); addPath(rubyDir + "site_ruby" + sep + "shared"); addPath(rubyDir + Constants.RUBY_MAJOR_VERSION); } String lowerCaseJRubyHome = jrubyHome.toLowerCase(); String upperCaseJRubyHome = lowerCaseJRubyHome.toUpperCase(); try { String canonNormal = new File(jrubyHome).getCanonicalPath(); String canonLower = new File(lowerCaseJRubyHome).getCanonicalPath(); String canonUpper = new File(upperCaseJRubyHome).getCanonicalPath(); if (canonNormal.equals(canonLower) && canonLower.equals(canonUpper)) { caseInsensitiveFS = true; } } catch (Exception e) {} } } catch(SecurityException ignore) {} // "." dir is used for relative path loads from a given file, as in require '../foo/bar' if (!runtime.is1_9() && runtime.getSafeLevel() == 0) { addPath("."); } } protected void addLoadedFeature(RubyString loadNameRubyString) { loadedFeaturesInternal.add(loadNameRubyString); } 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 SearchState findFileForLoad(String file) throws AlreadyLoaded { SearchState state = new SearchState(file); state.prepareRequireSearch(file); for (LoadSearcher searcher : searchers) { if (searcher.shouldTrySearch(state)) { searcher.trySearch(state); } else { continue; } } return state; } public boolean lockAndRequire(String requireName) { Object requireLock; try { synchronized (requireLocks) { requireLock = requireLocks.get(requireName); if (requireLock == null) { requireLock = new Object(); requireLocks.put(requireName, requireLock); } } synchronized (requireLock) { return require(requireName); } } finally { synchronized (requireLocks) { requireLocks.remove(requireName); } } } protected Map requireLocks = new Hashtable(); public boolean smartLoad(String file) { checkEmptyLoad(file); if (Platform.IS_WINDOWS) { file = file.replace('\\', '/'); } SearchState state; try { // 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"); } state = findFileForLoad(file); return tryLoadingLibraryOrScript(runtime, state); } catch (AlreadyLoaded al) { // Library has already been loaded in some form, bail out return false; } } 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(); System.err.println(getIndentString() + "-> " + file); return System.currentTimeMillis(); } @Override public void endLoad(String file, long startTime) { System.err.println(getIndentString() + "<- " + file + " - " + (System.currentTimeMillis() - startTime) + "ms"); indent.decrementAndGet(); } } public boolean require(String file) { if(!runtime.getProfile().allowRequire(file)) { throw runtime.newLoadError("No such file to load -- " + file); } if (featureAlreadyLoaded(RubyString.newString(runtime, file))) { return false; } long startTime = loadTimer.startLoad(file); try { return smartLoad(file); } finally { loadTimer.endLoad(file, startTime); } } /** * 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) { if (caseInsensitiveFS) { // on a case-insensitive filesystem, we need to search case-insensitively // to remove the loaded feature for (Iterator iter = loadedFeaturesInternal.iterator(); iter.hasNext();) { Object feature = iter.next(); if (feature.toString().equalsIgnoreCase(name)) { iter.remove(); } } } else { loadedFeaturesInternal.remove(name); } } protected boolean featureAlreadyLoaded(RubyString loadNameRubyString) { if (caseInsensitiveFS) { String name = loadNameRubyString.toString(); // on a case-insensitive filesystem, we need to search case-insensitively // to find the loaded feature for (Iterator iter = loadedFeaturesInternal.iterator(); iter.hasNext();) { Object feature = iter.next(); if (feature.toString().equalsIgnoreCase(name)) { return true; } } return false; } else { return loadedFeaturesInternal.contains(loadNameRubyString); } } protected boolean isJarfileLibrary(SearchState state, final String file) { return state.library instanceof JarredScript && file.endsWith(".jar"); } protected void removeLoadedFeature(RubyString loadNameRubyString) { loadedFeaturesInternal.remove(loadNameRubyString); } protected void reraiseRaiseExceptions(Throwable e) throws RaiseException { if (e instanceof RaiseException) { throw (RaiseException) e; } } public interface LoadSearcher { public boolean shouldTrySearch(SearchState state); public void trySearch(SearchState state) throws AlreadyLoaded; } public static class AlreadyLoaded extends Exception { private RubyString searchNameString; public AlreadyLoaded(RubyString searchNameString) { this.searchNameString = searchNameString; } public RubyString getSearchNameString() { return searchNameString; } } public class BailoutSearcher implements LoadSearcher { public boolean shouldTrySearch(SearchState state) { return state.library == null; } protected void trySearch(String file, SuffixType suffixType) throws AlreadyLoaded { for (String suffix : suffixType.getSuffixes()) { String searchName = file + suffix; RubyString searchNameString = RubyString.newString(runtime, searchName); if (featureAlreadyLoaded(searchNameString)) { throw new AlreadyLoaded(searchNameString); } } } public void trySearch(SearchState state) throws AlreadyLoaded { trySearch(state.searchFile, state.suffixType); } } public class SourceBailoutSearcher extends BailoutSearcher { public boolean shouldTrySearch(SearchState state) { // JRUBY-5032: Load extension files if they are required // explicitely, 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 void trySearch(SearchState state) throws AlreadyLoaded { super.trySearch(state.searchFile, SuffixType.Source); } } public class NormalSearcher implements LoadSearcher { public boolean shouldTrySearch(SearchState state) { return state.library == null; } public void trySearch(SearchState state) { state.library = findLibraryWithoutCWD(state, state.searchFile, state.suffixType); } } public class ClassLoaderSearcher implements LoadSearcher { public boolean shouldTrySearch(SearchState state) { return state.library == null; } public void trySearch(SearchState state) { state.library = findLibraryWithClassloaders(state, state.searchFile, state.suffixType); } } public class ExtensionSearcher implements LoadSearcher { public boolean shouldTrySearch(SearchState state) { return (state.library == null || state.library instanceof JarredScript) && !state.searchFile.equalsIgnoreCase(""); } public void 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) { throw runtime.newLoadError("no such file to load -- " + state.searchFile); } state.library = new ScriptClassLibrary(script); } } 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 { // 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 RubyString loadNameRubyString = RubyString.newString(runtime, state.loadName); try { synchronized (loadedFeaturesInternal) { if (featureAlreadyLoaded(loadNameRubyString)) { return false; } else { addLoadedFeature(loadNameRubyString); } } // otherwise load the library we've found 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; } removeLoadedFeature(loadNameRubyString); 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) { 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) { runtime.getErr().println( "LoadService: trying " + what + ": " + msg ); } } protected void debugLogFound(String what, String msg) { if (RubyInstanceConfig.DEBUG_LOAD_SERVICE) { runtime.getErr().println( "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(); } runtime.getErr().println( "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().fastGetConstant("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 { URL url = new URL(namePlusSuffix); debugLogTry("resourceFromJarURL", url.toString()); if (url.openStream() != null) { foundResource = new LoadServiceResource(url, namePlusSuffix); debugLogFound(foundResource); } } catch (FileNotFoundException e) { } 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) { foundResource = new LoadServiceResource(new URL("jar:file:" + jarFile + "!/" + expandedFilename), namePlusSuffix); debugLogFound(foundResource); } } 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. RubyString entryString = loadPath.eltInternal(i).convertToString(); String loadPathEntry = entryString.asJavaString(); 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); for (String suffix : suffixType.getSuffixes()) { String namePlusSuffix = baseName + suffix; if (looksLikeJarURL) { foundResource = tryResourceFromJarURLWithLoadPath(namePlusSuffix, loadPathEntry); } 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 LoadServiceResource tryResourceFromJarURLWithLoadPath(String namePlusSuffix, String loadPathEntry) { LoadServiceResource foundResource = null; JarFile current = jarFiles.get(loadPathEntry); boolean isFileJarUrl = loadPathEntry.startsWith("file:") && loadPathEntry.indexOf("!/") != -1; String after = isFileJarUrl ? loadPathEntry.substring(loadPathEntry.indexOf("!/") + 2) + "/" : ""; String before = isFileJarUrl ? loadPathEntry.substring(0, loadPathEntry.indexOf("!/")) : loadPathEntry; if(null == current) { try { if(loadPathEntry.startsWith("jar:")) { current = new JarFile(loadPathEntry.substring(4)); } else if (loadPathEntry.endsWith(".jar")) { current = new JarFile(loadPathEntry); } else { current = new JarFile(loadPathEntry.substring(5,loadPathEntry.indexOf("!/"))); } jarFiles.put(loadPathEntry,current); } catch (ZipException ignored) { if (runtime.getInstanceConfig().isDebug()) { runtime.getErr().println("ZipException trying to access " + loadPathEntry + ", stack trace follows:"); ignored.printStackTrace(runtime.getErr()); } } catch (FileNotFoundException ignored) { } catch (IOException e) { throw runtime.newIOErrorFromException(e); } } String canonicalEntry = after+namePlusSuffix; if (current != null ) { debugLogTry("resourceFromJarURLWithLoadPath", current.getName() + "!/" + canonicalEntry); if (current.getJarEntry(canonicalEntry) != null) { try { if (loadPathEntry.endsWith(".jar")) { foundResource = new LoadServiceResource(new URL("jar:file:" + loadPathEntry + "!/" + canonicalEntry), "/" + namePlusSuffix); } else if (loadPathEntry.startsWith("file:")) { foundResource = new LoadServiceResource(new URL("jar:" + before + "!/" + canonicalEntry), loadPathEntry + "/" + namePlusSuffix); } else { foundResource = new LoadServiceResource(new URL("jar:file:" + loadPathEntry.substring(4) + "!/" + namePlusSuffix), loadPathEntry + namePlusSuffix); } debugLogFound(foundResource); } catch (MalformedURLException e) { throw runtime.newIOErrorFromException(e); } } } return foundResource; } protected boolean loadPathLooksLikeJarURL(String loadPathEntry) { return loadPathEntry.startsWith("jar:") || loadPathEntry.endsWith(".jar") || (loadPathEntry.startsWith("file:") && loadPathEntry.indexOf("!/") != -1); } protected LoadServiceResource tryResourceFromLoadPath( String namePlusSuffix,String loadPathEntry) throws RaiseException { LoadServiceResource foundResource = null; try { if (!Ruby.isSecurityRestricted()) { String reportedPath = loadPathEntry + "/" + namePlusSuffix; JRubyFile actualPath; boolean absolute = false; // we check length == 0 for 'load', which does not use load path if (new File(reportedPath).isAbsolute()) { absolute = true; // it's an absolute path, use it as-is actualPath = JRubyFile.create(loadPathEntry, RubyFile.expandUserPath(runtime.getCurrentContext(), namePlusSuffix)); } else { absolute = false; // prepend ./ if . is not already there, since we're loading based on CWD if (reportedPath.charAt(0) != '.') { reportedPath = "./" + reportedPath; } actualPath = JRubyFile.create(JRubyFile.create(runtime.getCurrentDirectory(), loadPathEntry).getAbsolutePath(), 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. RubyString entryString = loadPath.eltInternal(i).convertToString(); String entry = entryString.asJavaString(); // 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()); } if (name.startsWith(entry)) { name = name.substring(entry.length()); } // otherwise, try to load from classpath (Note: Jar resources always uses '/') LoadServiceResource foundResource = getClassPathResource(classLoader, entry + "/" + name); 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); } 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; } /** * Is the jruby home dir on a case-insensitive fs. Determined by comparing * a canonicalized jruby home with canonicalized lower and upper-case versions * of the same path. * * @return true if jruby home is on a case-insensitive FS; false otherwise */ public boolean isCaseInsensitiveFS() { return caseInsensitiveFS; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy