001: /***** BEGIN LICENSE BLOCK *****
002: * Version: CPL 1.0/GPL 2.0/LGPL 2.1
003: *
004: * The contents of this file are subject to the Common Public
005: * License Version 1.0 (the "License"); you may not use this file
006: * except in compliance with the License. You may obtain a copy of
007: * the License at http://www.eclipse.org/legal/cpl-v10.html
008: *
009: * Software distributed under the License is distributed on an "AS
010: * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
011: * implied. See the License for the specific language governing
012: * rights and limitations under the License.
013: *
014: * Copyright (C) 2002-2004 Anders Bengtsson <ndrsbngtssn@yahoo.se>
015: * Copyright (C) 2002-2004 Jan Arne Petersen <jpetersen@uni-bonn.de>
016: * Copyright (C) 2004 Thomas E Enebo <enebo@acm.org>
017: * Copyright (C) 2004-2005 Charles O Nutter <headius@headius.com>
018: * Copyright (C) 2004 Stefan Matthias Aust <sma@3plus4.de>
019: * Copyright (C) 2006 Ola Bini <ola@ologix.com>
020: *
021: * Alternatively, the contents of this file may be used under the terms of
022: * either of the GNU General Public License Version 2 or later (the "GPL"),
023: * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
024: * in which case the provisions of the GPL or the LGPL are applicable instead
025: * of those above. If you wish to allow use of your version of this file only
026: * under the terms of either the GPL or the LGPL, and not to allow others to
027: * use your version of this file under the terms of the CPL, indicate your
028: * decision by deleting the provisions above and replace them with the notice
029: * and other provisions required by the GPL or the LGPL. If you do not delete
030: * the provisions above, a recipient may use your version of this file under
031: * the terms of any one of the CPL, the GPL or the LGPL.
032: ***** END LICENSE BLOCK *****/package org.jruby.runtime.load;
033:
034: import java.io.File;
035: import java.io.FileNotFoundException;
036: import java.io.IOException;
037: import java.net.MalformedURLException;
038: import java.net.URL;
039: import java.util.Collections;
040: import java.util.HashMap;
041: import java.util.HashSet;
042: import java.util.Iterator;
043: import java.util.List;
044: import java.util.Map;
045: import java.util.Set;
046: import java.util.jar.JarFile;
047: import java.util.regex.Matcher;
048: import java.util.regex.Pattern;
049:
050: import org.jruby.Ruby;
051: import org.jruby.RubyArray;
052: import org.jruby.RubyHash;
053: import org.jruby.RubyString;
054: import org.jruby.ast.executable.Script;
055: import org.jruby.exceptions.RaiseException;
056: import org.jruby.runtime.Constants;
057: import org.jruby.runtime.builtin.IRubyObject;
058: import org.jruby.util.BuiltinScript;
059: import org.jruby.util.JRubyFile;
060:
061: /**
062: * <b>How require works in JRuby</b>
063: * When requiring a name from Ruby, JRuby will first remove any file extension it knows about,
064: * thereby making it possible to use this string to see if JRuby has already loaded
065: * the name in question. If a .rb extension is specified, JRuby will only try
066: * those extensions when searching. If a .so, .o, .dll, or .jar extension is specified, JRuby
067: * will only try .so or .jar when searching. Otherwise, JRuby goes through the known suffixes
068: * (.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
069: * for all searchable extensions:
070: * <ol>
071: * <li>First, check if the name starts with 'jar:', then the path points to a jar-file resource which is returned.</li>
072: * <li>Second, try searching for the file in the current dir</li>
073: * <li>Then JRuby looks through the load path trying these variants:
074: * <ol>
075: * <li>See if the current load path entry starts with 'jar:', if so check if this jar-file contains the name</li>
076: * <li>Otherwise JRuby tries to construct a path by combining the entry and the current working directy, and then see if
077: * a file with the correct name can be reached from this point.</li>
078: * </ol>
079: * </li>
080: * <li>If all these fail, try to load the name as a resource from classloader resources, using the bare name as
081: * well as the load path entries</li>
082: * <li>When we get to this state, the normal JRuby loading has failed. At this stage JRuby tries to load
083: * Java native extensions, by following this process:
084: * <ol>
085: * <li>First it checks that we haven't already found a library. If we found a library of type JarredScript, the method continues.</li>
086: * <li>The first step is translating the name given into a valid Java Extension class name. First it splits the string into
087: * each path segment, and then makes all but the last downcased. After this it takes the last entry, removes all underscores
088: * and capitalizes each part separated by underscores. It then joins everything together and tacks on a 'Service' at the end.
089: * Lastly, it removes all leading dots, to make it a valid Java FWCN.</li>
090: * <li>If the previous library was of type JarredScript, we try to add the jar-file to the classpath</li>
091: * <li>Now JRuby tries to instantiate the class with the name constructed. If this works, we return a ClassExtensionLibrary. Otherwise,
092: * the old library is put back in place, if there was one.
093: * </ol>
094: * </li>
095: * <li>When all separate methods have been tried and there was no result, a LoadError will be raised.</li>
096: * <li>Otherwise, the name will be added to the loaded features, and the library loaded</li>
097: * </ol>
098: *
099: * <b>How to make a class that can get required by JRuby</b>
100: * <p>First, decide on what name should be used to require the extension.
101: * In this purely hypothetical example, this name will be 'active_record/connection_adapters/jdbc_adapter'.
102: * Then create the class name for this require-name, by looking at the guidelines above. Our class should
103: * be named active_record.connection_adapters.JdbcAdapterService, and implement one of the library-interfaces.
104: * The easiest one is BasicLibraryService, where you define the basicLoad-method, which will get called
105: * when your library should be loaded.</p>
106: * <p>The next step is to either put your compiled class on JRuby's classpath, or package the class/es inside a
107: * jar-file. To package into a jar-file, we first create the file, then rename it to jdbc_adapter.jar. Then
108: * we put this jar-file in the directory active_record/connection_adapters somewhere in JRuby's load path. For
109: * example, copying jdbc_adapter.jar into JRUBY_HOME/lib/ruby/site_ruby/1.8/active_record/connection_adapters
110: * will make everything work. If you've packaged your extension inside a RubyGem, write a setub.rb-script that
111: * copies the jar-file to this place.</p>
112: * <p>If you don't want to have the name of your extension-class to be prescribed, you can also put a file called
113: * jruby-ext.properties in your jar-files META-INF directory, where you can use the key <full-extension-name>.impl
114: * to make the extension library load the correct class. An example for the above would have a jruby-ext.properties
115: * that contained a ruby like: "active_record/connection_adapters/jdbc_adapter=org.jruby.ar.JdbcAdapter". (NOTE: THIS
116: * FEATURE IS NOT IMPLEMENTED YET.)</p>
117: *
118: * @author jpetersen
119: */
120: public class LoadService {
121: protected static final String JRUBY_BUILTIN_SUFFIX = ".rb";
122:
123: protected static final String[] sourceSuffixes = { ".rb" };
124: protected static final String[] extensionSuffixes = { ".so", ".jar" };
125: protected static final String[] allSuffixes = { ".rb", ".so",
126: ".jar" };
127: protected static final Pattern sourcePattern = Pattern
128: .compile("\\.(?:rb)$");
129: protected static final Pattern extensionPattern = Pattern
130: .compile("\\.(?:so|o|dll|jar)$");
131:
132: protected final RubyArray loadPath;
133: protected final RubyArray loadedFeatures;
134: protected final Set loadedFeaturesInternal = Collections
135: .synchronizedSet(new HashSet());
136: protected final Set firstLineLoadedFeatures = Collections
137: .synchronizedSet(new HashSet());
138: protected final Map builtinLibraries = new HashMap();
139:
140: protected final Map jarFiles = new HashMap();
141:
142: protected final Map autoloadMap = new HashMap();
143:
144: protected final Ruby runtime;
145:
146: public LoadService(Ruby runtime) {
147: this .runtime = runtime;
148: loadPath = RubyArray.newArray(runtime);
149: loadedFeatures = RubyArray.newArray(runtime);
150: }
151:
152: public void init(List additionalDirectories) {
153: // add all startup load paths to the list first
154: for (Iterator iter = additionalDirectories.iterator(); iter
155: .hasNext();) {
156: addPath((String) iter.next());
157: }
158:
159: // add $RUBYLIB paths
160: RubyHash env = (RubyHash) runtime.getObject()
161: .getConstant("ENV");
162: RubyString env_rubylib = runtime.newString("RUBYLIB");
163: if (env.has_key(env_rubylib).isTrue()) {
164: String rubylib = env.aref(env_rubylib).toString();
165: String[] paths = rubylib.split(File.pathSeparator);
166: for (int i = 0; i < paths.length; i++) {
167: addPath(paths[i]);
168: }
169: }
170:
171: // wrap in try/catch for security exceptions in an applet
172: if (!Ruby.isSecurityRestricted()) {
173: String jrubyHome = runtime.getJRubyHome();
174: if (jrubyHome != null) {
175: char sep = '/';
176: String rubyDir = jrubyHome + sep + "lib" + sep + "ruby"
177: + sep;
178:
179: addPath(rubyDir + "site_ruby" + sep
180: + Constants.RUBY_MAJOR_VERSION);
181: addPath(rubyDir + "site_ruby");
182: addPath(rubyDir + Constants.RUBY_MAJOR_VERSION);
183: addPath(rubyDir + Constants.RUBY_MAJOR_VERSION + sep
184: + "java");
185:
186: // Added to make sure we find default distribution files within jar file.
187: // TODO: Either make jrubyHome become the jar file or allow "classpath-only" paths
188: addPath("lib" + sep + "ruby" + sep
189: + Constants.RUBY_MAJOR_VERSION);
190: }
191: }
192:
193: // "." dir is used for relative path loads from a given file, as in require '../foo/bar'
194: if (runtime.getSafeLevel() == 0) {
195: addPath(".");
196: }
197: }
198:
199: private void addPath(String path) {
200: // Empty paths do not need to be added
201: if (path == null || path.length() == 0)
202: return;
203:
204: synchronized (loadPath) {
205: loadPath.add(runtime.newString(path.replace('\\', '/')));
206: }
207: }
208:
209: public void load(String file) {
210: if (!runtime.getProfile().allowLoad(file)) {
211: throw runtime.newLoadError("No such file to load -- "
212: + file);
213: }
214:
215: Library library = null;
216:
217: library = findLibrary(file);
218:
219: if (library == null) {
220: library = findLibraryWithClassloaders(file);
221: if (library == null) {
222: throw runtime.newLoadError("No such file to load -- "
223: + file);
224: }
225: }
226: try {
227: library.load(runtime);
228: } catch (IOException e) {
229: throw runtime.newLoadError("IO error -- " + file);
230: }
231: }
232:
233: public boolean smartLoad(String file) {
234: if (firstLineLoadedFeatures.contains(file)) {
235: return false;
236: }
237: Library library = null;
238: String loadName = file;
239: String[] extensionsToSearch = null;
240:
241: // if an extension is specified, try more targetted searches
242: if (file.lastIndexOf('.') > file.lastIndexOf('/')) {
243: Matcher matcher = null;
244: if ((matcher = sourcePattern.matcher(file)).find()) {
245: // source extensions
246: extensionsToSearch = sourceSuffixes;
247:
248: // trim extension to try other options
249: file = file.substring(0, matcher.start());
250: } else if ((matcher = extensionPattern.matcher(file))
251: .find()) {
252: // extension extensions
253: extensionsToSearch = extensionSuffixes;
254:
255: // trim extension to try other options
256: file = file.substring(0, matcher.start());
257: } else {
258: // unknown extension, fall back to search with extensions
259: extensionsToSearch = allSuffixes;
260: }
261: } else {
262: // try all extensions
263: extensionsToSearch = allSuffixes;
264: }
265:
266: // First try suffixes with normal loading
267: for (int i = 0; i < extensionsToSearch.length; i++) {
268: library = findLibrary(file + extensionsToSearch[i]);
269: if (library != null) {
270: loadName = file + extensionsToSearch[i];
271: break;
272: }
273: }
274:
275: // Then try suffixes with classloader loading
276: if (library == null) {
277: for (int i = 0; i < extensionsToSearch.length; i++) {
278: library = findLibraryWithClassloaders(file
279: + extensionsToSearch[i]);
280: if (library != null) {
281: loadName = file + extensionsToSearch[i];
282: break;
283: }
284: }
285: }
286:
287: library = tryLoadExtension(library, file);
288:
289: // no library or extension found, try to load directly as a class
290: Script script = null;
291: if (library == null) {
292: String className = file;
293: if (file.lastIndexOf(".") > file.lastIndexOf("/")) {
294: className = file.substring(file.lastIndexOf(".") + 1);
295: }
296: className = className.replace('/', '.');
297: try {
298: Class scriptClass = Class.forName(className);
299: script = (Script) scriptClass.newInstance();
300: } catch (Exception cnfe) {
301: throw runtime.newLoadError("no such file to load -- "
302: + file);
303: }
304: }
305:
306: if (loadedFeaturesInternal.contains(loadName)
307: || loadedFeatures
308: .include_p(runtime.newString(loadName))
309: .isTrue()) {
310: return false;
311: }
312:
313: // attempt to load the found library
314: try {
315: loadedFeaturesInternal.add(loadName);
316: firstLineLoadedFeatures.add(file);
317: synchronized (loadedFeatures) {
318: loadedFeatures.append(runtime.newString(loadName));
319: }
320:
321: if (script != null) {
322: runtime.loadScript(script);
323: return true;
324: }
325:
326: library.load(runtime);
327: return true;
328: } catch (Exception e) {
329: if (library instanceof JarredScript
330: && file.endsWith(".jar")) {
331: return true;
332: }
333:
334: loadedFeaturesInternal.remove(loadName);
335: firstLineLoadedFeatures.remove(file);
336: synchronized (loadedFeatures) {
337: loadedFeatures.remove(runtime.newString(loadName));
338: }
339: if (e instanceof RaiseException)
340: throw (RaiseException) e;
341:
342: if (runtime.getDebug().isTrue())
343: e.printStackTrace();
344:
345: RaiseException re = runtime.newLoadError("IO error -- "
346: + file);
347: re.initCause(e);
348: throw re;
349: }
350: }
351:
352: public boolean require(String file) {
353: if (!runtime.getProfile().allowRequire(file)) {
354: throw runtime.newLoadError("No such file to load -- "
355: + file);
356: }
357: return smartLoad(file);
358: }
359:
360: public IRubyObject getLoadPath() {
361: return loadPath;
362: }
363:
364: public IRubyObject getLoadedFeatures() {
365: return loadedFeatures;
366: }
367:
368: public IAutoloadMethod autoloadFor(String name) {
369: return (IAutoloadMethod) autoloadMap.get(name);
370: }
371:
372: public void removeAutoLoadFor(String name) {
373: autoloadMap.remove(name);
374: }
375:
376: public IRubyObject autoload(String name) {
377: IAutoloadMethod loadMethod = (IAutoloadMethod) autoloadMap
378: .remove(name);
379: if (loadMethod != null) {
380: return loadMethod.load(runtime, name);
381: }
382: return null;
383: }
384:
385: public void addAutoload(String name, IAutoloadMethod loadMethod) {
386: autoloadMap.put(name, loadMethod);
387: }
388:
389: public void registerBuiltin(String name, Library library) {
390: builtinLibraries.put(name, library);
391: }
392:
393: public void registerRubyBuiltin(String libraryName) {
394: registerBuiltin(libraryName + JRUBY_BUILTIN_SUFFIX,
395: new BuiltinScript(libraryName));
396: }
397:
398: private Library findLibrary(String file) {
399: if (builtinLibraries.containsKey(file)) {
400: return (Library) builtinLibraries.get(file);
401: }
402:
403: LoadServiceResource resource = findFile(file);
404: if (resource == null) {
405: return null;
406: }
407:
408: if (file.endsWith(".jar")) {
409: return new JarredScript(resource);
410: } else {
411: return new ExternalScript(resource, file);
412: }
413: }
414:
415: private Library findLibraryWithClassloaders(String file) {
416: LoadServiceResource resource = findFileInClasspath(file);
417: if (resource == null) {
418: return null;
419: }
420:
421: if (file.endsWith(".jar")) {
422: return new JarredScript(resource);
423: } else {
424: return new ExternalScript(resource, file);
425: }
426: }
427:
428: /**
429: * this method uses the appropriate lookup strategy to find a file.
430: * It is used by Kernel#require.
431: *
432: * @mri rb_find_file
433: * @param name the file to find, this is a path name
434: * @return the correct file
435: */
436: private LoadServiceResource findFile(String name) {
437: // if a jar URL, return load service resource directly without further searching
438: if (name.startsWith("jar:")) {
439: try {
440: return new LoadServiceResource(new URL(name), name);
441: } catch (MalformedURLException e) {
442: throw runtime.newIOErrorFromException(e);
443: }
444: }
445:
446: // check current directory; if file exists, retrieve URL and return resource
447: try {
448: JRubyFile file = JRubyFile.create(runtime
449: .getCurrentDirectory(), name);
450: if (file.isFile() && file.isAbsolute()) {
451: try {
452: return new LoadServiceResource(
453: file.toURI().toURL(), name);
454: } catch (MalformedURLException e) {
455: throw runtime.newIOErrorFromException(e);
456: }
457: }
458: } catch (IllegalArgumentException illArgEx) {
459: } catch (SecurityException secEx) {
460: }
461:
462: for (Iterator pathIter = loadPath.getList().iterator(); pathIter
463: .hasNext();) {
464: String entry = pathIter.next().toString();
465: if (entry.startsWith("jar:")) {
466: JarFile current = (JarFile) jarFiles.get(entry);
467: if (null == current) {
468: try {
469: current = new JarFile(entry.substring(4));
470: jarFiles.put(entry, current);
471: } catch (FileNotFoundException ignored) {
472: } catch (IOException e) {
473: throw runtime.newIOErrorFromException(e);
474: }
475: }
476:
477: if (current.getJarEntry(name) != null) {
478: try {
479: return new LoadServiceResource(new URL(
480: "jar:file:" + entry.substring(4) + "!/"
481: + name), entry + name);
482: } catch (MalformedURLException e) {
483: throw runtime.newIOErrorFromException(e);
484: }
485: }
486: }
487:
488: try {
489: JRubyFile current = JRubyFile.create(JRubyFile.create(
490: runtime.getCurrentDirectory(), entry)
491: .getAbsolutePath(), name);
492: if (current.isFile()) {
493: try {
494: return new LoadServiceResource(current.toURI()
495: .toURL(), current.getPath());
496: } catch (MalformedURLException e) {
497: throw runtime.newIOErrorFromException(e);
498: }
499: }
500: } catch (SecurityException secEx) {
501: }
502: }
503:
504: return null;
505: }
506:
507: /**
508: * this method uses the appropriate lookup strategy to find a file.
509: * It is used by Kernel#require.
510: *
511: * @mri rb_find_file
512: * @param name the file to find, this is a path name
513: * @return the correct file
514: */
515: private LoadServiceResource findFileInClasspath(String name) {
516: // Look in classpath next (we do not use File as a test since UNC names will match)
517: // Note: Jar resources must NEVER begin with an '/'. (previous code said "always begin with a /")
518: ClassLoader classLoader = Thread.currentThread()
519: .getContextClassLoader();
520:
521: for (Iterator pathIter = loadPath.getList().iterator(); pathIter
522: .hasNext();) {
523: String entry = pathIter.next().toString();
524:
525: // if entry starts with a slash, skip it since classloader resources never start with a /
526: if (entry.charAt(0) == '/'
527: || (entry.length() > 1 && entry.charAt(1) == ':'))
528: continue;
529:
530: // otherwise, try to load from classpath (Note: Jar resources always uses '/')
531: URL loc = classLoader.getResource(entry + "/" + name);
532:
533: // Make sure this is not a directory or unavailable in some way
534: if (isRequireable(loc)) {
535: return new LoadServiceResource(loc, loc.getPath());
536: }
537: }
538:
539: // if name starts with a / we're done (classloader resources won't load with an initial /)
540: if (name.charAt(0) == '/'
541: || (name.length() > 1 && name.charAt(1) == ':'))
542: return null;
543:
544: // Try to load from classpath without prefix. "A/b.rb" will not load as
545: // "./A/b.rb" in a jar file.
546: URL loc = classLoader.getResource(name);
547:
548: return isRequireable(loc) ? new LoadServiceResource(loc, loc
549: .getPath()) : null;
550: }
551:
552: private Library tryLoadExtension(Library library, String file) {
553: // This code exploits the fact that all .jar files will be found for the JarredScript feature.
554: // This is where the basic extension mechanism gets fixed
555: Library oldLibrary = library;
556:
557: if ((library == null || library instanceof JarredScript)
558: && !file.equalsIgnoreCase("")) {
559: // Create package name, by splitting on / and joining all but the last elements with a ".", and downcasing them.
560: String[] all = file.split("/");
561: StringBuffer finName = new StringBuffer();
562: for (int i = 0, j = (all.length - 1); i < j; i++) {
563: finName.append(all[i].toLowerCase()).append(".");
564:
565: }
566:
567: // Make the class name look nice, by splitting on _ and capitalize each segment, then joining
568: // the, together without anything separating them, and last put on "Service" at the end.
569: String[] last = all[all.length - 1].split("_");
570: for (int i = 0, j = last.length; i < j; i++) {
571: finName
572: .append(
573: Character
574: .toUpperCase(last[i].charAt(0)))
575: .append(last[i].substring(1));
576: }
577: finName.append("Service");
578:
579: // We don't want a package name beginning with dots, so we remove them
580: String className = finName.toString().replaceAll("^\\.*",
581: "");
582:
583: // If there is a jar-file with the required name, we add this to the class path.
584: if (library instanceof JarredScript) {
585: // It's _really_ expensive to check that the class actually exists in the Jar, so
586: // we don't do that now.
587: runtime.getJavaSupport()
588: .addToClasspath(
589: ((JarredScript) library).getResource()
590: .getURL());
591: }
592:
593: try {
594: Class theClass = runtime.getJavaSupport()
595: .loadJavaClass(className);
596: library = new ClassExtensionLibrary(theClass);
597: } catch (Exception ee) {
598: library = null;
599: runtime.getGlobalVariables()
600: .set("$!", runtime.getNil());
601: }
602: }
603:
604: // If there was a good library before, we go back to that
605: if (library == null && oldLibrary != null) {
606: library = oldLibrary;
607: }
608: return library;
609: }
610:
611: /* Directories and unavailable resources are not able to open a stream. */
612: private boolean isRequireable(URL loc) {
613: if (loc != null) {
614: if (loc.getProtocol().equals("file")
615: && new java.io.File(loc.getFile()).isDirectory()) {
616: return false;
617: }
618:
619: try {
620: loc.openConnection();
621: return true;
622: } catch (Exception e) {
623: }
624: }
625: return false;
626: }
627: }
|