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