Skip to content

Commit

Permalink
Merge pull request #2278 from jruby/redefine-uri-classloader-meaning
Browse files Browse the repository at this point in the history
Redefine uri classloader meaning
  • Loading branch information
mkristian committed Dec 6, 2014
2 parents 8c41ab6 + b04987a commit 1eedf72
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 85 deletions.
60 changes: 20 additions & 40 deletions core/src/main/java/org/jruby/embed/IsolatedScriptingContainer.java
Expand Up @@ -18,23 +18,16 @@
*
* in the OSGi case there are helper methods to add ClassLoaders to the LOAD_PATH or GEM_PATH
*
* a typical setup for the ContextClassLoader case looks likes this:
* a typical setup for the ContextClassLoader case and OSGi case looks likes this:
* <li>LOAD_PATH == [ "uri:classloader:/META-INF/jruby.home/lib/ruby/1.9/site_ruby",
* "uri:classloader:/META-INF/jruby.home/lib/ruby/shared",
* "uri:classloader:/META-INF/jruby.home/lib/ruby/1.9",
* "uri:classloader:" ]</li>
* <li>Gem::Specification.dirs == [ "uri:classloader:", "uri:classloader:/META-INF/jruby.home/lib/ruby/gems/shared" ]
* <li>Gem::Specification.dirs == [ "uri:classloader:/specifications", "uri:classloader:/META-INF/jruby.home/lib/ruby/gems/shared/specifications" ]
* here very resource is loaded via <code>Thread.currentTHread.getContextClassLoader().getResourceAsStream(...)</code>
*
* a typical setup for OSGi case (one bundle with everything):
* <li>LOAD_PATH == [ "uri:bundle://16.0:1/META-INF/jruby.home/lib/ruby/1.9/site_ruby",
* "uri:bundle://16.0:1/META-INF/jruby.home/lib/ruby/shared",
* "uri:bundle://16.0:1/META-INF/jruby.home/lib/ruby/1.9",
* "uri:bundle://16.0:1" ]</li>
* <li>Gem::Specification.dirs == [ "uri:bundle://16.0:1", "uri:bundle://16.0:1/META-INF/jruby.home/lib/ruby/gems/shared" ]
* other OSGi frameworks use other uris like bundleresource:/16.fwk1661197821. here very resource is loaded via
* <code>new URL( uri )openStream()</code>, i.e. <code>new URL(classloader.getResource().toString()).openStream()</code> has to work for
* those classloaders. felix and equinox OSGi framework do work.
* <code>new URL( uri ).openStream()</code>, i.e. <code>new URL(classloader.getResource().toString()).openStream()</code> has to work for
* those classloaders. felix, knoplerfish and equinox OSGi framework do work.
*
* NOTE: <code>Gem.path</code> is base for determine the <code>Gem::Specification.dirs</code> and <code>Gem::Specification.dirs</code> is
* used to find gemspec files of the installed gems.
Expand All @@ -43,7 +36,6 @@ public class IsolatedScriptingContainer extends ScriptingContainer {

private static final String JRUBYDIR = "/.jrubydir";
private static final String JRUBY_HOME = "/META-INF/jruby.home";
private static final String JRUBY_HOME_DIR = JRUBY_HOME + JRUBYDIR;

public IsolatedScriptingContainer()
{
Expand All @@ -70,37 +62,25 @@ public IsolatedScriptingContainer( LocalContextScope scope,
LocalVariableBehavior behavior,
boolean lazy )
{
super( scope, behavior, lazy );
URL home = Thread.currentThread().getContextClassLoader().getResource( JRUBY_HOME_DIR.substring( 1 ) );
final String baseuri;
if ( home == null ) {
home = this.getClass().getClassLoader().getResource( JRUBY_HOME_DIR );
if ( home == null ) {
throw new RuntimeException( "BUG can not find " + JRUBY_HOME_DIR );
}
setClassLoader( this.getClass().getClassLoader() );
setHomeDirectory( "uri:" + home.toString().replaceFirst( JRUBYDIR + "$", "" ) );
baseuri = createUri(getClassLoader(), "/jruby/java.rb" );
}
else {
setHomeDirectory( "uri:classloader:" + JRUBY_HOME );
baseuri = "uri:classloader:/";
}
super(scope, behavior, lazy);

// get the right classloader
ClassLoader cl = this.getClass().getClassLoader();
if (cl == null) cl = Thread.currentThread().getContextClassLoader();
setClassLoader( cl );

setLoadPaths( Arrays.asList( "uri:classloader:" ) );

// clean up LOAD_PATH
getProvider().getRubyInstanceConfig().setLoadPaths(Arrays.asList(baseuri));
runScriptlet( "$LOAD_PATH.delete_if{|p| p =~ /jar$/ };"
// TODO NormalizedFile does too much - should leave uri: files as they are
+ "$LOAD_PATH.each{|p| p.sub!( /:\\/([^\\/])/,'://\\1' )}" );

runScriptlet( "require 'rubygems/defaults/jruby';" // make sure we have the monkey patch Gem::Specification
// set the right jruby home
setHomeDirectory( "uri:classloader:" + JRUBY_HOME );

// setup the isolated GEM_PATH, i.e. without $HOME/.gem/**
runScriptlet("require 'rubygems/defaults/jruby';"
+ "Gem::Specification.reset;"
+ "Gem::Specification.add_dir '" + getHomeDirectory() + "/lib/ruby/gems/shared';"
// if jruby-core and jruby-stdlib comes from the same osgi bundle, assume the embedded gems
// are in the same bundle
+ (getHomeDirectory().startsWith(baseuri) ? "Gem::Specification.add_dir '" + baseuri + "';" : "" ) );
+ "Gem::Specification.add_dir 'uri:classloader:" + JRUBY_HOME + "/lib/ruby/gems/shared';"
+ "Gem::Specification.add_dir 'uri:classloader:';");
}

public void addLoadPath( ClassLoader cl ) {
addLoadPath( cl, JRUBYDIR );
}
Expand Down
Expand Up @@ -285,7 +285,7 @@ private void loadJar(Ruby runtime, boolean wrap) {
}
else if (location.startsWith(URLResource.URI)){
url = null;
runtime.getJRubyClassLoader().addURLNoIndex(URLResource.getResourceURL(location));
runtime.getJRubyClassLoader().addURLNoIndex(URLResource.getResourceURL(runtime, location));
}
else {
File f = new File(location);
Expand Down
8 changes: 6 additions & 2 deletions core/src/main/java/org/jruby/util/JRubyFile.java
Expand Up @@ -64,10 +64,14 @@ public static FileResource createResource(ThreadContext context, String pathname
}

public static FileResource createResource(Ruby runtime, String pathname) {
return createResource(runtime.getPosix(), runtime.getCurrentDirectory(), pathname);
return createResource(runtime.getPosix(), runtime, runtime.getCurrentDirectory(), pathname);
}

public static FileResource createResource(POSIX posix, String cwd, String pathname) {
return createResource(posix, null, cwd, pathname);
}

public static FileResource createResource(POSIX posix, Ruby runtime, String cwd, String pathname) {
FileResource emptyResource = EmptyFileResource.create(pathname);
if (emptyResource != null) return emptyResource;

Expand All @@ -80,7 +84,7 @@ public static FileResource createResource(POSIX posix, String cwd, String pathna
if (pathname.startsWith("classpath:")) return ClasspathResource.create(pathname);

// replace is needed for maven/jruby-complete/src/it/app_using_classpath_uri to work
if (pathname.startsWith("uri:")) return URLResource.create(pathname.replace("classpath:/", ""));
if (pathname.startsWith("uri:")) return URLResource.create(runtime, pathname.replace("classpath:/", ""));

if (pathname.startsWith("file:")) {
pathname = pathname.substring(5);
Expand Down
69 changes: 38 additions & 31 deletions core/src/main/java/org/jruby/util/URLResource.java
Expand Up @@ -14,6 +14,7 @@

import jnr.posix.FileStat;

import org.jruby.Ruby;
import org.jruby.util.io.ChannelDescriptor;
import org.jruby.Ruby;
import org.jruby.exceptions.RaiseException;
Expand All @@ -33,19 +34,21 @@ public class URLResource extends AbstractFileResource {
private final String pathname;

private final JarFileStat fileStat;
private final ClassLoader cl;

URLResource(String uri, URL url, String[] files) {
this(uri, url, null, files);
this(uri, url, null, null, files);
}

URLResource(String uri, String pathname, String[] files) {
this(uri, null, pathname, files);
URLResource(String uri, ClassLoader cl, String pathname, String[] files) {
this(uri, null, cl, pathname, files);
}

private URLResource(String uri, URL url, String pathname, String[] files) {
private URLResource(String uri, URL url, ClassLoader cl, String pathname, String[] files) {
this.uri = uri;
this.list = files;
this.url = url;
this.cl = cl;
this.pathname = pathname;
this.fileStat = new JarFileStat(this);
}
Expand Down Expand Up @@ -95,7 +98,7 @@ public long length()
@Override
public boolean canRead()
{
return isFile();
return exists();
}

@Override
Expand Down Expand Up @@ -128,49 +131,49 @@ public FileStat lstat() {

@Override
public JRubyFile hackyGetJRubyFile() {
return JRubyNonExistentFile.NOT_EXIST;
return JRubyNonExistentFile.NOT_EXIST;
}

@Override
InputStream openInputStream() throws IOException {
if (pathname != null) {
return Thread.currentThread().getContextClassLoader().getResourceAsStream(pathname);
}
return url.openStream();
InputStream openInputStream() throws IOException
{
if (pathname != null) {
return cl.getResourceAsStream(pathname);
}
return url.openStream();
}

@Override
public ChannelDescriptor openDescriptor(ModeFlags flags, int perm) throws ResourceException {
return new ChannelDescriptor(inputStream(), flags);
}

public static FileResource createClassloaderURI(String pathname) {
public static FileResource createClassloaderURI(Ruby runtime, String pathname) {
// retrieve the classloader from the runtime if available otherwise mimic how the runtime got its classloader and
// take this
ClassLoader cl = runtime != null ? runtime.getJRubyClassLoader() : URLResource.class.getClassLoader();
if (cl == null ) {
cl = Thread.currentThread().getContextClassLoader();
}
if (pathname.startsWith("/")) {
pathname = pathname.substring(1);
}
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(pathname);
if (is != null) {
try
{
is.close();
}
// need Exception here due to strange NPE in some cases
catch (Exception e) {}
}
String[] files = listClassLoaderFiles(pathname);
URL url = cl.getResource(pathname);
String[] files = listClassLoaderFiles(cl, pathname);
return new URLResource(URI_CLASSLOADER + pathname,
is == null ? null : pathname,
cl,
url == null ? null : pathname,
files);
}

public static FileResource create(String pathname)
public static FileResource create(Ruby runtime, String pathname)
{
if (!pathname.startsWith(URI)) {
return null;
}
pathname = pathname.substring(URI.length());
if (pathname.startsWith(CLASSLOADER)) {
return createClassloaderURI(pathname.substring(CLASSLOADER.length()));
return createClassloaderURI(runtime, pathname.substring(CLASSLOADER.length()));
}
return createRegularURI(pathname);
}
Expand All @@ -180,7 +183,10 @@ private static FileResource createRegularURI(String pathname) {
try
{
// TODO NormalizedFile does too much - should leave uri: files as they are
// and make file:/a protocol to be file:///a so the second replace does not apply
pathname = pathname.replaceFirst( "file:/([^/])", "file:///$1" );
pathname = pathname.replaceFirst( ":/([^/])", "://$1" );

url = new URL(pathname);
// we do not want to deal with those url here like this though they are valid url/uri
if (url.getProtocol().startsWith("http")){
Expand All @@ -189,6 +195,7 @@ private static FileResource createRegularURI(String pathname) {
}
catch (MalformedURLException e)
{
e.printStackTrace();
// file does not exists
return new URLResource(URI + pathname, (URL)null, null);
}
Expand Down Expand Up @@ -242,13 +249,14 @@ private static String[] listFilesFromInputStream(InputStream is) {
}
}
}
private static String[] listClassLoaderFiles(String pathname) {

private static String[] listClassLoaderFiles(ClassLoader classloader, String pathname) {
if (pathname.endsWith(".rb") || pathname.endsWith(".class") || pathname.endsWith(".jar")) {
return null;
}
try
{
Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(pathname + "/.jrubydir");
Enumeration<URL> urls = classloader.getResources(pathname + "/.jrubydir");
if (!urls.hasMoreElements()) {
return null;
}
Expand All @@ -275,8 +283,7 @@ private static String[] listFiles(String pathname) {
}
try
{
// TODO remove this replace
InputStream is = new URL(pathname.replace("file://", "file:/") + "/.jrubydir").openStream();
InputStream is = new URL(pathname + "/.jrubydir").openStream();
// no inputstream happens with knoplerfish OSGI and osgi tests from /maven/jruby-complete
if (is != null) {
return listFilesFromInputStream(is);
Expand All @@ -291,10 +298,10 @@ private static String[] listFiles(String pathname) {
}
}

public static URL getResourceURL(String location)
public static URL getResourceURL(Ruby runtime, String location)
{
if (location.startsWith(URI + CLASSLOADER)){
return Thread.currentThread().getContextClassLoader().getResource(location.substring(URI_CLASSLOADER.length()));
return runtime.getJRubyClassLoader().getResource(location.substring(URI_CLASSLOADER.length()));
}
try
{
Expand Down
60 changes: 52 additions & 8 deletions core/src/test/java/org/jruby/util/URLResourceTest.java
Expand Up @@ -8,8 +8,7 @@ public class URLResourceTest extends TestCase {

public void testDirectory(){
String uri = Thread.currentThread().getContextClassLoader().getResource( "somedir" ).toExternalForm();
// hmm not sure why the url from the classloader does not work :(
FileResource resource = URLResource.create( "uri:" + uri.replace( "file://", "file:/" ));
FileResource resource = URLResource.create( null, "uri:" + uri);

assertNotNull(resource );
assertFalse(resource.isFile());
Expand All @@ -21,8 +20,7 @@ public void testDirectory(){

public void testNoneDirectory(){
String uri = Thread.currentThread().getContextClassLoader().getResource( "somedir/dir_without_listing" ).toExternalForm();
// TODO once the URLResource does keep the protocol part of the uri as is we can remove this replace
FileResource resource = URLResource.create( "uri:" + uri.replace( "file:/", "file:///" ));
FileResource resource = URLResource.create( null, "uri:" + uri);

assertNotNull(resource );
// you can open streams on file-system directories
Expand All @@ -34,8 +32,7 @@ public void testNoneDirectory(){

public void testFile(){
String uri = Thread.currentThread().getContextClassLoader().getResource( "somedir/.jrubydir" ).toExternalForm();
// TODO once the URLResource does keep the protocol part of the uri as is we can remove this replace
FileResource resource = URLResource.create( "uri:" + uri.replace( "file:/", "file:///" ));
FileResource resource = URLResource.create( null, "uri:" + uri);

assertNotNull(resource );
// you can open streams on file-system directories
Expand All @@ -47,13 +44,60 @@ public void testFile(){

public void testNonExistingFile(){
String uri = Thread.currentThread().getContextClassLoader().getResource( "somedir" ).toExternalForm();
// TODO once the URLResource does keep the protocol part of the uri as is we can remove this replace
FileResource resource = URLResource.create( "uri:" + uri.replace( "file:/", "file:///" ) + "/not_there");
FileResource resource = URLResource.create( null, "uri:" + uri + "/not_there");

assertNotNull(resource );
assertFalse(resource.isFile());
assertFalse(resource.exists());
assertFalse(resource.isDirectory());
assertNull(resource.list());
}

public void testDirectoryClassloader()
{
FileResource resource = URLResource.create( null, "uri:classloader:/somedir");

assertNotNull( resource );
assertFalse( resource.isFile() );
assertTrue( resource.isDirectory() );
assertTrue( resource.exists() );
assertEquals( Arrays.asList( resource.list() ),
Arrays.asList( new String[] { ".", "dir_without_listing",
"dir_with_listing" } ) );
}

public void testNoneDirectoryClassloader()
{
FileResource resource = URLResource.create( null, "uri:classloader:/somedir/dir_without_listing");

assertNotNull( resource );
// you can open streams on file-system directories
assertTrue( resource.isFile() );
assertTrue( resource.exists() );
assertFalse( resource.isDirectory() );
assertNull( resource.list() );
}

public void testFileClassloader()
{
FileResource resource = URLResource.create( null, "uri:classloader:/somedir/.jrubydir" );

assertNotNull( resource );
// you can open streams on file-system directories
assertTrue( resource.isFile() );
assertTrue( resource.exists() );
assertFalse( resource.isDirectory() );
assertNull( resource.list() );
}

public void testNonExistingFileClassloader()
{
FileResource resource = URLResource.create( null, "uri:classloader:/somedir/not_there" );

assertNotNull( resource );
assertFalse( resource.isFile() );
assertFalse( resource.exists() );
assertFalse( resource.isDirectory() );
assertNull( resource.list() );
}
}
Expand Up @@ -93,7 +93,7 @@ public void testJRubyCreate() throws Exception {

String gemPath = (String) jruby.runScriptlet( "Gem::Specification.dirs.inspect" );
gemPath = gemPath.replaceAll( "bundle[^:]*://[^/]*", "bundle:/" );
assertEquals( gemPath, "[\"uri:bundle://specifications\", \"uri:bundle://specifications\", \"uri:bundle://META-INF/jruby.home/lib/ruby/gems/shared/specifications\"]" );
assertEquals( gemPath, "[\"uri:bundle://specifications\", \"uri:classloader:/specifications\", \"uri:classloader:/META-INF/jruby.home/lib/ruby/gems/shared/specifications\"]" );

// ensure we can load rake from the default gems
boolean loaded = (Boolean) jruby.runScriptlet( "require 'rake'" );
Expand Down

0 comments on commit 1eedf72

Please sign in to comment.