DEV Community

Franz Wong
Franz Wong

Posted on

Writing csv file with OpenCsv without capitalized headers and follows declaration order

(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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

But this is what we expect.

"id","country_code"
"HK","852"
"US","1"
"JP","81"
Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode

Now we can pass the strategy to StatefulBeanToCsvBuilder.

StatefulBeanToCsv<CsvRow> csv = new StatefulBeanToCsvBuilder<CsvRow>(writer)
    .withMappingStrategy(strategy)
    .build();
Enter fullscreen mode Exit fullscreen mode

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

"id","country_code"
"HK","852"
"US","1"
"JP","81"
Enter fullscreen mode Exit fullscreen mode

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));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
adriens profile image
adriens

Thanks bro', it helped a lot on one of our projects 🙏

Collapse
 
deltarunner77 profile image
deltarunner77

for (CsvRow csvRow : csv) {} what is the purpose for this for loop?

Collapse
 
franzwong profile image
Franz Wong

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