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.osgi;
018
019 import java.io.BufferedInputStream;
020 import java.io.BufferedReader;
021 import java.io.IOException;
022 import java.io.InputStreamReader;
023 import java.net.URL;
024 import java.util.ArrayList;
025 import java.util.Collection;
026 import java.util.Dictionary;
027 import java.util.Enumeration;
028 import java.util.HashMap;
029 import java.util.Hashtable;
030 import java.util.LinkedHashSet;
031 import java.util.List;
032 import java.util.Map;
033 import java.util.Properties;
034 import java.util.Set;
035 import java.util.StringTokenizer;
036 import java.util.concurrent.ConcurrentHashMap;
037
038 import org.apache.camel.CamelContext;
039 import org.apache.camel.Component;
040 import org.apache.camel.Converter;
041 import org.apache.camel.TypeConverter;
042 import org.apache.camel.TypeConverterLoaderException;
043 import org.apache.camel.impl.converter.AnnotationTypeConverterLoader;
044 import org.apache.camel.impl.osgi.tracker.BundleTracker;
045 import org.apache.camel.impl.osgi.tracker.BundleTrackerCustomizer;
046 import org.apache.camel.impl.scan.AnnotatedWithPackageScanFilter;
047 import org.apache.camel.model.DataFormatDefinition;
048 import org.apache.camel.spi.ComponentResolver;
049 import org.apache.camel.spi.DataFormat;
050 import org.apache.camel.spi.DataFormatResolver;
051 import org.apache.camel.spi.Injector;
052 import org.apache.camel.spi.Language;
053 import org.apache.camel.spi.LanguageResolver;
054 import org.apache.camel.spi.PackageScanFilter;
055 import org.apache.camel.spi.TypeConverterLoader;
056 import org.apache.camel.spi.TypeConverterRegistry;
057 import org.apache.camel.util.IOHelper;
058 import org.apache.camel.util.ObjectHelper;
059 import org.apache.camel.util.StringHelper;
060 import org.osgi.framework.Bundle;
061 import org.osgi.framework.BundleActivator;
062 import org.osgi.framework.BundleContext;
063 import org.osgi.framework.BundleEvent;
064 import org.osgi.framework.ServiceRegistration;
065
066 import org.slf4j.Logger;
067 import org.slf4j.LoggerFactory;
068
069 public 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 transient 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 resolvers.add(new BundleTypeConverterLoader(bundle));
180 }
181 }
182 }
183
184 protected static class BundleComponentResolver extends BaseResolver<Component> implements ComponentResolver {
185
186 private final Map<String, String> components;
187
188 public BundleComponentResolver(Bundle bundle, Map<String, String> components) {
189 super(bundle, Component.class);
190 this.components = components;
191 }
192
193 public Component resolveComponent(String name, CamelContext context) throws Exception {
194 return createInstance(name, components.get(name), context);
195 }
196
197 public void register() {
198 doRegister(ComponentResolver.class, "component", components.keySet());
199 }
200 }
201
202 protected static class BundleLanguageResolver extends BaseResolver<Language> implements LanguageResolver {
203
204 private final Map<String, String> languages;
205
206 public BundleLanguageResolver(Bundle bundle, Map<String, String> languages) {
207 super(bundle, Language.class);
208 this.languages = languages;
209 }
210
211 public Language resolveLanguage(String name, CamelContext context) {
212 return createInstance(name, languages.get(name), context);
213 }
214
215 public void register() {
216 doRegister(LanguageResolver.class, "language", languages.keySet());
217 }
218 }
219
220 protected static class BundleMetaLanguageResolver extends BaseResolver<LanguageResolver> implements LanguageResolver {
221
222 private final String name;
223 private final String path;
224
225 public BundleMetaLanguageResolver(Bundle bundle, String name, String path) {
226 super(bundle, LanguageResolver.class);
227 this.name = name;
228 this.path = path;
229 }
230
231 public Language resolveLanguage(String name, CamelContext context) {
232 LanguageResolver resolver = createInstance(this.name, path, context);
233 return resolver.resolveLanguage(name, context);
234 }
235
236 public void register() {
237 doRegister(LanguageResolver.class, "resolver", name);
238 }
239 }
240
241 protected static class BundleDataFormatResolver extends BaseResolver<DataFormat> implements DataFormatResolver {
242
243 private final Map<String, String> dataformats;
244
245 public BundleDataFormatResolver(Bundle bundle, Map<String, String> dataformats) {
246 super(bundle, DataFormat.class);
247 this.dataformats = dataformats;
248 }
249
250 public DataFormat resolveDataFormat(String name, CamelContext context) {
251 return createInstance(name, dataformats.get(name), context);
252 }
253
254 public DataFormatDefinition resolveDataFormatDefinition(String name, CamelContext context) {
255 return null;
256 }
257
258 public void register() {
259 doRegister(DataFormatResolver.class, "dataformat", dataformats.keySet());
260 }
261 }
262
263 protected static class BundleTypeConverterLoader extends BaseResolver<TypeConverter> implements TypeConverterLoader {
264
265 private final AnnotationTypeConverterLoader loader = new Loader();
266 private final Bundle bundle;
267
268 public BundleTypeConverterLoader(Bundle bundle) {
269 super(bundle, TypeConverter.class);
270 ObjectHelper.notNull(bundle, "bundle");
271 this.bundle = bundle;
272 }
273
274 public synchronized void load(TypeConverterRegistry registry) throws TypeConverterLoaderException {
275 // must be synchronized to ensure we don't load type converters concurrently
276 // which cause Camel apps to fails in OSGi thereafter
277 try {
278 loader.load(registry);
279 } catch (Exception e) {
280 throw new TypeConverterLoaderException("Cannot load type converters using OSGi bundle: " + bundle.getBundleId(), e);
281 }
282 }
283
284 public void register() {
285 doRegister(TypeConverterLoader.class);
286 }
287
288 class Loader extends AnnotationTypeConverterLoader {
289
290 Loader() {
291 super(null);
292 }
293
294 @SuppressWarnings("unchecked")
295 public void load(TypeConverterRegistry registry) throws TypeConverterLoaderException {
296 PackageScanFilter test = new AnnotatedWithPackageScanFilter(Converter.class, true);
297 Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
298 Set<String> packages = getConverterPackages(bundle.getEntry(META_INF_TYPE_CONVERTER));
299
300 if (LOG.isTraceEnabled()) {
301 LOG.trace("Found {} {} packages: {}", new Object[]{packages.size(), META_INF_TYPE_CONVERTER, packages});
302 }
303 // if we only have camel-core on the classpath then we have already pre-loaded all its type converters
304 // but we exposed the "org.apache.camel.core" package in camel-core. This ensures there is at least one
305 // packageName to scan, which triggers the scanning process. That allows us to ensure that we look for
306 // META-INF/services in all the JARs.
307 if (packages.size() == 1 && "org.apache.camel.core".equals(packages.iterator().next())) {
308 LOG.debug("No additional package names found in classpath for annotated type converters.");
309 // no additional package names found to load type converters so break out
310 return;
311 }
312
313 // now filter out org.apache.camel.core as its not needed anymore (it was just a dummy)
314 packages.remove("org.apache.camel.core");
315
316 for (String pkg : packages) {
317
318 if (StringHelper.hasUpperCase(pkg)) {
319 // its a FQN class name so load it directly
320 LOG.trace("Loading {} class", pkg);
321 try {
322 Class<?> clazz = bundle.loadClass(pkg);
323 if (test.matches(clazz)) {
324 classes.add(clazz);
325 }
326 // the class could be found and loaded so continue to next
327 continue;
328 } catch (Throwable t) {
329 // Ignore
330 LOG.trace("Failed to load " + pkg + " class due " + t.getMessage() + ". This exception will be ignored.", t);
331 }
332 }
333
334 // its not a FQN but a package name so scan for classes in the bundle
335 Enumeration<URL> e = bundle.findEntries("/" + pkg.replace('.', '/'), "*.class", true);
336 while (e != null && e.hasMoreElements()) {
337 String path = e.nextElement().getPath();
338 String externalName = path.substring(path.charAt(0) == '/' ? 1 : 0, path.indexOf('.')).replace('/', '.');
339 LOG.trace("Loading {} class", externalName);
340 try {
341 Class<?> clazz = bundle.loadClass(externalName);
342 if (test.matches(clazz)) {
343 classes.add(clazz);
344 }
345 } catch (Throwable t) {
346 // Ignore
347 LOG.trace("Failed to load " + externalName + " class due " + t.getMessage() + ". This exception will be ignored.", t);
348 }
349 }
350 }
351
352 // load the classes into type converter registry
353 LOG.info("Found {} @Converter classes to load", classes.size());
354 for (Class<?> type : classes) {
355 if (LOG.isTraceEnabled()) {
356 LOG.trace("Loading converter class: {}", ObjectHelper.name(type));
357 }
358 loadConverterMethods(registry, type);
359 }
360
361 // register fallback converters
362 URL fallbackUrl = bundle.getEntry(META_INF_FALLBACK_TYPE_CONVERTER);
363 if (fallbackUrl != null) {
364 TypeConverter tc = createInstance("FallbackTypeConverter", fallbackUrl, registry.getInjector());
365 registry.addFallbackTypeConverter(tc, false);
366 }
367
368 // now clear the maps so we do not hold references
369 visitedClasses.clear();
370 visitedURIs.clear();
371 }
372 }
373
374 }
375
376 protected abstract static class BaseResolver<T> extends BaseService {
377
378 private final Class<T> type;
379
380 public BaseResolver(Bundle bundle, Class<T> type) {
381 super(bundle);
382 this.type = type;
383 }
384
385 protected T createInstance(String name, String path, CamelContext context) {
386 if (path == null) {
387 return null;
388 }
389 URL url = bundle.getEntry(path);
390 LOG.debug("The entry {}'s url is {}", name, url);
391 return createInstance(name, url, context.getInjector());
392 }
393
394 @SuppressWarnings("unchecked")
395 protected T createInstance(String name, URL url, Injector injector) {
396 try {
397 Properties properties = loadProperties(url);
398 String classname = (String) properties.get("class");
399 Class<T> type = bundle.loadClass(classname);
400 if (!this.type.isAssignableFrom(type)) {
401 throw new IllegalArgumentException("Type is not a " + this.type.getName() + " implementation. Found: " + type.getName());
402 }
403 return injector.newInstance(type);
404 } catch (ClassNotFoundException e) {
405 throw new IllegalArgumentException("Invalid URI, no " + this.type.getName() + " registered for scheme : " + name, e);
406 }
407 }
408
409 }
410
411 protected abstract static class BaseService {
412
413 protected final Bundle bundle;
414 private ServiceRegistration reg;
415
416 protected BaseService(Bundle bundle) {
417 this.bundle = bundle;
418 }
419
420 public abstract void register();
421
422 protected void doRegister(Class<?> type, String key, Collection<String> value) {
423 doRegister(type, key, value.toArray(new String[value.size()]));
424 }
425
426 protected void doRegister(Class<?> type, String key, Object value) {
427 Hashtable<String, Object> props = new Hashtable<String, Object>();
428 props.put(key, value);
429 doRegister(type, props);
430 }
431
432 protected void doRegister(Class<?> type) {
433 doRegister(type, null);
434 }
435
436 protected void doRegister(Class<?> type, Dictionary<?, ?> props) {
437 reg = bundle.getBundleContext().registerService(type.getName(), this, props);
438 }
439
440 public void unregister() {
441 reg.unregister();
442 }
443 }
444
445 protected static Properties loadProperties(URL url) {
446 Properties properties = new Properties();
447 BufferedInputStream reader = null;
448 try {
449 reader = IOHelper.buffered(url.openStream());
450 properties.load(reader);
451 } catch (IOException e) {
452 throw new RuntimeException(e);
453 } finally {
454 IOHelper.close(reader, "properties", LOG);
455 }
456 return properties;
457 }
458
459 protected static boolean checkCompat(Bundle bundle, Class<?> clazz) {
460 // Check bundle compatibility
461 try {
462 if (bundle.loadClass(clazz.getName()) != clazz) {
463 return false;
464 }
465 } catch (Throwable t) {
466 return false;
467 }
468 return true;
469 }
470
471 protected static Set<String> getConverterPackages(URL resource) {
472 Set<String> packages = new LinkedHashSet<String>();
473 if (resource != null) {
474 BufferedReader reader = null;
475 try {
476 reader = IOHelper.buffered(new InputStreamReader(resource.openStream()));
477 while (true) {
478 String line = reader.readLine();
479 if (line == null) {
480 break;
481 }
482 line = line.trim();
483 if (line.startsWith("#") || line.length() == 0) {
484 continue;
485 }
486 StringTokenizer iter = new StringTokenizer(line, ",");
487 while (iter.hasMoreTokens()) {
488 String name = iter.nextToken().trim();
489 if (name.length() > 0) {
490 packages.add(name);
491 }
492 }
493 }
494 } catch (Exception ignore) {
495 // Do nothing here
496 } finally {
497 IOHelper.close(reader, null, LOG);
498 }
499 }
500 return packages;
501 }
502
503 }
504