OpenCSV: Как создать файл CSV из POJO с настраиваемыми заголовками столбцов и пользовательскими позициями столбцов?

Я создал класс MappingsBean, где указаны все столбцы CSV файла. Затем я разбираю XML файлы и создаю список объектов отображения. Затем я записываю эти данные в CSV файл в качестве отчета.

Я использую следующие аннотации:

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

И затем я использую класс StatefulBeanToCsv для записи в CSV файл:

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

Проблема с этим подходом заключается в том, что если я использую @CsvBindByPosition(position = 0) для управления тогда я не могу генерировать имена столбцов. Если я использую @CsvBindByName(column = "TradeID"), тогда я не могу установить положение столбцов.

Есть ли способ, которым я могу использовать обе аннотации, чтобы я мог создавать CSV файлы с заголовками столбцов, а также управлять позицией столбца?

С уважением, Викрам Патрания

Ответ 1

У меня была схожая проблема. AFAIK в OpenCSV нет встроенных функций, которые позволят написать bean в CSV с именами имен столбцов и.

В OpenCSV доступны два основных MappingStrategy, которые доступны в OpenCSV:

  • HeaderColumnNameMappingStrategy: позволяет сопоставить столбцы файла CVS с полями bean на основе пользовательского имени; при записи bean в CSV это позволяет изменить имя заголовка столбца, но мы не имеем контроля над порядком столбцов
  • ColumnPositionMappingStrategy: позволяет сопоставлять столбцы файла CSV с полями bean на основе упорядочения столбцов; при записи bean в CSV мы можем управлять порядком столбцов, но мы получаем пустой заголовок (реализация возвращает new String[0] в качестве заголовка)

Единственный способ, которым я нашел для достижения как пользовательских имен столбцов, так и упорядочения, - написать свой собственный MappingStrategy.

Первое решение: быстрое и легкое, но жестко закодированное

Создать пользовательский 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;
    }
}

И используйте его в 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()

В классе MappingsBean мы оставили CsvBindByPosition аннотации - для управления порядком (в этом решении CsvBindByName аннотации не нужны). Благодаря пользовательской стратегии сопоставления имена столбцов заголовка включаются в результирующий файл CSV.

Недостатком этого решения является то, что при изменении упорядочения столбцов с помощью аннотации CsvBindByPosition мы должны вручную изменить константу HEADER в нашей пользовательской стратегии сопоставления.

Второе решение: более гибкое

Первое решение работает, но это было плохо для меня. Основываясь на встроенных реализациях MappingStrategy, я придумал еще одну реализацию:

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

Вы можете использовать эту настраиваемую стратегию в StatefulBeanToCsvBuilder точно так же, как в первом решении (не забудьте вызвать mappingStrategy.setType(MappingsBean.class);, иначе это решение не сработает).

В настоящее время наш MappingsBean должен содержать аннотации CsvBindByName и CsvBindByPosition. Первый, чтобы указать имя столбца заголовка, а второй - создать порядок столбцов в выходном CSV-заголовке. Теперь, если мы изменим (используя аннотации) либо имя столбца, либо порядок в MappingsBean class - это изменение будет отражено в выходном файле CSV.

Ответ 2

Исправленный выше ответ, чтобы соответствовать с более новой версией.

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

Затем вызовите это, чтобы сгенерировать CSV. Я использовал Посетителей в качестве своего POJO для заполнения, обновления там, где это необходимо.

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

Мои аннотации к компонентам выглядят так

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


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

Ответ 3

Я хотел добиться двунаправленного импорта/экспорта - чтобы иметь возможность импортировать сгенерированный CSV обратно в POJO и наоборот.

Я не смог использовать @CsvBindByPosition для этого, потому что в этом случае - ColumnPositionMappingStrategy был выбран автоматически. По документам: эта стратегия требует, чтобы файл НЕ имел заголовка.

Что я использовал для достижения цели:

HeaderColumnNameMappingStrategy
mappingStrategy.setColumnOrderOnWrite(Comparator<String> writeOrder)

CsvUtils для чтения/записи 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 для импорта/экспорта

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
}

Пользовательский компаратор для предопределенного порядка строк:

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

Заказное письмо для POJO (ответ на первоначальный вопрос)

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

Читать экспортированный CSV обратно в POJO (дополнение к оригинальному ответу)

Важно: CSV может быть неупорядоченным, поскольку мы все еще используем привязку по имени:

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

В заключение:

  1. Мы можем читать CSV в POJO независимо от порядка столбцов - потому что мы используя @CsvBindByName
  2. Мы можем контролировать порядок столбцов при записи, используя пользовательский компаратор

Ответ 4

спасибо за этот поток, это было действительно полезно для меня... Я немного улучшил предоставленное решение для того, чтобы принимать также POJO, где некоторые поля не аннотированы (не предназначены для чтения/записи):

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

}

Ответ 5

Если вас интересует только сортировка столбцов CSV на основе порядка, в котором переменные-члены появляются в вашем классе модели (строка CsvRow в этом примере), то вы можете использовать реализацию Comparator чтобы решить эту проблему довольно простым способом. Вот пример, который делает это в 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)
    }

}

Этот Comparator делает следующее:

  1. Выбирает каждое поле переменной члена в нашем классе данных (CsvRow)
  2. Находит все из них с аннотацией @CsvBindByName (в порядке, который вы указали в модели CsvRow)
  3. Верхний регистр каждого соответствует реализации OpenCsv по умолчанию

Затем примените этот Comparator к вашему MappingStrategy, чтобы он сортировал на основе указанного порядка:

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

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

Для справки, вот пример класса CsvRow (вы захотите заменить его своей собственной моделью для ваших нужд):

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

Который будет производить CSV следующим образом:

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

Преимущество этого подхода состоит в том, что он устраняет необходимость в жестком кодировании любого из имен столбцов, что должно значительно упростить ситуацию, если вам когда-либо понадобится добавить/удалить столбцы.

Ответ 6

Если у вас нет метода getDeclaredAnnotationsByType, но вам нужно имя вашего исходного поля:

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

}

Ответ 7

Попробуйте что-то вроде ниже:

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

Затем используйте его следующим образом:

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

Где столбцы - это столбцы вашего компонента, а Employee - ваш компонент.

Ответ 8

Я улучшил предыдущие ответы, удалив все ссылки на устаревшие API при использовании последней версии opencsv (4.6).

Универсальное решение Kotlin

/**
 * 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
}

Затем используйте его следующим образом:

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

и для полноты здесь CSV-компонент в качестве класса данных Kotlin:

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
)