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