com.univocity.parsers.common.fields.FieldConversionMapping Maven / Gradle / Ivy
Show all versions of univocity-parsers Show documentation
/*******************************************************************************
* Copyright 2014 Univocity Software Pty Ltd
*
* 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.univocity.parsers.common.fields;
import com.univocity.parsers.annotations.helpers.*;
import com.univocity.parsers.common.*;
import com.univocity.parsers.conversions.*;
import java.util.*;
/**
* A class for mapping field selections to sequences of {@link Conversion} objects
*
* @author Univocity Software Pty Ltd - [email protected]
*/
public class FieldConversionMapping implements Cloneable {
@SuppressWarnings("rawtypes")
private static final Conversion[] EMPTY_CONVERSION_ARRAY = new Conversion[0];
public int[] validatedIndexes;
/**
* This list contains the sequence of conversions applied to sets of fields over multiple calls.
* It is shared by {@link FieldConversionMapping#fieldNameConversionMapping}, {@link FieldConversionMapping#fieldIndexConversionMapping} and {@link FieldConversionMapping#convertAllMapping}.
*
Every time the user associates a sequence of conversions to a field, conversionSequence list will receive the FieldSelector.
*/
private List conversionSequence = new ArrayList();
private AbstractConversionMapping fieldNameConversionMapping = new AbstractConversionMapping(conversionSequence) {
@Override
protected FieldSelector newFieldSelector() {
return new FieldNameSelector();
}
};
private AbstractConversionMapping fieldIndexConversionMapping = new AbstractConversionMapping(conversionSequence) {
@Override
protected FieldSelector newFieldSelector() {
return new FieldIndexSelector();
}
};
@SuppressWarnings("rawtypes")
private AbstractConversionMapping fieldEnumConversionMapping = new AbstractConversionMapping(conversionSequence) {
@Override
protected FieldSelector newFieldSelector() {
return new FieldEnumSelector();
}
};
private AbstractConversionMapping convertAllMapping = new AbstractConversionMapping(conversionSequence) {
@Override
protected FieldSelector newFieldSelector() {
return new AllIndexesSelector();
}
};
/**
* This is the final sequence of conversions applied to each index in a record. It is populated when {@link FieldConversionMapping#prepareExecution(boolean, String[])} is invoked.
*/
private Map>> conversionsByIndex = Collections.emptyMap();
private Map> validationsByIndex = Collections.emptyMap();
/**
* Prepares the conversions registered in this object to be executed against a given sequence of fields
*
* @param writing flag indicating whether a writing process is being initialized.
* @param values The field sequence that identifies how records will be organized.
* This is generally the sequence of headers in a record, but it might be just the first parsed row from a given input (as field selection by index is allowed).
*/
public void prepareExecution(boolean writing, String[] values) {
if (fieldNameConversionMapping.isEmpty() && fieldEnumConversionMapping.isEmpty() && fieldIndexConversionMapping.isEmpty() && convertAllMapping.isEmpty()) {
return;
}
if (!conversionsByIndex.isEmpty()) {
return;
}
//Note this property is shared across all conversion mappings. This is required so
//the correct conversion sequence is registered for all fields.
conversionsByIndex = new HashMap>>();
// adds the conversions in the sequence they were created.
for (FieldSelector next : conversionSequence) {
fieldNameConversionMapping.prepareExecution(writing, next, conversionsByIndex, values);
fieldIndexConversionMapping.prepareExecution(writing, next, conversionsByIndex, values);
fieldEnumConversionMapping.prepareExecution(writing, next, conversionsByIndex, values);
convertAllMapping.prepareExecution(writing, next, conversionsByIndex, values);
}
Iterator>>> entryIterator = conversionsByIndex.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry>> e = entryIterator.next();
Iterator> it = e.getValue().iterator();
while (it.hasNext()) {
Conversion conversion = it.next();
if (conversion instanceof ValidatedConversion) {
if (validationsByIndex.isEmpty()) {
validationsByIndex = new TreeMap>();
}
it.remove();
List validations = validationsByIndex.get(e.getKey());
if (validations == null) {
validations = new ArrayList(1);
validationsByIndex.put(e.getKey(), validations);
}
validations.add((ValidatedConversion) conversion);
}
}
if (e.getValue().isEmpty()) {
entryIterator.remove();
}
}
validatedIndexes = ArgumentUtils.toIntArray(validationsByIndex.keySet());
}
/**
* Applies a sequence of conversions on all fields.
*
* @param conversions the sequence of conversions to be applied
*/
public void applyConversionsOnAllFields(Conversion... conversions) {
convertAllMapping.registerConversions(conversions);
}
/**
* Applies a sequence of conversions on a selection of field indexes
*
* @param conversions the sequence of conversions to be applied
*
* @return a selector of column indexes.
*/
public FieldSet applyConversionsOnFieldIndexes(Conversion... conversions) {
return fieldIndexConversionMapping.registerConversions(conversions);
}
/**
* Applies a sequence of conversions on a selection of field name
*
* @param conversions the sequence of conversions to be applied
*
* @return a selector of column names.
*/
public FieldSet applyConversionsOnFieldNames(Conversion... conversions) {
return fieldNameConversionMapping.registerConversions(conversions);
}
/**
* Applies a sequence of conversions on a selection of enumerations that represent fields
*
* @param conversions the sequence of conversions to be applied
*
* @return a selector of enumerations.
*/
@SuppressWarnings("rawtypes")
public FieldSet applyConversionsOnFieldEnums(Conversion... conversions) {
return fieldEnumConversionMapping.registerConversions(conversions);
}
/**
* Applies any validations associated with a field at a given index in a record
* @param index The index of parsed value in a record
* @param value The value of the record at the given index
*/
public void executeValidations(int index, Object value) {
List validations = validationsByIndex.get(index);
if (validations != null) {
for (int i = 0; i < validations.size(); i++) {
validations.get(i).execute(value);
}
}
}
/**
* Applies a sequence of conversions associated with an Object value at a given index in a record.
*
* @param executeInReverseOrder flag to indicate whether or not the conversion sequence must be executed in reverse order
* @param index The index of parsed value in a record
* @param value The value in a record
* @param convertedFlags an array of flags that indicate whether a conversion occurred. Used to determine whether
* or not a default conversion by type (specified with {@link ConversionProcessor#convertType(Class, Conversion[])}) should be applied.
*
* @return the Object resulting from a sequence of conversions against the original value.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public Object reverseConversions(boolean executeInReverseOrder, int index, Object value, boolean[] convertedFlags) {
List> conversions = conversionsByIndex.get(index);
if (conversions != null) {
if (convertedFlags != null) {
convertedFlags[index] = true;
}
Conversion conversion = null;
try {
if (executeInReverseOrder) {
for (int i = conversions.size() - 1; i >= 0; i--) {
conversion = conversions.get(i);
value = conversion.revert(value);
}
} else {
for (Conversion, ?> c : conversions) {
conversion = c;
value = conversion.revert(value);
}
}
} catch (DataProcessingException ex) {
ex.setValue(value);
ex.setColumnIndex(index);
ex.markAsNonFatal();
throw ex;
} catch (Throwable ex) {
DataProcessingException exception;
if (conversion != null) {
exception = new DataProcessingException("Error converting value '{value}' using conversion " + conversion.getClass().getName(), ex);
} else {
exception = new DataProcessingException("Error converting value '{value}'", ex);
}
exception.setValue(value);
exception.setColumnIndex(index);
exception.markAsNonFatal();
throw exception;
}
}
return value;
}
/**
* Applies a sequence of conversions associated with a String value parsed from a given index.
*
* @param index The index of parsed value in a record
* @param stringValue The parsed value in a record
* @param convertedFlags an array of flags that indicate whether a conversion occurred. Used to determine whether
* or not a default conversion by type (specified with {@link ConversionProcessor#convertType(Class, Conversion[])}) should be applied.
*
* @return the Object produced by a sequence of conversions against the original String value.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public Object applyConversions(int index, String stringValue, boolean[] convertedFlags) {
List> conversions = conversionsByIndex.get(index);
if (conversions != null) {
if (convertedFlags != null) {
convertedFlags[index] = true;
}
Object result = stringValue;
for (Conversion conversion : conversions) {
try {
result = conversion.execute(result);
} catch (DataProcessingException ex) {
ex.setColumnIndex(index);
ex.markAsNonFatal();
throw ex;
} catch (Throwable ex) {
DataProcessingException exception = new DataProcessingException("Error converting value '{value}' using conversion " + conversion.getClass().getName(), ex);
exception.setValue(result);
exception.setColumnIndex(index);
exception.markAsNonFatal();
throw exception;
}
}
return result;
}
return stringValue;
}
/**
* Returns the sequence of conversions to be applied at a given column index
*
* @param index the index of the column where the conversions should be executed
* @param expectedType the type resulting from the conversion sequence.
*
* @return the sequence of conversions to be applied at a given column index
*/
@SuppressWarnings("rawtypes")
public Conversion[] getConversions(int index, Class> expectedType) {
List> conversions = conversionsByIndex.get(index);
Conversion[] out;
if (conversions != null) {
out = new Conversion[conversions.size()];
int i = 0;
for (Conversion conversion : conversions) {
out[i++] = conversion;
}
} else if (expectedType == String.class) {
return EMPTY_CONVERSION_ARRAY;
} else {
out = new Conversion[1];
out[0] = AnnotationHelper.getDefaultConversion(expectedType, null, null);
if (out[0] == null) {
return EMPTY_CONVERSION_ARRAY;
}
}
return out;
}
@Override
public FieldConversionMapping clone() {
try {
FieldConversionMapping out = (FieldConversionMapping) super.clone();
out.validatedIndexes = validatedIndexes == null ? null : this.validatedIndexes.clone();
out.conversionSequence = new ArrayList();
Map clonedSelectors = new HashMap();
for (FieldSelector selector : this.conversionSequence) {
FieldSelector clone = (FieldSelector) selector.clone();
out.conversionSequence.add(clone);
clonedSelectors.put(selector, clone);
}
out.fieldNameConversionMapping = fieldNameConversionMapping.clone(clonedSelectors, out.conversionSequence);
out.fieldIndexConversionMapping = fieldIndexConversionMapping.clone(clonedSelectors, out.conversionSequence);
out.fieldEnumConversionMapping = fieldEnumConversionMapping.clone(clonedSelectors, out.conversionSequence);
out.convertAllMapping = convertAllMapping.clone(clonedSelectors, out.conversionSequence);
out.conversionsByIndex = new HashMap>>(conversionsByIndex);
out.validationsByIndex = new TreeMap>(validationsByIndex);
return out;
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
}
}
/**
* Class responsible for managing field selections and any conversion sequence associated with each.
*
* @param the FieldSelector type information used to uniquely identify a field (e.g. references to field indexes would use Integer, while references to field names would use String).
*
* @author Univocity Software Pty Ltd - [email protected]
* @see FieldNameSelector
* @see FieldIndexSelector
*/
abstract class AbstractConversionMapping implements Cloneable {
private Map[]> conversionsMap;
private List conversionSequence;
AbstractConversionMapping(List conversionSequence) {
this.conversionSequence = conversionSequence;
}
/**
* Registers a sequence of conversions to a set of fields.
* The selector instance that is used to store which fields should be converted is added to the {@link AbstractConversionMapping#conversionSequence} list in order to keep track of the correct conversion order.
*
This is required further conversion sequences might be added to the same fields in separate calls.
*
* @param conversions the conversion sequence to be applied to a set of fields.
*
* @return a FieldSet which provides methods to select the fields that must be converted or null if the FieldSelector returned by #newFieldSelector is not an instance of FieldSet (which is the case of {@link AllIndexesSelector}).
*/
@SuppressWarnings("unchecked")
public FieldSet registerConversions(Conversion... conversions) {
ArgumentUtils.noNulls("Conversions", conversions);
FieldSelector selector = newFieldSelector();
if (conversionsMap == null) {
conversionsMap = new LinkedHashMap[]>();
}
conversionsMap.put(selector, conversions);
conversionSequence.add(selector);
if (selector instanceof FieldSet) {
return (FieldSet) selector;
}
return null;
}
/**
* Creates a FieldSelector instance of the desired type. Used in @link FieldConversionMapping}.
*
* @return a new FieldSelector instance.
*/
protected abstract FieldSelector newFieldSelector();
/**
* Get all indexes in the given selector and adds the conversions defined at that index to the map of conversionsByIndex.
* This method is called in the same sequence each selector was created (in {@link FieldConversionMapping#prepareExecution(boolean, String[])})
*
At the end of the process, the map of conversionsByIndex will have each index with its list of conversions in the order they were declared.
*
* @param writing flag indicating whether a writing process is being initialized.
* @param selector the selected fields for a given conversion sequence.
* @param conversionsByIndex map of all conversions registered to every field index, in the order they were declared
* @param values The field sequence that identifies how records will be organized.
*
This is generally the sequence of headers in a record, but it might be just the first parsed row from a given input (as field selection by index is allowed).
*/
public void prepareExecution(boolean writing, FieldSelector selector, Map>> conversionsByIndex, String[] values) {
if (conversionsMap == null) {
return;
}
//conversionsMap contains maps the conversions applied to a field selection
//we will match the indexes where these conversions where applied and add them to the corresponding list in conversionsByIndex
Conversion[] conversions = conversionsMap.get(selector);
if (conversions == null) {
return;
}
int[] fieldIndexes = selector.getFieldIndexes(NormalizedString.toIdentifierGroupArray(values));
if (fieldIndexes == null) {
fieldIndexes = ArgumentUtils.toIntArray(conversionsByIndex.keySet());
}
for (int fieldIndex : fieldIndexes) {
List> conversionsAtIndex = conversionsByIndex.get(fieldIndex);
if (conversionsAtIndex == null) {
conversionsAtIndex = new ArrayList>();
conversionsByIndex.put(fieldIndex, conversionsAtIndex);
}
validateDuplicates(selector, conversionsAtIndex, conversions);
conversionsAtIndex.addAll(Arrays.asList(conversions));
}
}
/**
* Ensures an individual field does not have the same conversion object applied to it more than once.
*
* @param selector the selection of fields
* @param conversionsAtIndex the sequence of conversions applied to a given index
* @param conversionsToAdd the sequence of conversions to add to conversionsAtIndex
*/
private static void validateDuplicates(FieldSelector selector, List> conversionsAtIndex, Conversion, ?>[] conversionsToAdd) {
for (Conversion, ?> toAdd : conversionsToAdd) {
for (Conversion, ?> existing : conversionsAtIndex) {
if (toAdd == existing) {
throw new DataProcessingException("Duplicate conversion " + toAdd.getClass().getName() + " being applied to " + selector.describe());
}
}
}
}
/**
* Queries if any conversions were associated with any field
*
* @return true if no conversions were associated with any field; false otherwise
*/
public boolean isEmpty() {
return conversionsMap == null || conversionsMap.isEmpty();
}
public AbstractConversionMapping clone() {
try {
return (AbstractConversionMapping) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
}
public AbstractConversionMapping clone(Map clonedSelectors, List clonedConversionSequence) {
AbstractConversionMapping out = clone();
out.conversionSequence = clonedConversionSequence;
if (conversionsMap != null) {
out.conversionsMap = new HashMap[]>();
for (FieldSelector selector : this.conversionSequence) {
FieldSelector clone = clonedSelectors.get(selector);
if (clone == null) {
throw new IllegalStateException("Internal error cloning conversion mappings");
}
Conversion[] conversions = conversionsMap.get(selector);
out.conversionsMap.put(clone, conversions);
}
}
return out;
}
}