/*
* Copyright 2010-2011 Stephen Colebourne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joda.convert;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Manager for conversion to and from a {@code String}, acting as the main client interface.
* <p>
* Support is provided for conversions based on the {@link StringConverter} interface
* or the {@link ToString} and {@link FromString} annotations.
* <p>
* StringConvert is thread-safe with concurrent caches.
*/
public final class StringConvert {
/**
* An immutable global instance.
* <p>
* This instance cannot be added to using {@link #register}, however annotated classes
* are picked up. To register your own converters, simply create an instance of this class.
*/
public static final StringConvert INSTANCE = new StringConvert();
/**
* The cache of converters.
*/
private final ConcurrentMap<Class<?>, StringConverter<?>> registered = new ConcurrentHashMap<Class<?>, StringConverter<?>>();
/**
* Creates a new conversion manager including the JDK converters.
*/
public StringConvert() {
this(true);
}
/**
* Creates a new conversion manager.
*
* @param includeJdkConverters true to include the JDK converters
*/
public StringConvert(boolean includeJdkConverters) {
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);
// JSR-310 classes
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");
}
}
/**
* 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 = getClass().getClassLoader().loadClass(className);
registerMethods(cls, "toString", fromStringMethodName);
} catch (Exception ex) {
// ignore
}
}
//-----------------------------------------------------------------------
/**
* Converts the specified object to a {@code String}.
* <p>
* This uses {@link #findConverter} to provide the converter.
*
* @param <T> the type to convert from
* @param object the object to convert, null returns null
* @return the converted string, may be null
* @throws RuntimeException (or subclass) if unable to convert
*/
@SuppressWarnings("unchecked")
public <T> String convertToString(T object) {
if (object == null) {
return null;
}
Class<T> cls = (Class<T>) object.getClass();
StringConverter<T> conv = findConverter(cls);
return conv.convertToString(object);
}
/**
* Converts the specified object to a {@code String}.
* <p>
* This uses {@link #findConverter} to provide the converter.
* The class can be provided to select a more specific converter.
*
* @param <T> the type to convert from
* @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 <T> String convertToString(Class<T> cls, T object) {
if (object == null) {
return null;
}
StringConverter<T> conv = findConverter(cls);
return conv.convertToString(object);
}
/**
* Converts the specified object from a {@code String}.
* <p>
* This uses {@link #findConverter} to provide the converter.
*
* @param <T> 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> T convertFromString(Class<T> cls, String str) {
if (str == null) {
return null;
}
StringConverter<T> conv = findConverter(cls);
return conv.convertFromString(cls, str);
}
/**
* Finds a suitable converter for the type.
* <p>
* This returns an instance of {@code StringConverter} for the specified class.
* This could be useful in other frameworks.
* <p>
* The search algorithm first searches the registered converters.
* It then searches for {@code ToString} and {@code FromString} annotations on the specified class.
* Both searches consider superclasses, but not interfaces.
*
* @param <T> 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
*/
@SuppressWarnings("unchecked")
public <T> StringConverter<T> findConverter(final Class<T> cls) {
if (cls == null) {
throw new IllegalArgumentException("Class must not be null");
}
StringConverter<T> conv = (StringConverter<T>) registered.get(cls);
if (conv == null) {
if (cls == Object.class) {
throw new IllegalStateException("No registered converter found: " + cls);
}
Class<?> loopCls = cls.getSuperclass();
while (loopCls != null && conv == null) {
conv = (StringConverter<T>) registered.get(loopCls);
loopCls = loopCls.getSuperclass();
}
if (conv == null) {
conv = findAnnotationConverter(cls);
if (conv == null) {
throw new IllegalStateException("No registered converter found: " + cls);
}
}
registered.putIfAbsent(cls, conv);
}
return conv;
}
/**
* Finds the conversion method.
*
* @param <T> 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 <T> StringConverter<T> findAnnotationConverter(final Class<T> cls) {
Method toString = findToStringMethod(cls);
if (toString == null) {
return null;
}
Constructor<T> con = findFromStringConstructor(cls);
Method fromString = findFromStringMethod(cls, con == null);
if (con == null && fromString == null) {
throw new IllegalStateException("Class annotated with @ToString but not with @FromString");
}
if (con != null && fromString != null) {
throw new IllegalStateException("Both method and constructor are annotated with @FromString");
}
if (con != null) {
return new MethodConstructorStringConverter<T>(cls, toString, con);
} else {
return new MethodsStringConverter<T>(cls, toString, fromString);
}
}
/**
* Finds the conversion method.
*
* @param cls the class to find a method for, not null
* @return the method to call, null means use {@code toString}
*/
private Method findToStringMethod(Class<?> cls) {
Method matched = null;
Class<?> loopCls = cls;
while (loopCls != null && matched == null) {
Method[] methods = loopCls.getDeclaredMethods();
for (Method method : methods) {
ToString toString = method.getAnnotation(ToString.class);
if (toString != null) {
if (matched != null) {
throw new IllegalStateException("Two methods are annotated with @ToString");
}
matched = method;
}
}
loopCls = loopCls.getSuperclass();
}
return matched;
}
/**
* Finds the conversion method.
*
* @param <T> 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 <T> Constructor<T> findFromStringConstructor(Class<T> cls) {
Constructor<T> con;
try {
con = cls.getDeclaredConstructor(String.class);
} catch (NoSuchMethodException ex) {
try {
con = cls.getDeclaredConstructor(CharSequence.class);
} catch (NoSuchMethodException ex2) {
return null;
}
}
FromString fromString = con.getAnnotation(FromString.class);
return fromString != null ? con : null;
}
/**
* Finds the conversion method.
*
* @param cls the class to find a method for, not null
* @return the method to call, null means use {@code toString}
*/
private Method findFromStringMethod(Class<?> cls, boolean searchSuperclasses) {
Method matched = null;
Class<?> loopCls = cls;
while (loopCls != null && matched == null) {
Method[] methods = loopCls.getDeclaredMethods();
for (Method method : methods) {
FromString fromString = method.getAnnotation(FromString.class);
if (fromString != null) {
if (matched != null) {
throw new IllegalStateException("Two methods are annotated with @ToString");
}
matched = method;
}
}
if (searchSuperclasses == false) {
break;
}
loopCls = loopCls.getSuperclass();
}
return matched;
}
//-----------------------------------------------------------------------
/**
* Registers a converter for a specific type.
* <p>
* The converter will be used for subclasses unless overidden.
* <p>
* No new converters may be registered for the global singleton.
*
* @param <T> 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 unable to register
* @throws IllegalStateException if class already registered
*/
public <T> void register(final Class<T> cls, StringConverter<T> 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");
}
StringConverter<?> old = registered.putIfAbsent(cls, converter);
if (old != null) {
throw new IllegalStateException("Converter already registered for class: " + cls);
}
}
/**
* Registers a converter for a specific type by method names.
* <p>
* 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.
* <p>
* No new converters may be registered for the global singleton.
* <p>
* For example, {@code convert.registerMethods(Distance.class, "toString", "parse");}
*
* @param <T> 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 unable to register
* @throws IllegalStateException if class already registered
*/
public <T> void registerMethods(final Class<T> 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<T> converter = new MethodsStringConverter<T>(cls, toString, fromString);
StringConverter<?> old = registered.putIfAbsent(cls, converter);
if (old != null) {
throw new IllegalStateException("Converter already registered for class: " + cls);
}
}
/**
* Registers a converter for a specific type by method and constructor.
* <p>
* 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.
* <p>
* No new converters may be registered for the global singleton.
* <p>
* For example, {@code convert.registerMethodConstructor(Distance.class, "toString");}
*
* @param <T> 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 unable to register
* @throws IllegalStateException if class already registered
*/
public <T> void registerMethodConstructor(final Class<T> 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<T> fromString = findFromStringConstructorByType(cls);
MethodConstructorStringConverter<T> converter = new MethodConstructorStringConverter<T>(cls, toString, fromString);
StringConverter<?> old = registered.putIfAbsent(cls, converter);
if (old != null) {
throw new IllegalStateException("Converter already registered for class: " + cls);
}
}
/**
* 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 <T> 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 <T> Constructor<T> findFromStringConstructorByType(Class<T> 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();
}
}