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