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}