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            private boolean checkMetaAnnotations;
132    
133            /**
134             * Constructs an AnnotatedWith test for the specified annotation type.
135             */
136            public AnnotatedWith(Class<? extends Annotation> annotation) {
137                this(annotation, false);
138            }
139    
140            /**
141             * Constructs an AnnotatedWith test for the specified annotation type.
142             */
143            public AnnotatedWith(Class<? extends Annotation> annotation, boolean checkMetaAnnotations) {
144                this.annotation = annotation;
145                this.checkMetaAnnotations = checkMetaAnnotations;
146            }
147    
148            /**
149             * Returns true if the type is annotated with the class provided to the
150             * constructor.
151             */
152            public boolean matches(Class type) {
153                return type != null && ObjectHelper.hasAnnotation(type, annotation, checkMetaAnnotations);
154            }
155    
156            @Override
157            public String toString() {
158                return "annotated with @" + annotation.getSimpleName();
159            }
160        }
161    
162        /**
163         * The set of matches being accumulated.
164         */
165        private Set<Class<? extends T>> matches = new HashSet<Class<? extends T>>();
166    
167        /**
168         * The ClassLoader to use when looking for classes. If null then the
169         * ClassLoader returned by Thread.currentThread().getContextClassLoader()
170         * will be used.
171         */
172        private Set<ClassLoader> classLoaders;
173    
174        /**
175         * Provides access to the classes discovered so far. If no calls have been
176         * made to any of the {@code find()} methods, this set will be empty.
177         *
178         * @return the set of classes that have been discovered.
179         */
180        public Set<Class<? extends T>> getClasses() {
181            return matches;
182        }
183    
184    
185        /**
186         * Returns the classloaders that will be used for scanning for classes. If no
187         * explicit ClassLoader has been set by the calling, the context class
188         * loader will and the one that has loaded this class ResolverUtil be used.
189         *
190         * @return the ClassLoader instances that will be used to scan for classes
191         */
192        public Set<ClassLoader> getClassLoaders() {
193            if (classLoaders == null) {
194                classLoaders = new HashSet<ClassLoader>();
195                classLoaders.add(Thread.currentThread().getContextClassLoader());
196                classLoaders.add(ResolverUtil.class.getClassLoader());
197            }
198            return classLoaders;
199        }
200    
201        /**
202         * Sets the ClassLoader instances that should be used when scanning for
203         * classes. If none is set then the context classloader will be used.
204         *
205         * @param classLoaders a ClassLoader to use when scanning for classes
206         */
207        public void setClassLoaders(Set<ClassLoader> classLoaders) {
208            this.classLoaders = classLoaders;
209        }
210    
211        /**
212         * Attempts to discover classes that are assignable to the type provided. In
213         * the case that an interface is provided this method will collect
214         * implementations. In the case of a non-interface class, subclasses will be
215         * collected. Accumulated classes can be accessed by calling
216         * {@link #getClasses()}.
217         *
218         * @param parent       the class of interface to find subclasses or
219         *                     implementations of
220         * @param packageNames one or more package names to scan (including
221         *                     subpackages) for classes
222         */
223        public void findImplementations(Class parent, String... packageNames) {
224            if (packageNames == null) {
225                return;
226            }
227    
228            if (LOG.isDebugEnabled()) {
229                LOG.debug("Searching for implementations of " + parent.getName() + " in packages: " + Arrays
230                    .asList(packageNames));
231            }
232    
233            Test test = new IsA(parent);
234            for (String pkg : packageNames) {
235                find(test, pkg);
236            }
237    
238            if (LOG.isDebugEnabled()) {
239                LOG.debug("Found: " + getClasses());
240            }
241        }
242    
243        /**
244         * Attempts to discover classes that are annotated with to the annotation.
245         * Accumulated classes can be accessed by calling {@link #getClasses()}.
246         *
247         * @param annotation   the annotation that should be present on matching
248         *                     classes
249         * @param packageNames one or more package names to scan (including
250         *                     subpackages) for classes
251         */
252        public void findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
253            if (packageNames == null) {
254                return;
255            }
256    
257            if (LOG.isDebugEnabled()) {
258                LOG.debug("Searching for annotations of " + annotation.getName() + " in packages: " + Arrays
259                    .asList(packageNames));
260            }
261    
262            Test test = new AnnotatedWith(annotation, true);
263            for (String pkg : packageNames) {
264                find(test, pkg);
265            }
266    
267            if (LOG.isDebugEnabled()) {
268                LOG.debug("Found: " + getClasses());
269            }
270        }
271    
272        /**
273         * Scans for classes starting at the package provided and descending into
274         * subpackages. Each class is offered up to the Test as it is discovered,
275         * and if the Test returns true the class is retained. Accumulated classes
276         * can be fetched by calling {@link #getClasses()}.
277         *
278         * @param test        an instance of {@link Test} that will be used to filter
279         *                    classes
280         * @param packageName the name of the package from which to start scanning
281         *                    for classes, e.g. {@code net.sourceforge.stripes}
282         */
283        public void find(Test test, String packageName) {
284            packageName = packageName.replace('.', '/');
285    
286            Set<ClassLoader> set = getClassLoaders();
287    
288            ClassLoader osgiClassLoader = getOsgiClassLoader(set);
289    
290            if (osgiClassLoader != null) {
291                // if we have an osgi bundle loader use this one only
292                LOG.debug("Using only osgi bundle classloader");
293                find(test, packageName, osgiClassLoader, true);
294            } else {
295                LOG.debug("Using only regular classloaders");
296                for (ClassLoader classLoader : set) {
297                    if (!isOsgiClassloader(classLoader)) {
298                        find(test, packageName, classLoader, false);
299                    }
300                }
301            }
302        }
303    
304        /**
305         * Gets the osgi classloader if any in the given set
306         */
307        private static ClassLoader getOsgiClassLoader(Set<ClassLoader> set) {
308            for (ClassLoader loader : set) {
309                if (isOsgiClassloader(loader)) {
310                    return loader;
311                }
312            }
313            return null;
314        }
315    
316        /**
317         * Is it an osgi classloader
318         */
319        private static boolean isOsgiClassloader(ClassLoader loader) {
320            try {
321                Method mth = loader.getClass().getMethod("getBundle", new Class[]{});
322                if (mth != null) {
323                    return true;
324                }
325            } catch (NoSuchMethodException e) {
326                // ignore its not an osgi loader
327            }
328            return false;
329        }
330    
331        /**
332         * Tries to find the reosurce in the package using the class loader.
333         * <p/>
334         * Will handle both plain URL based classloaders and OSGi bundle loaders.
335         *
336         * @param test what to find
337         * @param packageName the package to search in
338         * @param loader the class loader
339         * @param osgi true if its a osgi bundle loader, false if regular classloader
340         */
341        protected void find(Test test, String packageName, ClassLoader loader, boolean osgi) {
342            if (LOG.isTraceEnabled()) {
343                LOG.trace("Searching for: " + test + " in package: " + packageName + " using classloader: "
344                        + loader.getClass().getName() + " osgi bundle classloader: " + osgi);
345            }
346    
347            if (osgi) {
348                try {
349                    Method mth = loader.getClass().getMethod("getBundle", new Class[]{});
350                    if (mth != null) {
351                        if (LOG.isDebugEnabled()) {
352                            LOG.debug("Loading from osgi buindle using classloader: " + loader);
353                        }
354                        loadImplementationsInBundle(test, packageName, loader, mth);
355                        return;
356                    }
357                } catch (NoSuchMethodException e) {
358                    LOG.warn("It's not an osgi bundle classloader: " + loader);
359                    return;
360                }
361            }
362    
363            Enumeration<URL> urls;
364            try {
365                urls = getResources(loader, packageName);
366                if (!urls.hasMoreElements()) {
367                    LOG.trace("No URLs returned by classloader");
368                }
369            } catch (IOException ioe) {
370                LOG.warn("Could not read package: " + packageName, ioe);
371                return;
372            }
373    
374            while (urls.hasMoreElements()) {
375                URL url = null;
376                try {
377                    url = urls.nextElement();
378                    if (LOG.isTraceEnabled()) {
379                        LOG.trace("URL from classloader: " + url);
380                    }
381    
382                    String urlPath = url.getFile();
383                    urlPath = URLDecoder.decode(urlPath, "UTF-8");
384                    if (LOG.isTraceEnabled()) {
385                        LOG.trace("Decoded urlPath: " + urlPath);
386                    }
387    
388                    // If it's a file in a directory, trim the stupid file: spec
389                    if (urlPath.startsWith("file:")) {
390                        urlPath = urlPath.substring(5);
391                    }
392    
393                    // osgi bundles should be skipped
394                    if (url.toString().startsWith("bundle:") || urlPath.startsWith("bundle:")) {
395                        LOG.trace("It's a virtual osgi bundle, skipping");
396                        continue;
397                    }
398    
399                    // Else it's in a JAR, grab the path to the jar
400                    if (urlPath.indexOf('!') > 0) {
401                        urlPath = urlPath.substring(0, urlPath.indexOf('!'));
402                    }
403    
404                    if (LOG.isTraceEnabled()) {
405                        LOG.trace("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
406                    }
407    
408                    File file = new File(urlPath);
409                    if (file.isDirectory()) {
410                        if (LOG.isDebugEnabled()) {
411                            LOG.debug("Loading from directory: " + file);
412                        }
413                        loadImplementationsInDirectory(test, packageName, file);
414                    } else {
415                        InputStream stream;
416                        if (urlPath.startsWith("http:")) {
417                            // load resources using http such as java webstart
418                            LOG.debug("The current jar is accessed via http");
419                            URL urlStream = new URL(urlPath);
420                            URLConnection con = urlStream.openConnection();
421                            // disable cache mainly to avoid jar file locking on Windows
422                            con.setUseCaches(false);
423                            stream = con.getInputStream();
424                        } else {
425                            stream = new FileInputStream(file);
426                        }
427    
428                        if (LOG.isDebugEnabled()) {
429                            LOG.debug("Loading from jar: " + file);
430                        }
431                        loadImplementationsInJar(test, packageName, stream, urlPath);
432                    }
433                } catch (IOException ioe) {
434                    LOG.warn("Could not read entries in url: " + url, ioe);
435                }
436            }
437        }
438    
439        /**
440         * Strategy to get the resources by the given classloader.
441         * <p/>
442         * Notice that in WebSphere platforms there is a {@link org.apache.camel.util.WebSphereResolverUtil}
443         * to take care of WebSphere's odditiy of resource loading.
444         *
445         * @param loader  the classloader
446         * @param packageName   the packagename for the package to load
447         * @return  URL's for the given package
448         * @throws IOException is thrown by the classloader
449         */
450        protected Enumeration<URL> getResources(ClassLoader loader, String packageName) throws IOException {
451            if (LOG.isTraceEnabled()) {
452                LOG.trace("Getting resource URL for package: " + packageName + " with classloader: " + loader);
453            }
454            return loader.getResources(packageName);
455        }
456    
457        private void loadImplementationsInBundle(Test test, String packageName, ClassLoader loader, Method mth) {
458            // Use an inner class to avoid a NoClassDefFoundError when used in a non-osgi env
459            Set<String> urls = OsgiUtil.getImplementationsInBundle(test, packageName, loader, mth);
460            if (urls != null) {
461                for (String url : urls) {
462                    // substring to avoid leading slashes
463                    addIfMatching(test, url);
464                }
465            }
466        }
467    
468        private static final class OsgiUtil {
469            private OsgiUtil() {
470                // Helper class
471            }
472            static Set<String> getImplementationsInBundle(Test test, String packageName, ClassLoader loader, Method mth) {
473                try {
474                    org.osgi.framework.Bundle bundle = (org.osgi.framework.Bundle) mth.invoke(loader);
475                    org.osgi.framework.Bundle[] bundles = bundle.getBundleContext().getBundles();
476                    Set<String> urls = new HashSet<String>();
477                    for (org.osgi.framework.Bundle bd : bundles) {
478                        if (LOG.isTraceEnabled()) {
479                            LOG.trace("Searching in bundle:" + bd);
480                        }
481                        Enumeration<URL> paths = bd.findEntries("/" + packageName, "*.class", true);
482                        while (paths != null && paths.hasMoreElements()) {
483                            URL path = paths.nextElement();                        
484                            urls.add(path.getPath().substring(1));
485                        }
486                    }
487                    return urls;
488                } catch (Throwable t) {
489                    LOG.error("Could not search osgi bundles for classes matching criteria: " + test
490                              + "due to an Exception: " + t.getMessage());
491                    return null;
492                }
493            }
494        }
495    
496    
497        /**
498         * Finds matches in a physical directory on a filesystem. Examines all files
499         * within a directory - if the File object is not a directory, and ends with
500         * <i>.class</i> the file is loaded and tested to see if it is acceptable
501         * according to the Test. Operates recursively to find classes within a
502         * folder structure matching the package structure.
503         *
504         * @param test     a Test used to filter the classes that are discovered
505         * @param parent   the package name up to this directory in the package
506         *                 hierarchy. E.g. if /classes is in the classpath and we wish to
507         *                 examine files in /classes/org/apache then the values of
508         *                 <i>parent</i> would be <i>org/apache</i>
509         * @param location a File object representing a directory
510         */
511        private void loadImplementationsInDirectory(Test test, String parent, File location) {
512            File[] files = location.listFiles();
513            StringBuilder builder = null;
514    
515            for (File file : files) {
516                builder = new StringBuilder(100);
517                String name = file.getName();
518                if (name != null) {
519                    name = name.trim();
520                    builder.append(parent).append("/").append(name);
521                    String packageOrClass = parent == null ? name : builder.toString();
522    
523                    if (file.isDirectory()) {
524                        loadImplementationsInDirectory(test, packageOrClass, file);
525                    } else if (name.endsWith(".class")) {
526                        addIfMatching(test, packageOrClass);
527                    }
528                }
529            }
530        }
531    
532        /**
533         * Finds matching classes within a jar files that contains a folder
534         * structure matching the package structure. If the File is not a JarFile or
535         * does not exist a warning will be logged, but no error will be raised.
536         *
537         * @param test    a Test used to filter the classes that are discovered
538         * @param parent  the parent package under which classes must be in order to
539         *                be considered
540         * @param jarfile the jar file to be examined for classes
541         * @param stream  the inputstream of the jar file to be examined for classes
542         * @param urlPath the url of the jar file to be examined for classes
543         */
544        private void loadImplementationsInJar(Test test, String parent, InputStream stream, String urlPath) {
545            JarInputStream jarStream = null;
546            try {
547                jarStream = new JarInputStream(stream);
548    
549                JarEntry entry;
550                while ((entry = jarStream.getNextJarEntry()) != null) {
551                    String name = entry.getName();
552                    if (name != null) {
553                        name = name.trim();
554                        if (!entry.isDirectory() && name.startsWith(parent) && name.endsWith(".class")) {
555                            addIfMatching(test, name);
556                        }
557                    }
558                }
559            } catch (IOException ioe) {
560                LOG.error("Could not search jar file '" + urlPath + "' for classes matching criteria: " + test
561                    + " due to an IOException: " + ioe.getMessage(), ioe);
562            } finally {
563                ObjectHelper.close(jarStream, urlPath, LOG);
564            }
565        }
566    
567        /**
568         * Add the class designated by the fully qualified class name provided to
569         * the set of resolved classes if and only if it is approved by the Test
570         * supplied.
571         *
572         * @param test the test used to determine if the class matches
573         * @param fqn  the fully qualified name of a class
574         */
575        protected void addIfMatching(Test test, String fqn) {
576            try {
577                String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
578                Set<ClassLoader> set = getClassLoaders();
579                boolean found = false;
580                for (ClassLoader classLoader : set) {
581                    if (LOG.isTraceEnabled()) {
582                        LOG.trace("Testing for class " + externalName + " matches criteria [" + test + "]");
583                    }
584                    try {
585                        Class type = classLoader.loadClass(externalName);
586                        if (test.matches(type)) {
587                            if (LOG.isTraceEnabled()) {
588                                LOG.trace("Found class: " + type + " in classloader: " + classLoader);
589                            }
590                            matches.add((Class<T>)type);
591                        }
592                        found = true;
593                        break;
594                    } catch (ClassNotFoundException e) {
595                        LOG.debug("Could not find class '" + fqn + "' in classloader: " + classLoader
596                            + ". Reason: " + e, e);
597                    } catch (NoClassDefFoundError e) {
598                        LOG.debug("Could not find the class defintion '" + fqn + "' in classloader: " + classLoader
599                                  + ". Reason: " + e, e);
600                    }
601                }
602                if (!found) {
603                    LOG.warn("Could not find class '" + fqn + "' in any classloaders: " + set);
604                }
605            } catch (Throwable t) {
606                LOG.warn("Could not examine class '" + fqn + "' due to a " + t.getClass().getName()
607                    + " with message: " + t.getMessage(), t);
608            }
609        }
610    
611    }