All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.opencsv.bean.AbstractMappingStrategy Maven / Gradle / Ivy

There is a newer version: 5.9
Show newest version
/*
 * 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.SetUtils;
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.function.Function;

/**
 * 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 {@link #getBindingAnnotations()} they are not in use.

*

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

* * @param Type of object that is being processed. * @param The type of the internal many-to-one mapping * @param The initializer type used to build the internal many-to-one mapping * @param The type of the key used for internal indexing * * @author Andrew Rucker Jones * @since 4.2 */ public abstract class AbstractMappingStrategy, C extends ComplexFieldMapEntry, T> implements MappingStrategy { /** * 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 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. */ protected Class type; /** * 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 {@link CsvRecurse}. */ private RecursiveType recursiveTypeTree; /** Storage for all manually excluded class/field pairs. */ private MultiValuedMap, Field> ignoredFields = new ArrayListValuedHashMap<>(); /** Locale for error messages. */ protected Locale errorLocale = Locale.getDefault(); /** The profile for configuring bean fields. */ protected String profile = StringUtils.EMPTY; /** * 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) } */ protected abstract K chooseMultivaluedFieldIndexFromHeaderIndex(int index); /** * Returns the {@link FieldMap} associated with this mapping strategy. * * @return The {@link FieldMap} used by this strategy */ protected abstract FieldMap getFieldMap(); /** * 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> getBindingAnnotations() {return Collections.emptySet();} /** * Creates a map of annotated fields in the bean to be processed. *

This method is called by {@link #loadFieldMap()} when at least one * relevant annotation is found on a member variable.

*

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

* * @param fields A list of fields annotated with a binding annotation * in the bean to be processed * @since 5.0 */ protected void loadAnnotatedFieldMap(ListValuedMap, Field> fields) {} /** * 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 */ protected abstract void loadUnadornedFieldMap(ListValuedMap, 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 */ protected abstract void initializeFieldMap(); /** * 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 */ protected abstract BeanField 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. * * @param numberOfFields The number of fields present in the line of input * @throws CsvRequiredFieldEmptyException If a required column is missing * @since 4.0 */ protected abstract void verifyLineLength(int numberOfFields) throws CsvRequiredFieldEmptyException; /** * 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, 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, 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, Object> instanceMap, Object containingObject) throws InstantiationException, IllegalAccessException, InvocationTargetException { for(Map.Entry, 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. * * @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, Object> indexBean(T bean) throws IllegalAccessException, InvocationTargetException { Map, Object> instanceMap = new HashMap<>(); instanceMap.put(type, bean); indexSubordinateBeans(recursiveTypeTree, instanceMap, bean); return instanceMap; } private static void indexSubordinateBeans(RecursiveType typeTree, Map, Object> instanceMap, Object containingObject) throws IllegalAccessException, InvocationTargetException { for(Map.Entry, 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. * * @param col The column number for which the header is sought * @return The name of the header */ public abstract 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 * {@link com.opencsv.bean.FieldMap} in use.

*

This method should be called first by all overriding classes to make * certain {@link #headerIndex} is properly initialized.

*/ // 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. * * @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. * * @return Class of the object that this {@link MappingStrategy} will create. */ public Class getType() { return type; } @SuppressWarnings("unchecked") @Override public T populateNewBean(String[] line) throws CsvBeanIntrospectionException, CsvFieldAssignmentException, CsvChainedException { verifyLineLength(line.length); Map, Object> beanTree = createBean(); CsvChainedException chainedException = null; for (int col = 0; col < line.length; col++) { try { setFieldValue(beanTree, line[col], col); } catch (CsvFieldAssignmentException e) { if(chainedException != null) { chainedException.add(e); } else { chainedException = new CsvChainedException(e); } } } if(chainedException != null) { if (chainedException.hasOnlyOneException()) { throw chainedException.getFirstException(); } throw chainedException; } 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. */ // The rest of the Javadoc is inherited. @Override public void setType(Class type) throws CsvBadConverterException { this.type = type; loadFieldMap(); } /** * Sets the profile this mapping strategy will use when configuring bean * fields. */ // The rest of the Javadoc is inherited. @Override public void setProfile(String profile) { this.profile = StringUtils.defaultString(profile); } @Override public void ignoreFields(MultiValuedMap, Field> fields) throws IllegalArgumentException { // Check input for consistency if(fields == null) { ignoredFields = new ArrayListValuedHashMap<>(); } else { ignoredFields = fields; MapIterator, Field> it = ignoredFields.mapIterator(); it.forEachRemaining(t -> { final Field f = it.getValue(); if (t == null || f == null || !f.getDeclaringClass().isAssignableFrom(t)) { 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. * @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 */ protected List filterIgnoredFields(final Class type, Field[] fields) { final List filteredFields = new LinkedList<>(); for(Field f : fields) { CsvIgnore ignoreAnnotation = f.getAnnotation(CsvIgnore.class); Set ignoredProfiles = ignoreAnnotation == null ? SetUtils.emptySet() : new HashSet(Arrays.asList(ignoreAnnotation.profiles())); // This is easier in Java 9 with Set.of() if(!ignoredFields.containsMapping(type, f) && !ignoredProfiles.contains(profile) && !ignoredProfiles.contains(StringUtils.EMPTY)) { filteredFields.add(f); } } return filteredFields; } /** * 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, Field>> partitionedFields = partitionFields(); if(!partitionedFields.get(Boolean.TRUE).isEmpty()) { loadAnnotatedFieldMap(partitionedFields.get(Boolean.TRUE)); } else { loadUnadornedFieldMap(partitionedFields.get(Boolean.FALSE)); } } /** * @param type Class to be checked * @return Whether the type may be recursed into ({@code false}), or * must be considered a leaf node for recursion ({@code true}). This * implementation considers the boxed primitives forbidden. */ protected boolean isForbiddenClassForRecursion(Class type) { return FORBIDDEN_CLASSES_FOR_RECURSION.contains(type); } /** * 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> encounteredTypes) { // We cannot recurse into primitive types if (isForbiddenClassForRecursion(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> 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. * @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, 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 * {@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, Field>> partitionFields() { // Get a flat list of all fields ListValuedMap, Field> allFields = new ArrayListValuedHashMap<>(); assembleCompleteFieldList(recursiveTypeTree, allFields); // Determine which annotations need be considered final Set> bindingAnnotations = getBindingAnnotations(); // Split the fields (with associated types) into annotated and // non-annotated Map, 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. * * @param converter The class for a custom converter * @return The custom converter * @throws CsvBadConverterException If the class cannot be instantiated */ protected BeanField instantiateCustomConverter(Class> converter) throws CsvBadConverterException { try { BeanField 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. * * @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, Object> beanTree, String value, int column) throws CsvDataTypeMismatchException, CsvRequiredFieldEmptyException, CsvConstraintViolationException, CsvValidationException { BeanField 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 CsvFieldAssignmentException, CsvChainedException { int numColumns = headerIndex.findMaxIndex()+1; BeanField firstBeanField, subsequentBeanField; K firstIndex, subsequentIndex; List contents = new ArrayList<>(Math.max(numColumns, 0)); // Create a map of types to instances of subordinate beans Map, 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; } CsvChainedException chainedException = null; for(int i = 0; i < numColumns;) { // Determine the first value firstBeanField = findField(i); firstIndex = chooseMultivaluedFieldIndexFromHeaderIndex(i); String[] fields = ArrayUtils.EMPTY_STRING_ARRAY; if(firstBeanField != null) { try { fields = firstBeanField.write(instanceMap.get(firstBeanField.getType()), firstIndex); } catch(CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) { if(chainedException != null) { chainedException.add(e); } else { chainedException = new CsvChainedException(e); } } } 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); } } } } // If there were exceptions, throw them if(chainedException != null) { if (chainedException.hasOnlyOneException()) { throw chainedException.getFirstException(); } throw chainedException; } return contents.toArray(ArrayUtils.EMPTY_STRING_ARRAY); } /** * 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 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) || field.isAnnotationPresent(CsvDates.class)) { CsvDate annotation = selectAnnotationForProfile( field.getAnnotationsByType(CsvDate.class), CsvDate::profiles); if(annotation == null) { throw new CsvBadConverterException(CsvDate.class, String.format( ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME).getString("profile.not.found.date"), profile)); } 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) || field.isAnnotationPresent(CsvNumbers.class)) { CsvNumber annotation = selectAnnotationForProfile( field.getAnnotationsByType(CsvNumber.class), CsvNumber::profiles); if(annotation == null) { throw new CsvBadConverterException(CsvNumber.class, String.format( ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME).getString("profile.not.found.number"), profile)); } String readFormat = annotation.value(); String writeFormat = annotation.writeFormatEqualsReadFormat() ? readFormat : annotation.writeFormat(); converter = new ConverterNumber(elementType, locale, writeLocale, errorLocale, readFormat, writeFormat); } // or a Currency else if (elementType.equals(java.util.Currency.class)){ converter = new ConverterCurrency(errorLocale); } // Or an enumeration else if (elementType.isEnum()) { converter = new ConverterEnum(elementType, locale, writeLocale, errorLocale); } // or an UUID else if (elementType.equals(UUID.class)) { converter = new ConverterUUID(errorLocale); } // Otherwise a primitive else { converter = new ConverterPrimitiveTypes(elementType, locale, writeLocale, errorLocale); } return converter; } /** * Determines which one of a list of annotations applies to the currently * selected profile. * If no annotation specific to the profile is found, the annotation for * the default profile is returned. If neither is found, {@code null} is * returned. * * @param annotations All annotations of a given type * @param getProfiles A function mapping an annotation of type {@code A} to * the list of profiles it applies to * @param The annotation type being tested * @return The annotation with the appropriate profile or {@code null} if * nothing appropriate is found * @since 5.4 */ protected A selectAnnotationForProfile(A[] annotations, Function getProfiles) { A defaultAnnotation = null; String[] profilesForAnnotation; for(A annotation : annotations) { profilesForAnnotation = getProfiles.apply(annotation); for(String p : profilesForAnnotation) { if(profile.equals(p)) { return annotation; // I know. Bad style. I think we can live with it once. } if(StringUtils.EMPTY.equals(p)) { defaultAnnotation = annotation; } } } return defaultAnnotation; } /** * Encapsulates a bean type and all of the member variables that need to be * recursed into. */ protected static class RecursiveType { private final Class type; private final Map, RecursiveType> recursiveMembers = new HashMap<>(); /** * Constructs a {@link RecursiveType} with the specified type. * * @param type Type associated with this branch */ RecursiveType(Class type) { this.type = type; } /** * @return Type associated with this branch */ public Class getType() { return type; } /** * Used to add a recursive type. * * @param member Field access member to add a recursive type to * @param memberType {@link RecursiveType} to add */ public void addRecursiveMember(FieldAccess member, RecursiveType memberType) { recursiveMembers.put(member, memberType); } /** * @return {@link Map} of field access to {@link RecursiveType}. */ public Map, RecursiveType> getRecursiveMembers() { return recursiveMembers; } } }