There are multiple ways to do this. For example:
You could have an interface called Parser
package example;
public interface Parser {
boolean canParse(String fullQualifiedClassName);
Object parse(String fullQualifiedClassName, String value) throws ParseException;
class ParseException extends Exception {
public ParseException(String msg) {
super(msg);
}
public ParseException(Exception cause) {
super(cause);
}
}
}
And all your Default-Implementations in an Enum or statically defined in another way:
package example;
public enum DefaultParser implements Parser {
STRING {
@Override
public boolean canParse(String fullQualifiedClassName) {
return isClassAssignableFromClassName(fullQualifiedClassName, String.class);
}
@Override
public Object parse(String fullQualifiedClassName, String value) throws ParseException {
return value;
}
},
ENUM {
@Override
public boolean canParse(String fullQualifiedClassName) {
return isClassAssignableFromClassName(fullQualifiedClassName, Enum.class);
}
@Override
public Object parse(String fullQualifiedClassName, String value) throws ParseException {
final Class<? extends Enum> clazz;
try {
clazz = (Class<? extends Enum>) Class.forName(fullQualifiedClassName);
} catch (ClassNotFoundException e) {
throw new ParseException(e);
}
return Enum.valueOf(clazz, value);
}
},
BOOLEAN {
@Override
public boolean canParse(String fullQualifiedClassName) {
return isClassAssignableFromClassName(fullQualifiedClassName, Boolean.class);
}
@Override
public Object parse(String fullQualifiedClassName, String value) throws ParseException {
return value.toLowerCase().equals("true");
}
};
private static boolean isClassAssignableFromClassName(String fullQualifiedClassName, Class<?> clazz) {
try {
return clazz.isAssignableFrom(Class.forName(fullQualifiedClassName));
} catch (ClassNotFoundException e) {
return false;
}
}
}
And a ParentParser Implementation that combines multiple Parsers into one:
package example;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class ParentParser implements Parser {
private final List<Parser> parsers;
public ParentParser() {
this.parsers = new ArrayList<>();
this.parsers.addAll(Arrays.asList(DefaultParser.values()));
}
public void register(Parser parser) {
this.parsers.add(parser);
}
@Override
public boolean canParse(String fullQualifiedClassName) {
return findParser(fullQualifiedClassName).isPresent();
}
@Override
public Object parse(String fullQualifiedClassName, String value) throws ParseException {
return findParser(fullQualifiedClassName)
.orElseThrow(() -> new ParseException("no registered parser found for class=" + fullQualifiedClassName))
.parse(fullQualifiedClassName, value);
}
private Optional<Parser> findParser(String fullQualifiedClassName) {
return this.parsers.stream().filter(parser -> parser.canParse(fullQualifiedClassName)).findAny();
}
}
Which you can then use like this:
package example;
import example.Parser.ParseException;
public class Example {
public static void main(String[] args) throws ParseException {
final ParentParser parser = new ParentParser();
System.out.println(parser.parse("java.lang.String", "hello world"));
System.out.println(parser.parse("java.lang.Boolean", "true"));
System.out.println(parser.parse("java.time.DayOfWeek", "TUESDAY"));
}
}
And you could add more parsers, for example a parser using Jackson (JSON):
package example;
import com.fasterxml.jackson.databind.ObjectMapper;
import example.Parser.ParseException;
import java.io.IOException;
public class Example {
public static void main(String[] args) throws ParseException {
final ParentParser parser = new ParentParser();
System.out.println(parser.parse("java.lang.String", "hello world"));
System.out.println(parser.parse("java.lang.Boolean", "true"));
System.out.println(parser.parse("java.time.DayOfWeek", "TUESDAY"));
parser.register(new JacksonParser());
System.out.println(parser.parse("java.util.Map", "{\"key\":\"value\"}"));
}
private static class JacksonParser implements Parser {
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public boolean canParse(String fullQualifiedClassName) {
final Class<?> clazz;
try {
clazz = Class.forName(fullQualifiedClassName);
} catch (ClassNotFoundException e) {
return false;
}
return MAPPER.canDeserialize(MAPPER.constructType(clazz));
}
@Override
public Object parse(String fullQualifiedClassName, String value) throws ParseException {
try {
return MAPPER.readValue(value, Class.forName(fullQualifiedClassName));
} catch (ClassNotFoundException | IOException e) {
throw new ParseException(e);
}
}
}
}
Note that this can of course be optimized depending on your needs.
If your Parser-Implementations can only parse a static List of Types and there is only one Parser-Implementation per Class, you should change the List<Parser> to Map<Class<?>, Parser> and change the register-Method to register(Class<?> clazz, Parser parser) for example
#valueOf(String)method. You could try invoking that method via reflection. Though they aren't all semantically the same; for instance,Integer#valueOf(String)will throw an exception if the string cannot be parsed into an integer, butBoolean#valueOf(String)will returnfalseif the string is not "true" (ignoring case).else-ifblock in the future. Once I know the type, this is a simple problem. Finding the appropriately assignable type is the issue.valueOfmethod, just doClass.forName(...).getMethod("valueOf", String.class).invoke(null, arg). This will of course fail if the class does not have a public, static#valueOf(String)method.valueOffor now because 1) this breaks when I want to serialize custom classes, and 2) Boolean.valueOf() is laughably bad.ServiceLoaderso you don't have to manually register parser instances.