Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert CSV file into objects in a generic way

Tags:

java

Ok, so i want to create a generic reading function in Java without using any other libraries(except for opencsv). (I've asked a bit about it a few hours before, and thought i sorted it out but turns out it didnt work.)

public class Read {

    void Reader(String fileName) {

        Map<String, String> mapping = new
                HashMap<String, String>();
        //mapping.put("Name", "Name");
        //mapping.put("Price", "Price");

        HeaderColumnNameTranslateMappingStrategy<Product> strategy = new HeaderColumnNameTranslateMappingStrategy<>();
        strategy.setType(Product.class);
        strategy.setColumnMapping(mapping);
        ...

    }
}

This works. The problem is i need to make it generic, as i'd like to use it for different kinds of csv's. My generic skills are not exactly the best though. If i change it to

void <T> Reader(T Obj,...)
...

and i pass a product Obj, im not sure what to put instead of

#...<Product> strategy
#and strategy.setType(Product.class)

I've tried adding the relative path, or using Obj.class, but i keep getting cannot convert errors. Any ideas?

The mappings are just an example, i've tried parsing the CSV to get the object tags for any object and it works.

More info: Tried trampering with these:

public static <T> void Read(T Obj,String fileName) {
...
HeaderColumnNameTranslateMappingStrategy<T> strategy = new HeaderColumnNameTranslateMappingStrategy<>();
strategy.setType(Obj.getClass());

error:incompatible types: java.lang.Class<capture#1 of ? extends java.lang.Object> cannot be converted to java.lang.Class<? extends T>
like image 473
valentine george Avatar asked Jan 20 '26 15:01

valentine george


1 Answers

The following approach aims to:

1) Provide a generic method for reading CSV data to different beans.

2) Avoid the need for casting to those different beans.

3) Provide some flexibility for different CSV structures/formats.

My example uses a HeaderColumnNameMappingStrategy, rather than the HeaderColumnNameTranslateMappingStrategy in the question - but you can change that, as you see fit. I have a slight preference for the former, as it lets me use annotations in the beans.

Source CSV Data:

The products.csv file:

Product ID,Product Name,Product Price
100125,Product One,123.45
100347,Product Two,3456.78

The customers.csv file:

Customer ID,Customer Name
4544678,John Smith
4544679,"Acme Warehouse, Inc."

The Beans

These use the com.opencsv.bean.CsvBindByName annotation for my HeaderColumnNameMappingStrategy:

public class Product {

    @CsvBindByName(column = "Product ID")
    private long id;

    @CsvBindByName(column = "Product Name")
    private String name;

    @CsvBindByName(column = "Product Price")
    private double price;

    // getters and setters not shown
}

and:

public class Customer {

    @CsvBindByName(column = "Customer ID")
    private String id;

    @CsvBindByName(column = "Customer Name")
    private String name;

    // getters and setters not shown
}

The Generic Method

This relies on the fact that CsvToBean is a generic method. By passing a list of the required type into this method, we can avoid needing to cast the results in the calling methods:

private <T> List<T> readCsvToBeanList(Path path, Class clazz, List<T> list) throws Exception {
    HeaderColumnNameMappingStrategy ms = new HeaderColumnNameMappingStrategy();
    ms.setType(clazz);

    try (Reader reader = Files.newBufferedReader(path)) {
        CsvToBean cb = new CsvToBeanBuilder(reader)
                .withType(clazz)
                .withMappingStrategy(ms)
                .build();

         list = cb.parse();
    }
    return list;
}

The above method can be called for the two objects as follows:

Path path = Paths.get(ClassLoader.getSystemResource("csv/products.csv").toURI());
List<Product> products = new ArrayList();
products = readCsvToBeanList(path, Product.class, products);
for (Product p : products) {
    System.out.println(p.getId()+ " - " + p.getName() + " - " + p.getPrice());
}

And similarly for customers:

Path path = Paths.get(ClassLoader.getSystemResource("csv/customers.csv").toURI());
List<Customer> customers = new ArrayList();
customers = readCsvToBeanList(path, Customer.class, customers);
for (Customer c : customers) {
    System.out.println(c.getId()+ " - " + c.getName());
}

The output from the above examples is as follows:

100125 - Product One - 123.45
100347 - Product Two - 3456.78

4544678 - John Smith
4544679 - Acme Warehouse, Inc.

Final notes:

This assumes we always have CSV files of a certain type - namely using comma separators, with a heading row, no empty lines, etc. So, it is brittle. Also, there is no error handling at this point. You can either pass additional parameters into the readCsvToBeanList method - for example a separator value:

CsvToBean cb = new CsvToBeanBuilder(reader)
            .withType(clazz)
            .withMappingStrategy(ms)
            .withSeparator('\t') // or whatever char you provide
            .build();

Or you can build a set of CsvToBean objects using different pre-defined builder configurations.

This gives you a lot of flexibility, overall.

Hope this helps and can be adapted to your specific situation.

like image 91
andrewJames Avatar answered Jan 22 '26 07:01

andrewJames



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!