1

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
    }
5
  • 4
    All the example input types have a static #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, but Boolean#valueOf(String) will return false if the string is not "true" (ignoring case). Commented Apr 7, 2020 at 16:27
  • @Slaw, I am aware of the static methods, and those Utils do something similar. What I am asking for is a better way to route to the specific parser without having to add an else-if block in the future. Once I know the type, this is a simple problem. Finding the appropriately assignable type is the issue. Commented Apr 7, 2020 at 16:33
  • 1
    If you want to use the valueOf method, just do Class.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. Commented Apr 7, 2020 at 16:36
  • Fair. Thanks for the thought. I want to avoid valueOf for now because 1) this breaks when I want to serialize custom classes, and 2) Boolean.valueOf() is laughably bad. Commented Apr 7, 2020 at 16:42
  • 1
    Understandable. The other option I was going to suggest is an interface, but I see codeflush.dev has already given an answer. Only thing I'd add to that is, if you want, you can make use of ServiceLoader so you don't have to manually register parser instances. Commented Apr 7, 2020 at 17:59

2 Answers 2

1

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

Sign up to request clarification or add additional context in comments.

Comments

0

You can write a generic solution using reflection apis in java. That would reduce a lot amount of code and would be more extensible. Also not there is a separate processing required for enum types. I have covered the basic cases in the code shown below.

public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Object instance1 = parse("java.lang.String", "abc", false);
        Object instance2 = parse("java.lang.Boolean", "FALSE", false);
        Object instance3 = parse("java.lang.Integer", "123", false);
        Object instance4 = parse("com.me.Color", "RED", true);
    }

    private static Object parse(String className, String argument, boolean isEnum) throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException {
        if (isEnum) {
            Object value = Enum.valueOf((Class<? extends Enum>) Class.forName(className), argument);
            //System.out.println(value);
            return value;
        } else {
            return parse(className, new Object[]{argument}, isEnum);
        }
    }

    private static Object parse(String className, Object[] arguments, boolean isEnum) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<?> clazz = Class.forName(className);
        Constructor<?> ctor = clazz.getConstructor(String.class);
        Object object = ctor.newInstance(arguments);
        //System.out.println(object);
        return object;
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.