(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).