001: /* Copyright 2005-2006 Tim Fennell
002: *
003: * Licensed under the Apache License, Version 2.0 (the "License");
004: * you may not use this file except in compliance with the License.
005: * You may obtain a copy of the License at
006: *
007: * http://www.apache.org/licenses/LICENSE-2.0
008: *
009: * Unless required by applicable law or agreed to in writing, software
010: * distributed under the License is distributed on an "AS IS" BASIS,
011: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: * See the License for the specific language governing permissions and
013: * limitations under the License.
014: */
015: package org.caramba.config;
016:
017: import org.apache.commons.logging.Log;
018: import org.apache.commons.logging.LogFactory;
019:
020: import javax.servlet.ServletContext;
021: import java.io.File;
022: import java.io.FileInputStream;
023: import java.io.IOException;
024: import java.io.InputStream;
025: import java.io.UnsupportedEncodingException;
026: import java.net.URL;
027: import java.net.URLClassLoader;
028: import java.net.URLDecoder;
029: import java.util.Arrays;
030: import java.util.Collection;
031: import java.util.HashSet;
032: import java.util.Set;
033: import java.util.jar.JarEntry;
034: import java.util.jar.JarInputStream;
035:
036: /**
037: * <p>ResolverUtil is used to locate classes that implement an interface or extend a given base
038: * class. It does this in two different ways. The first way is by accessing the
039: * {@link Thread#getContextClassLoader() Context ClassLoader} and attempting to discover the set
040: * of URLs that are used for classloading. The second mechanism uses the {@link ServletContext}
041: * to discover classes under {@code /WEB-INF/classes/} and jar files under {@code /WEB-INF/lib/}</p>.
042: * <p/>
043: * <p>The first mechanism is generally preferred since it can usually discover classes in more
044: * locations, but it requires that the context class loader be a subclass of {@link URLClassLoader}.
045: * Most containers use class loaders that extend URLClassloader, but not all do. Since accessing
046: * resources through the ServletContext is mandated to work in the Servlet specification this should
047: * work in all containers.</p>
048: * <p/>
049: * <p>Since scanning all classpath entries and/or jars under {@code /WEB-INF/lib/} can take a
050: * non-trivial amount of time, it is possible to filter the set of locations and packages that
051: * are examined. This is done by supplying Collections of filter patterns. The
052: * {@code locationFilters} are used to match the locations (directories, jar files, etc.) examined.
053: * The {@code packageFilters} restricts the set of classes loaded by package. In both cases a
054: * simple sub-string match is used. For example if location patterns of ["project1", project2"] are
055: * supplied, you would see the following:</p>
056: * <p/>
057: * <pre>
058: * lib/project1/dependencies/dep1.jar -> scanned
059: * lib/project3/dependencies/dep79.jar -> not scanned
060: * WEB-INF/lib/project1-web.jar -> scanned
061: * WEB-INF/classes -> not scanned
062: * lib/project2/project2-business.jar -> scanned
063: * </pre>
064: * <p/>
065: * <p>If no location filters are supplied, all discovered locations will be scanned for classes.
066: * If no package filters are supplied, all classes discovered will be checked.</p>
067: * <p/>
068: * <p>At first glance it may seem redundant to provide the class type being searched for at
069: * instantiation time, and again when invoking one of the {@code load()} methods. However,
070: * this allows for certain usages that would not otherwise be possible. For example, the
071: * following is used to find all collections that support ordering of some kind:</p>
072: * <p/>
073: * <pre>
074: * ResolverUtil<Collection> resolver = new ResolverUtil<Collection>();
075: * resolver.loadImplementationsFromContextClassloader(List.class);
076: * resolver.loadImplementationsFromContextClassloader(SortedSet.class);
077: * Set<Class<? extends Collection>> classes = resolver.getClasses();
078: * </pre>
079: *
080: * @author Tim Fennell
081: */
082: public class AutoDiscoverCarambaConfigUtil<T> {
083: /**
084: * An instance of Log to use for logging in this class.
085: */
086: private static final transient Log log = LogFactory
087: .getLog(AutoDiscoverCarambaConfigUtil.class);
088:
089: /**
090: * Set of filter strings used to match URLs to check for classes.
091: */
092: private Set<String> locationFilters = new HashSet<String>();
093:
094: /**
095: * Set of filter strings used to match package names of classes to load and check.
096: */
097: private Set<String> packageFilters = new HashSet<String>();
098:
099: /**
100: * The set of implementations being accumulated.
101: */
102: private Set<Class<? extends T>> implementations = new HashSet<Class<? extends T>>();
103:
104: /**
105: * Sets the collection of location filter patterns to use when deciding whether to check
106: * a given location for classes. Removes any "*" wildcards from the String just in case.
107: *
108: * @param patterns a set of patterns used to match locations for finding classes
109: */
110: public void setLocationFilters(Collection<String> patterns) {
111: // Try and bullet proof this a little by removing any * characters folks
112: // might have added, thinking we actually support wild-carding ;)
113: this .locationFilters.clear();
114: for (String pattern : patterns) {
115: locationFilters.add(pattern.replace("*", ""));
116: }
117: }
118:
119: /**
120: * Sets the collection of package filter patterns to use when deciding whether to load
121: * and examine classes.
122: *
123: * @param patterns a set of patterns to match against fully qualified class names
124: */
125: public void setPackageFilters(Collection<String> patterns) {
126: this .packageFilters.clear();
127: for (String pattern : patterns) {
128: packageFilters.add(pattern.replace("*", "").replace(".",
129: "/"));
130: }
131: }
132:
133: /**
134: * Provides access to the classes discovered so far. If neither of
135: * {@link #loadImplementationsFromContextClassloader(Class)} or
136: * been called, this set will be empty.
137: *
138: * @return the set of classes that have been discovered.
139: */
140: public Set<Class<? extends T>> getClasses() {
141: return implementations;
142: }
143:
144: /**
145: * <p>Attempts to locate, load and examine classes using the ServletContext to load resources
146: * from {@code /WEB-INF/}. While dependent on the Servlet API and restricted to looking for
147: * classes in {@code /WEB-INF/classes} and libraries in {@code /WEB-INF/lib}, this method
148: * should work in all servlet containers regardless of classloading implementation.</p>
149: * <p/>
150: * <p>Locations and classes are examined with respect to any filters set. Classes are
151: * stored internally and may be accessed (along with any other previously resolved classes)
152: * by calling {@link #getClasses()}.</p>
153: *
154: * @param parentType an interface or class to find implementations or subclasses of.
155: * @param context a ServletContext from which to load resources
156: */
157: public void loadImplementationsFromServletContext(
158: Class<? extends T> parentType, ServletContext context,
159: boolean pCheckLibDir) {
160: // Always scan WEB-INF/classes
161: log
162: .info("Checking for classes in /WEB-INF/classes using ServletContext resources.");
163: loadImplementationsFromServletContext(parentType,
164: "/WEB-INF/classes/", context);
165:
166: if (pCheckLibDir) {
167: // Now scan WEB-INF/lib
168: Set<String> jars = context
169: .getResourcePaths("/WEB-INF/lib/");
170: if (jars != null) {
171: for (String jarName : jars) {
172: if (matchesAny(jarName, locationFilters)) {
173: // log.info("Checking web application library '", jarName,
174: // "' for instances of ", parentType.getName());
175:
176: loadImplementationsInJar(parentType, context
177: .getResourceAsStream(jarName), jarName);
178: }
179: }
180: }
181: }
182: }
183:
184: /**
185: * Internal method that will find any classes in the supplied sub-directory of
186: * {@code /WEB-INF/classes} and then recurse for any directories found within the
187: * current directory.
188: *
189: * @param parentType an interface or class to find implementations or subclasses of.
190: * @param context a ServletContext from which to load resources
191: * @param path the path within /WEB-INF/classes to be checked
192: */
193: private void loadImplementationsFromServletContext(
194: Class<? extends T> parentType, String path,
195: ServletContext context) {
196: Set<String> paths = context.getResourcePaths(path);
197: if (paths != null) {
198: for (String subPath : paths) {
199: // Recurse for directories
200: if (subPath.endsWith("/")) {
201: loadImplementationsFromServletContext(parentType,
202: subPath, context);
203: } else if (subPath.endsWith(".class")) {
204: addIfAssignableTo(parentType, subPath.replace(
205: "/WEB-INF/classes/", ""));
206: }
207: }
208: }
209: }
210:
211: /**
212: * <p>Locates all implementations of an interface in the classloader being used by this thread.
213: * Scans the current classloader and all parents. Scans only in the URLs in the ClassLoaders
214: * which match the filters provided, and within those URLs only checks classes within the
215: * packages defined by the package filters provided.</p>
216: * <p/>
217: * <p>This method relies on the fact that most ClassLoaders in the wild extend the built-in
218: * {@link URLClassLoader}. This is relied upon because there is no standard way to discover
219: * the set of locations from which a ClassLoader is loading classes. The URLClassLoader
220: * exposes methods to discover this, and those are made use of to within this method.</p>
221: *
222: * @param parentType an interface or class to find implementations or subclasses of.
223: * @return true if the classloader was a subclass of {@link URLClassLoader} and was scanned,
224: * false if the classloader could not be scanned.
225: */
226: public boolean loadImplementationsFromContextClassloader(
227: Class<? extends T> parentType) {
228: ClassLoader loader = this .getClass().getClassLoader();
229:
230: // If it's not a URLClassLoader, we can't deal with it!
231: if (!(loader instanceof URLClassLoader)) {
232: // log.error("The current ClassLoader is not castable to a URLClassLoader. ClassLoader ",
233: // "is of type [", loader.getClass().getName(), "]. Cannot scan ClassLoader for ",
234: // "implementations of ", parentType.getClass().getName(), ". When this is the ",
235: // "case you *must* put your ActionBean classes in either /WEB-INF/classes ",
236: // "or in a jar in /WEB-INF/lib for Stripes to find them."
237: // );
238: return false;
239: } else {
240: Collection<URL> urls = new HashSet<URL>();
241: while (loader != null) {
242: try {
243: URLClassLoader urlLoader = (URLClassLoader) loader;
244: urls.addAll(Arrays.asList(urlLoader.getURLs()));
245: } catch (Exception e) { /* Do nothing */
246: }
247: loader = loader.getParent();
248: }
249:
250: for (URL url : urls) {
251: String path = url.getFile();
252: System.out.println("loading path: " + path);
253: try {
254: path = URLDecoder.decode(path, "UTF-8");
255: } catch (UnsupportedEncodingException e) { /* UTF-8 is a required encoding */
256: }
257: File location = new File(path);
258:
259: // Manage what happens when Resin decides to return URLs that do not
260: // match reality! For whatever reason, Resin decides to return some JAR
261: // URLs with an extra '!/' on the end of the jar file name and a file:
262: // in front even though that's the protocol spec, not the path!
263: if (!location.exists()) {
264: if (path.endsWith("!/"))
265: path = path.substring(0, path.length() - 2);
266: if (path.startsWith("file:"))
267: path = path.substring(5);
268: location = new File(path);
269: }
270:
271: // Only process the URL if it matches one of our filter strings
272: if (matchesAny(path, locationFilters)) {
273: // log.info("Checking URL '", path, "' for instances of ", parentType.getName());
274: if (location.isDirectory()) {
275: loadImplementationsInDirectory(parentType,
276: null, location);
277: } else {
278: loadImplementationsInJar(parentType, null, path);
279: }
280: }
281: }
282:
283: return true;
284: }
285: }
286:
287: /**
288: * Checks to see if one or more of the filter strings occurs within the string specified. If
289: * so, returns true. Otherwise returns false.
290: *
291: * @param text the text within which to look for the filter strings
292: * @param filters a set of substrings to look for in the text
293: */
294: private boolean matchesAny(String text, Set<String> filters) {
295: if (filters.size() == 0) {
296: return true;
297: }
298: for (String filter : filters) {
299: if (text.indexOf(filter) != -1) {
300: return true;
301: }
302: }
303: return false;
304: }
305:
306: /**
307: * Finds implementations of an interface in a physical directory on a filesystem. Examines all
308: * files within a directory - if the File object is not a directory, and ends with <i>.class</i>
309: * the file is loaded and tested to see if it is an implemenation of the interface. Operates
310: * recursively to find classes within a folder structure matching the package structure.
311: *
312: * @param parentType an interface or class to find implementations or subclasses of.
313: * @param parent the package name up to this directory in the package hierarchy. E.g. if
314: * /classes is in the classpath and we wish to examine files in /classes/org/apache then
315: * the values of <i>parent</i> would be <i>org/apache</i>
316: * @param location a File object representing a directory
317: */
318: private void loadImplementationsInDirectory(
319: Class<? extends T> parentType, String parent, File location) {
320: File[] files = location.listFiles();
321: StringBuilder builder = null;
322:
323: for (File file : files) {
324: builder = new StringBuilder(100);
325: builder.append(parent).append("/").append(file.getName());
326: String packageOrClass = (parent == null ? file.getName()
327: : builder.toString());
328:
329: if (file.isDirectory()) {
330: loadImplementationsInDirectory(parentType,
331: packageOrClass, file);
332: } else if (file.getName().endsWith(".class")) {
333: if (matchesAny(packageOrClass, packageFilters)) {
334: addIfAssignableTo(parentType, packageOrClass);
335: }
336: }
337: }
338: }
339:
340: /**
341: * Finds implementations of an interface within a jar files that contains a folder structure
342: * matching the package structure. If the File is not a JarFile or does not exist a warning
343: * will be logged, but no error will be raised. In this case an empty Set will be returned.
344: *
345: * @param parentType an interface or class to find implementations or subclasses of.
346: * @param inputStream a regular (non-jar/non-zip) input stream from which to read the
347: * jar file in question
348: * @param location the location of the jar file being examined. Used to create the input
349: * stream if the input stream is null, and to log appropriate error messages
350: */
351: private void loadImplementationsInJar(
352: Class<? extends T> parentType, InputStream inputStream,
353: String location) {
354:
355: try {
356: JarEntry entry;
357: if (inputStream == null)
358: inputStream = new FileInputStream(location);
359: JarInputStream jarStream = new JarInputStream(inputStream);
360:
361: while ((entry = jarStream.getNextJarEntry()) != null) {
362: String name = entry.getName();
363: if (!entry.isDirectory() && name.endsWith(".class")) {
364: if (matchesAny(name, this .packageFilters)) {
365: addIfAssignableTo(parentType, name);
366: }
367: }
368: }
369: } catch (IOException ioe) {
370: // log.error("Could not search jar file '", location, "' for implementations of ",
371: // parentType.getName(), "due to an IOException: ", ioe.getMessage());
372: }
373: }
374:
375: /**
376: * Add the class designated by the fully qualified class name provided to the set of
377: * resolved classes if and only if it extends/implements the parent type supplied.
378: *
379: * @param parentType the interface or class to add implementations or subclasses of.
380: * @param fqn the fully qualified name of a class
381: */
382: private void addIfAssignableTo(Class<? extends T> parentType,
383: String fqn) {
384: try {
385: // log.trace("Checking to see if class '", fqn, "' implements ", parentType.getName());
386: ClassLoader loader = this .getClass().getClassLoader();
387: String externalName = fqn.substring(0, fqn.indexOf('.'))
388: .replace('/', '.');
389:
390: Class type = loader.loadClass(externalName);
391: if (parentType.isAssignableFrom(type)) {
392: implementations.add((Class<T>) type);
393: }
394: } catch (Throwable t) {
395: // log.warn("Could not examine class '", fqn, "'", " due to a ",
396: // t.getClass().getName(), " with message: ", t.getMessage());
397: }
398: }
399: }
|