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