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.CsvBadConverterException;
20 import com.opencsv.exceptions.CsvBeanIntrospectionException;
21 import com.opencsv.exceptions.CsvDataTypeMismatchException;
22 import org.apache.commons.collections4.ListValuedMap;
23 import org.apache.commons.collections4.MultiValuedMap;
24 import org.apache.commons.collections4.SetValuedMap;
25 import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
26 import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
27 import org.apache.commons.lang3.ArrayUtils;
28
29 import java.lang.reflect.Constructor;
30 import java.lang.reflect.Field;
31 import java.lang.reflect.InvocationTargetException;
32 import java.util.Collection;
33 import java.util.Locale;
34 import java.util.ResourceBundle;
35
36 /**
37 * This class is used for combining multiple columns of the input, possibly
38 * with multiple identically named columns, into one field.
39 *
40 * @param <T> The type of the bean being populated
41 * @param <I> The index of the {@link org.apache.commons.collections4.MultiValuedMap} in use
42 * @author Andrew Rucker Jones
43 * @since 4.2
44 */
45 abstract public class BeanFieldJoin<T, I> extends BeanFieldSingleValue<T, I> {
46
47 /**
48 * The type of the {@link org.apache.commons.collections4.MultiValuedMap}
49 * that should be instantiated for the bean field being populated.
50 */
51 private final Class<? extends MultiValuedMap> mapType;
52
53 /**
54 * Creates a new instance.
55 *
56 * @param type The type of the class in which this field is found. This is
57 * the type as instantiated by opencsv, and not necessarily the
58 * type in which the field is declared in the case of
59 * inheritance.
60 * @param field The bean field this object represents
61 * @param required Whether or not a value is always required for this field
62 * @param errorLocale The locale to use for error messages
63 * @param converter The converter to be used for performing the data
64 * conversion on reading or writing
65 * @param mapType The type of the
66 * {@link org.apache.commons.collections4.MultiValuedMap} that should be
67 * instantiated for the bean field being populated
68 * @param capture See {@link CsvBindAndJoinByName#capture()}
69 * @param format The format string used for packaging values to be written.
70 * If {@code null} or empty, it is ignored.
71 */
72 public BeanFieldJoin(
73 Class<?> type, Field field, boolean required, Locale errorLocale,
74 CsvConverter converter, Class<? extends MultiValuedMap> mapType,
75 String capture, String format) {
76
77 // Simple assignments
78 super(type, field, required, errorLocale, converter, capture, format);
79
80 // Check that we really have a collection
81 if(!MultiValuedMap.class.isAssignableFrom(field.getType())) {
82 throw new CsvBadConverterException(
83 BeanFieldJoin.class,
84 String.format(
85 ResourceBundle.getBundle(
86 ICSVParser.DEFAULT_BUNDLE_NAME,
87 errorLocale).getString("invalid.multivaluedmap.type"),
88 field.getType().toString()));
89 }
90
91 // Determine the MultiValuedMap implementation that should be
92 // instantiated for every bean.
93 Class<?> fieldType = field.getType();
94 if(!fieldType.isInterface()) {
95 this.mapType = (Class<MultiValuedMap>)field.getType();
96 }
97 else if(!mapType.isInterface()) {
98 this.mapType = mapType;
99 }
100 else {
101 if(MultiValuedMap.class.equals(fieldType) || ListValuedMap.class.equals(fieldType)) {
102 this.mapType = ArrayListValuedHashMap.class;
103 }
104 else if(SetValuedMap.class.equals(fieldType)) {
105 this.mapType = HashSetValuedHashMap.class;
106 }
107 else {
108 this.mapType = null;
109 throw new CsvBadConverterException(
110 BeanFieldJoin.class,
111 String.format(
112 ResourceBundle.getBundle(
113 ICSVParser.DEFAULT_BUNDLE_NAME,
114 errorLocale).getString("invalid.multivaluedmap.type"),
115 mapType.toString()));
116 }
117 }
118
119 // Now that we know what type we want to assign, run one last check
120 // that assignment is truly possible
121 if(!field.getType().isAssignableFrom(this.mapType)) {
122 throw new CsvBadConverterException(
123 BeanFieldJoin.class,
124 String.format(
125 ResourceBundle.getBundle(
126 ICSVParser.DEFAULT_BUNDLE_NAME,
127 errorLocale).getString("unassignable.multivaluedmap.type"),
128 mapType.getName(), field.getType().getName()));
129 }
130 }
131
132 /**
133 * Puts the value given in {@code newValue} into {@code map} using
134 * {@code index}.
135 * This allows derived classes to do something special before assigning the
136 * value, such as converting the index to a different type.
137 *
138 * @param map The map to which to assign the new value. Never null.
139 * @param index The index under which the new value should be placed in the
140 * map. Never null.
141 * @param newValue The new value to be put in the map
142 * @return The previous value under this index, or null if there was no
143 * previous value
144 */
145 abstract protected Object putNewValue(MultiValuedMap<I, Object> map, String index, Object newValue);
146
147 /**
148 * Assigns the value given to the proper field of the bean given.
149 * In the case of this kind of bean field, the new value will be added to
150 * an existing map, and a new map will be created if one does not already
151 * exist.
152 */
153 // The rest of the Javadoc is inherited
154 @Override
155 protected void assignValueToField(Object bean, Object obj, String header)
156 throws CsvDataTypeMismatchException {
157
158 // Find and use getter and setter methods if available
159 // obj == null means that the source field was empty. Then we simply
160 // make certain that a(n empty) map exists.
161 @SuppressWarnings("unchecked")
162 MultiValuedMap<I,Object> currentValue = (MultiValuedMap<I,Object>) getFieldValue(bean);
163 try {
164 if(currentValue == null) {
165 Constructor<? extends MultiValuedMap> c = mapType.getConstructor();
166 currentValue = c.newInstance();
167 }
168 putNewValue(currentValue, header, obj);
169 super.assignValueToField(bean, currentValue, header);
170 } catch (IllegalAccessException | InvocationTargetException | ClassCastException e) {
171 CsvBeanIntrospectionException csve =
172 new CsvBeanIntrospectionException(bean, field,
173 e.getLocalizedMessage());
174 csve.initCause(e);
175 throw csve;
176 } catch(InstantiationException | NoSuchMethodException e) {
177 CsvBadConverterException csve = new CsvBadConverterException(
178 BeanFieldJoin.class,
179 String.format(
180 ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
181 .getString("map.cannot.be.instantiated"),
182 mapType.getName()));
183 csve.initCause(e);
184 throw csve;
185 }
186 }
187
188 /**
189 * @return An array of all objects in the
190 * {@link org.apache.commons.collections4.MultiValuedMap} addressed by
191 * this bean field answering to the key given in {@code index}
192 */
193 // The rest of the Javadoc is inherited
194 @Override
195 public Object[] indexAndSplitMultivaluedField(Object value, I index)
196 throws CsvDataTypeMismatchException {
197 Object[] splitObjects = ArrayUtils.EMPTY_OBJECT_ARRAY;
198 if(value != null) {
199 if(MultiValuedMap.class.isAssignableFrom(value.getClass())) {
200 @SuppressWarnings("unchecked")
201 MultiValuedMap<Object,Object> map = (MultiValuedMap<Object,Object>) value;
202 Collection<Object> splitCollection = map.get(index);
203 splitObjects = splitCollection.toArray(ArrayUtils.EMPTY_OBJECT_ARRAY);
204 }
205 else {
206 // Note about code coverage: I sincerely doubt this code is
207 // reachable. It is meant as one more safeguard.
208 throw new CsvDataTypeMismatchException(value, String.class,
209 ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
210 .getString("field.not.multivaluedmap"));
211 }
212 }
213 return splitObjects;
214 }
215
216 /**
217 * Checks that {@code value} is not null and not empty.
218 */
219 // The rest of the Javadoc is inherited
220 @Override
221 @SuppressWarnings("unchecked")
222 protected boolean isFieldEmptyForWrite(Object value) {
223 return super.isFieldEmptyForWrite(value) || ((MultiValuedMap<Object, Object>)value).isEmpty();
224 }
225 }