001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2019 microBean™. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * 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 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.microprofile.config; 018 019import java.beans.PropertyEditor; 020import java.beans.PropertyEditorManager; 021 022import java.io.Closeable; 023import java.io.IOException; 024import java.io.ObjectInputStream; 025import java.io.ObjectOutputStream; 026import java.io.Serializable; 027 028import java.lang.reflect.Array; 029import java.lang.reflect.Constructor; 030import java.lang.reflect.Executable; 031import java.lang.reflect.Method; 032import java.lang.reflect.Modifier; 033import java.lang.reflect.ParameterizedType; 034import java.lang.reflect.Type; 035 036import java.net.MalformedURLException; 037import java.net.URI; 038import java.net.URL; 039 040import java.security.AccessController; 041import java.security.PrivilegedAction; 042 043import java.util.ArrayList; 044import java.util.Collection; 045import java.util.Collections; 046import java.util.HashMap; 047import java.util.HashSet; 048import java.util.Map; 049import java.util.Objects; 050import java.util.Optional; 051import java.util.ServiceLoader; 052import java.util.Set; 053 054import java.util.regex.Pattern; 055 056import org.eclipse.microprofile.config.spi.Converter; 057 058/** 059 * A {@link Serializable}, {@link Closeable} {@link TypeConverter} 060 * implementation that is based on a collection of {@link Converter}s. 061 * 062 * @author <a href="https://about.me/lairdnelson" 063 * target="_parent">Laird Nelson</a> 064 * 065 * @see #convert(String, Type) 066 */ 067public class ConversionHub implements Closeable, Serializable, TypeConverter { 068 069 private static final long serialVersionUID = 1L; 070 071 private static final Pattern splitPattern = Pattern.compile("(?<!\\\\),"); 072 073 private static final Pattern backslashCommaPattern = Pattern.compile("\\\\,"); 074 075 private static final Map<Class<?>, Class<?>> wrapperClasses; 076 077 static { 078 wrapperClasses = new HashMap<>(); 079 wrapperClasses.put(boolean.class, Boolean.class); 080 wrapperClasses.put(byte.class, Byte.class); 081 wrapperClasses.put(char.class, Character.class); 082 wrapperClasses.put(double.class, Double.class); 083 wrapperClasses.put(float.class, Float.class); 084 wrapperClasses.put(int.class, Integer.class); 085 wrapperClasses.put(long.class, Long.class); 086 wrapperClasses.put(short.class, Short.class); 087 } 088 089 private final Map<Type, Converter<?>> converters; 090 091 private volatile boolean closed; 092 093 /** 094 * Creates a new {@link ConversionHub}. 095 */ 096 public ConversionHub() { 097 super(); 098 final Map<? extends Type, ? extends Converter<?>> discoveredConverters = getDiscoveredConverters(null); 099 this.converters = new HashMap<>(discoveredConverters); 100 } 101 102 /** 103 * Creates a new {@link ConversionHub}. 104 * 105 * <h2>Thread Safety</h2> 106 * 107 * <p><strong>{@code converters} will be synchronized on and iterated 108 * over by this constructor</strong>, which may have implications on 109 * the type of {@link Map} supplied.</p> 110 * 111 * @param converters a {@link Map} of {@link Converter} instances, 112 * indexed by the {@link Type} describing the type of the return 113 * value of their respective {@link Converter#convert(String)} 114 * methods; may be {@code null}; <strong>will be synchronized on and 115 * iterated over</strong>; copied by value; no reference is kept to 116 * this object 117 */ 118 public ConversionHub(final Map<? extends Type, ? extends Converter<?>> converters) { 119 super(); 120 if (converters == null) { 121 this.converters = new HashMap<>(); 122 } else { 123 synchronized (converters) { 124 this.converters = new HashMap<>(converters); 125 } 126 } 127 } 128 129 /** 130 * Closes this {@link ConversionHub} using a best-effort strategy. 131 * 132 * <p>This method attempts to close each of this {@link 133 * ConversionHub}'s associated {@link Closeable} {@link Converter}s. 134 * Any {@link IOException} thrown during such an attempt does not 135 * abort the closing process.</p> 136 * 137 * <p>Once this method has been invoked:</p> 138 * 139 * <ul> 140 * 141 * <li>All future invocations of the {@link #convert(String, Type)} 142 * method will throw an {@link IllegalStateException}</li> 143 * 144 * <li>All future invocations of the {@link #isClosed()} method will 145 * return {@code true}</li> 146 * 147 * </ul> 148 * 149 * <p>{@link ConversionHub} instances are often {@linkplain 150 * Config#Config(Collection, TypeConverter) supplied to 151 * <code>Config</code> instances at construction time}, and so may 152 * be {@linkplain Config#close() closed by them}.</p> 153 * 154 * <h2>Thread Safety</h2> 155 * 156 * <p>This method is safe for concurrent use by multiple 157 * threads.</p> 158 * 159 * @exception IOException if at least one underlying {@link 160 * Closeable} {@link Converter} could not be closed 161 */ 162 @Override 163 public void close() throws IOException { 164 if (!this.isClosed()) { 165 /* 166 The specification says: 167 168 "A factory method ConfigProviderResolver#releaseConfig(Config 169 config) to release the Config instance [sic]. This will unbind 170 the current Config from the application. The ConfigSources 171 that implement the java.io.Closeable interface will be 172 properly destroyed. The Converters that implement the 173 java.io.Closeable interface will be properly destroyed." 174 175 It is not clear which ConfigSources and which Converters are 176 meant here, but assuming they are only those ones present "in" 177 the Config being released, there's no way to "get" those from 178 a given Config, since (a) there is no requirement that a 179 Config actually house Converters and (b) consequently there is 180 nothing like a Config#getConverters() method. 181 182 So we implement Closeable to at least provide the ability to 183 close everything cleanly and in a thread-safe manner. 184 */ 185 186 IOException throwMe = null; 187 synchronized (this.converters) { 188 if (!this.converters.isEmpty()) { 189 final Collection<? extends Converter<?>> converters = this.converters.values(); 190 assert converters != null; 191 assert !converters.isEmpty(); 192 for (final Converter<?> converter : converters) { 193 if (converter instanceof Closeable) { 194 try { 195 ((Closeable)converter).close(); 196 } catch (final IOException ioException) { 197 if (throwMe == null) { 198 throwMe = ioException; 199 } else { 200 throwMe.addSuppressed(ioException); 201 } 202 } 203 } 204 } 205 } 206 } 207 if (throwMe != null) { 208 throw throwMe; 209 } 210 this.closed = true; 211 } 212 } 213 214 /** 215 * Returns {@code true} if this {@link ConversionHub} has been 216 * {@linkplain #close() closed}. 217 * 218 * <p>All invocations of the {@link #convert(String, Type)} method 219 * will always throw an {@link IllegalStateException} once this 220 * {@link ConversionHub} has been {@linkplain #close() closed}.</p> 221 * 222 * <h2>Thread Safety</h2> 223 * 224 * <p>This method is idempotent and safe for concurrent use by 225 * multiple threads.</p> 226 * 227 * @return {@code true} if this {@link ConversionHub} has been 228 * {@linkplain #close() closed}; {@code false} otherwise 229 * 230 * @see #close() 231 */ 232 public final boolean isClosed() { 233 return this.closed; 234 } 235 236 /** 237 * Attempts to convert the supplied {@link String} value to an 238 * object assignable to the supplied {@link Type}, throwing an 239 * {@link IllegalArgumentException} if such conversion is 240 * impossible. 241 * 242 * <p>This method may return {@code null}.</p> 243 * 244 * <h2>Thread Safety</h2> 245 * 246 * <p>This method is safe for concurrent use by multiple 247 * threads.</p> 248 * 249 * @param value the value to convert; may be {@code null} 250 * 251 * @param type the {@link Type} to which the value should be 252 * converted; must not be {@code null}; the type of the return value 253 * resulting from invocations this method should be assignable to 254 * references of this type 255 * 256 * @return the converted object, which may be {@code null} 257 * 258 * @exception IllegalArgumentException if conversion could not occur 259 * for any reason 260 * 261 * @exception IllegalStateException if this {@link ConversionHub} 262 * was {@linkplain #close() closed} 263 */ 264 @Override 265 @SuppressWarnings("unchecked") 266 public final <T> T convert(final String value, final Type type) { 267 if (this.isClosed()) { 268 throw new IllegalStateException("this.isClosed()"); 269 } 270 Converter<T> converter; 271 synchronized (this.converters) { 272 converter = (Converter<T>)this.converters.get(type); 273 if (converter == null) { 274 try { 275 converter = this.computeConverter(type); 276 } catch (final ReflectiveOperationException reflectiveOperationException) { 277 throw new IllegalArgumentException(reflectiveOperationException.getMessage(), reflectiveOperationException); 278 } 279 if (converter != null) { 280 this.converters.put(type, converter); 281 } 282 } 283 } 284 if (converter == null) { 285 throw new IllegalArgumentException("\"" + value + "\" could not be converted to " + (type == null ? "null" : type.getTypeName())); 286 } 287 final T returnValue = converter.convert(value); 288 return returnValue; 289 } 290 291 @SuppressWarnings("unchecked") 292 private final <T> Converter<T> computeConverter(final Type conversionType) throws ReflectiveOperationException { 293 Converter<T> returnValue = null; 294 if (CharSequence.class.equals(conversionType) || 295 String.class.equals(conversionType) || 296 Serializable.class.equals(conversionType) || 297 Object.class.equals(conversionType)) { 298 returnValue = new SerializableConverter<T>() { 299 private static final long serialVersionUID = 1L; 300 @Override 301 public final T convert(final String rawValue) { 302 return (T)rawValue; 303 } 304 }; 305 306 } else if (Boolean.class.equals(conversionType) || boolean.class.equals(conversionType)) { 307 returnValue = new SerializableConverter<T>() { 308 private static final long serialVersionUID = 1L; 309 @Override 310 public final T convert(final String rawValue) { 311 return (T)Boolean.valueOf(rawValue != null && 312 ("true".equalsIgnoreCase(rawValue) || 313 "y".equalsIgnoreCase(rawValue) || 314 "yes".equalsIgnoreCase(rawValue) || 315 "on".equalsIgnoreCase(rawValue) || 316 "1".equals(rawValue))); 317 } 318 }; 319 320 } else if (Character.class.equals(conversionType) || char.class.equals(conversionType)) { 321 returnValue = new SerializableConverter<T>() { 322 private static final long serialVersionUID = 1L; 323 @Override 324 public final T convert(final String rawValue) { 325 if (rawValue == null || rawValue.isEmpty()) { 326 return null; 327 } else if (rawValue.length() != 1) { 328 throw new IllegalArgumentException("Unexpected length for character conversion: " + rawValue); 329 } 330 return (T)Character.valueOf(rawValue.charAt(0)); 331 } 332 }; 333 334 } else if (URL.class.equals(conversionType)) { 335 returnValue = new SerializableConverter<T>() { 336 private static final long serialVersionUID = 1L; 337 @Override 338 public final T convert(final String rawValue) { 339 try { 340 return (T)URI.create(rawValue).toURL(); 341 } catch (final MalformedURLException malformedUrlException) { 342 throw new IllegalArgumentException(malformedUrlException.getMessage(), malformedUrlException); 343 } 344 } 345 }; 346 347 } else if (Class.class.equals(conversionType)) { 348 returnValue = new SerializableConverter<T>() { 349 private static final long serialVersionUID = 1L; 350 @Override 351 public final T convert(final String rawValue) { 352 try { 353 // Seems odd that the specification mandates the use of 354 // the single-argument Class#forName(String) method, but 355 // it's spelled out in black and white: 356 // https://github.com/eclipse/microprofile-config/blob/20e1d59dd1055867a54e65b77405f9e68611544e/spec/src/main/asciidoc/converters.asciidoc#built-in-converters 357 // See 358 // https://github.com/eclipse/microprofile-config/issues/424. 359 return (T)Class.forName(rawValue); 360 } catch (final ClassNotFoundException classNotFoundException) { 361 throw new IllegalArgumentException(classNotFoundException.getMessage(), classNotFoundException); 362 } 363 } 364 }; 365 366 } else if (conversionType instanceof ParameterizedType) { 367 final ParameterizedType parameterizedType = (ParameterizedType)conversionType; 368 final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); 369 assert actualTypeArguments != null; 370 assert actualTypeArguments.length > 0; 371 final Type rawType = parameterizedType.getRawType(); 372 assert rawType instanceof Class : "!(parameterizedType.getRawType() instanceof Class): " + rawType; 373 final Class<?> conversionClass = (Class<?>)rawType; 374 assert !conversionClass.isArray(); 375 376 if (Optional.class.isAssignableFrom(conversionClass)) { 377 assert actualTypeArguments.length == 1; 378 final Type firstTypeArgument = actualTypeArguments[0]; 379 returnValue = new SerializableConverter<T>() { 380 private static final long serialVersionUID = 1L; 381 @Override 382 public final T convert(final String rawValue) { 383 return (T)Optional.ofNullable(ConversionHub.this.convert(rawValue, firstTypeArgument)); // XXX recursive call 384 } 385 }; 386 387 } else if (Class.class.isAssignableFrom(conversionClass)) { 388 returnValue = new SerializableConverter<T>() { 389 private static final long serialVersionUID = 1L; 390 @Override 391 public final T convert(final String rawValue) { 392 return ConversionHub.this.convert(rawValue, conversionClass); // XXX recursive call 393 } 394 }; 395 396 } else if (Collection.class.isAssignableFrom(conversionClass)) { 397 returnValue = new SerializableConverter<T>() { 398 private static final long serialVersionUID = 1L; 399 @Override 400 public final T convert(final String rawValue) { 401 Collection<Object> container = null; 402 if (conversionClass.isInterface()) { 403 if (Set.class.isAssignableFrom(conversionClass)) { 404 container = new HashSet<>(); 405 } else { 406 container = new ArrayList<>(); 407 } 408 } else { 409 try { 410 container = (Collection<Object>)conversionClass.getDeclaredConstructor().newInstance(); 411 } catch (final ReflectiveOperationException reflectiveOperationException) { 412 throw new IllegalArgumentException(reflectiveOperationException.getMessage(), reflectiveOperationException); 413 } 414 } 415 assert container != null; 416 final Type firstTypeArgument = actualTypeArguments[0]; 417 final String[] parts = split(rawValue); 418 assert parts != null; 419 assert parts.length > 0; 420 for (final String part : parts) { 421 final Object scalar = ConversionHub.this.convert(part, firstTypeArgument); // XXX recursive call 422 container.add(scalar); 423 } 424 final T temp = (T)container; 425 return temp; 426 } 427 }; 428 } else { 429 throw new IllegalArgumentException("Unhandled conversion type: " + conversionType); 430 } 431 432 } else if (conversionType instanceof Class) { 433 final Class<?> conversionClass = (Class<?>)conversionType; 434 if (conversionClass.isArray()) { 435 returnValue = new SerializableConverter<T>() { 436 private static final long serialVersionUID = 1L; 437 @Override 438 public final T convert(final String rawValue) { 439 final String[] parts = split(rawValue); 440 assert parts != null; 441 T container = (T)Array.newInstance(conversionClass.getComponentType(), parts.length); 442 for (int i = 0; i < parts.length; i++) { 443 final Object scalar = ConversionHub.this.convert(parts[i], conversionClass.getComponentType()); // XXX recursive call 444 Array.set(container, i, scalar); 445 } 446 return container; 447 } 448 }; 449 450 } else { 451 final Class<?> cls; 452 if (conversionClass.isPrimitive()) { 453 cls = wrapperClasses.get(conversionClass); 454 assert cls != null; 455 } else { 456 cls = conversionClass; 457 } 458 returnValue = getConverterFromStaticMethod(cls, "of", String.class); 459 if (returnValue == null) { 460 returnValue = getConverterFromStaticMethod(cls, "of", CharSequence.class); 461 if (returnValue == null) { 462 returnValue = getConverterFromStaticMethod(cls, "valueOf", String.class); 463 if (returnValue == null) { 464 returnValue = getConverterFromStaticMethod(cls, "valueOf", CharSequence.class); 465 if (returnValue == null) { 466 returnValue = getConverterFromStaticMethod(cls, "parse", String.class); 467 if (returnValue == null) { 468 returnValue = getConverterFromStaticMethod(cls, "parse", CharSequence.class); 469 if (returnValue == null) { 470 returnValue = getConverterFromConstructor((Class<T>)cls, String.class); 471 if (returnValue == null) { 472 returnValue = getConverterFromConstructor((Class<T>)cls, CharSequence.class); 473 if (returnValue == null) { 474 final PropertyEditor editor = PropertyEditorManager.findEditor(cls); 475 if (editor != null) { 476 returnValue = new PropertyEditorConverter<T>(cls, editor); 477 } 478 } 479 } 480 } 481 } 482 } 483 } 484 } 485 } 486 } 487 } else { 488 returnValue = null; 489 } 490 return returnValue; 491 } 492 493 private static final <T> Converter<T> getConverterFromStaticMethod(Class<?> methodHostClass, final String methodName, final Class<? extends CharSequence> soleParameterType) { 494 Objects.requireNonNull(methodHostClass); 495 Objects.requireNonNull(methodName); 496 Objects.requireNonNull(soleParameterType); 497 if (methodHostClass.isArray()) { 498 throw new IllegalArgumentException("methodHostClass.isArray(): " + methodHostClass.getName()); 499 } else if (methodHostClass.isPrimitive()) { 500 throw new IllegalArgumentException("methodHostClass.isPrimitive(): " + methodHostClass.getName()); 501 } 502 Converter<T> returnValue = null; 503 final Method method; 504 Method temp = null; 505 try { 506 temp = methodHostClass.getMethod(methodName, soleParameterType); 507 } catch (final NoSuchMethodException noSuchMethodException) { 508 509 } finally { 510 method = temp; 511 } 512 if (method != null && Modifier.isStatic(method.getModifiers()) && methodHostClass.isAssignableFrom(method.getReturnType())) { 513 returnValue = new ExecutableBasedConverter<>(method); 514 } 515 return returnValue; 516 } 517 518 private static final <T> Converter<T> getConverterFromConstructor(Class<T> constructorHostClass, final Class<? extends CharSequence> soleParameterType) { 519 Objects.requireNonNull(constructorHostClass); 520 Objects.requireNonNull(soleParameterType); 521 if (constructorHostClass.isPrimitive()) { 522 throw new IllegalArgumentException("constructorHostClass.isPrimitive(): " + constructorHostClass.getName()); 523 } else if (constructorHostClass.isArray()) { 524 throw new IllegalArgumentException("constructorHostClass.isArray(): " + constructorHostClass.getName()); 525 } 526 527 Converter<T> returnValue = null; 528 final Constructor<T> constructor; 529 Constructor<T> temp = null; 530 try { 531 temp = constructorHostClass.getConstructor(soleParameterType); 532 } catch (final NoSuchMethodException noSuchMethodException) { 533 534 } finally { 535 constructor = temp; 536 } 537 if (constructor != null) { 538 returnValue = new ExecutableBasedConverter<>(constructor); 539 } 540 return returnValue; 541 } 542 543 static final String[] split(final String text) { 544 final String[] returnValue; 545 if (text == null) { 546 returnValue = new String[0]; 547 } else { 548 returnValue = splitPattern.split(text); 549 assert returnValue != null; 550 for (int i = 0; i < returnValue.length; i++) { 551 returnValue[i] = backslashCommaPattern.matcher(returnValue[i]).replaceAll(","); 552 } 553 } 554 return returnValue; 555 } 556 557 static final Map<? extends Type, ? extends Converter<?>> getDiscoveredConverters(ClassLoader classLoader) { 558 if (classLoader == null) { 559 classLoader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>)() -> Thread.currentThread().getContextClassLoader()); 560 } 561 final Map<Type, Converter<?>> converters = new HashMap<>(); 562 @SuppressWarnings("rawtypes") 563 final ServiceLoader<Converter> discoveredConverters = ServiceLoader.load(Converter.class, classLoader); 564 assert discoveredConverters != null; 565 for (final Converter<?> discoveredConverter : discoveredConverters) { 566 if (discoveredConverter != null) { 567 final Type conversionType = Converters.getConversionType(discoveredConverter); 568 if (conversionType == null) { 569 throw new IllegalStateException("Could not determine the conversion type for converter: " + discoveredConverter); 570 } 571 converters.put(conversionType, discoveredConverter); 572 } 573 } 574 return Collections.unmodifiableMap(converters); 575 } 576 577 private static abstract class SerializableConverter<T> implements Converter<T>, Serializable { 578 579 private static final long serialVersionUID = 1L; 580 581 protected SerializableConverter() { 582 super(); 583 } 584 585 } 586 587 private static final class ExecutableBasedConverter<T> extends SerializableConverter<T> { 588 589 private static final long serialVersionUID = 1L; 590 591 private transient Executable executable; 592 593 private ExecutableBasedConverter(final Method method) { 594 super(); 595 this.executable = Objects.requireNonNull(method); 596 if (!Modifier.isStatic(method.getModifiers())) { 597 throw new IllegalArgumentException("method is not static: " + method); 598 } 599 } 600 601 private ExecutableBasedConverter(final Constructor<T> constructor) { 602 super(); 603 this.executable = Objects.requireNonNull(constructor); 604 } 605 606 @Override 607 public final T convert(final String rawValue) { 608 final T returnValue; 609 if (rawValue == null) { 610 // Most valueOf(String) methods and constructors that the 611 // specification intended this kludgy mechanism to handle do 612 // not accept null as a value. 613 returnValue = null; 614 } else { 615 T convertedObject = null; 616 try { 617 if (this.executable instanceof Method) { 618 @SuppressWarnings("unchecked") 619 final T invocationResult = (T)((Method)this.executable).invoke(null, rawValue); 620 convertedObject = invocationResult; 621 } else { 622 assert this.executable instanceof Constructor; 623 @SuppressWarnings("unchecked") 624 final T invocationResult = ((Constructor<T>)this.executable).newInstance(rawValue); 625 convertedObject = invocationResult; 626 } 627 } catch (final ReflectiveOperationException reflectiveOperationException) { 628 throw new IllegalArgumentException(reflectiveOperationException.getMessage(), reflectiveOperationException); 629 } finally { 630 returnValue = convertedObject; 631 } 632 } 633 return returnValue; 634 } 635 636 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 637 if (in != null) { 638 in.defaultReadObject(); 639 final boolean constructor = in.readBoolean(); 640 final Class<?> declaringClass = (Class<?>)in.readObject(); 641 assert declaringClass != null; 642 final String methodName; 643 if (constructor) { 644 methodName = null; 645 } else { 646 methodName = in.readUTF(); 647 assert methodName != null; 648 } 649 final Class<?>[] parameterTypes = (Class<?>[])in.readObject(); 650 assert parameterTypes != null; 651 try { 652 if (constructor) { 653 this.executable = declaringClass.getDeclaredConstructor(parameterTypes); 654 } else { 655 this.executable = declaringClass.getMethod(methodName, parameterTypes); 656 } 657 } catch (final ReflectiveOperationException reflectiveOperationException) { 658 throw new IOException(reflectiveOperationException.getMessage(), reflectiveOperationException); 659 } 660 assert this.executable != null; 661 } 662 } 663 664 private void writeObject(final ObjectOutputStream out) throws IOException { 665 if (out != null) { 666 out.defaultWriteObject(); 667 assert this.executable != null; 668 final boolean constructor = this.executable instanceof Constructor; 669 out.writeBoolean(constructor); // true means Constructor 670 out.writeObject(this.executable.getDeclaringClass()); 671 if (!constructor) { 672 out.writeUTF(this.executable.getName()); 673 } 674 out.writeObject(this.executable.getParameterTypes()); 675 } 676 } 677 678 } 679 680 private static final class PropertyEditorConverter<T> extends SerializableConverter<T> { 681 682 private static final long serialVersionUID = 1L; 683 684 private final Class<?> conversionClass; 685 686 private transient PropertyEditor editor; 687 688 private PropertyEditorConverter(final Class<?> conversionClass, final PropertyEditor editor) { 689 super(); 690 this.conversionClass = Objects.requireNonNull(conversionClass); 691 if (editor == null) { 692 this.editor = PropertyEditorManager.findEditor(conversionClass); 693 } else { 694 this.editor = editor; 695 } 696 } 697 698 @Override 699 public final T convert(final String rawValue) { 700 if (this.editor == null) { 701 throw new IllegalArgumentException("No PropertyEditor available to convert " + rawValue); 702 } 703 final T returnValue; 704 synchronized (this.editor) { 705 editor.setAsText(rawValue); 706 T result = null; 707 try { 708 @SuppressWarnings("unchecked") 709 final T temp = (T)editor.getValue(); 710 result = temp; 711 } catch (final ClassCastException classCastException) { 712 throw new IllegalArgumentException(classCastException.getMessage(), classCastException); 713 } finally { 714 returnValue = result; 715 } 716 } 717 return returnValue; 718 } 719 720 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 721 if (in != null) { 722 in.defaultReadObject(); 723 this.editor = PropertyEditorManager.findEditor(conversionClass); 724 } 725 } 726 727 } 728 729}