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