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 transient 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 = loader.loadClass(name);
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                        injector = handleHasConverterAnnotation(registry, type, injector, method);
268                    } else if (ObjectHelper.hasAnnotation(method, FallbackConverter.class, true)) {
269                        injector = handleHasFallbackConverterAnnotation(registry, type, injector, method);
270                    }
271                }
272    
273                Class<?> superclass = type.getSuperclass();
274                if (superclass != null && !superclass.equals(Object.class)) {
275                    loadConverterMethods(registry, superclass);
276                }
277            } catch (NoClassDefFoundError e) {
278                LOG.warn("Ignoring converter type: " + type.getCanonicalName() + " as a dependent class could not be found: " + e, e);
279            }
280        }
281    
282        private CachingInjector<?> handleHasConverterAnnotation(TypeConverterRegistry registry, Class<?> type, CachingInjector<?> injector, Method method) {
283            if (isValidConverterMethod(method)) {
284                int modifiers = method.getModifiers();
285                if (isAbstract(modifiers) || !isPublic(modifiers)) {
286                    LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: " + method
287                            + " as a converter method is not a public and concrete method");
288                } else {
289                    Class<?> toType = method.getReturnType();
290                    if (toType.equals(Void.class)) {
291                        LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: "
292                                + method + " as a converter method returns a void method");
293                    } else {
294                        Class<?> fromType = method.getParameterTypes()[0];
295                        if (isStatic(modifiers)) {
296                            registerTypeConverter(registry, method, toType, fromType,
297                                    new StaticMethodTypeConverter(method));
298                        } else {
299                            if (injector == null) {
300                                injector = new CachingInjector<Object>(registry, CastUtils.cast(type, Object.class));
301                            }
302                            registerTypeConverter(registry, method, toType, fromType,
303                                    new InstanceMethodTypeConverter(injector, method, registry));
304                        }
305                    }
306                }
307            } else {
308                LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: " + method
309                        + " as a converter method should have one parameter");
310            }
311            return injector;
312        }
313    
314        private CachingInjector<?> handleHasFallbackConverterAnnotation(TypeConverterRegistry registry, Class<?> type, CachingInjector<?> injector, Method method) {
315            if (isValidFallbackConverterMethod(method)) {
316                int modifiers = method.getModifiers();
317                if (isAbstract(modifiers) || !isPublic(modifiers)) {
318                    LOG.warn("Ignoring bad fallback converter on type: " + type.getCanonicalName() + " method: " + method
319                            + " as a fallback converter method is not a public and concrete method");
320                } else {
321                    Class<?> toType = method.getReturnType();
322                    if (toType.equals(Void.class)) {
323                        LOG.warn("Ignoring bad fallback converter on type: " + type.getCanonicalName() + " method: "
324                                + method + " as a fallback converter method returns a void method");
325                    } else {
326                        if (isStatic(modifiers)) {
327                            registerFallbackTypeConverter(registry, new StaticMethodFallbackTypeConverter(method, registry), method);
328                        } else {
329                            if (injector == null) {
330                                injector = new CachingInjector<Object>(registry, CastUtils.cast(type, Object.class));
331                            }
332                            registerFallbackTypeConverter(registry, new InstanceMethodFallbackTypeConverter(injector, method, registry), method);
333                        }
334                    }
335                }
336            } else {
337                LOG.warn("Ignoring bad fallback converter on type: " + type.getCanonicalName() + " method: " + method
338                        + " as a fallback converter method should have one parameter");
339            }
340            return injector;
341        }
342    
343        protected void registerTypeConverter(TypeConverterRegistry registry,
344                                             Method method, Class<?> toType, Class<?> fromType, TypeConverter typeConverter) {
345            registry.addTypeConverter(toType, fromType, typeConverter);
346        }
347    
348        protected boolean isValidConverterMethod(Method method) {
349            Class<?>[] parameterTypes = method.getParameterTypes();
350            return (parameterTypes != null) && (parameterTypes.length == 1
351                    || (parameterTypes.length == 2 && Exchange.class.isAssignableFrom(parameterTypes[1])));
352        }
353    
354        protected void registerFallbackTypeConverter(TypeConverterRegistry registry, TypeConverter typeConverter, Method method) {
355            boolean canPromote = false;
356            // check whether the annotation may indicate it can promote
357            if (method.getAnnotation(FallbackConverter.class) != null) {
358                canPromote = method.getAnnotation(FallbackConverter.class).canPromote();
359            }
360            registry.addFallbackTypeConverter(typeConverter, canPromote);
361        }
362    
363        protected boolean isValidFallbackConverterMethod(Method method) {
364            Class<?>[] parameterTypes = method.getParameterTypes();
365            return (parameterTypes != null) && (parameterTypes.length == 3
366                    || (parameterTypes.length == 4 && Exchange.class.isAssignableFrom(parameterTypes[1]))
367                    && (TypeConverterRegistry.class.isAssignableFrom(parameterTypes[parameterTypes.length - 1])));
368        }
369    
370        /**
371         * Filters the given list of packages
372         *
373         * @param name  the name to filter out
374         * @param packageNames the packages
375         * @return he packages without the given name
376         */
377        protected static String[] filterUnwantedPackage(String name, String[] packageNames) {
378            // the filtered packages to return
379            List<String> packages = new ArrayList<String>();
380    
381            for (String s : packageNames) {
382                if (!name.equals(s)) {
383                    packages.add(s);
384                }
385            }
386    
387            return packages.toArray(new String[packages.size()]);
388        }
389    
390    }