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