Java 8 here using Apache POI 4.1 to load Excel (XLSX) files into memory, and write lists of Java beans/POJOs back to new Excel files.
To me, an Excel file (at least the ones I'm working with) is really a list of POJOs, with each row being a different instance of the POJO, and each column a different field value for that instance. Observe:
Here I might have a POJO called Car, and the example spreadsheet above is a List<Car>:
#Getter
#Setter
public class Car {
private String manufacturer;
private String model;
private String color;
private String year;
private BigDecimal price;
}
So I have functioning code that will read an Excel file ("new-cars.xlsx") into a List<Car>, process that list, and then write the processed list back to an output file, say, "processed-cars.xlsx":
// 1. Load excel file into a List<Car>
InputStream inp = new FileInputStream("new-cars.xlsx");
Workbook workbook = WorkbookFactory.create(inp);
Iterator<Row> iterator = workbook.getSheetAt(0).iterator();
List<Car> carsInventory = new ArrayList<>();
while (iterator.hasNext()) {
Car car = new Car();
Row currentRow = iterator.next();
// don't read the header
if (currentRow.getRowNum() == 0) {
continue;
}
Iterator<Cell> cellIterator = currentRow.iterator();
while (cellIterator.hasNext()) {
Cell currentCell = cellIterator.next();
CellAddress address = currentCell.getAddress();
if (0 == address.getColumn()) {
// 1st col is "Manufacturer"
car.setManufacturer(currentCell.getStringCellValue());
} else if (1 == address.getColumn()) {
// 2nd col is "Model"
car.setModel(currentCell.getStringCellValue());
} else if (2 == address.getColumn()) {
// 3rd col is "Color"
car.setColor(currentCell.getStringCellValue());
} else if (3 == address.getColumn()) {
// 4th col is "Year"
car.setYear(currentCell.getStringCellValue());
} else if (4 == address.getColumn()) {
// 5th col is "Price"
car.setPrice(BigDecimal.valueOf(currentCell.getNumericCellValue()));
}
}
carsInventory.add(car);
}
// 2. Process the list of Cars; doesn't matter what this does
List<Car> processedInventory = processInventory(carsInventory);
// 3. Output to "processed-cars.xlsx"
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("Processed Inventory");
int rowNum = 0;
// create headers
Row headerRow = sheet.createRow(rowNum);
headerRow.createCell(0).setCellValue("Manufacturer");
headerRow.createCell(1).setCellValue("Model");
headerRow.createCell(2).setCellValue("Color");
headerRow.createCell(3).setCellValue("Year");
headerRow.createCell(4).setCellValue("Price");
rowNum++;
// rip through the cars list and convert each one into a subsequent row
for (Car processedCar : processedInventory) {
Row nextRow = sheet.createRow(rowNum);
nextRow.createCell(0).setCellValue(processedCar.getManufacturer());
nextRow.createCell(1).setCellValue(processedCar.getModel());
nextRow.createCell(2).setCellValue(processedCar.getColor());
nextRow.createCell(3).setCellValue(processedCar.getYear());
nextRow.createCell(4).setCellValue(processedCar.getPrice().doubleValue());
rowNum++;
}
FileOutputStream fos = new FileOutputStream("processed-cars.xlsx");
workbook.write(fos);
workbook.close();
While this works, it looks really ugly/nasty to me. I've used JSON mappers (Jackson, GSON, etc.), XML mappers (XStream) and OR/M tools (Hibernate) for years, and it occurred to me that POI's API (or some other library) might offer a "mapper-esque" solution that would allow me to map/bind the Excel data to/from a list of POJOs with minimal code and maximal elegance. However, I cannot find any such feature anywhere. Maybe this is because it doesn't exist, or maybe I'm just not searching for the right keywords.
Ideally, something along the lines of:
// Annotate the fields with something that POI (or whatever tool) can pick up
#Getter
#Setter
public class Car {
#ExcelColumn(name = "Manufacturer", col = 0)
private String manufacturer;
#ExcelColumn(name = "Model", col = 1)
private String model;
#ExcelColumn(name = "Color", col = 2)
private String color;
#ExcelColumn(name = "Year", col = 3)
private String year;
#ExcelColumn(name = "Price", col = 4)
private BigDecimal price;
}
// 2. Now load the Excel into a List<Car>
InputStream inp = new FileInputStream("new-cars.xlsx");
List<Car> carsInventory = WorkbookFactory.create(inp).buildList(Car.class);
// 3. Process the list
List<Car> processedInventory = processInventory(carsInventory);
//4. Write to a new file
WorkbookFactory.write(processInventory, "processed-cars.xlsx");
Does anything like this exist in POI-land? Or am I stuck with what I got?
As of now Apache POI does not such feature. There are external libraries which you can check. I provide below few libraries.
https://github.com/ozlerhakan/poiji
The library is available in mvnrepository, link is given below. This library provides only one way binding like from excel sheet to java pojo only.
https://mvnrepository.com/artifact/com.github.ozlerhakan/poiji/2.2.0
As per the above, you can do something like this.
public class Employee {
#ExcelRow
private int rowIndex;
#ExcelCell(0)
private long employeeId;
#ExcelCell(1)
private String name;
#ExcelCell(2)
private String surname;
#ExcelCell(3)
private int age;
}
To get the information from excel sheet to java object, you have to do in the following manner.
List<Employee> employees = Poiji.fromExcel(new File("employees.xls"), Employee.class);
There is another library which can do both things like excel to java and java to excel.
I provide below the link.
https://github.com/millij/poi-object-mapper
As per above library, you can do something like this.
#Sheet
public class Employee {
#SheetColumn("Age")
private Integer age;
#SheetColumn("Name")
public String getName() {
return name;
}
}
To get data from xlsx file, you have to write like this.
final File xlsxFile = new File("<path_to_file>");
final XlsReader reader = new XlsReader();
List<Employee> employees = reader.read(Employee.class, xlsxFile);
To write data to the excel sheet, you have to do like this.
List<Employee> employees = new ArrayList<Employee>();
employees.add(new Employee("1", "foo", 12, "MALE", 1.68));
SpreadsheetWriter writer = new SpreadsheetWriter("<output_file_path>");
writer.addSheet(Employee.class, employees);
writer.write();
You have to evaluate both the libraries for your use cases.
I would consider writing my own apache poi to/from POJO mapper package instead of simply searching for any available packages. Doing this you are more flexible in extending the functionality then because you then know how it works without the need of dive deep into code others have wrote and which is heavily divided up into classes and methods. Trying to understand such code can be really difficult. No to mention to know where to place your own wanted extensions then.
To have a start, here is a package PoiPOJO which until now only consists of two classes. PoiPOJOUtils which provides two static methods. One sheetToPOJO and one pojoToSheet. And ExcelColumn which is an Annotation interface usable in POJO classes then.
PoiPOJOUtils.java:
package PoiPOJO;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellUtil;
import java.util.*;
import java.lang.reflect.*;
public class PoiPOJOUtils {
public static <T> List<T> sheetToPOJO(Sheet sheet, Class<T> beanClass) throws Exception {
DataFormatter formatter = new DataFormatter(java.util.Locale.US);
FormulaEvaluator evaluator = sheet.getWorkbook().getCreationHelper().createFormulaEvaluator();
int headerRowNum = sheet.getFirstRowNum();
// collecting the column headers as a Map of header names to column indexes
Map<Integer, String> colHeaders = new HashMap<Integer, String>();
Row row = sheet.getRow(headerRowNum);
for (Cell cell : row) {
int colIdx = cell.getColumnIndex();
String value = formatter.formatCellValue(cell, evaluator);
colHeaders.put(colIdx, value);
}
// collecting the content rows
List<T> result = new ArrayList<T>();
String cellValue = "";
java.util.Date date = null;
Double num = null;
for (int r = headerRowNum + 1; r <= sheet.getLastRowNum(); r++) {
row = sheet.getRow(r); if (row == null) row = sheet.createRow(r);
T bean = beanClass.getDeclaredConstructor().newInstance();
for (Map.Entry<Integer, String> entry : colHeaders.entrySet()) {
int colIdx = entry.getKey();
Cell cell = row.getCell(colIdx); if (cell == null) cell = row.createCell(colIdx);
cellValue = formatter.formatCellValue(cell, evaluator); // string values and formatted numbers
// make some differences for numeric or formula content
date = null;
num = null;
if (cell.getCellType() == CellType.NUMERIC) {
if (DateUtil.isCellDateFormatted(cell)) { // date
date = cell.getDateCellValue();
} else { // other numbers
num = cell.getNumericCellValue();
}
} else if (cell.getCellType() == CellType.FORMULA) {
// if formula evaluates to numeric
if (evaluator.evaluateFormulaCell(cell) == CellType.NUMERIC) {
if (DateUtil.isCellDateFormatted(cell)) { // date
date = cell.getDateCellValue();
} else { // other numbers
num = cell.getNumericCellValue();
}
}
}
// fill the bean
for (Field f : beanClass.getDeclaredFields()) {
if (!f.isAnnotationPresent(ExcelColumn.class)) {
continue;
}
ExcelColumn ec = f.getAnnotation(ExcelColumn.class);
if(entry.getValue().equals(ec.name())) {
f.setAccessible(true);
if (f.getType() == String.class) {
f.set(bean, cellValue);
} else if (f.getType() == Double.class) {
f.set(bean, num);
} else if (f.getType() == java.util.Date.class) {
f.set(bean, date);
} else { // this is for all other; Integer, Boolean, ...
if (!"".equals(cellValue)) {
Method valueOf = f.getType().getDeclaredMethod("valueOf", String.class);
f.set(bean, valueOf.invoke(f.getType(), cellValue));
}
}
}
}
}
result.add(bean);
}
return result;
}
public static <T> void pojoToSheet(Sheet sheet, List<T> rows) throws Exception {
if (rows.size() > 0) {
Row row = null;
Cell cell = null;
int r = 0;
int c = 0;
int colCount = 0;
Map<String, Object> properties = null;
DataFormat dataFormat = sheet.getWorkbook().createDataFormat();
Class beanClass = rows.get(0).getClass();
// header row
row = sheet.createRow(r++);
for (Field f : beanClass.getDeclaredFields()) {
if (!f.isAnnotationPresent(ExcelColumn.class)) {
continue;
}
ExcelColumn ec = f.getAnnotation(ExcelColumn.class);
cell = row.createCell(c++);
// do formatting the header row
properties = new HashMap<String, Object>();
properties.put(CellUtil.FILL_PATTERN, FillPatternType.SOLID_FOREGROUND);
properties.put(CellUtil.FILL_FOREGROUND_COLOR, IndexedColors.GREY_25_PERCENT.getIndex());
CellUtil.setCellStyleProperties(cell, properties);
cell.setCellValue(ec.name());
}
colCount = c;
// contents
for (T bean : rows) {
c = 0;
row = sheet.createRow(r++);
for (Field f : beanClass.getDeclaredFields()) {
cell = row.createCell(c++);
if (!f.isAnnotationPresent(ExcelColumn.class)) {
continue;
}
ExcelColumn ec = f.getAnnotation(ExcelColumn.class);
// do number formatting the contents
String numberFormat = ec.numberFormat();
properties = new HashMap<String, Object>();
properties.put(CellUtil.DATA_FORMAT, dataFormat.getFormat(numberFormat));
CellUtil.setCellStyleProperties(cell, properties);
f.setAccessible(true);
Object value = f.get(bean);
if (value != null) {
if (value instanceof String) {
cell.setCellValue((String)value);
} else if (value instanceof Double) {
cell.setCellValue((Double)value);
} else if (value instanceof Integer) {
cell.setCellValue((Integer)value);
} else if (value instanceof java.util.Date) {
cell.setCellValue((java.util.Date)value);
} else if (value instanceof Boolean) {
cell.setCellValue((Boolean)value);
}
}
}
}
// auto size columns
for (int col = 0; col < colCount; col++) {
sheet.autoSizeColumn(col);
}
}
}
}
and
ExcelColumn.java:
package PoiPOJO;
import java.lang.annotation.*;
#Retention(RetentionPolicy.RUNTIME)
public #interface ExcelColumn {
String name();
String numberFormat() default "General";
}
This can be used then having ...
Car.java:
import PoiPOJO.ExcelColumn;
public class Car {
#ExcelColumn(name = "Manufacturer")
public String manufacturer;
#ExcelColumn(name = "Model")
public String model;
#ExcelColumn(name = "Color")
public String color;
#ExcelColumn(name = "Year", numberFormat = "0")
public Integer year;
#ExcelColumn(name = "Price", numberFormat = "$#,##0.00")
public Double price;
#ExcelColumn(name = "Date", numberFormat = "YYYY-MM-DD")
public java.util.Date date;
#ExcelColumn(name = "Available")
public Boolean available;
public String toString() {
String result = ""
+"Manufacturer=" + this.manufacturer
+" Model=" + this.model
+" Color=" + this.color
+" Year=" + this.year
+" Price=" + this.price
+" Date=" + this.date
+" Available=" + this.available
+"";
return result;
}
}
and
TestPoiPOJO.java:
import PoiPOJO.PoiPOJOUtils;
import org.apache.poi.ss.usermodel.*;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.List;
public class TestPoiPOJO {
public static void main(String[] args) throws Exception {
Workbook workbook = WorkbookFactory.create(new FileInputStream("ExcelCars.xlsx"));
Sheet sheet = workbook.getSheetAt(0);
List<Car> cars = PoiPOJOUtils.sheetToPOJO(sheet, Car.class);
System.out.println(cars);
Car car = new Car();
car.manufacturer = "Mercedes-Benz";
car.model = "S 560 4Matic";
car.color = "Bordeaux";
car.year = 2019;
car.price = 78456.78;
car.date = new java.util.Date();
car.available = true;
cars.add(car);
sheet = workbook.createSheet();
PoiPOJOUtils.pojoToSheet(sheet, cars);
FileOutputStream out = new FileOutputStream("ExcelCarsNew.xlsx");
workbook.write(out);
out.close();
workbook.close();
}
}
The ExcelCars.xlsx must contain your sample cars table in first sheet. The sequence of the columns is flexible. Only the headings must correspond to the names of the ExcelColumn annotations in class Car.
I would like to recommend to use oCell library for mapping Excel to POJO and POJO to Excel.
https://github.com/rushuat/ocell
<dependency>
<groupId>io.github.rushuat</groupId>
<artifactId>ocell</artifactId>
<version>0.1.7</version>
</dependency>
Plus, this library supports few types of annotations (i.e. oCell, Jackson, JAXB, JPA) and other features for mapping (e.g. data transformation, cell formatting, field ignoring, etc).
Car POJO:
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Car {
#FieldName("Manufacturer")
private String manufacturer;
#FieldName("Model")
private String model;
#FieldName("Color")
private String color;
#FieldAlignment(horizontal = "right")
#FieldConverter(YearConverter.class)
#FieldName("Year")
private String year;
#FieldAlignment(horizontal = "right")
#FieldFormat("_($* #,##0.00_);_($* (#,##0.00);_($* \"-\"??_);_(#_)")
#FieldConverter(PriceConverter.class)
#FieldName("Price")
private BigDecimal price;
}
Read/Write Excel:
Car hondaCar = new Car("Honda", "Pilot", "White", "2019", new BigDecimal(39000));
Car chevyCar = new Car("Chevy", "Silverado", "Green", "2018", new BigDecimal(34000));
Car toyotaCar = new Car("Toyota", "Corolla", "Silver", "2002", new BigDecimal(4000));
try (Document document = new DocumentOOXML()) {
List<Car> cars = Arrays.asList(hondaCar, chevyCar, toyotaCar);
document.addSheet(cars);
document.toFile("cars.xlsx");
}
try (Document document = new DocumentOOXML()) {
document.fromFile("cars.xlsx");
List<Car> cars = document.getSheet(Car.class);
}
Field Converters:
public class YearConverter implements ValueConverter<String, Number> {
#Override
public String convertInput(Number value) {
return value == null ? null : String.valueOf(value.intValue());
}
#Override
public Number convertOutput(String value) {
return value == null ? null : Integer.valueOf(value);
}
}
public class PriceConverter implements ValueConverter<BigDecimal, Number> {
#Override
public BigDecimal convertInput(Number value) {
return value == null ? null : new BigDecimal(value.longValue());
}
#Override
public Number convertOutput(BigDecimal value) {
return value == null ? null : value.longValue();
}
}
#FieldFormat Source:
Basic Excel currency format with Apache POI
A slight variation to #Axel Ritcher's answer, using parallel streams and for Java objects with a Set Field (and no formula evaluation) :
public class ExcelFileUtils {
#SneakyThrows
// Call this using ExcelFileUtils.sheetToPOJO(new FileInputStream("yourExcl.xlsx"),YourPojo.class)
public static <T> List<T> sheetToPOJO(InputStream is, Class<T> beanClass) {
Workbook workbook = WorkbookFactory.create(is);
Sheet sheet=workbook.getSheetAt(0);
Map<Integer, String> colHeadersByColIdx = getColHeadersByCoIndex(sheet);
Map<String, Field> beanFieldsByExlColName=beanFieldsByExlColName(beanClass);
return IntStream.range(sheet.getFirstRowNum()+1,sheet.getLastRowNum())
.parallel()
.mapToObj(rowNum->{
T bean = null;
try {
bean =beanClass.getDeclaredConstructor().newInstance();
Row currentRow=sheet.getRow(rowNum);
if(Objects.isNull(currentRow)) currentRow=sheet.createRow(rowNum);
Row finalCurrentRow = currentRow;
T finalBean = bean;
colHeadersByColIdx.keySet().parallelStream()
.forEach(colIdx->{
String colName=colHeadersByColIdx.get(colIdx);
Cell cell=finalCurrentRow.getCell(colIdx);
if(Objects.isNull(cell))cell=finalCurrentRow.createCell(colIdx);
String cellValue=cell.getStringCellValue();
Field fieldForColName=beanFieldsByExlColName.get(colName);
fieldForColName.setAccessible(true);
try {
if (fieldForColName.getType() == String.class) {
fieldForColName.set(finalBean, cellValue);
}
if(fieldForColName.getType() == Double.class){
fieldForColName.set(finalBean,cell.getNumericCellValue());
}
if(fieldForColName.getType() == Set.class ){
fieldForColName.set(finalBean, Arrays.stream(cellValue.split(",")).collect(Collectors.toSet()));
}
}catch (IllegalAccessException ex){
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,ex.getMessage());
}
});
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |NoSuchMethodException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,e.getMessage());
}
return bean;
}).collect(Collectors.toList());
}
private static <T> Map<String, Field> beanFieldsByExlColName(Class<T> beanClass){
Map<String, Field> beanFieldsByExlColName=new HashMap<>();
Arrays.stream(beanClass.getDeclaredFields())
.parallel()
.filter(field -> field.isAnnotationPresent(ExcelColumn.class))
.forEach(field -> {
ExcelColumn ec = field.getAnnotation(ExcelColumn.class);
beanFieldsByExlColName.put(ec.name(),field);
});
return beanFieldsByExlColName;
}
private static Map<Integer, String> getColHeadersByCoIndex(Sheet sheet){
Map<Integer, String> colHeadersByColIdx = new HashMap<Integer, String>();
Row row1 = sheet.getRow(sheet.getFirstRowNum());
for(Cell cell : row1){
int colIdx=cell.getColumnIndex();
colHeadersByColIdx.put(colIdx,cell.getStringCellValue());
}
return colHeadersByColIdx;
}
}
Please note that this example assumes that you have String, Double and Set in your pojo and the excel column that corresponds to the Set has comma separated values.
For example :
POJO :
#Data
public class TestProduct{
#ExcelColumn(name = "Product Name")
private String productName;
#ExcelColumn(name = "Image Urls")
private Set<String> mediaUrls;
}
And the Excel sheet :
I wanted to find a simple way to parse a xls/xlsx file to a list of pojo. After some searching i didn't find anything convenient and preferred to develop it quickly. Now i am able to get pojos by simply calling :
InputStream is = this.getClass().getResourceAsStream("/ExcelUtilsTest.xlsx");
List<Pojo> pojos = ExcelToPojoUtils.toPojo(Pojo.class, is);
If interested take a look on it :
https://github.com/ZPavel/excelToPojo
Related
While writing Beans to CSV file by using OpenCSV 4.6, all the headers are changing to uppercase. Eventhough bean has #CsvBindByName annotation it is changing to uppercase.
Java Bean:
public class ProjectInfo implements Serializable {
#CsvBindByName(column = "ProjectName",required = true)
private String projectName;
#CsvBindByName(column = "ProjectCode",required = true)
private String projectCode;
#CsvBindByName(column = "Visibility",required = true)
private String visibility;
//setters and getters
}
Main method
public static void main(String[] args) throws IOException {
Collection<Serializable> projectInfos = getProjectsInfo();
try(BufferedWriter writer = new BufferedWriter(new FileWriter("test.csv"))){
StatefulBeanToCsvBuilder builder = new StatefulBeanToCsvBuilder(writer);
StatefulBeanToCsv beanWriter = builder
.withSeparator(';')
.build();
try {
beanWriter.write(projectInfos.iterator());
writer.flush();
} catch (CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
throw new RuntimeException("Failed to download admin file");
}
}
}
Expected Result:
"ProjectCode";"ProjectName";"Visibility"
"ANY";"Country DU";"1"
"STD";"Standard";"1"
"TST";"Test";"1"
"CMM";"CMMTest";"1"
Acutal Result:
"PROJECTCODE";"PROJECTNAME";"VISIBILITY"
"ANY";"Country DU";"1"
"STD";"Standard";"1"
"TST";"Test";"1"
"CMM";"CMMTest";"1"
I don't have option to use ColumnMappingStrategy because I have to build this method as a generic solution.
can anyone suggest me how to write the headers as it is?
It happens, because the code in HeaderColumnNameMappingStrategy uses toUpperCase() for storing and retrieving the field names.
You could use the HeaderColumnNameTranslateMappingStrategy instead and create the mapping by reflection.
public class AnnotationStrategy extends HeaderColumnNameTranslateMappingStrategy
{
public AnnotationStrategy(Class<?> clazz)
{
Map<String,String> map=new HashMap<>();
//To prevent the column sorting
List<String> originalFieldOrder=new ArrayList<>();
for(Field field:clazz.getDeclaredFields())
{
CsvBindByName annotation = field.getAnnotation(CsvBindByName.class);
if(annotation!=null)
{
map.put(annotation.column(),annotation.column());
originalFieldOrder.add(annotation.column());
}
}
setType(clazz);
setColumnMapping(map);
//Order the columns as they were created
setColumnOrderOnWrite((a,b) -> Integer.compare(originalFieldOrder.indexOf(a), originalFieldOrder.indexOf(b)));
}
#Override
public String[] generateHeader(Object bean) throws CsvRequiredFieldEmptyException
{
String[] result=super.generateHeader(bean);
for(int i=0;i<result.length;i++)
{
result[i]=getColumnName(i);
}
return result;
}
}
And, assuming that there is only one class of items (and always at least one item), the creation of beanWriter has to be expanded:
StatefulBeanToCsv beanWriter = builder.withSeparator(';')
.withMappingStrategy(new AnnotationStrategy(projectInfos.iterator().next().getClass()))
.build();
Actually, HeaderColumnNameMappingStrategy uses toUpperCase() for storing and retrieving the field names.
In order to use custom field name you have to annotate you field with #CsvBindByName
#CsvBindByName(column = "Partner Code" )
private String partnerCode;
By default it will be capitalized to PARTNER CODE because of the above reason.
so, in order to take control over it we have to write a class implementing HeaderColumnNameTranslateMappingStrategy. With csv 5.0 and java8 i have implemented like this
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.HeaderColumnNameTranslateMappingStrategy;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
public class AnnotationStrategy<T> extends HeaderColumnNameTranslateMappingStrategy<T> {
Map<String, String> columnMap = new HashMap<>();
public AnnotationStrategy(Class<? extends T> clazz) {
for (Field field : clazz.getDeclaredFields()) {
CsvBindByName annotation = field.getAnnotation(CsvBindByName.class);
if (annotation != null) {
columnMap.put(field.getName().toUpperCase(), annotation.column());
}
}
setType(clazz);
}
#Override
public String getColumnName(int col) {
String name = headerIndex.getByPosition(col);
return name;
}
public String getColumnName1(int col) {
String name = headerIndex.getByPosition(col);
if(name != null) {
name = columnMap.get(name);
}
return name;
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
String[] result = super.generateHeader(bean);
for (int i = 0; i < result.length; i++) {
result[i] = getColumnName1(i);
}
return result;
}
}
I have tried other solutions but it doesn't work when the property name and column name are not the same.
I am using 5.6. My solution is to reuse the strategy.
public class CsvRow {
#CsvBindByName(column = "id")
private String id;
// Property name and column name are different
#CsvBindByName(column = "country_code")
private String countryCode;
}
// We are going to reuse this strategy
HeaderColumnNameMappingStrategy<CsvRow> strategy = new HeaderColumnNameMappingStrategy<>();
strategy.setType(CsvRow.class);
// Build the header line which respects the declaration order
// So its value will be "id,country_code"
String headerLine = Arrays.stream(CsvRow.class.getDeclaredFields())
.map(field -> field.getAnnotation(CsvBindByName.class))
.filter(Objects::nonNull)
.map(CsvBindByName::column)
.collect(Collectors.joining(","));
// Let the library to initialize column details in the strategy
try (StringReader stringReader = new StringReader(headerLine);
CSVReader reader = new CSVReader(stringReader)) {
CsvToBean<CsvRow> csv = new CsvToBeanBuilder<CsvRow>(reader)
.withType(CsvRow.class)
.withMappingStrategy(strategy)
.build();
for (CsvRow csvRow : csv) {}
}
The strategy is ready for writing csv file.
try (OutputStream outputStream = Files.newOutputStream(Path.of("test.csv"));
OutputStreamWriter writer = new OutputStreamWriter(outputStream)) {
StatefulBeanToCsv<CsvRow> csv = new StatefulBeanToCsvBuilder<CsvRow>(writer)
.withMappingStrategy(strategy)
.withThrowExceptions(true)
.build();
csv.write(csvRows);
}
Using opencsv 5.0 and Java 8, I had to modify AnnotationStrategy class code as follows to had it compiled :
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.HeaderColumnNameTranslateMappingStrategy;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
public class AnnotationStrategy<T> extends HeaderColumnNameTranslateMappingStrategy<T> {
public AnnotationStrategy(Class<? extends T> clazz) {
Map<String, String> map = new HashMap<>();
for (Field field : clazz.getDeclaredFields()) {
CsvBindByName annotation = field.getAnnotation(CsvBindByName.class);
if (annotation != null) {
map.put(annotation.column(), annotation.column());
}
}
setType(clazz);
setColumnMapping(map);
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
String[] result = super.generateHeader(bean);
for (int i = 0; i < result.length; i++) {
result[i] = getColumnName(i);
}
return result;
}
}
I have created a MappingsBean class where all the columns of the CSV file are specified. Next I parse XML files and create a list of mappingbeans. Then I write that data into CSV file as report.
I am using following annotations:
public class MappingsBean {
#CsvBindByName(column = "TradeID")
#CsvBindByPosition(position = 0)
private String tradeId;
#CsvBindByName(column = "GWML GUID", required = true)
#CsvBindByPosition(position = 1)
private String gwmlGUID;
#CsvBindByName(column = "MXML GUID", required = true)
#CsvBindByPosition(position = 2)
private String mxmlGUID;
#CsvBindByName(column = "GWML File")
#CsvBindByPosition(position = 3)
private String gwmlFile;
#CsvBindByName(column = "MxML File")
#CsvBindByPosition(position = 4)
private String mxmlFile;
#CsvBindByName(column = "MxML Counterparty")
#CsvBindByPosition(position = 5)
private String mxmlCounterParty;
#CsvBindByName(column = "GWML Counterparty")
#CsvBindByPosition(position = 6)
private String gwmlCounterParty;
}
And then I use StatefulBeanToCsv class to write into CSV file:
File reportFile = new File(reportOutputDir + "/" + REPORT_FILENAME);
Writer writer = new PrintWriter(reportFile);
StatefulBeanToCsv<MappingsBean> beanToCsv = new
StatefulBeanToCsvBuilder(writer).build();
beanToCsv.write(makeFinalMappingBeanList());
writer.close();
The problem with this approach is that if I use #CsvBindByPosition(position = 0) to control
position then I am not able to generate column names. If I use #CsvBindByName(column = "TradeID") then I am not able to set position of the columns.
Is there a way where I can use both annotations, so that I can create CSV files with column headers and also control column position?
Regards,
Vikram Pathania
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 MappingStrategyies 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 order
ColumnPositionMappingStrategy: 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.
First solution: fast and easy but hardcoded
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.
Second solution: more flexible
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.
What I've used to achieve the goal:
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);
}
To conclude:
We can read CSV to POJO, regardless of column order - because we are
using #CsvBindByName
We can control columns order on write using
custom comparator
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:
The column positioing in the generated CSV file will be as per the annotation #CsvBindByPosition
Header name in the generated CSV file:
If the field has #CsvBindByName, the generated header will be as per the annonation
If 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:
Fetches each member variable field in our data class (CsvRow)
Finds all the ones with the #CsvBindByName annotation (in the order you specified them in the CsvRow model)
Upper cases each to match the default OpenCsv implementation
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.
It is a solution for version greater than 4.3:
public class MappingBean {
#CsvBindByName(column = "column_a")
private String columnA;
#CsvBindByName(column = "column_b")
private String columnB;
#CsvBindByName(column = "column_c")
private String columnC;
// getters and setters
}
And use it as example:
import org.apache.commons.collections4.comparators.FixedOrderComparator;
...
var mappingStrategy = new HeaderColumnNameMappingStrategy<MappingBean>();
mappingStrategy.setType(MappingBean.class);
mappingStrategy.setColumnOrderOnWrite(new FixedOrderComparator<>("COLUMN_C", "COLUMN_B", "COLUMN_A"));
var sbc = new StatefulBeanToCsvBuilder<MappingBean>(writer)
.withMappingStrategy(mappingStrategy)
.build();
Result:
column_c | column_b | column_a
The following solution works with opencsv 5.0.
First, you need to inherit ColumnPositionMappingStrategy class and override generateHeader method to create your custom header for utilizing both CsvBindByName and CsvBindByPosition annotations as shown below.
import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
/**
* #param <T>
*/
class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
/*
* (non-Javadoc)
*
* #see com.opencsv.bean.ColumnPositionMappingStrategy#generateHeader(java.lang.
* Object)
*/
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
final int numColumns = getFieldMap().values().size();
if (numColumns == -1) {
return super.generateHeader(bean);
}
String[] header = new String[numColumns];
super.setColumnMapping(header);
BeanField<T, Integer> beanField;
for (int i = 0; i < numColumns; i++) {
beanField = findField(i);
String columnHeaderName = beanField.getField().getDeclaredAnnotation(CsvBindByName.class).column();
header[i] = columnHeaderName;
}
return header;
}
}
The next step is to use this mapping strategy while writing a bean to CSV as below.
CustomMappingStrategy<ScanReport> strategy = new CustomMappingStrategy<>();
strategy.setType(ScanReport.class);
// Write a bean to csv file.
StatefulBeanToCsv<ScanReport> beanToCsv = new StatefulBeanToCsvBuilder<ScanReport>(writer)
.withMappingStrategy(strategy).build();
beanToCsv.write(beanList);
I've improved on previous answers by removing all references to deprecated APIs while using the latest release of opencsv (4.6).
A Generic Kotlin Solution
/**
* Custom OpenCSV [ColumnPositionMappingStrategy] that allows for a header line to be generated from a target CSV
* bean model class using the following annotations when present:
* * [CsvBindByName]
* * [CsvCustomBindByName]
*/
class CustomMappingStrategy<T>(private val beanType: Class<T>) : ColumnPositionMappingStrategy<T>() {
init {
setType(beanType)
setColumnMapping(*getAnnotatedFields().map { it.extractHeaderName() }.toTypedArray())
}
override fun generateHeader(bean: T): Array<String> = columnMapping
private fun getAnnotatedFields() = beanType.declaredFields.filter { it.isAnnotatedByName() }.toList()
private fun Field.isAnnotatedByName() = isAnnotationPresent(CsvBindByName::class.java) || isAnnotationPresent(CsvCustomBindByName::class.java)
private fun Field.extractHeaderName() =
getAnnotation(CsvBindByName::class.java)?.column ?: getAnnotation(CsvCustomBindByName::class.java)?.column ?: EMPTY
}
Then use it as follows:
private fun csvBuilder(writer: Writer) =
StatefulBeanToCsvBuilder<MappingsBean>(writer)
.withSeparator(ICSVWriter.DEFAULT_SEPARATOR)
.withMappingStrategy(CustomMappingStrategy(MappingsBean::class.java))
.withApplyQuotesToAll(false)
.build()
// Kotlin try-with-resources construct
PrintWriter(File("$reportOutputDir/$REPORT_FILENAME")).use { writer ->
csvBuilder(writer).write(makeFinalMappingBeanList())
}
and for completeness, here's the CSV bean as a Kotlin data class:
data class MappingsBean(
#field:CsvBindByName(column = "TradeID")
#field:CsvBindByPosition(position = 0, required = true)
private val tradeId: String,
#field:CsvBindByName(column = "GWML GUID", required = true)
#field:CsvBindByPosition(position = 1)
private val gwmlGUID: String,
#field:CsvBindByName(column = "MXML GUID", required = true)
#field:CsvBindByPosition(position = 2)
private val mxmlGUID: String,
#field:CsvBindByName(column = "GWML File")
#field:CsvBindByPosition(position = 3)
private val gwmlFile: String? = null,
#field:CsvBindByName(column = "MxML File")
#field:CsvBindByPosition(position = 4)
private val mxmlFile: String? = null,
#field:CsvBindByName(column = "MxML Counterparty")
#field:CsvBindByPosition(position = 5)
private val mxmlCounterParty: String? = null,
#field:CsvBindByName(column = "GWML Counterparty")
#field:CsvBindByPosition(position = 6)
private val gwmlCounterParty: String? = null
)
I think the intended and most flexible way of handling the order of the header columns is to inject a comparator by HeaderColumnNameMappinStrategy.setColumnOrderOnWrite().
For me the most intuitive way was to write the CSV columns in the same order as they are specified in the CsvBean, but you can also adjust the Comparator to make use of your own annotations where you specify the order. DonĀ“t forget to rename the Comparator class then ;)
Integration:
HeaderColumnNameMappingStrategy<MyCsvBean> mappingStrategy = new HeaderColumnNameMappingStrategy<>();
mappingStrategy.setType(MyCsvBean.class);
mappingStrategy.setColumnOrderOnWrite(new ClassFieldOrderComparator(MyCsvBean.class));
Comparator:
private class ClassFieldOrderComparator implements Comparator<String> {
List<String> fieldNamesInOrderWithinClass;
public ClassFieldOrderComparator(Class<?> clazz) {
fieldNamesInOrderWithinClass = Arrays.stream(clazz.getDeclaredFields())
.filter(field -> field.getAnnotation(CsvBindByName.class) != null)
// Handle order by your custom annotation here
//.sorted((field1, field2) -> {
// int field1Order = field1.getAnnotation(YourCustomOrderAnnotation.class).getOrder();
// int field2Order = field2.getAnnotation(YourCustomOrderAnnotation.class).getOrder();
// return Integer.compare(field1Order, field2Order);
//})
.map(field -> field.getName().toUpperCase())
.collect(Collectors.toList());
}
#Override
public int compare(String o1, String o2) {
int fieldIndexo1 = fieldNamesInOrderWithinClass.indexOf(o1);
int fieldIndexo2 = fieldNamesInOrderWithinClass.indexOf(o2);
return Integer.compare(fieldIndexo1, fieldIndexo2);
}
}
This can be done using a HeaderColumnNameMappingStrategy along with a custom Comparator as well.
Which is recommended by the official doc http://opencsv.sourceforge.net/#mapping_strategies
File reportFile = new File(reportOutputDir + "/" + REPORT_FILENAME);
Writer writer = new PrintWriter(reportFile);
final List<String> order = List.of("TradeID", "GWML GUID", "MXML GUID", "GWML File", "MxML File", "MxML Counterparty", "GWML Counterparty");
final FixedOrderComparator comparator = new FixedOrderComparator(order);
HeaderColumnNameMappingStrategy<MappingsBean> strategy = new HeaderColumnNameMappingStrategy<>();
strategy.setType(MappingsBean.class);
strategy.setColumnOrderOnWrite(comparator);
StatefulBeanToCsv<MappingsBean> beanToCsv = new
StatefulBeanToCsvBuilder(writer)
.withMappingStrategy(strategy)
.build();
beanToCsv.write(makeFinalMappingBeanList());
writer.close();
CustomMappingStrategy for generic class.
public 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();
}
}
POJO Class
public class Customer{
#CsvBindByPosition(position=1)
#CsvBindByName(column="CUSTOMER", required = true)
private String customer;
}
Client Class
List<T> data = getEmployeeRecord();
CustomMappingStrategy custom = new CustomMappingStrategy();
custom.setType(Employee.class);
StatefulBeanToCsv<T> writer = new StatefulBeanToCsvBuilder<T>(response.getWriter())
.withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
.withSeparator('|')
.withOrderedResults(false)
.withMappingStrategy(custom)
.build();
writer.write(reportData);
There is another version for 5.2 version because I have a problem with #CsvCustomBindByName annotation when I tried answers above.
I defined custom annotation :
#Target(ElementType.FIELD)
#Inherited
#Retention(RetentionPolicy.RUNTIME)
public #interface CsvPosition {
int position();
}
and custom mapping strategy
public class CustomMappingStrategy<T> extends HeaderColumnNameMappingStrategy<T> {
private final Field[] fields;
public CustomMappingStrategy(Class<T> clazz) {
fields = clazz.getDeclaredFields();
Arrays.sort(fields, (f1, f2) -> {
CsvPosition position1 = f1.getAnnotation(CsvPosition.class);
CsvPosition position2 = f2.getAnnotation(CsvPosition.class);
return Integer.compare(position1.position(), position2.position());
});
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
String[] header = new String[fields.length];
for (Field f : fields) {
CsvPosition position = f.getAnnotation(CsvPosition.class);
header[position.position() - 1] = getName(f);
}
headerIndex.initializeHeaderIndex(header);
return header;
}
private String getName(Field f) {
CsvBindByName csvBindByName = f.getAnnotation(CsvBindByName.class);
CsvCustomBindByName csvCustomBindByName = f.getAnnotation(CsvCustomBindByName.class);
return csvCustomBindByName != null
? csvCustomBindByName.column() == null || csvCustomBindByName.column().isEmpty() ? f.getName() : csvCustomBindByName.column()
: csvBindByName.column() == null || csvBindByName.column().isEmpty() ? f.getName() : csvBindByName.column();
}
}
My POJO beans are annotated like this
public class Record {
#CsvBindByName(required = true)
#CsvPosition(position = 1)
Long id;
#CsvCustomBindByName(required = true, converter = BoolanCSVField.class)
#CsvPosition(position = 2)
Boolean deleted;
...
}
and final code for writer :
CustomMappingStrategy<Record> mappingStrategy = new CustomMappingStrategy<>(Record.class);
mappingStrategy.setType(Record.class);
StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer)
.withApplyQuotesToAll(false)
.withOrderedResults(true)
.withMappingStrategy(mappingStrategy)
.build();
I hope it will helpful for someone
Here is the code to add support for #CsvBindByPosition based ordering to default HeaderColumnNameMappingStrategy. Tested for latest version 5.2
Approach is to store 2 map. First headerPositionMap to store the position element so same can used to setColumnOrderOnWrite , second columnMap from which we can lookup actual column name rather than capitalized one
public class HeaderColumnNameWithPositionMappingStrategy<T> extends HeaderColumnNameMappingStrategy<T> {
protected Map<String, String> columnMap;
#Override
public void setType(Class<? extends T> type) throws CsvBadConverterException {
super.setType(type);
columnMap = new HashMap<>(this.getFieldMap().values().size());
Map<String, Integer> headerPositionMap = new HashMap<>(this.getFieldMap().values().size());
for (Field field : type.getDeclaredFields()) {
if (field.isAnnotationPresent(CsvBindByPosition.class) && field.isAnnotationPresent(CsvBindByName.class)) {
int position = field.getAnnotation(CsvBindByPosition.class).position();
String colName = "".equals(field.getAnnotation(CsvBindByName.class).column()) ? field.getName() : field.getAnnotation(CsvBindByName.class).column();
headerPositionMap.put(colName.toUpperCase().trim(), position);
columnMap.put(colName.toUpperCase().trim(), colName);
}
}
super.setColumnOrderOnWrite((String o1, String o2) -> {
if (!headerPositionMap.containsKey(o1) || !headerPositionMap.containsKey(o2)) {
return 0;
}
return headerPositionMap.get(o1) - headerPositionMap.get(o2);
});
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
String[] headersRaw = super.generateHeader(bean);
return Arrays.stream(headersRaw).map(h -> columnMap.get(h)).toArray(String[]::new);
}
}
If you dont have getDeclaredAnnotationsByType method, but need the name of your original field name:
beanField.getField().getName()
public 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().getDeclaredAnnotations().length == 0) {
return StringUtils.EMPTY;
}
return beanField.getField().getName();
}
}
Try something like below:
private static class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
String[] header;
public CustomMappingStrategy(String[] cols) {
header = cols;
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
return header;
}
}
Then use it as follows:
String[] columns = new String[]{"Name", "Age", "Company", "Salary"};
CustomMappingStrategy<Employee> mappingStrategy = new CustomMappingStrategy<Employee>(columns);
Where columns are columns of your bean and Employee is your bean
Great thread, I don't have any annotations in my pojo and this is how I did based on all the previous answers. Hope it helps others.
OpenCsv Version: 5.0
List readVendors = getFromMethod();
String[] fields= {"id","recordNumber","finVendorIdTb","finVenTechIdTb","finShortNameTb","finVenName1Tb","finVenName2Tb"};
String[] csvHeader= {"Id#","Shiv Record Number","Shiv Vendor Id","Shiva Tech Id#","finShortNameTb","finVenName1Tb","finVenName2Tb"};
CustomMappingStrategy<FinVendor> mappingStrategy = new CustomMappingStrategy(csvHeader);//csvHeader as per custom header irrespective of pojo field name
mappingStrategy.setType(FinVendor.class);
mappingStrategy.setColumnMapping(fields);//pojo mapping fields
StatefulBeanToCsv<FinVendor> beanToCsv = new StatefulBeanToCsvBuilder<FinVendor>(writer).withQuotechar(CSVWriter.NO_QUOTE_CHARACTER).withMappingStrategy(mappingStrategy).build();
beanToCsv.write(readVendors);
//custom mapping class as mentioned in the thread by many users
private static class CustomMappingStrategy extends ColumnPositionMappingStrategy {
String[] header;
public CustomMappingStrategy(String[] cols) {
header = cols;
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
super.generateHeader(bean);
return header;
}
}
Output:
Id# Shiv Record Number Shiv Vendor Id Fin Tech Id# finShortNameTb finVenName1Tb finVenName2Tb finVenDefaultLocTb
1 VEN00053 678 33316025986 THE ssOHIO S_2 THE UNIVERSITY CHK Test
2 VEN02277 1217 3044374205 Fe3 MECHA_1 FR3INC EFT-1
3 VEN03118 1310 30234484121 PE333PECTUS_1 PER332CTUS AR EFT-1 Test
The sebast26's first solution worked for me but for opencsv version 5.2 it requires a little change in the CustomMappingStrategy class:
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() {
super.generateHeader(bean); // without this the file contains ONLY headers
return HEADER;
}
}
In case you need this to preserve column ordering from the original CSV: use a HeaderColumnNameMappingStrategy for reading, then use the same strategy for writing. "Same" in this case meaning not just the same class, but really the same object.
From the javadoc of StatefulBeanToCsvBuilder.withMappingStrategy:
It is perfectly legitimate to read a CSV source, take the mapping
strategy from the read operation, and pass it in to this method for a
write operation. This conserves some processing time, but, more
importantly, preserves header ordering.
This way you will get a CSV including headers, with columns in the same order as the original CSV.
Worked for me using OpenCSV 5.4.
It took me time also but I found the solution.
Add these annotations to your POJO: #CsvBindByName, #CsvBindByPosition with the right name and position of each object.
My POJO:
#JsonIgnoreProperties(ignoreUnknown = true)
#Getter
#Setter
public class CsvReport {
#CsvBindByName(column = "Campaign")
#CsvBindByPosition(position = 0)
private String program;
#CsvBindByName(column = "Report")
#CsvBindByPosition(position = 1)
private String report;
#CsvBindByName(column = "Metric Label")
#CsvBindByPosition(position = 2)
private String metric;
}
And add this code (my POJO called CsvReport):
ColumnPositionMappingStrategy<CsvReport> mappingStrategy = new ColumnPositionMappingStrategyBuilder<CsvReport>().build();
mappingStrategy.setType(CsvReport.class);
//add your headers in the sort you want to be in the file:
String[] columns = new String[] { "Campaign", "Report", "Metric Label"};
mappingStrategy.setColumnMapping(columns);
//Write your headers first in your chosen Writer:
Writer responseWriter = response.getWriter();
responseWriter.append(String.join(",", columns)).append("\n");
// Configure the CSV writer builder
StatefulBeanToCsv<CsvReport> writer = new StatefulBeanToCsvBuilder<CsvReport>(responseWriter)
.withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
.withSeparator(CSVWriter.DEFAULT_SEPARATOR)
.withOrderedResults(true) //I needed to keep the order, if you don't put false.
.withMappingStrategy(mappingStrategy)
.build();
String fileName = "your file name";
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,String.format("attachment; filename=%s", fileName));
writer.write(csvReports);
This will create a new CSV file with your printed headers and ordered fields.
I am using opencsv-4.0 to write a csv file and I need to add column headers in output file.
Here is my code.
public static void buildProductCsv(final List<Product> product,
final String filePath) {
try {
Writer writer = new FileWriter(filePath);
// mapping of columns with their positions
ColumnPositionMappingStrategy<Product> mappingStrategy = new ColumnPositionMappingStrategy<Product>();
// Set mappingStrategy type to Product Type
mappingStrategy.setType(Product.class);
// Fields in Product Bean
String[] columns = new String[] { "productCode", "MFD", "EXD" };
// Setting the colums for mappingStrategy
mappingStrategy.setColumnMapping(columns);
StatefulBeanToCsvBuilder<Product> builder = new StatefulBeanToCsvBuilder<Product>(writer);
StatefulBeanToCsv<Product> beanWriter = builder.withMappingStrategy(mappingStrategy).build();
// Writing data to csv file
beanWriter.write(product);
writer.close();
log.info("Your csv file has been generated!");
} catch (Exception ex) {
log.warning("Exception: " + ex.getMessage());
}
}
Above code create a csv file with data. But it not include column headers in that file.
How could I add column headers to output csv?
ColumnPositionMappingStrategy#generateHeader returns empty array
/**
* This method returns an empty array.
* The column position mapping strategy assumes that there is no header, and
* thus it also does not write one, accordingly.
* #return An empty array
*/
#Override
public String[] generateHeader() {
return new String[0];
}
If you remove MappingStrategy from BeanToCsv builder
// replace
StatefulBeanToCsv<Product> beanWriter = builder.withMappingStrategy(mappingStrategy).build();
// with
StatefulBeanToCsv<Product> beanWriter = builder.build();
It will write Product's class members as CSV header
If your Product class members names are
"productCode", "MFD", "EXD"
This should be the right solution
Else, add #CsvBindByName annotation
import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.StatefulBeanToCsv;
import com.opencsv.bean.StatefulBeanToCsvBuilder;
import java.io.FileWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
public class CsvTest {
public static void main(String[] args) throws Exception {
Writer writer = new FileWriter(fileName);
StatefulBeanToCsvBuilder<Product> builder = new StatefulBeanToCsvBuilder<>(writer);
StatefulBeanToCsv<Product> beanWriter = builder.build();
List<Product> products = new ArrayList<>();
products.add(new Product("1", "11", "111"));
products.add(new Product("2", "22", "222"));
products.add(new Product("3", "33", "333"));
beanWriter.write(products);
writer.close();
}
public static class Product {
#CsvBindByName(column = "productCode")
String id;
#CsvBindByName(column = "MFD")
String member2;
#CsvBindByName(column = "EXD")
String member3;
Product(String id, String member2, String member3) {
this.id = id;
this.member2 = member2;
this.member3 = member3;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getMember2() {
return member2;
}
public void setMember2(String member2) {
this.member2 = member2;
}
public String getMember3() {
return member3;
}
public void setMember3(String member3) {
this.member3 = member3;
}
}
}
Output:
"EXD","MFD","PRODUCTCODE"
"111","11","1"
"222","22","2"
"333","33","3"
Pay attention; class, getters & setters needs to be public due to the use of Reflection by OpenCSV library
You can append by annotation
public void export(List<YourObject> list, PrintWriter writer) throws Exception {
writer.append( buildHeader( YourObject.class ) );
StatefulBeanToCsvBuilder<YourObject> builder = new StatefulBeanToCsvBuilder<>( writer );
StatefulBeanToCsv<YourObject> beanWriter = builder.build();
beanWriter.write( mapper.map( list ) );
writer.close();
}
private String buildHeader(Class<YourObject> clazz) {
return Arrays.stream( clazz.getDeclaredFields() )
.filter( f -> f.getAnnotation( CsvBindByPosition.class ) != null
&& f.getAnnotation( CsvBindByName.class ) != null )
.sorted( Comparator.comparing( f -> f.getAnnotation( CsvBindByPosition.class ).position() ) )
.map( f -> f.getAnnotation( CsvBindByName.class ).column() )
.collect( Collectors.joining( "," ) ) + "\n";
}
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class YourObject {
#CsvBindByPosition(position = 0)
#CsvBindByName(column = "A")
private Long a;
#CsvBindByPosition(position = 1)
#CsvBindByName(column = "B")
private String b;
#CsvBindByPosition(position = 2)
#CsvBindByName(column = "C")
private String c;
}
I may have missed something obvious here but couldn't you just append your header String to the writer object?
Writer writer = new FileWriter(filePath);
writer.append("header1, header2, header3, ...etc \n");
// This will be followed by your code with BeanToCsvBuilder
// Note: the terminating \n might differ pending env.
Use a HeaderColumnNameMappingStrategy for reading, then use the same strategy for writing. "Same" in this case meaning not just the same class, but really the same object.
From the javadoc of StatefulBeanToCsvBuilder.withMappingStrategy:
It is perfectly legitimate to read a CSV source, take the mapping strategy from the read operation, and pass it in to this method for a write operation. This conserves some processing time, but, more importantly, preserves header ordering.
This way you will get a CSV including headers, with columns in the same order as the original CSV.
Worked for me using OpenCSV 5.4.
Use a custom strategy
static class CustomStrategy<T> extends ColumnPositionMappingStrategy<T> {
public String[] generateHeader() {
return this.getColumnMapping();
}
}
and on CSV object that you write do not forget to provide both
#CsvBindByName(column="UID")
#CsvBindByPosition(position = 3)
You could also override the generateHeaders method and return the column mapping that is set, which will have header row in csv
ColumnPositionMappingStrategy<Product> mappingStrategy = new ColumnPositionMappingStrategy<Product>() {
#Override
public String[] generateHeader(Product bean) throws CsvRequiredFieldEmptyException {
return this.getColumnMapping();
}
};
I have created a MappingsBean class where all the columns of the CSV file are specified. Next I parse XML files and create a list of mappingbeans. Then I write that data into CSV file as report.
I am using following annotations:
public class MappingsBean {
#CsvBindByName(column = "TradeID")
#CsvBindByPosition(position = 0)
private String tradeId;
#CsvBindByName(column = "GWML GUID", required = true)
#CsvBindByPosition(position = 1)
private String gwmlGUID;
#CsvBindByName(column = "MXML GUID", required = true)
#CsvBindByPosition(position = 2)
private String mxmlGUID;
#CsvBindByName(column = "GWML File")
#CsvBindByPosition(position = 3)
private String gwmlFile;
#CsvBindByName(column = "MxML File")
#CsvBindByPosition(position = 4)
private String mxmlFile;
#CsvBindByName(column = "MxML Counterparty")
#CsvBindByPosition(position = 5)
private String mxmlCounterParty;
#CsvBindByName(column = "GWML Counterparty")
#CsvBindByPosition(position = 6)
private String gwmlCounterParty;
}
And then I use StatefulBeanToCsv class to write into CSV file:
File reportFile = new File(reportOutputDir + "/" + REPORT_FILENAME);
Writer writer = new PrintWriter(reportFile);
StatefulBeanToCsv<MappingsBean> beanToCsv = new
StatefulBeanToCsvBuilder(writer).build();
beanToCsv.write(makeFinalMappingBeanList());
writer.close();
The problem with this approach is that if I use #CsvBindByPosition(position = 0) to control
position then I am not able to generate column names. If I use #CsvBindByName(column = "TradeID") then I am not able to set position of the columns.
Is there a way where I can use both annotations, so that I can create CSV files with column headers and also control column position?
Regards,
Vikram Pathania
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 MappingStrategyies 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 order
ColumnPositionMappingStrategy: 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.
First solution: fast and easy but hardcoded
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.
Second solution: more flexible
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.
What I've used to achieve the goal:
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);
}
To conclude:
We can read CSV to POJO, regardless of column order - because we are
using #CsvBindByName
We can control columns order on write using
custom comparator
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:
The column positioing in the generated CSV file will be as per the annotation #CsvBindByPosition
Header name in the generated CSV file:
If the field has #CsvBindByName, the generated header will be as per the annonation
If 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:
Fetches each member variable field in our data class (CsvRow)
Finds all the ones with the #CsvBindByName annotation (in the order you specified them in the CsvRow model)
Upper cases each to match the default OpenCsv implementation
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.
It is a solution for version greater than 4.3:
public class MappingBean {
#CsvBindByName(column = "column_a")
private String columnA;
#CsvBindByName(column = "column_b")
private String columnB;
#CsvBindByName(column = "column_c")
private String columnC;
// getters and setters
}
And use it as example:
import org.apache.commons.collections4.comparators.FixedOrderComparator;
...
var mappingStrategy = new HeaderColumnNameMappingStrategy<MappingBean>();
mappingStrategy.setType(MappingBean.class);
mappingStrategy.setColumnOrderOnWrite(new FixedOrderComparator<>("COLUMN_C", "COLUMN_B", "COLUMN_A"));
var sbc = new StatefulBeanToCsvBuilder<MappingBean>(writer)
.withMappingStrategy(mappingStrategy)
.build();
Result:
column_c | column_b | column_a
The following solution works with opencsv 5.0.
First, you need to inherit ColumnPositionMappingStrategy class and override generateHeader method to create your custom header for utilizing both CsvBindByName and CsvBindByPosition annotations as shown below.
import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
/**
* #param <T>
*/
class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
/*
* (non-Javadoc)
*
* #see com.opencsv.bean.ColumnPositionMappingStrategy#generateHeader(java.lang.
* Object)
*/
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
final int numColumns = getFieldMap().values().size();
if (numColumns == -1) {
return super.generateHeader(bean);
}
String[] header = new String[numColumns];
super.setColumnMapping(header);
BeanField<T, Integer> beanField;
for (int i = 0; i < numColumns; i++) {
beanField = findField(i);
String columnHeaderName = beanField.getField().getDeclaredAnnotation(CsvBindByName.class).column();
header[i] = columnHeaderName;
}
return header;
}
}
The next step is to use this mapping strategy while writing a bean to CSV as below.
CustomMappingStrategy<ScanReport> strategy = new CustomMappingStrategy<>();
strategy.setType(ScanReport.class);
// Write a bean to csv file.
StatefulBeanToCsv<ScanReport> beanToCsv = new StatefulBeanToCsvBuilder<ScanReport>(writer)
.withMappingStrategy(strategy).build();
beanToCsv.write(beanList);
I've improved on previous answers by removing all references to deprecated APIs while using the latest release of opencsv (4.6).
A Generic Kotlin Solution
/**
* Custom OpenCSV [ColumnPositionMappingStrategy] that allows for a header line to be generated from a target CSV
* bean model class using the following annotations when present:
* * [CsvBindByName]
* * [CsvCustomBindByName]
*/
class CustomMappingStrategy<T>(private val beanType: Class<T>) : ColumnPositionMappingStrategy<T>() {
init {
setType(beanType)
setColumnMapping(*getAnnotatedFields().map { it.extractHeaderName() }.toTypedArray())
}
override fun generateHeader(bean: T): Array<String> = columnMapping
private fun getAnnotatedFields() = beanType.declaredFields.filter { it.isAnnotatedByName() }.toList()
private fun Field.isAnnotatedByName() = isAnnotationPresent(CsvBindByName::class.java) || isAnnotationPresent(CsvCustomBindByName::class.java)
private fun Field.extractHeaderName() =
getAnnotation(CsvBindByName::class.java)?.column ?: getAnnotation(CsvCustomBindByName::class.java)?.column ?: EMPTY
}
Then use it as follows:
private fun csvBuilder(writer: Writer) =
StatefulBeanToCsvBuilder<MappingsBean>(writer)
.withSeparator(ICSVWriter.DEFAULT_SEPARATOR)
.withMappingStrategy(CustomMappingStrategy(MappingsBean::class.java))
.withApplyQuotesToAll(false)
.build()
// Kotlin try-with-resources construct
PrintWriter(File("$reportOutputDir/$REPORT_FILENAME")).use { writer ->
csvBuilder(writer).write(makeFinalMappingBeanList())
}
and for completeness, here's the CSV bean as a Kotlin data class:
data class MappingsBean(
#field:CsvBindByName(column = "TradeID")
#field:CsvBindByPosition(position = 0, required = true)
private val tradeId: String,
#field:CsvBindByName(column = "GWML GUID", required = true)
#field:CsvBindByPosition(position = 1)
private val gwmlGUID: String,
#field:CsvBindByName(column = "MXML GUID", required = true)
#field:CsvBindByPosition(position = 2)
private val mxmlGUID: String,
#field:CsvBindByName(column = "GWML File")
#field:CsvBindByPosition(position = 3)
private val gwmlFile: String? = null,
#field:CsvBindByName(column = "MxML File")
#field:CsvBindByPosition(position = 4)
private val mxmlFile: String? = null,
#field:CsvBindByName(column = "MxML Counterparty")
#field:CsvBindByPosition(position = 5)
private val mxmlCounterParty: String? = null,
#field:CsvBindByName(column = "GWML Counterparty")
#field:CsvBindByPosition(position = 6)
private val gwmlCounterParty: String? = null
)
I think the intended and most flexible way of handling the order of the header columns is to inject a comparator by HeaderColumnNameMappinStrategy.setColumnOrderOnWrite().
For me the most intuitive way was to write the CSV columns in the same order as they are specified in the CsvBean, but you can also adjust the Comparator to make use of your own annotations where you specify the order. DonĀ“t forget to rename the Comparator class then ;)
Integration:
HeaderColumnNameMappingStrategy<MyCsvBean> mappingStrategy = new HeaderColumnNameMappingStrategy<>();
mappingStrategy.setType(MyCsvBean.class);
mappingStrategy.setColumnOrderOnWrite(new ClassFieldOrderComparator(MyCsvBean.class));
Comparator:
private class ClassFieldOrderComparator implements Comparator<String> {
List<String> fieldNamesInOrderWithinClass;
public ClassFieldOrderComparator(Class<?> clazz) {
fieldNamesInOrderWithinClass = Arrays.stream(clazz.getDeclaredFields())
.filter(field -> field.getAnnotation(CsvBindByName.class) != null)
// Handle order by your custom annotation here
//.sorted((field1, field2) -> {
// int field1Order = field1.getAnnotation(YourCustomOrderAnnotation.class).getOrder();
// int field2Order = field2.getAnnotation(YourCustomOrderAnnotation.class).getOrder();
// return Integer.compare(field1Order, field2Order);
//})
.map(field -> field.getName().toUpperCase())
.collect(Collectors.toList());
}
#Override
public int compare(String o1, String o2) {
int fieldIndexo1 = fieldNamesInOrderWithinClass.indexOf(o1);
int fieldIndexo2 = fieldNamesInOrderWithinClass.indexOf(o2);
return Integer.compare(fieldIndexo1, fieldIndexo2);
}
}
This can be done using a HeaderColumnNameMappingStrategy along with a custom Comparator as well.
Which is recommended by the official doc http://opencsv.sourceforge.net/#mapping_strategies
File reportFile = new File(reportOutputDir + "/" + REPORT_FILENAME);
Writer writer = new PrintWriter(reportFile);
final List<String> order = List.of("TradeID", "GWML GUID", "MXML GUID", "GWML File", "MxML File", "MxML Counterparty", "GWML Counterparty");
final FixedOrderComparator comparator = new FixedOrderComparator(order);
HeaderColumnNameMappingStrategy<MappingsBean> strategy = new HeaderColumnNameMappingStrategy<>();
strategy.setType(MappingsBean.class);
strategy.setColumnOrderOnWrite(comparator);
StatefulBeanToCsv<MappingsBean> beanToCsv = new
StatefulBeanToCsvBuilder(writer)
.withMappingStrategy(strategy)
.build();
beanToCsv.write(makeFinalMappingBeanList());
writer.close();
CustomMappingStrategy for generic class.
public 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();
}
}
POJO Class
public class Customer{
#CsvBindByPosition(position=1)
#CsvBindByName(column="CUSTOMER", required = true)
private String customer;
}
Client Class
List<T> data = getEmployeeRecord();
CustomMappingStrategy custom = new CustomMappingStrategy();
custom.setType(Employee.class);
StatefulBeanToCsv<T> writer = new StatefulBeanToCsvBuilder<T>(response.getWriter())
.withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
.withSeparator('|')
.withOrderedResults(false)
.withMappingStrategy(custom)
.build();
writer.write(reportData);
There is another version for 5.2 version because I have a problem with #CsvCustomBindByName annotation when I tried answers above.
I defined custom annotation :
#Target(ElementType.FIELD)
#Inherited
#Retention(RetentionPolicy.RUNTIME)
public #interface CsvPosition {
int position();
}
and custom mapping strategy
public class CustomMappingStrategy<T> extends HeaderColumnNameMappingStrategy<T> {
private final Field[] fields;
public CustomMappingStrategy(Class<T> clazz) {
fields = clazz.getDeclaredFields();
Arrays.sort(fields, (f1, f2) -> {
CsvPosition position1 = f1.getAnnotation(CsvPosition.class);
CsvPosition position2 = f2.getAnnotation(CsvPosition.class);
return Integer.compare(position1.position(), position2.position());
});
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
String[] header = new String[fields.length];
for (Field f : fields) {
CsvPosition position = f.getAnnotation(CsvPosition.class);
header[position.position() - 1] = getName(f);
}
headerIndex.initializeHeaderIndex(header);
return header;
}
private String getName(Field f) {
CsvBindByName csvBindByName = f.getAnnotation(CsvBindByName.class);
CsvCustomBindByName csvCustomBindByName = f.getAnnotation(CsvCustomBindByName.class);
return csvCustomBindByName != null
? csvCustomBindByName.column() == null || csvCustomBindByName.column().isEmpty() ? f.getName() : csvCustomBindByName.column()
: csvBindByName.column() == null || csvBindByName.column().isEmpty() ? f.getName() : csvBindByName.column();
}
}
My POJO beans are annotated like this
public class Record {
#CsvBindByName(required = true)
#CsvPosition(position = 1)
Long id;
#CsvCustomBindByName(required = true, converter = BoolanCSVField.class)
#CsvPosition(position = 2)
Boolean deleted;
...
}
and final code for writer :
CustomMappingStrategy<Record> mappingStrategy = new CustomMappingStrategy<>(Record.class);
mappingStrategy.setType(Record.class);
StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer)
.withApplyQuotesToAll(false)
.withOrderedResults(true)
.withMappingStrategy(mappingStrategy)
.build();
I hope it will helpful for someone
Here is the code to add support for #CsvBindByPosition based ordering to default HeaderColumnNameMappingStrategy. Tested for latest version 5.2
Approach is to store 2 map. First headerPositionMap to store the position element so same can used to setColumnOrderOnWrite , second columnMap from which we can lookup actual column name rather than capitalized one
public class HeaderColumnNameWithPositionMappingStrategy<T> extends HeaderColumnNameMappingStrategy<T> {
protected Map<String, String> columnMap;
#Override
public void setType(Class<? extends T> type) throws CsvBadConverterException {
super.setType(type);
columnMap = new HashMap<>(this.getFieldMap().values().size());
Map<String, Integer> headerPositionMap = new HashMap<>(this.getFieldMap().values().size());
for (Field field : type.getDeclaredFields()) {
if (field.isAnnotationPresent(CsvBindByPosition.class) && field.isAnnotationPresent(CsvBindByName.class)) {
int position = field.getAnnotation(CsvBindByPosition.class).position();
String colName = "".equals(field.getAnnotation(CsvBindByName.class).column()) ? field.getName() : field.getAnnotation(CsvBindByName.class).column();
headerPositionMap.put(colName.toUpperCase().trim(), position);
columnMap.put(colName.toUpperCase().trim(), colName);
}
}
super.setColumnOrderOnWrite((String o1, String o2) -> {
if (!headerPositionMap.containsKey(o1) || !headerPositionMap.containsKey(o2)) {
return 0;
}
return headerPositionMap.get(o1) - headerPositionMap.get(o2);
});
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
String[] headersRaw = super.generateHeader(bean);
return Arrays.stream(headersRaw).map(h -> columnMap.get(h)).toArray(String[]::new);
}
}
If you dont have getDeclaredAnnotationsByType method, but need the name of your original field name:
beanField.getField().getName()
public 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().getDeclaredAnnotations().length == 0) {
return StringUtils.EMPTY;
}
return beanField.getField().getName();
}
}
Try something like below:
private static class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
String[] header;
public CustomMappingStrategy(String[] cols) {
header = cols;
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
return header;
}
}
Then use it as follows:
String[] columns = new String[]{"Name", "Age", "Company", "Salary"};
CustomMappingStrategy<Employee> mappingStrategy = new CustomMappingStrategy<Employee>(columns);
Where columns are columns of your bean and Employee is your bean
Great thread, I don't have any annotations in my pojo and this is how I did based on all the previous answers. Hope it helps others.
OpenCsv Version: 5.0
List readVendors = getFromMethod();
String[] fields= {"id","recordNumber","finVendorIdTb","finVenTechIdTb","finShortNameTb","finVenName1Tb","finVenName2Tb"};
String[] csvHeader= {"Id#","Shiv Record Number","Shiv Vendor Id","Shiva Tech Id#","finShortNameTb","finVenName1Tb","finVenName2Tb"};
CustomMappingStrategy<FinVendor> mappingStrategy = new CustomMappingStrategy(csvHeader);//csvHeader as per custom header irrespective of pojo field name
mappingStrategy.setType(FinVendor.class);
mappingStrategy.setColumnMapping(fields);//pojo mapping fields
StatefulBeanToCsv<FinVendor> beanToCsv = new StatefulBeanToCsvBuilder<FinVendor>(writer).withQuotechar(CSVWriter.NO_QUOTE_CHARACTER).withMappingStrategy(mappingStrategy).build();
beanToCsv.write(readVendors);
//custom mapping class as mentioned in the thread by many users
private static class CustomMappingStrategy extends ColumnPositionMappingStrategy {
String[] header;
public CustomMappingStrategy(String[] cols) {
header = cols;
}
#Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
super.generateHeader(bean);
return header;
}
}
Output:
Id# Shiv Record Number Shiv Vendor Id Fin Tech Id# finShortNameTb finVenName1Tb finVenName2Tb finVenDefaultLocTb
1 VEN00053 678 33316025986 THE ssOHIO S_2 THE UNIVERSITY CHK Test
2 VEN02277 1217 3044374205 Fe3 MECHA_1 FR3INC EFT-1
3 VEN03118 1310 30234484121 PE333PECTUS_1 PER332CTUS AR EFT-1 Test
The sebast26's first solution worked for me but for opencsv version 5.2 it requires a little change in the CustomMappingStrategy class:
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() {
super.generateHeader(bean); // without this the file contains ONLY headers
return HEADER;
}
}
In case you need this to preserve column ordering from the original CSV: use a HeaderColumnNameMappingStrategy for reading, then use the same strategy for writing. "Same" in this case meaning not just the same class, but really the same object.
From the javadoc of StatefulBeanToCsvBuilder.withMappingStrategy:
It is perfectly legitimate to read a CSV source, take the mapping
strategy from the read operation, and pass it in to this method for a
write operation. This conserves some processing time, but, more
importantly, preserves header ordering.
This way you will get a CSV including headers, with columns in the same order as the original CSV.
Worked for me using OpenCSV 5.4.
It took me time also but I found the solution.
Add these annotations to your POJO: #CsvBindByName, #CsvBindByPosition with the right name and position of each object.
My POJO:
#JsonIgnoreProperties(ignoreUnknown = true)
#Getter
#Setter
public class CsvReport {
#CsvBindByName(column = "Campaign")
#CsvBindByPosition(position = 0)
private String program;
#CsvBindByName(column = "Report")
#CsvBindByPosition(position = 1)
private String report;
#CsvBindByName(column = "Metric Label")
#CsvBindByPosition(position = 2)
private String metric;
}
And add this code (my POJO called CsvReport):
ColumnPositionMappingStrategy<CsvReport> mappingStrategy = new ColumnPositionMappingStrategyBuilder<CsvReport>().build();
mappingStrategy.setType(CsvReport.class);
//add your headers in the sort you want to be in the file:
String[] columns = new String[] { "Campaign", "Report", "Metric Label"};
mappingStrategy.setColumnMapping(columns);
//Write your headers first in your chosen Writer:
Writer responseWriter = response.getWriter();
responseWriter.append(String.join(",", columns)).append("\n");
// Configure the CSV writer builder
StatefulBeanToCsv<CsvReport> writer = new StatefulBeanToCsvBuilder<CsvReport>(responseWriter)
.withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
.withSeparator(CSVWriter.DEFAULT_SEPARATOR)
.withOrderedResults(true) //I needed to keep the order, if you don't put false.
.withMappingStrategy(mappingStrategy)
.build();
String fileName = "your file name";
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,String.format("attachment; filename=%s", fileName));
writer.write(csvReports);
This will create a new CSV file with your printed headers and ordered fields.
I have a table with has the columns namely
recordID, recordName , titleFeild, titleIDMap, titleId, titleStartDate, titleEndDate, languageId
Now I have convert the data from above columns to the JSON object data which looks like below
{
"recordId" :10,
"recordName" : "RECORDS",
"records" : [ {
"titleField" : 1,
"titleIDMap" : null,
"titleId" : 500,
"titleStartDate" : "2013-12-22T00:00:00.000+0000",
"titleEndDate" : "2013-12-03T00:00:00.000+0000",
"languageId" : 20
}]
}
Please note that records is an array of columns ( titleFeild,titleIDMap,titleId,titleStartDate,titleEndDate,languageId)
The code so far I have developed is
List<Object[]> objList = dao.getStatus();
Integer result = null;
JSONObject jsonData = new JSONObject();
JSONArray jsonDataArray = new JSONArray();
if(objList!=null && objList.size()>10000)
{
for (Object[] nameObj : objList) {
jsonData.put("", nameObj.get(arg0) );
}
}
How do I construct the JSON Object from the columns data ?
You can easily achieve this with google-gson library. In simple terms you would have to create a couple of Pojos (with reference to another containin a list of references).
Consider RecordID and RecordName as Meta Data.
Create a pojo representing this information:
public class DbMetaPojo {
private int recordID;
private String recordName;
private List<Record> records;
public List<Record> getRecords() {
return records;
}
public void setRecords(List<Record> records) {
this.records = records;
}
public String getRecordName() {
return recordName;
}
public void setRecordName(String recordName) {
this.recordName = recordName;
}
public int getRecordID() {
return recordID;
}
public void setRecordID(int recordID) {
this.recordID = recordID;
}
}
Create another pojo with the actual Record fields:
public class Record {
public int getTitleFeild() {
return titleFeild;
}
public void setTitleFeild(int i) {
this.titleFeild = i;
}
public String getTitleIDMap() {
return titleIDMap;
}
public void setTitleIDMap(String titleIDMap) {
this.titleIDMap = titleIDMap;
}
public int getTitleId() {
return titleId;
}
public void setTitleId(int titleId) {
this.titleId = titleId;
}
public String getTitleStartDate() {
return titleStartDate;
}
public void setTitleStartDate(String titleStartDate) {
this.titleStartDate = titleStartDate;
}
public String getTitleEndDate() {
return titleEndDate;
}
public void setTitleEndDate(String titleEndDate) {
this.titleEndDate = titleEndDate;
}
public int getLanguageId() {
return languageId;
}
public void setLanguageId(int languageId) {
this.languageId = languageId;
}
private int titleFeild;
private String titleIDMap;
private int titleId;
private String titleStartDate;
private String titleEndDate;
private int languageId;
}
Now just a method to populate your POJOs with the relevant data (replace the hardcoding logic with your data retrieve):
public static void main(String... main) {
DbMetaPojo obj = new DbMetaPojo();
obj.setRecordID(10);
obj.setRecordName("RECORDS");
Record record = new Record();
record.setLanguageId(20);
record.setTitleEndDate("2013-12-22T00:00:00.000+0000");
record.setTitleFeild(1);
record.setTitleId(500);
record.setTitleIDMap("SOME NULL");
record.setTitleStartDate("2013-12-22T00:00:00.000+0000");
List<Record> list = new ArrayList<Record>();
list.add(record);
obj.setRecords(list);
Gson gson = new Gson();
String json = gson.toJson(obj);
System.out.println(json);
}
Output is your formed JSON:
{
"recordID": 10,
"recordName": "RECORDS",
"records": [
{
"titleFeild": 1,
"titleIDMap": "SOME NULL",
"titleId": 500,
"titleStartDate": "2013-12-22T00:00:00.000+0000",
"titleEndDate": "2013-12-22T00:00:00.000+0000",
"languageId": 20
}
]
}
EDIT:
To align to your code, you might want to do something like:
List<Object> objList = dao.getStatus();
List<DbMetaPojo> metaList = new ArrayList<DbMetaPojo> ();
if (objList != null && objList.size() > 10000) {
for (Object nameObj : objList) {
DbMetaPojo meta = new DbMetaPojo();
meta.setRecordID(nameObj[0]);
meta.setRecordName(nameObj[0]);
...
...
...
metaList.add(meta);
}
}
First of all what you have to do is retrieve the data from the columns of the table using your DAO and calling a Function from DAOIMPL which in turn will return the list of data(POJO probably).
Create a map like this which will contain your key value pair for example recordid and value,
recordname and value
Map<String,Object> objMap = new HashMap<String,Object>();
objMap.put("recordId", Record.getId());
objMap.put("recordName",Record.getName());
// Now here is the deal create another hashmap here whose key will be records "the key for your second array"
//Put the values in this second hashmap as instructed above and put it as a key value pair.
........
.......
.......
JSONObject JsonObject = JSONObject.fromObject(objMap);//This will create JSON object out of your hashmap.
objJSONList.add(JsonObject);
}
StringBuffer jsonBuffer = new StringBuffer();
jsonBuffer.append("{\"data\": {");
jsonBuffer.append(objJSONList.tostring());
jsonBuffer.append("}");
//jsonBuffer.append(",\"total\":"+ objJSONList.size());// TOTAL Optional
//jsonBuffer.append(",\"success\":true}");//SUCCESS message if using in callback Optional
Create an object which has your attribues. (recordID, recordName , titleFeild, titleIDMap, titleId, titleStartDate, titleEndDate, languageId)
Get data from dao and convert it to json. It will looks like what you want.
Gson gson = new Gson();
// convert java object to JSON format,
// and returned as JSON formatted string
String json = gson.toJson(obj);
I think your dao.getStatus() should return a List with Map keys and values. Your key would be column name and value would be content.
List<Map<String,Object>> objList = dao.getStatus();
if(objList!=null && objList.size()>10000){
for(Map<String,Object> row : objList) {
Iterator<String> keyList = row.keySet().iterator();
while(keyList.hasNext()){
String key = keyList.next();
jsonData.put(key, row.get(key));
}
}
}
For the records array you need to build it while iterating table columns.
Combining above code with building records array would be something like this..
String[] group = {"titleField","titleIDMap","titleId","titleStartDate","titleEndDate","languageId"};
List<String> recordGroup = Arrays.asList(group);
Map<Object, JSONArray> records = new HashMap<Object,JSONArray>();
List<Map<String,Object>> objList = dao.getStatus();
JSONObject jsonData = new JSONObject();
if(objList!=null && objList.size()>10000){
for(Map<String,Object> row : objList) {
int columnCount = 0;
Iterator<String> keyList = row.keySet().iterator();
while(keyList.hasNext()){
String key = keyList.next();
if(recordGroup.contains(key)){
Object recordId = row.get("recordId");
JSONArray recordArray = new JSONArray();
if(records.containsKey(recordId)){
recordArray = records.get(recordId);
JSONObject jsonObj = null;
if(columnCount >= recordGroup.size()){
jsonObj = new JSONObject();
recordarray.add(jsonObj);
columnCount = 0;
}
else {
jsonObj = (JSONObject) recordArray.get(recordArray.size()-1);
}
jsonObj.put(key, row.get(key));
columnCount++;
}
else {
JSONObject jsonObj = new JSONObject();
jsonObj.put(key, row.get(key));
recordArray.add(jsonObj);
records.put(recordId, recordArray);
}
jsonData.put("records", records.get(recordId));
}
else {
jsonData.put(key, row.get(key));
}
}
}
}