, TypedStringConverter>>();
//-----------------------------------------------------------------------
/**
* Creates a new conversion manager including the extended standard set of converters.
*
* The returned converter is a new instance that includes additional converters:
*
* JDK converters
* {@link NumericArrayStringConverterFactory}
* {@link NumericObjectArrayStringConverterFactory}
* {@link CharObjectArrayStringConverterFactory}
* {@link ByteObjectArrayStringConverterFactory}
* {@link BooleanArrayStringConverterFactory}
* {@link BooleanObjectArrayStringConverterFactory}
*
*
* The convert instance is mutable in a thread-safe manner.
* Converters may be altered at any time, including the JDK converters.
* It is strongly recommended to only alter the converters before performing
* actual conversions.
*
* @return the new converter, not null
* @since 1.5
*/
public static StringConvert create() {
return new StringConvert(true,
NumericArrayStringConverterFactory.INSTANCE,
NumericObjectArrayStringConverterFactory.INSTANCE,
CharObjectArrayStringConverterFactory.INSTANCE,
ByteObjectArrayStringConverterFactory.INSTANCE,
BooleanArrayStringConverterFactory.INSTANCE,
BooleanObjectArrayStringConverterFactory.INSTANCE);
}
//-----------------------------------------------------------------------
/**
* Creates a new conversion manager including the JDK converters.
*
* The convert instance is mutable in a thread-safe manner.
* Converters may be altered at any time, including the JDK converters.
* It is strongly recommended to only alter the converters before performing
* actual conversions.
*/
public StringConvert() {
this(true);
}
/**
* Creates a new conversion manager.
*
* The convert instance is mutable in a thread-safe manner.
* Converters may be altered at any time, including the JDK converters.
* It is strongly recommended to only alter the converters before performing
* actual conversions.
*
* If specified, the factories will be queried in the order specified.
*
* @param includeJdkConverters true to include the JDK converters
* @param factories optional array of factories to use, not null
*/
public StringConvert(boolean includeJdkConverters, StringConverterFactory... factories) {
if (factories == null) {
throw new IllegalArgumentException("StringConverterFactory array must not be null");
}
for (int i = 0; i < factories.length; i++) {
if (factories[i] == null) {
throw new IllegalArgumentException("StringConverterFactory array must not contain a null element");
}
}
if (includeJdkConverters) {
for (JDKStringConverter conv : JDKStringConverter.values()) {
registered.put(conv.getType(), conv);
}
registered.put(Boolean.TYPE, JDKStringConverter.BOOLEAN);
registered.put(Byte.TYPE, JDKStringConverter.BYTE);
registered.put(Short.TYPE, JDKStringConverter.SHORT);
registered.put(Integer.TYPE, JDKStringConverter.INTEGER);
registered.put(Long.TYPE, JDKStringConverter.LONG);
registered.put(Float.TYPE, JDKStringConverter.FLOAT);
registered.put(Double.TYPE, JDKStringConverter.DOUBLE);
registered.put(Character.TYPE, JDKStringConverter.CHARACTER);
// Guava
tryRegisterGuava();
// JDK 1.8 classes
tryRegister("java.time.Instant", "parse");
tryRegister("java.time.Duration", "parse");
tryRegister("java.time.LocalDate", "parse");
tryRegister("java.time.LocalTime", "parse");
tryRegister("java.time.LocalDateTime", "parse");
tryRegister("java.time.OffsetTime", "parse");
tryRegister("java.time.OffsetDateTime", "parse");
tryRegister("java.time.ZonedDateTime", "parse");
tryRegister("java.time.Year", "parse");
tryRegister("java.time.YearMonth", "parse");
tryRegister("java.time.MonthDay", "parse");
tryRegister("java.time.Period", "parse");
tryRegister("java.time.ZoneOffset", "of");
tryRegister("java.time.ZoneId", "of");
// ThreeTen backport classes
tryRegister("org.threeten.bp.Instant", "parse");
tryRegister("org.threeten.bp.Duration", "parse");
tryRegister("org.threeten.bp.LocalDate", "parse");
tryRegister("org.threeten.bp.LocalTime", "parse");
tryRegister("org.threeten.bp.LocalDateTime", "parse");
tryRegister("org.threeten.bp.OffsetTime", "parse");
tryRegister("org.threeten.bp.OffsetDateTime", "parse");
tryRegister("org.threeten.bp.ZonedDateTime", "parse");
tryRegister("org.threeten.bp.Year", "parse");
tryRegister("org.threeten.bp.YearMonth", "parse");
tryRegister("org.threeten.bp.MonthDay", "parse");
tryRegister("org.threeten.bp.Period", "parse");
tryRegister("org.threeten.bp.ZoneOffset", "of");
tryRegister("org.threeten.bp.ZoneId", "of");
// Old ThreeTen/JSR-310 classes v0.6.3 and beyond
tryRegister("javax.time.Instant", "parse");
tryRegister("javax.time.Duration", "parse");
tryRegister("javax.time.calendar.LocalDate", "parse");
tryRegister("javax.time.calendar.LocalTime", "parse");
tryRegister("javax.time.calendar.LocalDateTime", "parse");
tryRegister("javax.time.calendar.OffsetDate", "parse");
tryRegister("javax.time.calendar.OffsetTime", "parse");
tryRegister("javax.time.calendar.OffsetDateTime", "parse");
tryRegister("javax.time.calendar.ZonedDateTime", "parse");
tryRegister("javax.time.calendar.Year", "parse");
tryRegister("javax.time.calendar.YearMonth", "parse");
tryRegister("javax.time.calendar.MonthDay", "parse");
tryRegister("javax.time.calendar.Period", "parse");
tryRegister("javax.time.calendar.ZoneOffset", "of");
tryRegister("javax.time.calendar.ZoneId", "of");
tryRegister("javax.time.calendar.TimeZone", "of");
}
if (factories.length > 0) {
this.factories.addAll(Arrays.asList(factories));
}
this.factories.add(AnnotationStringConverterFactory.INSTANCE);
if (includeJdkConverters) {
this.factories.add(EnumStringConverterFactory.INSTANCE);
}
}
/**
* Tries to register the Guava converters class.
*
* @param className the class name, not null
*/
private void tryRegisterGuava() {
try {
RenameHandler.INSTANCE.loadType("com.google.common.reflect.Types");
@SuppressWarnings("unchecked")
Class> cls = (Class>) RenameHandler.INSTANCE
.loadType("org.joda.convert.TypeTokenStringConverter");
TypedStringConverter> conv = (TypedStringConverter>) cls.newInstance();
registered.put(conv.getEffectiveType(), conv);
@SuppressWarnings("unchecked")
Class> cls2 = (Class>) RenameHandler.INSTANCE
.loadType("org.joda.convert.TypeStringConverter");
TypedStringConverter> conv2 = (TypedStringConverter>) cls2.newInstance();
registered.put(conv2.getEffectiveType(), conv2);
} catch (Throwable ex) {
// ignore
}
}
/**
* Tries to register a class using the standard toString/parse pattern.
*
* @param className the class name, not null
*/
private void tryRegister(String className, String fromStringMethodName) {
try {
Class> cls = RenameHandler.INSTANCE.lookupType(className);
registerMethods(cls, "toString", fromStringMethodName);
} catch (Throwable ex) {
// ignore
}
}
//-----------------------------------------------------------------------
/**
* Converts the specified object to a {@code String}.
*
* This uses {@link #findConverter} to provide the converter.
*
* @param object the object to convert, null returns null
* @return the converted string, may be null
* @throws RuntimeException (or subclass) if unable to convert
*/
public String convertToString(Object object) {
if (object == null) {
return null;
}
Class> cls = object.getClass();
StringConverter conv = findConverterNoGenerics(cls);
return conv.convertToString(object);
}
/**
* Converts the specified object to a {@code String}.
*
* This uses {@link #findConverter} to provide the converter.
* The class can be provided to select a more specific converter.
*
* @param cls the class to convert from, not null
* @param object the object to convert, null returns null
* @return the converted string, may be null
* @throws RuntimeException (or subclass) if unable to convert
*/
public String convertToString(Class> cls, Object object) {
if (object == null) {
return null;
}
StringConverter conv = findConverterNoGenerics(cls);
return conv.convertToString(object);
}
/**
* Converts the specified object from a {@code String}.
*
* This uses {@link #findConverter} to provide the converter.
*
* @param the type to convert to
* @param cls the class to convert to, not null
* @param str the string to convert, null returns null
* @return the converted object, may be null
* @throws RuntimeException (or subclass) if unable to convert
*/
public T convertFromString(Class cls, String str) {
if (str == null) {
return null;
}
StringConverter conv = findConverter(cls);
return conv.convertFromString(cls, str);
}
//-----------------------------------------------------------------------
/**
* Checks if a suitable converter exists for the type.
*
* This performs the same checks as the {@code findConverter} methods.
* Calling this before {@code findConverter} will cache the converter.
*
* Note that all exceptions, including developer errors are caught and hidden.
*
* @param cls the class to find a converter for, null returns false
* @return true if convertible
* @since 1.5
*/
public boolean isConvertible(final Class> cls) {
try {
return cls != null && findConverterQuiet(cls) != null;
} catch (RuntimeException ex) {
return false;
}
}
/**
* Finds a suitable converter for the type.
*
* This returns an instance of {@code StringConverter} for the specified class.
* This is designed for user code where the {@code Class} object generics is known.
*
* The search algorithm first searches the registered converters in the
* class hierarchy and immediate parent interfaces.
* It then searches for {@code ToString} and {@code FromString} annotations on the
* specified class, class hierarchy or immediate parent interfaces.
* Finally, it handles {@code Enum} instances.
*
* @param the type of the converter
* @param cls the class to find a converter for, not null
* @return the converter, not null
* @throws RuntimeException (or subclass) if no converter found
*/
public StringConverter findConverter(final Class cls) {
return findTypedConverter(cls);
}
/**
* Finds a suitable converter for the type with open generics.
*
* This returns an instance of {@code StringConverter} for the specified class.
* This is designed for framework usage where the {@code Class} object generics are unknown'?'.
* The returned type is declared with {@code Object} instead of '?' to
* allow the {@link ToStringConverter} to be invoked.
*
* The search algorithm first searches the registered converters in the
* class hierarchy and immediate parent interfaces.
* It then searches for {@code ToString} and {@code FromString} annotations on the
* specified class, class hierarchy or immediate parent interfaces.
* Finally, it handles {@code Enum} instances.
*
* @param cls the class to find a converter for, not null
* @return the converter, using {@code Object} to avoid generics, not null
* @throws RuntimeException (or subclass) if no converter found
* @since 1.5
*/
public StringConverter findConverterNoGenerics(final Class> cls) {
return findTypedConverterNoGenerics(cls);
}
/**
* Finds a suitable converter for the type.
*
* This returns an instance of {@code TypedStringConverter} for the specified class.
* This is designed for user code where the {@code Class} object generics is known.
*
* The search algorithm first searches the registered converters in the
* class hierarchy and immediate parent interfaces.
* It then searches for {@code ToString} and {@code FromString} annotations on the
* specified class, class hierarchy or immediate parent interfaces.
* Finally, it handles {@code Enum} instances.
*
* The returned converter may be queried for the effective type of the conversion.
* This can be used to find the best type to send in a serialized form.
*
* NOTE: Changing the method return type of {@link #findConverter(Class)}
* would be source compatible but not binary compatible. As this is a low-level
* library, binary compatibility is important, hence the addition of this method.
*
* @param the type of the converter
* @param cls the class to find a converter for, not null
* @return the converter, not null
* @throws RuntimeException (or subclass) if no converter found
* @since 1.7
*/
public TypedStringConverter findTypedConverter(final Class cls) {
TypedStringConverter conv = findConverterQuiet(cls);
if (conv == null) {
throw new IllegalStateException("No registered converter found: " + cls);
}
return conv;
}
/**
* Finds a suitable converter for the type with open generics.
*
* This returns an instance of {@code TypedStringConverter} for the specified class.
* This is designed for framework usage where the {@code Class} object generics are unknown'?'.
* The returned type is declared with {@code Object} instead of '?' to
* allow the {@link ToStringConverter} to be invoked.
*
* The search algorithm first searches the registered converters in the
* class hierarchy and immediate parent interfaces.
* It then searches for {@code ToString} and {@code FromString} annotations on the
* specified class, class hierarchy or immediate parent interfaces.
* Finally, it handles {@code Enum} instances.
*
* The returned converter may be queried for the effective type of the conversion.
* This can be used to find the best type to send in a serialized form.
*
* NOTE: Changing the method return type of {@link #findConverterNoGenerics(Class)}
* would be source compatible but not binary compatible. As this is a low-level
* library, binary compatibility is important, hence the addition of this method.
*
* @param cls the class to find a converter for, not null
* @return the converter, using {@code Object} to avoid generics, not null
* @throws RuntimeException (or subclass) if no converter found
* @since 1.7
*/
@SuppressWarnings("unchecked")
public TypedStringConverter findTypedConverterNoGenerics(final Class> cls) {
TypedStringConverter conv = (TypedStringConverter) findConverterQuiet(cls);
if (conv == null) {
throw new IllegalStateException("No registered converter found: " + cls);
}
return conv;
}
/**
* Finds a converter searching registered and annotated.
*
* @param the type of the converter
* @param cls the class to find a method for, not null
* @return the converter, null if no converter
* @throws RuntimeException if invalid
*/
@SuppressWarnings("unchecked")
private TypedStringConverter findConverterQuiet(final Class cls) {
if (cls == null) {
throw new IllegalArgumentException("Class must not be null");
}
TypedStringConverter conv = (TypedStringConverter) registered.get(cls);
if (conv == CACHED_NULL) {
return null;
}
if (conv == null) {
try {
conv = findAnyConverter(cls);
} catch (RuntimeException ex) {
registered.putIfAbsent(cls, CACHED_NULL);
throw ex;
}
if (conv == null) {
registered.putIfAbsent(cls, CACHED_NULL);
return null;
}
registered.putIfAbsent(cls, conv);
}
return conv;
}
/**
* Finds a converter searching registered and annotated.
*
* @param the type of the converter
* @param cls the class to find a method for, not null
* @return the converter, not null
* @throws RuntimeException if invalid
*/
@SuppressWarnings("unchecked")
private TypedStringConverter findAnyConverter(final Class cls) {
TypedStringConverter conv = null;
// check for registered on superclass
Class> loopCls = cls.getSuperclass();
while (loopCls != null && conv == null) {
conv = (TypedStringConverter) registered.get(loopCls);
if (conv != null && conv != CACHED_NULL) {
return conv;
}
loopCls = loopCls.getSuperclass();
}
// check for registered on interfaces
for (Class> loopIfc : cls.getInterfaces()) {
conv = (TypedStringConverter) registered.get(loopIfc);
if (conv != null && conv != CACHED_NULL) {
return conv;
}
}
// check factories
for (StringConverterFactory factory : factories) {
StringConverter factoryConv = (StringConverter) factory.findConverter(cls);
if (factoryConv != null) {
return TypedAdapter.adapt(cls, factoryConv);
}
}
return null;
}
//-----------------------------------------------------------------------
/**
* Registers a converter factory.
*
* This will be registered ahead of all existing factories.
*
* No new factories may be registered for the global singleton.
*
* @param factory the converter factory, not null
* @throws IllegalStateException if trying to alter the global singleton
* @since 1.5
*/
public void registerFactory(final StringConverterFactory factory) {
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
if (this == INSTANCE) {
throw new IllegalStateException("Global singleton cannot be extended");
}
factories.add(0, factory);
}
//-----------------------------------------------------------------------
/**
* Registers a converter for a specific type.
*
* The converter will be used for subclasses unless overidden.
*
* No new converters may be registered for the global singleton.
*
* @param the type of the converter
* @param cls the class to register a converter for, not null
* @param converter the String converter, not null
* @throws IllegalArgumentException if the class or converter are null
* @throws IllegalStateException if trying to alter the global singleton
*/
public void register(final Class cls, StringConverter converter) {
if (cls == null) {
throw new IllegalArgumentException("Class must not be null");
}
if (converter == null) {
throw new IllegalArgumentException("StringConverter must not be null");
}
if (this == INSTANCE) {
throw new IllegalStateException("Global singleton cannot be extended");
}
registered.put(cls, TypedAdapter.adapt(cls, converter));
}
/**
* Registers a converter for a specific type using two separate converters.
*
* This method registers a converter for the specified class.
* It is primarily intended for use with JDK 1.8 method references or lambdas:
*
* sc.register(Distance.class, Distance::toString, Distance::parse);
*
* The converter will be used for subclasses unless overidden.
*
* No new converters may be registered for the global singleton.
*
* @param the type of the converter
* @param cls the class to register a converter for, not null
* @param toString the to String converter, typically a method reference, not null
* @param fromString the from String converter, typically a method reference, not null
* @throws IllegalArgumentException if the class or converter are null
* @throws IllegalStateException if trying to alter the global singleton
* @since 1.3
*/
public void register(final Class cls, final ToStringConverter toString, final FromStringConverter fromString) {
if (fromString == null || toString == null) {
throw new IllegalArgumentException("Converters must not be null");
}
register(cls, new TypedStringConverter() {
@Override
public String convertToString(T object) {
return toString.convertToString(object);
}
@Override
public T convertFromString(Class extends T> cls, String str) {
return fromString.convertFromString(cls, str);
}
@Override
public Class> getEffectiveType() {
return cls;
}
});
}
/**
* Registers a converter for a specific type by method names.
*
* This method allows the converter to be used when the target class cannot have annotations added.
* The two method names must obey the same rules as defined by the annotations
* {@link ToString} and {@link FromString}.
* The converter will be used for subclasses unless overidden.
*
* No new converters may be registered for the global singleton.
*
* For example, {@code convert.registerMethods(Distance.class, "toString", "parse");}
*
* @param the type of the converter
* @param cls the class to register a converter for, not null
* @param toStringMethodName the name of the method converting to a string, not null
* @param fromStringMethodName the name of the method converting from a string, not null
* @throws IllegalArgumentException if the class or method name are null or invalid
* @throws IllegalStateException if trying to alter the global singleton
*/
public void registerMethods(final Class cls, String toStringMethodName, String fromStringMethodName) {
if (cls == null) {
throw new IllegalArgumentException("Class must not be null");
}
if (toStringMethodName == null || fromStringMethodName == null) {
throw new IllegalArgumentException("Method names must not be null");
}
if (this == INSTANCE) {
throw new IllegalStateException("Global singleton cannot be extended");
}
Method toString = findToStringMethod(cls, toStringMethodName);
Method fromString = findFromStringMethod(cls, fromStringMethodName);
MethodsStringConverter converter = new MethodsStringConverter(cls, toString, fromString, cls);
registered.putIfAbsent(cls, converter);
}
/**
* Registers a converter for a specific type by method and constructor.
*
* This method allows the converter to be used when the target class cannot have annotations added.
* The two method name and constructor must obey the same rules as defined by the annotations
* {@link ToString} and {@link FromString}.
* The converter will be used for subclasses unless overidden.
*
* No new converters may be registered for the global singleton.
*
* For example, {@code convert.registerMethodConstructor(Distance.class, "toString");}
*
* @param the type of the converter
* @param cls the class to register a converter for, not null
* @param toStringMethodName the name of the method converting to a string, not null
* @throws IllegalArgumentException if the class or method name are null or invalid
* @throws IllegalStateException if trying to alter the global singleton
*/
public void registerMethodConstructor(final Class cls, String toStringMethodName) {
if (cls == null) {
throw new IllegalArgumentException("Class must not be null");
}
if (toStringMethodName == null) {
throw new IllegalArgumentException("Method name must not be null");
}
if (this == INSTANCE) {
throw new IllegalStateException("Global singleton cannot be extended");
}
Method toString = findToStringMethod(cls, toStringMethodName);
Constructor fromString = findFromStringConstructorByType(cls);
MethodConstructorStringConverter converter = new MethodConstructorStringConverter(cls, toString, fromString);
registered.putIfAbsent(cls, converter);
}
/**
* Finds the conversion method.
*
* @param cls the class to find a method for, not null
* @param methodName the name of the method to find, not null
* @return the method to call, null means use {@code toString}
*/
private Method findToStringMethod(Class> cls, String methodName) {
Method m;
try {
m = cls.getMethod(methodName);
} catch (NoSuchMethodException ex) {
throw new IllegalArgumentException(ex);
}
if (Modifier.isStatic(m.getModifiers())) {
throw new IllegalArgumentException("Method must not be static: " + methodName);
}
return m;
}
/**
* Finds the conversion method.
*
* @param cls the class to find a method for, not null
* @param methodName the name of the method to find, not null
* @return the method to call, null means use {@code toString}
*/
private Method findFromStringMethod(Class> cls, String methodName) {
Method m;
try {
m = cls.getMethod(methodName, String.class);
} catch (NoSuchMethodException ex) {
try {
m = cls.getMethod(methodName, CharSequence.class);
} catch (NoSuchMethodException ex2) {
throw new IllegalArgumentException("Method not found", ex2);
}
}
if (Modifier.isStatic(m.getModifiers()) == false) {
throw new IllegalArgumentException("Method must be static: " + methodName);
}
return m;
}
/**
* Finds the conversion method.
*
* @param the type of the converter
* @param cls the class to find a method for, not null
* @return the method to call, null means use {@code toString}
*/
private Constructor findFromStringConstructorByType(Class cls) {
try {
return cls.getDeclaredConstructor(String.class);
} catch (NoSuchMethodException ex) {
try {
return cls.getDeclaredConstructor(CharSequence.class);
} catch (NoSuchMethodException ex2) {
throw new IllegalArgumentException("Constructor not found", ex2);
}
}
}
//-----------------------------------------------------------------------
/**
* Returns a simple string representation of the object.
*
* @return the string representation, never null
*/
@Override
public String toString() {
return getClass().getSimpleName();
}
}