Cover image for CSV Writer with Java - Reflection API

CSV Writer with Java - Reflection API

Balaji Ramasamy's profile pic
Balaji Ramasamy Full Stack Developer

July 14, 2020

Context

One might wonder why we would need to create CSV parser when there are great libraries such as apache-poi available. But in my current project, the client did not want to rely on third party libraries, as it may cause a lot of dependency and security related issue. Also the feature is fairly simple that we can implement on our own.

Goal

The goal is to create a CSV converter, which will take a list of object and convert them into comma separated values which can be written to file and served.

And also it needs be done so we should be able to reuse the utility and easily extensible.

Writing to columns from nested objects is also an important feature.

Methodology

We will use Java Reflection API to dynamically invoke the getters of the objects. It may sound a little confusing but you will understand in a bit. Let's say we have a class called Foo. It has private property such as title and description , which means it will also have getters such as getTitle and getDescription which we can use to access the properties.

Foo foo = new Foo("title", "description");
String csv = foo.getTitle() + "," + foo.getDescription()
// yields
// title,description

The above code will give us a comma separated value which can be written to a file and served. But it is not reusable, if we have another class called Box we will have to write a separate function.

If we look closely, we can see that whatever the value may be we just have to concatenate them and return. For that we will need to access the values dynamically.

Java Reflection API

Java Reflection API, provides classes and methods which will our job easier. For example, Method is one of the class it provides which represents the method objects in all the classes

Foo foo = new Foo("title", "description");

Method getTitleMethod = foo.getClass().getMethod("getTitle");
// will return the getTitle metho
// now we can invoke it like
Object result = getTitleMethod.invoke(result);
// returns String object with value "title"

Model Classes

We will create a model class which will represent a data class. For this tutorial we will have an Employee model with common details and company details. The @Getter and @Setter annotations comes from lombok dependency.

@Getter
@Setter
public class Employee {
    private String name;
    private Integer age;
    private String city;
    private Company currentCompany;
}
@Getter
@Setter
public class Company {
    private String name;
}

CSV Column

Since we need this to be reusable utility, we have extract some of the configurations such as Column name, getter method names, file they want it to be saved.

First we need class which will represent our Column , it will contain basic information such as the property name and column display name. And From the property names will derive the method names.

@Getter
@Setter
public static class CSVColumn {

    private final String displayName; //   Company Name
    private final String propertyName; //  currentCompany.name
    private final String[] methodNames; // [getCurrentCompany, getName]

    public CSVColumn(String displayName, String propertyName) {
        this.displayName = displayName;
        this.propertyName = propertyName;
        String[] steps = propertyName.split("\\.");
        this.methodNames = new String[steps.length];
        for (int i = 0; i < steps.length; i++)
            this.methodNames[i] = createGetterName(steps[i]);
    }


    private String createGetterName(String name) {
        return "get" + name.substring(0, 1).toUpperCase() 
                + name.substring(1);
    }
}

CSV Configuration

Next thing we need to create is CSVConfig class. It will take take the different columns and other additional info for conversion.

public static class CSVConfig<T> {
    
    private final String fileName;
    private final String headerNames;
    private final CSVColumn[] csvColumns;
    private Class<T> tClass;

    public CSVConfig(String fileName, CSVColumn[] csvColumns) {
        this.fileName = fileName;
        this.csvColumns = csvColumns;
        StringBuilder stringBuilder = new StringBuilder();
        for (CSVColumn csvColumn : csvColumns)
            stringBuilder.append(csvColumn.getDisplayName())
                        .append(CSVConverter.DEFAULT_SEPARATOR);
        this.headerNames = stringBuilder.toString();
    }
}

Here we are just concatenating the header names to reuse since its not going to change.

Example CSVConfig Object
public static class Constants {
    public static final CSVConfig<Employee> CSV_CONFIG_EMPLOYEE = new CSVConfig<>(
            "Employee Details",
            new CSVColumn[]{
                    new CSVColumn("Emp Name", "name"),
                    new CSVColumn("Emp Age", "age"),
                    new CSVColumn("Emp City", "city"),
                    new CSVColumn("Emp Company Name", "currentCompany.name")
            }
    );
}

CSV Converter

Next thing we need to create is CSVConverter class. It will take take any object of CSVConfig object and a list of data class object then convert it to csv file.

public static class Constants CSVConverter<T> {
    
    public static final String DEFAULT_EXTENSION = ".csv";
    public static final String DEFAULT_SEPARATOR = ",";
    public static final String DEFAULT_EMPTY_VALUE = "";
    public static final String DEFAULT_NEW_LINE = "\n";
    public static final String DEFAULT_PATH_VARIABLE_SEPARATOR = "\\.";
    
    public File convert(Collection<T> data, CSVConfig<T> csvConfig) {
        File file = new File(csvConfig.getFileName() + CSVConverter.DEFAULT_EXTENSION);
        try (FileWriter fileWriter = new FileWriter(file)) {
            StringBuilder rows = new StringBuilder();
            rows.append(csvConfig.getHeaderNames());
            for (T object : data) {
                StringBuilder singleRow = new StringBuilder();
                for (CSVColumn csvColumn : csvConfig.getCsvColumns()) {
                    singleRow.append(this.get(csvColumn.getMethodNames(), object))
                            .append(CSVConverter.DEFAULT_SEPARATOR);
                }
                rows.append(CSVConverter.DEFAULT_NEW_LINE)
                     .append(singleRow.toString());
            }
            fileWriter.write(rows.toString());
        } catch (Exception e) {
            System.out.println(e.getMessage());
            throw new RuntimeException(e);
        }
        return file;
    }

    public Object get(String[] methodNames, T obj) throws Exception {
        Object result = obj;
        for (String methodName : methodNames) {
            if (result == null)
                return DEFAULT_EMPTY_VALUE;
            Method method = result.getClass().getMethod(methodName);
            result = method.invoke(result);
        }
        return result;
    }
}

Finally we can use this utility like,

public class Main {
    public static void main(String[] args) {
        List<Employee> employees = DataUtil.generateEmployees(4);
        CSVConverter<Employee> employeeCSVConverter = 
                        new CSVConverter<>(Constants.CSV_CONFIG_EMPLOYEE);
        File employeeFile = employeeCSVConverter.convert(employees);
    }
}

GitHub