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 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                public void load(TypeConverterRegistry registry) throws TypeConverterLoaderException {
295                    PackageScanFilter test = new AnnotatedWithPackageScanFilter(Converter.class, true);
296                    Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
297                    Set<String> packages = getConverterPackages(bundle.getEntry(META_INF_TYPE_CONVERTER));
298    
299                    if (LOG.isTraceEnabled()) {
300                        LOG.trace("Found {} {} packages: {}", new Object[]{packages.size(), META_INF_TYPE_CONVERTER, packages});
301                    }
302                    // if we only have camel-core on the classpath then we have already pre-loaded all its type converters
303                    // but we exposed the "org.apache.camel.core" package in camel-core. This ensures there is at least one
304                    // packageName to scan, which triggers the scanning process. That allows us to ensure that we look for
305                    // META-INF/services in all the JARs.
306                    if (packages.size() == 1 && "org.apache.camel.core".equals(packages.iterator().next())) {
307                        LOG.debug("No additional package names found in classpath for annotated type converters.");
308                        // no additional package names found to load type converters so break out
309                        return;
310                    }
311    
312                    // now filter out org.apache.camel.core as its not needed anymore (it was just a dummy)
313                    packages.remove("org.apache.camel.core");
314    
315                    for (String pkg : packages) {
316    
317                        if (StringHelper.hasUpperCase(pkg)) {
318                            // its a FQN class name so load it directly
319                            LOG.trace("Loading {} class", pkg);
320                            try {
321                                Class<?> clazz = bundle.loadClass(pkg);
322                                if (test.matches(clazz)) {
323                                    classes.add(clazz);
324                                }
325                                // the class could be found and loaded so continue to next
326                                continue;
327                            } catch (Throwable t) {
328                                // Ignore
329                                LOG.trace("Failed to load " + pkg + " class due " + t.getMessage() + ". This exception will be ignored.", t);
330                            }
331                        }
332    
333                        // its not a FQN but a package name so scan for classes in the bundle
334                        Enumeration<URL> e = bundle.findEntries("/" + pkg.replace('.', '/'), "*.class", true);
335                        while (e != null && e.hasMoreElements()) {
336                            String path = e.nextElement().getPath();
337                            String externalName = path.substring(path.charAt(0) == '/' ? 1 : 0, path.indexOf('.')).replace('/', '.');
338                            LOG.trace("Loading {} class", externalName);
339                            try {
340                                Class<?> clazz = bundle.loadClass(externalName);
341                                if (test.matches(clazz)) {
342                                    classes.add(clazz);
343                                }
344                            } catch (Throwable t) {
345                                // Ignore
346                                LOG.trace("Failed to load " + externalName + " class due " + t.getMessage() + ". This exception will be ignored.", t);
347                            }
348                        }
349                    }
350    
351                    // load the classes into type converter registry
352                    LOG.debug("Found {} @Converter classes to load", classes.size());
353                    for (Class<?> type : classes) {
354                        if (LOG.isTraceEnabled()) {
355                            LOG.trace("Loading converter class: {}", ObjectHelper.name(type));
356                        }
357                        loadConverterMethods(registry, type);
358                    }
359    
360                    // register fallback converters
361                    URL fallbackUrl = bundle.getEntry(META_INF_FALLBACK_TYPE_CONVERTER);
362                    if (fallbackUrl != null) {
363                        TypeConverter tc = createInstance("FallbackTypeConverter", fallbackUrl, registry.getInjector());
364                        registry.addFallbackTypeConverter(tc, false);
365                    }
366    
367                    // now clear the maps so we do not hold references
368                    visitedClasses.clear();
369                    visitedURIs.clear();
370                }
371            }
372    
373        }
374    
375        protected abstract static class BaseResolver<T> extends BaseService {
376    
377            private final Class<T> type;
378    
379            public BaseResolver(Bundle bundle, Class<T> type) {
380                super(bundle);
381                this.type = type;
382            }
383    
384            protected T createInstance(String name, String path, CamelContext context) {
385                if (path == null) {
386                    return null;
387                }
388                URL url = bundle.getEntry(path);
389                LOG.trace("The entry {}'s url is {}", name, url);
390                return createInstance(name, url, context.getInjector());
391            }
392    
393            @SuppressWarnings("unchecked")
394            protected T createInstance(String name, URL url, Injector injector) {
395                try {
396                    Properties properties = loadProperties(url);
397                    String classname = (String) properties.get("class");
398                    Class<?> type = bundle.loadClass(classname);
399                    if (!this.type.isAssignableFrom(type)) {
400                        throw new IllegalArgumentException("Type is not a " + this.type.getName() + " implementation. Found: " + type.getName());
401                    }
402                    return injector.newInstance((Class<T>) type);
403                } catch (ClassNotFoundException e) {
404                    throw new IllegalArgumentException("Invalid URI, no " + this.type.getName() + " registered for scheme : " + name, e);
405                }
406            }
407    
408        }
409    
410        protected abstract static class BaseService {
411    
412            protected final Bundle bundle;
413            private ServiceRegistration reg;
414    
415            protected BaseService(Bundle bundle) {
416                this.bundle = bundle;
417            }
418    
419            public abstract void register();
420    
421            protected void doRegister(Class<?> type, String key, Collection<String> value) {
422                doRegister(type, key, value.toArray(new String[value.size()]));
423            }
424    
425            protected void doRegister(Class<?> type, String key, Object value) {
426                Dictionary<String, Object> props = new Hashtable<String, Object>();
427                props.put(key, value);
428                doRegister(type, props);
429            }
430    
431            protected void doRegister(Class<?> type) {
432                doRegister(type, null);
433            }
434    
435            protected void doRegister(Class<?> type, Dictionary<String, ?> props) {
436                reg = bundle.getBundleContext().registerService(type.getName(), this, props);
437            }
438    
439            public void unregister() {
440                reg.unregister();
441            }
442        }
443    
444        protected static Properties loadProperties(URL url) {
445            Properties properties = new Properties();
446            BufferedInputStream reader = null;
447            try {
448                reader = IOHelper.buffered(url.openStream());
449                properties.load(reader);
450            } catch (IOException e) {
451                throw new RuntimeException(e);
452            } finally {
453                IOHelper.close(reader, "properties", LOG);
454            }
455            return properties;
456        }
457    
458        protected static boolean checkCompat(Bundle bundle, Class<?> clazz) {
459            // Check bundle compatibility
460            try {
461                if (bundle.loadClass(clazz.getName()) != clazz) {
462                    return false;
463                }
464            } catch (Throwable t) {
465                return false;
466            }
467            return true;
468        }
469    
470        protected static Set<String> getConverterPackages(URL resource) {
471            Set<String> packages = new LinkedHashSet<String>();
472            if (resource != null) {
473                BufferedReader reader = null;
474                try {
475                    reader = IOHelper.buffered(new InputStreamReader(resource.openStream()));
476                    while (true) {
477                        String line = reader.readLine();
478                        if (line == null) {
479                            break;
480                        }
481                        line = line.trim();
482                        if (line.startsWith("#") || line.length() == 0) {
483                            continue;
484                        }
485                        StringTokenizer iter = new StringTokenizer(line, ",");
486                        while (iter.hasMoreTokens()) {
487                            String name = iter.nextToken().trim();
488                            if (name.length() > 0) {
489                                packages.add(name);
490                            }
491                        }
492                    }
493                } catch (Exception ignore) {
494                    // Do nothing here
495                } finally {
496                    IOHelper.close(reader, null, LOG);
497                }
498            }
499            return packages;
500        }
501    
502    }
503