View Javadoc
1   /*
2    * Copyright 2018 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.CsvRequiredFieldEmptyException;
20  import org.apache.commons.collections4.MultiValuedMap;
21  import org.apache.commons.collections4.iterators.LazyIteratorChain;
22  import org.apache.commons.collections4.iterators.TransformIterator;
23  
24  import java.lang.reflect.Field;
25  import java.util.*;
26  import java.util.stream.Collectors;
27  
28  /**
29   * This class maintains a mapping from column position out of a CSV file to bean
30   * fields.
31   *
32   * @param <T> Type of the bean being converted
33   * @author Andrew Rucker Jones
34   * @since 4.2
35   */
36  public class FieldMapByPosition<T> extends AbstractFieldMap<String, Integer, PositionToBeanField<T>, T> implements Iterable<FieldMapByPositionEntry<T>> {
37      
38      private int maxIndex = Integer.MAX_VALUE;
39  
40      /** Holds a {@link java.util.Comparator} to sort columns on writing. */
41      private Comparator<Integer> writeOrder = null;
42      
43      /**
44       * Initializes this {@link FieldMap}.
45       * 
46       * @param errorLocale The locale to be used for error messages
47       */
48      public FieldMapByPosition(final Locale errorLocale) {
49          super(errorLocale);
50      }
51      
52      /**
53       * This method generates a header that can be used for writing beans of the
54       * type provided back to a file.
55       * The ordering of the headers can be determined with the
56       * {@link java.util.Comparator} passed in to
57       * {@link #setColumnOrderOnWrite(Comparator)}. Otherwise, it is ascending
58       * according to position.
59       */
60      // The rest of the Javadoc is inherited.
61      @Override
62      public String[] generateHeader(final T bean) throws CsvRequiredFieldEmptyException {
63          final List<Field> missingRequiredHeaders = new LinkedList<>();
64          final SortedMap<Integer, String> headerMap = new TreeMap<>(writeOrder);
65          for(Map.Entry<Integer, BeanField<T, Integer>> entry : simpleMap.entrySet()) {
66              headerMap.put(entry.getKey(), entry.getValue().getField().getName());
67          }
68          for(ComplexFieldMapEntry<String, Integer, T> r : complexMapList) {
69              @SuppressWarnings("unchecked")
70              final MultiValuedMap<Integer,T> m = (MultiValuedMap<Integer, T>) r.getBeanField().getFieldValue(bean);
71              boolean oneEntryMatched = false;
72              if(m != null && !m.isEmpty()) {
73                  for(Map.Entry<Integer,T> entry : m.entries()) {
74                      Integer key = entry.getKey();
75                      if(r.contains(key)) {
76                          headerMap.put(entry.getKey(), r.getBeanField().getField().getName());
77                          oneEntryMatched = true;
78                      }
79                  }
80              }
81              if(m == null || m.isEmpty() || !oneEntryMatched) {
82                  if(r.getBeanField().isRequired()) {
83                      missingRequiredHeaders.add(r.getBeanField().getField());
84                  }
85              }
86          }
87          
88          // Convert to an array of header "names".
89          // Since the user can pass in an arbitrary collation, we have to
90          // re-sort to get the highest value.
91          SortedSet<Integer> headerSet = new TreeSet<>(headerMap.keySet());
92          int arraySize = headerSet.isEmpty() ? 0 : headerSet.last()+1;
93          final String[] headers = new String[arraySize];
94          int previousIndex = headerSet.isEmpty() ? 0 : headerSet.first();
95          for(Integer i : headerSet) { // Fill in gaps
96              for(int j = previousIndex+1; j < i ; j++) {
97                  headerMap.put(j, null);
98              }
99              previousIndex = i;
100         }
101         previousIndex = 0;
102         for(String value : headerMap.values()) {
103             headers[previousIndex++] = value;
104         }
105         
106         // Report headers that should have been present
107         if(!missingRequiredHeaders.isEmpty()) {
108             String errorMessage = String.format(
109                     ResourceBundle
110                             .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
111                             .getString("header.required.field.absent"),
112                     missingRequiredHeaders.stream()
113                             .map(Field::getName)
114                             .collect(Collectors.joining(" ")),
115                     String.join(" ", headers));
116             throw new CsvRequiredFieldEmptyException(bean.getClass(), missingRequiredHeaders, errorMessage);
117         }
118         
119         return headers;
120     }
121     
122     /**
123      * @param rangeDefinition A string describing the column positions to be
124      *   matched.
125      * @see CsvBindAndJoinByPosition#position() 
126      */
127     // The rest of the Javadoc is inherited
128     @Override
129     public void putComplex(final String rangeDefinition, final BeanField<T, Integer> field) {
130         complexMapList.add(new PositionToBeanField<>(rangeDefinition, maxIndex, field, errorLocale));
131     }
132     
133     /**
134      * Sets the maximum index for all ranges specified in the entire field map.
135      * No ranges or mappings are ever removed so as to preserve information
136      * about required fields, but upper boundries are shortened as much as
137      * possible. If ranges or individual column positions were specified that
138      * lie wholly above {@code maxIndex}, these are preserved, though ranges
139      * are shortened to a single value (the lower boundry).
140      * 
141      * @param maxIndex The maximum index in the data being imported
142      */
143     public void setMaxIndex(int maxIndex) {
144         this.maxIndex = maxIndex;
145         
146         // Attenuate all ranges that end past the last index down to the last index
147         complexMapList.forEach(p -> p.attenuateRanges(maxIndex));
148     }
149 
150     @Override
151     public Iterator<FieldMapByPositionEntry<T>> iterator() {
152         return new LazyIteratorChain<FieldMapByPositionEntry<T>>() {
153             
154             @Override
155             protected Iterator<FieldMapByPositionEntry<T>> nextIterator(int count) {
156                 if(count <= complexMapList.size()) {
157                     return complexMapList.get(count-1).iterator();
158                 }
159                 if(count == complexMapList.size()+1) {
160                     return new TransformIterator<>(
161                             simpleMap.entrySet().iterator(),
162                             input -> new FieldMapByPositionEntry<T>(input.getKey(), input.getValue()));
163                 }
164                 return null;
165             }
166         };
167     }
168 
169     /**
170      * Sets the {@link java.util.Comparator} to be used to sort columns when
171      * writing beans to a CSV file.
172      *
173      * @param writeOrder The {@link java.util.Comparator} to use. May be
174      *   {@code null}, in which case the natural ordering is used.
175      * @since 4.3
176      */
177     public void setColumnOrderOnWrite(Comparator<Integer> writeOrder) {
178         this.writeOrder = writeOrder;
179     }
180 }