20

While writing Beans to CSV file by using OpenCSV 4.6, all the headers are changing to uppercase. Eventhough bean has @CsvBindByName annotation it is changing to uppercase.

Java Bean:

public class ProjectInfo implements Serializable {

    @CsvBindByName(column = "ProjectName",required = true)
    private String projectName;

    @CsvBindByName(column = "ProjectCode",required = true)
    private String projectCode;

    @CsvBindByName(column = "Visibility",required = true)
    private String visibility;
    //setters and getters
}

Main method

public static void main(String[] args) throws IOException {
    Collection<Serializable> projectInfos = getProjectsInfo();
    try(BufferedWriter writer = new BufferedWriter(new FileWriter("test.csv"))){
        StatefulBeanToCsvBuilder builder = new StatefulBeanToCsvBuilder(writer);
        StatefulBeanToCsv beanWriter = builder
                    .withSeparator(';')
                    .build();
        try {
              beanWriter.write(projectInfos.iterator());
              writer.flush();

         } catch (CsvDataTypeMismatchException | CsvRequiredFieldEmptyException  e) {
                throw new RuntimeException("Failed to download admin file");
            }
        }

    }

Expected Result:

"ProjectCode";"ProjectName";"Visibility"
"ANY";"Country DU";"1"
"STD";"Standard";"1"
"TST";"Test";"1"
"CMM";"CMMTest";"1"

Acutal Result:

"PROJECTCODE";"PROJECTNAME";"VISIBILITY"
"ANY";"Country DU";"1"
"STD";"Standard";"1"
"TST";"Test";"1"
"CMM";"CMMTest";"1"

I don't have option to use ColumnMappingStrategy because I have to build this method as a generic solution. can anyone suggest me how to write the headers as it is?

gprasadr8
  • 789
  • 1
  • 8
  • 12

4 Answers4

12

It happens, because the code in HeaderColumnNameMappingStrategy uses toUpperCase() for storing and retrieving the field names.

You could use the HeaderColumnNameTranslateMappingStrategy instead and create the mapping by reflection.

    
    public class AnnotationStrategy extends HeaderColumnNameTranslateMappingStrategy
    {
        public AnnotationStrategy(Class<?> clazz)
        {
            Map<String,String> map=new HashMap<>();
            //To prevent the column sorting
            List<String> originalFieldOrder=new ArrayList<>();
            for(Field field:clazz.getDeclaredFields())
            {
                CsvBindByName annotation = field.getAnnotation(CsvBindByName.class);
                if(annotation!=null)
                {
                    map.put(annotation.column(),annotation.column());
                    originalFieldOrder.add(annotation.column());
                }
            }
            setType(clazz);
            setColumnMapping(map);
            //Order the columns as they were created
            setColumnOrderOnWrite((a,b) -> Integer.compare(originalFieldOrder.indexOf(a), originalFieldOrder.indexOf(b)));
        }
        
        @Override
        public String[] generateHeader(Object bean) throws CsvRequiredFieldEmptyException
        {
            String[] result=super.generateHeader(bean);
            for(int i=0;i<result.length;i++)
            {
                result[i]=getColumnName(i);
            }
            return result;
        }
    }

And, assuming that there is only one class of items (and always at least one item), the creation of beanWriter has to be expanded:

StatefulBeanToCsv beanWriter = builder.withSeparator(';')
    .withMappingStrategy(new AnnotationStrategy(projectInfos.iterator().next().getClass()))
    .build();
Sascha
  • 1,320
  • 10
  • 16
  • Can I specify by using annotations at field level? – gprasadr8 May 16 '19 at 12:44
  • Look at [this solution](https://stackoverflow.com/questions/33778223/opencsv-writes-wrong-column-names-with-beantocsv-headercolumnnametranslatemapp), it's the inverse problem of yours. – Sascha May 16 '19 at 12:51
  • In that solution they know which headers to map for example 'COLUMN1'->'name', but here I will get headers dynamically so I can't create map. – gprasadr8 May 16 '19 at 13:08
  • if the field names are not as same as column names in @CsvBindByName then we have to change the map to take key and value both should be **annotation.column()**. If we use **annotation.column()** for both key and value then it will solve all the problems. Thanks a lot your answer, it helped me a lot. – gprasadr8 May 21 '19 at 11:45
  • I have used your solution it is useful to me like it has converted headers in lower case but it also sorted my headers in ASC order I dont want to change order of my headers.. can you please help me with this – Tirth Timaniya Aug 27 '21 at 07:25
  • @TirthTimaniya I've added some code to prevent the auto sorting. It's untested, so it may need some additional care. – Sascha Aug 28 '21 at 08:36
9

Actually, HeaderColumnNameMappingStrategy uses toUpperCase() for storing and retrieving the field names. In order to use custom field name you have to annotate you field with @CsvBindByName

@CsvBindByName(column = "Partner Code" )
private String partnerCode;

By default it will be capitalized to PARTNER CODE because of the above reason. so, in order to take control over it we have to write a class implementing HeaderColumnNameTranslateMappingStrategy. With csv 5.0 and java8 i have implemented like this

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.HeaderColumnNameTranslateMappingStrategy;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

public class AnnotationStrategy<T> extends HeaderColumnNameTranslateMappingStrategy<T> {
    Map<String, String> columnMap = new HashMap<>();
    public AnnotationStrategy(Class<? extends T> clazz) {

        for (Field field : clazz.getDeclaredFields()) {
            CsvBindByName annotation = field.getAnnotation(CsvBindByName.class);
            if (annotation != null) {

                    columnMap.put(field.getName().toUpperCase(), annotation.column());
            }
        }
        setType(clazz);      
    }

    @Override
    public String getColumnName(int col) {
        String name = headerIndex.getByPosition(col);
        return name;
    }

    public String getColumnName1(int col) {
        String name = headerIndex.getByPosition(col);
        if(name != null) {
            name = columnMap.get(name);
        }
        return name;
    }
    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
        String[] result = super.generateHeader(bean);
        for (int i = 0; i < result.length; i++) {
            result[i] = getColumnName1(i);
        }
        return result;
    }
}
Neo Ravi
  • 399
  • 5
  • 11
  • I have used your solution it is useful to me like it has converted headers in lower case but it also sorted my headers in ASC order I dont want to change order of my headers.. can you please help me with this – Tirth Timaniya Aug 27 '21 at 07:24
  • @TirthTimaniya, I just saw your comment. Hope you have got the solution – Neo Ravi Aug 01 '22 at 04:28
1

I have tried other solutions but it doesn't work when the property name and column name are not the same.

I am using 5.6. My solution is to reuse the strategy.

public class CsvRow {

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

    // Property name and column name are different
    @CsvBindByName(column = "country_code")
    private String countryCode;

}
// We are going to reuse this strategy
HeaderColumnNameMappingStrategy<CsvRow> strategy = new HeaderColumnNameMappingStrategy<>();
strategy.setType(CsvRow.class);

// Build the header line which respects the declaration order
// So its value will be "id,country_code"
String headerLine = Arrays.stream(CsvRow.class.getDeclaredFields())
        .map(field -> field.getAnnotation(CsvBindByName.class))
        .filter(Objects::nonNull)
        .map(CsvBindByName::column)
        .collect(Collectors.joining(","));

// Let the library to initialize column details in the strategy
try (StringReader stringReader = new StringReader(headerLine);
        CSVReader reader = new CSVReader(stringReader)) {
    CsvToBean<CsvRow> csv = new CsvToBeanBuilder<CsvRow>(reader)
            .withType(CsvRow.class)
            .withMappingStrategy(strategy)
            .build();
    for (CsvRow csvRow : csv) {}
}

The strategy is ready for writing csv file.

try (OutputStream outputStream = Files.newOutputStream(Path.of("test.csv"));
    OutputStreamWriter writer = new OutputStreamWriter(outputStream)) {

    StatefulBeanToCsv<CsvRow> csv = new StatefulBeanToCsvBuilder<CsvRow>(writer)
        .withMappingStrategy(strategy)
        .withThrowExceptions(true)
        .build();

    csv.write(csvRows);
}
Franz Wong
  • 1,024
  • 1
  • 10
  • 32
0

Using opencsv 5.0 and Java 8, I had to modify AnnotationStrategy class code as follows to had it compiled :

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.HeaderColumnNameTranslateMappingStrategy;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

public class AnnotationStrategy<T> extends HeaderColumnNameTranslateMappingStrategy<T> {
    public AnnotationStrategy(Class<? extends T> clazz) {
        Map<String, String> map = new HashMap<>();
        for (Field field : clazz.getDeclaredFields()) {
            CsvBindByName annotation = field.getAnnotation(CsvBindByName.class);
            if (annotation != null) {
                map.put(annotation.column(), annotation.column());
            }
        }
        setType(clazz);
        setColumnMapping(map);
    }

    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
        String[] result = super.generateHeader(bean);
        for (int i = 0; i < result.length; i++) {
            result[i] = getColumnName(i);
        }
        return result;
    }
}