1 /*
2 * Copyright 2018 Andrew Rucker Jones.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.opencsv.bean;
17
18 import com.opencsv.ICSVParser;
19 import com.opencsv.exceptions.*;
20 import org.apache.commons.collections4.ListValuedMap;
21 import org.apache.commons.collections4.MapIterator;
22 import org.apache.commons.collections4.MultiValuedMap;
23 import org.apache.commons.collections4.SetUtils;
24 import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
25 import org.apache.commons.lang3.ArrayUtils;
26 import org.apache.commons.lang3.ObjectUtils;
27 import org.apache.commons.lang3.StringUtils;
28 import org.apache.commons.lang3.reflect.FieldUtils;
29
30 import java.lang.annotation.Annotation;
31 import java.lang.reflect.Field;
32 import java.lang.reflect.InvocationTargetException;
33 import java.util.*;
34 import java.util.function.Function;
35
36 /**
37 * This class collects as many generally useful parts of the implementation
38 * of a mapping strategy as possible.
39 * <p>This mapping strategy knows of the existence of binding annotations, but
40 * assumes through {@link #getBindingAnnotations()} they are not in use.</p>
41 * <p>Anyone is welcome to use it as a base class for their own mapping
42 * strategies.</p>
43 *
44 * @param <T> Type of object that is being processed.
45 * @param <C> The type of the internal many-to-one mapping
46 * @param <I> The initializer type used to build the internal many-to-one mapping
47 * @param <K> The type of the key used for internal indexing
48 *
49 * @author Andrew Rucker Jones
50 * @since 4.2
51 */
52 public abstract class AbstractMappingStrategy<I, K extends Comparable<K>, C extends ComplexFieldMapEntry<I, K, T>, T> implements MappingStrategy<T> {
53
54 /**
55 * Set of classes where recursion is not allowed. Using HashSet because, given the large number of types, the
56 * contains method is quicker than an Array or ArrayList (Granted the number where Set is more efficient is different
57 * per Java release and system configuration). And being a Set we are noting that each value is unique.
58 */
59 // This is easier in Java 9 with Set.of()
60 private static final Set<Class<?>> FORBIDDEN_CLASSES_FOR_RECURSION = new HashSet<>(Arrays.asList(Byte.TYPE, Short.TYPE,
61 Integer.TYPE, Float.TYPE, Double.TYPE, Boolean.TYPE, Long.TYPE, Character.TYPE));
62
63 /** This is the class of the bean to be manipulated. */
64 protected Class<? extends T> type;
65
66 /**
67 * Maintains a bi-directional mapping between column position(s) and header
68 * name.
69 */
70 protected final HeaderIndex headerIndex = new HeaderIndex();
71
72 /**
73 * A tree of the types encountered during recursion through the root bean
74 * type.
75 * These are only the types (and associated fields) specifically annotated
76 * with {@link CsvRecurse}.
77 */
78 protected RecursiveType recursiveTypeTree;
79
80 /** Storage for all manually excluded class/field pairs. */
81 private MultiValuedMap<Class<?>, Field> ignoredFields = new ArrayListValuedHashMap<>();
82
83 /** Locale for error messages. */
84 protected Locale errorLocale = Locale.getDefault();
85
86 /** The profile for configuring bean fields. */
87 protected String profile = StringUtils.EMPTY;
88
89 /**
90 * For {@link BeanField#indexAndSplitMultivaluedField(java.lang.Object, java.lang.Object)}
91 * it is necessary to determine which index to pass in.
92 *
93 * @param index The current column position while transmuting a bean to CSV
94 * output
95 * @return The index to be used for this mapping strategy for
96 * {@link BeanField#indexAndSplitMultivaluedField(java.lang.Object, java.lang.Object) }
97 */
98 protected abstract K chooseMultivaluedFieldIndexFromHeaderIndex(int index);
99
100 /**
101 * Returns the {@link FieldMap} associated with this mapping strategy.
102 *
103 * @return The {@link FieldMap} used by this strategy
104 */
105 protected abstract FieldMap<I, K, ? extends C, T> getFieldMap();
106
107 /**
108 * Returns a set of the annotations that are used for binding in this
109 * mapping strategy.
110 * The default implementation returns the empty set.
111 *
112 * @return Annotations of the sort {@link CsvBindByName} or
113 * {@link CsvBindByPosition} that are relevant for binding input fields to
114 * bean members in this mapping strategy
115 * @since 5.0
116 */
117 protected Set<Class<? extends Annotation>> getBindingAnnotations() {return Collections.emptySet();}
118
119 /**
120 * Creates a map of annotated fields in the bean to be processed.
121 * <p>This method is called by {@link #loadFieldMap()} when at least one
122 * relevant annotation is found on a member variable.</p>
123 * <p>The default implementation assumes there are no annotations and does
124 * nothing.</p>
125 *
126 * @param fields A list of fields annotated with a binding annotation
127 * in the bean to be processed
128 * @since 5.0
129 */
130 protected void loadAnnotatedFieldMap(ListValuedMap<Class<?>, Field> fields) {}
131
132 /**
133 * Creates a map of fields in the bean to be processed that have no
134 * annotations.
135 * This method is called by {@link #loadFieldMap()} when absolutely no
136 * annotations that are relevant for this mapping strategy are found in the
137 * type of bean being processed.
138 *
139 * @param fields A list of all non-synthetic fields in the bean to be
140 * processed
141 * @since 5.0
142 */
143 protected abstract void loadUnadornedFieldMap(ListValuedMap<Class<?>, Field> fields);
144
145 /**
146 * Creates an empty binding-type-specific field map that can be filled in
147 * later steps.
148 * <p>This method may be called multiple times and must erase any state
149 * information from previous calls.</p>
150 *
151 * @since 5.0
152 */
153 protected abstract void initializeFieldMap();
154
155 /**
156 * Gets the field for a given column position.
157 *
158 * @param col The column to find the field for
159 * @return BeanField containing the field for a given column position, or
160 * null if one could not be found
161 * @throws CsvBadConverterException If a custom converter for a field cannot
162 * be initialized
163 */
164 protected abstract BeanField<T, K> findField(int col);
165
166 /**
167 * Must be called once the length of input for a line/record is known to
168 * verify that the line was complete.
169 * Complete in this context means, no required fields are missing. The issue
170 * here is, as long as a column is present but empty, we can check whether
171 * the field is required and throw an exception if it is not, but if the data
172 * end prematurely, we never have this chance without indication that no more
173 * data are on the way.
174 * Another validation is that the number of fields must match the number of
175 * headers to prevent a data mismatch situation.
176 *
177 * @param numberOfFields The number of fields present in the line of input
178 * @throws CsvRequiredFieldEmptyException If a required column is missing
179 * @since 4.0
180 */
181 protected abstract void verifyLineLength(int numberOfFields) throws CsvRequiredFieldEmptyException;
182
183 /**
184 * Implementation will return a bean of the type of object being mapped.
185 *
186 * @return A new instance of the class being mapped.
187 * @throws CsvBeanIntrospectionException Thrown on error creating object.
188 * @throws IllegalStateException If the type of the bean has not been
189 * initialized through {@link #setType(java.lang.Class)}
190 */
191 protected Map<Class<?>, Object> createBean()
192 throws CsvBeanIntrospectionException, IllegalStateException {
193 if(type == null) {
194 throw new IllegalStateException(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("type.unset"));
195 }
196
197 // Create the root bean and all beans underneath it
198 Map<Class<?>, Object> instanceMap = new HashMap<>();
199 try {
200 T rootBean = type.getDeclaredConstructor().newInstance();
201 instanceMap.put(type, rootBean);
202 createSubordinateBeans(recursiveTypeTree, instanceMap, rootBean);
203 } catch (InstantiationException | IllegalAccessException | InvocationTargetException | IllegalArgumentException | NoSuchMethodException | SecurityException e) {
204 CsvBeanIntrospectionException csve = new CsvBeanIntrospectionException(
205 ResourceBundle.getBundle(
206 ICSVParser.DEFAULT_BUNDLE_NAME,
207 errorLocale)
208 .getString("bean.instantiation.impossible"));
209 csve.initCause(e);
210 throw csve;
211 }
212
213 return instanceMap;
214 }
215
216 private static void createSubordinateBeans(RecursiveType typeTree, Map<Class<?>, Object> instanceMap, Object containingObject)
217 throws InstantiationException, IllegalAccessException, InvocationTargetException, IllegalArgumentException, NoSuchMethodException, SecurityException {
218 for(Map.Entry<FieldAccess<Object>, RecursiveType> entry : typeTree.getRecursiveMembers().entrySet()) {
219 Object childObject = entry.getKey().getField(containingObject);
220 if(childObject == null) {
221 childObject = entry.getValue().type.getDeclaredConstructor().newInstance();
222 entry.getKey().setField(containingObject, childObject);
223 }
224 instanceMap.put(entry.getValue().getType(), childObject);
225 createSubordinateBeans(entry.getValue(), instanceMap, childObject);
226 }
227 }
228
229 /**
230 * Creates an index of necessary types according to the mapping strategy
231 * and existing instances of (subordinate) beans.
232 *
233 * @param bean The root bean to be indexed
234 * @return The index from type to instance
235 * @throws IllegalAccessException If there are problems accessing a
236 * subordinate bean
237 * @throws InvocationTargetException If there are problems accessing a
238 * subordinate bean
239 * @since 5.0
240 */
241 protected Map<Class<?>, Object> indexBean(T bean)
242 throws IllegalAccessException, InvocationTargetException {
243 Map<Class<?>, Object> instanceMap = new HashMap<>();
244 instanceMap.put(type, bean);
245 indexSubordinateBeans(recursiveTypeTree, instanceMap, bean);
246 return instanceMap;
247 }
248
249 private static void indexSubordinateBeans(RecursiveType typeTree, Map<Class<?>, Object> instanceMap, Object containingObject)
250 throws IllegalAccessException, InvocationTargetException {
251 for(Map.Entry<FieldAccess<Object>, RecursiveType> entry : typeTree.getRecursiveMembers().entrySet()) {
252 Object childObject;
253 if(containingObject == null) {
254 childObject = null;
255 }
256 else {
257 childObject = entry.getKey().getField(containingObject);
258 }
259 instanceMap.put(entry.getValue().getType(), childObject);
260 indexSubordinateBeans(entry.getValue(), instanceMap, childObject);
261 }
262 }
263
264 /**
265 * Gets the name (or position number) of the header for the given column
266 * number.
267 * The column numbers are zero-based.
268 *
269 * @param col The column number for which the header is sought
270 * @return The name of the header
271 */
272 public abstract String findHeader(int col);
273
274 /**
275 * This method generates a header that can be used for writing beans of the
276 * type provided back to a file.
277 * <p>The ordering of the headers is determined by the
278 * {@link com.opencsv.bean.FieldMap} in use.</p>
279 * <p>This method should be called first by all overriding classes to make
280 * certain {@link #headerIndex} is properly initialized.</p>
281 */
282 // The rest of the Javadoc is inherited
283 @Override
284 public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
285 if(type == null) {
286 throw new IllegalStateException(ResourceBundle
287 .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
288 .getString("type.before.header"));
289 }
290
291 // Always take what's been given or previously determined first.
292 if(headerIndex.isEmpty()) {
293 String[] header = getFieldMap().generateHeader(bean);
294 headerIndex.initializeHeaderIndex(header);
295 return header;
296 }
297
298 // Otherwise, put headers in the right places.
299 return headerIndex.getHeaderIndex();
300 }
301
302 /**
303 * Get the column name for a given column position.
304 *
305 * @param col Column position.
306 * @return The column name or null if the position is larger than the
307 * header array or there are no headers defined.
308 */
309 protected String getColumnName(int col) {
310 // headerIndex is never null because it's final
311 return headerIndex.getByPosition(col);
312 }
313
314 /**
315 * Get the class type that the strategy is mapping.
316 *
317 * @return Class of the object that this {@link MappingStrategy} will create.
318 */
319 public Class<? extends T> getType() {
320 return type;
321 }
322
323 @SuppressWarnings("unchecked")
324 @Override
325 public T populateNewBean(String[] line)
326 throws CsvBeanIntrospectionException, CsvFieldAssignmentException,
327 CsvChainedException {
328 verifyLineLength(line.length);
329 Map<Class<?>, Object> beanTree = createBean();
330
331 CsvChainedException chainedException = null;
332 for (int col = 0; col < line.length; col++) {
333 try {
334 setFieldValue(beanTree, line[col], col);
335 } catch (CsvFieldAssignmentException e) {
336 if(chainedException != null) {
337 chainedException.add(e);
338 }
339 else {
340 chainedException = new CsvChainedException(e);
341 }
342 }
343 }
344 if(chainedException != null) {
345 if (chainedException.hasOnlyOneException()) {
346 throw chainedException.getFirstException();
347 }
348 throw chainedException;
349 }
350 return (T)beanTree.get(type);
351 }
352
353 /**
354 * Sets the class type that is being mapped.
355 * Also initializes the mapping between column names and bean fields
356 * and attempts to create one example bean to be certain there are no
357 * fundamental problems with creation.
358 */
359 // The rest of the Javadoc is inherited.
360 @Override
361 public void setType(Class<? extends T> type) throws CsvBadConverterException {
362 this.type = type;
363 loadFieldMap();
364 }
365
366 /**
367 * Sets the profile this mapping strategy will use when configuring bean
368 * fields.
369 */
370 // The rest of the Javadoc is inherited.
371 @Override
372 public void setProfile(String profile) {
373 this.profile = StringUtils.defaultString(profile);
374 }
375
376 @Override
377 public void ignoreFields(MultiValuedMap<Class<?>, Field> fields) throws IllegalArgumentException {
378
379 // Check input for consistency
380 if(fields == null) {
381 ignoredFields = new ArrayListValuedHashMap<>();
382 }
383 else {
384 ignoredFields = fields;
385 MapIterator<Class<?>, Field> it = ignoredFields.mapIterator();
386 it.forEachRemaining(t -> {
387 final Field f = it.getValue();
388 if (t == null || f == null
389 || !f.getDeclaringClass().isAssignableFrom(t)) {
390 throw new IllegalArgumentException(ResourceBundle.getBundle(
391 ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
392 .getString("ignore.field.inconsistent"));
393 }
394 });
395 }
396
397 // Reload field map
398 if(this.type != null) {
399 loadFieldMap();
400 }
401 }
402
403 /**
404 * Filters all fields that opencsv has been instructed to ignore and
405 * returns a list of the rest.
406 * @param type The class from which {@code fields} come. This must be the
407 * class as opencsv would seek to instantiate it, which in the
408 * case of inheritance is not necessarily the declaring class.
409 * @param fields The fields to be filtered
410 * @return A list of fields that exist for opencsv
411 */
412 protected List<Field> filterIgnoredFields(final Class<?> type, Field[] fields) {
413 final List<Field> filteredFields = new LinkedList<>();
414 for(Field f : fields) {
415 CsvIgnore ignoreAnnotation = f.getAnnotation(CsvIgnore.class);
416 Set<String> ignoredProfiles = ignoreAnnotation == null ?
417 SetUtils.<String>emptySet() :
418 new HashSet<String>(Arrays.asList(ignoreAnnotation.profiles())); // This is easier in Java 9 with Set.of()
419 if(!ignoredFields.containsMapping(type, f) &&
420 !ignoredProfiles.contains(profile) &&
421 !ignoredProfiles.contains(StringUtils.EMPTY)) {
422 filteredFields.add(f);
423 }
424 }
425 return filteredFields;
426 }
427
428 /**
429 * Builds a map of columns from the input to fields of the bean type.
430 *
431 * @throws CsvBadConverterException If there is a problem instantiating the
432 * custom converter for an annotated field
433 */
434 protected void loadFieldMap() throws CsvBadConverterException {
435
436 // Setup
437 initializeFieldMap();
438
439 // Deal with embedded classes through recursion
440 recursiveTypeTree = loadRecursiveClasses(this.type, new HashSet<>());
441
442 // Populate the field map according to annotations or not
443 Map<Boolean, ListValuedMap<Class<?>, Field>> partitionedFields = partitionFields();
444 if(!partitionedFields.get(Boolean.TRUE).isEmpty()) {
445 loadAnnotatedFieldMap(partitionedFields.get(Boolean.TRUE));
446 }
447 else {
448 loadUnadornedFieldMap(partitionedFields.get(Boolean.FALSE));
449 }
450 }
451
452 /**
453 * @param type Class to be checked
454 * @return Whether the type may be recursed into ({@code false}), or
455 * must be considered a leaf node for recursion ({@code true}). This
456 * implementation considers the boxed primitives forbidden.
457 */
458 protected boolean isForbiddenClassForRecursion(Class<?> type) {
459 return FORBIDDEN_CLASSES_FOR_RECURSION.contains(type);
460 }
461
462 /**
463 * Creates a tree of beans embedded in each other.
464 * These are the member variables annotated with {@link CsvRecurse} and
465 * their associated types. This method is used recursively.
466 *
467 * @param newType The type that is meant to be added to the tree
468 * @param encounteredTypes A set of types already encountered during
469 * recursion, as types may not be recursed into
470 * more than once.
471 * @return A representation of this type and all of the types beneath it in
472 * a tree
473 * @throws CsvRecursionException If recursion is attempted into a primitive
474 * type or a previously encountered type is added again or a member
475 * variable annotated with {@link CsvRecurse} is also annotated with a
476 * binding annotation
477 */
478 protected RecursiveType loadRecursiveClasses(Class<?> newType, Set<Class<?>> encounteredTypes) {
479
480 // We cannot recurse into primitive types
481 if (isForbiddenClassForRecursion(newType)) {
482 throw new CsvRecursionException(
483 ResourceBundle.getBundle(
484 ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
485 .getString("recursion.on.primitive"), newType);
486 }
487
488 // Guard against the same type being used twice
489 if(encounteredTypes.contains(newType)) {
490 throw new CsvRecursionException(String.format(ResourceBundle
491 .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
492 .getString("recursive.type.encountered.twice"), newType.toString()), newType);
493 }
494 encounteredTypes.add(newType);
495
496 // Find types to recurse through
497 RecursiveType localRecursiveTypeTree = new RecursiveType(newType);
498 for(Field f : filterIgnoredFields(newType, FieldUtils.getFieldsWithAnnotation(newType, CsvRecurse.class))) {
499
500 // Types that are recursed into cannot also be bound
501 Set<Class<? extends Annotation>> bindingAnnotations = getBindingAnnotations();
502 if(bindingAnnotations.stream().anyMatch(f::isAnnotationPresent)) {
503 throw new CsvRecursionException(
504 ResourceBundle.getBundle(
505 ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
506 .getString("recursion.binding.mutually.exclusive"),
507 f.getType());
508 }
509
510 // Recurse into that type
511 localRecursiveTypeTree.addRecursiveMember(
512 new FieldAccess<>(f),
513 loadRecursiveClasses(f.getType(), encounteredTypes));
514 }
515
516 return localRecursiveTypeTree;
517 }
518
519 /**
520 * Creates a non-tree (fairly flat) representation of all of the fields
521 * bound from all types.
522 * This method is used recursively.
523 * @param root The root of the type tree at this level of recursion
524 * @param encounteredFields A collection of all fields thus far included
525 * in the new representation. This collection will
526 * be added to and is the result of this method.
527 */
528 private void assembleCompleteFieldList(RecursiveType root, final ListValuedMap<Class<?>, Field> encounteredFields) {
529 encounteredFields.putAll(root.type, filterIgnoredFields(root.type, FieldUtils.getAllFields(root.type)));
530 root.getRecursiveMembers().values().forEach(f -> assembleCompleteFieldList(f, encounteredFields));
531 }
532
533 /**
534 * Partitions all non-synthetic fields of the bean type being processed
535 * into annotated and non-annotated fields according to
536 * {@link #getBindingAnnotations()}.
537 *
538 * @return A multi-valued map (class to multiple fields in that class) in
539 * which all annotated fields are mapped under {@link Boolean#TRUE}, and
540 * all non-annotated fields are mapped under {@link Boolean#FALSE}.
541 * @since 5.0
542 */
543 protected Map<Boolean, ListValuedMap<Class<?>, Field>> partitionFields() {
544 // Get a flat list of all fields
545 ListValuedMap<Class<?>, Field> allFields = new ArrayListValuedHashMap<>();
546 assembleCompleteFieldList(recursiveTypeTree, allFields);
547
548 // Determine which annotations need be considered
549 final Set<Class<? extends Annotation>> bindingAnnotations = getBindingAnnotations();
550
551 // Split the fields (with associated types) into annotated and
552 // non-annotated
553 Map<Boolean, ListValuedMap<Class<?>, Field>> returnValue = new TreeMap<>();
554 returnValue.put(Boolean.TRUE, new ArrayListValuedHashMap<>());
555 returnValue.put(Boolean.FALSE, new ArrayListValuedHashMap<>());
556 allFields.entries().stream()
557 .filter(entry -> !entry.getValue().isSynthetic())
558 .forEach(entry -> {
559 if(bindingAnnotations.stream()
560 .anyMatch(a -> entry.getValue().isAnnotationPresent(a))) {
561 returnValue.get(Boolean.TRUE).put(entry.getKey(), entry.getValue());
562 }
563 else {
564 returnValue.get(Boolean.FALSE).put(entry.getKey(), entry.getValue());
565 }
566 });
567 return returnValue;
568 }
569
570 /**
571 * Attempts to instantiate the class of the custom converter specified.
572 *
573 * @param converter The class for a custom converter
574 * @return The custom converter
575 * @throws CsvBadConverterException If the class cannot be instantiated
576 */
577 protected BeanField<T, K> instantiateCustomConverter(Class<? extends AbstractBeanField<T, K>> converter)
578 throws CsvBadConverterException {
579 try {
580 BeanField<T, K> c = converter.getDeclaredConstructor().newInstance();
581 c.setErrorLocale(errorLocale);
582 return c;
583 } catch (IllegalAccessException | InstantiationException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException oldEx) {
584 CsvBadConverterException newEx =
585 new CsvBadConverterException(converter,
586 String.format(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("custom.converter.invalid"), converter.getCanonicalName()));
587 newEx.initCause(oldEx);
588 throw newEx;
589 }
590 }
591
592 @Override
593 public void setErrorLocale(Locale errorLocale) {
594 this.errorLocale = ObjectUtils.defaultIfNull(errorLocale, Locale.getDefault());
595
596 // It's very possible that setType() was called first, which creates all
597 // of the BeanFields, so we need to go back through the list and correct
598 // them all.
599 if(getFieldMap() != null) {
600 getFieldMap().setErrorLocale(this.errorLocale);
601 getFieldMap().values().forEach(f -> f.setErrorLocale(this.errorLocale));
602 }
603 }
604
605 /**
606 * Populates the field corresponding to the column position indicated of the
607 * bean passed in according to the rules of the mapping strategy.
608 * This method performs conversion on the input string and assigns the
609 * result to the proper field in the provided bean.
610 *
611 * @param beanTree Object containing the field to be set.
612 * @param value String containing the value to set the field to.
613 * @param column The column position from the CSV file under which this
614 * value was found.
615 * @throws CsvDataTypeMismatchException When the result of data conversion returns
616 * an object that cannot be assigned to the selected field
617 * @throws CsvRequiredFieldEmptyException When a field is mandatory, but there is no
618 * input datum in the CSV file
619 * @throws CsvConstraintViolationException When the internal structure of
620 * data would be violated by the data in the CSV file
621 * @throws CsvValidationException If a user-supplied validator determines
622 * that the input is invalid
623 * @since 4.2
624 */
625 protected void setFieldValue(Map<Class<?>, Object> beanTree, String value, int column)
626 throws CsvDataTypeMismatchException, CsvRequiredFieldEmptyException,
627 CsvConstraintViolationException, CsvValidationException {
628 BeanField<T, K> beanField = findField(column);
629 if (beanField != null) {
630 Object subordinateBean = beanTree.get(beanField.getType());
631 beanField.setFieldValue(subordinateBean, value, findHeader(column));
632 }
633 }
634
635 @Override
636 public String[] transmuteBean(T bean) throws CsvFieldAssignmentException, CsvChainedException {
637 int numColumns = headerIndex.findMaxIndex()+1;
638 BeanField<T, K> firstBeanField, subsequentBeanField;
639 K firstIndex, subsequentIndex;
640 List<String> contents = new ArrayList<>(Math.max(numColumns, 0));
641
642 // Create a map of types to instances of subordinate beans
643 Map<Class<?>, Object> instanceMap;
644 try {
645 instanceMap = indexBean(bean);
646 }
647 catch(IllegalAccessException | InvocationTargetException e) {
648 // Our testing indicates these exceptions probably can't be thrown,
649 // but they're declared, so we have to deal with them. It's an
650 // alibi catch block.
651 CsvBeanIntrospectionException csve = new CsvBeanIntrospectionException(
652 ResourceBundle.getBundle(
653 ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
654 .getString("error.introspecting.beans"));
655 csve.initCause(e);
656 throw csve;
657 }
658
659 CsvChainedException chainedException = null;
660 for(int i = 0; i < numColumns;) {
661
662 // Determine the first value
663 firstBeanField = findField(i);
664 firstIndex = chooseMultivaluedFieldIndexFromHeaderIndex(i);
665 String[] fields = ArrayUtils.EMPTY_STRING_ARRAY;
666 if(firstBeanField != null) {
667 try {
668 fields = firstBeanField.write(instanceMap.get(firstBeanField.getType()), firstIndex);
669 }
670 catch(CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
671 if(chainedException != null) {
672 chainedException.add(e);
673 }
674 else {
675 chainedException = new CsvChainedException(e);
676 }
677 }
678 }
679
680 if(fields.length == 0) {
681
682 // Write the only value
683 contents.add(StringUtils.EMPTY);
684 i++; // Advance the index
685 }
686 else {
687
688 // Multiple values. Write the first.
689 contents.add(StringUtils.defaultString(fields[0]));
690
691 // Now write the rest.
692 // We must make certain that we don't write more fields
693 // than we have columns of the correct type to cover them.
694 int j = 1;
695 int displacedIndex = i+j;
696 subsequentBeanField = findField(displacedIndex);
697 subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(displacedIndex);
698 while(j < fields.length
699 && displacedIndex < numColumns
700 && Objects.equals(firstBeanField, subsequentBeanField)
701 && Objects.equals(firstIndex, subsequentIndex)) {
702 // This field still has a header, so add it
703 contents.add(StringUtils.defaultString(fields[j]));
704
705 // Prepare for the next loop through
706 displacedIndex = i + (++j);
707 subsequentBeanField = findField(displacedIndex);
708 subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(displacedIndex);
709 }
710
711 i = displacedIndex; // Advance the index
712
713 // And here's where we fill in any fields that are missing to
714 // cover the number of columns of the same type
715 if(i < numColumns) {
716 subsequentBeanField = findField(i);
717 subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(i);
718 while(Objects.equals(firstBeanField, subsequentBeanField)
719 && Objects.equals(firstIndex, subsequentIndex)
720 && i < numColumns) {
721 contents.add(StringUtils.EMPTY);
722 subsequentBeanField = findField(++i);
723 subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(i);
724 }
725 }
726 }
727 }
728
729 // If there were exceptions, throw them
730 if(chainedException != null) {
731 if (chainedException.hasOnlyOneException()) {
732 throw chainedException.getFirstException();
733 }
734 throw chainedException;
735 }
736
737 return contents.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
738 }
739
740 /**
741 * Given the information provided, determines the appropriate built-in
742 * converter to be passed in to the {@link BeanField} being created.
743 *
744 * @param field The field of the bean type in question
745 * @param elementType The type to be generated by the converter (on reading)
746 * @param locale The locale for conversion on reading. May be null or an
747 * empty string if a locale is not in use.
748 * @param writeLocale The locale for conversion on writing. May be null or
749 * an empty string if a locale is not in use.
750 * @param customConverter An optional custom converter
751 * @return The appropriate converter for the necessary conversion
752 * @throws CsvBadConverterException If the converter cannot be instantiated
753 *
754 * @since 4.2
755 */
756 protected CsvConverter determineConverter(Field field,
757 Class<?> elementType, String locale, String writeLocale,
758 Class<? extends AbstractCsvConverter> customConverter)
759 throws CsvBadConverterException {
760 CsvConverter converter;
761
762 // A custom converter always takes precedence if specified.
763 if (customConverter != null && !customConverter.equals(AbstractCsvConverter.class)) {
764 try {
765 converter = customConverter.getDeclaredConstructor().newInstance();
766 } catch (IllegalAccessException | InstantiationException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException oldEx) {
767 CsvBadConverterException newEx =
768 new CsvBadConverterException(customConverter,
769 String.format(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("custom.converter.invalid"), customConverter.getCanonicalName()));
770 newEx.initCause(oldEx);
771 throw newEx;
772 }
773 converter.setType(elementType);
774 converter.setLocale(locale);
775 converter.setWriteLocale(writeLocale);
776 converter.setErrorLocale(errorLocale);
777 }
778
779 // Perhaps a date instead
780 else if (field.isAnnotationPresent(CsvDate.class) || field.isAnnotationPresent(CsvDates.class)) {
781 CsvDate annotation = selectAnnotationForProfile(
782 field.getAnnotationsByType(CsvDate.class),
783 CsvDate::profiles);
784 if(annotation == null) {
785 throw new CsvBadConverterException(CsvDate.class, String.format(
786 ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME).getString("profile.not.found.date"),
787 profile));
788 }
789 String readFormat = annotation.value();
790 String writeFormat = annotation.writeFormatEqualsReadFormat()
791 ? readFormat : annotation.writeFormat();
792 String readChrono = annotation.chronology();
793 String writeChrono = annotation.writeChronologyEqualsReadChronology()
794 ? readChrono : annotation.writeChronology();
795 converter = new ConverterDate(elementType, locale, writeLocale,
796 errorLocale, readFormat, writeFormat, readChrono, writeChrono);
797 }
798
799 // Or a number
800 else if(field.isAnnotationPresent(CsvNumber.class) || field.isAnnotationPresent(CsvNumbers.class)) {
801 CsvNumber annotation = selectAnnotationForProfile(
802 field.getAnnotationsByType(CsvNumber.class),
803 CsvNumber::profiles);
804 if(annotation == null) {
805 throw new CsvBadConverterException(CsvNumber.class, String.format(
806 ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME).getString("profile.not.found.number"),
807 profile));
808 }
809 String readFormat = annotation.value();
810 String writeFormat = annotation.writeFormatEqualsReadFormat()
811 ? readFormat : annotation.writeFormat();
812 converter = new ConverterNumber(elementType, locale, writeLocale,
813 errorLocale, readFormat, writeFormat, annotation.roundingMode());
814 }
815
816 // or a Currency
817 else if (elementType.equals(java.util.Currency.class)){
818 converter = new ConverterCurrency(errorLocale);
819 }
820
821 // Or an enumeration
822 else if (elementType.isEnum()) {
823 converter = new ConverterEnum(elementType, locale, writeLocale, errorLocale);
824 }
825
826 // or an UUID
827 else if (elementType.equals(UUID.class)) {
828 converter = new ConverterUUID(errorLocale);
829 }
830 // Otherwise a primitive
831 else {
832 converter = new ConverterPrimitiveTypes(elementType, locale, writeLocale, errorLocale);
833 }
834
835 return converter;
836 }
837
838 /**
839 * Determines which one of a list of annotations applies to the currently
840 * selected profile.
841 * If no annotation specific to the profile is found, the annotation for
842 * the default profile is returned. If neither is found, {@code null} is
843 * returned.
844 *
845 * @param annotations All annotations of a given type
846 * @param getProfiles A function mapping an annotation of type {@code A} to
847 * the list of profiles it applies to
848 * @param <A> The annotation type being tested
849 * @return The annotation with the appropriate profile or {@code null} if
850 * nothing appropriate is found
851 * @since 5.4
852 */
853 protected <A extends Annotation> A selectAnnotationForProfile(A[] annotations, Function<A, String[]> getProfiles) {
854 A defaultAnnotation = null;
855 String[] profilesForAnnotation;
856 for(A annotation : annotations) {
857 profilesForAnnotation = getProfiles.apply(annotation);
858 for(String p : profilesForAnnotation) {
859 if(profile.equals(p)) {
860 return annotation; // I know. Bad style. I think we can live with it once.
861 }
862 if(StringUtils.EMPTY.equals(p)) {
863 defaultAnnotation = annotation;
864 }
865 }
866 }
867 return defaultAnnotation;
868 }
869
870 /**
871 * Encapsulates a bean type and all of the member variables that need to be
872 * recursed into.
873 */
874 protected static class RecursiveType {
875 private final Class<?> type;
876 private final Map<FieldAccess<Object>, RecursiveType> recursiveMembers = new HashMap<>();
877
878 /**
879 * Constructs a {@link RecursiveType} with the specified type.
880 *
881 * @param type Type associated with this branch
882 */
883 protected RecursiveType(Class<?> type) {
884 this.type = type;
885 }
886
887 /**
888 * @return Type associated with this branch
889 */
890 public Class<?> getType() {
891 return type;
892 }
893
894 /**
895 * Used to add a recursive type.
896 *
897 * @param member Field access member to add a recursive type to
898 * @param memberType {@link RecursiveType} to add
899 */
900 public void addRecursiveMember(FieldAccess<Object> member, RecursiveType memberType) {
901 recursiveMembers.put(member, memberType);
902 }
903
904 /**
905 * @return {@link Map} of field access to {@link RecursiveType}.
906 */
907 public Map<FieldAccess<Object>, RecursiveType> getRecursiveMembers() {
908 return recursiveMembers;
909 }
910 }
911 }