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 2007/01/27 04:13:32 UTC

svn commit: r500470 - in /tapestry/tapestry5/tapestry-core/trunk/src: main/java/org/apache/tapestry/internal/services/ main/java/org/apache/tapestry/services/ test/java/org/apache/tapestry/internal/services/

Author: hlship
Date: Fri Jan 26 19:13:31 2007
New Revision: 500470

URL: http://svn.apache.org/viewvc?view=rev&rev=500470
Log:
Check in first stage of case insensitive URLs -- ComponentClassResolver now does a scan, using ComponentClassLocator, to locate all page, component and mixin classes.

Added:
    tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassLocator.java
    tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassLocatorImpl.java
    tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/internal/services/ComponentClassLocatorImplTest.java
Modified:
    tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassResolverImpl.java
    tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/services/TapestryModule.java
    tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/internal/services/ComponentClassResolverImplTest.java

Added: tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassLocator.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassLocator.java?view=auto&rev=500470
==============================================================================
--- tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassLocator.java (added)
+++ tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassLocator.java Fri Jan 26 19:13:31 2007
@@ -0,0 +1,34 @@
+// Copyright 2007 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.tapestry.internal.services;
+
+import java.util.Collection;
+
+/**
+ * Scans the classpath for component classes, classes within particular packages.
+ */
+public interface ComponentClassLocator
+{
+    /**
+     * Searches for all component classes under the given package name. This consists of all
+     * top-level classes in the indicated package (or a sub-package), but excludes inner classes.
+     * <p>
+     * TODO: Sniff files for the ComponentClass annotation? A pain because of inheritance and such.
+     * 
+     * @param packageName
+     * @return fully qualified class names.
+     */
+    Collection<String> locateComponentClassNames(String packageName);
+}

Added: tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassLocatorImpl.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassLocatorImpl.java?view=auto&rev=500470
==============================================================================
--- tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassLocatorImpl.java (added)
+++ tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassLocatorImpl.java Fri Jan 26 19:13:31 2007
@@ -0,0 +1,194 @@
+// Copyright 2007 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.tapestry.internal.services;
+
+import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newLinkedList;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.Reader;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.LinkedList;
+import java.util.jar.JarEntry;
+
+import org.apache.tapestry.internal.TapestryUtils;
+import org.apache.tapestry.ioc.internal.util.CollectionFactory;
+
+public class ComponentClassLocatorImpl implements ComponentClassLocator
+{
+    private static final String CLASS_SUFFIX = ".class";
+
+    private final ClassLoader _contextClassLoader = Thread.currentThread().getContextClassLoader();
+
+    static class Queued
+    {
+        final URL _packageURL;
+
+        final String _packagePath;
+
+        public Queued(final URL packageURL, final String packagePath)
+        {
+            _packageURL = packageURL;
+            _packagePath = packagePath;
+        }
+    }
+
+    public Collection<String> locateComponentClassNames(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())
+            scanURL(packagePath, result, urls.nextElement());
+
+        return result;
+    }
+
+    private void scanURL(String packagePath, Collection<String> componentClassNames, URL url)
+            throws IOException
+    {
+        URLConnection connection = url.openConnection();
+
+        if (connection instanceof JarURLConnection)
+        {
+            scanJarConnection(packagePath, componentClassNames, (JarURLConnection) connection);
+            return;
+        }
+
+        // Otherwise, we're forced to assume that it is a file: URL for files in the user's
+        // workspace.
+
+        LinkedList<Queued> queue = newLinkedList();
+
+        queue.addFirst(new Queued(url, packagePath));
+
+        while (!queue.isEmpty())
+        {
+            Queued queued = queue.removeFirst();
+
+            scan(queued._packagePath, queued._packageURL, componentClassNames, queue);
+        }
+
+    }
+
+    private void scan(String packagePath, URL packageURL, Collection<String> componentClassNames,
+            LinkedList<Queued> queue) throws IOException
+    {
+        InputStream is = new BufferedInputStream(packageURL.openStream());
+        Reader reader = new InputStreamReader(is);
+        LineNumberReader lineReader = new LineNumberReader(reader);
+
+        String packageName = null;
+
+        try
+        {
+            while (true)
+            {
+                String line = lineReader.readLine();
+
+                if (line == null)
+                    break;
+
+                if (line.contains("$"))
+                    continue;
+
+                if (line.endsWith(CLASS_SUFFIX))
+                {
+                    if (packageName == null)
+                        packageName = packagePath.replace('/', '.');
+
+                    // packagePath ends with '/', packageName ends with '.'
+
+                    String fullClassName = packageName
+                            + line.substring(0, line.length() - CLASS_SUFFIX.length());
+
+                    componentClassNames.add(fullClassName);
+
+                    continue;
+                }
+
+                // Either a file or a hidden directory (such as .svn)
+
+                if (line.contains("."))
+                    continue;
+
+                // The name of a subdirectory.
+
+                URL newURL = new URL(packageURL.toExternalForm() + line + "/");
+                String newPackagePath = packagePath + line + "/";
+
+                queue.addFirst(new Queued(newURL, newPackagePath));
+            }
+
+            lineReader.close();
+            lineReader = null;
+        }
+        finally
+        {
+            TapestryUtils.close(lineReader);
+        }
+
+    }
+
+    private void scanJarConnection(String packagePath, Collection<String> componentClassNames,
+            JarURLConnection connection) throws IOException
+    {
+        Enumeration<JarEntry> e = connection.getJarFile().entries();
+
+        while (e.hasMoreElements())
+        {
+            String name = e.nextElement().getName();
+
+            if (!name.startsWith(packagePath))
+                continue;
+
+            if (!name.endsWith(CLASS_SUFFIX))
+                continue;
+
+            if (name.contains("$"))
+                continue;
+
+            // Strip off .class and convert the slashes back to periods.
+
+            String className = name.substring(0, name.length() - CLASS_SUFFIX.length()).replace(
+                    "/",
+                    ".");
+
+            componentClassNames.add(className);
+        }
+    }
+}

Modified: tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassResolverImpl.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassResolverImpl.java?view=diff&rev=500470&r1=500469&r2=500470
==============================================================================
--- tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassResolverImpl.java (original)
+++ tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/internal/services/ComponentClassResolverImpl.java Fri Jan 26 19:13:31 2007
@@ -12,313 +12,233 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package org.apache.tapestry.internal.services;
-
+package org.apache.tapestry.internal.services;
+
 import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newList;
 import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newMap;
-import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newThreadSafeMap;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.tapestry.events.InvalidationListener;
-import org.apache.tapestry.services.ComponentClassResolver;
-import org.apache.tapestry.services.LibraryMapping;
-
-/**
- * 
- */
-public class ComponentClassResolverImpl implements ComponentClassResolver, InvalidationListener
-{
-    private final ComponentInstantiatorSource _componentInstantiatorSource;
-
-    private String _appRootPackage;
-
-    private final Map<String, List<String>> _mappings = newMap();
-
-    // Cache of logical page names to page class names
-
-    private final Map<String, String> _pageClassCache = newThreadSafeMap();
-
-    // Cache of component types to component class names
-
-    private final Map<String, String> _componentClassCache = newThreadSafeMap();
-
-    private final Map<String, String> _mixinClassCache = newThreadSafeMap();
-
-    // Cache of page class names back to logical page names
-
-    private final Map<String, String> _logicalPageNameCache = newThreadSafeMap();
-
-    public ComponentClassResolverImpl(ComponentInstantiatorSource componentInstantiatorSource,
-            Collection<LibraryMapping> mappings)
-    {
-        _componentInstantiatorSource = componentInstantiatorSource;
-
-        for (LibraryMapping mapping : mappings)
-        {
-            String prefix = mapping.getPathPrefix();
-
-            // TODO: Check that prefix is well formed (no leading or trailing slash)
-
-            String rootPackage = mapping.getRootPackage();
-
-            List<String> packages = _mappings.get(prefix);
-
-            if (packages == null)
-            {
-                packages = newList();
-                _mappings.put(prefix, packages);
-            }
-
-            packages.add(rootPackage);
-
-            // These packages, which will contain classes subject to class transformation,
-            // must be registered with the component instantiator (which is responsible
-            // for transformation).
-
-            addPackagesToInstantiatorSource(rootPackage);
-        }
-
-    }
-
-    private void addPackagesToInstantiatorSource(String rootPackage)
-    {
-        _componentInstantiatorSource.addPackage(rootPackage + ".pages");
-        _componentInstantiatorSource.addPackage(rootPackage + ".components");
-        _componentInstantiatorSource.addPackage(rootPackage + ".mixins");
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.tapestry.events.InvalidationListener;
+import org.apache.tapestry.services.ComponentClassResolver;
+import org.apache.tapestry.services.LibraryMapping;
+
+public class ComponentClassResolverImpl implements ComponentClassResolver, InvalidationListener
+{
+    private static final String MIXINS_SUBPACKAGE = "mixins";
+
+    private static final String COMPONENTS_SUBPACKAGE = "components";
+
+    private static final String PAGES_SUBPACKAGE = "pages";
+
+    private final ComponentInstantiatorSource _componentInstantiatorSource;
+
+    private final ComponentClassLocator _componentClassLocator;
+
+    private String _appRootPackage;
+
+    // Map from folder name to a list of root package names.
+    // The key does not begin or end with a slash.
+
+    private final Map<String, List<String>> _mappings = newMap();
+
+    // Flag indicating that the maps have been cleared following an invalidation
+    // and need to be rebuilt. The flag and the four maps below are not synchronized
+    // because they are only modified inside a synchronized block. That should be strong enough ...
+    // and changes made will become "visible" at the end of the synchronized block. Because of the
+    // structure of Tapestry, there should not be any reader threads while the write thread
+    // is operating.
+
+    private boolean _rebuild = true;
+
+    private final Map<String, String> _pageToClassName = newMap();
+
+    private final Map<String, String> _componentToClassName = newMap();
+
+    private final Map<String, String> _mixinToClassName = newMap();
+
+    private final Map<String, String> _pageClassNameToLogicalName = newMap();
+
+    public ComponentClassResolverImpl(ComponentInstantiatorSource componentInstantiatorSource,
+            ComponentClassLocator componentClassLocator, Collection<LibraryMapping> mappings)
+    {
+        _componentInstantiatorSource = componentInstantiatorSource;
+        _componentClassLocator = componentClassLocator;
+
+        for (LibraryMapping mapping : mappings)
+        {
+            String prefix = mapping.getPathPrefix();
+
+            // TODO: Check that prefix is well formed (no leading or trailing slash)
+
+            String rootPackage = mapping.getRootPackage();
+
+            List<String> packages = _mappings.get(prefix);
+
+            if (packages == null)
+            {
+                packages = newList();
+                _mappings.put(prefix, packages);
+            }
+
+            packages.add(rootPackage);
+
+            // These packages, which will contain classes subject to class transformation,
+            // must be registered with the component instantiator (which is responsible
+            // for transformation).
+
+            addPackagesToInstantiatorSource(rootPackage);
+        }
+
+    }
+
+    private void addPackagesToInstantiatorSource(String rootPackage)
+    {
+        _componentInstantiatorSource.addPackage(rootPackage + ".pages");
+        _componentInstantiatorSource.addPackage(rootPackage + ".components");
+        _componentInstantiatorSource.addPackage(rootPackage + ".mixins");
         _componentInstantiatorSource.addPackage(rootPackage + ".base");
-    }
-
-    /** When the class loader is invalidated, clear any cached page names or component types. */
-    public void objectWasInvalidated()
-    {
-        _pageClassCache.clear();
-        _componentClassCache.clear();
-        _mixinClassCache.clear();
-
-        _logicalPageNameCache.clear();
-
-    }
-
-    public String resolvePageNameToClassName(String pageName)
-    {
-        String result = resolve(pageName, "pages", _pageClassCache);
-
-        if (result == null)
-            throw new IllegalArgumentException(ServicesMessages.couldNotResolvePageName(pageName));
-
-        return result;
-    }
-
-    public String resolveComponentTypeToClassName(String componentType)
-    {
-        String result = resolve(componentType, "components", _componentClassCache);
-
-        if (result == null)
-            throw new IllegalArgumentException(ServicesMessages.couldNotResolveComponentType(componentType));
-
-        return result;
-    }
-
-    public String resolveMixinTypeToClassName(String mixinType)
-    {
-        String result = resolve(mixinType, "mixins", _mixinClassCache);
-
-        if (result == null)
-            throw new IllegalArgumentException(ServicesMessages.couldNotResolveMixinType(mixinType));
-
-        return result;
-    }
-
-    // Used a thread-safe (concurrent) map rather than making this method itself
-    // concurrent to keep from single-threading the resolve process. If resolve() was
-    // @Concurrent.Write, then it would only be possible to resolve a single class name
-    // at a time, and there's no need for that.
-
-    private String resolve(String name, String subpackage, Map<String, String> cache)
-    {
-        // ConcurrentHashMap can't store a null, so we sometimes re-resolve
-        // a name, even though it can't be resolved. We could avoid this,
-        // but since its a transient programmer error, that's pretty low
-        // priority.
-
-        String result = cache.get(name);
-
-        if (result == null)
-        {
-            result = resolve(name, subpackage);
-
-            // Again, ConcurrentHashMap can't store a null.
-
-            if (result != null)
-                cache.put(name, result);
-        }
-
-        return result;
-    }
-
-    private String resolve(String name, String subpackage)
-    {
-        int lastx = name.length();
-
-        while (true)
-        {
-            int posx = name.lastIndexOf('/', lastx - 1);
-
-            if (posx < 0)
-                break;
-
-            // Extract up to but not including the slash
-
-            String prefix = name.substring(0, posx);
-
-            List<String> packages = _mappings.get(prefix);
-
-            if (packages != null)
-            {
-                String extension = name.substring(posx + 1).replace('/', '.');
-
-                String result = search(packages, subpackage, extension);
-
-                if (result != null)
-                    return result;
-            }
-
-            lastx = posx;
-        }
-
-        // Not found by prefix, lets try the application root package
-
-        String extension = name.replace('/', '.');
-
-        String result = buildClassName(_appRootPackage, subpackage, extension);
-
-        if (_componentInstantiatorSource.exists(result))
-            return result;
-
-        // We always expect there to be a core package.
-
-        return search(_mappings.get("core"), subpackage, extension);
-    }
-
-    /**
-     * Searches a number of root packages for the given class.
-     * 
-     * @param packages
-     *            the list of packages
-     * @param subpackage
-     *            "page" or "component"
-     * @param extension
-     *            the portion of the page name / component type after the library mapping prefix
-     * @return the fully qualified class name, if it exists or null if it does not exist
-     */
-    private String search(List<String> packages, String subpackage, String extension)
-    {
-        for (String rootPackage : packages)
-        {
-            String result = buildClassName(rootPackage, subpackage, extension);
-
-            if (_componentInstantiatorSource.exists(result))
-                return result;
-        }
-
-        return null;
-    }
-
-    private String buildClassName(String rootPackage, String subpackage, String extension)
-    {
-        return rootPackage + "." + subpackage + "." + extension;
-    }
-
-    public String resolvePageClassNameToPageName(String pageClassName)
-    {
-        String result = _logicalPageNameCache.get(pageClassName);
-
-        if (result == null)
-        {
-            result = findLogicalPageNameForClassName(pageClassName);
-            _logicalPageNameCache.put(pageClassName, result);
-        }
-
-        return result;
-    }
-
-    private String findLogicalPageNameForClassName(String pageClassName)
-    {
-        String bestName = null;
-
-        for (Map.Entry<String, List<String>> entry : _mappings.entrySet())
-        {
-            String prefix = entry.getKey();
-
-            for (String packageName : entry.getValue())
-            {
-                String potential = convertToLogicalPageName(pageClassName, packageName, prefix);
-
-                if (potential == null)
-                    continue;
-
-                if (bestName == null)
-                {
-                    bestName = potential;
-                    continue;
-                }
-
-                // Generally, for there to be more than one match, something borderline illegal
-                // must be going on.
-
-                if (potential.length() < bestName.length())
-                    bestName = potential;
-            }
-        }
-
-        if (bestName == null)
-            bestName = convertToLogicalPageName(pageClassName, _appRootPackage, null);
-
-        if (bestName == null)
-            throw new IllegalArgumentException(ServicesMessages.pageNameUnresolved(pageClassName));
-
-        return bestName;
-    }
-
-    private String convertToLogicalPageName(String pageClassName, String packageName, String prefix)
-    {
-        if (!pageClassName.startsWith(packageName))
-            return null;
-
-        String afterPackage = pageClassName.substring(packageName.length() + 1);
-        String[] names = afterPackage.split("\\.");
-
-        // The package name is the *root* package; we ignore it, but build up the rest
-        // of the logical page name from any intermediate folders/packages, leading
-        // up to the actual class name. If packages are nested in an ambiguous way,
-        // we may find something that looks like a mapping, but isn't.
-
-        if (!names[0].equals("pages"))
-            return null;
-
-        StringBuilder builder = new StringBuilder();
-
-        if (prefix != null)
-            builder.append(prefix);
-
-        for (int i = 1; i < names.length; i++)
-        {
-            if (builder.length() > 0)
-                builder.append("/");
-
-            builder.append(names[i]);
-        }
-
-        return builder.toString();
-    }
-
-    public void setApplicationPackage(String packageName)
-    {
-        _appRootPackage = packageName;
-
-        addPackagesToInstantiatorSource(packageName);
-    }
+    }
+
+    /** When the class loader is invalidated, clear any cached page names or component types. */
+    public synchronized void objectWasInvalidated()
+    {
+        _rebuild = true;
+
+        _pageToClassName.clear();
+        _componentToClassName.clear();
+        _mixinToClassName.clear();
+        _pageClassNameToLogicalName.clear();
+
+    }
+
+    private synchronized void rebuild()
+    {
+        if (!_rebuild)
+            return;
+
+        rebuild("", _appRootPackage);
+
+        for (String prefix : _mappings.keySet())
+        {
+            List<String> packages = _mappings.get(prefix);
+
+            String folder = prefix + "/";
+
+            for (String packageName : packages)
+                rebuild(folder, packageName);
+        }
+
+        _rebuild = false;
+    }
+
+    private void rebuild(String pathPrefix, String rootPackage)
+    {
+        fillCaselessMap(pathPrefix, rootPackage, PAGES_SUBPACKAGE, _pageToClassName);
+        fillCaselessMap(pathPrefix, rootPackage, COMPONENTS_SUBPACKAGE, _componentToClassName);
+        fillCaselessMap(pathPrefix, rootPackage, MIXINS_SUBPACKAGE, _mixinToClassName);
+    }
+
+    private void fillCaselessMap(String pathPrefix, String rootPackage, String subPackage,
+            Map<String, String> logicalNameToClassName)
+    {
+        String searchPackage = rootPackage + "." + subPackage;
+        boolean isPage = subPackage.equals(PAGES_SUBPACKAGE);
+
+        Collection<String> classNames = _componentClassLocator
+                .locateComponentClassNames(searchPackage);
+
+        int startPos = searchPackage.length() + 1;
+
+        for (String name : classNames)
+        {
+            String withinPackage = name.substring(startPos).replace('.', '/');
+
+            String logicalName = pathPrefix + withinPackage;
+
+            if (isPage)
+                _pageClassNameToLogicalName.put(name, logicalName);
+
+            logicalNameToClassName.put(logicalName.toLowerCase(), name);
+        }
+    }
+
+    public String resolvePageNameToClassName(String pageName)
+    {
+        String result = locate(pageName, _pageToClassName);
+
+        if (result == null)
+            throw new IllegalArgumentException(ServicesMessages.couldNotResolvePageName(pageName));
+
+        return result;
+    }
+
+    public String resolveComponentTypeToClassName(String componentType)
+    {
+        String result = locate(componentType, _componentToClassName);
+
+        if (result == null)
+            throw new IllegalArgumentException(ServicesMessages
+                    .couldNotResolveComponentType(componentType));
+
+        return result;
+    }
+
+    public String resolveMixinTypeToClassName(String mixinType)
+    {
+        String result = locate(mixinType, _mixinToClassName);
+
+        if (result == null)
+            throw new IllegalArgumentException(ServicesMessages.couldNotResolveMixinType(mixinType));
+
+        return result;
+    }
+
+    /**
+     * Locates a class name within the provided map, given its logical name. If not found naturally,
+     * a search inside the "core" library is included.
+     * 
+     * @param logicalName
+     *            name to search for
+     * @param logicalNameToClassName
+     *            mapping from logical name to class name
+     * @return the located class name or null
+     */
+    private String locate(String logicalName, Map<String, String> logicalNameToClassName)
+    {
+        rebuild();
+
+        String key = logicalName.toLowerCase();
+
+        String result = logicalNameToClassName.get(key);
+
+        // If not found, see if it exists under the core package. In this way,
+        // anything in core is "inherited" (but overridable) by the application.
+
+        if (result == null)
+            result = logicalNameToClassName.get("core/" + key);
+
+        return result;
+    }
+
+    public String resolvePageClassNameToPageName(String pageClassName)
+    {
+        rebuild();
+
+        String result = _pageClassNameToLogicalName.get(pageClassName);
+
+        if (result == null)
+            throw new IllegalArgumentException(ServicesMessages.pageNameUnresolved(pageClassName));
+
+        return result;
+    }
+
+    public void setApplicationPackage(String packageName)
+    {
+        _appRootPackage = packageName;
+
+        addPackagesToInstantiatorSource(packageName);
+    }
 }

Modified: tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/services/TapestryModule.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/services/TapestryModule.java?view=diff&rev=500470&r1=500469&r2=500470
==============================================================================
--- tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/services/TapestryModule.java (original)
+++ tapestry/tapestry5/tapestry-core/trunk/src/main/java/org/apache/tapestry/services/TapestryModule.java Fri Jan 26 19:13:31 2007
@@ -60,6 +60,7 @@
 import org.apache.tapestry.internal.services.BindingSourceImpl;
 import org.apache.tapestry.internal.services.ClasspathAssetAliasManagerImpl;
 import org.apache.tapestry.internal.services.CommonResourcesInjectionProvider;
+import org.apache.tapestry.internal.services.ComponentClassLocatorImpl;
 import org.apache.tapestry.internal.services.ComponentClassResolverImpl;
 import org.apache.tapestry.internal.services.ComponentEventDispatcher;
 import org.apache.tapestry.internal.services.ComponentInstanceResultProcessor;
@@ -549,7 +550,7 @@
             Collection<LibraryMapping> configuration)
     {
         ComponentClassResolverImpl service = new ComponentClassResolverImpl(
-                _componentInstantiatorSource, configuration);
+                _componentInstantiatorSource, new ComponentClassLocatorImpl(), configuration);
 
         // Allow the resolver to clean its cache when the source is invalidated
 

Added: tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/internal/services/ComponentClassLocatorImplTest.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/internal/services/ComponentClassLocatorImplTest.java?view=auto&rev=500470
==============================================================================
--- tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/internal/services/ComponentClassLocatorImplTest.java (added)
+++ tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/internal/services/ComponentClassLocatorImplTest.java Fri Jan 26 19:13:31 2007
@@ -0,0 +1,141 @@
+// Copyright 2007 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.tapestry.internal.services;
+
+import java.util.Collection;
+import java.util.Set;
+
+import org.apache.tapestry.ioc.internal.util.CollectionFactory;
+import org.apache.tapestry.ioc.internal.util.InternalUtils;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * Tricky to test, since the code is literally hunting around inside its own brain. There's a lot of
+ * room for unintended consequences here.
+ */
+public class ComponentClassLocatorImplTest extends Assert
+{
+    /**
+     * Use various packages in tapestry-ioc to test this, as those don't change unexpectedly(-ish)
+     * and we know they are in a JAR on the classpath.
+     */
+    @Test
+    public void classes_in_jar_file()
+    {
+        ComponentClassLocator locator = new ComponentClassLocatorImpl();
+
+        Collection<String> names = locator
+                .locateComponentClassNames("org.apache.tapestry.ioc.internal.util");
+
+        assertInList(
+                names,
+                "org.apache.tapestry.ioc.internal.util",
+                "MessagesImpl",
+                "LocalizedNameGenerator",
+                "IdAllocator");
+        assertNotInList(
+                names,
+                "org.apache.tapestry.ioc.internal.util",
+                "Orderer$1",
+                "InheritanceSearch$State");
+    }
+
+    @Test
+    public void classes_in_subpackage_in_jar_file()
+    {
+        ComponentClassLocator locator = new ComponentClassLocatorImpl();
+
+        Collection<String> names = locator.locateComponentClassNames("org.apache.tapestry.ioc");
+
+        assertInList(
+                names,
+                "org.apache.tapestry.ioc",
+                "internal.Module",
+                "internal.util.MessagesImpl");
+
+    }
+
+    /**
+     * This time, we use a selection of classes from tapestry-core, since those will never be
+     * packaged inside a JAR at this time.
+     */
+
+    @Test
+    public void classes_in_local_folders()
+    {
+        ComponentClassLocator locator = new ComponentClassLocatorImpl();
+
+        Collection<String> names = locator
+                .locateComponentClassNames("org.apache.tapestry.corelib.components");
+
+        assertInList(names, "org.apache.tapestry.corelib.components", "ActionLink", "Label");
+
+        assertNotInList(names, "org.apache.tapestry.corelib", "Label$1", "Loop$1");
+    }
+
+    @Test
+    public void classes_in_subpackages_in_local_folders()
+    {
+        ComponentClassLocator locator = new ComponentClassLocatorImpl();
+
+        Collection<String> names = locator.locateComponentClassNames("org.apache.tapestry.corelib");
+
+        assertInList(
+                names,
+                "org.apache.tapestry.corelib",
+                "components.ActionLink",
+                "base.AbstractField",
+                "mixins.RenderInformals");
+
+        assertNotInList(names, "org.apache.tapestry.corelib", "components.Label$1");
+    }
+
+    void assertInList(Collection<String> names, String packageName, String... classNames)
+    {
+        Set<String> classNameSet = CollectionFactory.newSet(names);
+
+        for (String className : classNames)
+        {
+            String fullName = packageName + "." + className;
+
+            if (classNameSet.contains(fullName))
+                continue;
+
+            String message = String.format("%s not found in %s.", fullName, InternalUtils
+                    .joinSorted(names));
+
+            throw new AssertionError(message);
+        }
+    }
+
+    void assertNotInList(Collection<String> names, String packageName, String... classNames)
+    {
+        Set<String> classNameSet = CollectionFactory.newSet(names);
+
+        for (String className : classNames)
+        {
+            String fullName = packageName + "." + className;
+
+            if (!classNameSet.contains(fullName))
+                continue;
+
+            String message = String.format("%s found in %s.", fullName, InternalUtils
+                    .joinSorted(names));
+
+            throw new AssertionError(message);
+        }
+    }
+}

Modified: tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/internal/services/ComponentClassResolverImplTest.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/internal/services/ComponentClassResolverImplTest.java?view=diff&rev=500470&r1=500469&r2=500470
==============================================================================
--- tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/internal/services/ComponentClassResolverImplTest.java (original)
+++ tapestry/tapestry5/tapestry-core/trunk/src/test/java/org/apache/tapestry/internal/services/ComponentClassResolverImplTest.java Fri Jan 26 19:13:31 2007
@@ -14,7 +14,11 @@
 
 package org.apache.tapestry.internal.services;
 
+import static org.easymock.EasyMock.isA;
+
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 import org.apache.tapestry.internal.test.InternalBaseTestCase;
@@ -35,11 +39,11 @@
     private static final String LIB_ROOT_PACKAGE = "org.example.lib";
 
     private ComponentClassResolverImpl create(ComponentInstantiatorSource source,
-            LibraryMapping... mappings)
+            ComponentClassLocator locator, LibraryMapping... mappings)
     {
         List<LibraryMapping> list = Arrays.asList(mappings);
 
-        ComponentClassResolverImpl resolver = new ComponentClassResolverImpl(source, list);
+        ComponentClassResolverImpl resolver = new ComponentClassResolverImpl(source, locator, list);
 
         resolver.setApplicationPackage(APP_ROOT_PACKAGE);
 
@@ -50,53 +54,83 @@
     public void simple_page_name()
     {
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_app_packages(source);
 
-        train_exists(source, APP_ROOT_PACKAGE + ".pages.SimplePage", true);
+        String className = APP_ROOT_PACKAGE + ".pages.SimplePage";
+
+        train_locateComponentClassNames(locator, APP_ROOT_PACKAGE + ".pages", className);
 
         replay();
 
-        ComponentClassResolver resolver = create(source);
+        ComponentClassResolver resolver = create(source, locator);
 
-        assertEquals(resolver.resolvePageNameToClassName("SimplePage"), APP_ROOT_PACKAGE
-                + ".pages.SimplePage");
+        assertEquals(resolver.resolvePageNameToClassName("SimplePage"), className);
 
         verify();
     }
 
+    protected final ComponentClassLocator newComponentClassLocator()
+    {
+        ComponentClassLocator locator = newMock(ComponentClassLocator.class);
+
+        stub_locateComponentClassNames(locator);
+
+        return locator;
+    }
+
+    private void stub_locateComponentClassNames(ComponentClassLocator locator)
+    {
+        Collection<String> noMatches = Collections.emptyList();
+
+        expect(locator.locateComponentClassNames(isA(String.class))).andStubReturn(noMatches);
+    }
+
+    protected final void train_locateComponentClassNames(ComponentClassLocator locator,
+            String packageName, String... classNames)
+    {
+        expect(locator.locateComponentClassNames(packageName)).andReturn(Arrays.asList(classNames));
+    }
+
     @Test
     public void class_name_to_simple_page_name()
     {
+        String className = APP_ROOT_PACKAGE + ".pages.SimplePage";
+
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_app_packages(source);
 
+        train_locateComponentClassNames(locator, APP_ROOT_PACKAGE + ".pages", className);
+
         replay();
 
-        ComponentClassResolver resolver = create(source);
+        ComponentClassResolver resolver = create(source, locator);
 
-        assertEquals(
-                resolver.resolvePageClassNameToPageName(APP_ROOT_PACKAGE + ".pages.SimplePage"),
-                "SimplePage");
+        assertEquals(resolver.resolvePageClassNameToPageName(className), "SimplePage");
 
         verify();
     }
 
+    /** All of the caches are handled identically, so we just test the pages for caching. */
     @Test
     public void resolved_page_names_are_cached()
     {
+
+        String pageClassName = APP_ROOT_PACKAGE + ".pages.SimplePage";
+
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_app_packages(source);
 
-        String pageClassName = APP_ROOT_PACKAGE + ".pages.SimplePage";
-
-        train_exists(source, pageClassName, true);
+        train_locateComponentClassNames(locator, APP_ROOT_PACKAGE + ".pages", pageClassName);
 
         replay();
 
-        ComponentClassResolverImpl resolver = create(source);
+        ComponentClassResolverImpl resolver = create(source, locator);
 
         assertEquals(resolver.resolvePageNameToClassName("SimplePage"), pageClassName);
 
@@ -112,7 +146,8 @@
 
         // After clearing the cache, redoes the work.
 
-        train_exists(source, pageClassName, true);
+        train_locateComponentClassNames(locator, APP_ROOT_PACKAGE + ".pages", pageClassName);
+        stub_locateComponentClassNames(locator);
 
         replay();
 
@@ -126,21 +161,22 @@
     @Test
     public void page_found_in_core_lib()
     {
+        String className = CORE_ROOT_PACKAGE + ".pages.CorePage";
+
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_packages(source, CORE_ROOT_PACKAGE);
         train_for_app_packages(source);
 
-        train_exists(source, APP_ROOT_PACKAGE + ".pages.CorePage", false);
-        train_exists(source, CORE_ROOT_PACKAGE + ".pages.CorePage", true);
+        train_locateComponentClassNames(locator, CORE_ROOT_PACKAGE + ".pages", className);
 
         replay();
 
-        ComponentClassResolver resolver = create(source, new LibraryMapping(CORE_PREFIX,
+        ComponentClassResolver resolver = create(source, locator, new LibraryMapping(CORE_PREFIX,
                 CORE_ROOT_PACKAGE));
 
-        assertEquals(resolver.resolvePageNameToClassName("CorePage"), CORE_ROOT_PACKAGE
-                + ".pages.CorePage");
+        assertEquals(resolver.resolvePageNameToClassName("CorePage"), className);
 
         verify();
     }
@@ -148,45 +184,22 @@
     @Test
     public void page_class_name_resolved_to_core_page()
     {
-        ComponentInstantiatorSource source = newComponentInstantiatorSource();
-
-        train_for_packages(source, CORE_ROOT_PACKAGE);
-        train_for_app_packages(source);
-
-        replay();
-
-        ComponentClassResolver resolver = create(source, new LibraryMapping(CORE_PREFIX,
-                CORE_ROOT_PACKAGE));
-
-        assertEquals(
-                resolver.resolvePageClassNameToPageName(CORE_ROOT_PACKAGE + ".pages.CorePage"),
-                "core/CorePage");
-
-        verify();
-    }
+        String className = CORE_ROOT_PACKAGE + ".pages.CorePage";
 
-    @Test
-    public void resolved_logical_page_names_are_cached()
-    {
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_packages(source, CORE_ROOT_PACKAGE);
         train_for_app_packages(source);
 
+        train_locateComponentClassNames(locator, CORE_ROOT_PACKAGE + ".pages", className);
+
         replay();
 
-        ComponentClassResolver resolver = create(source, new LibraryMapping(CORE_PREFIX,
+        ComponentClassResolver resolver = create(source, locator, new LibraryMapping(CORE_PREFIX,
                 CORE_ROOT_PACKAGE));
 
-        String className = CORE_ROOT_PACKAGE + ".pages.CorePage";
-
-        String logicalName1 = resolver.resolvePageClassNameToPageName(className);
-        String logicalName2 = resolver.resolvePageClassNameToPageName(className);
-
-        // Given that the value is computed on the fly, if its the same (not just equal, but the
-        // same), that proves that the value was cached.
-
-        assertSame(logicalName2, logicalName1);
+        assertEquals(resolver.resolvePageClassNameToPageName(className), "core/CorePage");
 
         verify();
     }
@@ -194,41 +207,48 @@
     @Test
     public void page_found_in_library()
     {
+        String className = LIB_ROOT_PACKAGE + ".pages.LibPage";
+
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_packages(source, LIB_ROOT_PACKAGE);
         train_for_packages(source, CORE_ROOT_PACKAGE);
         train_for_app_packages(source);
 
-        train_exists(source, LIB_ROOT_PACKAGE + ".pages.LibPage", true);
+        train_locateComponentClassNames(locator, LIB_ROOT_PACKAGE + ".pages", className);
 
         replay();
 
-        ComponentClassResolver resolver = create(source, new LibraryMapping(LIB_PREFIX,
+        ComponentClassResolver resolver = create(source, locator, new LibraryMapping(LIB_PREFIX,
                 LIB_ROOT_PACKAGE), new LibraryMapping(CORE_PREFIX, CORE_ROOT_PACKAGE));
 
-        assertEquals(resolver.resolvePageNameToClassName("lib/LibPage"), LIB_ROOT_PACKAGE
-                + ".pages.LibPage");
+        assertEquals(resolver.resolvePageNameToClassName("lib/LibPage"), className);
 
         verify();
     }
 
     @Test
-    public void class_name_resolves_to_folder_under_library()
+    public void lookup_by_logical_name_is_case_insensitive()
     {
+        String className = LIB_ROOT_PACKAGE + ".pages.LibPage";
+
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_packages(source, LIB_ROOT_PACKAGE);
         train_for_packages(source, CORE_ROOT_PACKAGE);
         train_for_app_packages(source);
 
+        train_locateComponentClassNames(locator, LIB_ROOT_PACKAGE + ".pages", className);
+
         replay();
 
-        ComponentClassResolver resolver = create(source, new LibraryMapping(LIB_PREFIX,
+        ComponentClassResolver resolver = create(source, locator, new LibraryMapping(LIB_PREFIX,
                 LIB_ROOT_PACKAGE), new LibraryMapping(CORE_PREFIX, CORE_ROOT_PACKAGE));
 
-        assertEquals(resolver.resolvePageClassNameToPageName(LIB_ROOT_PACKAGE
-                + ".pages.foo.bar.LibPage"), LIB_PREFIX + "/foo/bar/LibPage");
+        assertEquals(resolver.resolvePageNameToClassName("lib/libpage"), className);
+        assertEquals(resolver.resolvePageNameToClassName("LIB/LIBPAGE"), className);
 
         verify();
     }
@@ -237,13 +257,14 @@
     public void class_name_does_not_resolve_to_page_name()
     {
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_packages(source, CORE_ROOT_PACKAGE);
         train_for_app_packages(source);
 
         replay();
 
-        ComponentClassResolver resolver = create(source, new LibraryMapping(CORE_PREFIX,
+        ComponentClassResolver resolver = create(source, locator, new LibraryMapping(CORE_PREFIX,
                 CORE_ROOT_PACKAGE));
 
         String className = LIB_ROOT_PACKAGE + ".pages.LibPage";
@@ -266,13 +287,14 @@
     public void class_name_not_in_a_pages_package()
     {
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_packages(source, CORE_ROOT_PACKAGE);
         train_for_app_packages(source);
 
         replay();
 
-        ComponentClassResolver resolver = create(source, new LibraryMapping(CORE_PREFIX,
+        ComponentClassResolver resolver = create(source, locator, new LibraryMapping(CORE_PREFIX,
                 CORE_ROOT_PACKAGE));
 
         String className = CORE_ROOT_PACKAGE + ".foo.CorePage";
@@ -295,27 +317,28 @@
     public void multiple_mappings_for_same_prefix()
     {
         String secondaryLibPackage = "org.examples.addon.lib";
+        String className = secondaryLibPackage + ".pages.LibPage";
 
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_packages(source, LIB_ROOT_PACKAGE);
         train_for_packages(source, secondaryLibPackage);
         train_for_packages(source, CORE_ROOT_PACKAGE);
         train_for_app_packages(source);
 
-        train_exists(source, LIB_ROOT_PACKAGE + ".pages.LibPage", false);
-        train_exists(source, secondaryLibPackage + ".pages.LibPage", true);
+        train_locateComponentClassNames(locator, secondaryLibPackage + ".pages", className);
 
         replay();
 
         ComponentClassResolver resolver = create(
                 source,
+                locator,
                 new LibraryMapping(LIB_PREFIX, LIB_ROOT_PACKAGE),
                 new LibraryMapping(LIB_PREFIX, secondaryLibPackage),
                 new LibraryMapping(CORE_PREFIX, CORE_ROOT_PACKAGE));
 
-        assertEquals(resolver.resolvePageNameToClassName("lib/LibPage"), secondaryLibPackage
-                + ".pages.LibPage");
+        assertEquals(resolver.resolvePageNameToClassName("lib/LibPage"), className);
 
         verify();
     }
@@ -326,27 +349,20 @@
         String deepPackage = "org.deep";
 
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_packages(source, deepPackage);
         train_for_packages(source, LIB_ROOT_PACKAGE);
         train_for_packages(source, CORE_ROOT_PACKAGE);
         train_for_app_packages(source);
 
-        // In early searches, the prefix is "absorbed" into the root package.
-        // In later searches, the prefix is used as a sub-package.
-
-        train_exists(source, deepPackage + ".pages.DeepPage", false);
-        train_exists(source, LIB_ROOT_PACKAGE + ".pages.deep.DeepPage", false);
-        train_exists(source, APP_ROOT_PACKAGE + ".pages.lib.deep.DeepPage", false);
-        train_exists(source, CORE_ROOT_PACKAGE + ".pages.lib.deep.DeepPage", false);
+        // Is this test even needed any more with the new algorithm?
 
         replay();
 
-        ComponentClassResolver resolver = create(
-                source,
-                new LibraryMapping("lib/deep", deepPackage),
-                new LibraryMapping(LIB_PREFIX, LIB_ROOT_PACKAGE),
-                new LibraryMapping(CORE_PREFIX, CORE_ROOT_PACKAGE));
+        ComponentClassResolver resolver = create(source, locator, new LibraryMapping("lib/deep",
+                deepPackage), new LibraryMapping(LIB_PREFIX, LIB_ROOT_PACKAGE), new LibraryMapping(
+                CORE_PREFIX, CORE_ROOT_PACKAGE));
 
         try
         {
@@ -363,39 +379,6 @@
         verify();
     }
 
-    @Test
-    public void prefix_page_totally_missing()
-    {
-        ComponentInstantiatorSource source = newComponentInstantiatorSource();
-
-        train_for_packages(source, LIB_ROOT_PACKAGE);
-        train_for_packages(source, CORE_ROOT_PACKAGE);
-        train_for_app_packages(source);
-
-        train_exists(source, LIB_ROOT_PACKAGE + ".pages.MissingPage", false);
-        train_exists(source, APP_ROOT_PACKAGE + ".pages.lib.MissingPage", false);
-        train_exists(source, CORE_ROOT_PACKAGE + ".pages.lib.MissingPage", false);
-
-        replay();
-
-        ComponentClassResolver resolver = create(source, new LibraryMapping(LIB_PREFIX,
-                LIB_ROOT_PACKAGE), new LibraryMapping(CORE_PREFIX, CORE_ROOT_PACKAGE));
-
-        try
-        {
-            resolver.resolvePageNameToClassName("lib/MissingPage");
-            unreachable();
-        }
-        catch (IllegalArgumentException ex)
-        {
-            assertEquals(
-                    ex.getMessage(),
-                    "Unable to resolve page 'lib/MissingPage' to a component class name.");
-        }
-
-        verify();
-    }
-
     private void train_for_packages(ComponentInstantiatorSource source, String packageName)
     {
         source.addPackage(packageName + ".pages");
@@ -411,20 +394,20 @@
     @Test
     public void simple_component_type()
     {
-        String expectedClassName = APP_ROOT_PACKAGE + ".components.SimpleComponent";
+        String className = APP_ROOT_PACKAGE + ".components.SimpleComponent";
 
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_app_packages(source);
 
-        train_exists(source, expectedClassName, true);
+        train_locateComponentClassNames(locator, APP_ROOT_PACKAGE + ".components", className);
 
         replay();
 
-        ComponentClassResolver resolver = create(source);
+        ComponentClassResolver resolver = create(source, locator);
 
-        assertEquals(resolver.resolveComponentTypeToClassName("SimpleComponent"), APP_ROOT_PACKAGE
-                + ".components.SimpleComponent");
+        assertEquals(resolver.resolveComponentTypeToClassName("SimpleComponent"), className);
 
         verify();
     }
@@ -439,14 +422,15 @@
         String expectedClassName = APP_ROOT_PACKAGE + ".mixins.SimpleMixin";
 
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_app_packages(source);
 
-        train_exists(source, expectedClassName, true);
+        train_locateComponentClassNames(locator, APP_ROOT_PACKAGE + ".mixins", expectedClassName);
 
         replay();
 
-        ComponentClassResolver resolver = create(source);
+        ComponentClassResolver resolver = create(source, locator);
 
         assertEquals(resolver.resolveMixinTypeToClassName("SimpleMixin"), expectedClassName);
 
@@ -457,16 +441,14 @@
     public void mixin_type_not_found()
     {
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_packages(source, CORE_ROOT_PACKAGE);
         train_for_app_packages(source);
 
-        train_exists(source, APP_ROOT_PACKAGE + ".mixins.SimpleMixin", false);
-        train_exists(source, CORE_ROOT_PACKAGE + ".mixins.SimpleMixin", false);
-
         replay();
 
-        ComponentClassResolver resolver = create(source, new LibraryMapping(CORE_PREFIX,
+        ComponentClassResolver resolver = create(source, locator, new LibraryMapping(CORE_PREFIX,
                 CORE_ROOT_PACKAGE));
 
         try
@@ -489,16 +471,14 @@
     public void component_type_not_found()
     {
         ComponentInstantiatorSource source = newComponentInstantiatorSource();
+        ComponentClassLocator locator = newComponentClassLocator();
 
         train_for_packages(source, CORE_ROOT_PACKAGE);
         train_for_app_packages(source);
 
-        train_exists(source, APP_ROOT_PACKAGE + ".components.SimpleComponent", false);
-        train_exists(source, CORE_ROOT_PACKAGE + ".components.SimpleComponent", false);
-
         replay();
 
-        ComponentClassResolver resolver = create(source, new LibraryMapping(CORE_PREFIX,
+        ComponentClassResolver resolver = create(source, locator, new LibraryMapping(CORE_PREFIX,
                 CORE_ROOT_PACKAGE));
 
         try
@@ -515,59 +495,6 @@
 
         verify();
 
-    }
-
-    @Test
-    public void component_types_are_cached()
-    {
-        ComponentInstantiatorSource source = newComponentInstantiatorSource();
-
-        train_for_app_packages(source);
-
-        String componentClassName = APP_ROOT_PACKAGE + ".components.SimpleComponent";
-
-        train_exists(source, componentClassName, true);
-
-        replay();
-
-        ComponentClassResolverImpl resolver = create(source);
-
-        assertEquals(
-                resolver.resolveComponentTypeToClassName("SimpleComponent"),
-                componentClassName);
-
-        verify();
-
-        // No training, its in the cache
-
-        replay();
-
-        assertEquals(
-                resolver.resolveComponentTypeToClassName("SimpleComponent"),
-                componentClassName);
-
-        verify();
-
-        // The cache is cleared, so the search will re-execute
-
-        train_exists(source, componentClassName, true);
-
-        replay();
-
-        resolver.objectWasInvalidated();
-
-        assertEquals(
-                resolver.resolveComponentTypeToClassName("SimpleComponent"),
-                componentClassName);
-
-        verify();
-
-    }
-
-    protected final void train_exists(ComponentInstantiatorSource source, String className,
-            boolean exists)
-    {
-        expect(source.exists(className)).andReturn(exists);
     }
 
     private void train_for_app_packages(ComponentInstantiatorSource source)