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