From 3956366c654fc1feaae5c1709be13523e2cde024 Mon Sep 17 00:00:00 2001 From: Matt Fletcher Date: Sat, 6 Feb 2010 16:32:34 -0500 Subject: [PATCH] Forced Stas Garifulin's CompundJarClassLoader into JRubyClassLoader (along with CompoundJarURLStreamHandler to help it). This makes Charles example in http://jira.codehaus.org/browse/JRUBY-3299 work. --- .../jruby/util/CompoundJarURLStreamHandler.java | 130 ++++++++++ src/org/jruby/util/JRubyClassLoader.java | 247 ++++++++++++++++++++ 2 files changed, 377 insertions(+), 0 deletions(-) create mode 100644 src/org/jruby/util/CompoundJarURLStreamHandler.java diff --git a/src/org/jruby/util/CompoundJarURLStreamHandler.java b/src/org/jruby/util/CompoundJarURLStreamHandler.java new file mode 100644 index 0000000..00b6124 --- /dev/null +++ b/src/org/jruby/util/CompoundJarURLStreamHandler.java @@ -0,0 +1,130 @@ +package org.jruby.util; + +import java.io.Closeable; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +// From Stas Garifulin's CompoundJarURLStreamHandler. http://jira.codehaus.org/browse/JRUBY-3299 +public class CompoundJarURLStreamHandler extends URLStreamHandler { + + public static final String PROTOCOL = "compoundjar"; + + private static final CompoundJarURLStreamHandler instance = new CompoundJarURLStreamHandler(); + + public static URL createUrl(URL base, List path) throws MalformedURLException { + return createUrl(base, path.toArray(new String[0])); + } + + public static URL createUrl(URL base, String... path) throws MalformedURLException { + StringBuilder pathBuilder = new StringBuilder(); + + pathBuilder.append(base.toExternalForm()); + + for (String entry : path) { + pathBuilder.append("!"); + + if (!entry.startsWith("/")) { + pathBuilder.append("/"); + } + + pathBuilder.append(entry); + } + + return new URL(PROTOCOL, null, -1, pathBuilder.toString(), instance); + } + + static class CompoundJarURLConnection extends URLConnection { + + private final URL baseJarUrl; + + private final String[] path; + + CompoundJarURLConnection(URL url) throws MalformedURLException { + super(url); + + String spec = url.getPath(); + + path = spec.split("\\!\\/"); + baseJarUrl = new URL(path[0]); + } + + @Override + public void connect() throws IOException { + connected = true; + } + + private InputStream openEntry(String[] path, JarInputStream currentJar, int currentDepth) throws IOException { + + final String localPath = path[currentDepth]; + + for (JarEntry entry = currentJar.getNextJarEntry(); entry != null; entry = currentJar.getNextJarEntry()) { + + if (entry.getName().equals(localPath)) { + if (currentDepth + 1 < path.length) { + JarInputStream embeddedJar = new JarInputStream(currentJar); + + return openEntry(path, embeddedJar, currentDepth + 1); + } else { + return currentJar; + } + } + } + + return null; + } + + private static void close(Closeable resource) { + if (resource != null) { + try { + resource.close(); + } catch (IOException ignore) { + } + } + } + + @Override + public InputStream getInputStream() throws IOException { + + InputStream result; + + InputStream baseInputStream = baseJarUrl.openStream(); + + if (path.length > 1) { + try { + JarInputStream baseJar = new JarInputStream(baseInputStream); + + result = openEntry(path, baseJar, 1); + } catch (IOException ex) { + close(baseInputStream); + + throw ex; + } catch (RuntimeException ex) { + close(baseInputStream); + + throw ex; + } + } else { + result = baseInputStream; + } + + if (result == null) { + throw new FileNotFoundException(url.toExternalForm()); + } + + return result; + } + } + + @Override + protected URLConnection openConnection(URL url) throws IOException { + return new CompoundJarURLConnection(url); + } +} \ No newline at end of file diff --git a/src/org/jruby/util/JRubyClassLoader.java b/src/org/jruby/util/JRubyClassLoader.java index 1fa5dd5..93b5916 100644 --- a/src/org/jruby/util/JRubyClassLoader.java +++ b/src/org/jruby/util/JRubyClassLoader.java @@ -3,6 +3,18 @@ package org.jruby.util; import java.net.URL; import java.net.URLClassLoader; import java.security.ProtectionDomain; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; public class JRubyClassLoader extends URLClassLoader { private final static ProtectionDomain DEFAULT_DOMAIN @@ -25,4 +37,239 @@ public class JRubyClassLoader extends URLClassLoader { public Class defineClass(String name, byte[] bytes, ProtectionDomain domain) { return super.defineClass(name, bytes, 0, bytes.length, domain); } + + // From Stas Garifulin's CompundJarClassLoader. http://jira.codehaus.org/browse/JRUBY-3299 + @Override + protected Class findClass(String className) throws ClassNotFoundException { + try { + return super.findClass(className); + } catch (ClassNotFoundException ex) { + String resourceName = className.replace('.', '/').concat(".class"); + + for (URL jarUrl : getURLs()) { + try { + InputStream baseInputStream = jarUrl.openStream(); + + try { + JarInputStream baseJar = new JarInputStream(baseInputStream); + + InputStream input = performDeepSearch(baseJar, resourceName, 0); + + if (input != null) { + byte[] buffer = new byte[1024]; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + for (int count = input.read(buffer); count > 0; count = input.read(buffer)) { + output.write(buffer, 0, count); + } + + byte[] data = output.toByteArray(); + + return defineClass(className, data, 0, data.length); + } + } finally { + close(baseInputStream); + } + } catch (IOException innerEx) { + logException(innerEx); + } + } + + throw new ClassNotFoundException(className, ex); + } + } + + @Override + public URL findResource(String resourceName) { + URL result = super.findResource(resourceName); + + if (result == null) { + for (URL jarUrl : getURLs()) { + try { + InputStream baseInputStream = jarUrl.openStream(); + + try { + JarInputStream baseJar = new JarInputStream(baseInputStream); + + List path = findEmbeddedResource(baseJar, resourceName, new ArrayList(), 0); + + if (path != null) { + result = CompoundJarURLStreamHandler.createUrl(jarUrl, path); + } + + } finally { + close(baseInputStream); + } + } catch (IOException ex) { + logException(ex); + } + } + } + + return result; + } + + @Override + public Enumeration findResources(String resourceName) throws IOException { + + final List embeddedUrls = new ArrayList(); + + for (URL jarUrl : getURLs()) { + try { + InputStream baseInputStream = jarUrl.openStream(); + + try { + JarInputStream baseJar = new JarInputStream(baseInputStream); + List> result = new ArrayList>(); + + collectEmbeddedResources(result, baseJar, resourceName, new ArrayList(), 0); + + for (List path : result) { + embeddedUrls.add(CompoundJarURLStreamHandler.createUrl(jarUrl, path)); + } + } finally { + close(baseInputStream); + } + } catch (IOException ex) { + logException(ex); + } + } + + if (embeddedUrls.isEmpty()) { + return super.findResources(resourceName); + } else { + final Enumeration originalResult = super.findResources(resourceName); + + return new Enumeration() { + private Iterator extendedResult; + + @Override + public URL nextElement() { + if (extendedResult == null) { + return originalResult.nextElement(); + } else { + return extendedResult.next(); + } + } + + @Override + public boolean hasMoreElements() { + if (extendedResult == null) { + boolean result = originalResult.hasMoreElements(); + + if (!result) { + // original result is consumed, switching to result + // from embedded jars processing. + extendedResult = embeddedUrls.iterator(); + result = extendedResult.hasNext(); + } + return result; + } else { + return extendedResult.hasNext(); + } + } + }; + } + } + + private InputStream performDeepSearch(JarInputStream currentJar, String resourceName, int level) throws IOException { + + for (JarEntry entry = currentJar.getNextJarEntry(); entry != null; entry = currentJar.getNextJarEntry()) { + + String entryName = entry.getName(); + + if (level > 0 && entryName.equals(resourceName)) { + return currentJar; + } else if (isJarFile(entry)) { + JarInputStream embeddedJar = new JarInputStream(currentJar); + + InputStream result = performDeepSearch(embeddedJar, resourceName, level + 1); + + if (result != null) { + return result; + } + } + } + + return null; + } + + private List findEmbeddedResource(JarInputStream currentJar, String resourceName, List currentPath, + int level) throws IOException { + + for (JarEntry entry = currentJar.getNextJarEntry(); entry != null; entry = currentJar.getNextJarEntry()) { + + String entryName = entry.getName(); + + List result = null; + + if (level > 0 && entryName.equals(resourceName)) { + result = new ArrayList(currentPath); + result.add(resourceName); + } else if (isJarFile(entry)) { + JarInputStream embeddedJar = new JarInputStream(currentJar); + + currentPath.add(entryName); + + try { + result = findEmbeddedResource(embeddedJar, resourceName, currentPath, level + 1); + } finally { + currentPath.remove(entryName); + } + } + + if (result != null) { + return result; + } + } + + return null; + } + + private void collectEmbeddedResources(List> result, JarInputStream currentJar, String resourceName, + List currentPath, int level) throws IOException { + + for (JarEntry entry = currentJar.getNextJarEntry(); entry != null; entry = currentJar.getNextJarEntry()) { + + String entryName = entry.getName(); + + if (level > 0 && entryName.equals(resourceName)) { + List path = new ArrayList(currentPath); + path.add(resourceName); + + result.add(path); + } + + if (isJarFile(entry)) { + JarInputStream embeddedJar = new JarInputStream(currentJar); + + currentPath.add(entryName); + + try { + collectEmbeddedResources(result, embeddedJar, resourceName, currentPath, level + 1); + } finally { + currentPath.remove(entryName); + } + } + } + } + + private void logException(Exception ex) { + // TODO JRubyClassLoader should reference Ruby runtime in order to log + // to configured error stream. + ex.printStackTrace(); + } + + private static boolean isJarFile(JarEntry entry) { + return !entry.isDirectory() && entry.getName().endsWith(".jar"); + } + + private static void close(Closeable resource) { + if (resource != null) { + try { + resource.close(); + } catch (IOException ignore) { + } + } + } } -- 1.6.6