Skip to content
Alexander Poulikakos edited this page Dec 30, 2022 · 11 revisions

drapostolos codecov

Type Parser Library

This library parses a simple string (as read form a properties file, environment variable, system property etc) into a java type. The type can be either generic (e.g. Collection<E>, Map<K,V>, Optional<E> etc), non-generic (e.g. File, Integer, Enum, int[] etc) or your own custom made classes.

Typically a type-parser is used together with reflection to find out the java type in runtime with one of these methods:

  • java.lang.reflect.Method.getGenericParameterTypes()
  • java.lang.reflect.Method.getGenericReturnType()
  • java.lang.reflect.Field.getGenericType()

and then convert the simple string to that type.

Supported types and primitives

  • java.lang.Byte (byte)
  • java.lang.Integer (int)
  • java.lang.Long (long)
  • java.lang.Short (short)
  • java.lang.Float (float)
  • java.lang.Double (double)
  • java.lang.Boolean (boolean)
  • java.lang.Character (char)
  • java.math.BigInteger
  • java.math.BigDecimal
  • java.net.URL
  • java.net.URI
  • java.io.File
  • java.nio.file.Path
  • java.lang.String (obviously)
  • java.lang.Class<?>
  • java.lang.Enum<?> (Actually any Enum type)
  • java.lang.Number
  • Any class T that contains a single argument static factory method that returns an instance of it self, where the argument is any supported type. Example: T.anyName(Long) : T
  • Any class T that contains a single argument constructor where its argument is of any supported type.
  • Any type resolved by a java.beans.PropertyEditor registered in java.beans.PropertyEditorManager (Note this feature is disabled by default. )

Supported Array and Generic types:

(The type arguments E, K and V below are any of the supported types above)

  • Any array class of the above types. Example File[], int[] etc.
  • Collection subinterfaces:
    • Collection<E>, List<E>, Set<E>, SortedSet<E>, NavigableSet<E>, BlockingDeque<E>, BlockingQueue<E>, Deque<E>, Queue<E>.
  • Collection classes:
    • ArrayList<E>, HashSet<E>, LinkedHashSet<E>, LinkedList<E>, , TreeSet<E>, EnumSet<E>, ConcurrentLinkedQueue<E>, ConcurrentSkipListSet<E>, CopyOnWriteArrayList<E>, CopyOnWriteArraySet<E>, LinkedBlockingDeque<E>, LinkedBlockingQueue<E>, PriorityBlockingQueue<E>, ArrayDeque<E>, PriorityQueue<E>, Stack<E>, Vector<E>.
  • Map subinterfaces:
    • Map<K,V>, SortedMap<K,V>, ConcurrentMap<K,V>, ConcurrentNavigableMap<K,V>, NavigableMap<K,V>,
  • Map classes:
    • HashMap<K,V>, LinkedHashMap<K,V>, TreeMap<K,V>, ConcurrentHashMap<K,V>, ConcurrentSkipListMap<K,V>, WeakHashMap<K,V>, IdentityHashMap<K,V>, Hashtable<K,V>.
  • java.util.Optional<E>

Quick Guide

    public static void main(String[] args) throws Throwable {
        // Create a TypeParser instance with default settings.
        TypeParser parser = TypeParser.newBuilder().build();

        // in the most simple form, convert a string to Integer type,
        // when type known at compile time.
        Integer i = parser.parse("1", Integer.class);

        // The string "null" will by default be parsed to a null Object
        Integer i = parser.parse("null", Integer.class); // returns a null object.

        // Split a comma separated string to a Set of Integers, 
        // when type is known at compile time. 
        Set<Integer> setOfInts = parser.parse("1,2,3,4", new GenericType<Set<Integer>>() {});
        // or an empty Set<Integer> when input string is "null".
        Set<Integer> i = parser.parse("null", new GenericType<Set<Integer>>() {}); 

        // Split a comma separated string to a List of Booleans.
        List<Boolean> listOfBooleans = parser.parse("true, false", new GenericType<List<Boolean>>() {});

        // Split a comma separated string to an array of float. 
        float[] arrayOfFloat = parser.parse("1.3, .4, 3.56", float[].class);

        // Split a comma separated string to a Map of String and File. 
        Map<String, File> mapOfFiles = parser.parse("f1=C:\\temp\\f1.txt,f2=C:\\temp\\f2.txt",
                new GenericType<Map<String, File>>() {});

        // Parse a string to an Optional<?>
        Optional<Integer> optionalInteger = parser.parse("1", new GenericType<Optional<Integer>>() {});
        Optional<Integer> emptyOptional = parser.parse("null", new GenericType<Optional<Integer>>() {});
        Optional<List<Integer>> optionalList = parser.parse("1,2,3", new GenericType<Optional<List<Integer>>>() {});
        Optional<List<Integer>> emptyOptionalList = parser.parse("null", new GenericType<Optional<List<Integer>>>() {});

        // Find the parameter type in runtime with reflection and convert the input 
        // string to that type.
        Method m = ...; // get Method object for method "m" (see method below)
        Object parameter = parser.parseType("1,2,3,4", m.getGenericParameterTypes()[0]);
        //  Invoke method with parameter converted to a List<Integer>.
        m.invoke(null, parameter);
    }

    static void m(List<Integer> ints) {
        // do something with a list of integers
    }

API-DOCS

Latest javadoc is [here] (http://www.labelscans.com/javadoc/type-parser/latest/apidocs/).

You got Questions/Feedback/Requests/contributions, or just need help?

Please create a "new issue" in the issue tracker here at github, for any communication.

Maven artifact

Available in the Sonatype Central Repository

Maven

<dependency>
  <groupId>com.github.drapostolos</groupId>
  <artifactId>type-parser</artifactId>
  <version>0.8.1</version>
</dependency>

Gradle

implementation 'com.github.drapostolos:type-parser:0.8.1'

Changes

(See also Releases)

Version v0.8.1 (release)

  • New features, enhancements:
    • Add support for java.util.Optional<E> #40.

Version v0.7.0 (release)

  • New features, enhancements:
    • Add support for java.nio.file.Path.
    • Code is now built with Java 8.
    • Internal refactorings/code improvements using Java8 features.

Version v0.6.0 (release)

  • New features, enhancements:
    • add single argument static factory method support, regardless of method name. #29
    • Added optional support for PropertyEditor #30
    • Internal refactorings/code improvements. #30
      • This change contains a potential (minor) breaking change. Internally, client registered DynamicParser's are now executed after type lookup for regular Parser's. In other words, if a Parser matching a given type exists it will be used and no DynamicParser's will be called.

Version v0.5.0 (release)

  • New features, enhancements:
    • Add support for java.lang.Number type. #24.
    • internal optimizations/refactorings
  • Bugfixes:
    • Fixed issue #25 (Not possible to create type with valueOf(Enum) factory method)
    • Fixed issue #26 (Not possible to create type with constructor taking Enum argument)

Version v0.4.0 (release)

  • New features, enhancements:
    • Introduced NullStringStrategy interface according to #21.
    • [Breaking Change] Changed contract for InputPreprocessor interface, in favor of NullStringStrategy above.
      (the actual contract change is: prepare(...) method does not support returning null objects any more).
  • Bugfixes:
    • None.

Version v0.3.0 (release)

  • New features, enhancements:
    • Introduced DynamicParser interface, which gives clients a way to decide in runtime if a String can be parsed to any given type.
    • [Breaking Change] Removed method TypeParserBuilder.registerParserForTypesAssignableTo(Class<?>, Parser<?>) in favor of DynamicParser interface above.
    • Introduced new TypeParserException which is now thrown when parsing process fails.
    • [Breaking Change] renamed NoSuchRegisteredTypeParserException to NoSuchRegisteredParserException
    • Introduced new methods available in all *Helper classes
      • Type getTargetType()
      • List<Class<T>> getParameterizedClassArguments()
      • Class<T> getParameterizedClassArgumentByIndex(int)
      • Class<T> getTargetClass()
      • Class<T> getRawTargetClass()
      • boolean isTargetTypeParameterized()
      • boolean isTargetTypeAssignableTo(Class<?>)
      • boolean isTargetTypeEqualTo(Class<?>)
      • boolean isTargetTypeEqualTo(GenericType<?>)
      • boolean isRawTargetClassAnyOf(Class<?>...)
    • Added support for these Map subinterfaces:
      • ConcurrentMap, ConcurrentNavigableMap, NavigableMap, SortedMap
    • Added support for these Map classes:
      • ConcurrentHashMap, ConcurrentSkipListMap, Hashtable, IdentityHashMap, HashMap, TreeMap, WeakHashMap
    • Added support for these Collection subinterfaces:
      • BlockingQueue, BlockingDeque, Deque, Queue, NavigableSet, SortedSet,
    • Added support for these Collection classes:
      • ConcurrentLinkedQueue, ConcurrentSkipListSet, CopyOnWriteArrayList, CopyOnWriteArraySet, LinkedBlockingDeque, LinkedBlockingQueue, PriorityBlockingQueue, ArrayDeque, HashSet, LinkedList, PriorityQueue, Stack, TreeSet, Vector
    • Added support for custom made classes with a static valueOf(...) method, where the argument can be any supported type, not just String.
    • Added support for custom made classes with a private static valueOf(...) method.
    • Added support for custom made classes with a single argument constructor where the argument can be of any supported type.
    • Added support for custom made classes with a private single argument constructor.
  • Bugfixes:
    • None.

Version v0.2.0 (release)

  • New features, enhancements:
    • [Breaking Change] Renamed these classes/interface/methods according to issue #10
      • interface TypeParser -> interface Parser
      • class StringToTypeParser -> class TypeParser
      • StringToTypeParserBuilder -> TypeParserBuilder
      • StringToTypeParserBuilder.registerTypeParser(...) -> registerParser(...)
      • StringToTypeParserBuilder.unregisterTypeParser(...) -> unregisterParser(...)
    • Possible to register Parser for all sub-classes of a super-class according to issue #7
      • see new method: TypeParserBuilder.registerParserForTypesAssignableTo(Class<?>, Parser<?>)
    • Added support for these java types
      • java.net.URL
      • java.net.URI
      • java.math.BigInteger
    • Re-factored unit tests
    • Added these new methods according to issue #11
      • ParserHelper.getTargetClass() : Class<T>
      • ParserHelper.getParameterizedClassArguments() : List<Class<T>>
      • ParserHelper.getParameterizedClassArgumentByIndex(int index) : Class<T>
  • Bugfixes:
    • It is not possible to parse string to Class<Long> #5

Version v0.1.0 (release)

  • New features, enhancements:
    • Initial first release
  • Bugfixes:
    • None

Usage Examples

Construct a TypeParser instance

This is the way to construct a TypeParser with default settings:

TypeParser parser = TypeParser.newBuilder().build();

The TypeParser has a few optional configuration options that can be set as follow:

    public static void main(String[] args) {

        TypeParser parser = TypeParser.newBuilder()
                .setInputPreprocessor(InputPreprocessor)
                .registerParser(Class<?>, Parser)
                .registerParser(GenericType, Parser)
                .registerDynamicParser(DynamicParser)
                .setKeyValueSplitStrategy(SplitStrategy)
                .setNullStringStrategy(NullStringStrategy)
                .setSplitStrategy(SplitStrategy)
                .unregisterParser(Class<?>)
                .enablePropertyEditor()
                .build();
    }

For additional configuration possibilities have a look at the [javadoc] (http://www.labelscans.com/javadoc/type-parser/latest/apidocs/index.html?com/github/drapostolos/typeparser/TypeParserBuilder.html) for the TypeParserBuilder class.

Parse a string to a type known at compile time

When target type is known at compile time, use either one of these two methods:
TypeParser.parse(String, Class<T>)
TypeParser.parse(String, GenericType<T>)
as follow:

    public enum SomeEnum {THIS, OR, THAT}
    public static void main(String[] args) throws Throwable {
        // Create a TypeParser instance with default settings.
        TypeParser parser = TypeParser.newBuilder().build();

        // Parse input strings to various types known at compile time
        Integer i = parser.parse("1", Integer.class);
        File f = parser.parse("C:\\some\\path", File.class);
        BigDecimal bigDecimal = parser.parse("33", BigDecimal.class);
        boolean b = parser.parse("true", boolean.class);
        Class<?> longClass1 = parser.parse("java.lang.Long", Class.class);
        SomeEnum someEnum = parser.parse("THAT", SomeEnum.class);

        // NOTE parsing the string "null" will result in a null object for non-collection types
        Integer i = parser.parse("null", Integer.class); // returns null object.

    }

Note! When target type is of generic-type, java does not provide a good way to extract the generic type information. However, one way (the only?) of doing it is with sub-classing. You need to subclass GenericType<T> and provide the generic type arguments as follow:

        Class<?> longClass = parser.parse("java.lang.Long", new GenericType<Class<?>>() {});
        Class<Long> longCls = parser.parse("java.lang.Long", new GenericType<Class<Long>>() {});
        List<Integer> integers = parser.parse("1,2,3,4,5", new GenericType<List<Integer>>() {});
        Set<File> files = parser.parse("C:\\some\\path1, C:\\some\\path2", new GenericType<Set<File>>() {});

Note the ending {} since we are creating an anonymous subclass of GenericType<T> in above example. Note also in above examples when parsing a string to one of the java.util.Collection<E> types (or java.util.Map<K,V>) the input string is by default split with a comma , (the default behavior can be changed with a SplitStrategy as described further below).

Parse a string to a type known only in runtime

The typically usage of this library is to read a string from some source (.properties file, JVM arguments, xml file etc.), find out the java type during runtime (using reflection), and then convert the string to that java type using the following method:

TypeParser.parseType(String, java.lang.reflect.Type) 

The below example creates an instance of a class and calls the two methods on it, but before calling the two method the parameters passed to the methods are fetched from a .properties file and converted to expected type.

Consider the following class containing two methods, which takes two arguments respectively,

import java.io.File;
import java.util.List;

public class ClassWithMethods {
    public enum SomeEnum {THIS, OR, THAT}

    public String m1(List<Integer> intList, File file) throws Exception {
        return String.format("m1(%s, %s),", intList, file);
    }

    public String m2(SomeEnum someEnum, Class<?> type) {
        return String.format("m2(%s, %s),", someEnum, type);
    }
}

where the following args.properties file is located in the root of the classpath.

m1.arg0 = 1,2,3,4 
m1.arg1 = C:\\temp 
m2.arg0 = THAT 
m2.arg1 = java.lang.Long

This code creates an instance of the ClassWithMethods and call each method with the parameters found in the args.properties file. The parameters are converted to the types according to the method signature.

public class ReflectionDemo {
    private static TypeParser parser = TypeParser.newBuilder().build();

    public static void main(String[] args) throws Throwable {
        // load a properties file named "args.properties" in root of classpath.
        Properties props = new Properties();
        props.load(ClassLoader.getSystemResourceAsStream("args.properties"));

        // Create the instance to call methods on
        ClassWithMethods o = new ClassWithMethods();

        // Call each method with parameters from args.properties 
        for (Method m : o.getClass().getDeclaredMethods()) {
            int i = 0;
            List<Object> params = new ArrayList<Object>();
            for (Type t : m.getGenericParameterTypes()) {
                String key = m.getName() + ".arg" + i;
                String value = props.getProperty(key);
                params.add(parser.parseType(value, t));
                i++;
            }
            Object result = m.invoke(o, params.toArray());
            System.out.println("invoke: " + result);
        }
    }
}

Console output is

invoke: m2(THAT, class java.lang.Long),
invoke: m1([1, 2, 3, 4], C:\temp),

Preprocess the input string

A TypeParser can be configured to pre process the input string in a customized way, by implementing this interface

public interface InputPreprocessor {
    String prepare(String input, InputPreprocessorHelper helper);
}

The prepare(String, InputPreprocessorHelper) method accepts the input string and returns a modified (pre processed) version of that string. NOTE that returning a null object from the prepare(...) method is not supported.

See InputPreprocessor javadoc for its full contract.

In below example we strip off any trailing curly brackets appearing in the input string.

    public class MyInputPreprocessor implements InputPreprocessor {
        @Override
        public String prepare(String input, InputPreprocessorHelper helper) {
            // replaces first '{' and last '}' with "" (empty string)
            return = input.replaceAll("^\\{", "").replaceAll("\\}$", "");
        }
    }
    public static void main(String[] args) {
        TypeParser parser = TypeParser.newBuilder()
                .setInputPreprocessor(new MyInputPreprocessor())
                .build();

        List<String> intList = parser.parse("{1,2,3}", new GenericType<List<String>>() {});
        System.out.println("intList: " + intList);
    }

Console output is

intList: [1, 2, 3]

Have a look at the javadoc for InputPreprocessorHelper for helper functionality useful when pre processing the input string.

Register your own Parser

If you want to parse a string to a specific type not supported by the TypeParser (Can be a type from another library you don't have control over, or it could be your own custom class etc.), then you can register a Parser implementation for that type, by implementing this interface:

public interface Parser<T> {
    T parse(String input, ParserHelper helper);
}

and registering it like this:

        TypeParser parser = TypeParser.newBuilder()
                .registerParser(ThatType.class, new Parser<ThatType>() {
                    @Override
                    public ThatType parse(String input, ParserHelper helper) {
                        // Construct an instance of `ThatType` based on input String.
                        return ThatType.getInstance(input);
                    }
                })
                .build();

        parser.parse("some string to", ThatType.class);

Alternatives to creating a Parser

There is an alternative way of parsing strings to a type (than creating your own Parserfor it). Note! This requires you are in control of the type (i.e you can modify it). Do one of the following:

  • add a static factory method named valueOf(String)
  • add a single argument constructor taking a string.

Note1! The valueOf(String) method and the single argument constructor can be either public or private.
Note2! The argument doesn't necessary needs to be a String, it can be of any type as long the TypeParser knows how to parse it.
Note3! If a class has both a valueOf(...)' method and a single argument constructor, the valueOf(...)` method will be called.

    public static class TypeA {
        private static TypeA valueOf(String input) {
            // ignoring the input for the sake of this example.
            return new TypeA();
        }
    }

    public static class TypeB {
        private final Long value;
        // automatically converts to Long
        public TypeB(Long l) {
            value = l;
        }
    }

    public static void main(String[] args) {
        TypeParser parser = TypeParser.newBuilder().build();

        parser.parse("some string", TypeA.class);
        parser.parse("34", TypeB.class);
    }

Register your own DynamicParser

Unlike a Parser implementation (which is statically associated to a specific type), a DynamicParser implementation is not tied to a specific type. A DynamicParser implementation decides in runtime whether it can parse a string to a given type or not. The DynamicParser.parse(..., ...) method should either return the expected object, or the constant DynamicParser.TRY_NEXT. Multiple DynamicParser can be registered and the TypeParser will call them one by one (in the registered order, i.e. first one registered is the first one called) until the first one found not returning DynamicParser.TRY_NEXT.

Here is the interface:

public interface DynamicParser extends Parser<Object> {
    public static final Object TRY_NEXT = new Object();
    Object parse(String input, ParserHelper helper);
}

see javadoc for its complete contract.

...example coming...

Set your own split strategy

User guide is Comming...