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.converter;
018    
019    import java.io.BufferedReader;
020    import java.io.IOException;
021    import java.io.InputStreamReader;
022    import java.lang.reflect.Method;
023    import java.net.URL;
024    import java.util.ArrayList;
025    import java.util.Arrays;
026    import java.util.Enumeration;
027    import java.util.HashSet;
028    import java.util.List;
029    import java.util.Set;
030    import java.util.StringTokenizer;
031    import static java.lang.reflect.Modifier.isAbstract;
032    import static java.lang.reflect.Modifier.isPublic;
033    import static java.lang.reflect.Modifier.isStatic;
034    
035    import org.apache.camel.Converter;
036    import org.apache.camel.Exchange;
037    import org.apache.camel.FallbackConverter;
038    import org.apache.camel.TypeConverter;
039    import org.apache.camel.TypeConverterLoaderException;
040    import org.apache.camel.spi.PackageScanClassResolver;
041    import org.apache.camel.spi.TypeConverterLoader;
042    import org.apache.camel.spi.TypeConverterRegistry;
043    import org.apache.camel.util.CastUtils;
044    import org.apache.camel.util.IOHelper;
045    import org.apache.camel.util.ObjectHelper;
046    import org.apache.camel.util.StringHelper;
047    import org.slf4j.Logger;
048    import org.slf4j.LoggerFactory;
049    
050    /**
051     * A class which will auto-discover {@link Converter} objects and methods to pre-load
052     * the {@link TypeConverterRegistry} of converters on startup.
053     * <p/>
054     * This implementation supports scanning for type converters in JAR files. The {@link #META_INF_SERVICES}
055     * contains a list of packages or FQN class names for {@link Converter} classes. The FQN class names
056     * is loaded first and directly by the class loader.
057     * <p/>
058     * The {@link PackageScanClassResolver} is being used to scan packages for {@link Converter} classes and
059     * this procedure is slower than loading the {@link Converter} classes directly by its FQN class name.
060     * Therefore its recommended to specify FQN class names in the {@link #META_INF_SERVICES} file.
061     * Likewise the procedure for scanning using {@link PackageScanClassResolver} may require custom implementations
062     * to work in various containers such as JBoss, OSGi, etc.
063     *
064     * @version 
065     */
066    public class AnnotationTypeConverterLoader implements TypeConverterLoader {
067        public static final String META_INF_SERVICES = "META-INF/services/org/apache/camel/TypeConverter";
068        private static final Logger LOG = LoggerFactory.getLogger(AnnotationTypeConverterLoader.class);
069        protected PackageScanClassResolver resolver;
070        protected Set<Class<?>> visitedClasses = new HashSet<Class<?>>();
071        protected Set<String> visitedURIs = new HashSet<String>();
072    
073        public AnnotationTypeConverterLoader(PackageScanClassResolver resolver) {
074            this.resolver = resolver;
075        }
076    
077        @Override
078        public void load(TypeConverterRegistry registry) throws TypeConverterLoaderException {
079            String[] packageNames;
080    
081            LOG.trace("Searching for {} services", META_INF_SERVICES);
082            try {
083                packageNames = findPackageNames();
084                if (packageNames == null || packageNames.length == 0) {
085                    throw new TypeConverterLoaderException("Cannot find package names to be used for classpath scanning for annotated type converters.");
086                }
087            } catch (Exception e) {
088                throw new TypeConverterLoaderException("Cannot find package names to be used for classpath scanning for annotated type converters.", e);
089            }
090    
091            // if we only have camel-core on the classpath then we have already pre-loaded all its type converters
092            // but we exposed the "org.apache.camel.core" package in camel-core. This ensures there is at least one
093            // packageName to scan, which triggers the scanning process. That allows us to ensure that we look for
094            // META-INF/services in all the JARs.
095            if (packageNames.length == 1 && "org.apache.camel.core".equals(packageNames[0])) {
096                LOG.debug("No additional package names found in classpath for annotated type converters.");
097                // no additional package names found to load type converters so break out
098                return;
099            }
100    
101            // now filter out org.apache.camel.core as its not needed anymore (it was just a dummy)
102            packageNames = filterUnwantedPackage("org.apache.camel.core", packageNames);
103    
104            // filter out package names which can be loaded as a class directly so we avoid package scanning which
105            // is much slower and does not work 100% in all runtime containers
106            Set<Class<?>> classes = new HashSet<Class<?>>();
107            packageNames = filterPackageNamesOnly(resolver, packageNames, classes);
108            if (!classes.isEmpty()) {
109                LOG.debug("Loaded " + classes.size() + " @Converter classes");
110            }
111    
112            // if there is any packages to scan and load @Converter classes, then do it
113            if (packageNames != null && packageNames.length > 0) {
114                LOG.trace("Found converter packages to scan: {}", packageNames);
115                Set<Class<?>> scannedClasses = resolver.findAnnotated(Converter.class, packageNames);
116                if (scannedClasses.isEmpty()) {
117                    throw new TypeConverterLoaderException("Cannot find any type converter classes from the following packages: " + Arrays.asList(packageNames));
118                }
119                LOG.debug("Found " + packageNames.length + " packages with " + scannedClasses.size() + " @Converter classes to load");
120                classes.addAll(scannedClasses);
121            }
122    
123            // load all the found classes into the type converter registry
124            for (Class<?> type : classes) {
125                if (LOG.isTraceEnabled()) {
126                    LOG.trace("Loading converter class: {}", ObjectHelper.name(type));
127                }
128                loadConverterMethods(registry, type);
129            }
130    
131            // now clear the maps so we do not hold references
132            visitedClasses.clear();
133            visitedURIs.clear();
134        }
135    
136        /**
137         * Filters the given list of packages and returns an array of <b>only</b> package names.
138         * <p/>
139         * This implementation will check the given list of packages, and if it contains a class name,
140         * that class will be loaded directly and added to the list of classes. This optimizes the
141         * type converter to avoid excessive file scanning for .class files.
142         *
143         * @param resolver the class resolver
144         * @param packageNames the package names
145         * @param classes to add loaded @Converter classes
146         * @return the filtered package names
147         */
148        protected String[] filterPackageNamesOnly(PackageScanClassResolver resolver, String[] packageNames, Set<Class<?>> classes) {
149            if (packageNames == null || packageNames.length == 0) {
150                return packageNames;
151            }
152    
153            // optimize for CorePackageScanClassResolver
154            if (resolver.getClassLoaders().isEmpty()) {
155                return packageNames;
156            }
157    
158            // the filtered packages to return
159            List<String> packages = new ArrayList<String>();
160    
161            // try to load it as a class first
162            for (String name : packageNames) {
163                // must be a FQN class name by having an upper case letter
164                if (StringHelper.hasUpperCase(name)) {
165                    Class<?> clazz = null;
166                    for (ClassLoader loader : resolver.getClassLoaders()) {
167                        try {
168                            clazz = ObjectHelper.loadClass(name, loader);
169                            LOG.trace("Loaded {} as class {}", name, clazz);
170                            classes.add(clazz);
171                            // class founder, so no need to load it with another class loader
172                            break;
173                        } catch (Throwable e) {
174                            // do nothing here
175                        }
176                    }
177                    if (clazz == null) {
178                        // ignore as its not a class (will be package scan afterwards)
179                        packages.add(name);
180                    }
181                } else {
182                    // ignore as its not a class (will be package scan afterwards)
183                    packages.add(name);
184                }
185            }
186    
187            // return the packages which is not FQN classes
188            return packages.toArray(new String[packages.size()]);
189        }
190    
191        /**
192         * Finds the names of the packages to search for on the classpath looking
193         * for text files on the classpath at the {@link #META_INF_SERVICES} location.
194         *
195         * @return a collection of packages to search for
196         * @throws IOException is thrown for IO related errors
197         */
198        protected String[] findPackageNames() throws IOException {
199            Set<String> packages = new HashSet<String>();
200            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
201            if (ccl != null) {
202                findPackages(packages, ccl);
203            }
204            findPackages(packages, getClass().getClassLoader());
205            return packages.toArray(new String[packages.size()]);
206        }
207    
208        protected void findPackages(Set<String> packages, ClassLoader classLoader) throws IOException {
209            Enumeration<URL> resources = classLoader.getResources(META_INF_SERVICES);
210            while (resources.hasMoreElements()) {
211                URL url = resources.nextElement();
212                String path = url.getPath();
213                if (!visitedURIs.contains(path)) {
214                    // remember we have visited this uri so we wont read it twice
215                    visitedURIs.add(path);
216                    LOG.debug("Loading file {} to retrieve list of packages, from url: {}", META_INF_SERVICES, url);
217                    BufferedReader reader = IOHelper.buffered(new InputStreamReader(url.openStream()));
218                    try {
219                        while (true) {
220                            String line = reader.readLine();
221                            if (line == null) {
222                                break;
223                            }
224                            line = line.trim();
225                            if (line.startsWith("#") || line.length() == 0) {
226                                continue;
227                            }
228                            tokenize(packages, line);
229                        }
230                    } finally {
231                        IOHelper.close(reader, null, LOG);
232                    }
233                }
234            }
235        }
236    
237        /**
238         * Tokenizes the line from the META-IN/services file using commas and
239         * ignoring whitespace between packages
240         */
241        private void tokenize(Set<String> packages, String line) {
242            StringTokenizer iter = new StringTokenizer(line, ",");
243            while (iter.hasMoreTokens()) {
244                String name = iter.nextToken().trim();
245                if (name.length() > 0) {
246                    packages.add(name);
247                }
248            }
249        }
250    
251        /**
252         * Loads all of the converter methods for the given type
253         */
254        protected void loadConverterMethods(TypeConverterRegistry registry, Class<?> type) {
255            if (visitedClasses.contains(type)) {
256                return;
257            }
258            visitedClasses.add(type);
259            try {
260                Method[] methods = type.getDeclaredMethods();
261                CachingInjector<?> injector = null;
262    
263                for (Method method : methods) {
264                    // this may be prone to ClassLoader or packaging problems when the same class is defined
265                    // in two different jars (as is the case sometimes with specs).
266                    if (ObjectHelper.hasAnnotation(method, Converter.class, true)) {
267                        boolean allowNull = false;
268                        if (method.getAnnotation(Converter.class) != null) {
269                            allowNull = method.getAnnotation(Converter.class).allowNull();
270                        }
271                        injector = handleHasConverterAnnotation(registry, type, injector, method, allowNull);
272                    } else if (ObjectHelper.hasAnnotation(method, FallbackConverter.class, true)) {
273                        boolean allowNull = false;
274                        if (method.getAnnotation(FallbackConverter.class) != null) {
275                            allowNull = method.getAnnotation(FallbackConverter.class).allowNull();
276                        }
277                        injector = handleHasFallbackConverterAnnotation(registry, type, injector, method, allowNull);
278                    }
279                }
280    
281                Class<?> superclass = type.getSuperclass();
282                if (superclass != null && !superclass.equals(Object.class)) {
283                    loadConverterMethods(registry, superclass);
284                }
285            } catch (NoClassDefFoundError e) {
286                LOG.warn("Ignoring converter type: " + type.getCanonicalName() + " as a dependent class could not be found: " + e, e);
287            }
288        }
289    
290        private CachingInjector<?> handleHasConverterAnnotation(TypeConverterRegistry registry, Class<?> type,
291                                                                CachingInjector<?> injector, Method method, boolean allowNull) {
292            if (isValidConverterMethod(method)) {
293                int modifiers = method.getModifiers();
294                if (isAbstract(modifiers) || !isPublic(modifiers)) {
295                    LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: " + method
296                            + " as a converter method is not a public and concrete method");
297                } else {
298                    Class<?> toType = method.getReturnType();
299                    if (toType.equals(Void.class)) {
300                        LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: "
301                                + method + " as a converter method returns a void method");
302                    } else {
303                        Class<?> fromType = method.getParameterTypes()[0];
304                        if (isStatic(modifiers)) {
305                            registerTypeConverter(registry, method, toType, fromType,
306                                    new StaticMethodTypeConverter(method, allowNull));
307                        } else {
308                            if (injector == null) {
309                                injector = new CachingInjector<Object>(registry, CastUtils.cast(type, Object.class));
310                            }
311                            registerTypeConverter(registry, method, toType, fromType,
312                                    new InstanceMethodTypeConverter(injector, method, registry, allowNull));
313                        }
314                    }
315                }
316            } else {
317                LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: " + method
318                        + " as a converter method should have one parameter");
319            }
320            return injector;
321        }
322    
323        private CachingInjector<?> handleHasFallbackConverterAnnotation(TypeConverterRegistry registry, Class<?> type,
324                                                                        CachingInjector<?> injector, Method method, boolean allowNull) {
325            if (isValidFallbackConverterMethod(method)) {
326                int modifiers = method.getModifiers();
327                if (isAbstract(modifiers) || !isPublic(modifiers)) {
328                    LOG.warn("Ignoring bad fallback converter on type: " + type.getCanonicalName() + " method: " + method
329                            + " as a fallback converter method is not a public and concrete method");
330                } else {
331                    Class<?> toType = method.getReturnType();
332                    if (toType.equals(Void.class)) {
333                        LOG.warn("Ignoring bad fallback converter on type: " + type.getCanonicalName() + " method: "
334                                + method + " as a fallback converter method returns a void method");
335                    } else {
336                        if (isStatic(modifiers)) {
337                            registerFallbackTypeConverter(registry, new StaticMethodFallbackTypeConverter(method, registry, allowNull), method);
338                        } else {
339                            if (injector == null) {
340                                injector = new CachingInjector<Object>(registry, CastUtils.cast(type, Object.class));
341                            }
342                            registerFallbackTypeConverter(registry, new InstanceMethodFallbackTypeConverter(injector, method, registry, allowNull), method);
343                        }
344                    }
345                }
346            } else {
347                LOG.warn("Ignoring bad fallback converter on type: " + type.getCanonicalName() + " method: " + method
348                        + " as a fallback converter method should have one parameter");
349            }
350            return injector;
351        }
352    
353        protected void registerTypeConverter(TypeConverterRegistry registry,
354                                             Method method, Class<?> toType, Class<?> fromType, TypeConverter typeConverter) {
355            registry.addTypeConverter(toType, fromType, typeConverter);
356        }
357    
358        protected boolean isValidConverterMethod(Method method) {
359            Class<?>[] parameterTypes = method.getParameterTypes();
360            return (parameterTypes != null) && (parameterTypes.length == 1
361                    || (parameterTypes.length == 2 && Exchange.class.isAssignableFrom(parameterTypes[1])));
362        }
363    
364        protected void registerFallbackTypeConverter(TypeConverterRegistry registry, TypeConverter typeConverter, Method method) {
365            boolean canPromote = false;
366            // check whether the annotation may indicate it can promote
367            if (method.getAnnotation(FallbackConverter.class) != null) {
368                canPromote = method.getAnnotation(FallbackConverter.class).canPromote();
369            }
370            registry.addFallbackTypeConverter(typeConverter, canPromote);
371        }
372    
373        protected boolean isValidFallbackConverterMethod(Method method) {
374            Class<?>[] parameterTypes = method.getParameterTypes();
375            return (parameterTypes != null) && (parameterTypes.length == 3
376                    || (parameterTypes.length == 4 && Exchange.class.isAssignableFrom(parameterTypes[1]))
377                    && (TypeConverterRegistry.class.isAssignableFrom(parameterTypes[parameterTypes.length - 1])));
378        }
379    
380        /**
381         * Filters the given list of packages
382         *
383         * @param name  the name to filter out
384         * @param packageNames the packages
385         * @return he packages without the given name
386         */
387        protected static String[] filterUnwantedPackage(String name, String[] packageNames) {
388            // the filtered packages to return
389            List<String> packages = new ArrayList<String>();
390    
391            for (String s : packageNames) {
392                if (!name.equals(s)) {
393                    packages.add(s);
394                }
395            }
396    
397            return packages.toArray(new String[packages.size()]);
398        }
399    
400    }