0

I am trying to create a csv file with the headers coming from a map and the values coming from an object. I have a Java object which stores different attribute data.

public class MyObject {
    private String name;
    private String email;
    private Integer id;
    private String phone;

   getters and setters...
}

I have a Map where the user has key value pair, where the key is the attribute name of MyObject and the value is column headers for a csv file. Ex:

Map<String, String> columnMap = new HashMap<>();
columnMap.put("id", "Id");
columnMap.put("name", "User Name");
columnMap.put("email", "User Email");
columnMap.put("phone", "User Phone");

My goal is to create a csv file from the data for which I first want to retrieve the values from the map to set as headers and then create rows based on what is stored in MyObject class. Ex:

public class CopyMapToSBForFile {

    public static void main(String[] args) {

        List<MyObject> myObjects = new ArrayList<>();
        MyObject myObject1 = new MyObject();
        myObject1.setId(1);
        myObject1.setName("Name 1");
        myObject1.setEmail("Email 1");
        myObject1.setPhone("111-121-2121");

        MyObject myObject2 = new MyObject();
        myObject2.setId(2);
        myObject2.setName("Name 2");
        myObject2.setEmail("Email 2");
        myObject2.setPhone("111-121-2121");

        MyObject myObject3 = new MyObject();
        myObject3.setId(3);
        myObject3.setName("Name 3");
        myObject3.setEmail("Email 3");
        myObject3.setPhone("111-121-2121");

        myObjects.add(myObject1);
        myObjects.add(myObject2);
        myObjects.add(myObject3);

        Map<String, String> columnMap = new HashMap<>();
        columnMap.put("id", "Id");
        columnMap.put("name", "User Name");
        columnMap.put("email", "User Email");
        columnMap.put("phone", "User Phone");

        Field[] fields = MyObject.class.getDeclaredFields();

        StringBuffer sb = new StringBuffer();
        for (Field field : fields) {
            System.out.println(field.getName());
            if (columnMap.containsKey(field.getName())) {
                sb.append(columnMap.get(field.getName()));
                sb.append(",");
            }
        }
        
    }

}

From the above example you can see using reflection utils I am able to create the header object but I am stuck on how to append the values from MyObject to the StringBuffer object to create something like the below output:

User Name,User Email,Id,User Phone
Name 1,Email 1,1,111-121-2121
Name 2,Email 2,2,111-121-2121
Name 3,Email 3,3,111-121-2121

I have a code which will conver the StringBuffer to CSV but I am stuck on how to create the above output once I create the header. Also, I want to make sure the program handles the row correctly incase the order of the columns are changed where the Id is first column instead of third column.

10
  • Does your map always contain all fields of your object? What if there are fields in the map that your object does not have? What about the order, is it important in which order the attributes appear in the csv? Commented Oct 30, 2023 at 22:30
  • @Eritrean it will always match the fields of the object or it can have fewer columns (eg. only id, name and email) and the order can change but if it does we need to make sure the column header matches the row values. Commented Oct 31, 2023 at 0:05
  • You haven't explained what part you are stuck on. Commented Oct 31, 2023 at 0:54
  • 1
    You're going to have to use reflection to invoke the getter for the field name and also cast it to the proper type. Personally, I would just keep it simple and impose a specific order. And StringBuilder is preferred over StringBuffer, unless you are running multiple threads. And you might also consider using a constructor to facilitate creating MyObject instances. Commented Oct 31, 2023 at 1:18
  • 3
    If you are using OpenCSV, you should not be generating CSV by hand. What data are you building for it? It sounds like you are generating CSV by hand, so that OpenCSV can read the CSV and turn it back into MyObjects, which doesn't make a lot of sense. This seems like a giant XY Problem - what are you trying to do ultimately? Commented Oct 31, 2023 at 2:33

2 Answers 2

1

You don't need reflection to create the header. If, as per your comment, the map contains only fields which are available in your object, just grab the values from the map to create the header. Use a LinkedHashMap instead of HashMap, Since the first one guarantees the order while the second one does not. For the content, I would recomend to create a String array per row / object instead of using StringBuffer or StringBuilder and leave the rest to opencsv to take care of separators, quotes .. and so on.

See the following example as a starting point. You could improve it by not doing everything in the main method as I did but extracting more methods for creating header, writing to file etc. and implementing a decent error handling.

import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.opencsv.CSVWriter;

public class Example {


    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException {

        List<MyObject> myObjects = createSampleList();

        Map<String, String> columnMap = new LinkedHashMap<>();
        columnMap.put("name", "User Name");
        columnMap.put("id", "Id");
        columnMap.put("email", "User Email");
        columnMap.put("phone", "User Phone");


        List<String[]> csvData = new ArrayList<>();
        String[] header = columnMap.values().toArray(String[]::new);

        csvData.add(header);

        for (MyObject myObject : myObjects) {
            List<String> row = new ArrayList<>();
            for (String key : columnMap.keySet()) {
                Field field = MyObject.class.getDeclaredField(key);
                field.setAccessible(true);
                String value = String.valueOf(field.get(myObject));
                row.add(value);
            }
            csvData.add(row.toArray(String[]::new));
        }

        // default separator is a comma, default all fields are enclosed in double quotes, you can change this by providing a boolean flag writer.writeAll(csvData, false);
        try (CSVWriter writer = new CSVWriter(new FileWriter("/path/to/your/csv/test.csv"))) {
            writer.writeAll(csvData);
        }
    }

    private static List<MyObject> createSampleList() {
        List<MyObject> myObjects = new ArrayList<>();
        MyObject myObject1 = new MyObject();
        myObject1.setId(1);
        myObject1.setName("Name 1");
        myObject1.setEmail("Email 1");
        myObject1.setPhone("111-121-2121");

        MyObject myObject2 = new MyObject();
        myObject2.setId(2);
        myObject2.setName("Name 2");
        myObject2.setEmail("Email 2");
        myObject2.setPhone("111-121-2121");

        MyObject myObject3 = new MyObject();
        myObject3.setId(3);
        myObject3.setName("Name 3");
        myObject3.setEmail("Email 3");
        myObject3.setPhone("111-121-2121");

        myObjects.add(myObject1);
        myObjects.add(myObject2);
        myObjects.add(myObject3);

        return myObjects;
    }

    public static class MyObject {
        private String name;
        private String email;
        private Integer id;
        private String phone;

        //getters and setters
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks @Eritrean, this is what I was looking for. I think the way you added the for loop to match the column with the right data is what I was not able to figure out.
if I want this file to be downloaded on the browser by converting this to a Spring boot app, how do I do that? I want this to be an API that user calls and are able to download the CSV on their browser in my application.
@Mohit224 You're welcome. Happy to help. It would go beyond the scope of this thread and would generate a lot of effort to answer this question, which could serve as a template for a springboot application. Try it out yourself and ask specific questions if you get stuck. Here: spring-boot-download-csv-file someone has made his work available. You can learn a lot from it, e.g. adapt the controller, service and your model a little bit to fit your requirements.
0

Don't use Reflection at all.

You can use lambda and avoid it: I will take @Editrean answer' code as base:

List<MyObject> myObjects = createSampleList();
List<Function<MyObject, String>> extractors = new ArrayList<>();
extractors.add(MyObject::getName); // or o -> o.name if you don't want getter
extractors.add(MyObject::getId); // or MyObject::id if you use records
extractors.add(MyObject::getEmail);
extractors.add(MyObject::getPhone);

List<String[]> csvData = new ArrayList<>();
csvData.add(new String[] {"User Name", "Id", "User Email", "User Phone"});
for (MyObject myObject : myObjects) {
  row.add(extractors.stream()
     .map(fn -> fn.apply(myObject)) 
     .toArray(String[]::new));
}

This requires you creating getters (which you IDE will do for you) unless you use Java 17 record:

public record MyObject(String name, String email, Integer id, String phone) {}

The idea is simple:

  1. The header (first row) is populated using an array directly.
  2. Each object row is created using the lambda stored in extractors
  3. The rest of the code is same as @Editrean answer but perhaps you could also avoid array by checking CSVWriter javadoc which may have other alternative (eg: method like addCell(), newRow(), etc)

If you can't use CSVWriter, you can also use Collectors::joining which is more or less what you were doing using StringBuilder:

try (BufferedWriter bw = Files.newBufferedWriter("test.csv")) {
  bw.append(Stream.of("User Name", "Id", "User Email", "User Phone").collect(joining(","));
  bw.newLine();
  for (MyObject myObject : myObjects) {
    bw.append(extractors.stream()
                        .map(fn -> fn.apply(myObject))
                        .collect(joining(",")));
    bw.newLine();
  }
}

In any case, you would also need to properly encode each cell, especially if they may contains the delimiter (, or new line) which CSVWriter does for you.

Any way, As you can see, there are no need for Reflection due to the lambda.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.