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 */ 017package org.apache.camel.impl.osgi; 018 019import java.io.BufferedInputStream; 020import java.io.BufferedReader; 021import java.io.IOException; 022import java.io.InputStreamReader; 023import java.lang.reflect.Method; 024import java.net.URL; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Dictionary; 028import java.util.Enumeration; 029import java.util.HashMap; 030import java.util.Hashtable; 031import java.util.LinkedHashSet; 032import java.util.List; 033import java.util.Map; 034import java.util.Properties; 035import java.util.Set; 036import java.util.StringTokenizer; 037import java.util.concurrent.ConcurrentHashMap; 038 039import org.apache.camel.CamelContext; 040import org.apache.camel.Component; 041import org.apache.camel.Converter; 042import org.apache.camel.TypeConverter; 043import org.apache.camel.TypeConverterLoaderException; 044import org.apache.camel.impl.converter.AnnotationTypeConverterLoader; 045import org.apache.camel.impl.osgi.tracker.BundleTracker; 046import org.apache.camel.impl.osgi.tracker.BundleTrackerCustomizer; 047import org.apache.camel.impl.scan.AnnotatedWithPackageScanFilter; 048import org.apache.camel.model.DataFormatDefinition; 049import org.apache.camel.spi.ComponentResolver; 050import org.apache.camel.spi.DataFormat; 051import org.apache.camel.spi.DataFormatResolver; 052import org.apache.camel.spi.Injector; 053import org.apache.camel.spi.Language; 054import org.apache.camel.spi.LanguageResolver; 055import org.apache.camel.spi.PackageScanFilter; 056import org.apache.camel.spi.TypeConverterLoader; 057import org.apache.camel.spi.TypeConverterRegistry; 058import org.apache.camel.util.IOHelper; 059import org.apache.camel.util.ObjectHelper; 060import org.apache.camel.util.StringHelper; 061import org.osgi.framework.Bundle; 062import org.osgi.framework.BundleActivator; 063import org.osgi.framework.BundleContext; 064import org.osgi.framework.BundleEvent; 065import org.osgi.framework.Constants; 066import org.osgi.framework.ServiceRegistration; 067import org.osgi.framework.wiring.BundleCapability; 068import org.osgi.framework.wiring.BundleWire; 069import org.osgi.framework.wiring.BundleWiring; 070import org.slf4j.Logger; 071import org.slf4j.LoggerFactory; 072 073import static org.osgi.framework.wiring.BundleRevision.PACKAGE_NAMESPACE; 074 075public class Activator implements BundleActivator, BundleTrackerCustomizer { 076 077 public static final String META_INF_COMPONENT = "META-INF/services/org/apache/camel/component/"; 078 public static final String META_INF_LANGUAGE = "META-INF/services/org/apache/camel/language/"; 079 public static final String META_INF_LANGUAGE_RESOLVER = "META-INF/services/org/apache/camel/language/resolver/"; 080 public static final String META_INF_DATAFORMAT = "META-INF/services/org/apache/camel/dataformat/"; 081 public static final String META_INF_TYPE_CONVERTER = "META-INF/services/org/apache/camel/TypeConverter"; 082 public static final String META_INF_FALLBACK_TYPE_CONVERTER = "META-INF/services/org/apache/camel/FallbackTypeConverter"; 083 public static final String EXTENDER_NAMESPACE = "osgi.extender"; 084 public static final String CAMEL_EXTENDER = "org.apache.camel"; 085 086 private static final Logger LOG = LoggerFactory.getLogger(Activator.class); 087 088 private BundleTracker tracker; 089 private final Map<Long, List<BaseService>> resolvers = new ConcurrentHashMap<Long, List<BaseService>>(); 090 private long bundleId; 091 092 // Map from package name to the capability we export for this package 093 private final Map<String, BundleCapability> packageCapabilities = new HashMap<String, BundleCapability>(); 094 095 public void start(BundleContext context) throws Exception { 096 LOG.info("Camel activator starting"); 097 cachePackageCapabilities(context); 098 bundleId = context.getBundle().getBundleId(); 099 BundleContext systemBundleContext = context.getBundle(0).getBundleContext(); 100 tracker = new BundleTracker(systemBundleContext, Bundle.ACTIVE, this); 101 tracker.open(); 102 LOG.info("Camel activator started"); 103 } 104 105 public void stop(BundleContext context) throws Exception { 106 LOG.info("Camel activator stopping"); 107 tracker.close(); 108 packageCapabilities.clear(); 109 LOG.info("Camel activator stopped"); 110 } 111 112 /** 113 * Caches the package capabilities that are needed for a set of interface classes 114 */ 115 private void cachePackageCapabilities(BundleContext context) { 116 BundleWiring ourWiring = context.getBundle().adapt(BundleWiring.class); 117 List<BundleCapability> ourExports = ourWiring.getCapabilities(PACKAGE_NAMESPACE); 118 for (BundleCapability ourExport : ourExports) { 119 String ourPkgName = (String) ourExport.getAttributes().get(PACKAGE_NAMESPACE); 120 packageCapabilities.put(ourPkgName, ourExport); 121 } 122 } 123 124 public Object addingBundle(Bundle bundle, BundleEvent event) { 125 LOG.debug("Bundle started: {}", bundle.getSymbolicName()); 126 if (extenderCapabilityWired(bundle)) { 127 List<BaseService> r = new ArrayList<BaseService>(); 128 registerComponents(bundle, r); 129 registerLanguages(bundle, r); 130 registerDataFormats(bundle, r); 131 registerTypeConverterLoader(bundle, r); 132 for (BaseService service : r) { 133 service.register(); 134 } 135 resolvers.put(bundle.getBundleId(), r); 136 } 137 138 return bundle; 139 } 140 141 private boolean extenderCapabilityWired(Bundle bundle) { 142 BundleWiring wiring = bundle.adapt(BundleWiring.class); 143 if (wiring == null) { 144 return true; 145 } 146 List<BundleWire> requiredWires = wiring.getRequiredWires(EXTENDER_NAMESPACE); 147 for (BundleWire requiredWire : requiredWires) { 148 if (CAMEL_EXTENDER.equals(requiredWire.getCapability().getAttributes().get(EXTENDER_NAMESPACE))) { 149 if (this.bundleId == requiredWire.getProviderWiring().getBundle().getBundleId()) { 150 LOG.debug("Camel extender requirement of bundle {} correctly wired to this implementation", bundle.getBundleId()); 151 return true; 152 } else { 153 LOG.info("Not processing bundle {} as it requires a camel extender but is not wired to the this implementation", bundle.getBundleId()); 154 return false; 155 } 156 } 157 } 158 return true; 159 } 160 161 public void modifiedBundle(Bundle bundle, BundleEvent event, Object object) { 162 } 163 164 public void removedBundle(Bundle bundle, BundleEvent event, Object object) { 165 LOG.debug("Bundle stopped: {}", bundle.getSymbolicName()); 166 List<BaseService> r = resolvers.remove(bundle.getBundleId()); 167 if (r != null) { 168 for (BaseService service : r) { 169 service.unregister(); 170 } 171 } 172 } 173 174 protected void registerComponents(Bundle bundle, List<BaseService> resolvers) { 175 if (canSee(bundle, Component.class)) { 176 Map<String, String> components = new HashMap<String, String>(); 177 for (Enumeration<?> e = bundle.getEntryPaths(META_INF_COMPONENT); e != null && e.hasMoreElements();) { 178 String path = (String) e.nextElement(); 179 LOG.debug("Found entry: {} in bundle {}", path, bundle.getSymbolicName()); 180 String name = path.substring(path.lastIndexOf("/") + 1); 181 components.put(name, path); 182 } 183 if (!components.isEmpty()) { 184 resolvers.add(new BundleComponentResolver(bundle, components)); 185 } 186 } 187 } 188 189 protected void registerLanguages(Bundle bundle, List<BaseService> resolvers) { 190 if (canSee(bundle, Language.class)) { 191 Map<String, String> languages = new HashMap<String, String>(); 192 for (Enumeration<?> e = bundle.getEntryPaths(META_INF_LANGUAGE); e != null && e.hasMoreElements();) { 193 String path = (String) e.nextElement(); 194 LOG.debug("Found entry: {} in bundle {}", path, bundle.getSymbolicName()); 195 String name = path.substring(path.lastIndexOf("/") + 1); 196 languages.put(name, path); 197 } 198 if (!languages.isEmpty()) { 199 resolvers.add(new BundleLanguageResolver(bundle, languages)); 200 } 201 for (Enumeration<?> e = bundle.getEntryPaths(META_INF_LANGUAGE_RESOLVER); e != null && e.hasMoreElements();) { 202 String path = (String) e.nextElement(); 203 LOG.debug("Found entry: {} in bundle {}", path, bundle.getSymbolicName()); 204 String name = path.substring(path.lastIndexOf("/") + 1); 205 resolvers.add(new BundleMetaLanguageResolver(bundle, name, path)); 206 } 207 } 208 } 209 210 protected void registerDataFormats(Bundle bundle, List<BaseService> resolvers) { 211 if (canSee(bundle, DataFormat.class)) { 212 Map<String, String> dataformats = new HashMap<String, String>(); 213 for (Enumeration<?> e = bundle.getEntryPaths(META_INF_DATAFORMAT); e != null && e.hasMoreElements();) { 214 String path = (String) e.nextElement(); 215 LOG.debug("Found entry: {} in bundle {}", path, bundle.getSymbolicName()); 216 String name = path.substring(path.lastIndexOf("/") + 1); 217 dataformats.put(name, path); 218 } 219 if (!dataformats.isEmpty()) { 220 resolvers.add(new BundleDataFormatResolver(bundle, dataformats)); 221 } 222 } 223 } 224 225 protected void registerTypeConverterLoader(Bundle bundle, List<BaseService> resolvers) { 226 if (canSee(bundle, TypeConverter.class)) { 227 URL url1 = bundle.getEntry(META_INF_TYPE_CONVERTER); 228 URL url2 = bundle.getEntry(META_INF_FALLBACK_TYPE_CONVERTER); 229 if (url1 != null || url2 != null) { 230 LOG.debug("Found TypeConverter in bundle {}", bundle.getSymbolicName()); 231 resolvers.add(new BundleTypeConverterLoader(bundle, url2 != null)); 232 } 233 } 234 } 235 236 /** 237 * Check if bundle can see the given class 238 */ 239 protected boolean canSee(Bundle bundle, Class<?> clazz) { 240 if (bundle.getBundleId() == bundleId) { 241 // Need extra handling of camel core as it does not import the api 242 return true; 243 } 244 BundleCapability packageCap = packageCapabilities.get(clazz.getPackage().getName()); 245 if (packageCap != null) { 246 BundleWiring wiring = bundle.adapt(BundleWiring.class); 247 List<BundleWire> imports = wiring.getRequiredWires(PACKAGE_NAMESPACE); 248 for (BundleWire importWire : imports) { 249 if (packageCap.equals(importWire.getCapability())) { 250 return true; 251 } 252 } 253 } 254 255 // it may be running outside real OSGi container such as when unit testing with camel-test-blueprint 256 // then we need to use a different canSee algorithm that works outside real OSGi 257 if (bundle.getBundleId() > 0) { 258 Bundle root = bundle.getBundleContext().getBundle(0); 259 if (root != null && "org.apache.felix.connect".equals(root.getSymbolicName())) { 260 return checkCompat(bundle, clazz); 261 } 262 } 263 264 return false; 265 } 266 267 /** 268 * Check if bundle can see the given class used by camel-test-blueprint 269 */ 270 protected static boolean checkCompat(Bundle bundle, Class<?> clazz) { 271 // Check bundle compatibility 272 try { 273 if (bundle.loadClass(clazz.getName()) != clazz) { 274 return false; 275 } 276 } catch (Throwable t) { 277 return false; 278 } 279 return true; 280 } 281 282 protected static class BundleComponentResolver extends BaseResolver<Component> implements ComponentResolver { 283 284 private final Map<String, String> components; 285 286 public BundleComponentResolver(Bundle bundle, Map<String, String> components) { 287 super(bundle, Component.class); 288 this.components = components; 289 } 290 291 public Component resolveComponent(String name, CamelContext context) throws Exception { 292 return createInstance(name, components.get(name), context); 293 } 294 295 public void register() { 296 doRegister(ComponentResolver.class, "component", components.keySet()); 297 } 298 } 299 300 protected static class BundleLanguageResolver extends BaseResolver<Language> implements LanguageResolver { 301 302 private final Map<String, String> languages; 303 304 public BundleLanguageResolver(Bundle bundle, Map<String, String> languages) { 305 super(bundle, Language.class); 306 this.languages = languages; 307 } 308 309 public Language resolveLanguage(String name, CamelContext context) { 310 return createInstance(name, languages.get(name), context); 311 } 312 313 public void register() { 314 doRegister(LanguageResolver.class, "language", languages.keySet()); 315 } 316 } 317 318 protected static class BundleMetaLanguageResolver extends BaseResolver<LanguageResolver> implements LanguageResolver { 319 320 private final String name; 321 private final String path; 322 323 public BundleMetaLanguageResolver(Bundle bundle, String name, String path) { 324 super(bundle, LanguageResolver.class); 325 this.name = name; 326 this.path = path; 327 } 328 329 public Language resolveLanguage(String name, CamelContext context) { 330 LanguageResolver resolver = createInstance(this.name, path, context); 331 return resolver.resolveLanguage(name, context); 332 } 333 334 public void register() { 335 doRegister(LanguageResolver.class, "resolver", name); 336 } 337 } 338 339 protected static class BundleDataFormatResolver extends BaseResolver<DataFormat> implements DataFormatResolver { 340 341 private final Map<String, String> dataformats; 342 343 public BundleDataFormatResolver(Bundle bundle, Map<String, String> dataformats) { 344 super(bundle, DataFormat.class); 345 this.dataformats = dataformats; 346 } 347 348 @Override 349 public DataFormat resolveDataFormat(String name, CamelContext context) { 350 DataFormat dataFormat = createInstance(name, dataformats.get(name), context); 351 if (dataFormat == null) { 352 dataFormat = createDataFormat(name, context); 353 } 354 355 return dataFormat; 356 } 357 358 @Override 359 public DataFormat createDataFormat(String name, CamelContext context) { 360 return createInstance(name, dataformats.get(name), context); 361 } 362 363 public DataFormatDefinition resolveDataFormatDefinition(String name, CamelContext context) { 364 return null; 365 } 366 367 @Override 368 public void register() { 369 doRegister(DataFormatResolver.class, "dataformat", dataformats.keySet()); 370 } 371 } 372 373 protected static class BundleTypeConverterLoader extends BaseResolver<TypeConverter> implements TypeConverterLoader { 374 375 private final AnnotationTypeConverterLoader loader = new Loader(); 376 private final Bundle bundle; 377 private final boolean hasFallbackTypeConverter; 378 379 public BundleTypeConverterLoader(Bundle bundle, boolean hasFallbackTypeConverter) { 380 super(bundle, TypeConverter.class); 381 ObjectHelper.notNull(bundle, "bundle"); 382 this.bundle = bundle; 383 this.hasFallbackTypeConverter = hasFallbackTypeConverter; 384 } 385 386 public synchronized void load(TypeConverterRegistry registry) throws TypeConverterLoaderException { 387 // must be synchronized to ensure we don't load type converters concurrently 388 // which cause Camel apps to fails in OSGi thereafter 389 try { 390 loader.load(registry); 391 } catch (Exception e) { 392 throw new TypeConverterLoaderException("Cannot load type converters using OSGi bundle: " + bundle.getBundleId(), e); 393 } 394 } 395 396 public void register() { 397 if (hasFallbackTypeConverter) { 398 // The FallbackTypeConverter should have a higher ranking 399 doRegister(TypeConverterLoader.class, Constants.SERVICE_RANKING, 100); 400 } else { 401 // The default service ranking is Integer(0); 402 doRegister(TypeConverterLoader.class); 403 } 404 } 405 406 class Loader extends AnnotationTypeConverterLoader { 407 408 Loader() { 409 super(null); 410 } 411 412 public void load(TypeConverterRegistry registry) throws TypeConverterLoaderException { 413 PackageScanFilter test = new AnnotatedWithPackageScanFilter(Converter.class, true); 414 Set<Class<?>> classes = new LinkedHashSet<Class<?>>(); 415 Set<String> packages = getConverterPackages(bundle.getEntry(META_INF_TYPE_CONVERTER)); 416 417 if (LOG.isTraceEnabled()) { 418 LOG.trace("Found {} {} packages: {}", new Object[]{packages.size(), META_INF_TYPE_CONVERTER, packages}); 419 } 420 // if we only have camel-core on the classpath then we have already pre-loaded all its type converters 421 // but we exposed the "org.apache.camel.core" package in camel-core. This ensures there is at least one 422 // packageName to scan, which triggers the scanning process. That allows us to ensure that we look for 423 // META-INF/services in all the JARs. 424 if (packages.size() == 1 && "org.apache.camel.core".equals(packages.iterator().next())) { 425 LOG.debug("No additional package names found in classpath for annotated type converters."); 426 // no additional package names found to load type converters so break out 427 return; 428 } 429 430 // now filter out org.apache.camel.core as its not needed anymore (it was just a dummy) 431 packages.remove("org.apache.camel.core"); 432 433 for (String pkg : packages) { 434 435 if (StringHelper.isClassName(pkg)) { 436 // its a FQN class name so load it directly 437 LOG.trace("Loading {} class", pkg); 438 try { 439 Class<?> clazz = bundle.loadClass(pkg); 440 classes.add(clazz); 441 // the class could be found and loaded so continue to next 442 continue; 443 } catch (Throwable t) { 444 // Ignore 445 LOG.trace("Failed to load " + pkg + " class due " + t.getMessage() + ". This exception will be ignored.", t); 446 } 447 } 448 449 // its not a FQN but a package name so scan for classes in the bundle 450 Enumeration<URL> e = bundle.findEntries("/" + pkg.replace('.', '/'), "*.class", true); 451 while (e != null && e.hasMoreElements()) { 452 String path = e.nextElement().getPath(); 453 String externalName = path.substring(path.charAt(0) == '/' ? 1 : 0, path.indexOf('.')).replace('/', '.'); 454 LOG.trace("Loading {} class", externalName); 455 try { 456 Class<?> clazz = bundle.loadClass(externalName); 457 if (test.matches(clazz)) { 458 classes.add(clazz); 459 } 460 } catch (Throwable t) { 461 // Ignore 462 LOG.trace("Failed to load " + externalName + " class due " + t.getMessage() + ". This exception will be ignored.", t); 463 } 464 } 465 } 466 467 // load the classes into type converter registry 468 LOG.debug("Found {} @Converter classes to load", classes.size()); 469 for (Class<?> type : classes) { 470 if (LOG.isTraceEnabled()) { 471 LOG.trace("Loading converter class: {}", ObjectHelper.name(type)); 472 } 473 loadConverterMethods(registry, type); 474 } 475 476 // register fallback converters 477 URL fallbackUrl = bundle.getEntry(META_INF_FALLBACK_TYPE_CONVERTER); 478 if (fallbackUrl != null) { 479 LOG.debug("Found {} to load the FallbackTypeConverter", META_INF_FALLBACK_TYPE_CONVERTER); 480 TypeConverter tc = createInstance("FallbackTypeConverter", fallbackUrl, registry.getInjector()); 481 registry.addFallbackTypeConverter(tc, false); 482 } 483 484 // now clear the maps so we do not hold references 485 visitedClasses.clear(); 486 visitedURIs.clear(); 487 } 488 } 489 490 } 491 492 protected abstract static class BaseResolver<T> extends BaseService { 493 494 private final Class<T> type; 495 496 public BaseResolver(Bundle bundle, Class<T> type) { 497 super(bundle); 498 this.type = type; 499 } 500 501 protected T createInstance(String name, String path, CamelContext context) { 502 if (path == null) { 503 return null; 504 } 505 URL url = bundle.getEntry(path); 506 LOG.trace("The entry {}'s url is {}", name, url); 507 //Setup the TCCL with Camel context application class loader 508 ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); 509 try { 510 ClassLoader newClassLoader = context.getApplicationContextClassLoader(); 511 if (newClassLoader != null) { 512 Thread.currentThread().setContextClassLoader(newClassLoader); 513 } 514 T answer = createInstance(name, url, context.getInjector()); 515 if (answer != null) { 516 initBundleContext(answer); 517 } 518 return answer; 519 } finally { 520 Thread.currentThread().setContextClassLoader(oldClassLoader); 521 } 522 } 523 524 private void initBundleContext(T answer) { 525 try { 526 Method method = answer.getClass().getMethod("setBundleContext", BundleContext.class); 527 if (method != null) { 528 method.invoke(answer, bundle.getBundleContext()); 529 } 530 } catch (Exception e) { 531 // ignore 532 } 533 } 534 535 @SuppressWarnings("unchecked") 536 protected T createInstance(String name, URL url, Injector injector) { 537 try { 538 Properties properties = loadProperties(url); 539 String classname = (String) properties.get("class"); 540 Class<?> type = bundle.loadClass(classname); 541 if (!this.type.isAssignableFrom(type)) { 542 throw new IllegalArgumentException("Type is not a " + this.type.getName() + " implementation. Found: " + type.getName()); 543 } 544 return injector.newInstance((Class<T>) type); 545 } catch (ClassNotFoundException e) { 546 throw new IllegalArgumentException("Invalid URI, no " + this.type.getName() + " registered for scheme : " + name, e); 547 } 548 } 549 550 } 551 552 protected abstract static class BaseService { 553 554 protected final Bundle bundle; 555 private ServiceRegistration<?> reg; 556 557 protected BaseService(Bundle bundle) { 558 this.bundle = bundle; 559 } 560 561 public abstract void register(); 562 563 protected void doRegister(Class<?> type, String key, Collection<String> value) { 564 doRegister(type, key, value.toArray(new String[value.size()])); 565 } 566 567 protected void doRegister(Class<?> type, String key, Object value) { 568 Dictionary<String, Object> props = new Hashtable<String, Object>(); 569 props.put(key, value); 570 doRegister(type, props); 571 } 572 573 protected void doRegister(Class<?> type) { 574 doRegister(type, null); 575 } 576 577 protected void doRegister(Class<?> type, Dictionary<String, ?> props) { 578 reg = bundle.getBundleContext().registerService(type.getName(), this, props); 579 } 580 581 public void unregister() { 582 reg.unregister(); 583 } 584 } 585 586 protected static Properties loadProperties(URL url) { 587 Properties properties = new Properties(); 588 BufferedInputStream reader = null; 589 try { 590 reader = IOHelper.buffered(url.openStream()); 591 properties.load(reader); 592 } catch (IOException e) { 593 throw new RuntimeException(e); 594 } finally { 595 IOHelper.close(reader, "properties", LOG); 596 } 597 return properties; 598 } 599 600 protected static Set<String> getConverterPackages(URL resource) { 601 Set<String> packages = new LinkedHashSet<String>(); 602 if (resource != null) { 603 BufferedReader reader = null; 604 try { 605 reader = IOHelper.buffered(new InputStreamReader(resource.openStream())); 606 while (true) { 607 String line = reader.readLine(); 608 if (line == null) { 609 break; 610 } 611 line = line.trim(); 612 if (line.startsWith("#") || line.length() == 0) { 613 continue; 614 } 615 StringTokenizer iter = new StringTokenizer(line, ","); 616 while (iter.hasMoreTokens()) { 617 String name = iter.nextToken().trim(); 618 if (name.length() > 0) { 619 packages.add(name); 620 } 621 } 622 } 623 } catch (Exception ignore) { 624 // Do nothing here 625 } finally { 626 IOHelper.close(reader, null, LOG); 627 } 628 } 629 return packages; 630 } 631 632} 633 634