OpenCSV: How to create CSV file from POJO with custom column headers and custom column positions?

后端 未结 19 1454
温柔的废话
温柔的废话 2020-12-08 04:14

I have created a MappingsBean class where all the columns of the CSV file are specified. Next I parse XML files and create a list of mappingbeans. Then I write that data int

相关标签:
19条回答
  • 2020-12-08 04:59

    The following works for me to map a POJO to a CSV file with custom column positioning and custom column headers (tested with opencsv-5.0) :

    public class CustomBeanToCSVMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
    
        @Override
        public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
    
            String[] headersAsPerFieldName = getFieldMap().generateHeader(bean); // header name based on field name
    
            String[] header = new String[headersAsPerFieldName.length];
    
            for (int i = 0; i <= headersAsPerFieldName.length - 1; i++) {
    
                BeanField beanField = findField(i);
    
                String columnHeaderName = extractHeaderName(beanField); // header name based on @CsvBindByName annotation
    
                if (columnHeaderName.isEmpty()) // No @CsvBindByName is present
                    columnHeaderName = headersAsPerFieldName[i]; // defaults to header name based on field name
    
                header[i] = columnHeaderName;
            }
    
            headerIndex.initializeHeaderIndex(header);
    
            return header;
        }
    
        private String extractHeaderName(final BeanField beanField) {
            if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) {
                return StringUtils.EMPTY;
            }
    
            final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
            return bindByNameAnnotation.column();
        }
    }
    

    Pojo

    Column Positioning in the generated CSV file:

    • The column positioing in the generated CSV file will be as per the annotation @CsvBindByPosition

    Header name in the generated CSV file:

    • If the field has @CsvBindByName, the generated header will be as per the annonation
    • If the field doesn't have @CsvBindByName, then the generated header will be as per the field name

      @Getter @Setter @ToString
      public class Pojo {
      
          @CsvBindByName(column="Voucher Series") // header: "Voucher Series"
          @CsvBindByPosition(position=0)
          private String voucherSeries;
      
          @CsvBindByPosition(position=1) // header: "salePurchaseType"
          private String salePurchaseType;
      }
      

    Using the above Custom Mapping Strategy:

    CustomBeanToCSVMappingStrategy<Pojo> mappingStrategy = new CustomBeanToCSVMappingStrategy<>();
                mappingStrategy.setType(Pojo.class);
    
    StatefulBeanToCsv<Pojo> beanToCsv = new StatefulBeanToCsvBuilder<Pojo>(writer)
                        .withSeparator(CSVWriter.DEFAULT_SEPARATOR)
                        .withMappingStrategy(mappingStrategy)
                        .build();
    
    beanToCsv.write(pojoList);
    
    0 讨论(0)
  • 2020-12-08 05:00

    CustomMappingStrategy for generic class.

    public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
       @Override
       public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
    
           super.setColumnMapping(new String[ FieldUtils.getAllFields(bean.getClass()).length]);
           final int numColumns = findMaxFieldIndex();
           if (!isAnnotationDriven() || numColumns == -1) {
               return super.generateHeader(bean);
           }
    
           String[] header = new String[numColumns + 1];
    
           BeanField<T> 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> beanField) {
           if (beanField == null || beanField.getField() == null
                   || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) {
               return StringUtils.EMPTY;
           }
    
           final CsvBindByName bindByNameAnnotation = beanField.getField()
                   .getDeclaredAnnotationsByType(CsvBindByName.class)[0];
           return bindByNameAnnotation.column();
       }
    }
    

    POJO Class

     public class Customer{
    
         @CsvBindByPosition(position=1)
         @CsvBindByName(column="CUSTOMER", required = true)
         private String customer;
    }
    

    Client Class

     List<T> data = getEmployeeRecord();
    CustomMappingStrategy custom = new CustomMappingStrategy();
    custom.setType(Employee.class);
    StatefulBeanToCsv<T> writer = new StatefulBeanToCsvBuilder<T>(response.getWriter())
                    .withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
                    .withSeparator('|')
                    .withOrderedResults(false)
                    .withMappingStrategy(custom)
                    .build();
            writer.write(reportData);
    
    0 讨论(0)
  • 2020-12-08 05:03

    I've had similar problem. AFAIK there is no build-in functionality in OpenCSV that will allow to write bean to CSV with custom column names and ordering.

    There are two main MappingStrategyies that are available in OpenCSV out of the box:

    • HeaderColumnNameMappingStrategy: that allows to map CVS file columns to bean fields based on custom name; when writing bean to CSV this allows to change column header name but we have no control on column order
    • ColumnPositionMappingStrategy: that allows to map CSV file columns to bean fields based on column ordering; when writing bean to CSV we can control column order but we get an empty header (implementation returns new String[0] as a header)

    The only way I found to achieve both custom column names and ordering is to write your custom MappingStrategy.

    First solution: fast and easy but hardcoded

    Create custom MappingStrategy:

    class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
        private static final String[] HEADER = new String[]{"TradeID", "GWML GUID", "MXML GUID", "GWML File", "MxML File", "MxML Counterparty", "GWML Counterparty"};
    
        @Override
        public String[] generateHeader() {
            return HEADER;
        }
    }
    

    And use it in StatefulBeanToCsvBuilder:

    final CustomMappingStrategy<MappingsBean> mappingStrategy = new CustomMappingStrategy<>();
    mappingStrategy.setType(MappingsBean.class);
    
    final StatefulBeanToCsv<MappingsBean> beanToCsv = new StatefulBeanToCsvBuilder<MappingsBean>(writer)
        .withMappingStrategy(mappingStrategy)
        .build();
    beanToCsv.write(makeFinalMappingBeanList());
    writer.close()
    

    In MappingsBean class we left CsvBindByPosition annotations - to control ordering (in this solution CsvBindByName annotations are not needed). Thanks to custom mapping strategy the header column names are included in resulting CSV file.

    The downside of this solution is that when we change column ordering through CsvBindByPosition annotation we have to manually change also HEADER constant in our custom mapping strategy.

    Second solution: more flexible

    The first solution works, but it was not good for me. Based on build-in implementations of MappingStrategy I came up with yet another implementation:

    class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
        @Override
        public String[] generateHeader() {
            final int numColumns = findMaxFieldIndex();
            if (!isAnnotationDriven() || numColumns == -1) {
                return super.generateHeader();
            }
    
            header = new String[numColumns + 1];
    
            BeanField 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 beanField) {
            if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) {
                return StringUtils.EMPTY;
            }
    
            final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
            return bindByNameAnnotation.column();
        }
    }
    

    You can use this custom strategy in StatefulBeanToCsvBuilder exactly this same as in the first solution (remember to invoke mappingStrategy.setType(MappingsBean.class);, otherwise this solution will not work).

    Currently our MappingsBean has to contain both CsvBindByName and CsvBindByPosition annotations. The first to give header column name and the second to create ordering of columns in the output CSV header. Now if we change (using annotations) either column name or ordering in MappingsBean class - that change will be reflected in output CSV file.

    0 讨论(0)
  • 2020-12-08 05:08

    I've improved on previous answers by removing all references to deprecated APIs while using the latest release of opencsv (4.6).

    A Generic Kotlin Solution

    /**
     * Custom OpenCSV [ColumnPositionMappingStrategy] that allows for a header line to be generated from a target CSV
     * bean model class using the following annotations when present:
     * * [CsvBindByName]
     * * [CsvCustomBindByName]
     */
    class CustomMappingStrategy<T>(private val beanType: Class<T>) : ColumnPositionMappingStrategy<T>() {
        init {
            setType(beanType)
            setColumnMapping(*getAnnotatedFields().map { it.extractHeaderName() }.toTypedArray())
        }
    
        override fun generateHeader(bean: T): Array<String> = columnMapping
    
        private fun getAnnotatedFields() = beanType.declaredFields.filter { it.isAnnotatedByName() }.toList()
    
        private fun Field.isAnnotatedByName() = isAnnotationPresent(CsvBindByName::class.java) || isAnnotationPresent(CsvCustomBindByName::class.java)
    
        private fun Field.extractHeaderName() =
            getAnnotation(CsvBindByName::class.java)?.column ?: getAnnotation(CsvCustomBindByName::class.java)?.column ?: EMPTY
    }
    

    Then use it as follows:

    private fun csvBuilder(writer: Writer) =
        StatefulBeanToCsvBuilder<MappingsBean>(writer)
            .withSeparator(ICSVWriter.DEFAULT_SEPARATOR)
            .withMappingStrategy(CustomMappingStrategy(MappingsBean::class.java))
            .withApplyQuotesToAll(false)
            .build()
    
    // Kotlin try-with-resources construct
    PrintWriter(File("$reportOutputDir/$REPORT_FILENAME")).use { writer ->
        csvBuilder(writer).write(makeFinalMappingBeanList())
    }
    

    and for completeness, here's the CSV bean as a Kotlin data class:

    data class MappingsBean(
        @field:CsvBindByName(column = "TradeID")
        @field:CsvBindByPosition(position = 0, required = true)
        private val tradeId: String,
    
        @field:CsvBindByName(column = "GWML GUID", required = true)
        @field:CsvBindByPosition(position = 1)
        private val gwmlGUID: String,
    
        @field:CsvBindByName(column = "MXML GUID", required = true)
        @field:CsvBindByPosition(position = 2)
        private val mxmlGUID: String,
    
        @field:CsvBindByName(column = "GWML File")
        @field:CsvBindByPosition(position = 3)
        private val gwmlFile: String? = null,
    
        @field:CsvBindByName(column = "MxML File")
        @field:CsvBindByPosition(position = 4)
        private val mxmlFile: String? = null,
    
        @field:CsvBindByName(column = "MxML Counterparty")
        @field:CsvBindByPosition(position = 5)
        private val mxmlCounterParty: String? = null,
    
        @field:CsvBindByName(column = "GWML Counterparty")
        @field:CsvBindByPosition(position = 6)
        private val gwmlCounterParty: String? = null
    )
    
    0 讨论(0)
  • 2020-12-08 05:08

    If you dont have getDeclaredAnnotationsByType method, but need the name of your original field name:

    beanField.getField().getName()

    public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
    @Override
    public String[] generateHeader() {
        final int numColumns = findMaxFieldIndex();
        if (!isAnnotationDriven() || numColumns == -1) {
            return super.generateHeader();
        }
    
        header = new String[numColumns + 1];
    
        BeanField 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 beanField) {
        if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotations().length == 0) {
            return StringUtils.EMPTY;
        }
        return beanField.getField().getName();
    }
    

    }

    0 讨论(0)
  • 2020-12-08 05:09

    The following solution works with opencsv 5.0.

    First, you need to inherit ColumnPositionMappingStrategy class and override generateHeader method to create your custom header for utilizing both CsvBindByName and CsvBindByPosition annotations as shown below.

    import com.opencsv.bean.BeanField;
    import com.opencsv.bean.ColumnPositionMappingStrategy;
    import com.opencsv.bean.CsvBindByName;
    import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
    
    /**
     * @param <T>
     */
    class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
        /*
         * (non-Javadoc)
         * 
         * @see com.opencsv.bean.ColumnPositionMappingStrategy#generateHeader(java.lang.
         * Object)
         */
        @Override
        public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
            final int numColumns = getFieldMap().values().size();
            if (numColumns == -1) {
                return super.generateHeader(bean);
            }
    
            String[] header = new String[numColumns];
            super.setColumnMapping(header);
    
            BeanField<T, Integer> beanField;
            for (int i = 0; i < numColumns; i++) {
                beanField = findField(i);
                String columnHeaderName = beanField.getField().getDeclaredAnnotation(CsvBindByName.class).column();
                header[i] = columnHeaderName;
            }
            return header;
        }
    }
    

    The next step is to use this mapping strategy while writing a bean to CSV as below.

    CustomMappingStrategy<ScanReport> strategy = new CustomMappingStrategy<>();
                strategy.setType(ScanReport.class);
    
    // Write a bean to csv file.
    StatefulBeanToCsv<ScanReport> beanToCsv = new StatefulBeanToCsvBuilder<ScanReport>(writer)
                        .withMappingStrategy(strategy).build();
    beanToCsv.write(beanList);
    
    0 讨论(0)
提交回复
热议问题