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 */
017package org.apache.camel.impl.converter;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.concurrent.ConcurrentHashMap;
027import java.util.concurrent.ConcurrentMap;
028import java.util.concurrent.CopyOnWriteArrayList;
029import java.util.concurrent.ExecutionException;
030import java.util.concurrent.atomic.AtomicLong;
031
032import org.apache.camel.CamelExecutionException;
033import org.apache.camel.Exchange;
034import org.apache.camel.NoFactoryAvailableException;
035import org.apache.camel.NoTypeConversionAvailableException;
036import org.apache.camel.TypeConversionException;
037import org.apache.camel.TypeConverter;
038import org.apache.camel.spi.FactoryFinder;
039import org.apache.camel.spi.Injector;
040import org.apache.camel.spi.PackageScanClassResolver;
041import org.apache.camel.spi.TypeConverterAware;
042import org.apache.camel.spi.TypeConverterLoader;
043import org.apache.camel.spi.TypeConverterRegistry;
044import org.apache.camel.support.ServiceSupport;
045import org.apache.camel.util.LRUSoftCache;
046import org.apache.camel.util.MessageHelper;
047import org.apache.camel.util.ObjectHelper;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051/**
052 * Base implementation of a type converter registry used for
053 * <a href="http://camel.apache.org/type-converter.html">type converters</a> in Camel.
054 *
055 * @version 
056 */
057public abstract class BaseTypeConverterRegistry extends ServiceSupport implements TypeConverter, TypeConverterRegistry {
058    protected final Logger log = LoggerFactory.getLogger(getClass());
059    protected final ConcurrentMap<TypeMapping, TypeConverter> typeMappings = new ConcurrentHashMap<TypeMapping, TypeConverter>();
060    // for misses use a soft reference cache map, as the classes may be un-deployed at runtime
061    protected final LRUSoftCache<TypeMapping, TypeMapping> misses = new LRUSoftCache<TypeMapping, TypeMapping>(1000);
062    protected final List<TypeConverterLoader> typeConverterLoaders = new ArrayList<TypeConverterLoader>();
063    protected final List<FallbackTypeConverter> fallbackConverters = new CopyOnWriteArrayList<FallbackTypeConverter>();
064    protected final PackageScanClassResolver resolver;
065    protected Injector injector;
066    protected final FactoryFinder factoryFinder;
067    protected final Statistics statistics = new UtilizationStatistics();
068    protected final AtomicLong noopCounter = new AtomicLong();
069    protected final AtomicLong attemptCounter = new AtomicLong();
070    protected final AtomicLong missCounter = new AtomicLong();
071    protected final AtomicLong hitCounter = new AtomicLong();
072    protected final AtomicLong failedCounter = new AtomicLong();
073
074    public BaseTypeConverterRegistry(PackageScanClassResolver resolver, Injector injector, FactoryFinder factoryFinder) {
075        this.resolver = resolver;
076        this.injector = injector;
077        this.factoryFinder = factoryFinder;
078        this.typeConverterLoaders.add(new AnnotationTypeConverterLoader(resolver));
079
080        // add to string first as it will then be last in the last as to string can nearly
081        // always convert something to a string so we want it only as the last resort
082        // ToStringTypeConverter should NOT allow to be promoted
083        addFallbackTypeConverter(new ToStringTypeConverter(), false);
084        // enum is okay to be promoted
085        addFallbackTypeConverter(new EnumTypeConverter(), true);
086        // arrays is okay to be promoted
087        addFallbackTypeConverter(new ArrayTypeConverter(), true);
088        // and future should also not allowed to be promoted
089        addFallbackTypeConverter(new FutureTypeConverter(this), false);
090        // add sync processor to async processor converter is to be promoted
091        addFallbackTypeConverter(new AsyncProcessorTypeConverter(), true);
092    }
093
094    public List<TypeConverterLoader> getTypeConverterLoaders() {
095        return typeConverterLoaders;
096    }
097
098    @Override
099    public <T> T convertTo(Class<T> type, Object value) {
100        return convertTo(type, null, value);
101    }
102
103    @SuppressWarnings("unchecked")
104    @Override
105    public <T> T convertTo(Class<T> type, Exchange exchange, Object value) {
106        if (!isRunAllowed()) {
107            throw new IllegalStateException(this + " is not started");
108        }
109
110        Object answer;
111        try {
112            answer = doConvertTo(type, exchange, value, false);
113        } catch (Exception e) {
114            if (statistics.isStatisticsEnabled()) {
115                failedCounter.incrementAndGet();
116            }
117            // if its a ExecutionException then we have rethrow it as its not due to failed conversion
118            // this is special for FutureTypeConverter
119            boolean execution = ObjectHelper.getException(ExecutionException.class, e) != null
120                    || ObjectHelper.getException(CamelExecutionException.class, e) != null;
121            if (execution) {
122                throw ObjectHelper.wrapCamelExecutionException(exchange, e);
123            }
124
125            // error occurred during type conversion
126            if (e instanceof TypeConversionException) {
127                throw (TypeConversionException) e;
128            } else {
129                throw createTypeConversionException(exchange, type, value, e);
130            }
131        }
132        if (answer == Void.TYPE) {
133            if (statistics.isStatisticsEnabled()) {
134                missCounter.incrementAndGet();
135            }
136            // Could not find suitable conversion
137            return null;
138        } else {
139            if (statistics.isStatisticsEnabled()) {
140                hitCounter.incrementAndGet();
141            }
142            return (T) answer;
143        }
144    }
145
146    @Override
147    public <T> T mandatoryConvertTo(Class<T> type, Object value) throws NoTypeConversionAvailableException {
148        return mandatoryConvertTo(type, null, value);
149    }
150
151    @SuppressWarnings("unchecked")
152    @Override
153    public <T> T mandatoryConvertTo(Class<T> type, Exchange exchange, Object value) throws NoTypeConversionAvailableException {
154        if (!isRunAllowed()) {
155            throw new IllegalStateException(this + " is not started");
156        }
157
158        Object answer;
159        try {
160            answer = doConvertTo(type, exchange, value, false);
161        } catch (Exception e) {
162            if (statistics.isStatisticsEnabled()) {
163                failedCounter.incrementAndGet();
164            }
165            // error occurred during type conversion
166            if (e instanceof TypeConversionException) {
167                throw (TypeConversionException) e;
168            } else {
169                throw createTypeConversionException(exchange, type, value, e);
170            }
171        }
172        if (answer == Void.TYPE || value == null) {
173            if (statistics.isStatisticsEnabled()) {
174                missCounter.incrementAndGet();
175            }
176            // Could not find suitable conversion
177            throw new NoTypeConversionAvailableException(value, type);
178        } else {
179            if (statistics.isStatisticsEnabled()) {
180                hitCounter.incrementAndGet();
181            }
182            return (T) answer;
183        }
184    }
185
186    @Override
187    public <T> T tryConvertTo(Class<T> type, Object value) {
188        return tryConvertTo(type, null, value);
189    }
190
191    @SuppressWarnings("unchecked")
192    @Override
193    public <T> T tryConvertTo(Class<T> type, Exchange exchange, Object value) {
194        if (!isRunAllowed()) {
195            return null;
196        }
197
198        Object answer;
199        try {
200            answer = doConvertTo(type, exchange, value, true);
201        } catch (Exception e) {
202            if (statistics.isStatisticsEnabled()) {
203                failedCounter.incrementAndGet();
204            }
205            return null;
206        }
207        if (answer == Void.TYPE) {
208            // Could not find suitable conversion
209            if (statistics.isStatisticsEnabled()) {
210                missCounter.incrementAndGet();
211            }
212            return null;
213        } else {
214            if (statistics.isStatisticsEnabled()) {
215                hitCounter.incrementAndGet();
216            }
217            return (T) answer;
218        }
219    }
220
221    protected Object doConvertTo(final Class<?> type, final Exchange exchange, final Object value, final boolean tryConvert) {
222        if (log.isTraceEnabled()) {
223            log.trace("Converting {} -> {} with value: {}",
224                    new Object[]{value == null ? "null" : value.getClass().getCanonicalName(), 
225                        type.getCanonicalName(), value});
226        }
227
228        if (value == null) {
229            // no type conversion was needed
230            if (statistics.isStatisticsEnabled()) {
231                noopCounter.incrementAndGet();
232            }
233            // lets avoid NullPointerException when converting to boolean for null values
234            if (boolean.class.isAssignableFrom(type)) {
235                return Boolean.FALSE;
236            }
237            return null;
238        }
239
240        // same instance type
241        if (type.isInstance(value)) {
242            // no type conversion was needed
243            if (statistics.isStatisticsEnabled()) {
244                noopCounter.incrementAndGet();
245            }
246            return type.cast(value);
247        }
248
249        // special for NaN numbers, which we can only convert for floating numbers
250        if (ObjectHelper.isNaN(value)) {
251            // no type conversion was needed
252            if (statistics.isStatisticsEnabled()) {
253                noopCounter.incrementAndGet();
254            }
255            if (Float.class.isAssignableFrom(type)) {
256                return Float.NaN;
257            } else if (Double.class.isAssignableFrom(type)) {
258                return Double.NaN;
259            } else {
260                // we cannot convert the NaN
261                return Void.TYPE;
262            }
263        }
264
265        // okay we need to attempt to convert
266        if (statistics.isStatisticsEnabled()) {
267            attemptCounter.incrementAndGet();
268        }
269
270        // check if we have tried it before and if its a miss
271        TypeMapping key = new TypeMapping(type, value.getClass());
272        if (misses.containsKey(key)) {
273            // we have tried before but we cannot convert this one
274            return Void.TYPE;
275        }
276        
277        // try to find a suitable type converter
278        TypeConverter converter = getOrFindTypeConverter(key);
279        if (converter != null) {
280            log.trace("Using converter: {} to convert {}", converter, key);
281            Object rc;
282            if (tryConvert) {
283                rc = converter.tryConvertTo(type, exchange, value);
284            } else {
285                rc = converter.convertTo(type, exchange, value);
286            }
287            if (rc == null && converter.allowNull()) {
288                return null;
289            } else if (rc != null) {
290                return rc;
291            }
292        }
293
294        // not found with that type then if it was a primitive type then try again with the wrapper type
295        if (type.isPrimitive()) {
296            Class<?> primitiveType = ObjectHelper.convertPrimitiveTypeToWrapperType(type);
297            if (primitiveType != type) {
298                Class<?> fromType = value.getClass();
299                TypeConverter tc = getOrFindTypeConverter(new TypeMapping(primitiveType, fromType));
300                if (tc != null) {
301                    // add the type as a known type converter as we can convert from primitive to object converter
302                    addTypeConverter(type, fromType, tc);
303                    Object rc;
304                    if (tryConvert) {
305                        rc = tc.tryConvertTo(primitiveType, exchange, value);
306                    } else {
307                        rc = tc.convertTo(primitiveType, exchange, value);
308                    }
309                    if (rc == null && tc.allowNull()) {
310                        return null;
311                    } else if (rc != null) {
312                        return rc;
313                    }
314                }
315            }
316        }
317
318        // fallback converters
319        for (FallbackTypeConverter fallback : fallbackConverters) {
320            TypeConverter tc = fallback.getFallbackTypeConverter();
321            Object rc;
322            if (tryConvert) {
323                rc = tc.tryConvertTo(type, exchange, value);
324            } else {
325                rc = tc.convertTo(type, exchange, value);
326            }
327            if (rc == null && tc.allowNull()) {
328                return null;
329            }
330
331            if (Void.TYPE.equals(rc)) {
332                // it cannot be converted so give up
333                return Void.TYPE;
334            }
335
336            if (rc != null) {
337                // if fallback can promote then let it be promoted to a first class type converter
338                if (fallback.isCanPromote()) {
339                    // add it as a known type converter since we found a fallback that could do it
340                    if (log.isDebugEnabled()) {
341                        log.debug("Promoting fallback type converter as a known type converter to convert from: {} to: {} for the fallback converter: {}",
342                                new Object[]{type.getCanonicalName(), value.getClass().getCanonicalName(), fallback.getFallbackTypeConverter()});
343                    }
344                    addTypeConverter(type, value.getClass(), fallback.getFallbackTypeConverter());
345                }
346
347                if (log.isTraceEnabled()) {
348                    log.trace("Fallback type converter {} converted type from: {} to: {}",
349                            new Object[]{fallback.getFallbackTypeConverter(),
350                                type.getCanonicalName(), value.getClass().getCanonicalName()});
351                }
352
353                // return converted value
354                return rc;
355            }
356        }
357
358        if (!tryConvert) {
359            // Could not find suitable conversion, so remember it
360            // do not register misses for try conversions
361            misses.put(key, key);
362        }
363
364        // Could not find suitable conversion, so return Void to indicate not found
365        return Void.TYPE;
366    }
367
368    @Override
369    public void addTypeConverter(Class<?> toType, Class<?> fromType, TypeConverter typeConverter) {
370        log.trace("Adding type converter: {}", typeConverter);
371        TypeMapping key = new TypeMapping(toType, fromType);
372        TypeConverter converter = typeMappings.get(key);
373        // only override it if its different
374        // as race conditions can lead to many threads trying to promote the same fallback converter
375        if (typeConverter != converter) {
376            if (converter != null) {
377                log.warn("Overriding type converter from: " + converter + " to: " + typeConverter);
378            }
379            typeMappings.put(key, typeConverter);
380            // remove any previous misses, as we added the new type converter
381            misses.remove(key);
382        }
383    }
384
385    @Override
386    public boolean removeTypeConverter(Class<?> toType, Class<?> fromType) {
387        log.trace("Removing type converter from: {} to: {}", fromType, toType);
388        TypeMapping key = new TypeMapping(toType, fromType);
389        TypeConverter converter = typeMappings.remove(key);
390        if (converter != null) {
391            typeMappings.remove(key);
392            misses.remove(key);
393        }
394        return converter != null;
395    }
396
397    @Override
398    public void addFallbackTypeConverter(TypeConverter typeConverter, boolean canPromote) {
399        log.trace("Adding fallback type converter: {} which can promote: {}", typeConverter, canPromote);
400
401        // add in top of fallback as the toString() fallback will nearly always be able to convert
402        // the last one which is add to the FallbackTypeConverter will be called at the first place
403        fallbackConverters.add(0, new FallbackTypeConverter(typeConverter, canPromote));
404        if (typeConverter instanceof TypeConverterAware) {
405            TypeConverterAware typeConverterAware = (TypeConverterAware) typeConverter;
406            typeConverterAware.setTypeConverter(this);
407        }
408    }
409
410    public TypeConverter getTypeConverter(Class<?> toType, Class<?> fromType) {
411        TypeMapping key = new TypeMapping(toType, fromType);
412        return typeMappings.get(key);
413    }
414
415    @Override
416    public Injector getInjector() {
417        return injector;
418    }
419
420    @Override
421    public void setInjector(Injector injector) {
422        this.injector = injector;
423    }
424
425    public Set<Class<?>> getFromClassMappings() {
426        Set<Class<?>> answer = new HashSet<Class<?>>();
427        for (TypeMapping mapping : typeMappings.keySet()) {
428            answer.add(mapping.getFromType());
429        }
430        return answer;
431    }
432
433    public Map<Class<?>, TypeConverter> getToClassMappings(Class<?> fromClass) {
434        Map<Class<?>, TypeConverter> answer = new HashMap<Class<?>, TypeConverter>();
435        for (Map.Entry<TypeMapping, TypeConverter> entry : typeMappings.entrySet()) {
436            TypeMapping mapping = entry.getKey();
437            if (mapping.isApplicable(fromClass)) {
438                answer.put(mapping.getToType(), entry.getValue());
439            }
440        }
441        return answer;
442    }
443
444    public Map<TypeMapping, TypeConverter> getTypeMappings() {
445        return typeMappings;
446    }
447
448    protected <T> TypeConverter getOrFindTypeConverter(TypeMapping key) {
449        TypeConverter converter = typeMappings.get(key);
450        if (converter == null) {
451            // converter not found, try to lookup then
452            converter = lookup(key.getToType(), key.getFromType());
453            if (converter != null) {
454                typeMappings.putIfAbsent(key, converter);
455            }
456        }
457        return converter;
458    }
459
460    @Override
461    public TypeConverter lookup(Class<?> toType, Class<?> fromType) {
462        return doLookup(toType, fromType, false);
463    }
464
465    protected TypeConverter doLookup(Class<?> toType, Class<?> fromType, boolean isSuper) {
466
467        if (fromType != null) {
468            // lets try if there is a direct match
469            TypeConverter converter = getTypeConverter(toType, fromType);
470            if (converter != null) {
471                return converter;
472            }
473
474            // try the interfaces
475            for (Class<?> type : fromType.getInterfaces()) {
476                converter = getTypeConverter(toType, type);
477                if (converter != null) {
478                    return converter;
479                }
480            }
481
482            // try super then
483            Class<?> fromSuperClass = fromType.getSuperclass();
484            if (fromSuperClass != null && !fromSuperClass.equals(Object.class)) {
485                converter = doLookup(toType, fromSuperClass, true);
486                if (converter != null) {
487                    return converter;
488                }
489            }
490        }
491
492        // only do these tests as fallback and only on the target type (eg not on its super)
493        if (!isSuper) {
494            if (fromType != null && !fromType.equals(Object.class)) {
495
496                // lets try classes derived from this toType
497                Set<Map.Entry<TypeMapping, TypeConverter>> entries = typeMappings.entrySet();
498                for (Map.Entry<TypeMapping, TypeConverter> entry : entries) {
499                    TypeMapping key = entry.getKey();
500                    Class<?> aToType = key.getToType();
501                    if (toType.isAssignableFrom(aToType)) {
502                        Class<?> aFromType = key.getFromType();
503                        // skip Object based we do them last
504                        if (!aFromType.equals(Object.class) && aFromType.isAssignableFrom(fromType)) {
505                            return entry.getValue();
506                        }
507                    }
508                }
509
510                // lets test for Object based converters as last resort
511                TypeConverter converter = getTypeConverter(toType, Object.class);
512                if (converter != null) {
513                    return converter;
514                }
515            }
516        }
517
518        // none found
519        return null;
520    }
521
522    public List<Class<?>[]> listAllTypeConvertersFromTo() {
523        List<Class<?>[]> answer = new ArrayList<Class<?>[]>(typeMappings.size());
524        for (TypeMapping mapping : typeMappings.keySet()) {
525            answer.add(new Class<?>[]{mapping.getFromType(), mapping.getToType()});
526        }
527        return answer;
528    }
529
530    /**
531     * Loads the core type converters which is mandatory to use Camel
532     */
533    public void loadCoreTypeConverters() throws Exception {
534        // load all the type converters from camel-core
535        CoreTypeConverterLoader core = new CoreTypeConverterLoader();
536        core.load(this);
537    }
538
539    /**
540     * Checks if the registry is loaded and if not lazily load it
541     */
542    protected void loadTypeConverters() throws Exception {
543        for (TypeConverterLoader typeConverterLoader : getTypeConverterLoaders()) {
544            typeConverterLoader.load(this);
545        }
546
547        // lets try load any other fallback converters
548        try {
549            loadFallbackTypeConverters();
550        } catch (NoFactoryAvailableException e) {
551            // ignore its fine to have none
552        }
553    }
554
555    protected void loadFallbackTypeConverters() throws IOException, ClassNotFoundException {
556        List<TypeConverter> converters = factoryFinder.newInstances("FallbackTypeConverter", getInjector(), TypeConverter.class);
557        for (TypeConverter converter : converters) {
558            addFallbackTypeConverter(converter, false);
559        }
560    }
561
562    protected TypeConversionException createTypeConversionException(Exchange exchange, Class<?> type, Object value, Throwable cause) {
563        Object body;
564        // extract the body for logging which allows to limit the message body in the exception/stacktrace
565        // and also can be used to turn off logging sensitive message data
566        if (exchange != null) {
567            body = MessageHelper.extractValueForLogging(value, exchange.getIn());
568        } else {
569            body = value;
570        }
571        return new TypeConversionException(body, type, cause);
572    }
573
574    @Override
575    public Statistics getStatistics() {
576        return statistics;
577    }
578
579    @Override
580    public int size() {
581        return typeMappings.size();
582    }
583
584    @Override
585    protected void doStart() throws Exception {
586        // noop
587    }
588
589    @Override
590    protected void doStop() throws Exception {
591        // log utilization statistics when stopping, including mappings
592        if (statistics.isStatisticsEnabled()) {
593            String info = statistics.toString();
594            info += String.format(" mappings[total=%s, misses=%s]", typeMappings.size(), misses.size());
595            log.info(info);
596        }
597
598        typeMappings.clear();
599        misses.clear();
600        statistics.reset();
601    }
602
603    /**
604     * Represents utilization statistics
605     */
606    private final class UtilizationStatistics implements Statistics {
607
608        private boolean statisticsEnabled;
609
610        @Override
611        public long getNoopCounter() {
612            return noopCounter.get();
613        }
614
615        @Override
616        public long getAttemptCounter() {
617            return attemptCounter.get();
618        }
619
620        @Override
621        public long getHitCounter() {
622            return hitCounter.get();
623        }
624
625        @Override
626        public long getMissCounter() {
627            return missCounter.get();
628        }
629
630        @Override
631        public long getFailedCounter() {
632            return failedCounter.get();
633        }
634
635        @Override
636        public void reset() {
637            noopCounter.set(0);
638            attemptCounter.set(0);
639            hitCounter.set(0);
640            missCounter.set(0);
641            failedCounter.set(0);
642        }
643
644        @Override
645        public boolean isStatisticsEnabled() {
646            return statisticsEnabled;
647        }
648
649        @Override
650        public void setStatisticsEnabled(boolean statisticsEnabled) {
651            this.statisticsEnabled = statisticsEnabled;
652        }
653
654        @Override
655        public String toString() {
656            return String.format("TypeConverterRegistry utilization[noop=%s, attempts=%s, hits=%s, misses=%s, failures=%s]",
657                    getNoopCounter(), getAttemptCounter(), getHitCounter(), getMissCounter(), getFailedCounter());
658        }
659    }
660
661    /**
662     * Represents a mapping from one type (which can be null) to another
663     */
664    protected static class TypeMapping {
665        private final Class<?> toType;
666        private final Class<?> fromType;
667
668        TypeMapping(Class<?> toType, Class<?> fromType) {
669            this.toType = toType;
670            this.fromType = fromType;
671        }
672
673        public Class<?> getFromType() {
674            return fromType;
675        }
676
677        public Class<?> getToType() {
678            return toType;
679        }
680
681        @Override
682        public boolean equals(Object object) {
683            if (object instanceof TypeMapping) {
684                TypeMapping that = (TypeMapping) object;
685                return ObjectHelper.equal(this.fromType, that.fromType)
686                        && ObjectHelper.equal(this.toType, that.toType);
687            }
688            return false;
689        }
690
691        @Override
692        public int hashCode() {
693            int answer = toType.hashCode();
694            if (fromType != null) {
695                answer *= 37 + fromType.hashCode();
696            }
697            return answer;
698        }
699
700        @Override
701        public String toString() {
702            return "[" + fromType + "=>" + toType + "]";
703        }
704
705        public boolean isApplicable(Class<?> fromClass) {
706            return fromType.isAssignableFrom(fromClass);
707        }
708    }
709
710    /**
711     * Represents a fallback type converter
712     */
713    protected static class FallbackTypeConverter {
714        private final boolean canPromote;
715        private final TypeConverter fallbackTypeConverter;
716
717        FallbackTypeConverter(TypeConverter fallbackTypeConverter, boolean canPromote) {
718            this.canPromote = canPromote;
719            this.fallbackTypeConverter = fallbackTypeConverter;
720        }
721
722        public boolean isCanPromote() {
723            return canPromote;
724        }
725
726        public TypeConverter getFallbackTypeConverter() {
727            return fallbackTypeConverter;
728        }
729    }
730}