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