com.opencsv.bean.PositionToBeanField Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of opencsv Show documentation
Show all versions of opencsv Show documentation
A simple library for reading and writing CSV in 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 java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.ResourceBundle;
import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.StringUtils;
/**
* Maps any column position matching a range definition to a {@link BeanField}.
*
* @author Andrew Rucker Jones
*/
public class PositionToBeanField extends AbstractFieldMapEntry implements Iterable> {
/**
* 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> 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 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 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> it = ranges.listIterator();
boolean completelyContained = false;
while(it.hasNext() && ! completelyContained) {
final Range 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> rangeIterator = ranges.listIterator();
while(rangeIterator.hasNext()) {
Range 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) {
boolean foundRange = false;
ListIterator> rangeIterator = ranges.listIterator();
while(rangeIterator.hasNext() && !foundRange) {
final Range range = rangeIterator.next();
if(range.contains(key)) {
foundRange = true;
}
}
return foundRange;
}
@Override
public String getInitializer() {
return initializer;
}
@Override
public Iterator> iterator() {
return new PositionIterator();
}
/**
* This iterator is designed to iterate over every element of all of the
* ranges specified in the containing class.
* There is no guaranteed order.
* 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.
*/
private class PositionIterator implements Iterator> {
private ListIterator> rangeIterator;
private Range currentRange;
private int position;
public 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 next() {
// Standard handling
if(!hasNext()) {
throw new NoSuchElementException();
}
// Value to return
FieldMapByPositionEntry 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();
}
}
}