
org.codemonkey.javareflection.ValueConverter Maven / Gradle / Ivy
Show all versions of java-reflection Show documentation
package org.codemonkey.javareflection;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.math.NumberUtils;
/**
* This reflection utility class predicts (and converts) which types a specified value can be converted into. It can only do conversions of
* known 'common' types, which include:
*
* - Any {@link Number} type (Integer, Character, Double, byte, etc.)
* String
* Boolean
* Character
*
*
* @author Benny Bottema
* @see IncompatibleTypeException
*/
public final class ValueConverter {
/**
* List of common types that all other common types can always convert to. For example, String
and Integer
are
* basic common types and can be converted to any other common type.
*/
static final List> COMMON_TYPES = Arrays.asList(new Class>[] { String.class, Integer.class, int.class, Float.class,
float.class, Double.class, double.class, Long.class, long.class, Byte.class, byte.class, Short.class, short.class,
Boolean.class, boolean.class, Character.class, char.class });
/**
* A list of all primitive number types.
*/
static final List> PRIMITIVE_NUMBER_TYPES = Arrays.asList(new Class>[] { byte.class, short.class, int.class, long.class,
float.class, double.class });
/**
* @param c The class to inspect.
* @return whether given type is a known common type.
*/
public static boolean isCommonType(final Class> c) {
return COMMON_TYPES.contains(c);
}
/**
* Private constructor prevents from instantiating this utility class.
*/
private ValueConverter() {
// utility class
}
/**
* Determines to which types the specified value (its type) can be converted to. Most common types can be converted to most other common
* types and all types can be converted into a String using {@link Object#toString()}.
*
* @param c The input type to find compatible conversion output types for
* @return The list with compatible conversion output types.
*/
public static List> collectCompatibleTypes(final Class> c) {
if (isCommonType(c)) {
return Collections.unmodifiableList(COMMON_TYPES);
} else {
// not a common type, we only know we're able to convert to String
return Arrays.asList(new Class>[] { String.class });
}
}
/**
* Converts a list of values to their converted form, as indicated by the specified targetTypes.
*
* @param args The list with value to convert.
* @param targetTypes The output types the specified values should be converted into.
* @param useOriginalValueWhenIncompatible Indicates whether an exception should be thrown for inconvertible values or that the original
* value should be used instead.
* @return Array containing converted values where convertible or the original value otherwise.
* @throws IncompatibleTypeException Thrown when unable to convert and not use the original value.
*/
public static Object[] convert(final Object[] args, final Class>[] targetTypes, boolean useOriginalValueWhenIncompatible)
throws IncompatibleTypeException {
if (args.length != targetTypes.length) {
throw new IllegalStateException("number of target types should match the number of arguments");
}
final Object[] convertedValues = new Object[args.length];
for (int i = 0; i < targetTypes.length; i++) {
try {
convertedValues[i] = convert(args[i], targetTypes[i]);
} catch (IncompatibleTypeException e) {
if (useOriginalValueWhenIncompatible) {
// simply take over the original value and keep converting where possible
convertedValues[i] = args[i];
} else {
throw e;
}
}
}
return convertedValues;
}
/**
* Converts a single value into a target output datatype. Only input/output pairs should be passed in here according to the possible
* conversions as determined by {@link #collectCompatibleTypes(Class)}.
*
* First checks if the input and output types aren't the same. Then the conversions are checked for and done in the following order:
*
* - conversion to
String
* - conversion to any {@link Number}
* - conversion to
Boolean
* - conversion to
Character
*
*
* @param value The value to convert.
* @param targetType The target data type the value should be converted into.
* @return The converted value according the specified target data type.
* @throws IncompatibleTypeException Thrown by the various convert()
methods used.
*/
public static Object convert(final Object value, final Class> targetType)
throws IncompatibleTypeException {
if (value == null) {
return null;
}
final Class> valueType = value.getClass();
// 1. check if conversion is required to begin with
if (targetType.isAssignableFrom(valueType)) {
return value;
// 2. check if we can simply use Object.toString() implementation
} else if (targetType.equals(String.class)) {
return value.toString();
// 3. check if we can reuse conversion from String
} else if (valueType.equals(String.class)) {
return convert((String) value, targetType);
// 4. check if we can reuse conversion from a Number subtype
} else if (Number.class.isAssignableFrom(valueType) || isPrimitiveNumber(valueType)) {
return convert((Number) value, targetType);
// 4. check if we can reuse conversion from boolean value
} else if (valueType.equals(Boolean.class) || valueType.equals(boolean.class)) {
return convert((Boolean) value, targetType);
// 5. check if we can reuse conversion from character
} else if (valueType.equals(Character.class) || valueType.equals(char.class)) {
return convert((Character) value, targetType);
} else {
throw new IncompatibleTypeException(value, valueType.toString(), targetType.toString());
}
}
/**
* Attempts to convert a {@link Number} to the target datatype.
*
* NOTE: precision may be lost when converting from a wide number to a narrower number (say float to integer). These
* conversions are done by simply calling {@link Number#intValue()} and {@link Number#floatValue()} etc.
*
* @param value The number to convert.
* @param targetType The target datatype the number should be converted into.
* @return The converted number.
* @throws IncompatibleTypeException Thrown when unable to find a compatible conversion.
*/
public static Object convert(final Number value, final Class> targetType)
throws IncompatibleTypeException {
if (value == null) {
return null;
}
if (targetType.equals(String.class)) {
return value.toString();
} else if (targetType.equals(Integer.class) || targetType.equals(int.class)) {
return value.intValue();
} else if (targetType.equals(Boolean.class) || targetType.equals(boolean.class)) {
// any non-zero number converts to true
return value.intValue() > 0;
} else if (targetType.equals(Float.class) || targetType.equals(float.class)) {
return value.floatValue();
} else if (targetType.equals(Double.class) || targetType.equals(double.class)) {
return value.doubleValue();
} else if (targetType.equals(Long.class) || targetType.equals(long.class)) {
return value.longValue();
} else if (targetType.equals(Byte.class) || targetType.equals(byte.class)) {
return value.byteValue();
} else if (targetType.equals(Short.class) || targetType.equals(short.class)) {
return value.shortValue();
} else if (targetType.equals(Character.class) || targetType.equals(char.class)) {
return Character.forDigit(value.intValue(), 10);
} else {
throw new IncompatibleTypeException(value, value.getClass().toString(), targetType.toString());
}
}
/**
* Attempts to convert a Boolean
to the target datatype.
*
* @param value The boolean to convert.
* @param targetType The target datatype the boolean should be converted into.
* @return The converted boolean.
* @throws IncompatibleTypeException Thrown when unable to find a compatible conversion.
*/
@SuppressWarnings("unchecked")
public static Object convert(final Boolean value, final Class> targetType)
throws IncompatibleTypeException {
if (value == null) {
return null;
}
if (targetType.equals(Boolean.class) || targetType.equals(boolean.class)) {
return value;
} else if (targetType.equals(String.class)) {
return value.toString();
} else if (Number.class.isAssignableFrom(targetType) || isPrimitiveNumber(targetType)) {
return convertNumber(value ? "1" : "0", (Class) targetType);
} else if (targetType.equals(Character.class) || targetType.equals(char.class)) {
return value ? '1' : '0';
}
// Boolean value incompatible with targetType
throw new IncompatibleTypeException(value, Boolean.class.toString(), targetType.toString());
}
/**
* Attempts to convert a Character
to the target datatype.
*
* @param value The character to convert.
* @param targetType The target datatype the character should be converted into.
* @return The converted character.
* @throws IncompatibleTypeException Thrown when unable to find a compatible conversion.
*/
@SuppressWarnings("unchecked")
public static Object convert(final Character value, final Class> targetType)
throws IncompatibleTypeException {
if (value == null) {
return null;
}
if (targetType.equals(String.class)) {
return value.toString();
} else if (targetType.equals(char.class) || targetType.equals(Character.class)) {
return value;
} else if (Number.class.isAssignableFrom(targetType) || isPrimitiveNumber(targetType)) {
// convert Character to Number
return convertNumber(String.valueOf(value), (Class) targetType);
} else if (targetType.equals(Boolean.class) || targetType.equals(boolean.class)) {
// convert Character to Boolean
if (value.equals('0')) {
return false;
} else if (value.equals('1')) {
return true;
} else {
// Character incompatible with type Boolean
}
}
// Character incompatible with type targetType
throw new IncompatibleTypeException(value, Character.class.toString(), targetType.toString());
}
/**
* Attempts to convert a String
to the target datatype.
*
* @param value The string to convert.
* @param targetType The target datatype the string should be converted into.
* @return The converted string.
* @throws IncompatibleTypeException Thrown when unable to find a compatible conversion.
*/
@SuppressWarnings("unchecked")
public static Object convert(final String value, final Class> targetType)
throws IncompatibleTypeException {
if (value == null) {
return null;
} else if (targetType.equals(String.class)) {
return value;
} else if (targetType.isEnum()) {
return convertEnum(value, (Class extends Enum>>) targetType);
} else if (value.length() > 0) {
if (value.length() == 1) {
// Convert String as Character
if (targetType.equals(Character.class) || targetType.equals(char.class)) {
return value.charAt(0);
} else {
return convert(value.charAt(0), targetType);
}
} else if (Number.class.isAssignableFrom(targetType) || isPrimitiveNumber(targetType)) {
return convertNumber(value, (Class extends Number>) targetType);
} else if (targetType.equals(Boolean.class) || targetType.equals(boolean.class)) {
// convert String to Boolean
if (value.equalsIgnoreCase("false") || value.equalsIgnoreCase("0")) {
return false;
} else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("1")) {
return true;
} else {
// String incompatible with type Boolean
}
}
}
// String value incompatible with targetType
throw new IncompatibleTypeException(value, value.getClass().toString(), targetType.toString());
}
/**
* Attempts to convert a String
to an Enum instance, by mapping to the enum's name using
* {@link Enum#valueOf(Class, String)}.
*
* @param value The value, which should be the name of one instance of the given enum.
* @param targetType The enum type to which which we'll try to convert.
* @return An enum of the given type, or null
otherwise.
*/
public static Object convertEnum(final String value, final Class extends Enum>> targetType) {
if (value == null) {
return null;
}
// /CLOVER:OFF
try {
// /CLOVER:ON
return targetType.getMethod("valueOf", String.class).invoke(null, value);
// /CLOVER:OFF
} catch (final IllegalArgumentException e) {
throw new IncompatibleTypeException(value, Enum.class.toString(), targetType.toString(), e);
} catch (final SecurityException e) {
throw new IncompatibleTypeException(value, Enum.class.toString(), targetType.toString(), e);
} catch (final IllegalAccessException e) {
throw new IncompatibleTypeException(value, Enum.class.toString(), targetType.toString(), e);
} catch (final InvocationTargetException e) {
throw new IncompatibleTypeException(value, Enum.class.toString(), targetType.toString(), e);
} catch (final NoSuchMethodException e) {
throw new IncompatibleTypeException(value, Enum.class.toString(), targetType.toString(), e);
}
// /CLOVER:ON
}
/**
* Attempts to convert a String
to the specified Number
type.
*
* @param value The string value which should be a number.
* @param numberType The Class
type that should be one of Number
.
* @return A {@link Number} subtype value converted from value
(or null
if value is null
).
*/
public static Object convertNumber(final String value, final Class extends Number> numberType) {
if (value == null) {
return null;
}
Validate.isTrue(Number.class.isAssignableFrom(numberType) || isPrimitiveNumber(numberType));
if (NumberUtils.isNumber(value)) {
if (Integer.class.equals(numberType) || int.class.equals(numberType) || Number.class.equals(numberType)) {
return Integer.parseInt(value);
} else if (Byte.class.equals(numberType) || byte.class.equals(numberType)) {
return Byte.parseByte(value);
} else if (Short.class.equals(numberType) || short.class.equals(numberType)) {
return Short.parseShort(value);
} else if (Long.class.equals(numberType) || long.class.equals(numberType)) {
return Long.parseLong(value);
} else if (Float.class.equals(numberType) || float.class.equals(numberType)) {
return Float.parseFloat(value);
} else if (Double.class.equals(numberType) || double.class.equals(numberType)) {
return Double.parseDouble(value);
} else if (BigInteger.class.equals(numberType)) {
return BigInteger.valueOf(Long.parseLong(value));
} else if (BigDecimal.class.equals(numberType)) {
return BigDecimal.valueOf(Long.parseLong(value));
} else {
// specified type incompatible with Number
throw new IncompatibleTypeException(value, value.getClass().toString(), numberType.toString());
}
} else {
// specified value incompatible with Number
throw new IncompatibleTypeException(value, value.getClass().toString(), numberType.toString());
}
}
/**
* Returns whether a {@link Class} is a primitive number.
*
* @param targetType The class to check whether it's a number.
* @return Whether specified class is a primitive number.
*/
public final static boolean isPrimitiveNumber(final Class> targetType) {
return PRIMITIVE_NUMBER_TYPES.contains(targetType);
}
/**
* This exception can be thrown in any of the conversion methods of {@link ValueConverter}, to indicate a value could not be converted
* into the target datatype. It doesn't mean a failed attempt at a conversion, it means that there was no way to convert the input value
* to begin with.
*
* @author Benny Bottema
*/
public static final class IncompatibleTypeException extends RuntimeException {
private static final long serialVersionUID = -9234872336546L;
private static final String pattern = "error: unable to convert value '%s': '%s' to '%s'";
/**
* @see RuntimeException#RuntimeException(String, Throwable)
*/
@SuppressWarnings("javadoc")
public IncompatibleTypeException(final String message, final Exception e) {
super(message, e);
}
/**
* @see RuntimeException#RuntimeException(String)
*/
@SuppressWarnings("javadoc")
public IncompatibleTypeException(final Object value, final String className, final String targetName) {
super(String.format(pattern, value, className, targetName));
}
/**
* @see RuntimeException#RuntimeException(String, Throwable)
*/
@SuppressWarnings("javadoc")
public IncompatibleTypeException(final Object value, final String className, final String targetName,
final Exception nestedException) {
super(String.format(pattern, value, className, targetName), nestedException);
}
}
}