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