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.LRUCacheFactory;
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    private Map<String, List<String>> jarCache;
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 {}. This exception will be ignored.", e.getMessage());
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
397        List<String> entries = jarCache != null ? jarCache.get(urlPath) : null;
398        if (entries == null) {
399            entries = doLoadJarClassEntries(stream, urlPath);
400            if (jarCache != null) {
401                jarCache.put(urlPath, entries);
402                log.trace("Cached {} JAR with {} entries", urlPath, entries.size());
403            }
404        } else {
405            log.trace("Using cached {} JAR with {} entries", urlPath, entries.size());
406        }
407
408        doLoadImplementationsInJar(test, parent, entries, classes);
409    }
410
411    /**
412     * Loads all the class entries from the JAR.
413     *
414     * @param stream  the inputstream of the jar file to be examined for classes
415     * @param urlPath the url of the jar file to be examined for classes
416     * @return all the .class entries from the JAR
417     */
418    protected List<String> doLoadJarClassEntries(InputStream stream, String urlPath) {
419        List<String> entries = new ArrayList<String>();
420
421        JarInputStream jarStream = null;
422        try {
423            jarStream = new JarInputStream(stream);
424
425            JarEntry entry;
426            while ((entry = jarStream.getNextJarEntry()) != null) {
427                String name = entry.getName();
428                if (name != null) {
429                    name = name.trim();
430                    if (!entry.isDirectory() && name.endsWith(".class")) {
431                        entries.add(name);
432                    }
433                }
434            }
435        } catch (IOException ioe) {
436            log.warn("Cannot search jar file '" + urlPath + " due to an IOException: " + ioe.getMessage(), ioe);
437        } finally {
438            IOHelper.close(jarStream, urlPath, log);
439        }
440
441        return entries;
442    }
443
444    /**
445     * Adds all the matching implementations from from the JAR entries to the classes.
446     *
447     * @param test    a Test used to filter the classes that are discovered
448     * @param parent  the parent package under which classes must be in order to be considered
449     * @param entries the .class entries from the JAR
450     * @param classes to add found and matching classes
451     */
452    private void doLoadImplementationsInJar(PackageScanFilter test, String parent, List<String> entries, Set<Class<?>> classes) {
453        for (String entry : entries) {
454            if (entry.startsWith(parent)) {
455                addIfMatching(test, entry, classes);
456            }
457        }
458    }
459
460    /**
461     * Add the class designated by the fully qualified class name provided to
462     * the set of resolved classes if and only if it is approved by the Test
463     * supplied.
464     *
465     * @param test the test used to determine if the class matches
466     * @param fqn  the fully qualified name of a class
467     */    
468    protected void addIfMatching(PackageScanFilter test, String fqn, Set<Class<?>> classes) {
469        try {
470            String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
471            Set<ClassLoader> set = getClassLoaders();
472            boolean found = false;
473            for (ClassLoader classLoader : set) {
474                if (log.isTraceEnabled()) {
475                    log.trace("Testing for class {} matches criteria [{}] using classloader: {}", new Object[]{externalName, test, classLoader});
476                }
477                try {
478                    Class<?> type = classLoader.loadClass(externalName);
479                    log.trace("Loaded the class: {} in classloader: {}", type, classLoader);
480                    if (test.matches(type)) {
481                        log.trace("Found class: {} which matches the filter in classloader: {}", type, classLoader);
482                        classes.add(type);
483                    }
484                    found = true;
485                    break;
486                } catch (ClassNotFoundException e) {
487                    if (log.isTraceEnabled()) {
488                        log.trace("Cannot find class '" + fqn + "' in classloader: " + classLoader
489                                + ". Reason: " + e.getMessage(), e);
490                    }
491                } catch (NoClassDefFoundError e) {
492                    if (log.isTraceEnabled()) {
493                        log.trace("Cannot find the class definition '" + fqn + "' in classloader: " + classLoader
494                            + ". Reason: " + e.getMessage(), e);
495                    }
496                }
497            }
498            if (!found) {
499                log.debug("Cannot find class '{}' in any classloaders: {}", fqn, set);
500            }
501        } catch (Exception e) {
502            if (log.isWarnEnabled()) {
503                log.warn("Cannot examine class '" + fqn + "' due to a " + e.getClass().getName()
504                    + " with message: " + e.getMessage(), e);
505            }
506        }
507    }
508
509    @SuppressWarnings("unchecked")
510    protected void doStart() throws Exception {
511        if (jarCache == null) {
512            // 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
513            jarCache = LRUCacheFactory.newLRUCache(1000);
514        }
515    }
516
517    protected void doStop() throws Exception {
518        jarCache.clear();
519    }
520
521}