/*
 * Copyright 2018 Andrew Rucker Jones.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.opencsv.bean;

import com.opencsv.ICSVParser;
import com.opencsv.exceptions.*;
import org.apache.commons.collections4.ListValuedMap;
import org.apache.commons.collections4.MapIterator;
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

This class collects as many generally useful parts of the implementation of a mapping strategy as possible.

This mapping strategy knows of the existence of binding annotations, but assumes through getBindingAnnotations() they are not in use.

Anyone is welcome to use it as a base class for their own mapping strategies.

Author:Andrew Rucker Jones
Type parameters:
  • <T> – Type of object that is being processed.
  • <C> – The type of the internal many-to-one mapping
  • <I> – The initializer type used to build the internal many-to-one mapping
  • <K> – The type of the key used for internal indexing
Since:4.2
/** * This class collects as many generally useful parts of the implementation * of a mapping strategy as possible. * <p>This mapping strategy knows of the existence of binding annotations, but * assumes through {@link #getBindingAnnotations()} they are not in use.</p> * <p>Anyone is welcome to use it as a base class for their own mapping * strategies.</p> * * @param <T> Type of object that is being processed. * @param <C> The type of the internal many-to-one mapping * @param <I> The initializer type used to build the internal many-to-one mapping * @param <K> The type of the key used for internal indexing * * @author Andrew Rucker Jones * @since 4.2 */
abstract public class AbstractMappingStrategy<I, K extends Comparable<K>, C extends ComplexFieldMapEntry<I, K, T>, T> implements MappingStrategy<T> {
Set of classes where recursion is not allowed. Using HashSet because, given the large number of types, the contains method is quicker than an Array or ArrayList (Granted the number where Set is more efficient is different per Java release and system configuration). And being a Set we are noting that each value is unique.
/** * Set of classes where recursion is not allowed. Using HashSet because, given the large number of types, the * contains method is quicker than an Array or ArrayList (Granted the number where Set is more efficient is different * per Java release and system configuration). And being a Set we are noting that each value is unique. */
// This is easier in Java 9 with Set.of() private static final Set<Class> FORBIDDEN_CLASSES_FOR_RECURSION = new HashSet<>(Arrays.asList(Byte.TYPE, Short.TYPE, Integer.TYPE, Float.TYPE, Double.TYPE, Boolean.TYPE, Long.TYPE, Character.TYPE));
This is the class of the bean to be manipulated.
/** This is the class of the bean to be manipulated. */
protected Class<? extends T> type;
Maintains a bi-directional mapping between column position(s) and header name.
/** * Maintains a bi-directional mapping between column position(s) and header * name. */
protected final HeaderIndex headerIndex = new HeaderIndex();
A tree of the types encountered during recursion through the root bean type. These are only the types (and associated fields) specifically annotated with CsvRecurse.
/** * A tree of the types encountered during recursion through the root bean * type. * These are only the types (and associated fields) specifically annotated * with {@link CsvRecurse}. */
private RecursiveType recursiveTypeTree;
Storage for all manually excluded class/field pairs.
/** Storage for all manually excluded class/field pairs. */
private MultiValuedMap<Class<?>, Field> ignoredFields = new ArrayListValuedHashMap<>();
Locale for error messages.
/** Locale for error messages. */
protected Locale errorLocale = Locale.getDefault();
For BeanField.indexAndSplitMultivaluedField(Object, Object) it is necessary to determine which index to pass in.
Params:
  • index – The current column position while transmuting a bean to CSV output
Returns:The index to be used for this mapping strategy for BeanField.indexAndSplitMultivaluedField(Object, Object)
/** * For {@link BeanField#indexAndSplitMultivaluedField(java.lang.Object, java.lang.Object)} * it is necessary to determine which index to pass in. * * @param index The current column position while transmuting a bean to CSV * output * @return The index to be used for this mapping strategy for * {@link BeanField#indexAndSplitMultivaluedField(java.lang.Object, java.lang.Object) } */
abstract protected K chooseMultivaluedFieldIndexFromHeaderIndex(int index);
Returns the FieldMap associated with this mapping strategy.
Returns:The FieldMap used by this strategy
/** * Returns the {@link FieldMap} associated with this mapping strategy. * * @return The {@link FieldMap} used by this strategy */
abstract protected FieldMap<I,K,? extends C,T> getFieldMap();
Returns a set of the annotations that are used for binding in this mapping strategy. The default implementation returns the empty set.
Returns:Annotations of the sort CsvBindByName or CsvBindByPosition that are relevant for binding input fields to bean members in this mapping strategy
Since:5.0
/** * Returns a set of the annotations that are used for binding in this * mapping strategy. * The default implementation returns the empty set. * * @return Annotations of the sort {@link CsvBindByName} or * {@link CsvBindByPosition} that are relevant for binding input fields to * bean members in this mapping strategy * @since 5.0 */
protected Set<Class<? extends Annotation>> getBindingAnnotations() {return Collections.emptySet();}
Creates a map of annotated fields in the bean to be processed.

This method is called by loadFieldMap() when at least one relevant annotation is found on a member variable.

The default implementation assumes there are no annotations and does nothing.

Params:
  • fields – A list of fields annotated with a binding annotation in the bean to be processed
Since:5.0
/** * 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> * <p>The default implementation assumes there are no annotations and does * nothing.</p> * * @param fields A list of fields annotated with a binding annotation * in the bean to be processed * @since 5.0 */
protected void loadAnnotatedFieldMap(ListValuedMap<Class<?>, Field> fields) {}
Creates a map of fields in the bean to be processed that have no annotations. This method is called by loadFieldMap() when absolutely no annotations that are relevant for this mapping strategy are found in the type of bean being processed.
Params:
  • fields – A list of all non-synthetic fields in the bean to be processed
Since:5.0
/** * Creates a map of fields in the bean to be processed that have no * annotations. * This method is called by {@link #loadFieldMap()} when absolutely no * annotations that are relevant for this mapping strategy are found in the * type of bean being processed. * * @param fields A list of all non-synthetic fields in the bean to be * processed * @since 5.0 */
abstract protected void loadUnadornedFieldMap(ListValuedMap<Class<?>, Field> fields);
Creates an empty binding-type-specific field map that can be filled in later steps.

This method may be called multiple times and must erase any state information from previous calls.

Since:5.0
/** * Creates an empty binding-type-specific field map that can be filled in * later steps. * <p>This method may be called multiple times and must erase any state * information from previous calls.</p> * @since 5.0 */
abstract protected void initializeFieldMap();
Gets the field for a given column position.
Params:
  • col – The column to find the field for
Throws:
Returns:BeanField containing the field for a given column position, or null if one could not be found
/** * Gets the field for a given column position. * * @param col The column to find the field for * @return BeanField containing the field for a given column position, or * null if one could not be found * @throws CsvBadConverterException If a custom converter for a field cannot * be initialized */
abstract protected BeanField<T, K> findField(int col);
Must be called once the length of input for a line/record is known to verify that the line was complete. Complete in this context means, no required fields are missing. The issue here is, as long as a column is present but empty, we can check whether the field is required and throw an exception if it is not, but if the data end prematurely, we never have this chance without indication that no more data are on the way. Another validation is that the number of fields must match the number of headers to prevent a data mismatch situation.
Params:
  • numberOfFields – The number of fields present in the line of input
Throws:
Since:4.0
/** * Must be called once the length of input for a line/record is known to * verify that the line was complete. * Complete in this context means, no required fields are missing. The issue * here is, as long as a column is present but empty, we can check whether * the field is required and throw an exception if it is not, but if the data * end prematurely, we never have this chance without indication that no more * data are on the way. * Another validation is that the number of fields must match the number of * headers to prevent a data mismatch situation. * * @param numberOfFields The number of fields present in the line of input * @throws CsvRequiredFieldEmptyException If a required column is missing * @since 4.0 */
abstract protected void verifyLineLength(int numberOfFields) throws CsvRequiredFieldEmptyException;
Implementation will return a bean of the type of object being mapped.
Throws:
Returns:A new instance of the class being mapped.
/** * Implementation will return a bean of the type of object being mapped. * * @return A new instance of the class being mapped. * @throws CsvBeanIntrospectionException Thrown on error creating object. * @throws IllegalStateException If the type of the bean has not been * initialized through {@link #setType(java.lang.Class)} */
protected Map<Class<?>, Object> createBean() throws CsvBeanIntrospectionException, IllegalStateException { if(type == null) { throw new IllegalStateException(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("type.unset")); } // Create the root bean and all beans underneath it Map<Class<?>, Object> instanceMap = new HashMap<>(); try { T rootBean = type.newInstance(); instanceMap.put(type, rootBean); createSubordinateBeans(recursiveTypeTree, instanceMap, rootBean); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { CsvBeanIntrospectionException csve = new CsvBeanIntrospectionException( ResourceBundle.getBundle( ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale) .getString("bean.instantiation.impossible")); csve.initCause(e); throw csve; } return instanceMap; } private static void createSubordinateBeans(RecursiveType typeTree, Map<Class<?>, Object> instanceMap, Object containingObject) throws InstantiationException, IllegalAccessException, InvocationTargetException { for(Map.Entry<FieldAccess<Object>, RecursiveType> entry : typeTree.getRecursiveMembers().entrySet()) { Object childObject = entry.getKey().getField(containingObject); if(childObject == null) { childObject = entry.getValue().type.newInstance(); entry.getKey().setField(containingObject, childObject); } instanceMap.put(entry.getValue().getType(), childObject); createSubordinateBeans(entry.getValue(), instanceMap, childObject); } }
Creates an index of necessary types according to the mapping strategy and existing instances of (subordinate) beans.
Params:
  • bean – The root bean to be indexed
Throws:
Returns:The index from type to instance
Since:5.0
/** * Creates an index of necessary types according to the mapping strategy * and existing instances of (subordinate) beans. * * @param bean The root bean to be indexed * @return The index from type to instance * @throws IllegalAccessException If there are problems accessing a * subordinate bean * @throws InvocationTargetException If there are problems accessing a * subordinate bean * @since 5.0 */
protected Map<Class<?>, Object> indexBean(T bean) throws IllegalAccessException, InvocationTargetException { Map<Class<?>, Object> instanceMap = new HashMap<>(); instanceMap.put(type, bean); indexSubordinateBeans(recursiveTypeTree, instanceMap, bean); return instanceMap; } private static void indexSubordinateBeans(RecursiveType typeTree, Map<Class<?>, Object> instanceMap, Object containingObject) throws IllegalAccessException, InvocationTargetException { for(Map.Entry<FieldAccess<Object>, RecursiveType> entry : typeTree.getRecursiveMembers().entrySet()) { Object childObject; if(containingObject == null) { childObject = null; } else { childObject = entry.getKey().getField(containingObject); } instanceMap.put(entry.getValue().getType(), childObject); indexSubordinateBeans(entry.getValue(), instanceMap, childObject); } }
Gets the name (or position number) of the header for the given column number. The column numbers are zero-based.
Params:
  • col – The column number for which the header is sought
Returns:The name of the header
/** * Gets the name (or position number) of the header for the given column * number. * The column numbers are zero-based. * * @param col The column number for which the header is sought * @return The name of the header */
abstract public String findHeader(int col);
This method generates a header that can be used for writing beans of the type provided back to a file.

The ordering of the headers is determined by the FieldMap in use.

This method should be called first by all overriding classes to make certain AbstractMappingStrategy<I,K,C,T>.headerIndex is properly initialized.

/** * This method generates a header that can be used for writing beans of the * type provided back to a file. * <p>The ordering of the headers is determined by the * {@link com.opencsv.bean.FieldMap} in use.</p> * <p>This method should be called first by all overriding classes to make * certain {@link #headerIndex} is properly initialized.</p> */
// The rest of the Javadoc is inherited @Override public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException { if(type == null) { throw new IllegalStateException(ResourceBundle .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale) .getString("type.before.header")); } // Always take what's been given or previously determined first. if(headerIndex.isEmpty()) { String[] header = getFieldMap().generateHeader(bean); headerIndex.initializeHeaderIndex(header); return header; } // Otherwise, put headers in the right places. return headerIndex.getHeaderIndex(); }
Get the column name for a given column position.
Params:
  • col – Column position.
Returns:The column name or null if the position is larger than the header array or there are no headers defined.
/** * Get the column name for a given column position. * * @param col Column position. * @return The column name or null if the position is larger than the * header array or there are no headers defined. */
String getColumnName(int col) { // headerIndex is never null because it's final return headerIndex.getByPosition(col); }
Get the class type that the Strategy is mapping.
Returns:Class of the object that this MappingStrategy will create.
/** * Get the class type that the Strategy is mapping. * * @return Class of the object that this {@link MappingStrategy} will create. */
public Class<? extends T> getType() { return type; } @SuppressWarnings("unchecked") @Override public T populateNewBean(String[] line) throws CsvBeanIntrospectionException, CsvRequiredFieldEmptyException, CsvDataTypeMismatchException, CsvConstraintViolationException, CsvValidationException { verifyLineLength(line.length); Map<Class<?>, Object> beanTree = createBean(); for (int col = 0; col < line.length; col++) { setFieldValue(beanTree, line[col], col); } return (T)beanTree.get(type); }
Sets the class type that is being mapped. Also initializes the mapping between column names and bean fields and attempts to create one example bean to be certain there are no fundamental problems with creation.
/** * Sets the class type that is being mapped. * Also initializes the mapping between column names and bean fields * and attempts to create one example bean to be certain there are no * fundamental problems with creation. */
// The rest of the Javadoc is inherited. @Override public void setType(Class<? extends T> type) throws CsvBadConverterException { this.type = type; loadFieldMap(); } @Override public void ignoreFields(MultiValuedMap<Class<?>, Field> fields) throws IllegalArgumentException { // Check input for consistency if(fields == null) { ignoredFields = new ArrayListValuedHashMap<>(); } else { ignoredFields = fields; MapIterator<Class<?>, Field> it = ignoredFields.mapIterator(); it.forEachRemaining(type -> { final Field f = it.getValue(); if(type == null || f == null || !f.getDeclaringClass().isAssignableFrom(type)) { throw new IllegalArgumentException(ResourceBundle.getBundle( ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale) .getString("ignore.field.inconsistent")); } }); } // Reload field map if(this.type != null) { loadFieldMap(); } }
Filters all fields that opencsv has been instructed to ignore and returns a list of the rest.
Params:
  • type – The class from which fields come. This must be the class as opencsv would seek to instantiate it, which in the case of inheritance is not necessarily the declaring class.
  • fields – The fields to be filtered
Returns:A list of fields that exist for opencsv
/** * Filters all fields that opencsv has been instructed to ignore and * returns a list of the rest. * @param type The class from which {@code fields} come. This must be the * class as opencsv would seek to instantiate it, which in the * case of inheritance is not necessarily the declaring class. * @param fields The fields to be filtered * @return A list of fields that exist for opencsv */
private List<Field> filterIgnoredFields(final Class<?> type, Field[] fields) { return Stream.of(fields) .filter(f -> !ignoredFields.containsMapping(type, f) && !f.isAnnotationPresent(CsvIgnore.class)) .collect(Collectors.toList()); }
Builds a map of columns from the input to fields of the bean type.
Throws:
  • CsvBadConverterException – If there is a problem instantiating the custom converter for an annotated field
/** * Builds a map of columns from the input to fields of the bean type. * * @throws CsvBadConverterException If there is a problem instantiating the * custom converter for an annotated field */
protected void loadFieldMap() throws CsvBadConverterException { // Setup initializeFieldMap(); // Deal with embedded classes through recursion recursiveTypeTree = loadRecursiveClasses(this.type, new HashSet<>()); // Populate the field map according to annotations or not Map<Boolean, ListValuedMap<Class<?>, Field>> partitionedFields = partitionFields(); if(!partitionedFields.get(Boolean.TRUE).isEmpty()) { loadAnnotatedFieldMap(partitionedFields.get(Boolean.TRUE)); } else { loadUnadornedFieldMap(partitionedFields.get(Boolean.FALSE)); } }
Creates a tree of beans embedded in each other. These are the member variables annotated with CsvRecurse and their associated types. This method is used recursively.
Params:
  • newType – The type that is meant to be added to the tree
  • encounteredTypes – A set of types already encountered during recursion, as types may not be recursed into more than once.
Throws:
  • CsvRecursionException – If recursion is attempted into a primitive type or a previously encountered type is added again or a member variable annotated with CsvRecurse is also annotated with a binding annotation
Returns:A representation of this type and all of the types beneath it in a tree
/** * Creates a tree of beans embedded in each other. * These are the member variables annotated with {@link CsvRecurse} and * their associated types. This method is used recursively. * * @param newType The type that is meant to be added to the tree * @param encounteredTypes A set of types already encountered during * recursion, as types may not be recursed into * more than once. * @return A representation of this type and all of the types beneath it in * a tree * @throws CsvRecursionException If recursion is attempted into a primitive * type or a previously encountered type is added again or a member * variable annotated with {@link CsvRecurse} is also annotated with a * binding annotation */
protected RecursiveType loadRecursiveClasses(Class<?> newType, Set<Class<?>> encounteredTypes) { // We cannot recurse into primitive types if (FORBIDDEN_CLASSES_FOR_RECURSION.contains(newType)) { throw new CsvRecursionException( ResourceBundle.getBundle( ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale) .getString("recursion.on.primitive"), newType); } // Guard against the same type being used twice if(encounteredTypes.contains(newType)) { throw new CsvRecursionException(String.format(ResourceBundle .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale) .getString("recursive.type.encountered.twice"), newType.toString()), newType); } encounteredTypes.add(newType); // Find types to recurse through RecursiveType localRecursiveTypeTree = new RecursiveType(newType); for(Field f : filterIgnoredFields(newType, FieldUtils.getFieldsWithAnnotation(newType, CsvRecurse.class))) { // Types that are recursed into cannot also be bound Set<Class<? extends Annotation>> bindingAnnotations = getBindingAnnotations(); if(bindingAnnotations.stream().anyMatch(f::isAnnotationPresent)) { throw new CsvRecursionException( ResourceBundle.getBundle( ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale) .getString("recursion.binding.mutually.exclusive"), f.getType()); } // Recurse into that type localRecursiveTypeTree.addRecursiveMember( new FieldAccess<>(f), loadRecursiveClasses(f.getType(), encounteredTypes)); } return localRecursiveTypeTree; }
Creates a non-tree (fairly flat) representation of all of the fields bound from all types. This method is used recursively.
Params:
  • root – The root of the type tree at this level of recursion
  • encounteredFields – A collection of all fields thus far included in the new representation. This collection will be added to and is the result of this method.
/** * Creates a non-tree (fairly flat) representation of all of the fields * bound from all types. * This method is used recursively. * @param root The root of the type tree at this level of recursion * @param encounteredFields A collection of all fields thus far included * in the new representation. This collection will * be added to and is the result of this method. */
private void assembleCompleteFieldList(RecursiveType root, final ListValuedMap<Class<?>, Field> encounteredFields) { encounteredFields.putAll(root.type, filterIgnoredFields(root.type, FieldUtils.getAllFields(root.type))); root.getRecursiveMembers().values().forEach(f -> assembleCompleteFieldList(f, encounteredFields)); }
Partitions all non-synthetic fields of the bean type being processed into annotated and non-annotated fields according to getBindingAnnotations().
Returns:A multi-valued map (class to multiple fields in that class) in which all annotated fields are mapped under Boolean.TRUE, and all non-annotated fields are mapped under Boolean.FALSE.
Since:5.0
/** * Partitions all non-synthetic fields of the bean type being processed * into annotated and non-annotated fields according to * {@link #getBindingAnnotations()}. * * @return A multi-valued map (class to multiple fields in that class) in * which all annotated fields are mapped under {@link Boolean#TRUE}, and * all non-annotated fields are mapped under {@link Boolean#FALSE}. * @since 5.0 */
protected Map<Boolean, ListValuedMap<Class<?>, Field>> partitionFields() { // Get a flat list of all fields ListValuedMap<Class<?>, Field> allFields = new ArrayListValuedHashMap<>(); assembleCompleteFieldList(recursiveTypeTree, allFields); // Determine which annotations need be considered final Set<Class<? extends Annotation>> bindingAnnotations = getBindingAnnotations(); // Split the fields (with associated types) into annotated and // non-annotated Map<Boolean, ListValuedMap<Class<?>, Field>> returnValue = new TreeMap<>(); returnValue.put(Boolean.TRUE, new ArrayListValuedHashMap<>()); returnValue.put(Boolean.FALSE, new ArrayListValuedHashMap<>()); allFields.entries().stream() .filter(entry -> !entry.getValue().isSynthetic()) .forEach(entry -> { if(bindingAnnotations.stream() .anyMatch(a -> entry.getValue().isAnnotationPresent(a))) { returnValue.get(Boolean.TRUE).put(entry.getKey(), entry.getValue()); } else { returnValue.get(Boolean.FALSE).put(entry.getKey(), entry.getValue()); } }); return returnValue; }
Attempts to instantiate the class of the custom converter specified.
Params:
  • converter – The class for a custom converter
Throws:
Returns:The custom converter
/** * Attempts to instantiate the class of the custom converter specified. * * @param converter The class for a custom converter * @return The custom converter * @throws CsvBadConverterException If the class cannot be instantiated */
protected BeanField<T, K> instantiateCustomConverter(Class<? extends AbstractBeanField<T, K>> converter) throws CsvBadConverterException { try { BeanField<T, K> c = converter.newInstance(); c.setErrorLocale(errorLocale); return c; } catch (IllegalAccessException | InstantiationException oldEx) { CsvBadConverterException newEx = new CsvBadConverterException(converter, String.format(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("custom.converter.invalid"), converter.getCanonicalName())); newEx.initCause(oldEx); throw newEx; } } @Override public void setErrorLocale(Locale errorLocale) { this.errorLocale = ObjectUtils.defaultIfNull(errorLocale, Locale.getDefault()); // It's very possible that setType() was called first, which creates all // of the BeanFields, so we need to go back through the list and correct // them all. if(getFieldMap() != null) { getFieldMap().setErrorLocale(this.errorLocale); getFieldMap().values().forEach(f -> f.setErrorLocale(this.errorLocale)); } }
Populates the field corresponding to the column position indicated of the bean passed in according to the rules of the mapping strategy. This method performs conversion on the input string and assigns the result to the proper field in the provided bean.
Params:
  • beanTree – Object containing the field to be set.
  • value – String containing the value to set the field to.
  • column – The column position from the CSV file under which this value was found.
Throws:
Since:4.2
/** * Populates the field corresponding to the column position indicated of the * bean passed in according to the rules of the mapping strategy. * This method performs conversion on the input string and assigns the * result to the proper field in the provided bean. * * @param beanTree Object containing the field to be set. * @param value String containing the value to set the field to. * @param column The column position from the CSV file under which this * value was found. * @throws CsvDataTypeMismatchException When the result of data conversion returns * an object that cannot be assigned to the selected field * @throws CsvRequiredFieldEmptyException When a field is mandatory, but there is no * input datum in the CSV file * @throws CsvConstraintViolationException When the internal structure of * data would be violated by the data in the CSV file * @throws CsvValidationException If a user-supplied validator determines * that the input is invalid * @since 4.2 */
protected void setFieldValue(Map<Class<?>, Object> beanTree, String value, int column) throws CsvDataTypeMismatchException, CsvRequiredFieldEmptyException, CsvConstraintViolationException, CsvValidationException { BeanField<T, K> beanField = findField(column); if (beanField != null) { Object subordinateBean = beanTree.get(beanField.getType()); beanField.setFieldValue(subordinateBean, value, findHeader(column)); } } @Override public String[] transmuteBean(T bean) throws CsvDataTypeMismatchException, CsvRequiredFieldEmptyException { int numColumns = headerIndex.findMaxIndex()+1; BeanField<T, K> firstBeanField, subsequentBeanField; K firstIndex, subsequentIndex; List<String> contents = new ArrayList<>(Math.max(numColumns, 0)); // Create a map of types to instances of subordinate beans Map<Class<?>, Object> instanceMap; try { instanceMap = indexBean(bean); } catch(IllegalAccessException | InvocationTargetException e) { // Our testing indicates these exceptions probably can't be thrown, // but they're declared, so we have to deal with them. It's an // alibi catch block. CsvBeanIntrospectionException csve = new CsvBeanIntrospectionException( ResourceBundle.getBundle( ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale) .getString("error.introspecting.beans")); csve.initCause(e); throw csve; } for(int i = 0; i < numColumns;) { // Determine the first value firstBeanField = findField(i); firstIndex = chooseMultivaluedFieldIndexFromHeaderIndex(i); String[] fields = firstBeanField != null ? firstBeanField.write(instanceMap.get(firstBeanField.getType()), firstIndex) : ArrayUtils.EMPTY_STRING_ARRAY; if(fields.length == 0) { // Write the only value contents.add(StringUtils.EMPTY); i++; // Advance the index } else { // Multiple values. Write the first. contents.add(StringUtils.defaultString(fields[0])); // Now write the rest. // We must make certain that we don't write more fields // than we have columns of the correct type to cover them. int j = 1; int displacedIndex = i+j; subsequentBeanField = findField(displacedIndex); subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(displacedIndex); while(j < fields.length && displacedIndex < numColumns && Objects.equals(firstBeanField, subsequentBeanField) && Objects.equals(firstIndex, subsequentIndex)) { // This field still has a header, so add it contents.add(StringUtils.defaultString(fields[j])); // Prepare for the next loop through displacedIndex = i + (++j); subsequentBeanField = findField(displacedIndex); subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(displacedIndex); } i = displacedIndex; // Advance the index // And here's where we fill in any fields that are missing to // cover the number of columns of the same type if(i < numColumns) { subsequentBeanField = findField(i); subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(i); while(Objects.equals(firstBeanField, subsequentBeanField) && Objects.equals(firstIndex, subsequentIndex) && i < numColumns) { contents.add(StringUtils.EMPTY); subsequentBeanField = findField(++i); subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(i); } } } } return contents.toArray(ArrayUtils.EMPTY_STRING_ARRAY); }
Given the information provided, determines the appropriate built-in converter to be passed in to the BeanField being created.
Params:
  • field – The field of the bean type in question
  • elementType – The type to be generated by the converter (on reading)
  • locale – The locale for conversion on reading. May be null or an empty string if a locale is not in use.
  • writeLocale – The locale for conversion on writing. May be null or an empty string if a locale is not in use.
  • customConverter – An optional custom converter
Throws:
Returns:The appropriate converter for the necessary conversion
Since:4.2
/** * Given the information provided, determines the appropriate built-in * converter to be passed in to the {@link BeanField} being created. * * @param field The field of the bean type in question * @param elementType The type to be generated by the converter (on reading) * @param locale The locale for conversion on reading. May be null or an * empty string if a locale is not in use. * @param writeLocale The locale for conversion on writing. May be null or * an empty string if a locale is not in use. * @param customConverter An optional custom converter * @return The appropriate converter for the necessary conversion * @throws CsvBadConverterException If the converter cannot be instantiated * * @since 4.2 */
protected CsvConverter determineConverter(Field field, Class<?> elementType, String locale, String writeLocale, Class<? extends AbstractCsvConverter> customConverter) throws CsvBadConverterException { CsvConverter converter; // A custom converter always takes precedence if specified. if(customConverter != null && !customConverter.equals(AbstractCsvConverter.class)) { try { converter = customConverter.newInstance(); } catch (IllegalAccessException | InstantiationException oldEx) { CsvBadConverterException newEx = new CsvBadConverterException(customConverter, String.format(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("custom.converter.invalid"), customConverter.getCanonicalName())); newEx.initCause(oldEx); throw newEx; } converter.setType(elementType); converter.setLocale(locale); converter.setWriteLocale(writeLocale); converter.setErrorLocale(errorLocale); } // Perhaps a date instead else if (field.isAnnotationPresent(CsvDate.class)) { CsvDate annotation = field.getAnnotation(CsvDate.class); String readFormat = annotation.value(); String writeFormat = annotation.writeFormatEqualsReadFormat() ? readFormat : annotation.writeFormat(); String readChrono = annotation.chronology(); String writeChrono = annotation.writeChronologyEqualsReadChronology() ? readChrono : annotation.writeChronology(); converter = new ConverterDate(elementType, locale, writeLocale, errorLocale, readFormat, writeFormat, readChrono, writeChrono); } // Or a number else if(field.isAnnotationPresent(CsvNumber.class)) { CsvNumber annotation = field.getAnnotation(CsvNumber.class); String readFormat = annotation.value(); String writeFormat = annotation.writeFormatEqualsReadFormat() ? readFormat : annotation.writeFormat(); converter = new ConverterNumber(elementType, locale, writeLocale, errorLocale, readFormat, writeFormat); } // Otherwise it must be a primitive else { converter = new ConverterPrimitiveTypes( elementType, locale, writeLocale, errorLocale); } return converter; }
Encapsulates a bean type and all of the member variables that need to be recursed into.
/** * Encapsulates a bean type and all of the member variables that need to be * recursed into. */
private static class RecursiveType { private final Class<?> type; private final Map<FieldAccess<Object>, RecursiveType> recursiveMembers = new HashMap<>(); RecursiveType(Class<?> type) { this.type = type; } public Class<?> getType() {return type;} public RecursiveType addRecursiveMember(FieldAccess<Object> member, RecursiveType memberType) { return recursiveMembers.put(member, memberType); } public Map<FieldAccess<Object>, RecursiveType> getRecursiveMembers() {return recursiveMembers;} } }