Revision: 1190
          http://stripes.svn.sourceforge.net/stripes/?rev=1190&view=rev
Author:   bengunter
Date:     2009-10-23 20:14:05 +0000 (Fri, 23 Oct 2009)

Log Message:
-----------
First crack at a fix for STS-655 and STS-666. I have overhauled ResolverUtil to 
use the URLs returned by the class loader to list directories and read JARs as 
much as possible. So far I have confirmed this works on the following app 
servers in their default configurations: Tomcat 5.5, 6; Jetty 6; JBoss 4.2, 
5.0, 5.1.

Modified Paths:
--------------
    branches/1.5.x/stripes/src/net/sourceforge/stripes/util/ResolverUtil.java

Modified: 
branches/1.5.x/stripes/src/net/sourceforge/stripes/util/ResolverUtil.java
===================================================================
--- branches/1.5.x/stripes/src/net/sourceforge/stripes/util/ResolverUtil.java   
2009-10-22 15:10:40 UTC (rev 1189)
+++ branches/1.5.x/stripes/src/net/sourceforge/stripes/util/ResolverUtil.java   
2009-10-23 20:14:05 UTC (rev 1190)
@@ -14,16 +14,24 @@
  */
 package net.sourceforge.stripes.util;
 
+import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.lang.annotation.Annotation;
+import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.Enumeration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.jar.JarEntry;
 import java.util.jar.JarInputStream;
+import java.util.regex.Pattern;
 
 /**
  * <p>ResolverUtil is used to locate classes that are available in the/a class 
path and meet
@@ -61,6 +69,13 @@
     /** An instance of Log to use for logging in this class. */
     private static final Log log = Log.getInstance(ResolverUtil.class);
 
+    /** The magic header that indicates a JAR (ZIP) file. */
+    private static final byte[] JAR_MAGIC = { 'P', 'K', 3, 4 };
+
+    /** Regular expression that matches a Java identifier. */
+    private static final Pattern JAVA_IDENTIFIER_PATTERN = Pattern
+            
.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*");
+
     /**
      * A simple interface that specifies how to test classes to determine if 
they
      * are to be included in the results produced by the ResolverUtil.
@@ -201,109 +216,253 @@
      *        classes, e.g. {...@code net.sourceforge.stripes}
      */
     public ResolverUtil<T> find(Test test, String packageName) {
-        packageName = packageName.replace('.', '/');
-        ClassLoader loader = getClassLoader();
-        Enumeration<URL> urls;
+        String path = getPackagePath(packageName);
 
         try {
-            urls = loader.getResources(packageName);
+            List<URL> urls = 
Collections.list(getClassLoader().getResources(path));
+            for (URL url : urls) {
+                List<String> children = listClassResources(url, path);
+                for (String child : children) {
+                    addIfMatching(test, child);
+                }
+            }
         }
         catch (IOException ioe) {
-            log.warn("Could not read package: " + packageName, ioe);
-            return this;
+            log.warn("Could not read package: ", packageName, " -- ", ioe);
         }
 
-        while (urls.hasMoreElements()) {
-            String urlPath = urls.nextElement().getFile();
-            urlPath = StringUtil.urlDecode(urlPath);
+        return this;
+    }
 
-            // If it's a file in a directory, trim the stupid file: spec
-            if ( urlPath.startsWith("file:") ) {
-                urlPath = urlPath.substring(5);
+    /**
+     * Recursively list all resources under the given URL that appear to 
define a Java class.
+     * Matching resources will have a name that ends in ".class" and have a 
relative path such that
+     * each segment of the path is a valid Java identifier. The resource paths 
returned will be
+     * relative to the URL and begin with the specified path.
+     * 
+     * @param url The URL of the parent resource to search.
+     * @param path The path with which each matching resource path must begin, 
relative to the URL.
+     * @return A list of matching resources. The list may be empty.
+     * @throws IOException
+     */
+    protected List<String> listClassResources(URL url, String path) throws 
IOException {
+        InputStream is = null;
+        try {
+            List<String> resources = new ArrayList<String>();
+
+            URL jarUrl = findJarForResource(url, path);
+            if (jarUrl != null) {
+                is = jarUrl.openStream();
+                resources = listClassResources(new JarInputStream(is), path);
             }
+            else {
+                List<String> children = new ArrayList<String>();
+                try {
+                    if (isJar(url)) {
+                        // Some versions of JBoss VFS give a JAR stream even 
though the resource
+                        // referenced by the URL isn't actually a JAR
+                        is = url.openStream();
+                        JarInputStream jarInput = new JarInputStream(is);
+                        for (JarEntry entry; (entry = 
jarInput.getNextJarEntry()) != null;) {
+                            log.trace("Jar entry: " + entry.getName());
+                            if (isRelevantResource(entry.getName())) {
+                                children.add(entry.getName());
+                            }
+                        }
+                    }
+                    else {
+                        // Read listing, one per line
+                        is = url.openStream();
+                        BufferedReader reader = new BufferedReader(new 
InputStreamReader(is));
+                        for (String line; (line = reader.readLine()) != null;) 
{
+                            log.trace("Reader entry: " + line);
+                            if (isRelevantResource(line)) {
+                                children.add(line);
+                            }
+                        }
+                    }
+                }
+                catch (FileNotFoundException e) {
+                    // For file URLs the openStream() call will fail because 
you can't open a
+                    // directory for reading. List the directory directly 
instead.
+                    if ("file".equals(url.getProtocol()))
+                        children = Arrays.asList(new 
File(url.getFile()).list());
+                }
 
-            // Else it's in a JAR, grab the path to the jar
-            if (urlPath.indexOf('!') > 0) {
-                urlPath = urlPath.substring(0, urlPath.indexOf('!'));
+                // The URL prefix to use when recursively listing child 
resources
+                String prefix = url.toExternalForm();
+                if (!prefix.endsWith("/"))
+                    prefix = prefix + "/";
+
+                // Iterate over each immediate child, adding classes and 
recursing into directories
+                for (String child : children) {
+                    String resourcePath = path + "/" + child;
+                    if (child.endsWith(".class")) {
+                        log.trace("Found class file: ", resourcePath);
+                        resources.add(resourcePath);
+                    }
+                    else {
+                        URL childUrl = new URL(prefix + child);
+                        resources.addAll(listClassResources(childUrl, 
resourcePath));
+                    }
+                }
             }
 
-            log.info("Scanning for classes in [", urlPath, "] matching 
criteria: ", test);
-            File file = new File(urlPath);
-            if ( file.isDirectory() ) {
-                loadImplementationsInDirectory(test, packageName, file);
+            return resources;
+        }
+        finally {
+            try {
+                is.close();
             }
-            else {
-                loadImplementationsInJar(test, packageName, file);
+            catch (Exception e) {
             }
         }
-        
-        return this;
     }
 
-
     /**
-     * Finds matches in a physical directory on a filesystem.  Examines all
-     * files within a directory - if the File object is not a directory, and 
ends with <i>.class</i>
-     * the file is loaded and tested to see if it is acceptable according to 
the Test.  Operates
-     * recursively to find classes within a folder structure matching the 
package structure.
-     *
-     * @param test a Test used to filter the classes that are discovered
-     * @param parent the package name up to this directory in the package 
hierarchy.  E.g. if
-     *        /classes is in the classpath and we wish to examine files in 
/classes/org/apache then
-     *        the values of <i>parent</i> would be <i>org/apache</i>
-     * @param location a File object representing a directory
+     * List the names of the entries in the given {...@link JarInputStream} 
that begin with the
+     * specified {...@code path}. Entries will match with or without a leading 
slash.
+     * 
+     * @param jar The JAR input stream
+     * @param path The leading path to match
+     * @return The names of all the matching entries
+     * @throws IOException
      */
-    private void loadImplementationsInDirectory(Test test, String parent, File 
location) {
-        File[] files = location.listFiles();
-        StringBuilder builder = null;
+    protected List<String> listClassResources(JarInputStream jar, String path) 
throws IOException {
+        // Include the leading and trailing slash when matching names
+        if (!path.startsWith("/"))
+            path = "/" + path;
+        if (!path.endsWith("/"))
+            path = path + "/";
 
-               // File.listFiles() can return null when an IO error occurs!
-               if (files == null) {
-                       log.warn("Could not list directory " + 
location.getAbsolutePath() +
-                                " when looking for classes matching: " + test);
-                       return;
-               }
+        // Iterate over the entries and collect those that begin with the 
requested path
+        List<String> resources = new ArrayList<String>();
+        for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
+            if (!entry.isDirectory()) {
+                // Add leading slash if it's missing
+                String name = entry.getName();
+                if (!name.startsWith("/"))
+                    name = "/" + name;
 
-        for (File file : files) {
-            builder = new StringBuilder(100);
-            builder.append(parent).append("/").append(file.getName());
-            String packageOrClass = ( parent == null ? file.getName() : 
builder.toString() );
+                // Check file name
+                if (name.endsWith(".class") && name.startsWith(path)) {
+                    log.debug("Found class file: ", name);
+                    resources.add(name.substring(1)); // Trim leading slash
+                }
+            }
+        }
+        return resources;
+    }
 
-            if (file.isDirectory()) {
-                loadImplementationsInDirectory(test, packageOrClass, file);
+    /**
+     * Attempts to deconstruct the given URL to find a JAR file containing the 
resource referenced
+     * by the URL. That is, assuming the URL references a JAR entry, this 
method will return a URL
+     * that references the JAR file containing the entry. If the JAR cannot be 
located, then this
+     * method returns null.
+     * 
+     * @param url The URL of the JAR entry.
+     * @param path The path by which the URL was requested from the class 
loader.
+     * @return The URL of the JAR file, if one is found. Null if not.
+     * @throws MalformedURLException
+     */
+    protected URL findJarForResource(URL url, String path) throws 
MalformedURLException {
+        log.trace("Find JAR file: ", url);
+
+        // If the file part of the URL is itself a URL, then that URL probably 
points to the JAR
+        try {
+            for (;;) {
+                url = new URL(url.getFile());
+                log.trace("Inner URL: ", url);
             }
-            else if (file.getName().endsWith(".class")) {
-                addIfMatching(test, packageOrClass);
-            }
         }
+        catch (MalformedURLException e) {
+        }
+
+        // Look for the .jar extension and chop off everything after that
+        StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
+        int index = jarUrl.lastIndexOf(".jar");
+        if (index >= 0) {
+            jarUrl.setLength(index + 4);
+            log.debug("Extracted JAR URL: ", jarUrl);
+        }
+        else {
+            return null;
+        }
+
+        // Try to open and test it
+        try {
+            URL testUrl = new URL(jarUrl.toString());
+            if (isJar(testUrl))
+                return testUrl;
+        }
+        catch (MalformedURLException e) {
+            log.error("Invalid JAR URL: ", jarUrl);
+        }
+
+        return null;
     }
 
     /**
-     * Finds matching classes within a jar files that contains a folder 
structure
-     * matching the package structure.  If the File is not a JarFile or does 
not exist a warning
-     * will be logged, but no error will be raised.
-     *
-     * @param test a Test used to filter the classes that are discovered
-     * @param parent the parent package under which classes must be in order 
to be considered
-     * @param jarfile the jar file to be examined for classes
+     * Converts a Java package name to a path that can be looked up with a 
call to
+     * {...@link ClassLoader#getResources(String)}.
+     * 
+     * @param packageName The Java package name to convert to a path
      */
-    private void loadImplementationsInJar(Test test, String parent, File 
jarfile) {
+    protected String getPackagePath(String packageName) {
+        return packageName == null ? null : packageName.replace('.', '/');
+    }
 
+    /**
+     * Returns true if the name of a resource (file or directory) is one that 
matters in the search
+     * for classes. Relevant resources would be class files themselves (file 
names that end with
+     * ".class") and directories that might be a Java package name segment 
(java identifiers).
+     * 
+     * @param resourceName The resource name, without path information
+     */
+    protected boolean isRelevantResource(String resourceName) {
+        return resourceName != null
+                && (resourceName.endsWith(".class") || JAVA_IDENTIFIER_PATTERN
+                        .matcher(resourceName).matches());
+    }
+
+    /**
+     * Returns true if the resource located at the given URL is a JAR file.
+     * 
+     * @param url The URL of the resource to test.
+     */
+    protected boolean isJar(URL url) {
+        return isJar(url, new byte[JAR_MAGIC.length]);
+    }
+
+    /**
+     * Returns true if the resource located at the given URL is a JAR file.
+     * 
+     * @param url The URL of the resource to test.
+     * @param buffer A buffer into which the first few bytes of the resource 
are read. The buffer
+     *            must be at least the size of {...@link #JAR_MAGIC}. (The 
same buffer may be reused
+     *            for multiple calls as an optimization.)
+     */
+    protected boolean isJar(URL url, byte[] buffer) {
+        InputStream is = null;
         try {
-            JarEntry entry;
-            JarInputStream jarStream = new JarInputStream(new 
FileInputStream(jarfile));
-
-            while ( (entry = jarStream.getNextJarEntry() ) != null) {
-                String name = entry.getName();
-                if (!entry.isDirectory() && name.startsWith(parent) && 
name.endsWith(".class")) {
-                    addIfMatching(test, name);
-                }
+            is = url.openStream();
+            is.read(buffer, 0, JAR_MAGIC.length);
+            if (Arrays.equals(buffer, JAR_MAGIC)) {
+                log.trace("Found JAR: ", url);
+                return true;
             }
         }
-        catch (IOException ioe) {
-            log.error("Could not search jar file '", jarfile, "' for classes 
matching criteria: ",
-                      test, "due to an IOException: ", ioe.getMessage());
+        catch (Exception e) {
         }
+        finally {
+            try {
+                is.close();
+            }
+            catch (Exception e) {
+            }
+        }
+
+        return false;
     }
 
     /**


This was sent by the SourceForge.net collaborative development platform, the 
world's largest Open Source development site.

------------------------------------------------------------------------------
Come build with us! The BlackBerry(R) Developer Conference in SF, CA
is the only developer event you need to attend this year. Jumpstart your
developing skills, take BlackBerry mobile applications to market and stay 
ahead of the curve. Join us from November 9 - 12, 2009. Register now!
http://p.sf.net/sfu/devconference
_______________________________________________
Stripes-development mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/stripes-development

Reply via email to