View Javadoc
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 }