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                ClassLoader ccl = Thread.currentThread().getContextClassLoader();
196                if (ccl != null) {
197                    classLoaders.add(ccl);
198                }
199                classLoaders.add(ResolverUtil.class.getClassLoader());
200            }
201            return classLoaders;
202        }
203    
204        /**
205         * Sets the ClassLoader instances that should be used when scanning for
206         * classes. If none is set then the context classloader will be used.
207         *
208         * @param classLoaders a ClassLoader to use when scanning for classes
209         */
210        public void setClassLoaders(Set<ClassLoader> classLoaders) {
211            this.classLoaders = classLoaders;
212        }
213    
214        /**
215         * Attempts to discover classes that are assignable to the type provided. In
216         * the case that an interface is provided this method will collect
217         * implementations. In the case of a non-interface class, subclasses will be
218         * collected. Accumulated classes can be accessed by calling
219         * {@link #getClasses()}.
220         *
221         * @param parent       the class of interface to find subclasses or
222         *                     implementations of
223         * @param packageNames one or more package names to scan (including
224         *                     subpackages) for classes
225         */
226        public void findImplementations(Class parent, String... packageNames) {
227            if (packageNames == null) {
228                return;
229            }
230    
231            if (LOG.isDebugEnabled()) {
232                LOG.debug("Searching for implementations of " + parent.getName() + " in packages: " + Arrays
233                    .asList(packageNames));
234            }
235    
236            Test test = new IsA(parent);
237            for (String pkg : packageNames) {
238                find(test, pkg);
239            }
240    
241            if (LOG.isDebugEnabled()) {
242                LOG.debug("Found: " + getClasses());
243            }
244        }
245    
246        /**
247         * Attempts to discover classes that are annotated with to the annotation.
248         * Accumulated classes can be accessed by calling {@link #getClasses()}.
249         *
250         * @param annotation   the annotation that should be present on matching
251         *                     classes
252         * @param packageNames one or more package names to scan (including
253         *                     subpackages) for classes
254         */
255        public void findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
256            if (packageNames == null) {
257                return;
258            }
259    
260            if (LOG.isDebugEnabled()) {
261                LOG.debug("Searching for annotations of " + annotation.getName() + " in packages: " + Arrays
262                    .asList(packageNames));
263            }
264    
265            Test test = new AnnotatedWith(annotation, true);
266            for (String pkg : packageNames) {
267                find(test, pkg);
268            }
269    
270            if (LOG.isDebugEnabled()) {
271                LOG.debug("Found: " + getClasses());
272            }
273        }
274    
275        /**
276         * Scans for classes starting at the package provided and descending into
277         * subpackages. Each class is offered up to the Test as it is discovered,
278         * and if the Test returns true the class is retained. Accumulated classes
279         * can be fetched by calling {@link #getClasses()}.
280         *
281         * @param test        an instance of {@link Test} that will be used to filter
282         *                    classes
283         * @param packageName the name of the package from which to start scanning
284         *                    for classes, e.g. {@code net.sourceforge.stripes}
285         */
286        public void find(Test test, String packageName) {
287            packageName = packageName.replace('.', '/');
288    
289            Set<ClassLoader> set = getClassLoaders();
290    
291            LOG.debug("Using only regular classloaders");
292            for (ClassLoader classLoader : set) {            
293                find(test, packageName, classLoader);            
294            }
295        }
296    
297        
298    
299        /**
300         * Tries to find the reosurce in the package using the class loader.
301         * <p/>
302         * Will handle both plain URL based classloaders and OSGi bundle loaders.
303         *
304         * @param test what to find
305         * @param packageName the package to search in
306         * @param loader the class loader     
307         */
308        protected void find(Test test, String packageName, ClassLoader loader) {
309            if (LOG.isTraceEnabled()) {
310                LOG.trace("Searching for: " + test + " in package: " + packageName + " using classloader: "
311                        + loader.getClass().getName());
312            }        
313    
314            Enumeration<URL> urls;
315            try {
316                urls = getResources(loader, packageName);
317                if (!urls.hasMoreElements()) {
318                    LOG.trace("No URLs returned by classloader");
319                }
320            } catch (IOException ioe) {
321                LOG.warn("Could not read package: " + packageName, ioe);
322                return;
323            }
324    
325            while (urls.hasMoreElements()) {
326                URL url = null;
327                try {
328                    url = urls.nextElement();
329                    if (LOG.isTraceEnabled()) {
330                        LOG.trace("URL from classloader: " + url);
331                    }
332    
333                    String urlPath = url.getFile();
334                    urlPath = URLDecoder.decode(urlPath, "UTF-8");
335                    if (LOG.isTraceEnabled()) {
336                        LOG.trace("Decoded urlPath: " + urlPath);
337                    }
338    
339                    // If it's a file in a directory, trim the stupid file: spec
340                    if (urlPath.startsWith("file:")) {
341                        urlPath = urlPath.substring(5);
342                    }
343    
344                    // osgi bundles should be skipped
345                    if (url.toString().startsWith("bundle:") || urlPath.startsWith("bundle:")) {
346                        LOG.trace("It's a virtual osgi bundle, skipping");
347                        continue;
348                    }
349    
350                    // Else it's in a JAR, grab the path to the jar
351                    if (urlPath.indexOf('!') > 0) {
352                        urlPath = urlPath.substring(0, urlPath.indexOf('!'));
353                    }
354    
355                    if (LOG.isTraceEnabled()) {
356                        LOG.trace("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
357                    }
358    
359                    File file = new File(urlPath);
360                    if (file.isDirectory()) {
361                        if (LOG.isDebugEnabled()) {
362                            LOG.debug("Loading from directory: " + file);
363                        }
364                        loadImplementationsInDirectory(test, packageName, file);
365                    } else {
366                        InputStream stream;
367                        if (urlPath.startsWith("http:")) {
368                            // load resources using http such as java webstart
369                            LOG.debug("The current jar is accessed via http");
370                            URL urlStream = new URL(urlPath);
371                            URLConnection con = urlStream.openConnection();
372                            // disable cache mainly to avoid jar file locking on Windows
373                            con.setUseCaches(false);
374                            stream = con.getInputStream();
375                        } else {
376                            stream = new FileInputStream(file);
377                        }
378    
379                        if (LOG.isDebugEnabled()) {
380                            LOG.debug("Loading from jar: " + file);
381                        }
382                        loadImplementationsInJar(test, packageName, stream, urlPath);
383                    }
384                } catch (IOException ioe) {
385                    LOG.warn("Could not read entries in url: " + url, ioe);
386                }
387            }
388        }
389    
390        /**
391         * Strategy to get the resources by the given classloader.
392         * <p/>
393         * Notice that in WebSphere platforms there is a {@link org.apache.camel.util.WebSphereResolverUtil}
394         * to take care of WebSphere's odditiy of resource loading.
395         *
396         * @param loader  the classloader
397         * @param packageName   the packagename for the package to load
398         * @return  URL's for the given package
399         * @throws IOException is thrown by the classloader
400         */
401        protected Enumeration<URL> getResources(ClassLoader loader, String packageName) throws IOException {
402            if (LOG.isTraceEnabled()) {
403                LOG.trace("Getting resource URL for package: " + packageName + " with classloader: " + loader);
404            }
405            return loader.getResources(packageName);
406        }
407    
408        
409    
410    
411        /**
412         * Finds matches in a physical directory on a filesystem. Examines all files
413         * within a directory - if the File object is not a directory, and ends with
414         * <i>.class</i> the file is loaded and tested to see if it is acceptable
415         * according to the Test. Operates recursively to find classes within a
416         * folder structure matching the package structure.
417         *
418         * @param test     a Test used to filter the classes that are discovered
419         * @param parent   the package name up to this directory in the package
420         *                 hierarchy. E.g. if /classes is in the classpath and we wish to
421         *                 examine files in /classes/org/apache then the values of
422         *                 <i>parent</i> would be <i>org/apache</i>
423         * @param location a File object representing a directory
424         */
425        private void loadImplementationsInDirectory(Test test, String parent, File location) {
426            File[] files = location.listFiles();
427            StringBuilder builder = null;
428    
429            for (File file : files) {
430                builder = new StringBuilder(100);
431                String name = file.getName();
432                if (name != null) {
433                    name = name.trim();
434                    builder.append(parent).append("/").append(name);
435                    String packageOrClass = parent == null ? name : builder.toString();
436    
437                    if (file.isDirectory()) {
438                        loadImplementationsInDirectory(test, packageOrClass, file);
439                    } else if (name.endsWith(".class")) {
440                        addIfMatching(test, packageOrClass);
441                    }
442                }
443            }
444        }
445    
446        /**
447         * Finds matching classes within a jar files that contains a folder
448         * structure matching the package structure. If the File is not a JarFile or
449         * does not exist a warning will be logged, but no error will be raised.
450         *
451         * @param test    a Test used to filter the classes that are discovered
452         * @param parent  the parent package under which classes must be in order to
453         *                be considered
454         * @param jarfile the jar file to be examined for classes
455         * @param stream  the inputstream of the jar file to be examined for classes
456         * @param urlPath the url of the jar file to be examined for classes
457         */
458        private void loadImplementationsInJar(Test test, String parent, InputStream stream, String urlPath) {
459            JarInputStream jarStream = null;
460            try {
461                jarStream = new JarInputStream(stream);
462    
463                JarEntry entry;
464                while ((entry = jarStream.getNextJarEntry()) != null) {
465                    String name = entry.getName();
466                    if (name != null) {
467                        name = name.trim();
468                        if (!entry.isDirectory() && name.startsWith(parent) && name.endsWith(".class")) {
469                            addIfMatching(test, name);
470                        }
471                    }
472                }
473            } catch (IOException ioe) {
474                LOG.error("Could not search jar file '" + urlPath + "' for classes matching criteria: " + test
475                    + " due to an IOException: " + ioe.getMessage(), ioe);
476            } finally {
477                ObjectHelper.close(jarStream, urlPath, LOG);
478            }
479        }
480    
481        /**
482         * Add the class designated by the fully qualified class name provided to
483         * the set of resolved classes if and only if it is approved by the Test
484         * supplied.
485         *
486         * @param test the test used to determine if the class matches
487         * @param fqn  the fully qualified name of a class
488         */
489        protected void addIfMatching(Test test, String fqn) {
490            try {
491                String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
492                Set<ClassLoader> set = getClassLoaders();
493                boolean found = false;
494                for (ClassLoader classLoader : set) {
495                    if (LOG.isTraceEnabled()) {
496                        LOG.trace("Testing for class " + externalName + " matches criteria [" + test + "]");
497                    }
498                    try {
499                        Class type = classLoader.loadClass(externalName);
500                        if (test.matches(type)) {
501                            if (LOG.isTraceEnabled()) {
502                                LOG.trace("Found class: " + type + " in classloader: " + classLoader);
503                            }
504                            matches.add((Class<T>)type);
505                        }
506                        found = true;
507                        break;
508                    } catch (ClassNotFoundException e) {
509                        LOG.debug("Could not find class '" + fqn + "' in classloader: " + classLoader
510                            + ". Reason: " + e, e);
511                    } catch (NoClassDefFoundError e) {
512                        LOG.debug("Could not find the class defintion '" + fqn + "' in classloader: " + classLoader
513                                  + ". Reason: " + e, e);
514                    }
515                }
516                if (!found) {
517                    LOG.warn("Could not find class '" + fqn + "' in any classloaders: " + set);
518                }
519            } catch (Throwable t) {
520                LOG.warn("Could not examine class '" + fqn + "' due to a " + t.getClass().getName()
521                    + " with message: " + t.getMessage(), t);
522            }
523        }
524    
525    }