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);
}
В заключение:
- Мы можем читать CSV в POJO независимо от порядка столбцов - потому что мы
используя @CsvBindByName
- Мы можем контролировать порядок столбцов при записи, используя
пользовательский компаратор
Ответ 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
делает следующее:
- Выбирает каждое поле переменной члена в нашем классе данных (
CsvRow
) - Находит все из них с аннотацией
@CsvBindByName
(в порядке, который вы указали в модели CsvRow
) - Верхний регистр каждого соответствует реализации 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
)