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