Below is a generic code for parsing strings (read from file for example) and converting them to .java types, such as primitives (and their wrappers), java.io.File, Enum etc. There is also possible to register custom made TypeParser
's, for non-standard java types (Or provide a static factory method named: valueOf(String) in your class.).
- Is the usage of generics done in a correct way?
- Is the registration mechanism of custom made
TypeParser
's made with best practices way? - Is the usage of the Builder pattern done the right way?
- Any other comments?
package com.github.drapostolos.typeparser;
public interface TypeParser<T> {
T parse(String value);
}
package com.github.drapostolos.typeparser;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
class DefaultTypeParsers {
static List<TypeParser<?>> list() {
List<TypeParser<?>> result = new ArrayList<TypeParser<?>>();
for(Class<?> c : DefaultTypeParsers.class.getDeclaredClasses()){
if(TypeParser.class.isAssignableFrom(c)){
TypeParser<?> instance;
try {
instance = (TypeParser<?>) c.newInstance();
result.add(instance);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
return result;
}
static class BooleanTypeParser implements TypeParser<Boolean> {
@Override
public Boolean parse(final String value0) {
String value = value0.trim().toLowerCase();
if(value.equals("true") || value.equals("1")){
return Boolean.TRUE;
} else if(value.equals("false") || value.equals("0")){
return Boolean.FALSE;
}
String message = "\"%s\" is not parsable to a Boolean.";
throw new IllegalArgumentException(String.format(message, value0));
}
}
static class CharacterTypeParser implements TypeParser<Character>{
@Override
public Character parse(String value) {
if(value.length() == 1){
return Character.valueOf(value.charAt(0));
}
String message = "\"%s\" must only contain a single character.";
throw new IllegalArgumentException(String.format(message, value));
}
}
static class ByteTypeParser implements TypeParser<Byte> {
@Override
public Byte parse(String value) {
return Byte.valueOf(value.trim());
}
}
static class IntegerTypeParser implements TypeParser<Integer>{
@Override
public Integer parse(String value) {
return Integer.valueOf(value.trim());
}
}
static class LongTypeParser implements TypeParser<Long>{
@Override
public Long parse(String value) {
return Long.valueOf(value.trim());
}
}
static class ShortTypeParser implements TypeParser<Short>{
@Override
public Short parse(String value) {
return Short.valueOf(value.trim());
}
}
static class FloatTypeParser implements TypeParser<Float>{
@Override
public Float parse(String value) {
return Float.valueOf(value);
}
}
static class DoubleTypeParser implements TypeParser<Double>{
@Override
public Double parse(String value) {
return Double.valueOf(value);
}
}
static class FileTypeParser implements TypeParser<File>{
@Override
public File parse(String value) {
return new File(value.trim());
}
}
static class StringTypeParser implements TypeParser<String>{
@Override
public String parse(String value) {
return value;
}
}
}
package com.github.drapostolos.typeparser;
import java.util.HashMap;
import java.util.Map;
public final class StringToTypeParserBuilder {
private final static Map<Class<?>, TypeParser<?>> defaultTypeParsers = new HashMap<Class<?>, TypeParser<?>>();
private final static Map<Class<?>, Class<?>> wrapperToPrimitiveMapper = new HashMap<Class<?>, Class<?>>();
private Map<Class<?>, TypeParser<?>> typeParsers;
static {
wrapperToPrimitiveMapper.put(Boolean.class, boolean.class);
wrapperToPrimitiveMapper.put(Byte.class, byte.class);
wrapperToPrimitiveMapper.put(Short.class, short.class);
wrapperToPrimitiveMapper.put(Character.class, char.class);
wrapperToPrimitiveMapper.put(Integer.class, int.class);
wrapperToPrimitiveMapper.put(Long.class, long.class);
wrapperToPrimitiveMapper.put(Float.class, float.class);
wrapperToPrimitiveMapper.put(Double.class, double.class);
for(TypeParser<?> typeParser : DefaultTypeParsers.list()){
Class<?> type = Util.extractTypeParameter(typeParser);
defaultTypeParsers.put(type, typeParser);
if(wrapperToPrimitiveMapper.containsKey(type)){
// add primitive version, example int.class, boolean.class etc.
defaultTypeParsers.put(wrapperToPrimitiveMapper.get(type), typeParser);
}
}
}
StringToTypeParserBuilder() {
// Initialize with the default typeParsers
typeParsers = new HashMap<Class<?>, TypeParser<?>>(defaultTypeParsers);
}
public StringToTypeParserBuilder unregisterTypeParser(Class<?> type){
if(type == null) {
throw new NullPointerException(Util.nullArgumentErrorMsg("type"));
}
typeParsers.remove(type);
return this;
}
public StringToTypeParserBuilder registerTypeParser(TypeParser<?> typeParser){
if(typeParser == null) {
throw new NullPointerException(Util.nullArgumentErrorMsg("typeParser"));
}
typeParsers.put(Util.extractTypeParameter(typeParser), typeParser);
return this;
}
public <T> StringToTypeParserBuilder registerTypeParser(Class<T> type, TypeParser<? extends T> typeParser){
if(typeParser == null) {
throw new NullPointerException(Util.nullArgumentErrorMsg("typeParser"));
}
if(type == null) {
throw new NullPointerException(Util.nullArgumentErrorMsg("type"));
}
typeParsers.put(type, typeParser);
return this;
}
public StringToTypeParser build(){
return new StringToTypeParser(new HashMap<Class<?>, TypeParser<?>>(typeParsers));
}
}
package com.github.drapostolos.typeparser;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
public final class StringToTypeParser {
private static final StringToTypeParser defaultTypeParser = newBuilder().build();
private final Map<Class<?>, TypeParser<?>> typeParsers;
public static StringToTypeParserBuilder newBuilder() {
return new StringToTypeParserBuilder();
}
public static <T> T parse(String value, Class<T> type){
return defaultTypeParser.parseType(value, type);
}
StringToTypeParser(HashMap<Class<?>, TypeParser<?>> typeParsers) {
this.typeParsers = typeParsers;
}
public <T> T parseType(String value, Class<T> type) {
if (value == null) {
throw new NullPointerException(Util.nullArgumentErrorMsg("value"));
}
if (type == null) {
throw new NullPointerException(Util.nullArgumentErrorMsg("type"));
}
// convert "null" string to null type.
if (value.trim().equalsIgnoreCase("null")) {
if (type.isPrimitive()) {
String message = "'%s' primitive can not be set to null.";
throw new IllegalArgumentException(String.format(message, type.getName()));
}
return null;
}
Object result = null;
if (typeParsers.containsKey(type)) {
result = callTypeParser(value, type);
} else if ((result = callFactoryMethodIfExisting("valueOf", value, type)) != null) {
//
} else if ((result = callFactoryMethodIfExisting("of", value, type)) != null) {
//
} else {
String message = "There is no registered 'TypeParser' for that type, or that "
+ "type does not contain one of the following static factory methods: "
+ "'%s.valueOf(String)', or '%s.of(String)'.";
message = String.format(message, type.getSimpleName(), type.getSimpleName());
message = canNotParseErrorMsg(value, type, message);
throw new IllegalArgumentException(message);
}
/*
* This cast is correct, since all above checks ensures we're casting to
* the right type.
*/
@SuppressWarnings("unchecked")
T temp = (T) result;
return temp;
}
private Object callTypeParser(String value, Class<?> type) {
try {
return typeParsers.get(type).parse(value);
} catch (NumberFormatException e) {
String message = canNotParseErrorMsg(value, type, numberFormatErrorMsg(e));
throw new IllegalArgumentException(message, e);
} catch (RuntimeException e) {
throw new IllegalArgumentException(canNotParseErrorMsg(value, type, e.getMessage()), e);
}
}
private static String numberFormatErrorMsg(NumberFormatException e) {
return String.format("Number format exception %s.", e.getMessage());
}
private static String canNotParseErrorMsg(String value, Class<?> type, String message) {
return String.format("Can not parse \"%s\" to type '%s' due to: %s", value, type.getName(), message);
}
private static Object callFactoryMethodIfExisting(String methodName, String value, Class<?> type) {
Method m;
try {
m = type.getDeclaredMethod(methodName, String.class);
if (!Modifier.isStatic(m.getModifiers())) {
// Static factory method does not exists, return null
return null;
}
} catch (Exception e) {
// Static factory method does not exists, return null
return null;
}
try {
if(type.isEnum()){
return m.invoke(null, value.trim());
} else {
return m.invoke(null, value);
}
} catch (InvocationTargetException e) {
throw new IllegalArgumentException(makeErrorMsg(methodName, value, type), e.getCause());
} catch (Throwable t) {
throw new IllegalArgumentException(makeErrorMsg(methodName, value, type), t);
}
}
private static String makeErrorMsg(String methodName, String value, Class<?> type) {
String methodSignature = String.format("%s.%s('%s')", type.getName(), methodName, value);
String message = " Exception thrown in static factory method '%s'. See underlying "
+ "exception for additional information.";
return canNotParseErrorMsg(value, type, String.format(message, methodSignature));
}
}
package com.github.drapostolos.typeparser;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
class Util {
private Util() { /* Not meant for instantiation */}
static String nullArgumentErrorMsg(String argName) {
return String.format("Argument named '%s' is illegally set to null!", argName);
}
static Class<?> extractTypeParameter(TypeParser<?> typeParser) {
return extractTypeParameter(typeParser.getClass(), typeParser);
}
/*
* Pass the original TypeParser as well, to be able to print
* it in error message.
*/
private static Class<?> extractTypeParameter(Class<?> c, TypeParser<?> typeParser) {
if(c == null){
String message = "Can not find parametirized type in %s !?!?!? Must be a bug somewhere...";
throw new IllegalArgumentException(String.format(message, typeParser));
}
for (Type t : c.getGenericInterfaces()){
if(t instanceof ParameterizedType){
ParameterizedType type = (ParameterizedType) t;
if(TypeParser.class.equals(type.getRawType())){
return (Class<?>) type.getActualTypeArguments()[0];
}
}
}
return extractTypeParameter(c.getSuperclass(), typeParser);
}
}
I should also give some usage examples. Use the below static method for simple usage (only supports the default java types):
Integer i1 = StringToTypeParser.parse(" 1", Integer.class);
int i2 = StringToTypeParser.parse("2", int.class);
float f = StringToTypeParser.parse("2.2", float.class);
Number n = StringToTypeParser.parse("2.2", float.class);
MyEnum a = StringToTypeParser.parse("BBB", MyEnum.class);
File file = StringToTypeParser.parse("/path/to", File.class);
Boolean b1 = StringToTypeParser.parse("true", Boolean.class);
boolean b2 = StringToTypeParser.parse("0", Boolean.class);
where
public enum MyEnum{AAA, BBB};
Or if you want to register your own TypeParsers, build your own StringToTypeParser instance (which by default supports the java types) as follow:
StringToTypeParser parser = StringToTypeParser.newBuilder()
.registerTypeParser(new CarTypeParser())
.registerTypeParser(int.class, new MySpecialIntTypeParser())
.build();
Car volvo = parser.parseType("volvo", Car.class);
Edit: after comments from review:
Thanks for the great comments in the answer! Below are the final outcome of the changes made. The Util.java class is now removed, so is also all the reflection code, which makes a much nicer/cleaner code.
The TypeParser interface remains the same
package com.github.drapostolos.typeparser;
public interface TypeParser<T> {
T parse(String value);
}
Replaced the static inner classes with anonymous classes instead.
package com.github.drapostolos.typeparser;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
class DefaultTypeParsers {
private DefaultTypeParsers() { new AssertionError("Not meant for instantiation"); }
private static final String BOOLEAN_ERROR_MESSAGE = "\"%s\" is not parsable to a Boolean.";
private static final String CHARACTER_ERROR_MESSAGE = "\"%s\" must only contain a single character.";
private static final Map<Class<?>, TypeParser<?>> DEFAULT_TYPE_PARSERS = new HashMap<Class<?>, TypeParser<?>>();
private static final Map<Class<?>, Class<?>> WRAPPER_TO_PRIMITIVE = new HashMap<Class<?>, Class<?>>();
static Map<Class<?>, TypeParser<?>> copy() {
return new HashMap<Class<?>, TypeParser<?>>(DEFAULT_TYPE_PARSERS);
}
static{
WRAPPER_TO_PRIMITIVE.put(Boolean.class, boolean.class);
WRAPPER_TO_PRIMITIVE.put(Byte.class, byte.class);
WRAPPER_TO_PRIMITIVE.put(Short.class, short.class);
WRAPPER_TO_PRIMITIVE.put(Character.class, char.class);
WRAPPER_TO_PRIMITIVE.put(Integer.class, int.class);
WRAPPER_TO_PRIMITIVE.put(Long.class, long.class);
WRAPPER_TO_PRIMITIVE.put(Float.class, float.class);
WRAPPER_TO_PRIMITIVE.put(Double.class, double.class);
}
private static <T> void put(Class<T> type, TypeParser<? extends T> typeParser){
DEFAULT_TYPE_PARSERS.put(type, typeParser);
if(WRAPPER_TO_PRIMITIVE.containsKey(type)){
// add primitive targetType if existing, example int.class, boolean.class etc.
Class<?> primitiveType = WRAPPER_TO_PRIMITIVE.get(type);
DEFAULT_TYPE_PARSERS.put(primitiveType, typeParser);
}
}
static{
put(Boolean.class, new TypeParser<Boolean>() {
@Override
public Boolean parse(final String value0) {
String value = value0.trim().toLowerCase();
if(value.equals("true")){
return Boolean.TRUE;
} else if(value.equals("false")){
return Boolean.FALSE;
}
throw new IllegalArgumentException(String.format(BOOLEAN_ERROR_MESSAGE, value0));
}
});
put(Character.class, new TypeParser<Character>() {
@Override
public Character parse(String value) {
if(value.length() == 1){
return Character.valueOf(value.charAt(0));
}
throw new IllegalArgumentException(String.format(CHARACTER_ERROR_MESSAGE, value));
}
});
put(Byte.class, new TypeParser<Byte>() {
@Override
public Byte parse(String value) {
return Byte.valueOf(value.trim());
}
});
put(Integer.class, new TypeParser<Integer>() {
@Override
public Integer parse(String value) {
return Integer.valueOf(value.trim());
}
});
put(Long.class, new TypeParser<Long>() {
@Override
public Long parse(String value) {
return Long.valueOf(value.trim());
}
});
put(Short.class, new TypeParser<Short>() {
@Override
public Short parse(String value) {
return Short.valueOf(value.trim());
}
});
put(Float.class, new TypeParser<Float>() {
@Override
public Float parse(String value) {
return Float.valueOf(value);
}
});
put(Double.class, new TypeParser<Double>() {
@Override
public Double parse(String value) {
return Double.valueOf(value);
}
});
put(File.class, new TypeParser<File>() {
@Override
public File parse(String value) {
return new File(value.trim());
}
});
put(String.class, new TypeParser<String>() {
@Override
public String parse(String value) {
return value;
}
});
}
}
Removed the static StringToTypeParser.parse(...) method for simplicity.
package com.github.drapostolos.typeparser;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
public final class StringToTypeParser {
private static final Object STATIC_METHOD = null;
private final Map<Class<?>, TypeParser<?>> typeParsers;
public static StringToTypeParserBuilder newBuilder() {
return new StringToTypeParserBuilder();
}
StringToTypeParser(Map<Class<?>, TypeParser<?>> typeParsers) {
this.typeParsers = Collections.unmodifiableMap(new HashMap<Class<?>, TypeParser<?>>(typeParsers));
}
public <T> T parse(String value, Class<T> type) {
if (value == null) {
throw new NullPointerException(nullArgumentErrorMsg("value"));
}
if (type == null) {
throw new NullPointerException(nullArgumentErrorMsg("type"));
}
// convert "null" string to null type.
if (value.trim().equalsIgnoreCase("null")) {
if (type.isPrimitive()) {
String message = "'%s' primitive can not be set to null.";
throw new IllegalArgumentException(String.format(message, type.getName()));
}
return null;
}
Object result = null;
if (typeParsers.containsKey(type)) {
result = callTypeParser(value, type);
} else if ((result = callFactoryMethodIfExisting("valueOf", value, type)) != null) {
//
} else if ((result = callFactoryMethodIfExisting("of", value, type)) != null) {
//
} else {
String message = "There is no registered 'TypeParser' for that type, or that "
+ "type does not contain one of the following static factory methods: "
+ "'%s.valueOf(String)', or '%s.of(String)'.";
message = String.format(message, type.getSimpleName(), type.getSimpleName());
message = canNotParseErrorMsg(value, type, message);
throw new IllegalArgumentException(message);
}
/*
* This cast is correct, since all above checks ensures we're casting to
* the right type.
*/
@SuppressWarnings("unchecked")
T temp = (T) result;
return temp;
}
/**
* This method is static because it is also called from {@link StringToTypeParserBuilder}.
*/
static String nullArgumentErrorMsg(String argName) {
return String.format("Argument named '%s' is illegally set to null!", argName);
}
private Object callTypeParser(String value, Class<?> type) {
try {
return typeParsers.get(type).parse(value);
} catch (NumberFormatException e) {
String message = canNotParseErrorMsg(value, type, numberFormatErrorMsg(e));
throw new IllegalArgumentException(message, e);
} catch (RuntimeException e) {
throw new IllegalArgumentException(canNotParseErrorMsg(value, type, e.getMessage()), e);
}
}
private String numberFormatErrorMsg(NumberFormatException e) {
return String.format("Number format exception %s.", e.getMessage());
}
private String canNotParseErrorMsg(String value, Class<?> type, String message) {
return String.format("Can not parse \"%s\" to type '%s' due to: %s", value, type.getName(), message);
}
private Object callFactoryMethodIfExisting(String methodName, String value, Class<?> type) {
Method m;
try {
m = type.getDeclaredMethod(methodName, String.class);
m.setAccessible(true);
if (!Modifier.isStatic(m.getModifiers())) {
// Static factory method does not exists, return null
return null;
}
} catch (Exception e) {
// Static factory method does not exists, return null
return null;
}
try {
if(type.isEnum()){
value = value.trim();
}
return m.invoke(STATIC_METHOD, value);
} catch (InvocationTargetException e) {
// filter out the InvocationTargetException stacktrace/message.
throw new IllegalArgumentException(makeErrorMsg(methodName, value, type), e.getCause());
} catch (Throwable t) {
throw new IllegalArgumentException(makeErrorMsg(methodName, value, type), t);
}
}
private String makeErrorMsg(String methodName, String value, Class<?> type) {
String methodSignature = String.format("%s.%s('%s')", type.getName(), methodName, value);
String message = " Exception thrown in static factory method '%s'. See underlying "
+ "exception for additional information.";
return canNotParseErrorMsg(value, type, String.format(message, methodSignature));
}
}
Removed registerTypeParser(TypeParser) method, which allowed for removing reflection code.
package com.github.drapostolos.typeparser;
import java.util.Map;
public final class StringToTypeParserBuilder {
private Map<Class<?>, TypeParser<?>> typeParsers;
StringToTypeParserBuilder() {
// Initialize with the default typeParsers
typeParsers = DefaultTypeParsers.copy();
}
public StringToTypeParserBuilder unregisterTypeParser(Class<?> type){
if(type == null) {
throw new NullPointerException(StringToTypeParser.nullArgumentErrorMsg("type"));
}
typeParsers.remove(type);
return this;
}
public <T> StringToTypeParserBuilder registerTypeParser(Class<T> type, TypeParser<? extends T> typeParser){
if(typeParser == null) {
throw new NullPointerException(StringToTypeParser.nullArgumentErrorMsg("typeParser"));
}
if(type == null) {
throw new NullPointerException(StringToTypeParser.nullArgumentErrorMsg("type"));
}
typeParsers.put(type, typeParser);
return this;
}
public StringToTypeParser build(){
return new StringToTypeParser(typeParsers);
}
}
"1,2,3,4"
to e.g.int[]
but notList<Integer>
because of type erasure in java. \$\endgroup\$