You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by hl...@apache.org on 2012/10/05 23:42:31 UTC

git commit: Split the guts of ClassNameLocator into a new ClasspathScanner service

Updated Branches:
  refs/heads/5.4-js-rewrite 0eaeb8bb5 -> c8fd64807


Split the guts of ClassNameLocator into a new ClasspathScanner service


Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/c8fd6480
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/c8fd6480
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/c8fd6480

Branch: refs/heads/5.4-js-rewrite
Commit: c8fd648077b8be794ac78f03c666c4b8576c2869
Parents: 0eaeb8b
Author: Howard M. Lewis Ship <hl...@apache.org>
Authored: Fri Oct 5 14:39:54 2012 -0700
Committer: Howard M. Lewis Ship <hl...@apache.org>
Committed: Fri Oct 5 14:39:54 2012 -0700

----------------------------------------------------------------------
 .../hibernate/HibernateSessionSourceImplTest.java  |   11 +-
 .../internal/services/ClassNameLocatorImpl.java    |  318 ++-------------
 .../internal/services/ClasspathScannerImpl.java    |  314 ++++++++++++++
 .../tapestry5/ioc/services/ClassNameLocator.java   |    3 +-
 .../tapestry5/ioc/services/ClasspathMatcher.java   |   38 ++
 .../tapestry5/ioc/services/ClasspathScanner.java   |   38 ++
 .../tapestry5/ioc/services/TapestryIOCModule.java  |    1 +
 .../ioc/specs/ClassNameLocatorImplSpec.groovy      |   82 ++--
 8 files changed, 481 insertions(+), 324 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c8fd6480/tapestry-hibernate-core/src/test/java/org/apache/tapestry5/internal/hibernate/HibernateSessionSourceImplTest.java
----------------------------------------------------------------------
diff --git a/tapestry-hibernate-core/src/test/java/org/apache/tapestry5/internal/hibernate/HibernateSessionSourceImplTest.java b/tapestry-hibernate-core/src/test/java/org/apache/tapestry5/internal/hibernate/HibernateSessionSourceImplTest.java
index 515c076..0ea7d6a 100644
--- a/tapestry-hibernate-core/src/test/java/org/apache/tapestry5/internal/hibernate/HibernateSessionSourceImplTest.java
+++ b/tapestry-hibernate-core/src/test/java/org/apache/tapestry5/internal/hibernate/HibernateSessionSourceImplTest.java
@@ -1,4 +1,4 @@
-// Copyright 2007, 2008 The Apache Software Foundation
+// Copyright 2007, 2008, 2012 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ import org.apache.tapestry5.hibernate.HibernateConfigurer;
 import org.apache.tapestry5.hibernate.HibernateEntityPackageManager;
 import org.apache.tapestry5.hibernate.HibernateSessionSource;
 import org.apache.tapestry5.ioc.internal.services.ClassNameLocatorImpl;
+import org.apache.tapestry5.ioc.internal.services.ClasspathScannerImpl;
 import org.apache.tapestry5.ioc.internal.services.ClasspathURLConverterImpl;
 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
 import org.apache.tapestry5.ioc.test.IOCTestCase;
@@ -47,11 +48,15 @@ public class HibernateSessionSourceImplTest extends IOCTestCase
         HibernateEntityPackageManager packageManager = newMock(HibernateEntityPackageManager.class);
         TestBase.expect(packageManager.getPackageNames()).andReturn(packageNames);
 
+        ClasspathScannerImpl scanner = new ClasspathScannerImpl(new ClasspathURLConverterImpl());
+        ClassNameLocatorImpl classNameLocator = new ClassNameLocatorImpl(scanner);
+
         List<HibernateConfigurer> filters = Arrays.asList(new DefaultHibernateConfigurer(true),
-                new PackageNameHibernateConfigurer(packageManager, new ClassNameLocatorImpl(
-                        new ClasspathURLConverterImpl())));
+                new PackageNameHibernateConfigurer(packageManager, classNameLocator));
+
 
         replay();
+
         HibernateSessionSource source = new HibernateSessionSourceImpl(log, filters);
 
         Session session = source.create();

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c8fd6480/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/services/ClassNameLocatorImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/services/ClassNameLocatorImpl.java b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/services/ClassNameLocatorImpl.java
index 3738f83..18fb90d 100644
--- a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/services/ClassNameLocatorImpl.java
+++ b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/services/ClassNameLocatorImpl.java
@@ -1,4 +1,4 @@
-// Copyright 2007, 2008, 2010 The Apache Software Foundation
+// Copyright 2007, 2008, 2010, 2012 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,328 +14,84 @@
 
 package org.apache.tapestry5.ioc.internal.services;
 
-import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
-import org.apache.tapestry5.ioc.internal.util.InternalUtils;
+import org.apache.tapestry5.func.F;
+import org.apache.tapestry5.func.Mapper;
 import org.apache.tapestry5.ioc.services.ClassNameLocator;
-import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
-import org.apache.tapestry5.ioc.util.Stack;
+import org.apache.tapestry5.ioc.services.ClasspathMatcher;
+import org.apache.tapestry5.ioc.services.ClasspathScanner;
 
-import java.io.*;
-import java.net.JarURLConnection;
-import java.net.URL;
-import java.net.URLConnection;
+import java.io.IOException;
 import java.util.Collection;
-import java.util.Enumeration;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
 import java.util.regex.Pattern;
 
 public class ClassNameLocatorImpl implements ClassNameLocator
 {
-    private static final String CLASS_SUFFIX = ".class";
-    public static final String PACKAGE_INFO = "package-info.class";
-
-    private final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
-
-    private final ClasspathURLConverter converter;
+    private final ClasspathScanner scanner;
 
     // This matches normal class files but not inner class files (which contain a '$'.
 
     private final Pattern CLASS_NAME_PATTERN = Pattern.compile("^\\p{javaJavaIdentifierStart}[\\p{javaJavaIdentifierPart}&&[^\\$]]*\\.class$", Pattern.CASE_INSENSITIVE);
 
-    private final Pattern FOLDER_NAME_PATTERN = Pattern.compile("^\\p{javaJavaIdentifierStart}[\\p{javaJavaIdentifierPart}]*$", Pattern.CASE_INSENSITIVE);
-
-    static class Queued
-    {
-        final URL packageURL;
-
-        final String packagePath;
-
-        public Queued(final URL packageURL, final String packagePath)
-        {
-            this.packageURL = packageURL;
-            this.packagePath = packagePath;
-        }
-    }
-
-    public ClassNameLocatorImpl(ClasspathURLConverter converter)
-    {
-        this.converter = converter;
-    }
-
     /**
-     * Synchronization should not be necessary, but perhaps the underlying ClassLoader's are sensitive to threading.
+     * Matches paths that are classes, but not for inner classes, or the package-info.class psuedo-class (used for package-level annotations).
      */
-    public synchronized Collection<String> locateClassNames(String packageName)
-    {
-        String packagePath = packageName.replace('.', '/') + "/";
-
-        try
-        {
-
-            return findClassesWithinPath(packagePath);
-
-        } catch (IOException ex)
-        {
-            throw new RuntimeException(ex);
-        }
-    }
-
-    private Collection<String> findClassesWithinPath(String packagePath) throws IOException
-    {
-        Collection<String> result = CollectionFactory.newList();
-
-        Enumeration<URL> urls = contextClassLoader.getResources(packagePath);
-
-        while (urls.hasMoreElements())
-        {
-            URL url = urls.nextElement();
-
-            URL converted = converter.convert(url);
-
-            scanURL(packagePath, result, converted);
-        }
-
-        return result;
-    }
-
-    private void scanURL(String packagePath, Collection<String> componentClassNames, URL url) throws IOException
+    private final ClasspathMatcher CLASS_NAME_MATCHER = new ClasspathMatcher()
     {
-        URLConnection connection = url.openConnection();
-
-        JarFile jarFile;
-
-        if (connection instanceof JarURLConnection)
-        {
-            jarFile = ((JarURLConnection) connection).getJarFile();
-        } else
-        {
-            jarFile = getAlternativeJarFile(url);
-        }
-
-        if (jarFile != null)
-        {
-            scanJarFile(packagePath, componentClassNames, jarFile);
-        } else if (supportsDirStream(url))
+        @Override
+        public boolean matches(String packagePath, String fileName)
         {
-            Stack<Queued> queue = CollectionFactory.newStack();
-
-            queue.push(new Queued(url, packagePath));
-
-            while (!queue.isEmpty())
+            if (!CLASS_NAME_PATTERN.matcher(fileName).matches())
             {
-                Queued queued = queue.pop();
-
-                scanDirStream(queued.packagePath, queued.packageURL, componentClassNames, queue);
-            }
-        } else
-        {
-            // Try scanning file system.
-            String packageName = packagePath.replace("/", ".");
-            if (packageName.endsWith("."))
-            {
-                packageName = packageName.substring(0, packageName.length() - 1);
+                return false;
             }
-            scanDir(packageName, new File(url.getFile()), componentClassNames);
-        }
-
-    }
-
-    /**
-     * Check whether container supports opening a stream on a dir/package to get a list of its contents.
-     *
-     * @param packageURL
-     * @return
-     */
-    private boolean supportsDirStream(URL packageURL)
-    {
-        InputStream is = null;
-        try
-        {
-            is = packageURL.openStream();
-            return true;
-        } catch (FileNotFoundException ex)
-        {
-            return false;
-        } catch (IOException e)
-        {
-            return false;
-        } finally
-        {
-            InternalUtils.close(is);
-        }
-    }
-
-    private void scanDirStream(String packagePath, URL packageURL, Collection<String> componentClassNames,
-                               Stack<Queued> queue) throws IOException
-    {
-        InputStream is;
-
-        try
-        {
-            is = new BufferedInputStream(packageURL.openStream());
-        } catch (FileNotFoundException ex)
-        {
-            // This can happen for certain application servers (JBoss 4.0.5 for example), that
-            // export part of the exploded WAR for deployment, but leave part (WEB-INF/classes)
-            // unexploded.
-
-            return;
-        }
 
-        Reader reader = new InputStreamReader(is);
-        LineNumberReader lineReader = new LineNumberReader(reader);
+            // Filter out inner classes.
 
-        String packageName = null;
-
-        try
-        {
-            while (true)
+            if (fileName.contains("$") || fileName.equals("package-info.class"))
             {
-                String line = lineReader.readLine();
-
-                if (line == null) break;
-
-                if (CLASS_NAME_PATTERN.matcher(line).matches())
-                {
-                    if (packageName == null)
-                    {
-                        packageName = packagePath.replace('/', '.');
-                    }
-
-                    // packagePath ends with '/', packageName ends with '.'
-
-                    String fileName = line.substring(0, line.length() - CLASS_SUFFIX.length());
-
-                    if (!fileName.equals("package-info"))
-                    {
-                        String fullClassName = packageName + fileName;
-
-                        componentClassNames.add(fullClassName);
-                    }
-
-                    continue;
-                }
-
-                // This should match just directories.  It may also match files that have no extension;
-                // when we read those, none of the lines should look like class files.
-
-                if (FOLDER_NAME_PATTERN.matcher(line).matches())
-                {
-                    URL newURL = new URL(packageURL.toExternalForm() + line + "/");
-                    String newPackagePath = packagePath + line + "/";
-
-                    queue.push(new Queued(newURL, newPackagePath));
-                }
+                return false;
             }
 
-            lineReader.close();
-            lineReader = null;
-        } finally
-        {
-            InternalUtils.close(lineReader);
-        }
-
-    }
-
-    private void scanJarFile(String packagePath, Collection<String> componentClassNames, JarFile jarFile)
-    {
-        Enumeration<JarEntry> e = jarFile.entries();
-
-        while (e.hasMoreElements())
-        {
-            String name = e.nextElement().getName();
-
-            if (!name.startsWith(packagePath)) continue;
-
-
-            int lastSlashx = name.lastIndexOf('/');
-
-            String fileName = name.substring(lastSlashx + 1);
-
-            if (isClassName(fileName))
-            {
-
-                // Strip off .class and convert the slashes back to periods.
-                String className =
-                        name.substring(0, lastSlashx + 1).replace('/', '.') +
-                                fileName.substring(0, fileName.length() - CLASS_SUFFIX.length());
-
-
-                componentClassNames.add(className);
-            }
+            return true;
         }
-    }
+    };
 
     /**
-     * Scan a dir for classes. Will recursively look in the supplied directory and all sub directories.
-     *
-     * @param packageName         Name of package that this directory corresponds to.
-     * @param dir                 Dir to scan for classes.
-     * @param componentClassNames List of class names that have been found.
+     * Maps a path name ("foo/bar/Baz.class") to a class name ("foo.bar.Baz").
      */
-    private void scanDir(String packageName, File dir, Collection<String> componentClassNames)
+    private final Mapper<String, String> CLASS_NAME_MAPPER = new Mapper<String, String>()
     {
-        if (dir.exists() && dir.isDirectory())
+        @Override
+        public String map(String element)
         {
-            for (File file : dir.listFiles())
-            {
-                String fileName = file.getName();
-                if (file.isDirectory())
-                {
-                    scanDir(packageName + "." + fileName, file, componentClassNames);
-                }
-                // https://issues.apache.org/jira/browse/TAP5-1737
-                // Use of package-info.java leaves these package-info.class files around.
-                else if (isClassName(fileName))
-                {
-                    String className = packageName + "." + fileName.substring(0,
-                            fileName.length() - CLASS_SUFFIX.length());
-                    componentClassNames.add(className);
-                }
-            }
+            return element.substring(0, element.length() - 6).replace('/', '.');
         }
-    }
+    };
+
 
-    private boolean isClassName(String fileName)
+    public ClassNameLocatorImpl(ClasspathScanner scanner)
     {
-        return fileName.endsWith(CLASS_SUFFIX) && !fileName.equals(PACKAGE_INFO) && !fileName.contains("$");
+        this.scanner = scanner;
     }
 
     /**
-     * For URLs to JARs that do not use JarURLConnection - allowed by the servlet spec - attempt to produce a JarFile
-     * object all the same. Known servlet engines that function like this include Weblogic and OC4J. This is not a full
-     * solution, since an unpacked WAR or EAR will not have JAR "files" as such.
-     *
-     * @param url URL of jar
-     * @return JarFile or null
-     * @throws java.io.IOException If error occurs creating jar file
+     * Synchronization should not be necessary, but perhaps the underlying ClassLoader's are sensitive to threading.
      */
-    private JarFile getAlternativeJarFile(URL url) throws IOException
+    public synchronized Collection<String> locateClassNames(String packageName)
     {
-        String urlFile = url.getFile();
-        // Trim off any suffix - which is prefixed by "!/" on Weblogic
-        int separatorIndex = urlFile.indexOf("!/");
+        String packagePath = packageName.replace('.', '/') + "/";
 
-        // OK, didn't find that. Try the less safe "!", used on OC4J
-        if (separatorIndex == -1)
+        try
         {
-            separatorIndex = urlFile.indexOf('!');
-        }
+            Collection<String> matches = scanner.scan(packagePath, CLASS_NAME_MATCHER);
 
-        if (separatorIndex != -1)
-        {
-            String jarFileUrl = urlFile.substring(0, separatorIndex);
-            // And trim off any "file:" prefix.
-            if (jarFileUrl.startsWith("file:"))
-            {
-                jarFileUrl = jarFileUrl.substring("file:".length());
-            }
+            return F.flow(matches).map(CLASS_NAME_MAPPER).toSet();
 
-            return new JarFile(jarFileUrl);
+        } catch (IOException ex)
+        {
+            throw new RuntimeException(ex);
         }
-
-        return null;
     }
 
+
 }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c8fd6480/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/services/ClasspathScannerImpl.java
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/services/ClasspathScannerImpl.java b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/services/ClasspathScannerImpl.java
new file mode 100644
index 0000000..c006c0f
--- /dev/null
+++ b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/internal/services/ClasspathScannerImpl.java
@@ -0,0 +1,314 @@
+// Copyright 2012 The Apache Software Foundation
+//
+// 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 org.apache.tapestry5.ioc.internal.services;
+
+import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry5.ioc.internal.util.InternalUtils;
+import org.apache.tapestry5.ioc.services.ClasspathMatcher;
+import org.apache.tapestry5.ioc.services.ClasspathScanner;
+import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
+import org.apache.tapestry5.ioc.util.Stack;
+
+import java.io.*;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.regex.Pattern;
+
+public class ClasspathScannerImpl implements ClasspathScanner
+{
+    private final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+
+    private final ClasspathURLConverter converter;
+
+    private final Pattern FOLDER_NAME_PATTERN = Pattern.compile("^\\p{javaJavaIdentifierStart}[\\p{javaJavaIdentifierPart}]*$", Pattern.CASE_INSENSITIVE);
+
+    public ClasspathScannerImpl(ClasspathURLConverter converter)
+    {
+        this.converter = converter;
+    }
+
+    /**
+     * Scans the indicated package path for matches.
+     *
+     * @param packagePath
+     *         a package path (like a package name, but using '/' instead of '.', and ending with '/')
+     * @param matcher
+     *         passed a resource path from the package (or a sub-package), returns true if the provided
+     *         path should be included in the returned collection
+     * @return collection of matching paths, in no specified order
+     * @throws java.io.IOException
+     */
+    public Set<String> scan(String packagePath, ClasspathMatcher matcher) throws IOException
+    {
+        assert packagePath != null && packagePath.endsWith("/");
+        assert matcher != null;
+
+        return new Job(matcher).findMatches(packagePath);
+    }
+
+    /**
+     * Check whether container supports opening a stream on a dir/package to get a list of its contents.
+     */
+    private boolean supportsDirStream(URL packageURL)
+    {
+        InputStream is = null;
+
+        try
+        {
+            is = packageURL.openStream();
+
+            return true;
+        } catch (FileNotFoundException ex)
+        {
+            return false;
+        } catch (IOException ex)
+        {
+            return false;
+        } finally
+        {
+            InternalUtils.close(is);
+        }
+    }
+
+    /**
+     * For URLs to JARs that do not use JarURLConnection - allowed by the servlet spec - attempt to produce a JarFile
+     * object all the same. Known servlet engines that function like this include Weblogic and OC4J. This is not a full
+     * solution, since an unpacked WAR or EAR will not have JAR "files" as such.
+     *
+     * @param url
+     *         URL of jar
+     * @return JarFile or null
+     * @throws java.io.IOException
+     *         If error occurs creating jar file
+     */
+    private JarFile getAlternativeJarFile(URL url) throws IOException
+    {
+        String urlFile = url.getFile();
+        // Trim off any suffix - which is prefixed by "!/" on Weblogic
+        int separatorIndex = urlFile.indexOf("!/");
+
+        // OK, didn't find that. Try the less safe "!", used on OC4J
+        if (separatorIndex == -1)
+        {
+            separatorIndex = urlFile.indexOf('!');
+        }
+
+        if (separatorIndex != -1)
+        {
+            String jarFileUrl = urlFile.substring(0, separatorIndex);
+            // And trim off any "file:" prefix.
+            if (jarFileUrl.startsWith("file:"))
+            {
+                jarFileUrl = jarFileUrl.substring("file:".length());
+            }
+
+            return new JarFile(jarFileUrl);
+        }
+
+        return null;
+    }
+
+    static class Queued
+    {
+        final URL packageURL;
+
+        final String packagePath;
+
+        public Queued(final URL packageURL, final String packagePath)
+        {
+            this.packageURL = packageURL;
+            this.packagePath = packagePath;
+        }
+    }
+
+    class Job
+    {
+        final ClasspathMatcher matcher;
+
+        final Set<String> matches = CollectionFactory.newSet();
+
+        final Stack<Queued> queue = CollectionFactory.newStack();
+
+        Job(ClasspathMatcher matcher)
+        {
+            this.matcher = matcher;
+        }
+
+        Set<String> findMatches(String packagePath) throws IOException
+        {
+
+            Enumeration<URL> urls = contextClassLoader.getResources(packagePath);
+
+            while (urls.hasMoreElements())
+            {
+                URL url = urls.nextElement();
+
+                URL converted = converter.convert(url);
+
+                scanURL(packagePath, converted);
+
+                while (!queue.isEmpty())
+                {
+                    Queued queued = queue.pop();
+
+                    scanDirStream(queued.packagePath, queued.packageURL);
+                }
+            }
+
+            return matches;
+        }
+
+        void scanURL(String packagePath, URL url) throws IOException
+        {
+            URLConnection connection = url.openConnection();
+
+            JarFile jarFile;
+
+            if (connection instanceof JarURLConnection)
+            {
+                jarFile = ((JarURLConnection) connection).getJarFile();
+            } else
+            {
+                jarFile = getAlternativeJarFile(url);
+            }
+
+            if (jarFile != null)
+            {
+                scanJarFile(packagePath, jarFile);
+            } else if (supportsDirStream(url))
+            {
+                queue.push(new Queued(url, packagePath));
+            } else
+            {
+                // Try scanning file system.
+
+                scanDir(packagePath, new File(url.getFile()));
+            }
+
+        }
+
+        /**
+         * Scan a dir for classes. Will recursively look in the supplied directory and all sub directories.
+         *
+         * @param packagePath
+         *         Name of package that this directory corresponds to.
+         * @param packageDir
+         *         Dir to scan for classes.
+         */
+        private void scanDir(String packagePath, File packageDir)
+        {
+            if (packageDir.exists() && packageDir.isDirectory())
+            {
+                for (File file : packageDir.listFiles())
+                {
+                    String fileName = file.getName();
+
+                    if (file.isDirectory())
+                    {
+                        // TODO: A second queue instead of recursion.
+
+                        scanDir(packagePath + fileName + "/", file);
+                    }
+
+                    if (matcher.matches(packagePath, fileName))
+                    {
+                        matches.add(packagePath + fileName);
+                    }
+                }
+            }
+        }
+
+        private void scanDirStream(String packagePath, URL packageURL) throws IOException
+        {
+            InputStream is;
+
+            try
+            {
+                is = new BufferedInputStream(packageURL.openStream());
+            } catch (FileNotFoundException ex)
+            {
+                // This can happen for certain application servers (JBoss 4.0.5 for example), that
+                // export part of the exploded WAR for deployment, but leave part (WEB-INF/classes)
+                // unexploded.
+
+                return;
+            }
+
+            Reader reader = new InputStreamReader(is);
+            LineNumberReader lineReader = new LineNumberReader(reader);
+
+            try
+            {
+                while (true)
+                {
+                    String line = lineReader.readLine();
+
+                    if (line == null) break;
+
+                    if (matcher.matches(packagePath, line))
+                    {
+                        matches.add(packagePath + line);
+                    } else
+                    {
+
+                        // This should match just directories.  It may also match files that have no extension;
+                        // when we read those, none of the lines should look like class files.
+
+                        if (FOLDER_NAME_PATTERN.matcher(line).matches())
+                        {
+                            URL newURL = new URL(packageURL.toExternalForm() + line + "/");
+                            String newPackagePath = packagePath + line + "/";
+
+                            queue.push(new Queued(newURL, newPackagePath));
+                        }
+                    }
+                }
+                lineReader.close();
+                lineReader = null;
+            } finally
+            {
+                InternalUtils.close(lineReader);
+            }
+
+        }
+
+        private void scanJarFile(String packagePath, JarFile jarFile)
+        {
+            Enumeration<JarEntry> e = jarFile.entries();
+
+            while (e.hasMoreElements())
+            {
+                String name = e.nextElement().getName();
+
+                if (!name.startsWith(packagePath)) continue;
+
+                int lastSlashx = name.lastIndexOf('/');
+
+                String filePackagePath = name.substring(0, lastSlashx + 1);
+                String fileName = name.substring(lastSlashx + 1);
+
+                if (matcher.matches(filePackagePath, fileName))
+                {
+                    matches.add(name);
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c8fd6480/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClassNameLocator.java
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClassNameLocator.java b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClassNameLocator.java
index 3c9ded8..8b22404 100644
--- a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClassNameLocator.java
+++ b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClassNameLocator.java
@@ -1,4 +1,4 @@
-// Copyright 2007, 2008 The Apache Software Foundation
+// Copyright 2007, 2008, 2012 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import java.util.Collection;
  * Scans the classpath for top-level classes within particular packages.
  *
  * @see org.apache.tapestry5.ioc.services.ClasspathURLConverter
+ * @see ClasspathScanner
  */
 public interface ClassNameLocator
 {

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c8fd6480/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClasspathMatcher.java
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClasspathMatcher.java b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClasspathMatcher.java
new file mode 100644
index 0000000..27a23c5
--- /dev/null
+++ b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClasspathMatcher.java
@@ -0,0 +1,38 @@
+// Copyright 2012 The Apache Software Foundation
+//
+// 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 org.apache.tapestry5.ioc.services;
+
+/**
+ * Used to determine which files will be included in the set of matches paths within a particular
+ * package.
+ *
+ * @see ClasspathScanner
+ * @since 5.4
+ */
+public interface ClasspathMatcher
+{
+    /**
+     * Invoked for each located file, to determine if it belongs. May be passed file names
+     * that are actually nested folders. Typically, an implementation determined what matches
+     * based on a file extension of naming pattern.
+     *
+     * @param packagePath
+     *         package path containing the file, ending with '/'
+     * @param fileName
+     *         name of file within the package
+     * @return true to include, false to exclude
+     */
+    boolean matches(String packagePath, String fileName);
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c8fd6480/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClasspathScanner.java
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClasspathScanner.java b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClasspathScanner.java
new file mode 100644
index 0000000..e308374
--- /dev/null
+++ b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/ClasspathScanner.java
@@ -0,0 +1,38 @@
+// Copyright 2012 The Apache Software Foundation
+//
+// 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 org.apache.tapestry5.ioc.services;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * Used to scan a portion of the classpath for files that match a particular pattern, defined by a {@link ClasspathMatcher}.
+ *
+ * @since 5.4
+ */
+public interface ClasspathScanner
+{
+    /**
+     * Perform a scan of the indicated package path and any nested packages.
+     *
+     * @param packagePath
+     *         defines the root of the search as a path, e.g., "org/apache/tapestry5/" not "org.apache.tapestry5"
+     * @param matcher
+     *         passed each potential match to determine which are included in the final result
+     * @return matching paths based on the search and the matcher
+     * @throws IOException
+     */
+    Set<String> scan(String packagePath, ClasspathMatcher matcher) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c8fd6480/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/TapestryIOCModule.java
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/TapestryIOCModule.java b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/TapestryIOCModule.java
index d0e0c52..3bd7a6d 100644
--- a/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/TapestryIOCModule.java
+++ b/tapestry-ioc/src/main/java/org/apache/tapestry5/ioc/services/TapestryIOCModule.java
@@ -65,6 +65,7 @@ public final class TapestryIOCModule
         binder.bind(Runnable.class, RegistryStartup.class).withSimpleId();
         binder.bind(MasterObjectProvider.class, MasterObjectProviderImpl.class).preventReloading();
         binder.bind(ClassNameLocator.class, ClassNameLocatorImpl.class);
+        binder.bind(ClasspathScanner.class, ClasspathScannerImpl.class);
         binder.bind(AspectDecorator.class, AspectDecoratorImpl.class);
         binder.bind(ClasspathURLConverter.class, ClasspathURLConverterImpl.class);
         binder.bind(ServiceOverride.class, ServiceOverrideImpl.class);

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c8fd6480/tapestry-ioc/src/test/groovy/ioc/specs/ClassNameLocatorImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ClassNameLocatorImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ClassNameLocatorImplSpec.groovy
index 106cb29..f90ff2c 100644
--- a/tapestry-ioc/src/test/groovy/ioc/specs/ClassNameLocatorImplSpec.groovy
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ClassNameLocatorImplSpec.groovy
@@ -1,76 +1,80 @@
 package ioc.specs
 
 import org.apache.tapestry5.ioc.internal.services.ClassNameLocatorImpl
+import org.apache.tapestry5.ioc.internal.services.ClasspathScannerImpl
 import org.apache.tapestry5.ioc.internal.services.ClasspathURLConverterImpl
 import org.apache.tapestry5.ioc.services.ClassNameLocator
+import org.apache.tapestry5.ioc.services.ClasspathScanner
 import spock.lang.Specification
 
 class ClassNameLocatorImplSpec extends Specification {
 
-  ClassNameLocator locator = new ClassNameLocatorImpl(new ClasspathURLConverterImpl());
+    ClasspathScanner scanner = new ClasspathScannerImpl(new ClasspathURLConverterImpl())
 
-  def assertInList(classNames, packageName, String... expectedNames) {
+    ClassNameLocator locator = new ClassNameLocatorImpl(scanner);
 
-    expectedNames.each { name ->
-      String qualifiedName = "${packageName}.${name}"
+    def assertInList(classNames, packageName, String... expectedNames) {
 
-      assert classNames.contains(qualifiedName), "[$qualifiedName] not present in ${classNames.join(', ')}."
+        expectedNames.each { name ->
+            String qualifiedName = "${packageName}.${name}"
+
+            assert classNames.contains(qualifiedName), "[$qualifiedName] not present in ${classNames.join(', ')}."
+        }
     }
-  }
 
-  def assertNotInList(classNames, packageName, String... expectedNames) {
+    def assertNotInList(classNames, packageName, String... expectedNames) {
 
-    expectedNames.each { name ->
-      String qualifiedName = "${packageName}.${name}"
+        expectedNames.each { name ->
+            String qualifiedName = "${packageName}.${name}"
 
-      assert !classNames.contains(qualifiedName), "[$qualifiedName] should not be present in ${classNames.join(', ')}."
+            assert !classNames.contains(qualifiedName), "[$qualifiedName] should not be present in ${classNames.join(', ')}."
+        }
     }
-  }
 
-  def "locate classes inside a JAR file on the classpath"() {
+    def "locate classes inside a JAR file on the classpath"() {
 
-    expect:
+        expect:
 
-    assertInList locator.locateClassNames("javax.inject"),
-        "javax.inject",
-        "Inject", "Named", "Singleton"
-  }
+        assertInList locator.locateClassNames("javax.inject"),
+            "javax.inject",
+            "Inject", "Named", "Singleton"
+    }
 
-  def "can locate classes inside a subpackage, inside a classpath JAR file"() {
+    def "can locate classes inside a subpackage, inside a classpath JAR file"() {
 
-    expect:
+        expect:
 
-    assertInList locator.locateClassNames("org.slf4j"),
-        "org.slf4j",
-        "spi.MDCAdapter"
-  }
+        assertInList locator.locateClassNames("org.slf4j"),
+            "org.slf4j",
+            "spi.MDCAdapter"
+    }
 
-  def "can locate classes in local folder, but exclude inner classes"() {
+    def "can locate classes in local folder, but exclude inner classes"() {
 
-    def packageName = "org.apache.tapestry5.ioc.services"
+        def packageName = "org.apache.tapestry5.ioc.services"
 
-    when:
+        when:
 
-    def names = locator.locateClassNames packageName
+        def names = locator.locateClassNames packageName
 
-    then:
+        then:
 
-    assertInList names, packageName, "SymbolSource", "TapestryIOCModule"
+        assertInList names, packageName, "SymbolSource", "TapestryIOCModule"
 
-    assertNotInList names, packageName, 'TapestryIOCMOdules$1'
-  }
+        assertNotInList names, packageName, 'TapestryIOCMOdules$1'
+    }
 
-  def "can locate classes in subpackage of local folders"() {
-    def packageName = "org.apache.tapestry5"
+    def "can locate classes in subpackage of local folders"() {
+        def packageName = "org.apache.tapestry5"
 
-    when:
+        when:
 
-    def names = locator.locateClassNames packageName
+        def names = locator.locateClassNames packageName
 
-    then:
+        then:
 
-    assertInList names, packageName, "ioc.Orderable", "ioc.services.ChainBuilder"
-    assertNotInList names, packageName, 'services.TapestryIOCModule$1'
-  }
+        assertInList names, packageName, "ioc.Orderable", "ioc.services.ChainBuilder"
+        assertNotInList names, packageName, 'services.TapestryIOCModule$1'
+    }
 
 }