| /** |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.camel.impl.converter; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.lang.reflect.Method; |
| import java.net.URL; |
| import java.util.Arrays; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.Set; |
| import java.util.StringTokenizer; |
| import static java.lang.reflect.Modifier.isAbstract; |
| import static java.lang.reflect.Modifier.isPublic; |
| import static java.lang.reflect.Modifier.isStatic; |
| |
| import org.apache.camel.Converter; |
| import org.apache.camel.Exchange; |
| import org.apache.camel.FallbackConverter; |
| import org.apache.camel.TypeConverter; |
| import org.apache.camel.TypeConverterLoaderException; |
| import org.apache.camel.spi.PackageScanClassResolver; |
| import org.apache.camel.spi.TypeConverterLoader; |
| import org.apache.camel.spi.TypeConverterRegistry; |
| import org.apache.camel.util.CastUtils; |
| import org.apache.camel.util.IOHelper; |
| import org.apache.camel.util.ObjectHelper; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * A class which will auto-discover converter objects and methods to pre-load |
| * the registry of converters on startup |
| * |
| * @version |
| */ |
| public class AnnotationTypeConverterLoader implements TypeConverterLoader { |
| public static final String META_INF_SERVICES = "META-INF/services/org/apache/camel/TypeConverter"; |
| private static final transient Logger LOG = LoggerFactory.getLogger(AnnotationTypeConverterLoader.class); |
| protected PackageScanClassResolver resolver; |
| protected Set<Class<?>> visitedClasses = new HashSet<Class<?>>(); |
| protected Set<String> visitedURIs = new HashSet<String>(); |
| |
| public AnnotationTypeConverterLoader(PackageScanClassResolver resolver) { |
| this.resolver = resolver; |
| } |
| |
| public void load(TypeConverterRegistry registry) throws TypeConverterLoaderException { |
| String[] packageNames; |
| |
| LOG.trace("Searching for {} services", META_INF_SERVICES); |
| try { |
| packageNames = findPackageNames(); |
| if (packageNames == null || packageNames.length == 0) { |
| throw new TypeConverterLoaderException("Cannot find package names to be used for classpath scanning for annotated type converters."); |
| } |
| } catch (Exception e) { |
| throw new TypeConverterLoaderException("Cannot find package names to be used for classpath scanning for annotated type converters.", e); |
| } |
| |
| // if we only have camel-core on the classpath then we have already pre-loaded all its type converters |
| // but we exposed the "org.apache.camel.core" package in camel-core. This ensures there is at least one |
| // packageName to scan, which triggers the scanning process. That allows us to ensure that we look for |
| // type converters in all the JARs. |
| if (packageNames.length == 1 && "org.apache.camel.core".equals(packageNames[0])) { |
| LOG.debug("No additional package names found in classpath for annotated type converters."); |
| // no additional package names found to load type converters so break out |
| return; |
| } |
| |
| LOG.trace("Found converter packages to scan: {}", packageNames); |
| Set<Class<?>> classes = resolver.findAnnotated(Converter.class, packageNames); |
| if (classes == null || classes.isEmpty()) { |
| throw new TypeConverterLoaderException("Cannot find any type converter classes from the following packages: " + Arrays.asList(packageNames)); |
| } |
| |
| LOG.info("Found " + packageNames.length + " packages with " + classes.size() + " @Converter classes to load"); |
| |
| for (Class type : classes) { |
| if (LOG.isTraceEnabled()) { |
| LOG.trace("Loading converter class: {}", ObjectHelper.name(type)); |
| } |
| loadConverterMethods(registry, type); |
| } |
| |
| // now clear the maps so we do not hold references |
| visitedClasses.clear(); |
| visitedURIs.clear(); |
| } |
| |
| /** |
| * Finds the names of the packages to search for on the classpath looking |
| * for text files on the classpath at the {@link #META_INF_SERVICES} location. |
| * |
| * @return a collection of packages to search for |
| * @throws IOException is thrown for IO related errors |
| */ |
| protected String[] findPackageNames() throws IOException { |
| Set<String> packages = new HashSet<String>(); |
| ClassLoader ccl = Thread.currentThread().getContextClassLoader(); |
| if (ccl != null) { |
| findPackages(packages, ccl); |
| } |
| findPackages(packages, getClass().getClassLoader()); |
| return packages.toArray(new String[packages.size()]); |
| } |
| |
| protected void findPackages(Set<String> packages, ClassLoader classLoader) throws IOException { |
| Enumeration<URL> resources = classLoader.getResources(META_INF_SERVICES); |
| while (resources.hasMoreElements()) { |
| URL url = resources.nextElement(); |
| String path = url.getPath(); |
| if (!visitedURIs.contains(path)) { |
| // remember we have visited this uri so we wont read it twice |
| visitedURIs.add(path); |
| LOG.debug("Loading file {} to retrieve list of packages, from url: {}", META_INF_SERVICES, url); |
| BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream())); |
| try { |
| while (true) { |
| String line = reader.readLine(); |
| if (line == null) { |
| break; |
| } |
| line = line.trim(); |
| if (line.startsWith("#") || line.length() == 0) { |
| continue; |
| } |
| tokenize(packages, line); |
| } |
| } finally { |
| IOHelper.close(reader, null, LOG); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Tokenizes the line from the META-IN/services file using commas and |
| * ignoring whitespace between packages |
| */ |
| private void tokenize(Set<String> packages, String line) { |
| StringTokenizer iter = new StringTokenizer(line, ","); |
| while (iter.hasMoreTokens()) { |
| String name = iter.nextToken().trim(); |
| if (name.length() > 0) { |
| packages.add(name); |
| } |
| } |
| } |
| |
| /** |
| * Loads all of the converter methods for the given type |
| */ |
| protected void loadConverterMethods(TypeConverterRegistry registry, Class<?> type) { |
| if (visitedClasses.contains(type)) { |
| return; |
| } |
| visitedClasses.add(type); |
| try { |
| Method[] methods = type.getDeclaredMethods(); |
| CachingInjector<?> injector = null; |
| |
| for (Method method : methods) { |
| // this may be prone to ClassLoader or packaging problems when the same class is defined |
| // in two different jars (as is the case sometimes with specs). |
| if (ObjectHelper.hasAnnotation(method, Converter.class, true)) { |
| injector = handleHasConverterAnnotation(registry, type, injector, method); |
| } else if (ObjectHelper.hasAnnotation(method, FallbackConverter.class, true)) { |
| injector = handleHasFallbackConverterAnnotation(registry, type, injector, method); |
| } |
| } |
| |
| Class<?> superclass = type.getSuperclass(); |
| if (superclass != null && !superclass.equals(Object.class)) { |
| loadConverterMethods(registry, superclass); |
| } |
| } catch (NoClassDefFoundError e) { |
| LOG.warn("Ignoring converter type: " + type.getCanonicalName() + " as a dependent class could not be found: " + e, e); |
| } |
| } |
| |
| private CachingInjector<?> handleHasConverterAnnotation(TypeConverterRegistry registry, Class<?> type, CachingInjector<?> injector, Method method) { |
| if (isValidConverterMethod(method)) { |
| int modifiers = method.getModifiers(); |
| if (isAbstract(modifiers) || !isPublic(modifiers)) { |
| LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: " + method |
| + " as a converter method is not a public and concrete method"); |
| } else { |
| Class<?> toType = method.getReturnType(); |
| if (toType.equals(Void.class)) { |
| LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: " |
| + method + " as a converter method returns a void method"); |
| } else { |
| Class<?> fromType = method.getParameterTypes()[0]; |
| if (isStatic(modifiers)) { |
| registerTypeConverter(registry, method, toType, fromType, |
| new StaticMethodTypeConverter(method)); |
| } else { |
| if (injector == null) { |
| injector = new CachingInjector<Object>(registry, CastUtils.cast(type, Object.class)); |
| } |
| registerTypeConverter(registry, method, toType, fromType, |
| new InstanceMethodTypeConverter(injector, method, registry)); |
| } |
| } |
| } |
| } else { |
| LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: " + method |
| + " as a converter method should have one parameter"); |
| } |
| return injector; |
| } |
| |
| private CachingInjector<?> handleHasFallbackConverterAnnotation(TypeConverterRegistry registry, Class<?> type, CachingInjector<?> injector, Method method) { |
| if (isValidFallbackConverterMethod(method)) { |
| int modifiers = method.getModifiers(); |
| if (isAbstract(modifiers) || !isPublic(modifiers)) { |
| LOG.warn("Ignoring bad fallback converter on type: " + type.getCanonicalName() + " method: " + method |
| + " as a fallback converter method is not a public and concrete method"); |
| } else { |
| Class<?> toType = method.getReturnType(); |
| if (toType.equals(Void.class)) { |
| LOG.warn("Ignoring bad fallback converter on type: " + type.getCanonicalName() + " method: " |
| + method + " as a fallback converter method returns a void method"); |
| } else { |
| if (isStatic(modifiers)) { |
| registerFallbackTypeConverter(registry, new StaticMethodFallbackTypeConverter(method, registry), method); |
| } else { |
| if (injector == null) { |
| injector = new CachingInjector<Object>(registry, CastUtils.cast(type, Object.class)); |
| } |
| registerFallbackTypeConverter(registry, new InstanceMethodFallbackTypeConverter(injector, method, registry), method); |
| } |
| } |
| } |
| } else { |
| LOG.warn("Ignoring bad fallback converter on type: " + type.getCanonicalName() + " method: " + method |
| + " as a fallback converter method should have one parameter"); |
| } |
| return injector; |
| } |
| |
| protected void registerTypeConverter(TypeConverterRegistry registry, |
| Method method, Class<?> toType, Class<?> fromType, TypeConverter typeConverter) { |
| registry.addTypeConverter(toType, fromType, typeConverter); |
| } |
| |
| protected boolean isValidConverterMethod(Method method) { |
| Class<?>[] parameterTypes = method.getParameterTypes(); |
| return (parameterTypes != null) && (parameterTypes.length == 1 |
| || (parameterTypes.length == 2 && Exchange.class.isAssignableFrom(parameterTypes[1]))); |
| } |
| |
| protected void registerFallbackTypeConverter(TypeConverterRegistry registry, TypeConverter typeConverter, Method method) { |
| boolean canPromote = false; |
| // check whether the annotation may indicate it can promote |
| if (method.getAnnotation(FallbackConverter.class) != null) { |
| canPromote = method.getAnnotation(FallbackConverter.class).canPromote(); |
| } |
| registry.addFallbackTypeConverter(typeConverter, canPromote); |
| } |
| |
| protected boolean isValidFallbackConverterMethod(Method method) { |
| Class<?>[] parameterTypes = method.getParameterTypes(); |
| return (parameterTypes != null) && (parameterTypes.length == 3 |
| || (parameterTypes.length == 4 && Exchange.class.isAssignableFrom(parameterTypes[1])) |
| && (TypeConverterRegistry.class.isAssignableFrom(parameterTypes[parameterTypes.length - 1]))); |
| } |
| } |