PositionToBeanField.java
/*
* Copyright 2018 Andrew Rucker Jones.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.opencsv.bean;
import com.opencsv.ICSVParser;
import com.opencsv.exceptions.CsvBadConverterException;
import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
/**
* Maps any column position matching a range definition to a {@link BeanField}.
*
* @param <T> Type of the bean being converted
* @author Andrew Rucker Jones
*/
public class PositionToBeanField<T> extends AbstractFieldMapEntry<String, Integer, T> implements Iterable<FieldMapByPositionEntry<T>> {
/**
* This is the string used to initialize this set of ranges.
* This is necessary because the ranges may be attenuated later by
* {@link #attenuateRanges(int)}, rendering a reconstruction of the original
* initialization information impossible.
*/
private final String initializer;
/** A list of ranges of column indices that should be mapped to the associated bean. */
private final List<Range<Integer>> ranges;
/**
* Initializes this mapping with a list of ranges and the associated
* {@link BeanField}.
*
* @param rangeDefinition A definition of ranges as documented in
* {@link CsvBindAndJoinByPosition#position()}
* @param maxIndex The maximum index allowed for a range. Ranges will be
* adjusted as documented in {@link #attenuateRanges(int)}.
* @param field The {@link BeanField} this mapping maps to
* @param errorLocale The locale for error messages
* @throws CsvBadConverterException If {@code rangeDefinition} cannot be parsed
*/
public PositionToBeanField(final String rangeDefinition, int maxIndex, final BeanField<T, Integer> field, Locale errorLocale) {
super(field, errorLocale);
initializer = rangeDefinition;
ranges = new LinkedList<>();
// Error on empty range
if(StringUtils.isBlank(rangeDefinition)) {
throw new CsvBadConverterException(
BeanFieldJoin.class,
String.format(
ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, this.errorLocale)
.getString("invalid.range.definition"),
rangeDefinition));
}
final String[] partialRangeDefinitions = rangeDefinition.split(",");
try {
for(String r : partialRangeDefinitions) {
if(StringUtils.isNotEmpty(r)) {
Range<Integer> range;
// Create the next range
if(r.contains("-")) {
final String[] endpoints = r.split("-", 2);
final Integer min = StringUtils.isEmpty(endpoints[0]) ? Integer.valueOf(0) : Integer.valueOf(endpoints[0].trim());
Integer max = maxIndex;
if(endpoints.length == 2 && StringUtils.isNotEmpty(endpoints[1])) {
max = Integer.valueOf(endpoints[1].trim());
}
if(max >= maxIndex) {
if(min >= maxIndex) {
max = min;
}
else {
max = maxIndex;
}
}
range = Range.between(min, max);
}
else {
range = Range.is(Integer.valueOf(r));
}
// Find out if this new range overlaps any of the
// preexisting ranges, and consolidate as much as possible
final ListIterator<Range<Integer>> it = ranges.listIterator();
boolean completelyContained = false;
while(it.hasNext() && ! completelyContained) {
final Range<Integer> next = it.next();
if(next.containsRange(range)) {
completelyContained = true;
}
else {
if(next.isOverlappedBy(range)) {
range = Range.between(
Math.min(next.getMinimum(), range.getMinimum()),
Math.max(next.getMaximum(), range.getMaximum()));
it.remove();
}
else if(next.getMaximum()+1 == range.getMinimum()) {
range = Range.between(next.getMinimum(), range.getMaximum());
}
else if(range.getMaximum()+1 == next.getMinimum()) {
range = Range.between(range.getMinimum(), next.getMaximum());
}
}
}
if(!completelyContained) {
ranges.add(range);
}
}
}
}
catch(NumberFormatException e) {
// If the programmer specified non-numbers in the range
final CsvBadConverterException csve = new CsvBadConverterException(
BeanFieldJoin.class,
String.format(
ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, this.errorLocale)
.getString("invalid.range.definition"),
rangeDefinition));
csve.initCause(e);
throw csve;
}
}
/**
* If there are ranges in the list of ranges encompassed by this mapping
* that stretch beyond the maximum index given, they are shortened to be
* no longer than the maximum index.
* Ranges that lie completely beyond the maximum index are shortened to a
* one-element range consisting of the range's lower boundary. No ranges are
* under any circumstances removed, as this might compromise checks for
* required fields.
*
* @param maxIndex The new maximum for ranges
*/
public void attenuateRanges(int maxIndex) {
ListIterator<Range<Integer>> rangeIterator = ranges.listIterator();
while(rangeIterator.hasNext()) {
Range<Integer> r = rangeIterator.next();
if(r.getMaximum() > maxIndex) {
if(r.getMinimum() > maxIndex) {
rangeIterator.set(Range.is(r.getMinimum()));
}
else {
rangeIterator.set(Range.between(r.getMinimum(), maxIndex));
}
}
}
}
@Override
public boolean contains(Integer key) {
return ranges.stream().anyMatch(range -> range.contains(key));
}
@Override
public String getInitializer() {
return initializer;
}
@Override
public Iterator<FieldMapByPositionEntry<T>> iterator() {
return new PositionIterator();
}
/**
* This iterator is designed to iterate over every element of all of the
* ranges specified in the containing class.
* <p>There is no guaranteed order.</p>
* <p>There is one exception to returning all values: if a range ends at
* {@link Integer#MAX_VALUE}, only the minimum in the range is returned.
* This is to prevent a loop that for all practical purposes might as well
* be infinite. Unless someone foolishly specifies {@link Integer#MAX_VALUE}
* as a column position, this only occurs after reading in ranges and before
* the first line of the input is read. There is no reason in the opencsv
* code to iterate at this point, and it is not done. There should be no
* reason for user code to use this iterator at all, but if it does, the
* user is herewith warned.</p>
*/
private class PositionIterator implements Iterator<FieldMapByPositionEntry<T>> {
private ListIterator<Range<Integer>> rangeIterator;
private Range<Integer> currentRange;
private int position;
PositionIterator() {
if(ranges.isEmpty()) {
position = -1;
}
else {
rangeIterator = ranges.listIterator();
currentRange = rangeIterator.next();
position = currentRange.getMinimum();
}
}
@Override
public boolean hasNext() {
return position != -1;
}
@Override
public FieldMapByPositionEntry<T> next() {
// Standard handling
if(!hasNext()) {
throw new NoSuchElementException();
}
// Value to return
FieldMapByPositionEntry<T> entry = new FieldMapByPositionEntry<>(position, field);
// Advance the cursor. We add one extra precaution here: if a range
// goes out to Integer.MAX_VALUE, we only return the minimum. This
// is to prevent a seemingly endless loop on iteration.
if(position == currentRange.getMaximum()
|| Integer.MAX_VALUE == currentRange.getMaximum()) {
if(!rangeIterator.hasNext()) {
position = -1;
}
else {
currentRange = rangeIterator.next();
position = currentRange.getMinimum();
}
}
else {
position++;
}
return entry;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
}