001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.core.convert.support;
018
019import java.lang.reflect.Array;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Deque;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Optional;
027import java.util.Set;
028import java.util.concurrent.ConcurrentHashMap;
029import java.util.concurrent.ConcurrentLinkedDeque;
030import java.util.concurrent.CopyOnWriteArraySet;
031
032import org.springframework.core.DecoratingProxy;
033import org.springframework.core.ResolvableType;
034import org.springframework.core.convert.ConversionException;
035import org.springframework.core.convert.ConversionFailedException;
036import org.springframework.core.convert.ConversionService;
037import org.springframework.core.convert.ConverterNotFoundException;
038import org.springframework.core.convert.TypeDescriptor;
039import org.springframework.core.convert.converter.ConditionalConverter;
040import org.springframework.core.convert.converter.ConditionalGenericConverter;
041import org.springframework.core.convert.converter.Converter;
042import org.springframework.core.convert.converter.ConverterFactory;
043import org.springframework.core.convert.converter.ConverterRegistry;
044import org.springframework.core.convert.converter.GenericConverter;
045import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
046import org.springframework.lang.Nullable;
047import org.springframework.util.Assert;
048import org.springframework.util.ClassUtils;
049import org.springframework.util.ConcurrentReferenceHashMap;
050import org.springframework.util.StringUtils;
051
052/**
053 * Base {@link ConversionService} implementation suitable for use in most environments.
054 * Indirectly implements {@link ConverterRegistry} as registration API through the
055 * {@link ConfigurableConversionService} interface.
056 *
057 * @author Keith Donald
058 * @author Juergen Hoeller
059 * @author Chris Beams
060 * @author Phillip Webb
061 * @author David Haraburda
062 * @since 3.0
063 */
064public class GenericConversionService implements ConfigurableConversionService {
065
066        /**
067         * General NO-OP converter used when conversion is not required.
068         */
069        private static final GenericConverter NO_OP_CONVERTER = new NoOpConverter("NO_OP");
070
071        /**
072         * Used as a cache entry when no converter is available.
073         * This converter is never returned.
074         */
075        private static final GenericConverter NO_MATCH = new NoOpConverter("NO_MATCH");
076
077
078        private final Converters converters = new Converters();
079
080        private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<>(64);
081
082
083        // ConverterRegistry implementation
084
085        @Override
086        public void addConverter(Converter<?, ?> converter) {
087                ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);
088                if (typeInfo == null && converter instanceof DecoratingProxy) {
089                        typeInfo = getRequiredTypeInfo(((DecoratingProxy) converter).getDecoratedClass(), Converter.class);
090                }
091                if (typeInfo == null) {
092                        throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " +
093                                        "Converter [" + converter.getClass().getName() + "]; does the class parameterize those types?");
094                }
095                addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));
096        }
097
098        @Override
099        public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter) {
100                addConverter(new ConverterAdapter(
101                                converter, ResolvableType.forClass(sourceType), ResolvableType.forClass(targetType)));
102        }
103
104        @Override
105        public void addConverter(GenericConverter converter) {
106                this.converters.add(converter);
107                invalidateCache();
108        }
109
110        @Override
111        public void addConverterFactory(ConverterFactory<?, ?> factory) {
112                ResolvableType[] typeInfo = getRequiredTypeInfo(factory.getClass(), ConverterFactory.class);
113                if (typeInfo == null && factory instanceof DecoratingProxy) {
114                        typeInfo = getRequiredTypeInfo(((DecoratingProxy) factory).getDecoratedClass(), ConverterFactory.class);
115                }
116                if (typeInfo == null) {
117                        throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " +
118                                        "ConverterFactory [" + factory.getClass().getName() + "]; does the class parameterize those types?");
119                }
120                addConverter(new ConverterFactoryAdapter(factory,
121                                new ConvertiblePair(typeInfo[0].toClass(), typeInfo[1].toClass())));
122        }
123
124        @Override
125        public void removeConvertible(Class<?> sourceType, Class<?> targetType) {
126                this.converters.remove(sourceType, targetType);
127                invalidateCache();
128        }
129
130
131        // ConversionService implementation
132
133        @Override
134        public boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType) {
135                Assert.notNull(targetType, "Target type to convert to cannot be null");
136                return canConvert((sourceType != null ? TypeDescriptor.valueOf(sourceType) : null),
137                                TypeDescriptor.valueOf(targetType));
138        }
139
140        @Override
141        public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
142                Assert.notNull(targetType, "Target type to convert to cannot be null");
143                if (sourceType == null) {
144                        return true;
145                }
146                GenericConverter converter = getConverter(sourceType, targetType);
147                return (converter != null);
148        }
149
150        /**
151         * Return whether conversion between the source type and the target type can be bypassed.
152         * <p>More precisely, this method will return true if objects of sourceType can be
153         * converted to the target type by returning the source object unchanged.
154         * @param sourceType context about the source type to convert from
155         * (may be {@code null} if source is {@code null})
156         * @param targetType context about the target type to convert to (required)
157         * @return {@code true} if conversion can be bypassed; {@code false} otherwise
158         * @throws IllegalArgumentException if targetType is {@code null}
159         * @since 3.2
160         */
161        public boolean canBypassConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
162                Assert.notNull(targetType, "Target type to convert to cannot be null");
163                if (sourceType == null) {
164                        return true;
165                }
166                GenericConverter converter = getConverter(sourceType, targetType);
167                return (converter == NO_OP_CONVERTER);
168        }
169
170        @Override
171        @SuppressWarnings("unchecked")
172        @Nullable
173        public <T> T convert(@Nullable Object source, Class<T> targetType) {
174                Assert.notNull(targetType, "Target type to convert to cannot be null");
175                return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
176        }
177
178        @Override
179        @Nullable
180        public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
181                Assert.notNull(targetType, "Target type to convert to cannot be null");
182                if (sourceType == null) {
183                        Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
184                        return handleResult(null, targetType, convertNullSource(null, targetType));
185                }
186                if (source != null && !sourceType.getObjectType().isInstance(source)) {
187                        throw new IllegalArgumentException("Source to convert from must be an instance of [" +
188                                        sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
189                }
190                GenericConverter converter = getConverter(sourceType, targetType);
191                if (converter != null) {
192                        Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
193                        return handleResult(sourceType, targetType, result);
194                }
195                return handleConverterNotFound(source, sourceType, targetType);
196        }
197
198        /**
199         * Convenience operation for converting a source object to the specified targetType,
200         * where the target type is a descriptor that provides additional conversion context.
201         * Simply delegates to {@link #convert(Object, TypeDescriptor, TypeDescriptor)} and
202         * encapsulates the construction of the source type descriptor using
203         * {@link TypeDescriptor#forObject(Object)}.
204         * @param source the source object
205         * @param targetType the target type
206         * @return the converted value
207         * @throws ConversionException if a conversion exception occurred
208         * @throws IllegalArgumentException if targetType is {@code null},
209         * or sourceType is {@code null} but source is not {@code null}
210         */
211        @Nullable
212        public Object convert(@Nullable Object source, TypeDescriptor targetType) {
213                return convert(source, TypeDescriptor.forObject(source), targetType);
214        }
215
216        @Override
217        public String toString() {
218                return this.converters.toString();
219        }
220
221
222        // Protected template methods
223
224        /**
225         * Template method to convert a {@code null} source.
226         * <p>The default implementation returns {@code null} or the Java 8
227         * {@link java.util.Optional#empty()} instance if the target type is
228         * {@code java.util.Optional}. Subclasses may override this to return
229         * custom {@code null} objects for specific target types.
230         * @param sourceType the source type to convert from
231         * @param targetType the target type to convert to
232         * @return the converted null object
233         */
234        @Nullable
235        protected Object convertNullSource(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
236                if (targetType.getObjectType() == Optional.class) {
237                        return Optional.empty();
238                }
239                return null;
240        }
241
242        /**
243         * Hook method to lookup the converter for a given sourceType/targetType pair.
244         * First queries this ConversionService's converter cache.
245         * On a cache miss, then performs an exhaustive search for a matching converter.
246         * If no converter matches, returns the default converter.
247         * @param sourceType the source type to convert from
248         * @param targetType the target type to convert to
249         * @return the generic converter that will perform the conversion,
250         * or {@code null} if no suitable converter was found
251         * @see #getDefaultConverter(TypeDescriptor, TypeDescriptor)
252         */
253        @Nullable
254        protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
255                ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
256                GenericConverter converter = this.converterCache.get(key);
257                if (converter != null) {
258                        return (converter != NO_MATCH ? converter : null);
259                }
260
261                converter = this.converters.find(sourceType, targetType);
262                if (converter == null) {
263                        converter = getDefaultConverter(sourceType, targetType);
264                }
265
266                if (converter != null) {
267                        this.converterCache.put(key, converter);
268                        return converter;
269                }
270
271                this.converterCache.put(key, NO_MATCH);
272                return null;
273        }
274
275        /**
276         * Return the default converter if no converter is found for the given sourceType/targetType pair.
277         * <p>Returns a NO_OP Converter if the source type is assignable to the target type.
278         * Returns {@code null} otherwise, indicating no suitable converter could be found.
279         * @param sourceType the source type to convert from
280         * @param targetType the target type to convert to
281         * @return the default generic converter that will perform the conversion
282         */
283        @Nullable
284        protected GenericConverter getDefaultConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
285                return (sourceType.isAssignableTo(targetType) ? NO_OP_CONVERTER : null);
286        }
287
288
289        // Internal helpers
290
291        @Nullable
292        private ResolvableType[] getRequiredTypeInfo(Class<?> converterClass, Class<?> genericIfc) {
293                ResolvableType resolvableType = ResolvableType.forClass(converterClass).as(genericIfc);
294                ResolvableType[] generics = resolvableType.getGenerics();
295                if (generics.length < 2) {
296                        return null;
297                }
298                Class<?> sourceType = generics[0].resolve();
299                Class<?> targetType = generics[1].resolve();
300                if (sourceType == null || targetType == null) {
301                        return null;
302                }
303                return generics;
304        }
305
306        private void invalidateCache() {
307                this.converterCache.clear();
308        }
309
310        @Nullable
311        private Object handleConverterNotFound(
312                        @Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
313
314                if (source == null) {
315                        assertNotPrimitiveTargetType(sourceType, targetType);
316                        return null;
317                }
318                if ((sourceType == null || sourceType.isAssignableTo(targetType)) &&
319                                targetType.getObjectType().isInstance(source)) {
320                        return source;
321                }
322                throw new ConverterNotFoundException(sourceType, targetType);
323        }
324
325        @Nullable
326        private Object handleResult(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType, @Nullable Object result) {
327                if (result == null) {
328                        assertNotPrimitiveTargetType(sourceType, targetType);
329                }
330                return result;
331        }
332
333        private void assertNotPrimitiveTargetType(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
334                if (targetType.isPrimitive()) {
335                        throw new ConversionFailedException(sourceType, targetType, null,
336                                        new IllegalArgumentException("A null value cannot be assigned to a primitive type"));
337                }
338        }
339
340
341        /**
342         * Adapts a {@link Converter} to a {@link GenericConverter}.
343         */
344        @SuppressWarnings("unchecked")
345        private final class ConverterAdapter implements ConditionalGenericConverter {
346
347                private final Converter<Object, Object> converter;
348
349                private final ConvertiblePair typeInfo;
350
351                private final ResolvableType targetType;
352
353                public ConverterAdapter(Converter<?, ?> converter, ResolvableType sourceType, ResolvableType targetType) {
354                        this.converter = (Converter<Object, Object>) converter;
355                        this.typeInfo = new ConvertiblePair(sourceType.toClass(), targetType.toClass());
356                        this.targetType = targetType;
357                }
358
359                @Override
360                public Set<ConvertiblePair> getConvertibleTypes() {
361                        return Collections.singleton(this.typeInfo);
362                }
363
364                @Override
365                public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
366                        // Check raw type first...
367                        if (this.typeInfo.getTargetType() != targetType.getObjectType()) {
368                                return false;
369                        }
370                        // Full check for complex generic type match required?
371                        ResolvableType rt = targetType.getResolvableType();
372                        if (!(rt.getType() instanceof Class) && !rt.isAssignableFrom(this.targetType) &&
373                                        !this.targetType.hasUnresolvableGenerics()) {
374                                return false;
375                        }
376                        return !(this.converter instanceof ConditionalConverter) ||
377                                        ((ConditionalConverter) this.converter).matches(sourceType, targetType);
378                }
379
380                @Override
381                @Nullable
382                public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
383                        if (source == null) {
384                                return convertNullSource(sourceType, targetType);
385                        }
386                        return this.converter.convert(source);
387                }
388
389                @Override
390                public String toString() {
391                        return (this.typeInfo + " : " + this.converter);
392                }
393        }
394
395
396        /**
397         * Adapts a {@link ConverterFactory} to a {@link GenericConverter}.
398         */
399        @SuppressWarnings("unchecked")
400        private final class ConverterFactoryAdapter implements ConditionalGenericConverter {
401
402                private final ConverterFactory<Object, Object> converterFactory;
403
404                private final ConvertiblePair typeInfo;
405
406                public ConverterFactoryAdapter(ConverterFactory<?, ?> converterFactory, ConvertiblePair typeInfo) {
407                        this.converterFactory = (ConverterFactory<Object, Object>) converterFactory;
408                        this.typeInfo = typeInfo;
409                }
410
411                @Override
412                public Set<ConvertiblePair> getConvertibleTypes() {
413                        return Collections.singleton(this.typeInfo);
414                }
415
416                @Override
417                public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
418                        boolean matches = true;
419                        if (this.converterFactory instanceof ConditionalConverter) {
420                                matches = ((ConditionalConverter) this.converterFactory).matches(sourceType, targetType);
421                        }
422                        if (matches) {
423                                Converter<?, ?> converter = this.converterFactory.getConverter(targetType.getType());
424                                if (converter instanceof ConditionalConverter) {
425                                        matches = ((ConditionalConverter) converter).matches(sourceType, targetType);
426                                }
427                        }
428                        return matches;
429                }
430
431                @Override
432                @Nullable
433                public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
434                        if (source == null) {
435                                return convertNullSource(sourceType, targetType);
436                        }
437                        return this.converterFactory.getConverter(targetType.getObjectType()).convert(source);
438                }
439
440                @Override
441                public String toString() {
442                        return (this.typeInfo + " : " + this.converterFactory);
443                }
444        }
445
446
447        /**
448         * Key for use with the converter cache.
449         */
450        private static final class ConverterCacheKey implements Comparable<ConverterCacheKey> {
451
452                private final TypeDescriptor sourceType;
453
454                private final TypeDescriptor targetType;
455
456                public ConverterCacheKey(TypeDescriptor sourceType, TypeDescriptor targetType) {
457                        this.sourceType = sourceType;
458                        this.targetType = targetType;
459                }
460
461                @Override
462                public boolean equals(@Nullable Object other) {
463                        if (this == other) {
464                                return true;
465                        }
466                        if (!(other instanceof ConverterCacheKey)) {
467                                return false;
468                        }
469                        ConverterCacheKey otherKey = (ConverterCacheKey) other;
470                        return (this.sourceType.equals(otherKey.sourceType)) &&
471                                        this.targetType.equals(otherKey.targetType);
472                }
473
474                @Override
475                public int hashCode() {
476                        return (this.sourceType.hashCode() * 29 + this.targetType.hashCode());
477                }
478
479                @Override
480                public String toString() {
481                        return ("ConverterCacheKey [sourceType = " + this.sourceType +
482                                        ", targetType = " + this.targetType + "]");
483                }
484
485                @Override
486                public int compareTo(ConverterCacheKey other) {
487                        int result = this.sourceType.getResolvableType().toString().compareTo(
488                                        other.sourceType.getResolvableType().toString());
489                        if (result == 0) {
490                                result = this.targetType.getResolvableType().toString().compareTo(
491                                                other.targetType.getResolvableType().toString());
492                        }
493                        return result;
494                }
495        }
496
497
498        /**
499         * Manages all converters registered with the service.
500         */
501        private static class Converters {
502
503                private final Set<GenericConverter> globalConverters = new CopyOnWriteArraySet<>();
504
505                private final Map<ConvertiblePair, ConvertersForPair> converters = new ConcurrentHashMap<>(256);
506
507                public void add(GenericConverter converter) {
508                        Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
509                        if (convertibleTypes == null) {
510                                Assert.state(converter instanceof ConditionalConverter,
511                                                "Only conditional converters may return null convertible types");
512                                this.globalConverters.add(converter);
513                        }
514                        else {
515                                for (ConvertiblePair convertiblePair : convertibleTypes) {
516                                        getMatchableConverters(convertiblePair).add(converter);
517                                }
518                        }
519                }
520
521                private ConvertersForPair getMatchableConverters(ConvertiblePair convertiblePair) {
522                        return this.converters.computeIfAbsent(convertiblePair, k -> new ConvertersForPair());
523                }
524
525                public void remove(Class<?> sourceType, Class<?> targetType) {
526                        this.converters.remove(new ConvertiblePair(sourceType, targetType));
527                }
528
529                /**
530                 * Find a {@link GenericConverter} given a source and target type.
531                 * <p>This method will attempt to match all possible converters by working
532                 * through the class and interface hierarchy of the types.
533                 * @param sourceType the source type
534                 * @param targetType the target type
535                 * @return a matching {@link GenericConverter}, or {@code null} if none found
536                 */
537                @Nullable
538                public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
539                        // Search the full type hierarchy
540                        List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
541                        List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
542                        for (Class<?> sourceCandidate : sourceCandidates) {
543                                for (Class<?> targetCandidate : targetCandidates) {
544                                        ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
545                                        GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
546                                        if (converter != null) {
547                                                return converter;
548                                        }
549                                }
550                        }
551                        return null;
552                }
553
554                @Nullable
555                private GenericConverter getRegisteredConverter(TypeDescriptor sourceType,
556                                TypeDescriptor targetType, ConvertiblePair convertiblePair) {
557
558                        // Check specifically registered converters
559                        ConvertersForPair convertersForPair = this.converters.get(convertiblePair);
560                        if (convertersForPair != null) {
561                                GenericConverter converter = convertersForPair.getConverter(sourceType, targetType);
562                                if (converter != null) {
563                                        return converter;
564                                }
565                        }
566                        // Check ConditionalConverters for a dynamic match
567                        for (GenericConverter globalConverter : this.globalConverters) {
568                                if (((ConditionalConverter) globalConverter).matches(sourceType, targetType)) {
569                                        return globalConverter;
570                                }
571                        }
572                        return null;
573                }
574
575                /**
576                 * Returns an ordered class hierarchy for the given type.
577                 * @param type the type
578                 * @return an ordered list of all classes that the given type extends or implements
579                 */
580                private List<Class<?>> getClassHierarchy(Class<?> type) {
581                        List<Class<?>> hierarchy = new ArrayList<>(20);
582                        Set<Class<?>> visited = new HashSet<>(20);
583                        addToClassHierarchy(0, ClassUtils.resolvePrimitiveIfNecessary(type), false, hierarchy, visited);
584                        boolean array = type.isArray();
585
586                        int i = 0;
587                        while (i < hierarchy.size()) {
588                                Class<?> candidate = hierarchy.get(i);
589                                candidate = (array ? candidate.getComponentType() : ClassUtils.resolvePrimitiveIfNecessary(candidate));
590                                Class<?> superclass = candidate.getSuperclass();
591                                if (superclass != null && superclass != Object.class && superclass != Enum.class) {
592                                        addToClassHierarchy(i + 1, candidate.getSuperclass(), array, hierarchy, visited);
593                                }
594                                addInterfacesToClassHierarchy(candidate, array, hierarchy, visited);
595                                i++;
596                        }
597
598                        if (Enum.class.isAssignableFrom(type)) {
599                                addToClassHierarchy(hierarchy.size(), Enum.class, array, hierarchy, visited);
600                                addToClassHierarchy(hierarchy.size(), Enum.class, false, hierarchy, visited);
601                                addInterfacesToClassHierarchy(Enum.class, array, hierarchy, visited);
602                        }
603
604                        addToClassHierarchy(hierarchy.size(), Object.class, array, hierarchy, visited);
605                        addToClassHierarchy(hierarchy.size(), Object.class, false, hierarchy, visited);
606                        return hierarchy;
607                }
608
609                private void addInterfacesToClassHierarchy(Class<?> type, boolean asArray,
610                                List<Class<?>> hierarchy, Set<Class<?>> visited) {
611
612                        for (Class<?> implementedInterface : type.getInterfaces()) {
613                                addToClassHierarchy(hierarchy.size(), implementedInterface, asArray, hierarchy, visited);
614                        }
615                }
616
617                private void addToClassHierarchy(int index, Class<?> type, boolean asArray,
618                                List<Class<?>> hierarchy, Set<Class<?>> visited) {
619
620                        if (asArray) {
621                                type = Array.newInstance(type, 0).getClass();
622                        }
623                        if (visited.add(type)) {
624                                hierarchy.add(index, type);
625                        }
626                }
627
628                @Override
629                public String toString() {
630                        StringBuilder builder = new StringBuilder();
631                        builder.append("ConversionService converters =\n");
632                        for (String converterString : getConverterStrings()) {
633                                builder.append('\t').append(converterString).append('\n');
634                        }
635                        return builder.toString();
636                }
637
638                private List<String> getConverterStrings() {
639                        List<String> converterStrings = new ArrayList<>();
640                        for (ConvertersForPair convertersForPair : this.converters.values()) {
641                                converterStrings.add(convertersForPair.toString());
642                        }
643                        Collections.sort(converterStrings);
644                        return converterStrings;
645                }
646        }
647
648
649        /**
650         * Manages converters registered with a specific {@link ConvertiblePair}.
651         */
652        private static class ConvertersForPair {
653
654                private final Deque<GenericConverter> converters = new ConcurrentLinkedDeque<>();
655
656                public void add(GenericConverter converter) {
657                        this.converters.addFirst(converter);
658                }
659
660                @Nullable
661                public GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
662                        for (GenericConverter converter : this.converters) {
663                                if (!(converter instanceof ConditionalGenericConverter) ||
664                                                ((ConditionalGenericConverter) converter).matches(sourceType, targetType)) {
665                                        return converter;
666                                }
667                        }
668                        return null;
669                }
670
671                @Override
672                public String toString() {
673                        return StringUtils.collectionToCommaDelimitedString(this.converters);
674                }
675        }
676
677
678        /**
679         * Internal converter that performs no operation.
680         */
681        private static class NoOpConverter implements GenericConverter {
682
683                private final String name;
684
685                public NoOpConverter(String name) {
686                        this.name = name;
687                }
688
689                @Override
690                @Nullable
691                public Set<ConvertiblePair> getConvertibleTypes() {
692                        return null;
693                }
694
695                @Override
696                @Nullable
697                public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
698                        return source;
699                }
700
701                @Override
702                public String toString() {
703                        return this.name;
704                }
705        }
706
707}