I'm an experienced Java developer, however I don't have experience with reflection or Annotation classes. For fun, I tried to develop a CSV Reader class which can parse each row into a particular type.
Here is my code:
package com.richardrobinson;
import java.io.BufferedReader;
import java.lang.annotation.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static java.util.Map.*;
/**
* This class allows CSV text files to conveniently be parsed into a stream objects of the specified type.
* <p>
* By default, CSVReader supports {@code Integer, Double, Character, String,} and {@code Boolean} types. Other types may be added via {@link CSVReader#registerParser(Class, Function)}
* <p>
* For example, given a class {@code Foo}:
* <pre>{@code
* class Foo {
* final Integer i;
* final String s;
*
* @CSVConstructor public Foo(Integer i, String s) {
* this.i = i;
* this.s = s;
* }
* }
* }</pre>
*
* and a {@link BufferedReader} {@code reader} whose contents are
* <pre>
* num,str
* 1;hello
* 2;world
* </pre>
*
* then the reader may be parsed via
* <pre>
* var csv = CSVReader.of(reader, Foo.class)
* .ignoringHeader()
* .withDelimiter(";")
* </pre>
*
* @param <T> the type of the objects. The class of {@code T} must have a constructor which satisfies the following properties:
* <ul>
* <li>It is annotated with {@link CSVConstructor}</li>
* <li>The number of parameters is no more than the number of fields per CSV line</li>
* <li>The types of the parameters must be a supported type.</li>
* </ul>
*
* @author Richard I. Robinson
*/
public class CSVReader<T> {
/**
* An annotation which may be applied to a constructor to indicate that such constructor should be used when being instantiated via {@link CSVReader}
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR})
public @interface CSVConstructor {}
private final BufferedReader reader;
private final Class<T> clazz;
private String delimiter = ",";
private boolean ignoreHeader = false;
private static final Map<Class<?>, Function<String, ?>> PARSERS = new HashMap<>(ofEntries(
entry(Integer.class, Integer::parseInt),
entry(Double.class, Double::parseDouble),
entry(Character.class, s -> s.charAt(0)),
entry(String.class, s -> s),
entry(Boolean.class, Boolean::parseBoolean)
));
/**
* Enables support for a type {@code T} for CSVReader instances in addition to the types supported by default
*
* @param cls the Class to add support for (for example, {@code Foo.class})
* @param parser a Function mapping a {@link String} to a {@code T}
* @param <T> the type corresponding to {@code cls}
*/
public static <T> void registerParser(Class<T> cls, Function<String, T> parser) {
PARSERS.put(cls, parser);
}
private CSVReader(BufferedReader reader, Class<T> clazz) {
this.reader = reader;
this.clazz = clazz;
}
/**
* Creates a new CSVReader instance from the specified {@code reader}, whose lines may be parsed into instances of type {@code clazz}. By default, the delimiter used is {@code ","}, and it is assumed there is no header line. These options may be configured via their respective builder methods.
*
* @param reader a {@link BufferedReader} containing {@code n} lines of text, with each line containing {@code m} fields separated by a delimiter.
* @param clazz the class of the type of object that each row is parsed into. For example, {@code Foo.class}
* @param <T> the type corresponding to {@code clazz}
* @return a new CSVReader instance, which may be further configured with the builder options
* @see #withDelimiter(String)
* @see #ignoringHeader()
*/
public static <T> CSVReader<T> of(BufferedReader reader, Class<T> clazz) {
return new CSVReader<>(reader, clazz);
}
/**
* Sets a custom delimiter to be used
* @param delimiter the delimiter to use to separate fields of each row
* @return {@code this} CSVReader with the specified delimiter
*/
public CSVReader<T> withDelimiter(String delimiter) {
this.delimiter = delimiter;
return this;
}
/**
* If a header line is present, this method should be invoked so that this CSVReader ignores the first line
* @return {@code this} CSVReader with the header line ignored
*/
public CSVReader<T> ignoringHeader() {
this.ignoreHeader = true;
return this;
}
/**
* Maps each line of the reader to a parsed instance of type {@code T}. The number of fields per line must be no less than the number of fields of class {@code T}.
* @return a Stream of instances of type {@code T} corresponding to each line
*/
public Stream<T> rows() {
return reader.lines().skip(ignoreHeader ? 1 : 0).map(this::parseRow);
}
@SuppressWarnings("unchecked")
private T parseRow(String row) {
final var split = row.split(delimiter);
final var annotatedCtor = Arrays.stream(clazz.getConstructors())
.filter(ctor -> ctor.isAnnotationPresent(CSVConstructor.class))
.findFirst()
.orElseThrow();
final var ctorParams = annotatedCtor.getParameterTypes();
final var args = IntStream.range(0, ctorParams.length)
.mapToObj(i -> PARSERS.get(ctorParams[i]).apply(split[i]))
.toArray();
try {
return (T) annotatedCtor.newInstance(args);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
It works perfectly as designed, however I was wondering if there are any best practices with regards to reflection and annotations which I am not using and should be using, or if there's edge case problems in my code. I'm also super open to advice on the class design overall! Thanks!
For an example of the class usage, check out the JavaDoc comment above the class declaration.