(OpenCSV 5.6 is used)
Problem
This class describes each CSV row. It has only 2 columns. id
is the 1st column and country_code
is the 2nd column.
public class CsvRow {
@CsvBindByName(column = "id")
private String id;
@CsvBindByName(column = "country_code")
private String countryCode;
public CsvRow(String id, String countryCode) {
this.id = id;
this.countryCode = countryCode;
}
}
CSV file is generated by StatefulBeanToCsv
.
public class Application {
public static void main(String[] args) throws Exception {
CsvRow[] csvRows = new CsvRow[] {
new CsvRow("HK", "852"),
new CsvRow("US", "1"),
new CsvRow("JP", "81"),
};
Path outputPath = Path.of("countries.csv");
try (var writer = Files.newBufferedWriter(outputPath)) {
StatefulBeanToCsv<CsvRow> csv = new StatefulBeanToCsvBuilder<CsvRow>(writer)
.build();
csv.write(Arrays.asList(csvRows));
}
}
}
Here is the csv file generated. All headers are capitalized and the order of headers are sorted by alphabetical order.
"COUNTRY_CODE","ID"
"852","HK"
"1","US"
"81","JP"
But this is what we expect.
"id","country_code"
"HK","852"
"US","1"
"JP","81"
Solution
We have to use mapping strategy.
From 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.
It means that we need to initialize a mapping strategy by reading an existing CSV file. We can programmatically create a CSV file and read it.
// Create our strategy
HeaderColumnNameMappingStrategy<CsvRow> strategy = new HeaderColumnNameMappingStrategy<>();
strategy.setType(CsvRow.class);
// Build the header line which respects the declaration order
String headerLine = Arrays.stream(CsvRow.class.getDeclaredFields())
.map(field -> field.getAnnotation(CsvBindByName.class))
.filter(Objects::nonNull)
.map(CsvBindByName::column)
.collect(Collectors.joining(","));
// Initialize strategy by reading a CSV with header only
try (StringReader reader = new StringReader(headerLine)) {
CsvToBean<CsvRow> csv = new CsvToBeanBuilder<CsvRow>(reader)
.withType(CsvRow.class)
.withMappingStrategy(strategy)
.build();
for (CsvRow csvRow : csv) {}
}
Now we can pass the strategy to StatefulBeanToCsvBuilder
.
StatefulBeanToCsv<CsvRow> csv = new StatefulBeanToCsvBuilder<CsvRow>(writer)
.withMappingStrategy(strategy)
.build();
Re-execute the application and we will have the CSV file we expected.
"id","country_code"
"HK","852"
"US","1"
"JP","81"
This is how the application looks like.
public class Application {
public static void main(String[] args) throws Exception {
CsvRow[] csvRows = new CsvRow[] {
new CsvRow("HK", "852"),
new CsvRow("US", "1"),
new CsvRow("JP", "81"),
};
HeaderColumnNameMappingStrategy<CsvRow> strategy = new HeaderColumnNameMappingStrategy<>();
strategy.setType(CsvRow.class);
String headerLine = Arrays.stream(CsvRow.class.getDeclaredFields())
.map(field -> field.getAnnotation(CsvBindByName.class))
.filter(Objects::nonNull)
.map(CsvBindByName::column)
.collect(Collectors.joining(","));
try (StringReader reader = new StringReader(headerLine)) {
CsvToBean<CsvRow> csv = new CsvToBeanBuilder<CsvRow>(reader)
.withType(CsvRow.class)
.withMappingStrategy(strategy)
.build();
for (CsvRow csvRow : csv) {}
}
Path outputPath = Path.of("countries.csv");
try (var writer = Files.newBufferedWriter(outputPath)) {
StatefulBeanToCsv<CsvRow> csv = new StatefulBeanToCsvBuilder<CsvRow>(writer)
.withMappingStrategy(strategy)
.build();
csv.write(Arrays.asList(csvRows));
}
}
}
Top comments (3)
Thanks bro', it helped a lot on one of our projects 🙏
for (CsvRow csvRow : csv) {} what is the purpose for this for loop?
Sorry for late reply. I think that loop is not necessary. Without knowing when CsvToBean loading the header, I just put it there to make sure it read anything from the reader (even there is no non-header row).