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.CsvBadConverterException;
20  import org.apache.commons.lang3.Range;
21  import org.apache.commons.lang3.StringUtils;
22  
23  import java.util.*;
24  
25  /**
26   * Maps any column position matching a range definition to a {@link BeanField}.
27   *
28   * @param <T> Type of the bean being converted
29   * @author Andrew Rucker Jones
30   */
31  public class PositionToBeanField<T> extends AbstractFieldMapEntry<String, Integer, T> implements Iterable<FieldMapByPositionEntry<T>> {
32      
33      /**
34       * This is the string used to initialize this set of ranges.
35       * This is necessary because the ranges may be attenuated later by
36       * {@link #attenuateRanges(int)}, rendering a reconstruction of the original
37       * initialization information impossible.
38       */
39      private final String initializer;
40  
41      /** A list of ranges of column indices that should be mapped to the associated bean. */
42      private final List<Range<Integer>> ranges;
43  
44      /**
45       * Initializes this mapping with a list of ranges and the associated
46       * {@link BeanField}.
47       * 
48       * @param rangeDefinition A definition of ranges as documented in
49       *   {@link CsvBindAndJoinByPosition#position()}
50       * @param maxIndex The maximum index allowed for a range. Ranges will be
51       *   adjusted as documented in {@link #attenuateRanges(int)}.
52       * @param field The {@link BeanField} this mapping maps to
53       * @param errorLocale The locale for error messages
54       * @throws CsvBadConverterException If {@code rangeDefinition} cannot be parsed
55       */
56      public PositionToBeanField(final String rangeDefinition, int maxIndex, final BeanField<T, Integer> field, Locale errorLocale) {
57          super(field, errorLocale);
58          initializer = rangeDefinition;
59          ranges = new LinkedList<>();
60  
61          // Error on empty range
62          if(StringUtils.isBlank(rangeDefinition)) {
63              throw new CsvBadConverterException(
64                      BeanFieldJoin.class,
65                      String.format(
66                          ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, this.errorLocale)
67                                  .getString("invalid.range.definition"),
68                          rangeDefinition));
69          }
70  
71          final String[] partialRangeDefinitions = rangeDefinition.split(",");
72          try {
73              for(String r : partialRangeDefinitions) {
74                  if(StringUtils.isNotEmpty(r)) {
75                      Range<Integer> range;
76                      
77                      // Create the next range
78                      if(r.contains("-")) {
79                          final String[] endpoints = r.split("-", 2);
80                          final Integer min = StringUtils.isEmpty(endpoints[0]) ? Integer.valueOf(0) : Integer.valueOf(endpoints[0].trim());
81                          Integer max = maxIndex;
82                          if(endpoints.length == 2 && StringUtils.isNotEmpty(endpoints[1])) {
83                              max = Integer.valueOf(endpoints[1].trim());
84                          }
85                          if(max >= maxIndex) {
86                              if(min >= maxIndex) {
87                                  max = min;
88                              }
89                              else {
90                                  max = maxIndex;
91                              }
92                          }
93                          range = Range.between(min, max);
94                      }
95                      else {
96                          range = Range.is(Integer.valueOf(r));
97                      }
98  
99                      // Find out if this new range overlaps any of the
100                     // preexisting ranges, and consolidate as much as possible
101                     final ListIterator<Range<Integer>> it = ranges.listIterator();
102                     boolean completelyContained = false;
103                     while(it.hasNext() && ! completelyContained) {
104                         final Range<Integer> next = it.next();
105                         if(next.containsRange(range)) {
106                             completelyContained = true;
107                         }
108                         else {
109                             if(next.isOverlappedBy(range)) {
110                                 range = Range.between(
111                                         Math.min(next.getMinimum(), range.getMinimum()),
112                                         Math.max(next.getMaximum(), range.getMaximum()));
113                                 it.remove();
114                             }
115                             else if(next.getMaximum()+1 == range.getMinimum()) {
116                                 range = Range.between(next.getMinimum(), range.getMaximum());
117                             }
118                             else if(range.getMaximum()+1 == next.getMinimum()) {
119                                 range = Range.between(range.getMinimum(), next.getMaximum());
120                             }
121                         }
122                     }
123                     if(!completelyContained) {
124                         ranges.add(range);
125                     }
126                 }
127             }
128         }
129         catch(NumberFormatException e) {
130             // If the programmer specified non-numbers in the range
131             final CsvBadConverterException csve = new CsvBadConverterException(
132                     BeanFieldJoin.class,
133                     String.format(
134                         ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, this.errorLocale)
135                                 .getString("invalid.range.definition"),
136                         rangeDefinition));
137             csve.initCause(e);
138             throw csve;
139         }
140     }
141     
142     /**
143      * If there are ranges in the list of ranges encompassed by this mapping
144      * that stretch beyond the maximum index given, they are shortened to be
145      * no longer than the maximum index.
146      * Ranges that lie completely beyond the maximum index are shortened to a
147      * one-element range consisting of the range's lower boundary. No ranges are
148      * under any circumstances removed, as this might compromise checks for
149      * required fields.
150      * 
151      * @param maxIndex The new maximum for ranges
152      */
153     public void attenuateRanges(int maxIndex) {
154         ListIterator<Range<Integer>> rangeIterator = ranges.listIterator();
155         while(rangeIterator.hasNext()) {
156             Range<Integer> r = rangeIterator.next();
157             if(r.getMaximum() > maxIndex) {
158                 if(r.getMinimum() > maxIndex) {
159                     rangeIterator.set(Range.is(r.getMinimum()));
160                 }
161                 else {
162                     rangeIterator.set(Range.between(r.getMinimum(), maxIndex));
163                 }
164             }
165         }
166     }
167     
168     @Override
169     public boolean contains(Integer key) {
170         return ranges.stream().anyMatch(range -> range.contains(key));
171     }
172 
173     @Override
174     public String getInitializer() {
175         return initializer;
176     }
177     
178     @Override
179     public Iterator<FieldMapByPositionEntry<T>> iterator() {
180         return new PositionIterator();
181     }
182     
183     /**
184      * This iterator is designed to iterate over every element of all of the
185      * ranges specified in the containing class.
186      * <p>There is no guaranteed order.</p>
187      * <p>There is one exception to returning all values: if a range ends at
188      * {@link Integer#MAX_VALUE}, only the minimum in the range is returned.
189      * This is to prevent a loop that for all practical purposes might as well
190      * be infinite. Unless someone foolishly specifies {@link Integer#MAX_VALUE}
191      * as a column position, this only occurs after reading in ranges and before
192      * the first line of the input is read. There is no reason in the opencsv
193      * code to iterate at this point, and it is not done. There should be no
194      * reason for user code to use this iterator at all, but if it does, the
195      * user is herewith warned.</p>
196      */
197     private class PositionIterator implements Iterator<FieldMapByPositionEntry<T>> {
198 
199         private ListIterator<Range<Integer>> rangeIterator;
200         private Range<Integer> currentRange;
201         private int position;
202 
203         PositionIterator() {
204             if(ranges.isEmpty()) {
205                 position = -1;
206             }
207             else {
208                 rangeIterator = ranges.listIterator();
209                 currentRange = rangeIterator.next();
210                 position = currentRange.getMinimum();
211             }
212         }
213 
214         @Override
215         public boolean hasNext() {
216             return position != -1;
217         }
218 
219         @Override
220         public FieldMapByPositionEntry<T> next() {
221             
222             // Standard handling
223             if(!hasNext()) {
224                 throw new NoSuchElementException();
225             }
226             
227             // Value to return
228             FieldMapByPositionEntry<T> entry = new FieldMapByPositionEntry<>(position, field);
229             
230             // Advance the cursor. We add one extra precaution here: if a range
231             // goes out to Integer.MAX_VALUE, we only return the minimum. This
232             // is to prevent a seemingly endless loop on iteration.
233             if(position == currentRange.getMaximum()
234                     || Integer.MAX_VALUE == currentRange.getMaximum()) {
235                 if(!rangeIterator.hasNext()) {
236                     position = -1;
237                 }
238                 else {
239                     currentRange = rangeIterator.next();
240                     position = currentRange.getMinimum();
241                 }
242             }
243             else {
244                 position++;
245             }
246             return entry;
247         }
248 
249         @Override
250         public void remove() {
251             throw new UnsupportedOperationException();
252         }
253     }
254 }