Writing csv file with OpenCsv without capitalized headers and follows declaration order
Tags: opencsv, java
Published: 2022-06-05 05:55:47

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

```java
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`.

```java
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.

```csv
"COUNTRY_CODE","ID"
"852","HK"
"1","US"
"81","JP"
```

But this is what we expect.

```csv
"id","country_code"
"HK","852"
"US","1"
"JP","81"
```

## Solution

We have to use mapping strategy.

From [javadoc](http://opencsv.sourceforge.net/apidocs/com/opencsv/bean/StatefulBeanToCsvBuilder.html#withMappingStrategy-com.opencsv.bean.MappingStrategy-) 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.

```java
// 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`.

```java
StatefulBeanToCsv<CsvRow> csv = new StatefulBeanToCsvBuilder<CsvRow>(writer)
    .withMappingStrategy(strategy)
    .build();
```

Re-execute the application and we will have the CSV file we expected.

```csv
"id","country_code"
"HK","852"
"US","1"
"JP","81"
```

This is how the application looks like.

```java
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));
        }
    }
}
```