Search code examples
javagenericsdesign-patternsfunctional-programmingfunctional-interface

Creating a parser of Class name + String value to a typed value


I am trying to write a method that can take in a String classname and a String value, and return the value represented as that String.

Example inputs:

parse("java.lang.String", "abc") -> String "ABC"
parse("java.lang.Boolean", "FALSE") -> Boolean FALSE
parse("java.lang.Integer", "123") -> Integer 123
parse("com.me.Color", "RED") -> enum Color.RED

I have found that if I use an if block containing assignableFrom calls, I can achieve this. But would prefer writing something more extendable, so it isn't as difficult to add a new parser tomorrow.

This is what I have now:

    String stringClassName = //stringified full class name
    String value = //value to parse
    Class<?> fieldType = Class.forName(stringClassName)
    if (fieldType.isAssignableFrom(String.class)) {
      return value;
    } else if (fieldType.isAssignableFrom(Boolean.class)) {
      return Util.toBoolean(value);
    } else if (fieldType.isEnum()) {
      return Util.toEnum(fieldType, value);
    } else {
      // throw exception
    }

Solution

  • 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