Create a Java StringBuffer from a Map and Java Object

104 views Asked by At

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.

2

There are 2 answers

3
Eritrean On BEST ANSWER

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
    }
}
0
NoDataFound On

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.