FieldConversionMapping.java

/*******************************************************************************
 * 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 - <a href="mailto:parsers@univocity.com">parsers@univocity.com</a>
 */
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.
	 * <p>It is shared by {@link FieldConversionMapping#fieldNameConversionMapping}, {@link FieldConversionMapping#fieldIndexConversionMapping} and {@link FieldConversionMapping#convertAllMapping}.
	 * <p>Every time the user associates a sequence of conversions to a field, conversionSequence list will receive the FieldSelector.
	 */
	private List<FieldSelector> conversionSequence = new ArrayList<FieldSelector>();

	private AbstractConversionMapping<String> fieldNameConversionMapping = new AbstractConversionMapping<String>(conversionSequence) {
		@Override
		protected FieldSelector newFieldSelector() {
			return new FieldNameSelector();
		}
	};

	private AbstractConversionMapping<Integer> fieldIndexConversionMapping = new AbstractConversionMapping<Integer>(conversionSequence) {
		@Override
		protected FieldSelector newFieldSelector() {
			return new FieldIndexSelector();
		}
	};

	@SuppressWarnings("rawtypes")
	private AbstractConversionMapping<Enum> fieldEnumConversionMapping = new AbstractConversionMapping<Enum>(conversionSequence) {
		@Override
		protected FieldSelector newFieldSelector() {
			return new FieldEnumSelector();
		}
	};

	private AbstractConversionMapping<Integer> convertAllMapping = new AbstractConversionMapping<Integer>(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<Integer, List<Conversion<?, ?>>> conversionsByIndex = Collections.emptyMap();

	private Map<Integer, List<ValidatedConversion>> 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.
	 *                <p> 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<Integer, List<Conversion<?, ?>>>();

		// 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<Map.Entry<Integer, List<Conversion<?, ?>>>> entryIterator = conversionsByIndex.entrySet().iterator();

		while (entryIterator.hasNext()) {
			Map.Entry<Integer, List<Conversion<?, ?>>> e = entryIterator.next();
			Iterator<Conversion<?, ?>> it = e.getValue().iterator();
			while (it.hasNext()) {
				Conversion conversion = it.next();
				if (conversion instanceof ValidatedConversion) {
					if (validationsByIndex.isEmpty()) {
						validationsByIndex = new TreeMap<Integer, List<ValidatedConversion>>();
					}

					it.remove();
					List<ValidatedConversion> validations = validationsByIndex.get(e.getKey());
					if (validations == null) {
						validations = new ArrayList<ValidatedConversion>(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<String, ?>... 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<Integer> applyConversionsOnFieldIndexes(Conversion<String, ?>... 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<String> applyConversionsOnFieldNames(Conversion<String, ?>... 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<Enum> applyConversionsOnFieldEnums(Conversion<String, ?>... 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<ValidatedConversion> 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<Conversion<?, ?>> 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<Conversion<?, ?>> 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<Conversion<?, ?>> 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<FieldSelector>();

			Map<FieldSelector, FieldSelector> clonedSelectors = new HashMap<FieldSelector, FieldSelector>();

			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<Integer, List<Conversion<?, ?>>>(conversionsByIndex);
			out.validationsByIndex = new TreeMap<Integer, List<ValidatedConversion>>(validationsByIndex);
			return out;
		} catch (CloneNotSupportedException e) {
			throw new IllegalStateException(e);
		}
	}
}

/**
 * Class responsible for managing field selections and any conversion sequence associated with each.
 *
 * @param <T> 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 - <a href="mailto:parsers@univocity.com">parsers@univocity.com</a>
 * @see FieldNameSelector
 * @see FieldIndexSelector
 */
abstract class AbstractConversionMapping<T> implements Cloneable {

	private Map<FieldSelector, Conversion<String, ?>[]> conversionsMap;
	private List<FieldSelector> conversionSequence;

	AbstractConversionMapping(List<FieldSelector> conversionSequence) {
		this.conversionSequence = conversionSequence;
	}

	/**
	 * Registers a sequence of conversions to a set of fields.
	 * <p>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.
	 * <p>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<T> registerConversions(Conversion<String, ?>... conversions) {
		ArgumentUtils.noNulls("Conversions", conversions);

		FieldSelector selector = newFieldSelector();

		if (conversionsMap == null) {
			conversionsMap = new LinkedHashMap<FieldSelector, Conversion<String, ?>[]>();
		}
		conversionsMap.put(selector, conversions);
		conversionSequence.add(selector);
		if (selector instanceof FieldSet) {
			return (FieldSet<T>) 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.
	 * <p>This method is called in the same sequence each selector was created (in {@link FieldConversionMapping#prepareExecution(boolean, String[])})
	 * <p>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.
	 *                           <p> 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<Integer, List<Conversion<?, ?>>> 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<String, ?>[] 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<Conversion<?, ?>> conversionsAtIndex = conversionsByIndex.get(fieldIndex);
			if (conversionsAtIndex == null) {
				conversionsAtIndex = new ArrayList<Conversion<?, ?>>();
				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<Conversion<?, ?>> 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<T> clone() {
		try {
			return (AbstractConversionMapping<T>) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new IllegalStateException(e);
		}
	}

	public AbstractConversionMapping<T> clone(Map<FieldSelector, FieldSelector> clonedSelectors, List<FieldSelector> clonedConversionSequence) {
		AbstractConversionMapping<T> out = clone();
		out.conversionSequence = clonedConversionSequence;

		if (conversionsMap != null) {
			out.conversionsMap = new HashMap<FieldSelector, Conversion<String, ?>[]>();

			for (FieldSelector selector : this.conversionSequence) {
				FieldSelector clone = clonedSelectors.get(selector);

				if (clone == null) {
					throw new IllegalStateException("Internal error cloning conversion mappings");
				}

				Conversion<String, ?>[] conversions = conversionsMap.get(selector);
				out.conversionsMap.put(clone, conversions);
			}
		}

		return out;

	}
}