For Gradle Project, you can include the OpenCSV dependency. You can Download OpenCSV Jar and include in your project class path. Writing a CSV file is as simple as reading. Create an instance of CSVWriter by passing FileWriter object as parameter and start writing data to CSV file using methods of CSVWriter Class.
A header of the CSV file is an array of values assigned to each of the columns. It acts as a row header for the data. Initially, the CSV file is converted to a data frame and then a header is added to the data frame. The contents of the data frame are again stored back into the CSV file.
I've had similar problem. AFAIK there is no build-in functionality in OpenCSV that will allow to write bean to CSV with custom column names and ordering.
There are two main MappingStrategy
ies that are available in OpenCSV out of the box:
HeaderColumnNameMappingStrategy
: that allows to map CVS file columns to bean fields based on custom name; when writing bean to CSV this allows to change column header name but we have no control on column orderColumnPositionMappingStrategy
: that allows to map CSV file columns to bean fields based on column ordering; when writing bean to CSV we can control column order but we get an empty header (implementation returns new String[0]
as a header)The only way I found to achieve both custom column names and ordering is to write your custom MappingStrategy
.
Create custom MappingStrategy
:
class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
private static final String[] HEADER = new String[]{"TradeID", "GWML GUID", "MXML GUID", "GWML File", "MxML File", "MxML Counterparty", "GWML Counterparty"};
@Override
public String[] generateHeader() {
return HEADER;
}
}
And use it in StatefulBeanToCsvBuilder
:
final CustomMappingStrategy<MappingsBean> mappingStrategy = new CustomMappingStrategy<>();
mappingStrategy.setType(MappingsBean.class);
final StatefulBeanToCsv<MappingsBean> beanToCsv = new StatefulBeanToCsvBuilder<MappingsBean>(writer)
.withMappingStrategy(mappingStrategy)
.build();
beanToCsv.write(makeFinalMappingBeanList());
writer.close()
In MappingsBean
class we left CsvBindByPosition
annotations - to control ordering (in this solution CsvBindByName
annotations are not needed). Thanks to custom mapping strategy the header column names are included in resulting CSV file.
The downside of this solution is that when we change column ordering through CsvBindByPosition
annotation we have to manually change also HEADER
constant in our custom mapping strategy.
The first solution works, but it was not good for me. Based on build-in implementations of MappingStrategy
I came up with yet another implementation:
class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
@Override
public String[] generateHeader() {
final int numColumns = findMaxFieldIndex();
if (!isAnnotationDriven() || numColumns == -1) {
return super.generateHeader();
}
header = new String[numColumns + 1];
BeanField beanField;
for (int i = 0; i <= numColumns; i++) {
beanField = findField(i);
String columnHeaderName = extractHeaderName(beanField);
header[i] = columnHeaderName;
}
return header;
}
private String extractHeaderName(final BeanField beanField) {
if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) {
return StringUtils.EMPTY;
}
final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
return bindByNameAnnotation.column();
}
}
You can use this custom strategy in StatefulBeanToCsvBuilder
exactly this same as in the first solution (remember to invoke mappingStrategy.setType(MappingsBean.class);
, otherwise this solution will not work).
Currently our MappingsBean
has to contain both CsvBindByName
and CsvBindByPosition
annotations. The first to give header column name and the second to create ordering of columns in the output CSV header. Now if we change (using annotations) either column name or ordering in MappingsBean
class - that change will be reflected in output CSV file.
Corrected above answer to match with newer version.
package csvpojo;
import org.apache.commons.lang3.StringUtils;
import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
@Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
super.setColumnMapping(new String[ FieldUtils.getAllFields(bean.getClass()).length]);
final int numColumns = findMaxFieldIndex();
if (!isAnnotationDriven() || numColumns == -1) {
return super.generateHeader(bean);
}
String[] header = new String[numColumns + 1];
BeanField<T> beanField;
for (int i = 0; i <= numColumns; i++) {
beanField = findField(i);
String columnHeaderName = extractHeaderName(beanField);
header[i] = columnHeaderName;
}
return header;
}
private String extractHeaderName(final BeanField<T> beanField) {
if (beanField == null || beanField.getField() == null
|| beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) {
return StringUtils.EMPTY;
}
final CsvBindByName bindByNameAnnotation = beanField.getField()
.getDeclaredAnnotationsByType(CsvBindByName.class)[0];
return bindByNameAnnotation.column();
}
}
Then call this to generate CSV. I have used Visitors as my POJO to populate, update wherever necessary.
CustomMappingStrategy<Visitors> mappingStrategy = new CustomMappingStrategy<>();
mappingStrategy.setType(Visitors.class);
// writing sample
List<Visitors> beans2 = new ArrayList<Visitors>();
Visitors v = new Visitors();
v.set_1_firstName(" test1");
v.set_2_lastName("lastname1");
v.set_3_visitsToWebsite("876");
beans2.add(v);
v = new Visitors();
v.set_1_firstName(" firstsample2");
v.set_2_lastName("lastname2");
v.set_3_visitsToWebsite("777");
beans2.add(v);
Writer writer = new FileWriter("G://output.csv");
StatefulBeanToCsv<Visitors> beanToCsv = new StatefulBeanToCsvBuilder<Visitors>(writer)
.withMappingStrategy(mappingStrategy).withSeparator(',').withApplyQuotesToAll(false).build();
beanToCsv.write(beans2);
writer.close();
My bean annotations looks like this
@CsvBindByName (column = "First Name", required = true)
@CsvBindByPosition(position=1)
private String firstName;
@CsvBindByName (column = "Last Name", required = true)
@CsvBindByPosition(position=0)
private String lastName;
I wanted to achieve bi-directional import/export - to be able to import generated CSV back to POJO and visa versa.
I was not able to use @CsvBindByPosition for this, because in this case - ColumnPositionMappingStrategy was selected automatically. Per documents: this strategy requires that the file does NOT have a header.
HeaderColumnNameMappingStrategy
mappingStrategy.setColumnOrderOnWrite(Comparator<String> writeOrder)
CsvUtils to read/write csv
import com.opencsv.CSVWriter;
import com.opencsv.bean.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.List;
public class CsvUtils {
private CsvUtils() {
}
public static <T> String convertToCsv(List<T> entitiesList, MappingStrategy<T> mappingStrategy) throws Exception {
try (Writer writer = new StringWriter()) {
StatefulBeanToCsv<T> beanToCsv = new StatefulBeanToCsvBuilder<T>(writer)
.withMappingStrategy(mappingStrategy)
.withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
.build();
beanToCsv.write(entitiesList);
return writer.toString();
}
}
@SuppressWarnings("unchecked")
public static <T> List<T> convertFromCsv(MultipartFile file, Class clazz) throws IOException {
try (Reader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
CsvToBean<T> csvToBean = new CsvToBeanBuilder<T>(reader).withType(clazz).build();
return csvToBean.parse();
}
}
}
POJO for import/export
public class LocalBusinessTrainingPairDTO {
//this is used for CSV columns ordering on exporting LocalBusinessTrainingPairs
public static final String[] FIELDS_ORDER = {"leftId", "leftName", "rightId", "rightName"};
@CsvBindByName(column = "leftId")
private int leftId;
@CsvBindByName(column = "leftName")
private String leftName;
@CsvBindByName(column = "rightId")
private int rightId;
@CsvBindByName(column = "rightName")
private String rightName;
// getters/setters omitted, do not forget to add them
}
Custom comparator for predefined String ordering:
public class OrderedComparatorIgnoringCase implements Comparator<String> {
private List<String> predefinedOrder;
public OrderedComparatorIgnoringCase(String[] predefinedOrder) {
this.predefinedOrder = new ArrayList<>();
for (String item : predefinedOrder) {
this.predefinedOrder.add(item.toLowerCase());
}
}
@Override
public int compare(String o1, String o2) {
return predefinedOrder.indexOf(o1.toLowerCase()) - predefinedOrder.indexOf(o2.toLowerCase());
}
}
Ordered writing for POJO (answer to initial question)
public static void main(String[] args) throws Exception {
List<LocalBusinessTrainingPairDTO> localBusinessTrainingPairsDTO = new ArrayList<>();
LocalBusinessTrainingPairDTO localBusinessTrainingPairDTO = new LocalBusinessTrainingPairDTO();
localBusinessTrainingPairDTO.setLeftId(1);
localBusinessTrainingPairDTO.setLeftName("leftName");
localBusinessTrainingPairDTO.setRightId(2);
localBusinessTrainingPairDTO.setRightName("rightName");
localBusinessTrainingPairsDTO.add(localBusinessTrainingPairDTO);
//Creating HeaderColumnNameMappingStrategy
HeaderColumnNameMappingStrategy<LocalBusinessTrainingPairDTO> mappingStrategy = new HeaderColumnNameMappingStrategy<>();
mappingStrategy.setType(LocalBusinessTrainingPairDTO.class);
//Setting predefined order using String comparator
mappingStrategy.setColumnOrderOnWrite(new OrderedComparatorIgnoringCase(LocalBusinessTrainingPairDTO.FIELDS_ORDER));
String csv = convertToCsv(localBusinessTrainingPairsDTO, mappingStrategy);
System.out.println(csv);
}
Read exported CSV back to POJO (addition to original answer)
Important: CSV can be unordered, as we are still using binding by name:
public static void main(String[] args) throws Exception {
//omitted code from writing
String csv = convertToCsv(localBusinessTrainingPairsDTO, mappingStrategy);
//Exported CSV should be compatible for further import
File temp = File.createTempFile("tempTrainingPairs", ".csv");
temp.deleteOnExit();
BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
bw.write(csv);
bw.close();
MultipartFile multipartFile = new MockMultipartFile("tempTrainingPairs.csv", new FileInputStream(temp));
List<LocalBusinessTrainingPairDTO> localBusinessTrainingPairDTOList = convertFromCsv(multipartFile, LocalBusinessTrainingPairDTO.class);
}
In the latest version the solution of @Sebast26 does no longer work. However the basic is still very good. Here is a working solution with v5.0
import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
import org.apache.commons.lang3.StringUtils;
class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
@Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
final int numColumns = getFieldMap().values().size();
super.generateHeader(bean);
String[] header = new String[numColumns];
BeanField beanField;
for (int i = 0; i < numColumns; i++) {
beanField = findField(i);
String columnHeaderName = extractHeaderName(beanField);
header[i] = columnHeaderName;
}
return header;
}
private String extractHeaderName(final BeanField beanField) {
if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(
CsvBindByName.class).length == 0) {
return StringUtils.EMPTY;
}
final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
return bindByNameAnnotation.column();
}
}
And the model looks like this:
@CsvBindByName(column = "id")
@CsvBindByPosition(position = 0)
private Long id;
@CsvBindByName(column = "name")
@CsvBindByPosition(position = 1)
private String name;
And my generation helper looks something like this:
public static <T extends AbstractCsv> String createCsv(List<T> data, Class<T> beanClazz) {
CustomMappingStrategy<T> mappingStrategy = new CustomMappingStrategy<T>();
mappingStrategy.setType(beanClazz);
StringWriter writer = new StringWriter();
String csv = "";
try {
StatefulBeanToCsv sbc = new StatefulBeanToCsvBuilder(writer)
.withSeparator(';')
.withMappingStrategy(mappingStrategy)
.build();
sbc.write(data);
csv = writer.toString();
} catch (CsvRequiredFieldEmptyException e) {
// TODO add some logging...
} catch (CsvDataTypeMismatchException e) {
// TODO add some logging...
} finally {
try {
writer.close();
} catch (IOException e) {
}
}
return csv;
}
The following works for me to map a POJO to a CSV file with custom column positioning and custom column headers (tested with opencsv-5.0) :
public class CustomBeanToCSVMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
@Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
String[] headersAsPerFieldName = getFieldMap().generateHeader(bean); // header name based on field name
String[] header = new String[headersAsPerFieldName.length];
for (int i = 0; i <= headersAsPerFieldName.length - 1; i++) {
BeanField beanField = findField(i);
String columnHeaderName = extractHeaderName(beanField); // header name based on @CsvBindByName annotation
if (columnHeaderName.isEmpty()) // No @CsvBindByName is present
columnHeaderName = headersAsPerFieldName[i]; // defaults to header name based on field name
header[i] = columnHeaderName;
}
headerIndex.initializeHeaderIndex(header);
return header;
}
private String extractHeaderName(final BeanField beanField) {
if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) {
return StringUtils.EMPTY;
}
final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
return bindByNameAnnotation.column();
}
}
Pojo
Column Positioning in the generated CSV file:
@CsvBindByPosition
Header name in the generated CSV file:
@CsvBindByName
, the generated header will be as per the annonationIf the field doesn't have @CsvBindByName
, then the generated header will be as per the field name
@Getter @Setter @ToString
public class Pojo {
@CsvBindByName(column="Voucher Series") // header: "Voucher Series"
@CsvBindByPosition(position=0)
private String voucherSeries;
@CsvBindByPosition(position=1) // header: "salePurchaseType"
private String salePurchaseType;
}
Using the above Custom Mapping Strategy:
CustomBeanToCSVMappingStrategy<Pojo> mappingStrategy = new CustomBeanToCSVMappingStrategy<>();
mappingStrategy.setType(Pojo.class);
StatefulBeanToCsv<Pojo> beanToCsv = new StatefulBeanToCsvBuilder<Pojo>(writer)
.withSeparator(CSVWriter.DEFAULT_SEPARATOR)
.withMappingStrategy(mappingStrategy)
.build();
beanToCsv.write(pojoList);
thanks for this thread, it has been really useful for me... I've enhanced a little bit the provided solution in order to accept also POJO where some fields are not annotated (not meant to be read/written):
public class ColumnAndNameMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
@Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
super.setColumnMapping(new String[ getAnnotatedFields(bean)]);
final int numColumns = getAnnotatedFields(bean);
final int totalFieldNum = findMaxFieldIndex();
if (!isAnnotationDriven() || numColumns == -1) {
return super.generateHeader(bean);
}
String[] header = new String[numColumns];
BeanField<T> beanField;
for (int i = 0; i <= totalFieldNum; i++) {
beanField = findField(i);
if (isFieldAnnotated(beanField.getField())) {
String columnHeaderName = extractHeaderName(beanField);
header[i] = columnHeaderName;
}
}
return header;
}
private int getAnnotatedFields(T bean) {
return (int) Arrays.stream(FieldUtils.getAllFields(bean.getClass()))
.filter(this::isFieldAnnotated)
.count();
}
private boolean isFieldAnnotated(Field f) {
return f.isAnnotationPresent(CsvBindByName.class) || f.isAnnotationPresent(CsvCustomBindByName.class);
}
private String extractHeaderName(final BeanField beanField) {
if (beanField == null || beanField.getField() == null) {
return StringUtils.EMPTY;
}
Field field = beanField.getField();
if (field.getDeclaredAnnotationsByType(CsvBindByName.class).length != 0) {
final CsvBindByName bindByNameAnnotation = field.getDeclaredAnnotationsByType(CsvBindByName.class)[0];
return bindByNameAnnotation.column();
}
if (field.getDeclaredAnnotationsByType(CsvCustomBindByName.class).length != 0) {
final CsvCustomBindByName bindByNameAnnotation = field.getDeclaredAnnotationsByType(CsvCustomBindByName.class)[0];
return bindByNameAnnotation.column();
}
return StringUtils.EMPTY;
}
}
If you're only interested in sorting the CSV columns based on the order in which member variables appear in your model class (CsvRow
row in this example), then you can use a Comparator
implementation to solve this in a rather simple manner. Here's an example that does this in Kotlin:
class ByMemberOrderCsvComparator : Comparator<String> {
private val memberOrder by lazy {
FieldUtils.getAllFields(CsvRow::class.java)
.map { it.getDeclaredAnnotation(CsvBindByName::class.java) }
.map { it?.column ?: "" }
.map { it.toUpperCase(Locale.US) } // OpenCSV UpperCases all headers, so we do this to match
}
override fun compare(field1: String?, field2: String?): Int {
return memberOrder.indexOf(field1) - memberOrder.indexOf(field2)
}
}
This Comparator
does the following:
CsvRow
)@CsvBindByName
annotation (in the order you specified them in the CsvRow
model)Next, apply this Comparator
to your MappingStrategy
, so it'll sort based off the specified order:
val mappingStrategy = HeaderColumnNameMappingStrategy<OrderSummaryCsvRow>()
mappingStrategy.setColumnOrderOnWrite(ByMemberOrderCsvComparator())
mappingStrategy.type = CsvRow::class.java
mappingStrategy.setErrorLocale(Locale.US)
val csvWriter = StatefulBeanToCsvBuilder<OrderSummaryCsvRow>(writer)
.withMappingStrategy(mappingStrategy)
.build()
For reference, here's an example CsvRow
class (you'll want to replace this with your own model for your needs):
data class CsvRow(
@CsvBindByName(column = "Column 1")
val column1: String,
@CsvBindByName(column = "Column 2")
val column2: String,
@CsvBindByName(column = "Column 3")
val column3: String,
// Other columns here ...
)
Which would produce a CSV as follows:
"COLUMN 1","COLUMN 2","COLUMN 3",...
"value 1a","value 2a","value 3a",...
"value 1b","value 2b","value 3b",...
The benefit of this approach is that it removes the need to hard-code any of your column names, which should greatly simplify things if you ever need to add/remove columns.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With