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.exceptions.CsvRequiredFieldEmptyException;
20  import org.apache.commons.collections4.MultiValuedMap;
21  import org.apache.commons.lang3.ArrayUtils;
22  
23  import java.lang.reflect.Field;
24  import java.util.*;
25  import java.util.stream.Collectors;
26  
27  /**
28   * This class maintains a mapping from header names out of a CSV file to bean
29   * fields.
30   * Simple entries are matched using string equality. Complex entries are matched
31   * using regular expressions.
32   *
33   * @param <T> Type of the bean being converted
34   * @author Andrew Rucker Jones
35   * @since 4.2
36   */
37  public class FieldMapByName<T> extends AbstractFieldMap<String, String, RegexToBeanField<T>, T> {
38  
39      /** Holds a {@link java.util.Comparator} to sort columns on writing. */
40      private Comparator<String> writeOrder = null;
41      
42      /**
43       * Initializes this {@link FieldMap}.
44       * 
45       * @param errorLocale The locale to be used for error messages
46       */
47      public FieldMapByName(final Locale errorLocale) {
48          super(errorLocale);
49      }
50      
51      /**
52       * @param key A regular expression matching header names
53       */
54      // The rest of the Javadoc is inherited
55      @Override
56      public void putComplex(final String key, final BeanField<T, String> value) {
57          complexMapList.add(new RegexToBeanField<>(key, value, errorLocale));
58      }
59      
60      /**
61       * Returns a list of required headers that were not present in the input.
62       * 
63       * @param headersPresent An array of all headers present from the input
64       * @return A list of name + field for all the required headers that were
65       *   not found
66       */
67      public List<FieldMapByNameEntry<T>> determineMissingRequiredHeaders(final String[] headersPresent) {
68          
69          // Start with collections of all required headers
70          final List<String> requiredStringList = simpleMap.entrySet().stream()
71                  .filter(e -> e.getValue().isRequired())
72                  .map(Map.Entry::getKey)
73                  .collect(Collectors.toCollection(LinkedList::new));
74          final List<ComplexFieldMapEntry<String, String, T>> requiredRegexList = complexMapList.stream()
75                  .filter(r -> r.getBeanField().isRequired())
76                  .collect(Collectors.toList());
77  
78          // Now remove the ones we found
79          for(String h : headersPresent) {
80              if(!requiredStringList.remove(h.toUpperCase().trim())) {
81                  final ListIterator<ComplexFieldMapEntry<String, String, T>> requiredRegexListIterator = requiredRegexList.listIterator();
82                  boolean found = false;
83                  while(!found && requiredRegexListIterator.hasNext()) {
84                      final ComplexFieldMapEntry<String, String, T> r = requiredRegexListIterator.next();
85                      if(r.contains(h)) {
86                          found = true;
87                          requiredRegexListIterator.remove();
88                      }
89                  }
90              }
91          }
92          
93          // Repackage what remains
94          List<FieldMapByNameEntry<T>> missingRequiredHeaders = new LinkedList<>();
95          for(String s : requiredStringList) {
96              missingRequiredHeaders.add(new FieldMapByNameEntry<>(s, simpleMap.get(s), false));
97          }
98          for(ComplexFieldMapEntry<String, String, T> r : requiredRegexList) {
99              missingRequiredHeaders.add(new FieldMapByNameEntry<>(r.getInitializer(), r.getBeanField(), true));
100         }
101         
102         return missingRequiredHeaders;
103     }
104     
105     /**
106      * This method generates a header that can be used for writing beans of the
107      * type provided back to a file.
108      * <p>The ordering of the headers is determined by the
109      * {@link java.util.Comparator} passed in to
110      * {@link #setColumnOrderOnWrite(Comparator)}, should that method be called,
111      * otherwise the natural ordering is used (alphabetically ascending).</p>
112      * <p>This implementation will not write headers discovered in multi-valued
113      * bean fields if the headers would not be matched by the bean field on
114      * reading. There are two reasons for this:</p>
115      * <ol><li>opencsv always tries to create data that are round-trip
116      * equivalent, and that would not be the case if it generated data on
117      * writing that it would discard on reading.</li>
118      * <li>As the code is currently written, the header name is used on writing
119      * each bean field to determine the appropriate {@link BeanField} for
120      * information concerning conversions, locales, necessity (whether or not
121      * the field is required). Without this information, conversion is
122      * impossible, and every value written under the unmatched header is blank,
123      * regardless of the contents of the bean.</li></ol>
124      */
125     // The rest of the Javadoc is inherited.
126     @Override
127     public String[] generateHeader(final T bean) throws CsvRequiredFieldEmptyException {
128         final List<Field> missingRequiredHeaders = new LinkedList<>();
129         final List<String> headerList = new ArrayList<>(simpleMap.keySet());
130         for(ComplexFieldMapEntry<String, String, T> r : complexMapList) {
131             @SuppressWarnings("unchecked")
132             final MultiValuedMap<String,T> m = (MultiValuedMap<String,T>) r.getBeanField().getFieldValue(bean);
133             if(m != null && !m.isEmpty()) {
134                 headerList.addAll(m.entries().stream()
135                         .map(Map.Entry::getKey)
136                         .filter(r::contains)
137                         .collect(Collectors.toList()));
138             }
139             else {
140                 if(r.getBeanField().isRequired()) {
141                     missingRequiredHeaders.add(r.getBeanField().getField());
142                 }
143             }
144         }
145         
146         // Report headers that should have been present
147         if(!missingRequiredHeaders.isEmpty()) {
148             String errorMessage = String.format(
149                     ResourceBundle
150                             .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
151                             .getString("header.required.field.absent"),
152                     missingRequiredHeaders.stream()
153                             .map(Field::getName)
154                             .collect(Collectors.joining(" ")),
155                     String.join(" ", headerList));
156             throw new CsvRequiredFieldEmptyException(bean.getClass(), missingRequiredHeaders, errorMessage);
157         }
158         
159         // Sort and return
160         headerList.sort(writeOrder);
161         return headerList.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
162     }
163 
164     /**
165      * Sets the {@link java.util.Comparator} to be used to sort columns when
166      * writing beans to a CSV file.
167      *
168      * @param writeOrder The {@link java.util.Comparator} to use. May be
169      *   {@code null}, in which case the natural ordering is used.
170      * @since 4.3
171      */
172     public void setColumnOrderOnWrite(Comparator<String> writeOrder) {
173         this.writeOrder = writeOrder;
174     }
175 }