You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@click.apache.org by sa...@apache.org on 2009/11/06 12:23:07 UTC

svn commit: r833349 - in /incubator/click/trunk/click: LICENSE.txt NOTICE.txt framework/src/org/apache/click/service/DeployUtils.java framework/src/org/apache/click/service/XmlConfigService.java

Author: sabob
Date: Fri Nov  6 11:23:06 2009
New Revision: 833349

URL: http://svn.apache.org/viewvc?rev=833349&view=rev
Log:
fixed resource deployment for JBoss 5. CLK-589

Added:
    incubator/click/trunk/click/framework/src/org/apache/click/service/DeployUtils.java
Modified:
    incubator/click/trunk/click/LICENSE.txt
    incubator/click/trunk/click/NOTICE.txt
    incubator/click/trunk/click/framework/src/org/apache/click/service/XmlConfigService.java

Modified: incubator/click/trunk/click/LICENSE.txt
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/LICENSE.txt?rev=833349&r1=833348&r2=833349&view=diff
==============================================================================
--- incubator/click/trunk/click/LICENSE.txt (original)
+++ incubator/click/trunk/click/LICENSE.txt Fri Nov  6 11:23:06 2009
@@ -804,3 +804,10 @@
          here. Licensor shall not be bound by any additional provisions that
          may appear in any communication from You. This License may not be
          modified without the mutual written agreement of the Licensor and You.
+
+======================================================================
+
+STRIPES FRAMEWORK LICENSE
+
+org.apache.click.service.DeployUtils is taken from the Stripes Framework
+distributed under the terms of the Apache Software License 2.0.

Modified: incubator/click/trunk/click/NOTICE.txt
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/NOTICE.txt?rev=833349&r1=833348&r2=833349&view=diff
==============================================================================
--- incubator/click/trunk/click/NOTICE.txt (original)
+++ incubator/click/trunk/click/NOTICE.txt Fri Nov  6 11:23:06 2009
@@ -82,4 +82,8 @@
 
 This project includes the FamFamFam icons by Mark James, distributed
 under the terms of the Creative Commons Attribution 2.5 License.
-http://www.famfamfam.com/lab/icons
\ No newline at end of file
+http://www.famfamfam.com/lab/icons
+
+This product includes the class org.apache.click.service.DeployUtils.java
+from the Stripes Framework, released under the Apache 2.0 license.
+http://stripesframework.org
\ No newline at end of file

Added: incubator/click/trunk/click/framework/src/org/apache/click/service/DeployUtils.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/service/DeployUtils.java?rev=833349&view=auto
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/service/DeployUtils.java (added)
+++ incubator/click/trunk/click/framework/src/org/apache/click/service/DeployUtils.java Fri Nov  6 11:23:06 2009
@@ -0,0 +1,552 @@
+/* Copyright 2005-2006 Tim Fennell
+ *
+ * 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.click.service;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import org.apache.click.util.ClickUtils;
+
+/**
+ * <p>ResolverUtil is used to locate classes that are available in the/a class path and meet
+ * arbitrary conditions. The two most common conditions are that a class implements/extends
+ * another class, or that is it annotated with a specific annotation. However, through the use
+ * of the {@link Test} class it is possible to search using arbitrary conditions.</p>
+ *
+ * <p>A ClassLoader is used to locate all locations (directories and jar files) in the class
+ * path that contain classes within certain packages, and then to load those classes and
+ * check them. By default the ClassLoader returned by
+ *  {@code Thread.currentThread().getContextClassLoader()} is used, but this can be overridden
+ * by calling {@link #setClassLoader(ClassLoader)} prior to invoking any of the {@code find()}
+ * methods.</p>
+ *
+ * <p>General searches are initiated by calling the
+ * {@link #find(net.sourceforge.stripes.util.ResolverUtil.Test, String)} ()} method and supplying
+ * a package name and a Test instance. This will cause the named package <b>and all sub-packages</b>
+ * to be scanned for classes that meet the test. There are also utility methods for the common
+ * use cases of scanning multiple packages for extensions of particular classes, or classes
+ * annotated with a specific annotation.</p>
+ *
+ * <p>The standard usage pattern for the ResolverUtil class is as follows:</p>
+ *
+ *<pre>
+ *ResolverUtil&lt;ActionBean&gt; resolver = new ResolverUtil&lt;ActionBean&gt;();
+ *resolver.findImplementation(ActionBean.class, pkg1, pkg2);
+ *resolver.find(new CustomTest(), pkg1);
+ *resolver.find(new CustomTest(), pkg2);
+ *Collection&lt;ActionBean&gt; beans = resolver.getClasses();
+ *</pre>
+ *
+ * This class was copied and adpated from the Stripes Framework -
+ * <a href="http://www.stripesframework.org">Stripes</a>.
+ *
+ * @author Tim Fennell
+ */
+class DeployUtils<T> {
+
+    // -------------------------------------------------------------- Constants
+
+    /** The magic header that indicates a JAR (ZIP) file. */
+    private static final byte[] JAR_MAGIC = { 'P', 'K', 3, 4 };
+
+    // -------------------------------------------------------------- Variables
+
+    /** The set of matches being accumulated. */
+    private List<String> matches = new ArrayList<String>();
+
+    /** The log service to log output to. */
+    private LogService logService;
+
+    /**
+     * The ClassLoader to use when looking for classes. If null then the ClassLoader
+     * returned by Thread.currentThread().getContextClassLoader() will be used.
+     */
+    private ClassLoader classloader;
+
+    // ----------------------------------------------------------- Constructors
+
+    /**
+     * Create a new DeployUtils instance.
+     *
+     * @param logService the logService to log output to
+     */
+    public DeployUtils(LogService logService) {
+        this.logService = logService;
+    }
+
+    // --------------------------------------------------------- Public Methods
+
+    /**
+     * Provides access to the resources discovered so far. If no calls have been
+     * made to any of the {@code find()} methods, this list will be empty.
+     *
+     * @return the list of resources that have been discovered.
+     */
+    public List<String> getResources() {
+        return matches;
+    }
+
+    /**
+     * Returns the classloader that will be used for scanning for classes. If no explicit
+     * ClassLoader has been set by the calling, the context class loader will be used.
+     *
+     * @return the ClassLoader that will be used to scan for classes
+     */
+    public ClassLoader getClassLoader() {
+        return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader;
+    }
+
+    /**
+     * Sets an explicit ClassLoader that should be used when scanning for classes. If none
+     * is set then the context classloader will be used.
+     *
+     * @param classloader a ClassLoader to use when scanning for classes
+     */
+    public void setClassLoader(ClassLoader classloader) {
+        this.classloader = classloader;
+    }
+
+    /**
+     * Attemp to discover resources inside the given directory. Accumulated
+     * resources can be accessed by calling {@link #getResources()}.
+     *
+     * @param dirs one or more directories to scan (including sub-directories)
+     * for resources
+     * @return instance of DeployUtils allows for chaining calls
+     */
+    public DeployUtils<T> findResources(String... dirs) {
+        if (dirs == null) {
+            return this;
+        }
+
+        Test test = new IsDeployable();
+        for (String dir : dirs) {
+            find(test, dir);
+        }
+
+        return this;
+    }
+
+    /**
+     * Scans for classes starting at the package provided and descending into subpackages.
+     * Each class is offered up to the Test as it is discovered, and if the Test returns
+     * true the class is retained.  Accumulated classes can be fetched by calling
+     * {@link #getClasses()}.
+     *
+     * @param test an instance of {@link Test} that will be used to filter classes
+     * @param packageName the name of the package from which to start scanning for
+     *        classes, e.g. {@code net.sourceforge.stripes}
+     */
+    public DeployUtils<T> find(Test test, String packageName) {
+        String path = getPackagePath(packageName);
+
+        try {
+            List<URL> urls = Collections.list(getClassLoader().getResources(path));
+            for (URL url : urls) {
+                List<String> children = listClassResources(url, path);
+                for (String child : children) {
+                    addIfMatching(test, child);
+                }
+            }
+        }
+        catch (IOException ioe) {
+            logService.error("Could not read package: " + packageName + " -- " + ioe);
+        }
+
+        return this;
+    }
+
+    // ------------------------------------------------------ Protected Methods
+
+    /**
+     * Recursively list all resources under the given URL that appear to define a Java class.
+     * Matching resources will have a name that ends in ".class" and have a relative path such that
+     * each segment of the path is a valid Java identifier. The resource paths returned will be
+     * relative to the URL and begin with the specified path.
+     *
+     * @param url The URL of the parent resource to search.
+     * @param path The path with which each matching resource path must begin, relative to the URL.
+     * @return A list of matching resources. The list may be empty.
+     * @throws IOException
+     */
+    protected List<String> listClassResources(URL url, String path) throws IOException {
+        if (logService.isDebugEnabled()) {
+            logService.debug("Listing classes in " + url);
+        }
+
+        InputStream is = null;
+        try {
+            List<String> resources = new ArrayList<String>();
+
+            // First, try to find the URL of a JAR file containing the requested
+            // resource. If a JAR file is found, then we'll list child resources
+            // by reading the JAR.
+            URL jarUrl = findJarForResource(url, path);
+            if (jarUrl != null) {
+                // example jarUrl : jar:c:/dev/mylib.jar
+                is = jarUrl.openStream();
+                resources = listClassResources(new JarInputStream(is), path);
+
+            } else {
+                List<String> children = new ArrayList<String>();
+                try {
+                    if (isJar(url)) {
+                        // example url : jar:c:/dev/mylib.jar/META-INF/resources
+
+                        // Some versions of JBoss VFS might give a JAR stream even
+                        // if the resource referenced by the URL isn't actually a JAR
+                        is = url.openStream();
+                        JarInputStream jarInput = new JarInputStream(is);
+                        for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
+                            if (logService.isTraceEnabled()) {
+                                logService.trace("Jar entry: " + entry.getName());
+                            }
+
+                            if (isRelevantResource(entry.getName())) {
+                                children.add(entry.getName());
+                            }
+                        }
+                    } else {
+                        // Some servlet containers allow reading from "directory"
+                        // resources like text file, listing the child resources
+                        // one per line.
+                        is = url.openStream();
+
+                        // There is the possibility that a file doesn't have an
+                        // extension and would be seen as a directory. Guard against
+                        // that by checking that the url is a file and adding it
+                        // to the list of resources
+                        File file = new File(url.getFile());
+                        if (file.isFile()) {
+                            if (isRelevantResource(file.getName())) {
+                                resources.add(path);
+                            }
+                        } else {
+                            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+                            for (String line; (line = reader.readLine()) != null;) {
+                                if (logService.isTraceEnabled()) {
+                                    logService.trace("Reader entry: " + line);
+                                }
+                                if (isRelevantResource(line)) {
+                                    children.add(line);
+                                }
+                            }
+                        }
+                    }
+                } catch (FileNotFoundException e) {
+                    /*
+                     * For file URLs the openStream() call might fail, depending on the servlet
+                     * container, because directories can't be opened for reading. If that happens,
+                     * then list the directory directly instead.
+                     */
+                    if ("file".equals(url.getProtocol())) {
+                        File file = new File(url.getFile());
+                        if (file.isDirectory()) {
+                            children = Arrays.asList(file.list(new FilenameFilter() {
+                                public boolean accept(File dir, String name) {
+                                    return isRelevantResource(name);
+                                }
+                            }));
+                        }
+                    } else {
+                        // No idea where the exception came from so log it
+                        logService.error("Could not deploy the resources from"
+                            + " the url '" + url + "'. You will need to"
+                            + " manually included resources from this url in"
+                            + " your application.");
+                    }
+                }
+
+                // The URL prefix to use when recursively listing child resources
+                String prefix = url.toExternalForm();
+                if (!prefix.endsWith("/"))
+                    prefix = prefix + "/";
+
+                // Iterate over each immediate child, adding classes and recursing into directories
+                for (String child : children) {
+                    String resourcePath = path + "/" + child;
+                    if (child.indexOf(".") != -1) {
+                        if (logService.isTraceEnabled()) {
+                            logService.trace("Found deployable resource: " + resourcePath);
+                        }
+                        resources.add(resourcePath);
+                    }
+                    else {
+                        URL childUrl = new URL(prefix + child);
+                        resources.addAll(listClassResources(childUrl, resourcePath));
+                    }
+                }
+            }
+
+            return resources;
+        }
+        finally {
+            try {
+                is.close();
+            }
+            catch (Exception e) {
+            }
+        }
+    }
+
+    /**
+     * List the names of the entries in the given {@link JarInputStream} that begin with the
+     * specified {@code path}. Entries will match with or without a leading slash.
+     *
+     * @param jar The JAR input stream
+     * @param path The leading path to match
+     * @return The names of all the matching entries
+     * @throws IOException
+     */
+    protected List<String> listClassResources(JarInputStream jar, String path) throws IOException {
+        // Include the leading and trailing slash when matching names
+        if (!path.startsWith("/"))
+            path = "/" + path;
+        if (!path.endsWith("/"))
+            path = path + "/";
+
+        // Iterate over the entries and collect those that begin with the requested path
+        List<String> resources = new ArrayList<String>();
+        for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
+            if (!entry.isDirectory()) {
+                // Add leading slash if it's missing
+                String name = entry.getName();
+                if (!name.startsWith("/"))
+                    name = "/" + name;
+
+                // Check resource name
+                if (name.startsWith(path)) {
+                    resources.add(name.substring(1)); // Trim leading slash
+                }
+            }
+        }
+        return resources;
+    }
+
+    /**
+     * Attempts to deconstruct the given URL to find a JAR file containing the resource referenced
+     * by the URL. That is, assuming the URL references a JAR entry, this method will return a URL
+     * that references the JAR file containing the entry. If the JAR cannot be located, then this
+     * method returns null.
+     *
+     * @param url The URL of the JAR entry.
+     * @param path The path by which the URL was requested from the class loader.
+     * @return The URL of the JAR file, if one is found. Null if not.
+     * @throws MalformedURLException
+     */
+    protected URL findJarForResource(URL url, String path) throws MalformedURLException {
+        if (logService.isTraceEnabled()) {
+            logService.trace("Find JAR URL: " + url);
+        }
+
+        // If the file part of the URL is itself a URL, then that URL probably points to the JAR
+        try {
+            for (;;) {
+                url = new URL(url.getFile());
+                if (logService.isTraceEnabled()) {
+                    logService.trace("Inner URL: " + url);
+                }
+            }
+        } catch (MalformedURLException e) {
+            // This will happen at some point and serves a break in the loop
+        }
+
+        // Look for the .jar extension and chop off everything after that
+        StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
+        int index = jarUrl.lastIndexOf(".jar");
+        if (index >= 0) {
+            jarUrl.setLength(index + 4);
+            if (logService.isTraceEnabled()) {
+                logService.trace("Extracted JAR URL: " + jarUrl);
+            }
+        } else {
+            if (logService.isTraceEnabled()) {
+                logService.trace("Not a JAR: " + jarUrl);
+            }
+            return null;
+        }
+
+        // Try to open and test it
+        try {
+            URL testUrl = new URL(jarUrl.toString());
+            if (isJar(testUrl)) {
+                return testUrl;
+            } else {
+                // WebLogic fix: check if the URL's file exists in the filesystem.
+                if (logService.isTraceEnabled()) {
+                    logService.trace("Not a JAR: " + jarUrl);
+                }
+
+                jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
+
+                // File name might be URL-encoded
+                File file = new File(ClickUtils.decodeURL(jarUrl.toString()));
+                if (file.exists()) {
+                    if (logService.isTraceEnabled()) {
+                        logService.trace("Trying real file: " + file.getAbsolutePath());
+                    }
+                    testUrl =  file.toURI().toURL();
+                    if (isJar(testUrl)) {
+                        return testUrl;
+                    }
+                }
+            }
+        }
+        catch (MalformedURLException e) {
+            logService.warn("Invalid JAR URL: " + e.getMessage());
+        }
+
+        if (logService.isTraceEnabled()) {
+            logService.trace("Not a JAR: " + jarUrl);
+        }
+        return null;
+    }
+
+    /**
+     * Converts a Java package name to a path that can be looked up with a call to
+     * {@link ClassLoader#getResources(String)}.
+     *
+     * @param packageName The Java package name to convert to a path
+     */
+    protected String getPackagePath(String packageName) {
+        return packageName == null ? null : packageName.replace('.', '/');
+    }
+
+    /**
+     * Returns true if the name of a resource (file or directory) is one that matters in the search
+     * for classes. Relevant resources would be class files themselves (file names that end with
+     * ".class") and directories that might be a Java package name segment (java identifiers).
+     *
+     * @param resourceName The resource name, without path information
+     */
+    protected boolean isRelevantResource(String resourceName) {
+        return resourceName != null && !resourceName.equals("");
+    }
+
+    /**
+     * Returns true if the resource located at the given URL is a JAR file.
+     *
+     * @param url The URL of the resource to test.
+     */
+    protected boolean isJar(URL url) {
+        return isJar(url, new byte[JAR_MAGIC.length]);
+    }
+
+    /**
+     * Returns true if the resource located at the given URL is a JAR file.
+     *
+     * @param url The URL of the resource to test.
+     * @param buffer A buffer into which the first few bytes of the resource are read. The buffer
+     *            must be at least the size of {@link #JAR_MAGIC}. (The same buffer may be reused
+     *            for multiple calls as an optimization.)
+     */
+    protected boolean isJar(URL url, byte[] buffer) {
+        InputStream is = null;
+        try {
+            is = url.openStream();
+            is.read(buffer, 0, JAR_MAGIC.length);
+            if (Arrays.equals(buffer, JAR_MAGIC)) {
+                if (logService.isInfoEnabled()) {
+                    logService.info("Found JAR: " + url);
+                }
+                return true;
+            }
+        }
+        catch (Exception e) {
+            // Failure to read the stream means this is not a JAR
+        }
+        finally {
+            try {
+                is.close();
+            }
+            catch (Exception e) {
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Add the class designated by the fully qualified class name provided to the set of
+     * resolved classes if and only if it is approved by the Test supplied.
+     *
+     * @param test the test used to determine if the class matches
+     * @param fqn the fully qualified name of a class
+     */
+    @SuppressWarnings("unchecked")
+    protected void addIfMatching(Test test, String fqn) {
+        try {
+            if (test.matches(fqn) ) {
+                matches.add(fqn);
+            }
+        }
+        catch (Throwable t) {
+            logService.error("Could not examine class '" + fqn + "'" + " due to a "
+                + t.getClass().getName() + " with message: " + t.getMessage());
+        }
+    }
+
+    // ---------------------------------------------------------- Inner classes
+
+    /**
+     * A simple interface that specifies how to test classes to determine if they
+     * are to be included in the results produced by the ResolverUtil.
+     */
+    static interface Test {
+        /**
+         * Will be called repeatedly with candidate classes. Must return True if a class
+         * is to be included in the results, false otherwise.
+         */
+        boolean matches(String resource);
+    }
+
+    /**
+     * This test matches deployable resources.
+     */
+    static class IsDeployable implements Test {
+
+        /**
+         * Default constructor.
+         */
+        IsDeployable() {
+        }
+
+        /**
+         * Return true if the given resource should be included in the results,
+         * false otherwise.
+         *
+         * @param resource the resource to be included in the results
+         * @return true if the resource should be included in the results, false
+         * otherwise
+         */
+        @SuppressWarnings("unchecked")
+        public boolean matches(String resource) {
+            // If a resource is found, it must be deployed
+            return true;
+        }
+    }
+}

Modified: incubator/click/trunk/click/framework/src/org/apache/click/service/XmlConfigService.java
URL: http://svn.apache.org/viewvc/incubator/click/trunk/click/framework/src/org/apache/click/service/XmlConfigService.java?rev=833349&r1=833348&r2=833349&view=diff
==============================================================================
--- incubator/click/trunk/click/framework/src/org/apache/click/service/XmlConfigService.java (original)
+++ incubator/click/trunk/click/framework/src/org/apache/click/service/XmlConfigService.java Fri Nov  6 11:23:06 2009
@@ -18,8 +18,6 @@
  */
 package org.apache.click.service;
 
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.Field;
@@ -28,7 +26,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
-import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -37,8 +34,6 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.StringTokenizer;
-import java.util.jar.JarEntry;
-import java.util.jar.JarInputStream;
 
 import javax.servlet.ServletContext;
 
@@ -50,8 +45,6 @@
 import org.apache.click.util.ClickUtils;
 import org.apache.click.util.Format;
 import org.apache.click.util.HtmlStringBuffer;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.filefilter.TrueFileFilter;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang.Validate;
 import org.w3c.dom.Document;
@@ -880,6 +873,26 @@
         return pageClass;
     }
 
+    /**
+     * Returns true if Click resources (JavaScript, CSS, images etc) packaged
+     * in jars can be deployed to the root directory of the webapp, false
+     * otherwise.
+     * <p/>
+     * By default this method will return false in restricted environments where
+     * write access to the underlying file system is disallowed. Example
+     * environments where write access is not allowed include the WebLogic JEE
+     * server and Google App Engine. (Note: WebLogic provides the property
+     * <tt>"Archived Real Path Enabled"</tt> that controls whether web
+     * applications can access the file system or not. See the Click user manual
+     * for details).
+     *
+     * @return true if resources can be deployed, false otherwise
+     */
+    protected boolean isResourcesDeployable() {
+        // Only deploy if writes are allowed
+        return ClickUtils.isResourcesDeployable(servletContext);
+    }
+
     // ------------------------------------------------ Package Private Methods
 
     /**
@@ -1230,29 +1243,22 @@
      */
     private void deployFiles(Element rootElm) throws Exception {
 
-        // Deploy application files if they are not already present.
-        // Only deploy if writes are allowed
-        boolean isResourcesDeployable =
-            ClickUtils.isResourcesDeployable(servletContext);
-
-        if (isResourcesDeployable) {
+        if (isResourcesDeployable()) {
             deployControls(getResourceRootElement("/click-controls.xml"));
             deployControls(getResourceRootElement("/extras-controls.xml"));
             deployControls(rootElm);
             deployControlSets(rootElm);
-
             deployResourcesOnClasspath();
+
         } else {
-            String msg = "Could not auto deploy files to the 'click' web folder."
-                + " This can occur if the call to ServletContext.getRealPath(\"/\") "
-                + " returns null, which means the web application cannot determine"
-                + " the file system path to deploy files to. Another common problem"
-                + " is if the web application is not allowed to write to the file"
-                + " system. To resolve this issue you need to manually include"
-                + " click resources in your web application at build time. You"
-                + " can use the Ant 'DeployTask' that is shipped with Click"
-                + " to include resources at build time. The 'DeployTask' is"
-                + " included in the jar 'lib\\click-dev-tasks.jar'";
+            String msg = "WARNING: could not deploy Click resources to the"
+                + " 'click' web folder.\nThis can occur if the call to"
+                + " ServletContext.getRealPath(\"/\") returns null, which means"
+                + " the web application cannot determine the file system path"
+                + " to deploy files to. Another common problem is if the web"
+                + " application is not allowed to write to the file"
+                + " system.\nTo resolve this issue please see the Click user-guide: "
+                + "http://incubator.apache.org/click/docs/user-guide/html/ch04s03.html#deploying-restricted-env";
             getLogService().warn(msg);
         }
     }
@@ -1267,179 +1273,28 @@
      * @throws java.lang.IOException if the resources cannot be deployed
      */
     private void deployResourcesOnClasspath() throws IOException {
-
         long startTime = System.currentTimeMillis();
 
-        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
-
         // Find all jars and directories on the classpath that contains the
         // directory "META-INF/resources/", and deploy those resources
-        String resourceDirectory = "META-INF/resources/";
+        String resourceDirectory = "META-INF/resources";
 
-        // TODO: finding resources in jars and META-INF/classes might not always
-        // work on all servers. Might need to strip the final '/' from resourceDirectory
-        Enumeration<URL> en = classLoader.getResources(resourceDirectory);
-        while (en.hasMoreElements()) {
-            URL url = en.nextElement();
-            deployResourcesOnClasspath(url, resourceDirectory);
+        List<String> resources = new DeployUtils(logService).findResources(resourceDirectory).getResources();
+        for (String resource : resources) {
+            deployFile(resource, resourceDirectory);
         }
 
         // For backward compatibility, find all jars and directories on the
         // classpath that contains the directory "META-INF/web/", and deploy those
         // resources
-        resourceDirectory = "META-INF/web/";
-        en = classLoader.getResources(resourceDirectory);
-        while (en.hasMoreElements()) {
-            URL url = en.nextElement();
-            deployResourcesOnClasspath(url, resourceDirectory);
+        resourceDirectory = "META-INF/web";
+        resources = new DeployUtils(logService).findResources(resourceDirectory).getResources();
+        for (String resource : resources) {
+            deployFile(resource, resourceDirectory);
         }
 
-        if (logService.isTraceEnabled()) {
-            logService.trace("deployed files from jars and folders - "
-                + (System.currentTimeMillis() - startTime) + " ms");
-        }
-    }
-
-    /**
-     * Deploy from the url all resources found under the prefix.
-     *
-     * @param url the url of the jar or folder which resources to deploy
-     * @param resourceDirectory the directory under which resources are found
-     * @throws IOException if resources from the url cannot be deployed
-     */
-    private void deployResourcesOnClasspath(URL url, String resourceDirectory)
-        throws IOException {
-
-        String path = url.getFile();
-
-        // Decode the url, esp on Windows where file paths can have their
-        // spaces encoded. decodeURL will convert C:\Program%20Files\project
-        // to C:\Program Files\project
-        path = ClickUtils.decodeURL(path);
-
-        // Strip file prefix
-        if (path.startsWith("file:")) {
-            path = path.substring(5);
-        }
-
-        String jarPath = null;
-
-        // Check if path represents a jar
-        if (path.indexOf('!') > 0) {
-            jarPath = path.substring(0, path.indexOf('!'));
-
-            File jar = new File(jarPath);
-
-            if (jar.exists()) {
-                deployFilesInJar(jar, resourceDirectory);
-
-            } else {
-                logService.error("Could not deploy the jar '" + jarPath
-                    + "'. Please ensure this file exists in the specified"
-                    + " location.");
-            }
-        } else {
-            File dir = new File(path);
-            deployFilesInDir(dir, resourceDirectory);
-        }
-    }
-
-    /**
-     * Deploy files from the specified directory which are stored under the given
-     * resourceDirectory.
-     *
-     * @param dir the directory which resources will be deployed
-     * @param resourceDirectory the directory under which resources are found
-     * @throws java.lang.IOException if for some reason the files cannot be
-     * deployed
-     */
-    private void deployFilesInDir(File dir, String resourceDirectory)
-        throws IOException {
-
-        if (dir == null) {
-            throw new IllegalArgumentException("Dir cannot be null");
-        }
-
-        if (!dir.exists()) {
-            logService.trace("No resources deployed from the folder '" + dir.getAbsolutePath()
-                + "' as it does not exist.");
-            return;
-        }
-
-        Iterator files = FileUtils.iterateFiles(dir, TrueFileFilter.INSTANCE,
-            TrueFileFilter.INSTANCE);
-
-        boolean logFeedback = true;
-        while (files.hasNext()) {
-            // file example -> META-INF/resources/click/table.css
-            File file = (File) files.next();
-            String fileName = file.getCanonicalPath().replace('\\', '/');
-
-            // Only deploy resources from "META-INF/resources/"
-            int pathIndex = fileName.indexOf(resourceDirectory);
-            if (pathIndex != -1) {
-                if (logFeedback && logService.isTraceEnabled()) {
-                    logService.trace("deploy files from folder -> "
-                        + dir.getAbsolutePath());
-
-                    // Only provide feedback once per dir
-                    logFeedback = false;
-                }
-                fileName = fileName.substring(pathIndex);
-                deployFile(fileName, resourceDirectory);
-            }
-        }
-    }
-
-    /**
-     * Deploy files from the specified jar which are stored under the given
-     * resourceDirectory.
-     *
-     * @param jar the jar which resources will be deployed
-     * @param resourceDirectory the directory under which resources are found
-     * @throws java.lang.IOException if for some reason the files cannot be
-     * deployed
-     */
-    private void deployFilesInJar(File jar, String resourceDirectory)
-        throws IOException {
-
-        if (jar == null) {
-            throw new IllegalArgumentException("Jar cannot be null");
-        }
-
-        InputStream inputStream = null;
-        JarInputStream jarInputStream = null;
-
-        try {
-
-            inputStream = new FileInputStream(jar);
-            jarInputStream = new JarInputStream(inputStream);
-            JarEntry jarEntry = null;
-
-            // Indicates whether feedback should be logged about the files deployed
-            // from jar
-            boolean logFeedback = true;
-            while ((jarEntry = jarInputStream.getNextJarEntry()) != null) {
-                // jarEntryName example -> META-INF/resources/click/table.css
-                String jarEntryName = jarEntry.getName();
-
-                // Only deploy resources from "META-INF/resources/"
-                int pathIndex = jarEntryName.indexOf(resourceDirectory);
-                if (pathIndex == 0) {
-                    if (logFeedback && logService.isTraceEnabled()) {
-                        logService.trace("deploy files from jar -> "
-                                         + jar.getCanonicalPath());
-
-                        // Only provide feedback once per jar
-                        logFeedback = false;
-                    }
-                    deployFile(jarEntryName, resourceDirectory);
-                }
-            }
-        } finally {
-            ClickUtils.close(jarInputStream);
-            ClickUtils.close(inputStream);
-        }
+        logService.trace("deployed files from jars and folders - "
+            + (System.currentTimeMillis() - startTime) + " ms");
     }
 
     /**