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.StaticService;
041import org.apache.camel.impl.scan.AnnotatedWithAnyPackageScanFilter;
042import org.apache.camel.impl.scan.AnnotatedWithPackageScanFilter;
043import org.apache.camel.impl.scan.AssignableToPackageScanFilter;
044import org.apache.camel.impl.scan.CompositePackageScanFilter;
045import org.apache.camel.spi.PackageScanClassResolver;
046import org.apache.camel.spi.PackageScanFilter;
047import org.apache.camel.support.ServiceSupport;
048import org.apache.camel.util.IOHelper;
049import org.apache.camel.util.LRUSoftCache;
050import org.apache.camel.util.ObjectHelper;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054/**
055 * Default implement of {@link org.apache.camel.spi.PackageScanClassResolver}
056 */
057public 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}