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.ArrayList;
030 import java.util.Arrays;
031 import java.util.Collections;
032 import java.util.Enumeration;
033 import java.util.LinkedHashSet;
034 import java.util.List;
035 import java.util.Map;
036 import java.util.Set;
037 import java.util.jar.JarEntry;
038 import java.util.jar.JarInputStream;
039
040 import org.apache.camel.impl.scan.AnnotatedWithAnyPackageScanFilter;
041 import org.apache.camel.impl.scan.AnnotatedWithPackageScanFilter;
042 import org.apache.camel.impl.scan.AssignableToPackageScanFilter;
043 import org.apache.camel.impl.scan.CompositePackageScanFilter;
044 import org.apache.camel.spi.PackageScanClassResolver;
045 import org.apache.camel.spi.PackageScanFilter;
046 import org.apache.camel.support.ServiceSupport;
047 import org.apache.camel.util.IOHelper;
048 import org.apache.camel.util.LRUSoftCache;
049 import org.apache.camel.util.ObjectHelper;
050 import org.apache.camel.util.ServiceHelper;
051 import org.slf4j.Logger;
052 import org.slf4j.LoggerFactory;
053
054 /**
055 * Default implement of {@link org.apache.camel.spi.PackageScanClassResolver}
056 */
057 public class DefaultPackageScanClassResolver extends ServiceSupport implements PackageScanClassResolver {
058
059 protected final transient 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 ServiceHelper.startService(jarCache);
512 }
513
514 protected void doStop() throws Exception {
515 ServiceHelper.stopService(jarCache);
516 }
517
518 }