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