001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.util;
018    
019    import java.io.File;
020    import java.io.FileInputStream;
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.lang.annotation.Annotation;
024    import java.lang.reflect.Method;
025    import java.net.URL;
026    import java.net.URLConnection;
027    import java.net.URLDecoder;
028    import java.util.Arrays;
029    import java.util.Enumeration;
030    import java.util.HashSet;
031    import java.util.Set;
032    import java.util.jar.JarEntry;
033    import java.util.jar.JarInputStream;
034    
035    import org.apache.commons.logging.Log;
036    import org.apache.commons.logging.LogFactory;
037    
038    /**
039     * <p>
040     * ResolverUtil is used to locate classes that are available in the/a class path
041     * and meet arbitrary conditions. The two most common conditions are that a
042     * class implements/extends another class, or that is it annotated with a
043     * specific annotation. However, through the use of the {@link Test} class it is
044     * possible to search using arbitrary conditions.
045     * </p>
046     * <p/>
047     * <p>
048     * A ClassLoader is used to locate all locations (directories and jar files) in
049     * the class path that contain classes within certain packages, and then to load
050     * those classes and check them. By default the ClassLoader returned by
051     * {@code Thread.currentThread().getContextClassLoader()} is used, but this can
052     * be overridden by calling {@link #setClassLoaders(Set)} prior to
053     * invoking any of the {@code find()} methods.
054     * </p>
055     * <p/>
056     * <p>
057     * General searches are initiated by calling the
058     * {@link #find(ResolverUtil.Test, String)} ()} method and supplying a package
059     * name and a Test instance. This will cause the named package <b>and all
060     * sub-packages</b> to be scanned for classes that meet the test. There are
061     * also utility methods for the common use cases of scanning multiple packages
062     * for extensions of particular classes, or classes annotated with a specific
063     * annotation.
064     * </p>
065     * <p/>
066     * <p>
067     * The standard usage pattern for the ResolverUtil class is as follows:
068     * </p>
069     * <p/>
070     * <pre>
071     * resolverUtil&lt;ActionBean&gt; resolver = new ResolverUtil&lt;ActionBean&gt;();
072     * resolver.findImplementation(ActionBean.class, pkg1, pkg2);
073     * resolver.find(new CustomTest(), pkg1);
074     * resolver.find(new CustomTest(), pkg2);
075     * collection&lt;ActionBean&gt; beans = resolver.getClasses();
076     * </pre>
077     *
078     * @author Tim Fennell
079     */
080    public class ResolverUtil<T> {
081        protected static final transient Log LOG = LogFactory.getLog(ResolverUtil.class);
082    
083        /**
084         * A simple interface that specifies how to test classes to determine if
085         * they are to be included in the results produced by the ResolverUtil.
086         */
087        public static interface Test {
088            /**
089             * Will be called repeatedly with candidate classes. Must return True if
090             * a class is to be included in the results, false otherwise.
091             */
092            boolean matches(Class type);
093        }
094    
095        /**
096         * A Test that checks to see if each class is assignable to the provided
097         * class. Note that this test will match the parent type itself if it is
098         * presented for matching.
099         */
100        public static class IsA implements Test {
101            private Class parent;
102    
103            /**
104             * Constructs an IsA test using the supplied Class as the parent
105             * class/interface.
106             */
107            public IsA(Class parentType) {
108                this.parent = parentType;
109            }
110    
111            /**
112             * Returns true if type is assignable to the parent type supplied in the
113             * constructor.
114             */
115            public boolean matches(Class type) {
116                return type != null && parent.isAssignableFrom(type);
117            }
118    
119            @Override
120            public String toString() {
121                return "is assignable to " + parent.getSimpleName();
122            }
123        }
124    
125        /**
126         * A Test that checks to see if each class is annotated with a specific
127         * annotation. If it is, then the test returns true, otherwise false.
128         */
129        public static class AnnotatedWith implements Test {
130            private Class<? extends Annotation> annotation;
131    
132            /**
133             * Constructs an AnnotatedWith test for the specified annotation type.
134             */
135            public AnnotatedWith(Class<? extends Annotation> annotation) {
136                this.annotation = annotation;
137            }
138    
139            /**
140             * Returns true if the type is annotated with the class provided to the
141             * constructor.
142             */
143            public boolean matches(Class type) {
144                return type != null && type.isAnnotationPresent(annotation);
145            }
146    
147            @Override
148            public String toString() {
149                return "annotated with @" + annotation.getSimpleName();
150            }
151        }
152    
153        /**
154         * The set of matches being accumulated.
155         */
156        private Set<Class<? extends T>> matches = new HashSet<Class<? extends T>>();
157    
158        /**
159         * The ClassLoader to use when looking for classes. If null then the
160         * ClassLoader returned by Thread.currentThread().getContextClassLoader()
161         * will be used.
162         */
163        private Set<ClassLoader> classLoaders;
164    
165        /**
166         * Provides access to the classes discovered so far. If no calls have been
167         * made to any of the {@code find()} methods, this set will be empty.
168         *
169         * @return the set of classes that have been discovered.
170         */
171        public Set<Class<? extends T>> getClasses() {
172            return matches;
173        }
174    
175    
176        /**
177         * Returns the classloaders that will be used for scanning for classes. If no
178         * explicit ClassLoader has been set by the calling, the context class
179         * loader will and the one that has loaded this class ResolverUtil be used.
180         *
181         * @return the ClassLoader instances that will be used to scan for classes
182         */
183        public Set<ClassLoader> getClassLoaders() {
184            if (classLoaders == null) {
185                classLoaders = new HashSet<ClassLoader>();
186                classLoaders.add(Thread.currentThread().getContextClassLoader());
187                classLoaders.add(ResolverUtil.class.getClassLoader());
188            }
189            return classLoaders;
190        }
191    
192        /**
193         * Sets the ClassLoader instances that should be used when scanning for
194         * classes. If none is set then the context classloader will be used.
195         *
196         * @param classLoaders a ClassLoader to use when scanning for classes
197         */
198        public void setClassLoaders(Set<ClassLoader> classLoaders) {
199            this.classLoaders = classLoaders;
200        }
201    
202        /**
203         * Attempts to discover classes that are assignable to the type provided. In
204         * the case that an interface is provided this method will collect
205         * implementations. In the case of a non-interface class, subclasses will be
206         * collected. Accumulated classes can be accessed by calling
207         * {@link #getClasses()}.
208         *
209         * @param parent       the class of interface to find subclasses or
210         *                     implementations of
211         * @param packageNames one or more package names to scan (including
212         *                     subpackages) for classes
213         */
214        public void findImplementations(Class parent, String... packageNames) {
215            if (packageNames == null) {
216                return;
217            }
218    
219            if (LOG.isDebugEnabled()) {
220                LOG.debug("Searching for implementations of " + parent.getName() + " in packages: " + Arrays
221                    .asList(packageNames));
222            }
223    
224            Test test = new IsA(parent);
225            for (String pkg : packageNames) {
226                find(test, pkg);
227            }
228    
229            if (LOG.isDebugEnabled()) {
230                LOG.debug("Found: " + getClasses());
231            }
232        }
233    
234        /**
235         * Attempts to discover classes that are annotated with to the annotation.
236         * Accumulated classes can be accessed by calling {@link #getClasses()}.
237         *
238         * @param annotation   the annotation that should be present on matching
239         *                     classes
240         * @param packageNames one or more package names to scan (including
241         *                     subpackages) for classes
242         */
243        public void findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
244            if (packageNames == null) {
245                return;
246            }
247    
248            if (LOG.isDebugEnabled()) {
249                LOG.debug("Searching for annotations of " + annotation.getName() + " in packages: " + Arrays
250                    .asList(packageNames));
251            }
252    
253            Test test = new AnnotatedWith(annotation);
254            for (String pkg : packageNames) {
255                find(test, pkg);
256            }
257    
258            if (LOG.isDebugEnabled()) {
259                LOG.debug("Found: " + getClasses());
260            }
261        }
262    
263        /**
264         * Scans for classes starting at the package provided and descending into
265         * subpackages. Each class is offered up to the Test as it is discovered,
266         * and if the Test returns true the class is retained. Accumulated classes
267         * can be fetched by calling {@link #getClasses()}.
268         *
269         * @param test        an instance of {@link Test} that will be used to filter
270         *                    classes
271         * @param packageName the name of the package from which to start scanning
272         *                    for classes, e.g. {@code net.sourceforge.stripes}
273         */
274        public void find(Test test, String packageName) {
275            packageName = packageName.replace('.', '/');
276    
277            Set<ClassLoader> set = getClassLoaders();
278    
279            ClassLoader osgiClassLoader = getOsgiClassLoader(set);
280    
281            if (osgiClassLoader != null) {
282                // if we have an osgi bundle loader use this one only
283                LOG.debug("Using only osgi bundle classloader");
284                find(test, packageName, osgiClassLoader, true);
285            } else {
286                LOG.debug("Using only regular classloaders");
287                for (ClassLoader classLoader : set) {
288                    if (!isOsgiClassloader(classLoader)) {
289                        find(test, packageName, classLoader, false);
290                    }
291                }
292            }
293        }
294    
295        /**
296         * Gets the osgi classloader if any in the given set
297         */
298        private static ClassLoader getOsgiClassLoader(Set<ClassLoader> set) {
299            for (ClassLoader loader : set) {
300                if (isOsgiClassloader(loader)) {
301                    return loader;
302                }
303            }
304            return null;
305        }
306    
307        /**
308         * Is it an osgi classloader
309         */
310        private static boolean isOsgiClassloader(ClassLoader loader) {
311            try {
312                Method mth = loader.getClass().getMethod("getBundle", new Class[]{});
313                if (mth != null) {
314                    return true;
315                }
316            } catch (NoSuchMethodException e) {
317                // ignore its not an osgi loader
318            }
319            return false;
320        }
321    
322        /**
323         * Tries to find the reosurce in the package using the class loader.
324         * <p/>
325         * Will handle both plain URL based classloaders and OSGi bundle loaders.
326         *
327         * @param test what to find
328         * @param packageName the package to search in
329         * @param loader the class loader
330         * @param osgi true if its a osgi bundle loader, false if regular classloader
331         */
332        protected void find(Test test, String packageName, ClassLoader loader, boolean osgi) {
333            if (LOG.isTraceEnabled()) {
334                LOG.trace("Searching for: " + test + " in package: " + packageName + " using classloader: "
335                        + loader.getClass().getName() + " osgi bundle classloader: " + osgi);
336            }
337    
338            if (osgi) {
339                try {
340                    Method mth = loader.getClass().getMethod("getBundle", new Class[]{});
341                    if (mth != null) {
342                        if (LOG.isDebugEnabled()) {
343                            LOG.debug("Loading from osgi buindle using classloader: " + loader);
344                        }
345                        loadImplementationsInBundle(test, packageName, loader, mth);
346                        return;
347                    }
348                } catch (NoSuchMethodException e) {
349                    LOG.warn("It's not an osgi bundle classloader: " + loader);
350                    return;
351                }
352            }
353    
354            Enumeration<URL> urls;
355            try {
356                urls = getResources(loader, packageName);
357                if (!urls.hasMoreElements()) {
358                    LOG.trace("No URLs returned by classloader");
359                }
360            } catch (IOException ioe) {
361                LOG.warn("Could not read package: " + packageName, ioe);
362                return;
363            }
364    
365            while (urls.hasMoreElements()) {
366                URL url = null;
367                try {
368                    url = urls.nextElement();
369                    if (LOG.isTraceEnabled()) {
370                        LOG.trace("URL from classloader: " + url);
371                    }
372    
373                    String urlPath = url.getFile();
374                    urlPath = URLDecoder.decode(urlPath, "UTF-8");
375                    if (LOG.isTraceEnabled()) {
376                        LOG.trace("Decoded urlPath: " + urlPath);
377                    }
378    
379                    // If it's a file in a directory, trim the stupid file: spec
380                    if (urlPath.startsWith("file:")) {
381                        urlPath = urlPath.substring(5);
382                    }
383    
384                    // osgi bundles should be skipped
385                    if (urlPath.startsWith("bundle:")) {
386                        LOG.trace("It's a virtual osgi bundle, skipping");
387                        continue;
388                    }
389    
390                    // Else it's in a JAR, grab the path to the jar
391                    if (urlPath.indexOf('!') > 0) {
392                        urlPath = urlPath.substring(0, urlPath.indexOf('!'));
393                    }
394    
395                    if (LOG.isTraceEnabled()) {
396                        LOG.trace("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
397                    }
398    
399                    File file = new File(urlPath);
400                    if (file.isDirectory()) {
401                        if (LOG.isDebugEnabled()) {
402                            LOG.debug("Loading from directory: " + file);
403                        }
404                        loadImplementationsInDirectory(test, packageName, file);
405                    } else {
406                        InputStream stream;
407                        if (urlPath.startsWith("http:")) {
408                            // load resources using http such as java webstart
409                            LOG.debug("The current jar is accessed via http");
410                            URL urlStream = new URL(urlPath);
411                            URLConnection con = urlStream.openConnection();
412                            // disable cache mainly to avoid jar file locking on Windows
413                            con.setUseCaches(false);
414                            stream = con.getInputStream();
415                        } else {
416                            stream = new FileInputStream(file);
417                        }
418    
419                        if (LOG.isDebugEnabled()) {
420                            LOG.debug("Loading from jar: " + file);
421                        }
422                        loadImplementationsInJar(test, packageName, stream, urlPath);
423                    }
424                } catch (IOException ioe) {
425                    LOG.warn("Could not read entries in url: " + url, ioe);
426                }
427            }
428        }
429    
430        /**
431         * Strategy to get the resources by the given classloader.
432         * <p/>
433         * Notice that in WebSphere platforms there is a {@link org.apache.camel.util.WebSphereResolverUtil}
434         * to take care of WebSphere's odditiy of resource loading.
435         *
436         * @param loader  the classloader
437         * @param packageName   the packagename for the package to load
438         * @return  URL's for the given package
439         * @throws IOException is thrown by the classloader
440         */
441        protected Enumeration<URL> getResources(ClassLoader loader, String packageName) throws IOException {
442            if (LOG.isTraceEnabled()) {
443                LOG.trace("Getting resource URL for package: " + packageName + " with classloader: " + loader);
444            }
445            return loader.getResources(packageName);
446        }
447    
448        private void loadImplementationsInBundle(Test test, String packageName, ClassLoader loader, Method mth) {
449            // Use an inner class to avoid a NoClassDefFoundError when used in a non-osgi env
450            Set<String> urls = OsgiUtil.getImplementationsInBundle(test, packageName, loader, mth);
451            if (urls != null) {
452                for (String url : urls) {
453                    // substring to avoid leading slashes
454                    addIfMatching(test, url);
455                }
456            }
457        }
458    
459        private static final class OsgiUtil {
460            private OsgiUtil() {
461                // Helper class
462            }
463            static Set<String> getImplementationsInBundle(Test test, String packageName, ClassLoader loader, Method mth) {
464                try {
465                    org.osgi.framework.Bundle bundle = (org.osgi.framework.Bundle) mth.invoke(loader);
466                    org.osgi.framework.Bundle[] bundles = bundle.getBundleContext().getBundles();
467                    Set<String> urls = new HashSet<String>();
468                    for (org.osgi.framework.Bundle bd : bundles) {
469                        if (LOG.isTraceEnabled()) {
470                            LOG.trace("Searching in bundle:" + bd);
471                        }
472                        Enumeration<URL> paths = bd.findEntries("/" + packageName, "*.class", true);
473                        while (paths != null && paths.hasMoreElements()) {
474                            URL path = paths.nextElement();
475                            urls.add(path.getPath().substring(1));
476                        }
477                    }
478                    return urls;
479                } catch (Throwable t) {
480                    LOG.error("Could not search osgi bundles for classes matching criteria: " + test
481                              + "due to an Exception: " + t.getMessage());
482                    return null;
483                }
484            }
485        }
486    
487    
488        /**
489         * Finds matches in a physical directory on a filesystem. Examines all files
490         * within a directory - if the File object is not a directory, and ends with
491         * <i>.class</i> the file is loaded and tested to see if it is acceptable
492         * according to the Test. Operates recursively to find classes within a
493         * folder structure matching the package structure.
494         *
495         * @param test     a Test used to filter the classes that are discovered
496         * @param parent   the package name up to this directory in the package
497         *                 hierarchy. E.g. if /classes is in the classpath and we wish to
498         *                 examine files in /classes/org/apache then the values of
499         *                 <i>parent</i> would be <i>org/apache</i>
500         * @param location a File object representing a directory
501         */
502        private void loadImplementationsInDirectory(Test test, String parent, File location) {
503            File[] files = location.listFiles();
504            StringBuilder builder = null;
505    
506            for (File file : files) {
507                builder = new StringBuilder(100);
508                String name = file.getName();
509                if (name != null) {
510                    name = name.trim();
511                    builder.append(parent).append("/").append(name);
512                    String packageOrClass = parent == null ? name : builder.toString();
513    
514                    if (file.isDirectory()) {
515                        loadImplementationsInDirectory(test, packageOrClass, file);
516                    } else if (name.endsWith(".class")) {
517                        addIfMatching(test, packageOrClass);
518                    }
519                }
520            }
521        }
522    
523        /**
524         * Finds matching classes within a jar files that contains a folder
525         * structure matching the package structure. If the File is not a JarFile or
526         * does not exist a warning will be logged, but no error will be raised.
527         *
528         * @param test    a Test used to filter the classes that are discovered
529         * @param parent  the parent package under which classes must be in order to
530         *                be considered
531         * @param jarfile the jar file to be examined for classes
532         * @param stream  the inputstream of the jar file to be examined for classes
533         * @param urlPath the url of the jar file to be examined for classes
534         */
535        private void loadImplementationsInJar(Test test, String parent, InputStream stream, String urlPath) {
536            JarInputStream jarStream = null;
537            try {
538                jarStream = new JarInputStream(stream);
539    
540                JarEntry entry;
541                while ((entry = jarStream.getNextJarEntry()) != null) {
542                    String name = entry.getName();
543                    if (name != null) {
544                        name = name.trim();
545                        if (!entry.isDirectory() && name.startsWith(parent) && name.endsWith(".class")) {
546                            addIfMatching(test, name);
547                        }
548                    }
549                }
550            } catch (IOException ioe) {
551                LOG.error("Could not search jar file '" + urlPath + "' for classes matching criteria: " + test
552                    + " due to an IOException: " + ioe.getMessage(), ioe);
553            } finally {
554                ObjectHelper.close(jarStream, urlPath, LOG);
555            }
556        }
557    
558        /**
559         * Add the class designated by the fully qualified class name provided to
560         * the set of resolved classes if and only if it is approved by the Test
561         * supplied.
562         *
563         * @param test the test used to determine if the class matches
564         * @param fqn  the fully qualified name of a class
565         */
566        protected void addIfMatching(Test test, String fqn) {
567            try {
568                String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
569                Set<ClassLoader> set = getClassLoaders();
570                boolean found = false;
571                for (ClassLoader classLoader : set) {
572                    if (LOG.isTraceEnabled()) {
573                        LOG.trace("Testing for class " + externalName + " matches criteria [" + test + "]");
574                    }
575                    try {
576                        Class type = classLoader.loadClass(externalName);
577                        if (test.matches(type)) {
578                            if (LOG.isTraceEnabled()) {
579                                LOG.trace("Found class: " + type + " in classloader: " + classLoader);
580                            }
581                            matches.add((Class<T>)type);
582                        }
583                        found = true;
584                        break;
585                    } catch (ClassNotFoundException e) {
586                        LOG.debug("Could not find class '" + fqn + "' in classloader: " + classLoader
587                            + ". Reason: " + e, e);
588                    } catch (NoClassDefFoundError e) {
589                        LOG.debug("Could not find the class defintion '" + fqn + "' in classloader: " + classLoader
590                                  + ". Reason: " + e, e);
591                    }
592                }
593                if (!found) {
594                    LOG.warn("Could not find class '" + fqn + "' in any classloaders: " + set);
595                }
596            } catch (Throwable t) {
597                LOG.warn("Could not examine class '" + fqn + "' due to a " + t.getClass().getName()
598                    + " with message: " + t.getMessage(), t);
599            }
600        }
601    
602    }