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.impl;
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.URI;
025    import java.net.URISyntaxException;
026    import java.net.URL;
027    import java.net.URLConnection;
028    import java.net.URLDecoder;
029    import java.util.Arrays;
030    import java.util.Collections;
031    import java.util.Enumeration;
032    import java.util.HashSet;
033    import java.util.LinkedHashSet;
034    import java.util.Set;
035    import java.util.jar.JarEntry;
036    import java.util.jar.JarInputStream;
037    
038    import org.apache.camel.impl.scan.AnnotatedWithAnyPackageScanFilter;
039    import org.apache.camel.impl.scan.AnnotatedWithPackageScanFilter;
040    import org.apache.camel.impl.scan.AssignableToPackageScanFilter;
041    import org.apache.camel.impl.scan.CompositePackageScanFilter;
042    import org.apache.camel.spi.PackageScanClassResolver;
043    import org.apache.camel.spi.PackageScanFilter;
044    import org.apache.camel.util.ObjectHelper;
045    import org.apache.commons.logging.Log;
046    import org.apache.commons.logging.LogFactory;
047    
048    /**
049     * Default implement of {@link org.apache.camel.spi.PackageScanClassResolver}
050     */
051    public class DefaultPackageScanClassResolver implements PackageScanClassResolver {
052    
053        protected final transient Log log = LogFactory.getLog(getClass());
054        private Set<ClassLoader> classLoaders;
055        private Set<PackageScanFilter> scanFilters;
056    
057        public void addClassLoader(ClassLoader classLoader) {
058            getClassLoaders().add(classLoader);
059        }
060    
061        public void addFilter(PackageScanFilter filter) {
062            if (scanFilters == null) {
063                scanFilters = new LinkedHashSet<PackageScanFilter>();
064            }
065            scanFilters.add(filter);
066        }
067    
068        public Set<ClassLoader> getClassLoaders() {
069            if (classLoaders == null) {
070                classLoaders = new HashSet<ClassLoader>();
071                ClassLoader ccl = Thread.currentThread().getContextClassLoader();
072                if (ccl != null) {
073                    if (log.isTraceEnabled()) {
074                        log.trace("The thread context class loader: " + ccl + "  is used to load the class");
075                    }
076                    classLoaders.add(ccl);
077                }
078                classLoaders.add(DefaultPackageScanClassResolver.class.getClassLoader());
079            }
080            return classLoaders;
081        }
082    
083        public void setClassLoaders(Set<ClassLoader> classLoaders) {
084            this.classLoaders = classLoaders;
085        }
086    
087        @SuppressWarnings("unchecked")
088        public Set<Class<?>> findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
089            if (packageNames == null) {
090                return Collections.EMPTY_SET;
091            }
092    
093            if (log.isDebugEnabled()) {
094                log.debug("Searching for annotations of " + annotation.getName() + " in packages: " + Arrays.asList(packageNames));
095            }
096    
097            PackageScanFilter test = getCompositeFilter(new AnnotatedWithPackageScanFilter(annotation, true));
098            Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
099            for (String pkg : packageNames) {
100                find(test, pkg, classes);
101            }
102    
103            if (log.isDebugEnabled()) {
104                log.debug("Found: " + classes);
105            }
106    
107            return classes;
108        }
109    
110        @SuppressWarnings("unchecked")
111        public Set<Class<?>> findAnnotated(Set<Class<? extends Annotation>> annotations, String... packageNames) {
112            if (packageNames == null) {
113                return Collections.EMPTY_SET;
114            }
115    
116            if (log.isDebugEnabled()) {
117                log.debug("Searching for annotations of " + annotations + " in packages: " + Arrays.asList(packageNames));
118            }
119    
120            PackageScanFilter test = getCompositeFilter(new AnnotatedWithAnyPackageScanFilter(annotations, true));
121            Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
122            for (String pkg : packageNames) {
123                find(test, pkg, classes);
124            }
125    
126            if (log.isDebugEnabled()) {
127                log.debug("Found: " + classes);
128            }
129    
130            return classes;
131        }
132    
133        @SuppressWarnings("unchecked")
134        public Set<Class<?>> findImplementations(Class parent, String... packageNames) {
135            if (packageNames == null) {
136                return Collections.EMPTY_SET;
137            }
138    
139            if (log.isDebugEnabled()) {
140                log.debug("Searching for implementations of " + parent.getName() + " in packages: " + Arrays.asList(packageNames));
141            }
142    
143            PackageScanFilter test = getCompositeFilter(new AssignableToPackageScanFilter(parent));
144            Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
145            for (String pkg : packageNames) {
146                find(test, pkg, classes);
147            }
148    
149            if (log.isDebugEnabled()) {
150                log.debug("Found: " + classes);
151            }
152    
153            return classes;
154        }
155    
156        @SuppressWarnings("unchecked")
157        public Set<Class<?>> findByFilter(PackageScanFilter filter, String... packageNames) {
158            if (packageNames == null) {
159                return Collections.EMPTY_SET;
160            }
161    
162            Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
163            for (String pkg : packageNames) {
164                find(filter, pkg, classes);
165            }
166    
167            if (log.isDebugEnabled()) {
168                log.debug("Found: " + classes);
169            }
170    
171            return classes;
172        }
173    
174        protected void find(PackageScanFilter test, String packageName, Set<Class<?>> classes) {
175            packageName = packageName.replace('.', '/');
176    
177            Set<ClassLoader> set = getClassLoaders();
178    
179            for (ClassLoader classLoader : set) {
180                find(test, packageName, classLoader, classes);
181            }
182        }
183    
184        protected void find(PackageScanFilter test, String packageName, ClassLoader loader, Set<Class<?>> classes) {
185            if (log.isTraceEnabled()) {
186                log.trace("Searching for: " + test + " in package: " + packageName + " using classloader: "
187                        + loader.getClass().getName());
188            }
189    
190            Enumeration<URL> urls;
191            try {
192                urls = getResources(loader, packageName);
193                if (!urls.hasMoreElements()) {
194                    log.trace("No URLs returned by classloader");
195                }
196            } catch (IOException ioe) {
197                log.warn("Cannot read package: " + packageName, ioe);
198                return;
199            }
200    
201            while (urls.hasMoreElements()) {
202                URL url = null;
203                try {
204                    url = urls.nextElement();
205                    if (log.isTraceEnabled()) {
206                        log.trace("URL from classloader: " + url);
207                    }
208    
209                    String urlPath = url.getFile();
210                    urlPath = URLDecoder.decode(urlPath, "UTF-8");
211                    if (log.isTraceEnabled()) {
212                        log.trace("Decoded urlPath: " + urlPath + " with protocol: " + url.getProtocol());
213                    }
214    
215                    boolean isLocalFileSystem = "file".equals(url.getProtocol());
216    
217                    // If it's a file in a directory, trim the stupid file: spec
218                    if (urlPath.startsWith("file:")) {
219                        // file path can be temporary folder which uses characters that the URLDecoder decodes wrong
220                        // for example + being decoded to something else (+ can be used in temp folders on Mac OS)
221                        // to remedy this then create new path without using the URLDecoder
222                        try {
223                            urlPath = new URI(url.getFile()).getPath();
224                        } catch (URISyntaxException e) {
225                            // fallback to use as it was given from the URLDecoder
226                            // this allows us to work on Windows if users have spaces in paths
227                        }
228    
229                        if (urlPath.startsWith("file:")) {
230                            urlPath = urlPath.substring(5);
231                        }
232                        isLocalFileSystem = true;
233                    }
234    
235                    // osgi bundles should be skipped
236                    if (url.toString().startsWith("bundle:") || urlPath.startsWith("bundle:")) {
237                        log.trace("It's a virtual osgi bundle, skipping");
238                        continue;
239                    }
240    
241                    // Else it's in a JAR, grab the path to the jar
242                    if (urlPath.indexOf('!') > 0) {
243                        urlPath = urlPath.substring(0, urlPath.indexOf('!'));
244                    }
245    
246                    if (log.isTraceEnabled()) {
247                        log.trace("isLocalFileSystem: " + isLocalFileSystem);
248                        log.trace("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
249                    }
250    
251                    File file = new File(urlPath);
252                    if (file.isDirectory()) {
253                        if (log.isTraceEnabled()) {
254                            log.trace("Loading from directory using file: " + file);
255                        }
256                        loadImplementationsInDirectory(test, packageName, file, classes);
257                    } else {
258                        InputStream stream;
259                        if (!isLocalFileSystem) {
260                            // load resources using http (and other protocols) such as java webstart
261                            if (log.isTraceEnabled()) {
262                                log.trace("Loading from jar using http/https: " + urlPath);
263                            }
264                            URL urlStream = new URL(urlPath);
265                            URLConnection con = urlStream.openConnection();
266                            // disable cache mainly to avoid jar file locking on Windows
267                            con.setUseCaches(false);
268                            stream = con.getInputStream();
269                        } else {
270                            if (log.isTraceEnabled()) {
271                                log.trace("Loading from jar using file: " + file);
272                            }
273                            stream = new FileInputStream(file);
274                        }
275    
276                        loadImplementationsInJar(test, packageName, stream, urlPath, classes);
277                    }
278                } catch (IOException e) {
279                    // use debug logging to avoid being to noisy in logs
280                    log.debug("Cannot read entries in url: " + url, e);
281                }
282            }
283        }
284    
285        /**
286         * Strategy to get the resources by the given classloader.
287         * <p/>
288         * Notice that in WebSphere platforms there is a {@link WebSpherePackageScanClassResolver}
289         * to take care of WebSphere's odditiy of resource loading.
290         *
291         * @param loader  the classloader
292         * @param packageName   the packagename for the package to load
293         * @return  URL's for the given package
294         * @throws IOException is thrown by the classloader
295         */
296        protected Enumeration<URL> getResources(ClassLoader loader, String packageName) throws IOException {
297            if (log.isTraceEnabled()) {
298                log.trace("Getting resource URL for package: " + packageName + " with classloader: " + loader);
299            }
300            
301            // If the URL is a jar, the URLClassloader.getResources() seems to require a trailing slash.  The
302            // trailing slash is harmless for other URLs  
303            if (!packageName.endsWith("/")) {
304                packageName = packageName + "/";
305            }
306            return loader.getResources(packageName);
307        }
308    
309        private PackageScanFilter getCompositeFilter(PackageScanFilter filter) {
310            if (scanFilters != null) {
311                CompositePackageScanFilter composite = new CompositePackageScanFilter(scanFilters);
312                composite.addFilter(filter);
313                return composite;
314            }
315            return filter;
316        }
317    
318        /**
319         * Finds matches in a physical directory on a filesystem. Examines all files
320         * within a directory - if the File object is not a directory, and ends with
321         * <i>.class</i> the file is loaded and tested to see if it is acceptable
322         * according to the Test. Operates recursively to find classes within a
323         * folder structure matching the package structure.
324         *
325         * @param test     a Test used to filter the classes that are discovered
326         * @param parent   the package name up to this directory in the package
327         *                 hierarchy. E.g. if /classes is in the classpath and we wish to
328         *                 examine files in /classes/org/apache then the values of
329         *                 <i>parent</i> would be <i>org/apache</i>
330         * @param location a File object representing a directory
331         */
332        private void loadImplementationsInDirectory(PackageScanFilter test, String parent, File location, Set<Class<?>> classes) {
333            File[] files = location.listFiles();
334            StringBuilder builder = null;
335    
336            for (File file : files) {
337                builder = new StringBuilder(100);
338                String name = file.getName();
339                if (name != null) {
340                    name = name.trim();
341                    builder.append(parent).append("/").append(name);
342                    String packageOrClass = parent == null ? name : builder.toString();
343    
344                    if (file.isDirectory()) {
345                        loadImplementationsInDirectory(test, packageOrClass, file, classes);
346                    } else if (name.endsWith(".class")) {
347                        addIfMatching(test, packageOrClass, classes);
348                    }
349                }
350            }
351        }
352    
353        /**
354         * Finds matching classes within a jar files that contains a folder
355         * structure matching the package structure. If the File is not a JarFile or
356         * does not exist a warning will be logged, but no error will be raised.
357         *
358         * @param test    a Test used to filter the classes that are discovered
359         * @param parent  the parent package under which classes must be in order to
360         *                be considered
361         * @param stream  the inputstream of the jar file to be examined for classes
362         * @param urlPath the url of the jar file to be examined for classes
363         */
364        private void loadImplementationsInJar(PackageScanFilter test, String parent, InputStream stream, String urlPath, Set<Class<?>> classes) {
365            JarInputStream jarStream = null;
366            try {
367                jarStream = new JarInputStream(stream);
368    
369                JarEntry entry;
370                while ((entry = jarStream.getNextJarEntry()) != null) {
371                    String name = entry.getName();
372                    if (name != null) {
373                        name = name.trim();
374                        if (!entry.isDirectory() && name.startsWith(parent) && name.endsWith(".class")) {
375                            addIfMatching(test, name, classes);
376                        }
377                    }
378                }
379            } catch (IOException ioe) {
380                log.warn("Cannot search jar file '" + urlPath + "' for classes matching criteria: " + test
381                    + " due to an IOException: " + ioe.getMessage(), ioe);
382            } finally {
383                ObjectHelper.close(jarStream, urlPath, log);
384            }
385        }
386    
387        /**
388         * Add the class designated by the fully qualified class name provided to
389         * the set of resolved classes if and only if it is approved by the Test
390         * supplied.
391         *
392         * @param test the test used to determine if the class matches
393         * @param fqn  the fully qualified name of a class
394         */    
395        protected void addIfMatching(PackageScanFilter test, String fqn, Set<Class<?>> classes) {
396            try {
397                String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
398                Set<ClassLoader> set = getClassLoaders();
399                boolean found = false;
400                for (ClassLoader classLoader : set) {
401                    if (log.isTraceEnabled()) {
402                        log.trace("Testing for class " + externalName + " matches criteria [" + test + "] using classloader:" + classLoader);
403                    }
404                    try {
405                        Class<?> type = classLoader.loadClass(externalName);
406                        if (log.isTraceEnabled()) {
407                            log.trace("Loaded the class: " + type + " in classloader: " + classLoader);
408                        }
409                        if (test.matches(type)) {
410                            if (log.isTraceEnabled()) {
411                                log.trace("Found class: " + type + " which matches the filter in classloader: " + classLoader);
412                            }
413                            classes.add(type);
414                        }
415                        found = true;
416                        break;
417                    } catch (ClassNotFoundException e) {
418                        if (log.isTraceEnabled()) {
419                            log.trace("Cannot find class '" + fqn + "' in classloader: " + classLoader
420                                    + ". Reason: " + e, e);
421                        }
422                    } catch (NoClassDefFoundError e) {
423                        if (log.isTraceEnabled()) {
424                            log.trace("Cannot find the class definition '" + fqn + "' in classloader: " + classLoader
425                                + ". Reason: " + e, e);
426                        }
427                    }
428                }
429                if (!found) {
430                    if (log.isDebugEnabled()) {
431                        // use debug to avoid being noisy in logs
432                        log.debug("Cannot find class '" + fqn + "' in any classloaders: " + set);
433                    }
434                }
435            } catch (Exception e) {
436                if (log.isWarnEnabled()) {
437                    log.warn("Cannot examine class '" + fqn + "' due to a " + e.getClass().getName()
438                        + " with message: " + e.getMessage(), e);
439                }
440            }
441        }
442    
443    }