Writing generic POJO to CSV transformer
Solution 1
I have created a CSVUtil Class similar to below which uses java reflection.
Example to use below CSVUtil Assuming POJO Student ,
List<Student> StudentList = new ArrayList<Student>();
String StudentCSV = CSVUtil.toCSV(StudentList,' ',false);
import java.lang.reflect.Field;
import java.util.List;
import java.util.logging.Logger;
CSVUtil class
public class CSVUtil {
private static final Logger LOGGER = Logger.getLogger(CSVUtil.class .getName());
private final static char DEFAULT_SEPARATOR = ' ';
public static String toCSV(List<?> objectList, char separator, boolean displayHeader) {
StringBuilder result =new StringBuilder();
if (objectList.size() == 0) {
return result.toString();
}
if(displayHeader){
result.append(getHeaders(objectList.get(0),separator));
result.append("\n");
}
for (Object obj : objectList) {
result.append(addObjectRow(obj, separator)).append("\n");
}
return result.toString();
}
public static String getHeaders(Object obj,char separator) {
StringBuilder resultHeader = new StringBuilder();
boolean firstField = true;
Field fields[] = obj.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String value;
try {
value = field.getName();
if(firstField){
resultHeader.append(value);
firstField = false;
}
else{
resultHeader.append(separator).append(value);
}
field.setAccessible(false);
} catch (IllegalArgumentException e) {
LOGGER.severe(e.toString());
}
}
return resultHeader.toString();
}
public static String addObjectRow(Object obj, char separator) {
StringBuilder csvRow =new StringBuilder();
Field fields[] = obj.getClass().getDeclaredFields();
boolean firstField = true;
for (Field field : fields) {
field.setAccessible(true);
Object value;
try {
value = field.get(obj);
if(value == null)
value = "";
if(firstField){
csvRow.append(value);
firstField = false;
}
else{
csvRow.append(separator).append(value);
}
field.setAccessible(false);
} catch (IllegalArgumentException | IllegalAccessException e) {
LOGGER.severe(e.toString());
}
}
return csvRow.toString();
}
}
Solution 2
There is a simple option. I've added some lines to your code to show it :
public <T> List<String> convertToString(List<T> objectList) {
if(objectList.isEmpty())
return Collections.emptyList();
T entry = objectList.get(0);
List<String> stringList = new ArrayList<>();
char delimiter = ',';
char quote = '"';
String lineSep = "\n";
CsvMapper mapper = new CsvMapper();
CsvSchema schema = mapper.schemaFor(entry.getClass());
for (T object : objectList) {
try {
String csv = mapper.writer(schema
.withColumnSeparator(delimiter)
.withQuoteChar(quote)
.withLineSeparator(lineSep)).writeValueAsString(object);
stringList.add(csv);
} catch (JsonProcessingException e) {
System.out.println(e);
}
}
return stringList;
}
The trick is to get one of the elements of the list. In order to avoid crashs I've added a little data integrity test at the beginning that return an unmodifiable empty list in the case there are no items in the input list. Then you retrieve an instance of your Object and use that to get the class.
Alternatively if the convertToString method is in a parametrized class you can do that in a slightly different way
public class GenericClass<T> {
private final Class<T> type;
public GenericClass(Class<T> type) {
this.type = type;
}
public Class<T> getMyType() {
return this.type;
}
}
This solution allow you to get the class of T. I don't think you'll need it for this question but it might comes in handy.
dushyantashu
Updated on June 04, 2022Comments
-
dushyantashu almost 2 years
My use case was to write a generic CSV transformer, which should be able to convert any Java POJO to CSV string.
My Implementation :
public <T> List<String> convertToString(List<T> objectList) { List<String> stringList = new ArrayList<>(); char delimiter = ','; char quote = '"'; String lineSep = "\n"; CsvMapper mapper = new CsvMapper(); CsvSchema schema = mapper.schemaFor(!HOW_TO!); for (T object : objectList) { try { String csv = mapper.writer(schema .withColumnSeparator(delimiter) .withQuoteChar(quote) .withLineSeparator(lineSep)).writeValueAsString(object); } catch (JsonProcessingException e) { System.out.println(e); } } return stringList; }
I was using Jackson-dataformat-csv library, but I'm stuck with !HOW_TO! part, ie How to extract the .class of the object from the objectList. I was studying and came across Type Erasure, So I think it is somehow not possible other than giving the .class as parameter to my function. But I'm also extracting this object list from generic entity using Java Reflection, so I can't have the option to provide the .class params.
Is there a workaround for this?
OR
Any other approaches/libraries where I can convert a generic
List<T> objectList to List<String> csvList
with functionality of adding delimiters, quote characters, line separators etc.Thanks!
-
Mark Adelsberger over 7 yearsAnd if the first element of the list happens to be a subclass not shared by other elements of the list?
-
Bruno Delor over 7 yearsIt dosen't matter then. It's a list, it is known through the list as it's super class. If he needed the mapper to handle subclasses the solution is as simple. Just recreate a schema in the for loop and do the same getClass on each element. In his example he set up his schema out of the loop, I hence assumed all elements of the list must be handled in the same way. So there's no need for special cases
-
Mark Adelsberger over 7 yearsYes, it does matter. Given a
List<MyBaseClass>
, wanting to handle all elements the same, theClass
needed isMyBaseClass
. But if element zero happens to point to an instance whose runtime type isSubclassA
, then your method will convince him that he has aList<SubclassA>
and he'll quickly get in trouble if all the other instances are not also of that subclass. Your statement "it is known through the list as it's super class" is incorrect. The reference type in the list isObject
; the runtime type is the actual subclass type. It is only known as the base class at compile time. -
Bruno Delor over 7 yearsYou're right and I was wrong, I went a bit too quick in my thinking. However OP haven't mentioned any sub-classing. And since there is no way to get the parameter of List in runtime I only see these two solutions. Either ensuring there are no subclasses or getting a new CsvSchema for each item. In my solution I went for the easiest one and it's true it's not perfect. The only solution I can see to mitigate this would be to pass the target Class in the method call. Or to embed it in a generic class and use my second piece of code to know what is the target class to use.