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.of(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.of(
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.of(next.getMinimum(), range.getMaximum());
117 }
118 else if(range.getMaximum()+1 == next.getMinimum()) {
119 range = Range.of(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.of(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 }