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