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.net.URL;
025    import java.net.URLConnection;
026    import java.net.URLDecoder;
027    import java.util.Arrays;
028    import java.util.Enumeration;
029    import java.util.HashSet;
030    import java.util.Set;
031    import java.util.jar.JarEntry;
032    import java.util.jar.JarInputStream;
033    
034    import org.apache.commons.logging.Log;
035    import org.apache.commons.logging.LogFactory;
036    
037    /**
038     * <p>
039     * ResolverUtil is used to locate classes that are available in the/a class path
040     * and meet arbitrary conditions. The two most common conditions are that a
041     * class implements/extends another class, or that is it annotated with a
042     * specific annotation. However, through the use of the {@link Test} class it is
043     * possible to search using arbitrary conditions.
044     * </p>
045     * <p/>
046     * <p>
047     * A ClassLoader is used to locate all locations (directories and jar files) in
048     * the class path that contain classes within certain packages, and then to load
049     * those classes and check them. By default the ClassLoader returned by
050     * {@code Thread.currentThread().getContextClassLoader()} is used, but this can
051     * be overridden by calling {@link #setClassLoaders(Set)} prior to
052     * invoking any of the {@code find()} methods.
053     * </p>
054     * <p/>
055     * <p>
056     * General searches are initiated by calling the
057     * {@link #find(org.apache.camel.util.ResolverUtil.Test, String)}} method and supplying a package
058     * name and a Test instance. This will cause the named package <b>and all
059     * sub-packages</b> to be scanned for classes that meet the test. There are
060     * also utility methods for the common use cases of scanning multiple packages
061     * for extensions of particular classes, or classes annotated with a specific
062     * annotation.
063     * </p>
064     * <p/>
065     * <p>
066     * The standard usage pattern for the ResolverUtil class is as follows:
067     * </p>
068     * <p/>
069     * <pre>
070     * resolverUtil&lt;ActionBean&gt; resolver = new ResolverUtil&lt;ActionBean&gt;();
071     * resolver.findImplementation(ActionBean.class, pkg1, pkg2);
072     * resolver.find(new CustomTest(), pkg1);
073     * resolver.find(new CustomTest(), pkg2);
074     * collection&lt;ActionBean&gt; beans = resolver.getClasses();
075     * </pre>
076     */
077    public class ResolverUtil<T> {
078        protected final transient Log log = LogFactory.getLog(getClass());
079    
080        /**
081         * A simple interface that specifies how to test classes to determine if
082         * they are to be included in the results produced by the ResolverUtil.
083         */
084        public static interface Test {
085            /**
086             * Will be called repeatedly with candidate classes. Must return True if
087             * a class is to be included in the results, false otherwise.
088             */
089            boolean matches(Class type);
090        }
091    
092        /**
093         * A Test that checks to see if each class is assignable to the provided
094         * class. Note that this test will match the parent type itself if it is
095         * presented for matching.
096         */
097        public static class IsA implements Test {
098            private Class parent;
099    
100            /**
101             * Constructs an IsA test using the supplied Class as the parent
102             * class/interface.
103             */
104            public IsA(Class parentType) {
105                this.parent = parentType;
106            }
107    
108            /**
109             * Returns true if type is assignable to the parent type supplied in the
110             * constructor.
111             */
112            public boolean matches(Class type) {
113                return type != null && parent.isAssignableFrom(type);
114            }
115    
116            @Override
117            public String toString() {
118                return "is assignable to " + parent.getSimpleName();
119            }
120        }
121    
122        /**
123         * A Test that checks to see if each class is annotated with a specific
124         * annotation. If it is, then the test returns true, otherwise false.
125         */
126        public static class AnnotatedWith implements Test {
127            private Class<? extends Annotation> annotation;
128            private boolean checkMetaAnnotations;
129    
130            /**
131             * Constructs an AnnotatedWith test for the specified annotation type.
132             */
133            public AnnotatedWith(Class<? extends Annotation> annotation) {
134                this(annotation, false);
135            }
136    
137            /**
138             * Constructs an AnnotatedWith test for the specified annotation type.
139             */
140            public AnnotatedWith(Class<? extends Annotation> annotation, boolean checkMetaAnnotations) {
141                this.annotation = annotation;
142                this.checkMetaAnnotations = checkMetaAnnotations;
143            }
144    
145            /**
146             * Returns true if the type is annotated with the class provided to the
147             * constructor.
148             */
149            public boolean matches(Class type) {
150                return type != null && ObjectHelper.hasAnnotation(type, annotation, checkMetaAnnotations);
151            }
152    
153            @Override
154            public String toString() {
155                return "annotated with @" + annotation.getSimpleName();
156            }
157        }
158    
159        /**
160         * The set of matches being accumulated.
161         */
162        private Set<Class<? extends T>> matches = new HashSet<Class<? extends T>>();
163    
164        /**
165         * The ClassLoader to use when looking for classes. If null then the
166         * ClassLoader returned by Thread.currentThread().getContextClassLoader()
167         * will be used.
168         */
169        private Set<ClassLoader> classLoaders;
170    
171        /**
172         * Provides access to the classes discovered so far. If no calls have been
173         * made to any of the {@code find()} methods, this set will be empty.
174         *
175         * @return the set of classes that have been discovered.
176         */
177        public Set<Class<? extends T>> getClasses() {
178            return matches;
179        }
180    
181    
182        /**
183         * Returns the classloaders that will be used for scanning for classes. If no
184         * explicit ClassLoader has been set by the calling, the context class
185         * loader will and the one that has loaded this class ResolverUtil be used.
186         *
187         * @return the ClassLoader instances that will be used to scan for classes
188         */
189        public Set<ClassLoader> getClassLoaders() {
190            if (classLoaders == null) {
191                classLoaders = new HashSet<ClassLoader>();
192                ClassLoader ccl = Thread.currentThread().getContextClassLoader();
193                if (ccl != null) {
194                    classLoaders.add(ccl);
195                }
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            log.debug("Using only regular classloaders");
289            for (ClassLoader classLoader : set) {            
290                find(test, packageName, classLoader);            
291            }
292        }
293    
294        
295    
296        /**
297         * Tries to find the reosurce in the package using the class loader.
298         * <p/>
299         * Will handle both plain URL based classloaders and OSGi bundle loaders.
300         *
301         * @param test what to find
302         * @param packageName the package to search in
303         * @param loader the class loader     
304         */
305        protected void find(Test test, String packageName, ClassLoader loader) {
306            if (log.isTraceEnabled()) {
307                log.trace("Searching for: " + test + " in package: " + packageName + " using classloader: "
308                        + loader.getClass().getName());
309            }        
310    
311            Enumeration<URL> urls;
312            try {
313                urls = getResources(loader, packageName);
314                if (!urls.hasMoreElements()) {
315                    log.trace("No URLs returned by classloader");
316                }
317            } catch (IOException ioe) {
318                log.warn("Could not read package: " + packageName, ioe);
319                return;
320            }
321    
322            while (urls.hasMoreElements()) {
323                URL url = null;
324                try {
325                    url = urls.nextElement();
326                    if (log.isTraceEnabled()) {
327                        log.trace("URL from classloader: " + url);
328                    }
329    
330                    String urlPath = url.getFile();
331                    urlPath = URLDecoder.decode(urlPath, "UTF-8");
332                    if (log.isTraceEnabled()) {
333                        log.trace("Decoded urlPath: " + urlPath);
334                    }
335    
336                    // If it's a file in a directory, trim the stupid file: spec
337                    if (urlPath.startsWith("file:")) {
338                        urlPath = urlPath.substring(5);
339                    }
340    
341                    // osgi bundles should be skipped
342                    if (url.toString().startsWith("bundle:") || urlPath.startsWith("bundle:")) {
343                        log.trace("It's a virtual osgi bundle, skipping");
344                        continue;
345                    }
346    
347                    // Else it's in a JAR, grab the path to the jar
348                    if (urlPath.indexOf('!') > 0) {
349                        urlPath = urlPath.substring(0, urlPath.indexOf('!'));
350                    }
351    
352                    if (log.isTraceEnabled()) {
353                        log.trace("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
354                    }
355    
356                    File file = new File(urlPath);
357                    if (file.isDirectory()) {
358                        if (log.isDebugEnabled()) {
359                            log.debug("Loading from directory: " + file);
360                        }
361                        loadImplementationsInDirectory(test, packageName, file);
362                    } else {
363                        InputStream stream;
364                        if (urlPath.startsWith("http:")) {
365                            // load resources using http such as java webstart
366                            log.debug("The current jar is accessed via http");
367                            URL urlStream = new URL(urlPath);
368                            URLConnection con = urlStream.openConnection();
369                            // disable cache mainly to avoid jar file locking on Windows
370                            con.setUseCaches(false);
371                            stream = con.getInputStream();
372                        } else {
373                            stream = new FileInputStream(file);
374                        }
375    
376                        if (log.isDebugEnabled()) {
377                            log.debug("Loading from jar: " + file);
378                        }
379                        loadImplementationsInJar(test, packageName, stream, urlPath);
380                    }
381                } catch (IOException ioe) {
382                    log.warn("Could not read entries in url: " + url, ioe);
383                }
384            }
385        }
386    
387        /**
388         * Strategy to get the resources by the given classloader.
389         * <p/>
390         * Notice that in WebSphere platforms there is a {@link org.apache.camel.util.WebSphereResolverUtil}
391         * to take care of WebSphere's odditiy of resource loading.
392         *
393         * @param loader  the classloader
394         * @param packageName   the packagename for the package to load
395         * @return  URL's for the given package
396         * @throws IOException is thrown by the classloader
397         */
398        protected Enumeration<URL> getResources(ClassLoader loader, String packageName) throws IOException {
399            if (log.isTraceEnabled()) {
400                log.trace("Getting resource URL for package: " + packageName + " with classloader: " + loader);
401            }
402            return loader.getResources(packageName);
403        }
404    
405        
406    
407    
408        /**
409         * Finds matches in a physical directory on a filesystem. Examines all files
410         * within a directory - if the File object is not a directory, and ends with
411         * <i>.class</i> the file is loaded and tested to see if it is acceptable
412         * according to the Test. Operates recursively to find classes within a
413         * folder structure matching the package structure.
414         *
415         * @param test     a Test used to filter the classes that are discovered
416         * @param parent   the package name up to this directory in the package
417         *                 hierarchy. E.g. if /classes is in the classpath and we wish to
418         *                 examine files in /classes/org/apache then the values of
419         *                 <i>parent</i> would be <i>org/apache</i>
420         * @param location a File object representing a directory
421         */
422        private void loadImplementationsInDirectory(Test test, String parent, File location) {
423            File[] files = location.listFiles();
424            StringBuilder builder = null;
425    
426            for (File file : files) {
427                builder = new StringBuilder(100);
428                String name = file.getName();
429                if (name != null) {
430                    name = name.trim();
431                    builder.append(parent).append("/").append(name);
432                    String packageOrClass = parent == null ? name : builder.toString();
433    
434                    if (file.isDirectory()) {
435                        loadImplementationsInDirectory(test, packageOrClass, file);
436                    } else if (name.endsWith(".class")) {
437                        addIfMatching(test, packageOrClass);
438                    }
439                }
440            }
441        }
442    
443        /**
444         * Finds matching classes within a jar files that contains a folder
445         * structure matching the package structure. If the File is not a JarFile or
446         * does not exist a warning will be logged, but no error will be raised.
447         *
448         * @param test    a Test used to filter the classes that are discovered
449         * @param parent  the parent package under which classes must be in order to
450         *                be considered
451         * @param stream  the inputstream of the jar file to be examined for classes
452         * @param urlPath the url of the jar file to be examined for classes
453         */
454        private void loadImplementationsInJar(Test test, String parent, InputStream stream, String urlPath) {
455            JarInputStream jarStream = null;
456            try {
457                jarStream = new JarInputStream(stream);
458    
459                JarEntry entry;
460                while ((entry = jarStream.getNextJarEntry()) != null) {
461                    String name = entry.getName();
462                    if (name != null) {
463                        name = name.trim();
464                        if (!entry.isDirectory() && name.startsWith(parent) && name.endsWith(".class")) {
465                            addIfMatching(test, name);
466                        }
467                    }
468                }
469            } catch (IOException ioe) {
470                log.error("Could not search jar file '" + urlPath + "' for classes matching criteria: " + test
471                    + " due to an IOException: " + ioe.getMessage(), ioe);
472            } finally {
473                ObjectHelper.close(jarStream, urlPath, log);
474            }
475        }
476    
477        /**
478         * Add the class designated by the fully qualified class name provided to
479         * the set of resolved classes if and only if it is approved by the Test
480         * supplied.
481         *
482         * @param test the test used to determine if the class matches
483         * @param fqn  the fully qualified name of a class
484         */
485        protected void addIfMatching(Test test, String fqn) {
486            try {
487                String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
488                Set<ClassLoader> set = getClassLoaders();
489                boolean found = false;
490                for (ClassLoader classLoader : set) {
491                    if (log.isTraceEnabled()) {
492                        log.trace("Testing for class " + externalName + " matches criteria [" + test + "]");
493                    }
494                    try {
495                        Class type = classLoader.loadClass(externalName);
496                        if (test.matches(type)) {
497                            if (log.isTraceEnabled()) {
498                                log.trace("Found class: " + type + " in classloader: " + classLoader);
499                            }
500                            matches.add((Class<T>)type);
501                        }
502                        found = true;
503                        break;
504                    } catch (ClassNotFoundException e) {
505                        log.debug("Could not find class '" + fqn + "' in classloader: " + classLoader
506                            + ". Reason: " + e, e);
507                    } catch (NoClassDefFoundError e) {
508                        log.debug("Could not find the class defintion '" + fqn + "' in classloader: " + classLoader
509                                  + ". Reason: " + e, e);
510                    }
511                }
512                if (!found) {
513                    log.warn("Could not find class '" + fqn + "' in any classloaders: " + set);
514                }
515            } catch (Throwable t) {
516                log.warn("Could not examine class '" + fqn + "' due to a " + t.getClass().getName()
517                    + " with message: " + t.getMessage(), t);
518            }
519        }
520    
521    }