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