View Javadoc
1   package com.opencsv.bean;
2   
3   import com.opencsv.CSVReader;
4   import com.opencsv.ICSVParser;
5   import com.opencsv.exceptions.CsvBadConverterException;
6   import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
7   import org.apache.commons.collections4.ListValuedMap;
8   import org.apache.commons.lang3.ArrayUtils;
9   import org.apache.commons.lang3.StringUtils;
10  
11  import java.io.IOException;
12  import java.io.Serializable;
13  import java.lang.reflect.Field;
14  import java.util.*;
15  
16  /**
17   * This class serves as a location to collect code common to a mapping strategy
18   * that maps header names to member variables.
19   *
20   * @param <T> The type of bean being created or written
21   * @author Andrew Rucker Jones
22   * @since 5.0
23   */
24  abstract public class HeaderNameBaseMappingStrategy<T> extends AbstractMappingStrategy<String, String, ComplexFieldMapEntry<String, String, T>, T> {
25  
26      /**
27       * Given a header name, this map allows one to find the corresponding
28       * {@link BeanField}.
29       */
30      protected FieldMapByName<T> fieldMap = null;
31  
32      /** Holds a {@link java.util.Comparator} to sort columns on writing. */
33      protected Comparator<String> writeOrder = null;
34  
35      /** If set, every record will be shortened or lengthened to match the number of headers. */
36      protected final boolean forceCorrectRecordLength;
37  
38      /** Nullary constructor for compatibility. */
39      public HeaderNameBaseMappingStrategy() {
40          this.forceCorrectRecordLength = false;
41      }
42  
43      /**
44       * Constructor to allow setting options for header name mapping.
45       *
46       * @param forceCorrectRecordLength If set, every record will be shortened
47       *                                 or lengthened to match the number of
48       *                                 headers
49       */
50      public HeaderNameBaseMappingStrategy(boolean forceCorrectRecordLength) {
51          this.forceCorrectRecordLength = forceCorrectRecordLength;
52      }
53  
54      @Override
55      public void captureHeader(CSVReader reader) throws IOException, CsvRequiredFieldEmptyException {
56          // Validation
57          if(type == null) {
58              throw new IllegalStateException(ResourceBundle
59                      .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
60                      .getString("type.unset"));
61          }
62  
63          // Read the header
64          String[] header = ArrayUtils.nullToEmpty(reader.readNextSilently());
65          for(int i = 0; i < header.length; i++) {
66              // For the case that a header is empty and someone configured
67              // empty fields to be null
68              if(header[i] == null) {
69                  header[i] = StringUtils.EMPTY;
70              }
71          }
72          headerIndex.initializeHeaderIndex(header);
73  
74          // Throw an exception if any required headers are missing
75          List<FieldMapByNameEntry<T>> missingRequiredHeaders = fieldMap.determineMissingRequiredHeaders(header);
76          if (!missingRequiredHeaders.isEmpty()) {
77              String[] requiredHeaderNames = new String[missingRequiredHeaders.size()];
78              List<Field> requiredFields = new ArrayList<>(missingRequiredHeaders.size());
79              for(int i = 0; i < missingRequiredHeaders.size(); i++) {
80                  FieldMapByNameEntry<T> fme = missingRequiredHeaders.get(i);
81                  if(fme.isRegexPattern()) {
82                      requiredHeaderNames[i] = String.format(
83                              ResourceBundle
84                                      .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
85                                      .getString("matching"),
86                              fme.getName());
87                  } else {
88                      requiredHeaderNames[i] = fme.getName();
89                  }
90                  requiredFields.add(fme.getField().getField());
91              }
92              String missingRequiredFields = String.join(", ", requiredHeaderNames);
93              String allHeaders = String.join(",", header);
94              CsvRequiredFieldEmptyException e = new CsvRequiredFieldEmptyException(type, requiredFields,
95                      String.format(
96                              ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
97                                      .getString("header.required.field.absent"),
98                              missingRequiredFields, allHeaders));
99              e.setLine(header);
100             throw e;
101         }
102     }
103 
104     @Override
105     protected String chooseMultivaluedFieldIndexFromHeaderIndex(int index) {
106         String[] s = headerIndex.getHeaderIndex();
107         return index >= s.length ? null: s[index];
108     }
109 
110     @Override
111     public void verifyLineLength(int numberOfFields) throws CsvRequiredFieldEmptyException {
112         if(!headerIndex.isEmpty()) {
113             if (numberOfFields != headerIndex.getHeaderIndexLength() && !forceCorrectRecordLength) {
114                 throw new CsvRequiredFieldEmptyException(type, ResourceBundle
115                         .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
116                         .getString("header.data.mismatch"));
117             }
118         }
119     }
120 
121     @Override
122     protected BeanField<T, String> findField(int col) throws CsvBadConverterException {
123         BeanField<T, String> beanField = null;
124         String columnName = getColumnName(col);
125         if (columnName == null) {
126             return null;
127         }
128         columnName = columnName.trim();
129         if (!columnName.isEmpty()) {
130             beanField = fieldMap.get(columnName.toUpperCase());
131         }
132         return beanField;
133     }
134 
135     /**
136      * Creates a map of fields in the bean to be processed that have no binding
137      * annotations.
138      * <p>This method is called by {@link #loadFieldMap()} when absolutely no
139      * binding annotations that are relevant for this mapping strategy are
140      * found in the type of bean being processed. It is then assumed that every
141      * field is to be included, and that the name of the member variable must
142      * exactly match the header name of the input.</p>
143      * <p>Two exceptions are made to the rule that everything is written:<ol>
144      *     <li>Any field annotated with {@link CsvIgnore} will be
145      *     ignored on writing</li>
146      *     <li>Any field named "serialVersionUID" will be ignored if the
147      *     enclosing class implements {@link java.io.Serializable}.</li>
148      * </ol></p>
149      * <p>{@link CsvRecurse} is respected.</p>
150      */
151     @Override
152     protected void loadUnadornedFieldMap(ListValuedMap<Class<?>, Field> fields) {
153         fields.entries().stream()
154                 .filter(entry -> !(Serializable.class.isAssignableFrom(entry.getKey()) && "serialVersionUID".equals(entry.getValue().getName())))
155                 .filter(entry -> !entry.getValue().isAnnotationPresent(CsvRecurse.class))
156                 .forEach(entry -> {
157                     final CsvConverter converter = determineConverter(entry.getValue(), entry.getValue().getType(), null, null, null);
158                     fieldMap.put(entry.getValue().getName().toUpperCase(), new BeanFieldSingleValue<>(
159                             entry.getKey(), entry.getValue(),
160                             false, errorLocale, converter, null, null));
161                 });
162     }
163 
164     @Override
165     protected void initializeFieldMap() {
166         fieldMap = new FieldMapByName<>(errorLocale);
167         fieldMap.setColumnOrderOnWrite(writeOrder);
168     }
169 
170     @Override
171     protected FieldMap<String, String, ? extends ComplexFieldMapEntry<String, String, T>, T> getFieldMap() {return fieldMap;}
172 
173     @Override
174     public String findHeader(int col) {
175         return headerIndex.getByPosition(col);
176     }
177 
178     /**
179      * Sets the {@link java.util.Comparator} to be used to sort columns when
180      * writing beans to a CSV file.
181      * Behavior of this method when used on a mapping strategy intended for
182      * reading data from a CSV source is not defined.
183      *
184      * @param writeOrder The {@link java.util.Comparator} to use. May be
185      *   {@code null}, in which case the natural ordering is used.
186      * @since 4.3
187      */
188     public void setColumnOrderOnWrite(Comparator<String> writeOrder) {
189         this.writeOrder = writeOrder;
190         if(fieldMap != null) {
191             fieldMap.setColumnOrderOnWrite(this.writeOrder);
192         }
193     }
194 }