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}