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            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 jarfile the jar file to be examined for classes
452         * @param stream  the inputstream of the jar file to be examined for classes
453         * @param urlPath the url of the jar file to be examined for classes
454         */
455        private void loadImplementationsInJar(Test test, String parent, InputStream stream, String urlPath) {
456            JarInputStream jarStream = null;
457            try {
458                jarStream = new JarInputStream(stream);
459    
460                JarEntry entry;
461                while ((entry = jarStream.getNextJarEntry()) != null) {
462                    String name = entry.getName();
463                    if (name != null) {
464                        name = name.trim();
465                        if (!entry.isDirectory() && name.startsWith(parent) && name.endsWith(".class")) {
466                            addIfMatching(test, name);
467                        }
468                    }
469                }
470            } catch (IOException ioe) {
471                LOG.error("Could not search jar file '" + urlPath + "' for classes matching criteria: " + test
472                    + " due to an IOException: " + ioe.getMessage(), ioe);
473            } finally {
474                ObjectHelper.close(jarStream, urlPath, LOG);
475            }
476        }
477    
478        /**
479         * Add the class designated by the fully qualified class name provided to
480         * the set of resolved classes if and only if it is approved by the Test
481         * supplied.
482         *
483         * @param test the test used to determine if the class matches
484         * @param fqn  the fully qualified name of a class
485         */
486        protected void addIfMatching(Test test, String fqn) {
487            try {
488                String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
489                Set<ClassLoader> set = getClassLoaders();
490                boolean found = false;
491                for (ClassLoader classLoader : set) {
492                    if (LOG.isTraceEnabled()) {
493                        LOG.trace("Testing for class " + externalName + " matches criteria [" + test + "]");
494                    }
495                    try {
496                        Class type = classLoader.loadClass(externalName);
497                        if (test.matches(type)) {
498                            if (LOG.isTraceEnabled()) {
499                                LOG.trace("Found class: " + type + " in classloader: " + classLoader);
500                            }
501                            matches.add((Class<T>)type);
502                        }
503                        found = true;
504                        break;
505                    } catch (ClassNotFoundException e) {
506                        LOG.debug("Could not find class '" + fqn + "' in classloader: " + classLoader
507                            + ". Reason: " + e, e);
508                    } catch (NoClassDefFoundError e) {
509                        LOG.debug("Could not find the class defintion '" + fqn + "' in classloader: " + classLoader
510                                  + ". Reason: " + e, e);
511                    }
512                }
513                if (!found) {
514                    LOG.warn("Could not find class '" + fqn + "' in any classloaders: " + set);
515                }
516            } catch (Throwable t) {
517                LOG.warn("Could not examine class '" + fqn + "' due to a " + t.getClass().getName()
518                    + " with message: " + t.getMessage(), t);
519            }
520        }
521    
522    }