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.bean.util.OpencsvUtils;
20  import com.opencsv.exceptions.CsvBadConverterException;
21  import com.opencsv.exceptions.CsvBeanIntrospectionException;
22  import com.opencsv.exceptions.CsvConstraintViolationException;
23  import com.opencsv.exceptions.CsvDataTypeMismatchException;
24  import org.apache.commons.collections4.Bag;
25  import org.apache.commons.collections4.SortedBag;
26  import org.apache.commons.collections4.bag.HashBag;
27  import org.apache.commons.collections4.bag.TreeBag;
28  import org.apache.commons.lang3.ArrayUtils;
29  import org.apache.commons.lang3.StringUtils;
30  
31  import java.lang.reflect.Field;
32  import java.util.*;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  
36  /**
37   * This class concerns itself with handling collection-valued bean fields.
38   * 
39   * @param <T> The type of the bean being populated
40   * @param <I> Type of the index into a multivalued field
41   * @author Andrew Rucker Jones
42   * @since 4.2
43   */
44  public class BeanFieldSplit<T, I> extends AbstractBeanField<T, I> {
45      
46      private final Pattern splitOn, capture;
47      private final String writeDelimiter, writeFormat;
48      private final Class<? extends Collection> collectionType;
49      private final Class<?> elementType;
50      
51      /**
52       * The only valid constructor.
53       *
54       * @param type The type of the class in which this field is found. This is
55       *             the type as instantiated by opencsv, and not necessarily the
56       *             type in which the field is declared in the case of
57       *             inheritance.
58       * @param field A {@link java.lang.reflect.Field} object.
59       * @param required Whether or not this field is required in input
60       * @param errorLocale The errorLocale to use for error messages.
61       * @param converter The converter to be used to perform the actual data
62       *   conversion
63       * @param splitOn See {@link CsvBindAndSplitByName#splitOn()}
64       * @param writeDelimiter See {@link CsvBindAndSplitByName#writeDelimiter()}
65       * @param collectionType  See {@link CsvBindAndSplitByName#collectionType()}
66       * @param elementType See {@link CsvBindAndSplitByName#elementType()}
67       * @param capture See {@link CsvBindAndSplitByName#capture()}
68       * @param format The format string used for packaging values to be written.
69       *               If {@code null} or empty, it is ignored.
70       */
71      public BeanFieldSplit(
72              Class<?> type, Field field, boolean required, Locale errorLocale,
73              CsvConverter converter, String splitOn, String writeDelimiter,
74              Class<? extends Collection> collectionType, Class<?> elementType,
75              String capture, String format) {
76          
77          // Simple assignments
78          super(type, field, required, errorLocale, converter);
79          this.writeDelimiter = writeDelimiter;
80          this.writeFormat = format;
81          this.elementType = elementType;
82          
83          // Check that we really have a collection
84          if(!Collection.class.isAssignableFrom(field.getType())) {
85              throw new CsvBadConverterException(
86                      BeanFieldSplit.class,
87                      String.format(
88                              ResourceBundle.getBundle(
89                                      ICSVParser.DEFAULT_BUNDLE_NAME,
90                                      this.errorLocale).getString("invalid.collection.type"),
91                              field.getType().toString()));
92          }
93          
94          // Check the regular expressions for validity and compile once for speed
95          this.splitOn = OpencsvUtils.compilePattern(splitOn, 0,
96                  BeanFieldSplit.class, this.errorLocale);
97          this.capture = OpencsvUtils.compilePatternAtLeastOneGroup(capture, 0,
98                  BeanFieldSplit.class, this.errorLocale);
99  
100         // Verify that the format string works as expected
101         OpencsvUtils.verifyFormatString(this.writeFormat, BeanFieldSplit.class, this.errorLocale);
102 
103         // Determine the Collection implementation that should be instantiated
104         // for every bean.
105         Class<?> fieldType = field.getType();
106         if(!fieldType.isInterface()) {
107             this.collectionType = (Class<Collection>)field.getType();
108         }
109         else if(!collectionType.isInterface()) {
110             this.collectionType = collectionType;
111         }
112         else {
113             if(Collection.class.equals(fieldType) || List.class.equals(fieldType)) {
114                 this.collectionType = ArrayList.class;
115             }
116             else if(Set.class.equals(fieldType)) {
117                 if(fieldType.isEnum()) {
118                     this.collectionType = EnumSet.class;
119                 }
120                 else {
121                     this.collectionType = HashSet.class;
122                 }
123             }
124             else if(SortedSet.class.equals(fieldType) || NavigableSet.class.equals(fieldType)) {
125                 this.collectionType = TreeSet.class;
126             }
127             else if(Queue.class.equals(fieldType) || Deque.class.equals(fieldType)) {
128                 this.collectionType = ArrayDeque.class;
129             }
130             else if(Bag.class.equals(fieldType)) {
131                 this.collectionType = HashBag.class;
132             }
133             else if(SortedBag.class.equals(fieldType)) {
134                 this.collectionType = TreeBag.class;
135             }
136             else {
137                 this.collectionType = null;
138                 throw new CsvBadConverterException(
139                         BeanFieldSplit.class,
140                         String.format(
141                                 ResourceBundle.getBundle(
142                                         ICSVParser.DEFAULT_BUNDLE_NAME,
143                                         this.errorLocale).getString("invalid.collection.type"),
144                                 collectionType.toString()));
145             }
146         }
147         
148         // Now that we know what type we want to assign, run one last check
149         // that assignment is truly possible
150         if(!field.getType().isAssignableFrom(this.collectionType)) {
151             throw new CsvBadConverterException(
152                     BeanFieldSplit.class,
153                     String.format(
154                             ResourceBundle.getBundle(
155                                     ICSVParser.DEFAULT_BUNDLE_NAME,
156                                     this.errorLocale).getString("unassignable.collection.type"),
157                             collectionType.getName(), field.getType().getName()));
158         }
159     }
160 
161     /**
162      * This method manages the collection being created as well as splitting the
163      * data.
164      * Once the data are split, they are sent to the converter for the actual
165      * conversion.
166      * 
167      * @see ConverterPrimitiveTypes#convertToRead(java.lang.String) 
168      * @see ConverterDate#convertToRead(java.lang.String)
169      * @see ConverterNumber#convertToRead(String) 
170      */
171     // The rest of the Javadoc is inherited
172     @Override
173     protected Object convert(String value) throws CsvDataTypeMismatchException, CsvConstraintViolationException {
174         Collection<Object> collection;
175         try {
176             if(collectionType.equals(EnumSet.class)) {
177                 collection = (Collection)EnumSet.noneOf((Class<Enum>)elementType);
178             }
179             else {
180                 collection = collectionType.newInstance();
181             }
182         }
183         catch(InstantiationException | IllegalAccessException e) {
184             CsvBeanIntrospectionException csve = new CsvBeanIntrospectionException(
185                     String.format(
186                             ResourceBundle
187                                     .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
188                                     .getString("collection.cannot.be.instantiated"),
189                             collectionType.getCanonicalName()));
190             csve.initCause(e);
191             throw csve;
192         }
193         
194         String[] splitValues = value == null ? ArrayUtils.EMPTY_STRING_ARRAY : splitOn.split(value);
195         for(String s : splitValues) {
196             if(capture != null) {
197                 Matcher m = capture.matcher(s);
198                 if(m.matches()) {
199                     s = m.group(1);
200                 }
201                 // Otherwise s remains intentionally unchanged
202             }
203             collection.add(converter.convertToRead(s));
204         }
205         return collection;
206     }
207 
208     /**
209      * Manages converting a collection of values into a single string.
210      * The conversion of each individual value is performed by the converter.
211      */
212     // The rest of the Javadoc is inherited
213     @Override
214     protected String convertToWrite(Object value)
215             throws CsvDataTypeMismatchException {
216         String retval = StringUtils.EMPTY;
217         if(value != null) {
218             @SuppressWarnings("unchecked") Collection<Object> collection = (Collection<Object>) value;
219             String[] convertedValue = new String[collection.size()];
220             int i = 0;
221             for(Object o : collection) {
222                 convertedValue[i] = converter.convertToWrite(o);
223                 if(StringUtils.isNotEmpty(this.writeFormat)
224                         && StringUtils.isNotEmpty(convertedValue[i])) {
225                     convertedValue[i] = String.format(this.writeFormat, convertedValue[i]);
226                 }
227                 i++;
228             }
229             retval = StringUtils.join(convertedValue, writeDelimiter); // String.join() make null into "null"
230         }
231         return retval;
232     }
233     
234     /**
235      * Checks that {@code value} is not null and not an empty
236      * {@link java.util.Collection}.
237      */
238     // The rest of the Javadoc is inherited
239     @SuppressWarnings("unchecked")
240     @Override
241     protected boolean isFieldEmptyForWrite(Object value) {
242         return super.isFieldEmptyForWrite(value) || ((Collection<Object>)value).isEmpty();
243     }
244 }