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