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

JavaXmlCsvOpencsv

Java Problem Overview


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 into CSV file as report.

I am using following annotations:

public class MappingsBean {

    @CsvBindByName(column = "TradeID")
    @CsvBindByPosition(position = 0)
    private String tradeId;

    @CsvBindByName(column = "GWML GUID", required = true)
    @CsvBindByPosition(position = 1)
    private String gwmlGUID;

    @CsvBindByName(column = "MXML GUID", required = true)
    @CsvBindByPosition(position = 2)
    private String mxmlGUID;

    @CsvBindByName(column = "GWML File")
    @CsvBindByPosition(position = 3)
    private String gwmlFile;

    @CsvBindByName(column = "MxML File")
    @CsvBindByPosition(position = 4)
    private String mxmlFile;

    @CsvBindByName(column = "MxML Counterparty")
    @CsvBindByPosition(position = 5)
    private String mxmlCounterParty;

    @CsvBindByName(column = "GWML Counterparty")
    @CsvBindByPosition(position = 6)
    private String gwmlCounterParty;
}

And then I use StatefulBeanToCsv class to write into CSV file:

File reportFile = new File(reportOutputDir + "/" + REPORT_FILENAME);
Writer writer = new PrintWriter(reportFile);
StatefulBeanToCsv<MappingsBean> beanToCsv = new 
                              StatefulBeanToCsvBuilder(writer).build();
beanToCsv.write(makeFinalMappingBeanList());
writer.close();
 

The problem with this approach is that if I use @CsvBindByPosition(position = 0) to control position then I am not able to generate column names. If I use @CsvBindByName(column = "TradeID") then I am not able to set position of the columns.

Is there a way where I can use both annotations, so that I can create CSV files with column headers and also control column position?

Regards, Vikram Pathania

Java Solutions


Solution 1 - Java

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.

Solution 2 - Java

Corrected above answer to match with newer version.

package csvpojo;

import org.apache.commons.lang3.StringUtils;

import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

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();
	}
}

Then call this to generate CSV. I have used Visitors as my POJO to populate, update wherever necessary.

	    CustomMappingStrategy<Visitors> mappingStrategy = new CustomMappingStrategy<>();
		mappingStrategy.setType(Visitors.class);
		// writing sample
		List<Visitors> beans2 = new ArrayList<Visitors>();

		Visitors v = new Visitors();
		v.set_1_firstName(" test1");
		v.set_2_lastName("lastname1");
		v.set_3_visitsToWebsite("876");
		beans2.add(v);

		v = new Visitors();
		v.set_1_firstName(" firstsample2");
		v.set_2_lastName("lastname2");
		v.set_3_visitsToWebsite("777");
		beans2.add(v);

		Writer writer = new FileWriter("G://output.csv");
		StatefulBeanToCsv<Visitors> beanToCsv = new StatefulBeanToCsvBuilder<Visitors>(writer)
				.withMappingStrategy(mappingStrategy).withSeparator(',').withApplyQuotesToAll(false).build();
		beanToCsv.write(beans2);
		writer.close();

My bean annotations looks like this

 @CsvBindByName (column = "First Name", required = true)
 @CsvBindByPosition(position=1)
 private String firstName;


 @CsvBindByName (column = "Last Name", required = true)
 @CsvBindByPosition(position=0)
 private String lastName;

Solution 3 - Java

I wanted to achieve bi-directional import/export - to be able to import generated CSV back to POJO and visa versa.

I was not able to use @CsvBindByPosition for this, because in this case - ColumnPositionMappingStrategy was selected automatically. Per documents: this strategy requires that the file does NOT have a header.

#What I've used to achieve the goal:#

HeaderColumnNameMappingStrategy
mappingStrategy.setColumnOrderOnWrite(Comparator<String> writeOrder)

CsvUtils to read/write csv

import com.opencsv.CSVWriter;
import com.opencsv.bean.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.List;

public class CsvUtils {
    private CsvUtils() {
    }

    public static <T> String convertToCsv(List<T> entitiesList, MappingStrategy<T> mappingStrategy) throws Exception {
        try (Writer writer = new StringWriter()) {
            StatefulBeanToCsv<T> beanToCsv = new StatefulBeanToCsvBuilder<T>(writer)
                    .withMappingStrategy(mappingStrategy)
                    .withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
                    .build();
            beanToCsv.write(entitiesList);
            return writer.toString();
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> List<T> convertFromCsv(MultipartFile file, Class clazz) throws IOException {
        try (Reader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
            CsvToBean<T> csvToBean = new CsvToBeanBuilder<T>(reader).withType(clazz).build();
            return csvToBean.parse();
        }
    }
}

POJO for import/export

public class LocalBusinessTrainingPairDTO {
    //this is used for CSV columns ordering on exporting LocalBusinessTrainingPairs
    public static final String[] FIELDS_ORDER = {"leftId", "leftName", "rightId", "rightName"};

    @CsvBindByName(column = "leftId")
    private int leftId;

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

    @CsvBindByName(column = "rightId")
    private int rightId;

    @CsvBindByName(column = "rightName")
    private String rightName;
    // getters/setters omitted, do not forget to add them
}

Custom comparator for predefined String ordering:

public class OrderedComparatorIgnoringCase implements Comparator<String> {
    private List<String> predefinedOrder;

    public OrderedComparatorIgnoringCase(String[] predefinedOrder) {
        this.predefinedOrder = new ArrayList<>();
        for (String item : predefinedOrder) {
            this.predefinedOrder.add(item.toLowerCase());
        }
    }

    @Override
    public int compare(String o1, String o2) {
        return predefinedOrder.indexOf(o1.toLowerCase()) - predefinedOrder.indexOf(o2.toLowerCase());
    }
}

Ordered writing for POJO (answer to initial question)

public static void main(String[] args) throws Exception {
     List<LocalBusinessTrainingPairDTO> localBusinessTrainingPairsDTO = new ArrayList<>();
     LocalBusinessTrainingPairDTO localBusinessTrainingPairDTO = new LocalBusinessTrainingPairDTO();
     localBusinessTrainingPairDTO.setLeftId(1);
     localBusinessTrainingPairDTO.setLeftName("leftName");
     localBusinessTrainingPairDTO.setRightId(2);
     localBusinessTrainingPairDTO.setRightName("rightName");

     localBusinessTrainingPairsDTO.add(localBusinessTrainingPairDTO);

     //Creating HeaderColumnNameMappingStrategy
     HeaderColumnNameMappingStrategy<LocalBusinessTrainingPairDTO> mappingStrategy = new HeaderColumnNameMappingStrategy<>();
     mappingStrategy.setType(LocalBusinessTrainingPairDTO.class);
     //Setting predefined order using String comparator
     mappingStrategy.setColumnOrderOnWrite(new OrderedComparatorIgnoringCase(LocalBusinessTrainingPairDTO.FIELDS_ORDER));
     String csv = convertToCsv(localBusinessTrainingPairsDTO, mappingStrategy);
     System.out.println(csv);
}

Read exported CSV back to POJO (addition to original answer)

Important: CSV can be unordered, as we are still using binding by name:

public static void main(String[] args) throws Exception {
    //omitted code from writing
    String csv = convertToCsv(localBusinessTrainingPairsDTO, mappingStrategy);

    //Exported CSV should be compatible for further import
    File temp = File.createTempFile("tempTrainingPairs", ".csv");
    temp.deleteOnExit();
    BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
    bw.write(csv);
    bw.close();
    MultipartFile multipartFile = new MockMultipartFile("tempTrainingPairs.csv", new FileInputStream(temp));

    List<LocalBusinessTrainingPairDTO> localBusinessTrainingPairDTOList = convertFromCsv(multipartFile, LocalBusinessTrainingPairDTO.class);
}

#To conclude:#

  1. We can read CSV to POJO, regardless of column order - because we are using @CsvBindByName
  2. We can control columns order on write using custom comparator

Solution 4 - Java

In the latest version the solution of @Sebast26 does no longer work. However the basic is still very good. Here is a working solution with v5.0

import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
import org.apache.commons.lang3.StringUtils;

class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
        final int numColumns = getFieldMap().values().size();
        super.generateHeader(bean);

        String[] header = new String[numColumns];

        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();
    }
}

And the model looks like this:

@CsvBindByName(column = "id")
@CsvBindByPosition(position = 0)
private Long id;
@CsvBindByName(column = "name")
@CsvBindByPosition(position = 1)
private String name;

And my generation helper looks something like this:

public static <T extends AbstractCsv> String createCsv(List<T> data, Class<T> beanClazz) {
    CustomMappingStrategy<T> mappingStrategy = new CustomMappingStrategy<T>();
    mappingStrategy.setType(beanClazz);

    StringWriter writer = new StringWriter();
    String csv = "";
    try {
        StatefulBeanToCsv sbc = new StatefulBeanToCsvBuilder(writer)
                .withSeparator(';')
                .withMappingStrategy(mappingStrategy)
                .build();
        sbc.write(data);
        csv = writer.toString();
    } catch (CsvRequiredFieldEmptyException e) {
        // TODO add some logging...
    } catch (CsvDataTypeMismatchException e) {
        // TODO add some logging...
    } finally {
        try {
            writer.close();
        } catch (IOException e) {
        }
    }
    return csv;
}

Solution 5 - Java

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);

Solution 6 - Java

thanks for this thread, it has been really useful for me... I've enhanced a little bit the provided solution in order to accept also POJO where some fields are not annotated (not meant to be read/written):

public class ColumnAndNameMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {

@Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {

    super.setColumnMapping(new String[ getAnnotatedFields(bean)]);
    final int numColumns = getAnnotatedFields(bean);
    final int totalFieldNum = findMaxFieldIndex();
    if (!isAnnotationDriven() || numColumns == -1) {
        return super.generateHeader(bean);
    }

    String[] header = new String[numColumns];

    BeanField<T> beanField;
    for (int i = 0; i <= totalFieldNum; i++) {
        beanField = findField(i);
        if (isFieldAnnotated(beanField.getField())) {
            String columnHeaderName = extractHeaderName(beanField);
            header[i] = columnHeaderName;
        }
    }
    return header;
}

private int getAnnotatedFields(T bean) {
    return (int) Arrays.stream(FieldUtils.getAllFields(bean.getClass()))
            .filter(this::isFieldAnnotated)
            .count();
}

private boolean isFieldAnnotated(Field f) {
    return f.isAnnotationPresent(CsvBindByName.class) || f.isAnnotationPresent(CsvCustomBindByName.class);
}

private String extractHeaderName(final BeanField beanField) {
    if (beanField == null || beanField.getField() == null) {
        return StringUtils.EMPTY;
    }

    Field field = beanField.getField();

    if (field.getDeclaredAnnotationsByType(CsvBindByName.class).length != 0) {
        final CsvBindByName bindByNameAnnotation = field.getDeclaredAnnotationsByType(CsvBindByName.class)[0];
        return bindByNameAnnotation.column();
    }

    if (field.getDeclaredAnnotationsByType(CsvCustomBindByName.class).length != 0) {
        final CsvCustomBindByName bindByNameAnnotation = field.getDeclaredAnnotationsByType(CsvCustomBindByName.class)[0];
        return bindByNameAnnotation.column();
    }

    return StringUtils.EMPTY;
}

}

Solution 7 - Java

If you're only interested in sorting the CSV columns based on the order in which member variables appear in your model class (CsvRow row in this example), then you can use a Comparator implementation to solve this in a rather simple manner. Here's an example that does this in Kotlin:

class ByMemberOrderCsvComparator : Comparator<String> {

    private val memberOrder by lazy {
        FieldUtils.getAllFields(CsvRow::class.java)
                .map { it.getDeclaredAnnotation(CsvBindByName::class.java) }
                .map { it?.column ?: "" }
                .map { it.toUpperCase(Locale.US) } // OpenCSV UpperCases all headers, so we do this to match
    }

    override fun compare(field1: String?, field2: String?): Int {
        return memberOrder.indexOf(field1) - memberOrder.indexOf(field2)
    }

}

This Comparator does the following:

  1. Fetches each member variable field in our data class (CsvRow)
  2. Finds all the ones with the @CsvBindByName annotation (in the order you specified them in the CsvRow model)
  3. Upper cases each to match the default OpenCsv implementation

Next, apply this Comparator to your MappingStrategy, so it'll sort based off the specified order:

val mappingStrategy = HeaderColumnNameMappingStrategy<OrderSummaryCsvRow>()
mappingStrategy.setColumnOrderOnWrite(ByMemberOrderCsvComparator())
mappingStrategy.type = CsvRow::class.java
mappingStrategy.setErrorLocale(Locale.US)

val csvWriter = StatefulBeanToCsvBuilder<OrderSummaryCsvRow>(writer)
                    .withMappingStrategy(mappingStrategy)
                    .build()

For reference, here's an example CsvRow class (you'll want to replace this with your own model for your needs):

data class CsvRow(
    @CsvBindByName(column = "Column 1")
    val column1: String,

    @CsvBindByName(column = "Column 2")
    val column2: String,

    @CsvBindByName(column = "Column 3")
    val column3: String,

    // Other columns here ...
)

Which would produce a CSV as follows:

"COLUMN 1","COLUMN 2","COLUMN 3",...
"value 1a","value 2a","value 3a",...
"value 1b","value 2b","value 3b",...

The benefit of this approach is that it removes the need to hard-code any of your column names, which should greatly simplify things if you ever need to add/remove columns.

Solution 8 - Java

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);

Solution 9 - Java

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
)

Solution 10 - Java

It is a solution for version greater than 4.3:

public class MappingBean {
     @CsvBindByName(column = "column_a")
     private String columnA;
     @CsvBindByName(column = "column_b")
     private String columnB;
     @CsvBindByName(column = "column_c")
     private String columnC;

     // getters and setters
}

And use it as example:

import org.apache.commons.collections4.comparators.FixedOrderComparator;

...

var mappingStrategy = new HeaderColumnNameMappingStrategy<MappingBean>();
mappingStrategy.setType(MappingBean.class);        
mappingStrategy.setColumnOrderOnWrite(new FixedOrderComparator<>("COLUMN_C", "COLUMN_B", "COLUMN_A"));

var sbc = new StatefulBeanToCsvBuilder<MappingBean>(writer)
      .withMappingStrategy(mappingStrategy)
      .build();

Result:

column_c | column_b | column_a

Solution 11 - Java

I think the intended and most flexible way of handling the order of the header columns is to inject a comparator by HeaderColumnNameMappinStrategy.setColumnOrderOnWrite().

For me the most intuitive way was to write the CSV columns in the same order as they are specified in the CsvBean, but you can also adjust the Comparator to make use of your own annotations where you specify the order. Don´t forget to rename the Comparator class then ;)

Integration:

HeaderColumnNameMappingStrategy<MyCsvBean> mappingStrategy = new HeaderColumnNameMappingStrategy<>();
    mappingStrategy.setType(MyCsvBean.class);
    mappingStrategy.setColumnOrderOnWrite(new ClassFieldOrderComparator(MyCsvBean.class));

Comparator:

private class ClassFieldOrderComparator implements Comparator<String> {

    List<String> fieldNamesInOrderWithinClass;

    public ClassFieldOrderComparator(Class<?> clazz) {
        fieldNamesInOrderWithinClass = Arrays.stream(clazz.getDeclaredFields())
                .filter(field -> field.getAnnotation(CsvBindByName.class) != null)
              // Handle order by your custom annotation here
              //.sorted((field1, field2) -> {
              //   int field1Order = field1.getAnnotation(YourCustomOrderAnnotation.class).getOrder();
              //   int field2Order = field2.getAnnotation(YourCustomOrderAnnotation.class).getOrder();
              //   return Integer.compare(field1Order, field2Order);
              //})
                .map(field -> field.getName().toUpperCase())
                .collect(Collectors.toList());
    }

    @Override
    public int compare(String o1, String o2) {
        int fieldIndexo1 = fieldNamesInOrderWithinClass.indexOf(o1);
        int fieldIndexo2 = fieldNamesInOrderWithinClass.indexOf(o2);
        return Integer.compare(fieldIndexo1, fieldIndexo2);
    }
}

Solution 12 - Java

This can be done using a HeaderColumnNameMappingStrategy along with a custom Comparator as well. Which is recommended by the official doc http://opencsv.sourceforge.net/#mapping_strategies

    File reportFile = new File(reportOutputDir + "/" + REPORT_FILENAME);
    Writer writer = new PrintWriter(reportFile);
    
final List<String> order = List.of("TradeID", "GWML GUID", "MXML GUID", "GWML File", "MxML File", "MxML Counterparty", "GWML Counterparty");
    final FixedOrderComparator comparator = new FixedOrderComparator(order);
    HeaderColumnNameMappingStrategy<MappingsBean> strategy = new HeaderColumnNameMappingStrategy<>();
    strategy.setType(MappingsBean.class);
    strategy.setColumnOrderOnWrite(comparator);

    StatefulBeanToCsv<MappingsBean> beanToCsv = new
      StatefulBeanToCsvBuilder(writer)
      .withMappingStrategy(strategy)
      .build();
    beanToCsv.write(makeFinalMappingBeanList());
    writer.close();

Solution 13 - Java

There is another version for 5.2 version because I have a problem with @CsvCustomBindByName annotation when I tried answers above.

I defined custom annotation :

@Target(ElementType.FIELD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface CsvPosition {

  int position();
}

and custom mapping strategy

public class CustomMappingStrategy<T> extends HeaderColumnNameMappingStrategy<T> {

  private final Field[] fields;

  public CustomMappingStrategy(Class<T> clazz) {
    fields = clazz.getDeclaredFields();
    Arrays.sort(fields, (f1, f2) -> {
      CsvPosition position1 = f1.getAnnotation(CsvPosition.class);
      CsvPosition position2 = f2.getAnnotation(CsvPosition.class);
      return Integer.compare(position1.position(), position2.position());
    });
  }

  @Override
  public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
    String[] header = new String[fields.length];
    for (Field f : fields) {
      CsvPosition position = f.getAnnotation(CsvPosition.class);
      header[position.position() - 1] = getName(f);
    }
    headerIndex.initializeHeaderIndex(header);
    return header;
  }

  private String getName(Field f) {
    CsvBindByName csvBindByName = f.getAnnotation(CsvBindByName.class);
    CsvCustomBindByName csvCustomBindByName = f.getAnnotation(CsvCustomBindByName.class);
    return csvCustomBindByName != null
      ? csvCustomBindByName.column() == null || csvCustomBindByName.column().isEmpty() ? f.getName() : csvCustomBindByName.column()
      : csvBindByName.column() == null || csvBindByName.column().isEmpty() ? f.getName() : csvBindByName.column();
  }

}

My POJO beans are annotated like this

public class Record {

  @CsvBindByName(required = true)
  @CsvPosition(position = 1)
  Long id;
  @CsvCustomBindByName(required = true, converter = BoolanCSVField.class)
  @CsvPosition(position = 2)
  Boolean deleted;
  ...
}

and final code for writer :

CustomMappingStrategy<Record> mappingStrategy = new CustomMappingStrategy<>(Record.class);
mappingStrategy.setType(Record.class);
StatefulBeanToCsv beanToCsv = new StatefulBeanToCsvBuilder(writer)
.withApplyQuotesToAll(false)
.withOrderedResults(true)
.withMappingStrategy(mappingStrategy)
.build();

I hope it will helpful for someone

Solution 14 - Java

Here is the code to add support for @CsvBindByPosition based ordering to default HeaderColumnNameMappingStrategy. Tested for latest version 5.2

Approach is to store 2 map. First headerPositionMap to store the position element so same can used to setColumnOrderOnWrite , second columnMap from which we can lookup actual column name rather than capitalized one

public class HeaderColumnNameWithPositionMappingStrategy<T> extends HeaderColumnNameMappingStrategy<T> {

    protected Map<String, String> columnMap;

    @Override
    public void setType(Class<? extends T> type) throws CsvBadConverterException {
        super.setType(type);
        columnMap = new HashMap<>(this.getFieldMap().values().size());
        Map<String, Integer> headerPositionMap = new HashMap<>(this.getFieldMap().values().size());
        for (Field field : type.getDeclaredFields()) {
            if (field.isAnnotationPresent(CsvBindByPosition.class) && field.isAnnotationPresent(CsvBindByName.class)) {
                int position = field.getAnnotation(CsvBindByPosition.class).position();
                String colName = "".equals(field.getAnnotation(CsvBindByName.class).column()) ? field.getName() : field.getAnnotation(CsvBindByName.class).column();
                headerPositionMap.put(colName.toUpperCase().trim(), position);
                columnMap.put(colName.toUpperCase().trim(), colName);
            }
        }
        super.setColumnOrderOnWrite((String o1, String o2) -> {
            if (!headerPositionMap.containsKey(o1) || !headerPositionMap.containsKey(o2)) {
                return 0;
            }
            return headerPositionMap.get(o1) - headerPositionMap.get(o2);
        });
    }

    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
        String[] headersRaw = super.generateHeader(bean);
        return Arrays.stream(headersRaw).map(h -> columnMap.get(h)).toArray(String[]::new);
    }
}

Solution 15 - Java

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();
}

}

Solution 16 - Java

Try something like below:

private static class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {

    String[] header;

    public CustomMappingStrategy(String[] cols) {
        header = cols;
    }

    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
        return header;
    }
}

Then use it as follows:

String[] columns = new String[]{"Name", "Age", "Company", "Salary"};
        CustomMappingStrategy<Employee> mappingStrategy = new CustomMappingStrategy<Employee>(columns);

Where columns are columns of your bean and Employee is your bean

Solution 17 - Java

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);

Solution 18 - Java

Great thread, I don't have any annotations in my pojo and this is how I did based on all the previous answers. Hope it helps others.

OpenCsv Version: 5.0 List readVendors = getFromMethod(); String[] fields= {"id","recordNumber","finVendorIdTb","finVenTechIdTb","finShortNameTb","finVenName1Tb","finVenName2Tb"};

            String[] csvHeader= {"Id#","Shiv Record Number","Shiv Vendor Id","Shiva Tech Id#","finShortNameTb","finVenName1Tb","finVenName2Tb"};

            CustomMappingStrategy<FinVendor> mappingStrategy = new CustomMappingStrategy(csvHeader);//csvHeader as per custom header irrespective of pojo field name
            mappingStrategy.setType(FinVendor.class);
            mappingStrategy.setColumnMapping(fields);//pojo mapping fields


            StatefulBeanToCsv<FinVendor> beanToCsv = new StatefulBeanToCsvBuilder<FinVendor>(writer).withQuotechar(CSVWriter.NO_QUOTE_CHARACTER).withMappingStrategy(mappingStrategy).build();
            beanToCsv.write(readVendors);

//custom mapping class as mentioned in the thread by many users private static class CustomMappingStrategy extends ColumnPositionMappingStrategy {

                    String[] header;

                    public CustomMappingStrategy(String[] cols) {

                            header = cols;
                    }

                    @Override
                    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
                        super.generateHeader(bean);
                            return header;
                    }
                    }

Output:

Id#     Shiv Record Number      Shiv Vendor Id   Fin Tech Id#      finShortNameTb  finVenName1Tb   finVenName2Tb   finVenDefaultLocTb
1       VEN00053                678             33316025986        THE ssOHIO S_2  THE UNIVERSITY     CHK         Test
2       VEN02277                1217            3044374205         Fe3 MECHA_1     FR3INC             EFT-1
3       VEN03118                1310            30234484121        PE333PECTUS_1   PER332CTUS AR      EFT-1       Test

Solution 19 - Java

The sebast26's first solution worked for me but for opencsv version 5.2 it requires a little change in the CustomMappingStrategy class:

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() {
    super.generateHeader(bean); // without this the file contains ONLY headers
    return HEADER;
}

}

Solution 20 - Java

In case you need this to preserve column ordering from the original CSV: use a HeaderColumnNameMappingStrategy for reading, then use the same strategy for writing. "Same" in this case meaning not just the same class, but really the same object.

From the javadoc of StatefulBeanToCsvBuilder.withMappingStrategy:

> It is perfectly legitimate to read a CSV source, take the mapping strategy from the read operation, and pass it in to this method for a write operation. This conserves some processing time, but, more importantly, preserves header ordering.

This way you will get a CSV including headers, with columns in the same order as the original CSV.

Worked for me using OpenCSV 5.4.

Solution 21 - Java

I used this with 5.6.

public class CsvRow {
    @CsvBindByName(column = "column1")
    private String a;

    @CsvBindByName(column = "column2")
    private String b;

    @CsvBindByName(column = "column3")
    private String c;
}
// FieldUtils is from Apache Commons Lang3
List<String> columnNameList = FieldUtils
        .getFieldsListWithAnnotation(CsvRow.class, CsvBindByName.class)
        .stream()
        .map(field -> field.getAnnotation(CsvBindByName.class))
        .map(CsvBindByName::column)
        .collect(Collectors.toList());

// FixedOrderComparator is from Apache Commons Collections 4
FixedOrderComparator comparator = new FixedOrderComparator(columnNameList);

HeaderColumnNameMappingStrategy<CsvRow> strategy = new HeaderColumnNameMappingStrategy<>();
strategy.setType(CsvRow.class);
strategy.setColumnOrderOnWrite(comparator);

StatefulBeanToCsvBuilder<CsvRow> builder = new StatefulBeanToCsvBuilder<>(writer);
builder.withMappingStrategy(strategy);

StatefulBeanToCsv<CsvRow> beanToCsv = builder.build();

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionVikram PathaniaView Question on Stackoverflow
Solution 1 - Javasebast26View Answer on Stackoverflow
Solution 2 - JavaLalji GajeraView Answer on Stackoverflow
Solution 3 - JavaAnton KumpanView Answer on Stackoverflow
Solution 4 - JavadirkvranckaertView Answer on Stackoverflow
Solution 5 - JavaAmitView Answer on Stackoverflow
Solution 6 - JavaRodrigo BroggiView Answer on Stackoverflow
Solution 7 - JavawrbView Answer on Stackoverflow
Solution 8 - JavaHarshitView Answer on Stackoverflow
Solution 9 - JavaLex LuthorView Answer on Stackoverflow
Solution 10 - JavaVladyslav VynnykView Answer on Stackoverflow
Solution 11 - JavamaimArtView Answer on Stackoverflow
Solution 12 - JavaAritra DasView Answer on Stackoverflow
Solution 13 - JavajherkelView Answer on Stackoverflow
Solution 14 - JavaAtish ChoudhuryView Answer on Stackoverflow
Solution 15 - JavaakashaView Answer on Stackoverflow
Solution 16 - JavaKalya ElishaView Answer on Stackoverflow
Solution 17 - JavaRaviView Answer on Stackoverflow
Solution 18 - JavaShiv GunsView Answer on Stackoverflow
Solution 19 - Javaalcott80View Answer on Stackoverflow
Solution 20 - JavaYonasView Answer on Stackoverflow
Solution 21 - JavaFranz WongView Answer on Stackoverflow