4

I try to map a nested bean structure with openCSV. I found the @CsvRecurse annotation, but this does not seem to work if a nested bean is used multiple times.

What options do I have to solve this?

Example (adapted from the docs linked above)

Data structure to map :

title,author1 given name,author1 surname,author2 given name,author2 surname
Space Opera 2.0,Andrew,Jones,Hanna,Smith

I would like to get the following beans

public class Book {
    @CsvBindByName
    private String title;

    // TODO: How to bind author1 and author2?
    private Author author1;
    private Author author2;

    // Accessor methods go here.
}

public class Author {
    // TODO: can I somehow use a prefix/Regex for the column here to differentiate between author1 and author2?
    @CsvBindByName(column = "author[1/2] given name")
    private String givenName;

    @CsvBindByName(column = "author[1/2] surname")
    private String surname;

    // Accessor methods go here.
}
Iris Hunkeler
  • 208
  • 1
  • 10

2 Answers2

0

You can use custom 'ColumnPositionMappingStrategy'. Override 'populateNewBean' method which takes String[] (row of csv) and can form any object out of it.

Like this

'@CsvRecurse' should work for Open OpenCSV 5.0 btw.

  • Thank you for your suggestion. I will check if a custom mapping strategy can solve my issue (but I will probably opt for ```HeaderNameBaseMappingStrategy``` since the order of the columns can change in the input). However, I would prefer to use ```@CsvRecurse``` if possible. But it does not seem to do what I need it to do: it only seems to apply to cases where there is max. 1 variable of the same class type, right? So it would work perfectly, if I had 1 ```Author``` variable. But is there a way to use it with 2 ```Author``` variables in my source bean? – Iris Hunkeler Jun 10 '20 at 06:49
0

Here is another option:

The opencsv library has a lot of useful and flexible annotations, but in this specific case, I would not use any of them.

Instead, I would use the opencsv CSVReaderHeaderAware class. Using this will allow you to keep your two Book and Author classes. The only thing I would change is to add constructors to each class as follows:

Book:

public class Book {

    private final String title;
    private final Author author1;
    private final Author author2;

    public Book(String title, Author author1, Author author2) {
        this.title = title;
        this.author1 = author1;
        this.author2 = author2;
    }

    public String getTitle() {
        return title;
    }

    public Author getAuthor1() {
        return author1;
    }

    public Author getAuthor2() {
        return author2;
    }

}

Author:

public class Author {

    private final String givenName;
    private final String surname;

    public Author(String givenName, String surname) {
        this.givenName = givenName;
        this.surname = surname;
    }

    public String getGivenName() {
        return givenName;
    }

    public String getSurname() {
        return surname;
    }

}

To populate a list of Book objects from a CSV file, do this:

import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import com.opencsv.CSVReaderHeaderAware;
import com.opencsv.exceptions.CsvValidationException;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

...

    public static void main(String[] args) throws FileNotFoundException, 
            IOException, CsvValidationException {
        String bookFile = "/path/to/titles.csv";

        CSVReaderHeaderAware csvReader = new CSVReaderHeaderAware(new FileReader(bookFile));

        List<Book> books = new ArrayList();

        Map<String, String> bookRecord;
        while ((bookRecord = csvReader.readMap()) != null) {
            books.add(handleBook(bookRecord));
        }
    }

    private static Book handleBook(Map<String, String> bookRecord) {
        Author author1 = new Author(
                bookRecord.get("author1 given name"),
                bookRecord.get("author1 surname")
        );

        Author author2 = new Author(
                bookRecord.get("author2 given name"),
                bookRecord.get("author2 surname")
        );

        return new Book(bookRecord.get("title"), author1, author2);
    }

Each row of data from the CSV file is read into a Map object, where the file headers are the map's keys (you need make sure the file headers are unique). The Map's corresponding values represent one row of data from the CSV file.

The only downside to this is you may need to cast string values to other data types - although not in this case, because the data items are already strings.

andrewJames
  • 19,570
  • 8
  • 19
  • 51