package com.opencsv.bean;
import com.opencsv.CSVReader;
import com.opencsv.ICSVParser;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
import org.apache.commons.collections4.ListValuedMap;
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;
Allows for the mapping of columns with their positions. Using this strategy without annotations (CsvBindByPosition
or CsvCustomBindByPosition
) requires all the columns to be present in the CSV file and for them to be in a particular order. Using annotations allows one to specify arbitrary zero-based column numbers for each bean member variable to be filled. Also this strategy requires that the file does NOT have a header. That said, the main use of this strategy is files that do not have headers. Type parameters: - <T> – Type of object that is being processed.
/**
* Allows for the mapping of columns with their positions. Using this strategy
* without annotations ({@link com.opencsv.bean.CsvBindByPosition} or
* {@link com.opencsv.bean.CsvCustomBindByPosition}) requires all the columns
* to be present in the CSV file and for them to be in a particular order. Using
* annotations allows one to specify arbitrary zero-based column numbers for
* each bean member variable to be filled. Also this strategy requires that the
* file does NOT have a header. That said, the main use of this strategy is
* files that do not have headers.
*
* @param <T> Type of object that is being processed.
*/
public class ColumnPositionMappingStrategy<T> extends AbstractMappingStrategy<String, Integer, ComplexFieldMapEntry<String, Integer, T>, T> {
Whether the user has programmatically set the map from column positions
to field names.
/**
* Whether the user has programmatically set the map from column positions
* to field names.
*/
private boolean columnsExplicitlySet = false;
The map from column position to BeanField
. /**
* The map from column position to {@link BeanField}.
*/
private FieldMapByPosition<T> fieldMap;
Holds a Comparator
to sort columns on writing. /**
* Holds a {@link java.util.Comparator} to sort columns on writing.
*/
private Comparator<Integer> writeOrder;
Used to store a mapping from presumed input column index to desired output column index, as determined by applying ColumnPositionMappingStrategy<T>.writeOrder
. /**
* Used to store a mapping from presumed input column index to desired
* output column index, as determined by applying {@link #writeOrder}.
*/
private Integer[] columnIndexForWriting = null;
Default constructor.
/**
* Default constructor.
*/
public ColumnPositionMappingStrategy() {
}
There is no header per se for this mapping strategy, but this method
checks the first line to determine how many fields are present and
adjusts its field map accordingly.
/**
* There is no header per se for this mapping strategy, but this method
* checks the first line to determine how many fields are present and
* adjusts its field map accordingly.
*/
// The rest of the Javadoc is inherited
@Override
public void captureHeader(CSVReader reader) throws IOException {
// Validation
if (type == null) {
throw new IllegalStateException(ResourceBundle
.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
.getString("type.unset"));
}
String[] firstLine = ObjectUtils.defaultIfNull(reader.peek(), ArrayUtils.EMPTY_STRING_ARRAY);
fieldMap.setMaxIndex(firstLine.length - 1);
if (!columnsExplicitlySet) {
headerIndex.clear();
for (FieldMapByPositionEntry<T> entry : fieldMap) {
Field f = entry.getField().getField();
if (f.getAnnotation(CsvCustomBindByPosition.class) != null
|| f.getAnnotation(CsvBindAndSplitByPosition.class) != null
|| f.getAnnotation(CsvBindAndJoinByPosition.class) != null
|| f.getAnnotation(CsvBindByPosition.class) != null) {
headerIndex.put(entry.getPosition(), f.getName().toUpperCase().trim());
}
}
}
}
Returns: {@inheritDoc} For this mapping strategy, it's simply index
wrapped as an Integer
.
/**
* @return {@inheritDoc} For this mapping strategy, it's simply
* {@code index} wrapped as an {@link java.lang.Integer}.
*/
// The rest of the Javadoc is inherited
@Override
protected Integer chooseMultivaluedFieldIndexFromHeaderIndex(int index) {
return Integer.valueOf(index);
}
@Override
protected BeanField<T, Integer> findField(int col) {
// If we have a mapping for changing the order of the columns on
// writing, be sure to use it.
if (columnIndexForWriting != null) {
return col < columnIndexForWriting.length ? fieldMap.get(columnIndexForWriting[col]) : null;
}
return fieldMap.get(col);
}
This method returns an empty array.
The column position mapping strategy assumes that there is no header, and
thus it also does not write one, accordingly.
Returns: An empty array
/**
* This method returns an empty array.
* The column position mapping strategy assumes that there is no header, and
* thus it also does not write one, accordingly.
*
* @return An empty array
*/
// The rest of the Javadoc is inherited
@Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
String[] h = super.generateHeader(bean);
columnIndexForWriting = new Integer[h.length];
Arrays.setAll(columnIndexForWriting, i -> i);
// Create the mapping for input column index to output column index.
Arrays.sort(columnIndexForWriting, writeOrder);
return ArrayUtils.EMPTY_STRING_ARRAY;
}
Gets a column name.
Params: - col – Position of the column.
Returns: Column name or null if col > number of mappings.
/**
* Gets a column name.
*
* @param col Position of the column.
* @return Column name or null if col > number of mappings.
*/
@Override
public String getColumnName(int col) {
return headerIndex.getByPosition(col);
}
Retrieves the column mappings.
Returns: String array with the column mappings.
/**
* Retrieves the column mappings.
*
* @return String array with the column mappings.
*/
public String[] getColumnMapping() {
return headerIndex.getHeaderIndex();
}
Setter for the column mapping.
This mapping is for reading. Use of this method in conjunction with
writing is undefined.
Params: - columnMapping – Column names to be mapped.
/**
* Setter for the column mapping.
* This mapping is for reading. Use of this method in conjunction with
* writing is undefined.
*
* @param columnMapping Column names to be mapped.
*/
public void setColumnMapping(String... columnMapping) {
if (columnMapping != null) {
headerIndex.initializeHeaderIndex(columnMapping);
} else {
headerIndex.clear();
}
columnsExplicitlySet = true;
if(getType() != null) {
loadFieldMap(); // In case setType() was called first.
}
}
Creates a map of annotated fields in the bean to be processed.
This method is called by AbstractMappingStrategy.loadFieldMap()
when at least one relevant annotation is found on a member variable.
/**
* Creates a map of annotated fields in the bean to be processed.
* <p>This method is called by {@link #loadFieldMap()} when at least one
* relevant annotation is found on a member variable.</p>
*/
@Override
protected void loadAnnotatedFieldMap(ListValuedMap<Class<?>, Field> fields) {
boolean required;
for (Map.Entry<Class<?>, Field> classAndField : fields.entries()) {
Class<?> localType = classAndField.getKey();
Field localField = classAndField.getValue();
String fieldLocale, fieldWriteLocale, capture, format;
// Custom converters always have precedence.
if (localField.isAnnotationPresent(CsvCustomBindByPosition.class)) {
CsvCustomBindByPosition annotation = localField
.getAnnotation(CsvCustomBindByPosition.class);
@SuppressWarnings("unchecked")
Class<? extends AbstractBeanField<T, Integer>> converter = (Class<? extends AbstractBeanField<T, Integer>>)annotation.converter();
BeanField<T, Integer> bean = instantiateCustomConverter(converter);
bean.setType(localType);
bean.setField(localField);
required = annotation.required();
bean.setRequired(required);
fieldMap.put(annotation.position(), bean);
}
// Then check for a collection
else if (localField.isAnnotationPresent(CsvBindAndSplitByPosition.class)) {
CsvBindAndSplitByPosition annotation = localField.getAnnotation(CsvBindAndSplitByPosition.class);
required = annotation.required();
fieldLocale = annotation.locale();
fieldWriteLocale = annotation.writeLocaleEqualsReadLocale()
? fieldLocale
: annotation.writeLocale();
String splitOn = annotation.splitOn();
String writeDelimiter = annotation.writeDelimiter();
Class<? extends Collection> collectionType = annotation.collectionType();
Class<?> elementType = annotation.elementType();
Class<? extends AbstractCsvConverter> splitConverter = annotation.converter();
capture = annotation.capture();
format = annotation.format();
CsvConverter converter = determineConverter(localField, elementType, fieldLocale, fieldWriteLocale, splitConverter);
fieldMap.put(annotation.position(), new BeanFieldSplit<>(
localType, localField, required, errorLocale, converter, splitOn,
writeDelimiter, collectionType, capture, format));
}
// Then check for a multi-column annotation
else if (localField.isAnnotationPresent(CsvBindAndJoinByPosition.class)) {
CsvBindAndJoinByPosition annotation = localField.getAnnotation(CsvBindAndJoinByPosition.class);
required = annotation.required();
fieldLocale = annotation.locale();
fieldWriteLocale = annotation.writeLocaleEqualsReadLocale()
? fieldLocale
: annotation.writeLocale();
Class<?> elementType = annotation.elementType();
Class<? extends MultiValuedMap> mapType = annotation.mapType();
Class<? extends AbstractCsvConverter> joinConverter = annotation.converter();
capture = annotation.capture();
format = annotation.format();
CsvConverter converter = determineConverter(localField, elementType, fieldLocale, fieldWriteLocale, joinConverter);
fieldMap.putComplex(annotation.position(), new BeanFieldJoinIntegerIndex<>(
localType, localField, required, errorLocale, converter, mapType, capture, format));
}
// Then it must be a bind by position.
else {
CsvBindByPosition annotation = localField.getAnnotation(CsvBindByPosition.class);
required = annotation.required();
fieldLocale = annotation.locale();
fieldWriteLocale = annotation.writeLocaleEqualsReadLocale()
? fieldLocale
: annotation.writeLocale();
capture = annotation.capture();
format = annotation.format();
CsvConverter converter = determineConverter(localField, localField.getType(), fieldLocale, fieldWriteLocale, null);
fieldMap.put(annotation.position(), new BeanFieldSingleValue<>(
localType, localField, required, errorLocale, converter, capture, format));
}
}
}
@Override
protected void loadUnadornedFieldMap(ListValuedMap<Class<?>, Field> fields) {
for(Map.Entry<Class<?>, Field> classAndField : fields.entries()) {
Class<?> localType = classAndField.getKey();
Field localField = classAndField.getValue();
CsvConverter converter = determineConverter(localField, localField.getType(), null, null, null);
int[] indices = headerIndex.getByName(localField.getName());
if(indices.length != 0) {
fieldMap.put(indices[0], new BeanFieldSingleValue<>(
localType, localField, false, errorLocale, converter, null, null));
}
}
}
Returns a set of the annotations that are used for binding in this
mapping strategy.
In this mapping strategy, those are currently:
/**
* Returns a set of the annotations that are used for binding in this
* mapping strategy.
* <p>In this mapping strategy, those are currently:<ul>
* <li>{@link CsvBindByPosition}</li>
* <li>{@link CsvCustomBindByPosition}</li>
* <li>{@link CsvBindAndJoinByPosition}</li>
* <li>{@link CsvBindAndSplitByPosition}</li>
* </ul></p>
*/
@Override
protected Set<Class<? extends Annotation>> getBindingAnnotations() {
// With Java 9 this can be done more easily with Set.of()
return new HashSet<>(Arrays.asList(
CsvBindByPosition.class,
CsvCustomBindByPosition.class,
CsvBindAndJoinByPosition.class,
CsvBindAndSplitByPosition.class));
}
@Override
protected void initializeFieldMap() {
fieldMap = new FieldMapByPosition<>(errorLocale);
fieldMap.setColumnOrderOnWrite(writeOrder);
}
@Override
protected void verifyLineLength(int numberOfFields) throws CsvRequiredFieldEmptyException {
if (!headerIndex.isEmpty()) {
BeanField<T, Integer> f;
StringBuilder sb = null;
for (int i = numberOfFields; i <= headerIndex.findMaxIndex(); i++) {
f = findField(i);
if (f != null && f.isRequired()) {
if (sb == null) {
sb = new StringBuilder(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("multiple.required.field.empty"));
}
sb.append(' ');
sb.append(f.getField().getName());
}
}
if (sb != null) {
throw new CsvRequiredFieldEmptyException(type, sb.toString());
}
}
}
Returns the column position for the given column number.
Yes, they're the same thing. For this mapping strategy, it's a simple
conversion from an integer to a string.
/**
* Returns the column position for the given column number.
* Yes, they're the same thing. For this mapping strategy, it's a simple
* conversion from an integer to a string.
*/
// The rest of the Javadoc is inherited
@Override
public String findHeader(int col) {
return Integer.toString(col);
}
@Override
protected FieldMap<String, Integer, ? extends ComplexFieldMapEntry<String, Integer, T>, T> getFieldMap() {
return fieldMap;
}
Sets the Comparator
to be used to sort columns when writing beans to a CSV file. Behavior of this method when used on a mapping strategy intended for reading data from a CSV source is not defined. Params: - writeOrder – The
Comparator
to use. May be null
, in which case the natural ordering is used.
Since: 4.3
/**
* Sets the {@link java.util.Comparator} to be used to sort columns when
* writing beans to a CSV file.
* Behavior of this method when used on a mapping strategy intended for
* reading data from a CSV source is not defined.
*
* @param writeOrder The {@link java.util.Comparator} to use. May be
* {@code null}, in which case the natural ordering is used.
* @since 4.3
*/
public void setColumnOrderOnWrite(Comparator<Integer> writeOrder) {
this.writeOrder = writeOrder;
if (fieldMap != null) {
fieldMap.setColumnOrderOnWrite(this.writeOrder);
}
}
}