1 /*
2 * Copyright 2017 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.CsvRequiredFieldEmptyException;
20 import org.apache.commons.collections4.MultiValuedMap;
21 import org.apache.commons.lang3.ArrayUtils;
22
23 import java.lang.reflect.Field;
24 import java.util.*;
25 import java.util.stream.Collectors;
26
27 /**
28 * This class maintains a mapping from header names out of a CSV file to bean
29 * fields.
30 * Simple entries are matched using string equality. Complex entries are matched
31 * using regular expressions.
32 *
33 * @param <T> Type of the bean being converted
34 * @author Andrew Rucker Jones
35 * @since 4.2
36 */
37 public class FieldMapByName<T> extends AbstractFieldMap<String, String, RegexToBeanField<T>, T> {
38
39 /** Holds a {@link java.util.Comparator} to sort columns on writing. */
40 private Comparator<String> writeOrder = null;
41
42 /**
43 * Initializes this {@link FieldMap}.
44 *
45 * @param errorLocale The locale to be used for error messages
46 */
47 public FieldMapByName(final Locale errorLocale) {
48 super(errorLocale);
49 }
50
51 /**
52 * @param key A regular expression matching header names
53 */
54 // The rest of the Javadoc is inherited
55 @Override
56 public void putComplex(final String key, final BeanField<T, String> value) {
57 complexMapList.add(new RegexToBeanField<>(key, value, errorLocale));
58 }
59
60 /**
61 * Returns a list of required headers that were not present in the input.
62 *
63 * @param headersPresent An array of all headers present from the input
64 * @return A list of name + field for all the required headers that were
65 * not found
66 */
67 public List<FieldMapByNameEntry<T>> determineMissingRequiredHeaders(final String[] headersPresent) {
68
69 // Start with collections of all required headers
70 final List<String> requiredStringList = simpleMap.entrySet().stream()
71 .filter(e -> e.getValue().isRequired())
72 .map(Map.Entry::getKey)
73 .collect(Collectors.toCollection(LinkedList::new));
74 final List<ComplexFieldMapEntry<String, String, T>> requiredRegexList = complexMapList.stream()
75 .filter(r -> r.getBeanField().isRequired())
76 .collect(Collectors.toList());
77
78 // Now remove the ones we found
79 for(String h : headersPresent) {
80 if(!requiredStringList.remove(h.toUpperCase().trim())) {
81 final ListIterator<ComplexFieldMapEntry<String, String, T>> requiredRegexListIterator = requiredRegexList.listIterator();
82 boolean found = false;
83 while(!found && requiredRegexListIterator.hasNext()) {
84 final ComplexFieldMapEntry<String, String, T> r = requiredRegexListIterator.next();
85 if(r.contains(h)) {
86 found = true;
87 requiredRegexListIterator.remove();
88 }
89 }
90 }
91 }
92
93 // Repackage what remains
94 List<FieldMapByNameEntry<T>> missingRequiredHeaders = new LinkedList<>();
95 for(String s : requiredStringList) {
96 missingRequiredHeaders.add(new FieldMapByNameEntry<>(s, simpleMap.get(s), false));
97 }
98 for(ComplexFieldMapEntry<String, String, T> r : requiredRegexList) {
99 missingRequiredHeaders.add(new FieldMapByNameEntry<>(r.getInitializer(), r.getBeanField(), true));
100 }
101
102 return missingRequiredHeaders;
103 }
104
105 /**
106 * This method generates a header that can be used for writing beans of the
107 * type provided back to a file.
108 * <p>The ordering of the headers is determined by the
109 * {@link java.util.Comparator} passed in to
110 * {@link #setColumnOrderOnWrite(Comparator)}, should that method be called,
111 * otherwise the natural ordering is used (alphabetically ascending).</p>
112 * <p>This implementation will not write headers discovered in multi-valued
113 * bean fields if the headers would not be matched by the bean field on
114 * reading. There are two reasons for this:</p>
115 * <ol><li>opencsv always tries to create data that are round-trip
116 * equivalent, and that would not be the case if it generated data on
117 * writing that it would discard on reading.</li>
118 * <li>As the code is currently written, the header name is used on writing
119 * each bean field to determine the appropriate {@link BeanField} for
120 * information concerning conversions, locales, necessity (whether or not
121 * the field is required). Without this information, conversion is
122 * impossible, and every value written under the unmatched header is blank,
123 * regardless of the contents of the bean.</li></ol>
124 */
125 // The rest of the Javadoc is inherited.
126 @Override
127 public String[] generateHeader(final T bean) throws CsvRequiredFieldEmptyException {
128 final List<Field> missingRequiredHeaders = new LinkedList<>();
129 final List<String> headerList = new ArrayList<>(simpleMap.keySet());
130 for(ComplexFieldMapEntry<String, String, T> r : complexMapList) {
131 @SuppressWarnings("unchecked")
132 final MultiValuedMap<String,T> m = (MultiValuedMap<String,T>) r.getBeanField().getFieldValue(bean);
133 if(m != null && !m.isEmpty()) {
134 headerList.addAll(m.entries().stream()
135 .map(Map.Entry::getKey)
136 .filter(r::contains)
137 .collect(Collectors.toList()));
138 }
139 else {
140 if(r.getBeanField().isRequired()) {
141 missingRequiredHeaders.add(r.getBeanField().getField());
142 }
143 }
144 }
145
146 // Report headers that should have been present
147 if(!missingRequiredHeaders.isEmpty()) {
148 String errorMessage = String.format(
149 ResourceBundle
150 .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
151 .getString("header.required.field.absent"),
152 missingRequiredHeaders.stream()
153 .map(Field::getName)
154 .collect(Collectors.joining(" ")),
155 String.join(" ", headerList));
156 throw new CsvRequiredFieldEmptyException(bean.getClass(), missingRequiredHeaders, errorMessage);
157 }
158
159 // Sort and return
160 headerList.sort(writeOrder);
161 return headerList.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
162 }
163
164 /**
165 * Sets the {@link java.util.Comparator} to be used to sort columns when
166 * writing beans to a CSV file.
167 *
168 * @param writeOrder The {@link java.util.Comparator} to use. May be
169 * {@code null}, in which case the natural ordering is used.
170 * @since 4.3
171 */
172 public void setColumnOrderOnWrite(Comparator<String> writeOrder) {
173 this.writeOrder = writeOrder;
174 }
175 }