Skip to content

Instantly share code, notes, and snippets.

@ammmze
Last active June 7, 2024 09:27
Show Gist options
  • Save ammmze/ec0334d107cb63c586ffd8fc51ec5757 to your computer and use it in GitHub Desktop.
Save ammmze/ec0334d107cb63c586ffd8fc51ec5757 to your computer and use it in GitHub Desktop.
opencsv HeaderColumnNameAndOrderMappingStrategy

HeaderColumnNameAndOrderMappingStrategy

This creates a MappingStrategy for use with OpenCSV (specifically tested for generating a CSV from beans) which does the following:

  1. Preserves the column name casing in the @CsvBindByName annotation
  2. Adds a @CsvBindByNameOrder annotation you can apply to the bean class to define the order of the columns.
  • Any field not included in the order, but is still annotated with @CsvBindName will still be included AFTER all the columns that have a defined order. Those remaining columns will be added in alphabetical order (this is the default behavior of the HeaderColumnNameMappingStrategy)
  1. Overrides the converter used with @CsvDate to use a custom converter that adds support for the java time api (LocalDate, LocalTime, and LocalDateTime are tested)

Usage

Annotate your bean with something like...

@CsvBindByNameOrder({"Foo","Bar"})
public class MyBean {
    @CsvBindByName(column = "Foo")
    private String foo;
    
    @CsvBindByName(column = "Bar")
    private String bar;
    
    // getter/setters omitted for brevity
}

Setup your writer...

List<MyBean> beans = new ArrayList();
MyBean bean = new MyBean();
bean.setFoo("fooit");
bean.setBar("barit");
beans.add(bean);

StringWriter writer = new StringWriter();
StatefulBeanToCsv<MyBean> csvWriter = new StatefulBeanToCsvBuilder<MyBean>(writer)
    .withApplyQuotesToAll(false)
    .withMappingStrategy(new HeaderColumnNameAndOrderMappingStrategy<>(MyBean.class))
    .build();
csvWriter.write(beans);
return writer.toString();

Results

With the above you should get something like...

Foo,Bar
fooit,barit

package com.example.csv;
import com.opencsv.bean.ConverterDate;
import com.opencsv.exceptions.CsvDataTypeMismatchException;
import java.lang.reflect.InvocationTargetException;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
import java.util.Locale;
import org.apache.commons.lang3.StringUtils;
public class ConverterDateAndJavaTime extends ConverterDate {
private static String DEFAULT_FORMAT = "yyyyMMdd'T'HHmmss";
private static String DEFAULT_DATE_ONLY_FORMAT = "yyyyMMdd";
private static String DEFAULT_TIME_ONLY_FORMAT = "HHmmss";
private DateTimeFormatter format;
/**
* @param type The type of the field being populated
* @param formatString The string to use for formatting the date. See
* {@link com.opencsv.bean.CsvDate#value()}
* @param locale If not null or empty, specifies the locale used for
* converting locale-specific data types
* @param errorLocale The locale to use for error messages.
*/
public ConverterDateAndJavaTime(Class<?> type, String locale, Locale errorLocale, String formatString) {
super(type, locale, errorLocale, formatString);
// if the type is LocalDate and using the default format, we know it will fail. Lets use just the date portion of the default date format
if (DEFAULT_FORMAT.equals(formatString) && LocalDate.class.isAssignableFrom(type)) {
formatString = DEFAULT_DATE_ONLY_FORMAT;
} else if (DEFAULT_FORMAT.equals(formatString) && LocalTime.class.isAssignableFrom(type)) {
formatString = DEFAULT_TIME_ONLY_FORMAT;
}
if (this.locale != null) {
format = DateTimeFormatter.ofPattern(formatString, this.locale);
} else {
format = DateTimeFormatter.ofPattern(formatString);
}
}
@Override
public Object convertToRead(String value) throws CsvDataTypeMismatchException {
if (StringUtils.isNotBlank(value) && Temporal.class.isAssignableFrom(type)) {
return convertToTemporal(value);
}
return super.convertToRead(value);
}
@Override
public String convertToWrite(Object value) throws CsvDataTypeMismatchException {
if (value != null && Temporal.class.isAssignableFrom(value.getClass())) {
return convertFromTemporal((Temporal) value);
}
return super.convertToWrite(value);
}
private Temporal convertToTemporal(String value) {
try {
return (Temporal) type.getMethod("parse", CharSequence.class, DateTimeFormatter.class).invoke(null, value, format);
} catch (NoSuchMethodException|IllegalAccessException|InvocationTargetException e) {
throw new RuntimeException("Failed to invoke the parse method of " + type.getName(), e);
}
}
private String convertFromTemporal(Temporal value) {
return format.format(value);
}
}
package com.example.csv;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import com.opencsv.bean.CsvConverter;
import com.opencsv.bean.CsvDate;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Locale;
import org.junit.Test;
public class ConverterDateAndJavaTimeTest {
private static final Locale ERROR_LOCALE = Locale.getDefault();
private static final String LOCALE = ERROR_LOCALE.toString();
private static String DEFAULT_FORMAT;
static {
try {
DEFAULT_FORMAT = (String) CsvDate.class.getMethod("value").getDefaultValue();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
@Test
public void convertToRead_When_ValueIsBlank_Expect_ToReceiveNull() throws Exception {
assertNull(converter(LocalDate.class, DEFAULT_FORMAT).convertToRead(""));
}
@Test
public void convertToRead_When_ValueIsValidAndTypeIsLocalDate_Expect_ToReceiveLocalDate() throws Exception {
LocalDate actual = (LocalDate) converter(LocalDate.class, DEFAULT_FORMAT).convertToRead("20190214");
assertNotNull(actual);
assertEquals(2019, actual.getYear());
assertEquals(2, actual.getMonthValue());
assertEquals(14, actual.getDayOfMonth());
}
@Test
public void convertToRead_When_ValueIsValidAndTypeIsLocalTime_Expect_ToReceiveLocalTime() throws Exception {
LocalTime actual = (LocalTime) converter(LocalTime.class, DEFAULT_FORMAT).convertToRead("221546");
assertNotNull(actual);
assertEquals(22, actual.getHour());
assertEquals(15, actual.getMinute());
assertEquals(46, actual.getSecond());
}
@Test
public void convertToRead_When_ValueIsValidAndTypeIsLocalDateTime_Expect_ToReceiveLocalDateTime() throws Exception {
LocalDateTime actual = (LocalDateTime) converter(LocalDateTime.class, DEFAULT_FORMAT).convertToRead("20190214T221546");
assertNotNull(actual);
assertEquals(2019, actual.getYear());
assertEquals(2, actual.getMonthValue());
assertEquals(14, actual.getDayOfMonth());
assertEquals(22, actual.getHour());
assertEquals(15, actual.getMinute());
assertEquals(46, actual.getSecond());
}
@Test
public void convertToWrite_When_ValueIsLocalDate_Expect_ToReceiveFormattedDate() throws Exception {
String actual = converter(LocalDate.class, DEFAULT_FORMAT).convertToWrite(LocalDate.of(2019, 2, 14));
assertEquals("20190214", actual);
}
@Test
public void convertToWrite_When_ValueIsLocalTime_Expect_ToReceiveFormattedTime() throws Exception {
String actual = converter(LocalTime.class, DEFAULT_FORMAT).convertToWrite(LocalTime.of(22, 15, 46));
assertEquals("221546", actual);
}
@Test
public void convertToWrite_When_ValueIsLocalDateTime_Expect_ToReceiveFormattedDateTime() throws Exception {
String actual = converter(LocalDateTime.class, DEFAULT_FORMAT).convertToWrite(LocalDateTime.of(2019, 2, 14,22, 15, 46));
assertEquals("20190214T221546", actual);
}
private CsvConverter converter(Class type, String format) {
return new ConverterDateAndJavaTime(type, LOCALE, ERROR_LOCALE, format);
}
}
package com.example.csv;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CsvBindByNameOrder {
String[] value() default {};
}
package com.example.csv;
import com.opencsv.bean.BeanField;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.CsvCustomBindByName;
import com.opencsv.bean.HeaderColumnNameMappingStrategy;
import com.opencsv.bean.comparator.LiteralComparator;
import com.opencsv.exceptions.CsvBadConverterException;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
import java.util.Arrays;
import org.apache.commons.lang3.StringUtils;
public class HeaderColumnNameAndOrderMappingStrategy<T> extends HeaderColumnNameMappingStrategy<T> {
public HeaderColumnNameAndOrderMappingStrategy(Class<T> type) {
setType(type);
}
@Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
// overriding this method to allow us to preserve the header column name casing
String[] header = super.generateHeader(bean);
final int numColumns = findMaxFieldIndex();
if (!isAnnotationDriven() || numColumns == -1) {
return header;
}
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;
}
@Override
protected void loadFieldMap() throws CsvBadConverterException {
// overriding this method to support setting column order by the custom `CsvBindByNameOrder` annotation
if (writeOrder == null && type.isAnnotationPresent(CsvBindByNameOrder.class)) {
setColumnOrderOnWrite(
new LiteralComparator<>(Arrays.stream(type.getAnnotation(CsvBindByNameOrder.class).value())
.map(String::toUpperCase).toArray(String[]::new)));
}
super.loadFieldMap();
}
private String extractHeaderName(final BeanField beanField) {
if (beanField == null || beanField.getField() == null
|| beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) {
return StringUtils.EMPTY;
}
if (beanField.getField().isAnnotationPresent(CsvBindByName.class)) {
return beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0].column();
} else if (beanField.getField().isAnnotationPresent(CsvCustomBindByName.class)) {
return beanField.getField().getDeclaredAnnotationsByType(CsvCustomBindByName.class)[0].column();
}
return StringUtils.EMPTY;
}
}
package com.example.csv;
import com.opencsv.bean.AbstractCsvConverter;
import com.opencsv.bean.BeanField;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.bean.CsvConverter;
import com.opencsv.bean.CsvCustomBindByName;
import com.opencsv.bean.CsvDate;
import com.opencsv.bean.HeaderColumnNameMappingStrategy;
import com.opencsv.bean.comparator.LiteralComparator;
import com.opencsv.exceptions.CsvBadConverterException;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
import java.lang.reflect.Field;
import java.util.Arrays;
import org.apache.commons.lang3.StringUtils;
public class HeaderColumnNameAndOrderMappingStrategy<T> extends HeaderColumnNameMappingStrategy<T> {
public HeaderColumnNameAndOrderMappingStrategy(Class<T> type) {
setType(type);
}
@Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
// overriding this method to allow us to preserve the header column name casing
String[] header = super.generateHeader(bean);
final int numColumns = findMaxFieldIndex();
if (!isAnnotationDriven() || numColumns == -1) {
return header;
}
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;
}
@Override
protected void loadFieldMap() throws CsvBadConverterException {
// overriding this method to support setting column order by the custom `CsvBindByNameOrder` annotation
if (writeOrder == null && type.isAnnotationPresent(CsvBindByNameOrder.class)) {
setColumnOrderOnWrite(
new LiteralComparator<>(Arrays.stream(type.getAnnotation(CsvBindByNameOrder.class).value())
.map(String::toUpperCase).toArray(String[]::new)));
}
super.loadFieldMap();
}
@Override
protected CsvConverter determineConverter(Field field, Class<?> elementType, String locale,
Class<? extends AbstractCsvConverter> customConverter) throws CsvBadConverterException {
// overrides the converter for the `CsvDate` to use our custom converter that supports java.time api
// A custom converter always takes precedence if specified.
if(customConverter != null && !customConverter.equals(AbstractCsvConverter.class)) {
return super.determineConverter(field, elementType, locale, customConverter);
}
// Perhaps a date instead
else if(field.isAnnotationPresent(CsvDate.class)) {
String formatString = field.getAnnotation(CsvDate.class).value();
return new ConverterDateAndJavaTime(elementType, locale, errorLocale, formatString);
}
return super.determineConverter(field, elementType, locale, customConverter);
}
private String extractHeaderName(final BeanField beanField) {
if (beanField == null || beanField.getField() == null) {
return StringUtils.EMPTY;
}
if (beanField.getField().isAnnotationPresent(CsvBindByName.class)) {
return beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0].column();
} else if (beanField.getField().isAnnotationPresent(CsvCustomBindByName.class)) {
return beanField.getField().getDeclaredAnnotationsByType(CsvCustomBindByName.class)[0].column();
}
return StringUtils.EMPTY;
}
}
@buccio
Copy link

buccio commented Jun 7, 2024

Thank you guys @ammmze, @nerdmeeting and @aarrsseni !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment