View Javadoc
1   package com.opencsv.bean;
2   
3   import com.opencsv.CSVReader;
4   import com.opencsv.ICSVParser;
5   import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
6   import org.apache.commons.collections4.ListValuedMap;
7   import org.apache.commons.lang3.ArrayUtils;
8   
9   import java.io.IOException;
10  import java.lang.annotation.Annotation;
11  import java.lang.reflect.Field;
12  import java.util.*;
13  
14  /**
15   * Allows for the mapping of columns with their positions. Using this strategy
16   * without annotations ({@link com.opencsv.bean.CsvBindByPosition} or
17   * {@link com.opencsv.bean.CsvCustomBindByPosition}) requires all the columns
18   * to be present in the CSV file and for them to be in a particular order. Using
19   * annotations allows one to specify arbitrary zero-based column numbers for
20   * each bean member variable to be filled. Also this strategy requires that the
21   * file does NOT have a header. That said, the main use of this strategy is
22   * files that do not have headers.
23   *
24   * @param <T> Type of object that is being processed.
25   */
26  public class ColumnPositionMappingStrategy<T> extends AbstractMappingStrategy<String, Integer, ComplexFieldMapEntry<String, Integer, T>, T> {
27  
28      /**
29       * Whether the user has programmatically set the map from column positions
30       * to field names.
31       */
32      private boolean columnsExplicitlySet = false;
33  
34      /**
35       * The map from column position to {@link BeanField}.
36       */
37      private FieldMapByPosition<T> fieldMap;
38  
39      /**
40       * Holds a {@link java.util.Comparator} to sort columns on writing.
41       */
42      private Comparator<Integer> writeOrder;
43  
44      /**
45       * Used to store a mapping from presumed input column index to desired
46       * output column index, as determined by applying {@link #writeOrder}.
47       */
48      private Integer[] columnIndexForWriting = null;
49  
50      /**
51       * Default constructor. Considered stable.
52       * @see ColumnPositionMappingStrategyBuilder
53       */
54      public ColumnPositionMappingStrategy() {
55      }
56  
57      /**
58       * There is no header per se for this mapping strategy, but this method
59       * checks the first line to determine how many fields are present and
60       * adjusts its field map accordingly.
61       */
62      // The rest of the Javadoc is inherited
63      @Override
64      public void captureHeader(CSVReader reader) throws IOException {
65          // Validation
66          if (type == null) {
67              throw new IllegalStateException(ResourceBundle
68                      .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
69                      .getString("type.unset"));
70          }
71  
72          String[] firstLine = ArrayUtils.nullToEmpty(reader.peek());
73          fieldMap.setMaxIndex(firstLine.length - 1);
74          if (!columnsExplicitlySet) {
75              headerIndex.clear();
76              for (FieldMapByPositionEntry<T> entry : fieldMap) {
77                  Field f = entry.getField().getField();
78                  if (f.getAnnotation(CsvCustomBindByPosition.class) != null
79                          || f.getAnnotation(CsvBindAndSplitByPosition.class) != null
80                          || f.getAnnotation(CsvBindAndJoinByPosition.class) != null
81                          || f.getAnnotation(CsvBindByPosition.class) != null) {
82                      headerIndex.put(entry.getPosition(), f.getName().toUpperCase().trim());
83                  }
84              }
85          }
86      }
87  
88      /**
89       * @return {@inheritDoc} For this mapping strategy, it's simply
90       * {@code index} wrapped as an {@link java.lang.Integer}.
91       */
92      // The rest of the Javadoc is inherited
93      @Override
94      protected Integer chooseMultivaluedFieldIndexFromHeaderIndex(int index) {
95          return Integer.valueOf(index);
96      }
97  
98      @Override
99      protected BeanField<T, Integer> findField(int col) {
100         // If we have a mapping for changing the order of the columns on
101         // writing, be sure to use it.
102         if (columnIndexForWriting != null) {
103             return col < columnIndexForWriting.length ? fieldMap.get(columnIndexForWriting[col]) : null;
104         }
105         return fieldMap.get(col);
106     }
107 
108     /**
109      * This method returns an empty array.
110      * The column position mapping strategy assumes that there is no header, and
111      * thus it also does not write one, accordingly.
112      *
113      * @return An empty array
114      */
115     // The rest of the Javadoc is inherited
116     @Override
117     public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
118         String[] h = super.generateHeader(bean);
119         columnIndexForWriting = new Integer[h.length];
120         Arrays.setAll(columnIndexForWriting, i -> i);
121 
122         // Create the mapping for input column index to output column index.
123         Arrays.sort(columnIndexForWriting, writeOrder);
124         return ArrayUtils.EMPTY_STRING_ARRAY;
125     }
126 
127     /**
128      * Gets a column name.
129      *
130      * @param col Position of the column.
131      * @return Column name or null if col &gt; number of mappings.
132      */
133     @Override
134     public String getColumnName(int col) {
135         return headerIndex.getByPosition(col);
136     }
137 
138     /**
139      * Retrieves the column mappings.
140      *
141      * @return String array with the column mappings.
142      */
143     public String[] getColumnMapping() {
144         return headerIndex.getHeaderIndex();
145     }
146 
147     /**
148      * Setter for the column mapping.
149      * This mapping is for reading. Use of this method in conjunction with
150      * writing is undefined.
151      *
152      * @param columnMapping Column names to be mapped.
153      */
154     public void setColumnMapping(String... columnMapping) {
155         if (columnMapping != null) {
156             headerIndex.initializeHeaderIndex(columnMapping);
157         } else {
158             headerIndex.clear();
159         }
160         columnsExplicitlySet = true;
161         if(getType() != null) {
162             loadFieldMap(); // In case setType() was called first.
163         }
164     }
165 
166     /**
167      * Register a binding between a bean field and a custom converter.
168      *
169      * @param annotation The annotation attached to the bean field
170      * @param localType The class/type in which the field resides
171      * @param localField The bean field
172      */
173     private void registerCustomBinding(CsvCustomBindByPosition annotation, Class<?> localType, Field localField) {
174         @SuppressWarnings("unchecked")
175         Class<? extends AbstractBeanField<T, Integer>> converter = (Class<? extends AbstractBeanField<T, Integer>>)annotation.converter();
176         BeanField<T, Integer> bean = instantiateCustomConverter(converter);
177         bean.setType(localType);
178         bean.setField(localField);
179         bean.setRequired(annotation.required());
180         fieldMap.put(annotation.position(), bean);
181     }
182 
183     /**
184      * Register a binding between a bean field and a collection converter that
185      * splits input into multiple values.
186      *
187      * @param annotation The annotation attached to the bean field
188      * @param localType The class/type in which the field resides
189      * @param localField The bean field
190      */
191     private void registerSplitBinding(CsvBindAndSplitByPosition annotation, Class<?> localType, Field localField) {
192         String fieldLocale = annotation.locale();
193         String fieldWriteLocale = annotation.writeLocaleEqualsReadLocale()
194                 ? fieldLocale
195                 : annotation.writeLocale();
196         Class<?> elementType = annotation.elementType();
197         CsvConverter converter = determineConverter(localField, elementType,
198                 fieldLocale, fieldWriteLocale, annotation.converter());
199         fieldMap.put(annotation.position(), new BeanFieldSplit<>(
200                 localType, localField, annotation.required(), errorLocale, converter,
201                 annotation.splitOn(), annotation.writeDelimiter(),
202                 annotation.collectionType(), elementType, annotation.capture(),
203                 annotation.format()));
204     }
205 
206     /**
207      * Register a binding between a bean field and a multi-valued converter
208      * that joins values from multiple columns.
209      *
210      * @param annotation The annotation attached to the bean field
211      * @param localType The class/type in which the field resides
212      * @param localField The bean field
213      */
214     private void registerJoinBinding(CsvBindAndJoinByPosition annotation, Class<?> localType, Field localField) {
215         String fieldLocale = annotation.locale();
216         String fieldWriteLocale = annotation.writeLocaleEqualsReadLocale()
217                 ? fieldLocale
218                 : annotation.writeLocale();
219         CsvConverter converter = determineConverter(localField, annotation.elementType(),
220                 fieldLocale, fieldWriteLocale, annotation.converter());
221         fieldMap.putComplex(annotation.position(), new BeanFieldJoinIntegerIndex<>(
222                 localType, localField, annotation.required(), errorLocale, converter,
223                 annotation.mapType(), annotation.capture(), annotation.format()));
224     }
225 
226     /**
227      * Register a binding between a bean field and a simple converter.
228      *
229      * @param annotation The annotation attached to the bean field
230      * @param localType The class/type in which the field resides
231      * @param localField The bean field
232      */
233     private void registerBinding(CsvBindByPosition annotation, Class<?> localType, Field localField) {
234         String fieldLocale = annotation.locale();
235         String fieldWriteLocale = annotation.writeLocaleEqualsReadLocale()
236                 ? fieldLocale
237                 : annotation.writeLocale();
238         CsvConverter converter = determineConverter(localField, localField.getType(), fieldLocale, fieldWriteLocale, null);
239         fieldMap.put(annotation.position(), new BeanFieldSingleValue<>(
240                 localType, localField, annotation.required(), errorLocale,
241                 converter, annotation.capture(), annotation.format()));
242     }
243 
244     /**
245      * Creates a map of annotated fields in the bean to be processed.
246      * <p>This method is called by {@link #loadFieldMap()} when at least one
247      * relevant annotation is found on a member variable.</p>
248      */
249     @Override
250     protected void loadAnnotatedFieldMap(ListValuedMap<Class<?>, Field> fields) {
251         for (Map.Entry<Class<?>, Field> classAndField : fields.entries()) {
252             Class<?> localType = classAndField.getKey();
253             Field localField = classAndField.getValue();
254 
255             // Custom converters always have precedence.
256             if (localField.isAnnotationPresent(CsvCustomBindByPosition.class)
257                     || localField.isAnnotationPresent(CsvCustomBindByPositions.class)) {
258                 CsvCustomBindByPosition annotation = selectAnnotationForProfile(
259                         localField.getAnnotationsByType(CsvCustomBindByPosition.class),
260                         CsvCustomBindByPosition::profiles);
261                 if (annotation != null) {
262                     registerCustomBinding(annotation, localType, localField);
263                 }
264             }
265 
266             // Then check for a collection
267             else if (localField.isAnnotationPresent(CsvBindAndSplitByPosition.class)
268                     || localField.isAnnotationPresent(CsvBindAndSplitByPositions.class)) {
269                 CsvBindAndSplitByPosition annotation = selectAnnotationForProfile(
270                         localField.getAnnotationsByType(CsvBindAndSplitByPosition.class),
271                         CsvBindAndSplitByPosition::profiles);
272                 if (annotation != null) {
273                     registerSplitBinding(annotation, localType, localField);
274                 }
275             }
276 
277             // Then check for a multi-column annotation
278             else if (localField.isAnnotationPresent(CsvBindAndJoinByPosition.class)
279                     || localField.isAnnotationPresent(CsvBindAndJoinByPositions.class)) {
280                 CsvBindAndJoinByPosition annotation = selectAnnotationForProfile(
281                         localField.getAnnotationsByType(CsvBindAndJoinByPosition.class),
282                         CsvBindAndJoinByPosition::profiles);
283                 if (annotation != null) {
284                     registerJoinBinding(annotation, localType, localField);
285                 }
286             }
287 
288             // Then it must be a bind by position.
289             else {
290                 CsvBindByPosition annotation = selectAnnotationForProfile(
291                         localField.getAnnotationsByType(CsvBindByPosition.class),
292                         CsvBindByPosition::profiles);
293                 if (annotation != null) {
294                     registerBinding(annotation, localType, localField);
295                 }
296             }
297         }
298     }
299 
300     @Override
301     protected void loadUnadornedFieldMap(ListValuedMap<Class<?>, Field> fields) {
302         for(Map.Entry<Class<?>, Field> classAndField : fields.entries()) {
303             Class<?> localType = classAndField.getKey();
304             Field localField = classAndField.getValue();
305             CsvConverter converter = determineConverter(localField, localField.getType(), null, null, null);
306             int[] indices = headerIndex.getByName(localField.getName());
307             if(indices.length != 0) {
308                 fieldMap.put(indices[0], new BeanFieldSingleValue<>(
309                         localType, localField, false, errorLocale, converter, null, null));
310             }
311         }
312     }
313 
314     /**
315      * Returns a set of the annotations that are used for binding in this
316      * mapping strategy.
317      * <p>In this mapping strategy, those are currently:<ul>
318      *     <li>{@link CsvBindByPosition}</li>
319      *     <li>{@link CsvCustomBindByPosition}</li>
320      *     <li>{@link CsvBindAndJoinByPosition}</li>
321      *     <li>{@link CsvBindAndSplitByPosition}</li>
322      * </ul></p>
323      */
324     @Override
325     protected Set<Class<? extends Annotation>> getBindingAnnotations() {
326         // With Java 9 this can be done more easily with Set.of()
327         return new HashSet<>(Arrays.asList(
328                 CsvBindByPositions.class,
329                 CsvCustomBindByPositions.class,
330                 CsvBindAndJoinByPositions.class,
331                 CsvBindAndSplitByPositions.class,
332                 CsvBindByPosition.class,
333                 CsvCustomBindByPosition.class,
334                 CsvBindAndJoinByPosition.class,
335                 CsvBindAndSplitByPosition.class));
336     }
337 
338     @Override
339     protected void initializeFieldMap() {
340         fieldMap = new FieldMapByPosition<>(errorLocale);
341         fieldMap.setColumnOrderOnWrite(writeOrder);
342     }
343 
344     @Override
345     protected void verifyLineLength(int numberOfFields) throws CsvRequiredFieldEmptyException {
346         if (!headerIndex.isEmpty()) {
347             BeanField<T, Integer> f;
348             StringBuilder sb = null;
349             for (int i = numberOfFields; i <= headerIndex.findMaxIndex(); i++) {
350                 f = findField(i);
351                 if (f != null && f.isRequired()) {
352                     if (sb == null) {
353                         sb = new StringBuilder(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale).getString("multiple.required.field.empty"));
354                     }
355                     sb.append(' ');
356                     sb.append(f.getField().getName());
357                 }
358             }
359             if (sb != null) {
360                 throw new CsvRequiredFieldEmptyException(type, sb.toString());
361             }
362         }
363     }
364 
365     /**
366      * Returns the column position for the given column number.
367      * Yes, they're the same thing. For this mapping strategy, it's a simple
368      * conversion from an integer to a string.
369      */
370     // The rest of the Javadoc is inherited
371     @Override
372     public String findHeader(int col) {
373         return Integer.toString(col);
374     }
375 
376     @Override
377     protected FieldMap<String, Integer, ? extends ComplexFieldMapEntry<String, Integer, T>, T> getFieldMap() {
378         return fieldMap;
379     }
380 
381     /**
382      * Sets the {@link java.util.Comparator} to be used to sort columns when
383      * writing beans to a CSV file.
384      * Behavior of this method when used on a mapping strategy intended for
385      * reading data from a CSV source is not defined.
386      *
387      * @param writeOrder The {@link java.util.Comparator} to use. May be
388      *                   {@code null}, in which case the natural ordering is used.
389      * @since 4.3
390      */
391     public void setColumnOrderOnWrite(Comparator<Integer> writeOrder) {
392         this.writeOrder = writeOrder;
393         if (fieldMap != null) {
394             fieldMap.setColumnOrderOnWrite(this.writeOrder);
395         }
396     }
397 }