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