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 }