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);
}
}