Revision: 1336 http://stripes.svn.sourceforge.net/stripes/?rev=1336&view=rev Author: bengunter Date: 2010-11-12 16:25:10 +0000 (Fri, 12 Nov 2010)
Log Message: ----------- Applied fix for STS-776, STS-773 and STS-775 from 1.5.x branch. Modified Paths: -------------- trunk/stripes/src/net/sourceforge/stripes/config/BootstrapPropertyResolver.java trunk/stripes/src/net/sourceforge/stripes/util/ResolverUtil.java Added Paths: ----------- trunk/stripes/src/net/sourceforge/stripes/vfs/ trunk/stripes/src/net/sourceforge/stripes/vfs/DefaultVFS.java trunk/stripes/src/net/sourceforge/stripes/vfs/JBoss6VFS.java trunk/stripes/src/net/sourceforge/stripes/vfs/VFS.java trunk/stripes/src/net/sourceforge/stripes/vfs/package.html Modified: trunk/stripes/src/net/sourceforge/stripes/config/BootstrapPropertyResolver.java =================================================================== --- trunk/stripes/src/net/sourceforge/stripes/config/BootstrapPropertyResolver.java 2010-11-12 15:50:31 UTC (rev 1335) +++ trunk/stripes/src/net/sourceforge/stripes/config/BootstrapPropertyResolver.java 2010-11-12 16:25:10 UTC (rev 1336) @@ -28,6 +28,7 @@ import net.sourceforge.stripes.util.ReflectUtil; import net.sourceforge.stripes.util.ResolverUtil; import net.sourceforge.stripes.util.StringUtil; +import net.sourceforge.stripes.vfs.VFS; /** * <p>Resolves configuration properties that are used to bootstrap the system. Essentially this boils @@ -50,12 +51,16 @@ private FilterConfig filterConfig; + /** The Configuration Key for looking up the comma separated list of VFS classes. */ + public static final String VFS_CLASSES = "VFS.Classes"; + /** The Configuration Key for looking up the comma separated list of extension packages. */ public static final String PACKAGES = "Extension.Packages"; /** Constructs a new BootstrapPropertyResolver with the given ServletConfig. */ public BootstrapPropertyResolver(FilterConfig filterConfig) { setFilterConfig(filterConfig); + initVFS(); } /** Stores a reference to the filter's FilterConfig object. */ @@ -68,6 +73,18 @@ return this.filterConfig; } + /** Add {...@link VFS} implementations that are specified in the filter configuration. */ + @SuppressWarnings("unchecked") + protected void initVFS() { + List<Class<?>> vfsImpls = getClassPropertyList(VFS_CLASSES); + for (Class<?> clazz : vfsImpls) { + if (!VFS.class.isAssignableFrom(clazz)) + log.warn("Class ", clazz.getName(), " does not extend ", VFS.class.getName()); + else + VFS.addImplClass((Class<? extends VFS>) clazz); + } + } + /** * Fetches a configuration property in the manner described in the class level javadoc for * this class. Modified: trunk/stripes/src/net/sourceforge/stripes/util/ResolverUtil.java =================================================================== --- trunk/stripes/src/net/sourceforge/stripes/util/ResolverUtil.java 2010-11-12 15:50:31 UTC (rev 1335) +++ trunk/stripes/src/net/sourceforge/stripes/util/ResolverUtil.java 2010-11-12 16:25:10 UTC (rev 1336) @@ -14,26 +14,14 @@ */ package net.sourceforge.stripes.util; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FilenameFilter; 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.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; +import net.sourceforge.stripes.vfs.VFS; + /** * <p>ResolverUtil is used to locate classes that are available in the/a class path and meet * arbitrary conditions. The two most common conditions are that a class implements/extends @@ -70,13 +58,6 @@ /** 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. @@ -100,8 +81,7 @@ public IsA(Class<?> parentType) { this.parent = parentType; } /** Returns true if type is assignable to the parent type supplied in the constructor. */ - @SuppressWarnings("unchecked") - public boolean matches(Class type) { + public boolean matches(Class<?> type) { return type != null && parent.isAssignableFrom(type); } @@ -121,8 +101,7 @@ public AnnotatedWith(Class<? extends Annotation> annotation) { this.annotation = annotation; } /** Returns true if the type is annotated with the class provided to the constructor. */ - @SuppressWarnings("unchecked") - public boolean matches(Class type) { + public boolean matches(Class<?> type) { return type != null && type.isAnnotationPresent(annotation); } @@ -220,12 +199,10 @@ String path = getPackagePath(packageName); try { - List<URL> urls = Collections.list(getClassLoader().getResources(path)); - for (URL url : urls) { - List<String> children = listClassResources(url, path); - for (String child : children) { + List<String> children = VFS.getInstance().list(path); + for (String child : children) { + if (child.endsWith(".class")) addIfMatching(test, child); - } } } catch (IOException ioe) { @@ -236,218 +213,6 @@ } /** - * 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 { - log.debug("Listing classes in ", url); - - InputStream is = null; - try { - List<String> resources = new ArrayList<String>(); - - // First, try to find the URL of a JAR file containing the requested resource. If a JAR - // file is found, then we'll list child resources by reading the JAR. - 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 might give a JAR stream even if 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 { - // Some servlet containers allow reading from "directory" resources like a - // text file, listing the child resources 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 might fail, depending on the servlet - * container, because directories can't be opened for reading. If that happens, - * then list the directory directly instead. - */ - if ("file".equals(url.getProtocol())) { - File file = new File(url.getFile()); - log.trace("Listing directory ", file.getAbsolutePath()); - if (file.isDirectory()) { - children = Arrays.asList(file.list(new FilenameFilter() { - public boolean accept(File dir, String name) { - return isRelevantResource(name); - } - })); - } - } - else { - // No idea where the exception came from so rethrow it - throw e; - } - } - - // 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)); - } - } - } - - return resources; - } - finally { - try { - is.close(); - } - catch (Exception e) { - } - } - } - - /** - * 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 - */ - 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 + "/"; - - // 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; - - // Check file name - if (name.endsWith(".class") && name.startsWith(path)) { - log.trace("Found class file: ", name); - resources.add(name.substring(1)); // Trim leading slash - } - } - } - return resources; - } - - /** - * 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 URL: ", 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); - } - } - catch (MalformedURLException e) { - // This will happen at some point and serves a break in the loop - } - - // 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.trace("Extracted JAR URL: ", jarUrl); - } - else { - log.trace("Not a JAR: ", jarUrl); - return null; - } - - // Try to open and test it - try { - URL testUrl = new URL(jarUrl.toString()); - if (isJar(testUrl)) { - return testUrl; - } - else { - // WebLogic fix: check if the URL's file exists in the filesystem. - log.trace("Not a JAR: ", jarUrl); - jarUrl.replace(0, jarUrl.length(), testUrl.getFile()); - File file = new File(jarUrl.toString()); - - // File name might be URL-encoded - if (!file.exists()) { - file = new File(StringUtil.urlDecode(jarUrl.toString())); - } - - if (file.exists()) { - log.trace("Trying real file: ", file.getAbsolutePath()); - testUrl = file.toURI().toURL(); - if (isJar(testUrl)) { - return testUrl; - } - } - } - } - catch (MalformedURLException e) { - log.warn("Invalid JAR URL: ", jarUrl); - } - - log.trace("Not a JAR: ", jarUrl); - return null; - } - - /** * Converts a Java package name to a path that can be looked up with a call to * {...@link ClassLoader#getResources(String)}. * @@ -458,60 +223,6 @@ } /** - * 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 { - is = url.openStream(); - is.read(buffer, 0, JAR_MAGIC.length); - if (Arrays.equals(buffer, JAR_MAGIC)) { - log.debug("Found JAR: ", url); - return true; - } - } - catch (Exception e) { - // Failure to read the stream means this is not a JAR - } - finally { - try { - is.close(); - } - catch (Exception e) { - } - } - - return false; - } - - /** * Add the class designated by the fully qualified class name provided to the set of * resolved classes if and only if it is approved by the Test supplied. * @@ -525,7 +236,7 @@ ClassLoader loader = getClassLoader(); log.trace("Checking to see if class ", externalName, " matches criteria [", test, "]"); - Class type = loader.loadClass(externalName); + Class<?> type = loader.loadClass(externalName); if (test.matches(type) ) { matches.add( (Class<T>) type); } Added: trunk/stripes/src/net/sourceforge/stripes/vfs/DefaultVFS.java =================================================================== --- trunk/stripes/src/net/sourceforge/stripes/vfs/DefaultVFS.java (rev 0) +++ trunk/stripes/src/net/sourceforge/stripes/vfs/DefaultVFS.java 2010-11-12 16:25:10 UTC (rev 1336) @@ -0,0 +1,306 @@ +/* Copyright 2010 Ben Gunter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.sourceforge.stripes.vfs; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +import net.sourceforge.stripes.util.Log; +import net.sourceforge.stripes.util.StringUtil; + +/** + * A default implementation of {...@link VFS} that works for most application servers. + * + * @author Ben Gunter + */ +public class DefaultVFS extends VFS { + private Log log = Log.getInstance(DefaultVFS.class); + + /** The magic header that indicates a JAR (ZIP) file. */ + private static final byte[] JAR_MAGIC = { 'P', 'K', 3, 4 }; + + @Override + public boolean isValid() { + return true; + } + + @Override + public List<String> list(URL url, String path) throws IOException { + InputStream is = null; + try { + List<String> resources = new ArrayList<String>(); + + // First, try to find the URL of a JAR file containing the requested resource. If a JAR + // file is found, then we'll list child resources by reading the JAR. + URL jarUrl = findJarForResource(url); + if (jarUrl != null) { + is = jarUrl.openStream(); + log.debug("Listing ", url); + resources = listResources(new JarInputStream(is), path); + } + else { + List<String> children = new ArrayList<String>(); + try { + if (isJar(url)) { + // Some versions of JBoss VFS might give a JAR stream even if the resource + // referenced by the URL isn't actually a JAR + is = url.openStream(); + JarInputStream jarInput = new JarInputStream(is); + log.debug("Listing ", url); + for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) { + log.trace("Jar entry: ", entry.getName()); + children.add(entry.getName()); + } + } + else { + /* + * Some servlet containers allow reading from directory resources like a + * text file, listing the child resources one per line. However, there is no + * way to differentiate between directory and file resources just by reading + * them. To work around that, as each line is read, try to look it up via + * the class loader as a child of the current resource. If any line fails + * then we assume the current resource is not a directory. + */ + is = url.openStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + List<String> lines = new ArrayList<String>(); + for (String line; (line = reader.readLine()) != null;) { + log.trace("Reader entry: ", line); + lines.add(line); + if (getResources(path + "/" + line).isEmpty()) { + lines.clear(); + break; + } + } + + if (!lines.isEmpty()) { + log.debug("Listing ", url); + children.addAll(lines); + } + } + } + catch (FileNotFoundException e) { + /* + * For file URLs the openStream() call might fail, depending on the servlet + * container, because directories can't be opened for reading. If that happens, + * then list the directory directly instead. + */ + if ("file".equals(url.getProtocol())) { + File file = new File(url.getFile()); + log.trace("Listing directory ", file.getAbsolutePath()); + if (file.isDirectory()) { + log.debug("Listing ", url); + children = Arrays.asList(file.list()); + } + } + else { + // No idea where the exception came from so rethrow it + throw e; + } + } + + // The URL prefix to use when recursively listing child resources + String prefix = url.toExternalForm(); + if (!prefix.endsWith("/")) + prefix = prefix + "/"; + + // Iterate over immediate children, adding files and recursing into directories + for (String child : children) { + String resourcePath = path + "/" + child; + resources.add(resourcePath); + URL childUrl = new URL(prefix + child); + resources.addAll(list(childUrl, resourcePath)); + } + } + + return resources; + } + finally { + try { + if (is != null) + is.close(); + } + catch (Exception e) { + } + } + } + + /** + * 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 If I/O errors occur + */ + protected List<String> listResources(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 + "/"; + + // 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; + + // Check file name + if (name.startsWith(path)) { + log.trace("Found resource: ", name); + resources.add(name.substring(1)); // Trim leading slash + } + } + } + return resources; + } + + /** + * 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. + * @return The URL of the JAR file, if one is found. Null if not. + * @throws MalformedURLException + */ + protected URL findJarForResource(URL url) throws MalformedURLException { + log.trace("Find JAR URL: ", 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); + } + } + catch (MalformedURLException e) { + // This will happen at some point and serves as a break in the loop + } + + // 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.trace("Extracted JAR URL: ", jarUrl); + } + else { + log.trace("Not a JAR: ", jarUrl); + return null; + } + + // Try to open and test it + try { + URL testUrl = new URL(jarUrl.toString()); + if (isJar(testUrl)) { + return testUrl; + } + else { + // WebLogic fix: check if the URL's file exists in the filesystem. + log.trace("Not a JAR: ", jarUrl); + jarUrl.replace(0, jarUrl.length(), testUrl.getFile()); + File file = new File(jarUrl.toString()); + + // File name might be URL-encoded + if (!file.exists()) { + file = new File(StringUtil.urlDecode(jarUrl.toString())); + } + + if (file.exists()) { + log.trace("Trying real file: ", file.getAbsolutePath()); + testUrl = file.toURI().toURL(); + if (isJar(testUrl)) { + return testUrl; + } + } + } + } + catch (MalformedURLException e) { + log.warn("Invalid JAR URL: ", jarUrl); + } + + log.trace("Not a JAR: ", jarUrl); + return null; + } + + /** + * 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 + */ + protected String getPackagePath(String packageName) { + return packageName == null ? null : packageName.replace('.', '/'); + } + + /** + * 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 { + is = url.openStream(); + is.read(buffer, 0, JAR_MAGIC.length); + if (Arrays.equals(buffer, JAR_MAGIC)) { + log.debug("Found JAR: ", url); + return true; + } + } + catch (Exception e) { + // Failure to read the stream means this is not a JAR + } + finally { + try { + is.close(); + } + catch (Exception e) { + } + } + + return false; + } +} Added: trunk/stripes/src/net/sourceforge/stripes/vfs/JBoss6VFS.java =================================================================== --- trunk/stripes/src/net/sourceforge/stripes/vfs/JBoss6VFS.java (rev 0) +++ trunk/stripes/src/net/sourceforge/stripes/vfs/JBoss6VFS.java 2010-11-12 16:25:10 UTC (rev 1336) @@ -0,0 +1,169 @@ +/* Copyright 2010 Ben Gunter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.sourceforge.stripes.vfs; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.sourceforge.stripes.util.Log; + +/** + * A {...@link VFS} implementation that works with the VFS API provided by JBoss 6. + * + * @author Ben Gunter + */ +public class JBoss6VFS extends net.sourceforge.stripes.vfs.VFS { + private static final Log log = Log.getInstance(JBoss6VFS.class); + + /** A class that mimics a tiny subset of the JBoss VirtualFile class. */ + static class VirtualFile { + static Class<?> VirtualFile; + static Method getPathNameRelativeTo, getChildrenRecursively; + + Object virtualFile; + + VirtualFile(Object virtualFile) { + this.virtualFile = virtualFile; + } + + String getPathNameRelativeTo(VirtualFile parent) { + try { + return invoke(getPathNameRelativeTo, virtualFile, parent.virtualFile); + } + catch (IOException e) { + // This exception is not thrown by the called method + log.error("This should not be possible. VirtualFile.getPathNameRelativeTo() threw IOException."); + return null; + } + } + + List<VirtualFile> getChildren() throws IOException { + List<?> objects = invoke(getChildrenRecursively, virtualFile); + List<VirtualFile> children = new ArrayList<VirtualFile>(objects.size()); + for (Object object : objects) { + children.add(new VirtualFile(object)); + } + return children; + } + } + + /** A class that mimics a tiny subset of the JBoss VFS class. */ + static class VFS { + static Class<?> VFS; + static Method getChild; + + static VirtualFile getChild(URL url) throws IOException { + Object o = invoke(getChild, VFS, url); + return o == null ? null : new VirtualFile(o); + } + } + + /** Flag that indicates if this VFS is valid for the current environment. */ + private static Boolean valid; + + /** Find all the classes and methods that are required to access the JBoss 6 VFS. */ + protected static synchronized void initialize() { + if (valid == null) { + // Assume valid. It will get flipped later if something goes wrong. + valid = true; + + // Look up and verify required classes + VFS.VFS = checkNotNull(getClass("org.jboss.vfs.VFS")); + VirtualFile.VirtualFile = checkNotNull(getClass("org.jboss.vfs.VirtualFile")); + + // Look up and verify required methods + VFS.getChild = checkNotNull(getMethod(VFS.VFS, "getChild", URL.class)); + VirtualFile.getChildrenRecursively = checkNotNull(getMethod(VirtualFile.VirtualFile, + "getChildrenRecursively")); + VirtualFile.getPathNameRelativeTo = checkNotNull(getMethod(VirtualFile.VirtualFile, + "getPathNameRelativeTo", VirtualFile.VirtualFile)); + + // Verify that the API has not changed + checkReturnType(VFS.getChild, VirtualFile.VirtualFile); + checkReturnType(VirtualFile.getChildrenRecursively, List.class); + checkReturnType(VirtualFile.getPathNameRelativeTo, String.class); + } + } + + /** + * Verifies that the provided object reference is null. If it is null, then this VFS is marked + * as invalid for the current environment. + * + * @param objects The object references to check for null. + */ + protected static <T> T checkNotNull(T object) { + if (object == null) + setInvalid(); + return object; + } + + /** + * Verifies that the return type of a method is what it is expected to be. If it is not, then + * this VFS is marked as invalid for the current environment. + * + * @param method The method whose return type is to be checked. + * @param expected A type to which the method's return type must be assignable. + * @see Class#isAssignableFrom(Class) + */ + protected static void checkReturnType(Method method, Class<?> expected) { + if (method != null && !expected.isAssignableFrom(method.getReturnType())) { + log.warn("Method ", method.getClass().getName(), ".", method.getName(), + "(..) should return ", expected.getName(), " but returns ", // + method.getReturnType().getName(), " instead."); + setInvalid(); + } + } + + /** Mark this {...@link VFS} as invalid for the current environment. */ + protected static void setInvalid() { + if (JBoss6VFS.valid != null && JBoss6VFS.valid) { + log.debug("JBoss 6 VFS API is not available in this environment."); + JBoss6VFS.valid = false; + } + } + + static { + initialize(); + } + + @Override + public boolean isValid() { + return valid; + } + + @Override + public List<String> list(URL url, String path) throws IOException { + VirtualFile directory; + directory = VFS.getChild(url); + if (directory == null) + return Collections.emptyList(); + + if (!path.endsWith("/")) + path += "/"; + + List<VirtualFile> children = directory.getChildren(); + List<String> names = new ArrayList<String>(children.size()); + for (VirtualFile vf : children) { + String relative = vf.getPathNameRelativeTo(directory); + names.add(path + relative); + } + + return names; + } +} Added: trunk/stripes/src/net/sourceforge/stripes/vfs/VFS.java =================================================================== --- trunk/stripes/src/net/sourceforge/stripes/vfs/VFS.java (rev 0) +++ trunk/stripes/src/net/sourceforge/stripes/vfs/VFS.java 2010-11-12 16:25:10 UTC (rev 1336) @@ -0,0 +1,206 @@ +/* Copyright 2010 Ben Gunter + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.sourceforge.stripes.vfs; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import net.sourceforge.stripes.exception.StripesRuntimeException; +import net.sourceforge.stripes.util.Log; +import net.sourceforge.stripes.util.ReflectUtil; + +/** + * Provides a very simple API for accessing resources within an application server. + * + * @author Ben Gunter + */ +public abstract class VFS { + private static final Log log = Log.getInstance(VFS.class); + + /** The built-in implementations. */ + public static final Class<?>[] IMPLEMENTATIONS = { JBoss6VFS.class, DefaultVFS.class }; + + /** The list to which implementations are added by {...@link #addImplClass(String)}. */ + public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<Class<? extends VFS>>(); + + /** Singleton instance. */ + private static VFS instance; + + /** + * Get the singleton {...@link VFS} instance. If no {...@link VFS} implementation can be found for the + * current environment, then this method returns null. + * + * @return + */ + @SuppressWarnings("unchecked") + public static VFS getInstance() { + if (instance != null) + return instance; + + // Try the user implementations first, then the built-ins + List<Class<? extends VFS>> impls = new ArrayList<Class<? extends VFS>>(); + impls.addAll(USER_IMPLEMENTATIONS); + impls.addAll(Arrays.asList((Class<? extends VFS>[]) IMPLEMENTATIONS)); + + // Try each implementation class until a valid one is found + VFS vfs = null; + for (int i = 0; vfs == null || !vfs.isValid(); i++) { + Class<? extends VFS> impl = impls.get(i); + try { + vfs = impl.newInstance(); + if (vfs == null || !vfs.isValid()) { + log.debug("VFS implementation ", impl.getName(), + " is not valid in this environment."); + } + } + catch (InstantiationException e) { + log.error(e, "Failed to instantiate ", impl); + return null; + } + catch (IllegalAccessException e) { + log.error(e, "Failed to instantiate ", impl); + return null; + } + } + + log.info("Using VFS adapter ", vfs.getClass().getName()); + return VFS.instance = vfs; + } + + /** + * Adds the specified class to the list of {...@link VFS} implementations. Classes added in this + * manner are tried in the order they are added and before any of the built-in implementations. + * + * @param className The name of the {...@link VFS} implementation class to add. + */ + public static void addImplClass(Class<? extends VFS> clazz) { + if (clazz != null) + USER_IMPLEMENTATIONS.add(clazz); + } + + /** Get a class by name. If the class is not found then return null. */ + protected static Class<?> getClass(String className) { + try { + return ReflectUtil.findClass(className); + } + catch (ClassNotFoundException e) { + log.debug("Class not found: ", className); + return null; + } + } + + /** + * Get a method by name and parameter types. If the method is not found then return null. + * + * @param clazz The class to which the method belongs. + * @param methodName The name of the method. + * @param parameterTypes The types of the parameters accepted by the method. + */ + protected static Method getMethod(Class<?> clazz, String methodName, Class<?>... parameterTypes) { + try { + if (clazz == null) + return null; + else + return clazz.getMethod(methodName, parameterTypes); + } + catch (SecurityException e) { + log.debug(e, "Security exception looking for method ", clazz.getName(), ".", methodName); + return null; + } + catch (NoSuchMethodException e) { + log.debug(e, "Method not found ", clazz.getName(), ".", methodName); + return null; + } + } + + /** + * Invoke a method on an object and return whatever it returns. + * + * @param method The method to invoke. + * @param object The instance or class (for static methods) on which to invoke the method. + * @param parameters The parameters to pass to the method. + * @return Whatever the method returns. + * @throws IOException If I/O errors occur + * @throws StripesRuntimeException If anything else goes wrong + */ + @SuppressWarnings("unchecked") + protected static <T> T invoke(Method method, Object object, Object... parameters) + throws IOException, StripesRuntimeException { + try { + return (T) method.invoke(object, parameters); + } + catch (IllegalArgumentException e) { + throw new StripesRuntimeException(e); + } + catch (IllegalAccessException e) { + throw new StripesRuntimeException(e); + } + catch (InvocationTargetException e) { + if (e.getTargetException() instanceof IOException) + throw (IOException) e.getTargetException(); + else + throw new StripesRuntimeException(e); + } + } + + /** + * Get a list of {...@link URL}s from the context classloader for all the resources found at the + * specified path. + * + * @param path The resource path. + * @return A list of {...@link URL}s, as returned by {...@link ClassLoader#getResources(String)}. + * @throws IOException If I/O errors occur + */ + protected static List<URL> getResources(String path) throws IOException { + return Collections.list(Thread.currentThread().getContextClassLoader().getResources(path)); + } + + /** Return true if the {...@link VFS} implementation is valid for the current environment. */ + public abstract boolean isValid(); + + /** + * Recursively list the full resource path of all the resources that are children of the + * resource identified by a URL. + * + * @param url The URL that identifies the resource to list. + * @param forPath The path to the resource that is identified by the URL. Generally, this is the + * value passed to {...@link #getResources(String)} to get the resource URL. + * @return A list containing the names of the child resources. + * @throws IOException If I/O errors occur + */ + protected abstract List<String> list(URL url, String forPath) throws IOException; + + /** + * Recursively list the full resource path of all the resources that are children of all the + * resources found at the specified path. + * + * @param path The path of the resource(s) to list. + * @return A list containing the names of the child resources. + * @throws IOException If I/O errors occur + */ + public List<String> list(String path) throws IOException { + List<String> names = new ArrayList<String>(); + for (URL url : getResources(path)) { + names.addAll(list(url, path)); + } + return names; + } +} Added: trunk/stripes/src/net/sourceforge/stripes/vfs/package.html =================================================================== --- trunk/stripes/src/net/sourceforge/stripes/vfs/package.html (rev 0) +++ trunk/stripes/src/net/sourceforge/stripes/vfs/package.html 2010-11-12 16:25:10 UTC (rev 1336) @@ -0,0 +1,3 @@ +<body> +<p>Contains classes that facilitate discovery of resources via the class loader.</p> +</body> \ No newline at end of file This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. ------------------------------------------------------------------------------ Centralized Desktop Delivery: Dell and VMware Reference Architecture Simplifying enterprise desktop deployment and management using Dell EqualLogic storage and VMware View: A highly scalable, end-to-end client virtualization framework. Read more! http://p.sf.net/sfu/dell-eql-dev2dev _______________________________________________ Stripes-development mailing list Stripes-development@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/stripes-development