Search code examples
opencsv

With OpenCSV, how do I append to existing CSV using a MappingStrategy?


With OpenCSV, how do I append to existing CSV using a MappingStrategy? There are lots of examples I could find where NOT using a Bean mapping stategy BUT I like the dynamic nature of the column mapping with bean strategy and would like to get it working this way. Here is my code, which just rewrites the single line to CSV file instead of appending.

How can I fix this? Using OpenCSV 4.5 . Note: I set my FileWriter for append=true . This scenario is not working as I expected. Re-running this method simply results in over-writing the entire file with a header and a single row.

public void addRowToCSV(PerfMetric rowData) {
    File file = new File(PerfTestMetric.CSV_FILE_PATH);
    try {
        CSVWriter writer = new CSVWriter(new FileWriter(file, true));

        CustomCSVMappingStrategy<PerfMetric> mappingStrategy 
          = new CustomCSVMappingStrategy<>();
        mappingStrategy.setType(PerfMetric.class);

        StatefulBeanToCsv<PerfMetric> beanToCsv 
           = new StatefulBeanToCsvBuilder<PerfMetric>(writer)
            .withMappingStrategy(mappingStrategy)
            .withSeparator(',')
            .withApplyQuotesToAll(false)
            .build();

        try {
            beanToCsv.write(rowData);
        } catch (CsvDataTypeMismatchException e) {
            e.printStackTrace();
        } catch (CsvRequiredFieldEmptyException e) {
            e.printStackTrace();
        }
        writer.flush();
        writer.close();
    } catch (IOException e) {
            e.printStackTrace();
    }
}

Or, is the usual pattern to load all rows into a List and then re-write entire file? I was able to get it to work by writing two MappingStrategy mapping strategies and then conditionally using them with a if-file-exists but doing it that way leaves me with a "Unchecked assignment" warning in my code. Not ideal; hoping for an elegant solution?


Solution

  • I've updated OpenCSV to version 5.1 and got it working. In my case I needed the CSV headers to have a specific name and position, so I'm using both @CsvBindByName and @CsvBindByPosition, and needed to create a custom MappingStrategy to get it working.

    Later, I needed to edit the MappingStrategy to enable appending, so when it's in Appending mode I don't need to generate a CSV header.

    public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
        private boolean useHeader=true;
    
        public CustomMappingStrategy(){
        }
    
        public CustomMappingStrategy(boolean useHeader) {
            this.useHeader = useHeader;
        }
    
        @Override
        public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
            final int numColumns = FieldUtils.getAllFields(bean.getClass()).length;
            super.setColumnMapping(new String[numColumns]);
    
            if (numColumns == -1) {
                return super.generateHeader(bean);
            }
    
            String[] header = new String[numColumns];
    
            if(!useHeader){
                return ArrayUtils.EMPTY_STRING_ARRAY;
            }
            BeanField<T, Integer> beanField;
            for (int i = 0; i < numColumns; i++){
                beanField = findField(i);
                String columnHeaderName = extractHeaderName(beanField);
                header[i] = columnHeaderName;
            }
    
            return header;
        }
    
        private String extractHeaderName(final BeanField<T, Integer> beanField){
            if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0){
                return StringUtils.EMPTY;
            }
    
            //return value of CsvBindByName annotation
            final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
            return bindByNameAnnotation.column();
        }
    
    }
    

    Now if you use the default constructor it'll add the header to the generated CSV, and using a boolean you can tell it to add a header or to ignore it.