HeaderNameBaseMappingStrategy.java

package com.opencsv.bean;

import com.opencsv.CSVReader;
import com.opencsv.ICSVParser;
import com.opencsv.exceptions.CsvBadConverterException;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
import org.apache.commons.collections4.ListValuedMap;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.*;

/**
 * This class serves as a location to collect code common to a mapping strategy
 * that maps header names to member variables.
 *
 * @param <T> The type of bean being created or written
 * @author Andrew Rucker Jones
 * @since 5.0
 */
abstract public class HeaderNameBaseMappingStrategy<T> extends AbstractMappingStrategy<String, String, ComplexFieldMapEntry<String, String, T>, T> {

    /**
     * Given a header name, this map allows one to find the corresponding
     * {@link BeanField}.
     */
    protected FieldMapByName<T> fieldMap = null;

    /** Holds a {@link java.util.Comparator} to sort columns on writing. */
    protected Comparator<String> writeOrder = null;

    /** If set, every record will be shortened or lengthened to match the number of headers. */
    protected final boolean forceCorrectRecordLength;

    /** Nullary constructor for compatibility. */
    public HeaderNameBaseMappingStrategy() {
        this.forceCorrectRecordLength = false;
    }

    /**
     * Constructor to allow setting options for header name mapping.
     *
     * @param forceCorrectRecordLength If set, every record will be shortened
     *                                 or lengthened to match the number of
     *                                 headers
     */
    public HeaderNameBaseMappingStrategy(boolean forceCorrectRecordLength) {
        this.forceCorrectRecordLength = forceCorrectRecordLength;
    }

    @Override
    public void captureHeader(CSVReader reader) throws IOException, CsvRequiredFieldEmptyException {
        // Validation
        if(type == null) {
            throw new IllegalStateException(ResourceBundle
                    .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
                    .getString("type.unset"));
        }

        // Read the header
        String[] header = ArrayUtils.nullToEmpty(reader.readNextSilently());
        for(int i = 0; i < header.length; i++) {
            // For the case that a header is empty and someone configured
            // empty fields to be null
            if(header[i] == null) {
                header[i] = StringUtils.EMPTY;
            }
        }
        headerIndex.initializeHeaderIndex(header);

        // Throw an exception if any required headers are missing
        List<FieldMapByNameEntry<T>> missingRequiredHeaders = fieldMap.determineMissingRequiredHeaders(header);
        if (!missingRequiredHeaders.isEmpty()) {
            String[] requiredHeaderNames = new String[missingRequiredHeaders.size()];
            List<Field> requiredFields = new ArrayList<>(missingRequiredHeaders.size());
            for(int i = 0; i < missingRequiredHeaders.size(); i++) {
                FieldMapByNameEntry<T> fme = missingRequiredHeaders.get(i);
                if(fme.isRegexPattern()) {
                    requiredHeaderNames[i] = String.format(
                            ResourceBundle
                                    .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
                                    .getString("matching"),
                            fme.getName());
                } else {
                    requiredHeaderNames[i] = fme.getName();
                }
                requiredFields.add(fme.getField().getField());
            }
            String missingRequiredFields = String.join(", ", requiredHeaderNames);
            String allHeaders = String.join(",", header);
            CsvRequiredFieldEmptyException e = new CsvRequiredFieldEmptyException(type, requiredFields,
                    String.format(
                            ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
                                    .getString("header.required.field.absent"),
                            missingRequiredFields, allHeaders));
            e.setLine(header);
            throw e;
        }
    }

    @Override
    protected String chooseMultivaluedFieldIndexFromHeaderIndex(int index) {
        String[] s = headerIndex.getHeaderIndex();
        return index >= s.length ? null: s[index];
    }

    @Override
    public void verifyLineLength(int numberOfFields) throws CsvRequiredFieldEmptyException {
        if(!headerIndex.isEmpty()) {
            if (numberOfFields != headerIndex.getHeaderIndexLength() && !forceCorrectRecordLength) {
                throw new CsvRequiredFieldEmptyException(type, ResourceBundle
                        .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
                        .getString("header.data.mismatch"));
            }
        }
    }

    @Override
    protected BeanField<T, String> findField(int col) throws CsvBadConverterException {
        BeanField<T, String> beanField = null;
        String columnName = getColumnName(col);
        if (columnName == null) {
            return null;
        }
        columnName = columnName.trim();
        if (!columnName.isEmpty()) {
            beanField = fieldMap.get(columnName.toUpperCase());
        }
        return beanField;
    }

    /**
     * Creates a map of fields in the bean to be processed that have no binding
     * annotations.
     * <p>This method is called by {@link #loadFieldMap()} when absolutely no
     * binding annotations that are relevant for this mapping strategy are
     * found in the type of bean being processed. It is then assumed that every
     * field is to be included, and that the name of the member variable must
     * exactly match the header name of the input.</p>
     * <p>Two exceptions are made to the rule that everything is written:<ol>
     *     <li>Any field annotated with {@link CsvIgnore} will be
     *     ignored on writing</li>
     *     <li>Any field named "serialVersionUID" will be ignored if the
     *     enclosing class implements {@link java.io.Serializable}.</li>
     * </ol></p>
     * <p>{@link CsvRecurse} is respected.</p>
     */
    @Override
    protected void loadUnadornedFieldMap(ListValuedMap<Class<?>, Field> fields) {
        fields.entries().stream()
                .filter(entry -> !(Serializable.class.isAssignableFrom(entry.getKey()) && "serialVersionUID".equals(entry.getValue().getName())))
                .filter(entry -> !entry.getValue().isAnnotationPresent(CsvRecurse.class))
                .forEach(entry -> {
                    final CsvConverter converter = determineConverter(entry.getValue(), entry.getValue().getType(), null, null, null);
                    fieldMap.put(entry.getValue().getName().toUpperCase(), new BeanFieldSingleValue<>(
                            entry.getKey(), entry.getValue(),
                            false, errorLocale, converter, null, null));
                });
    }

    @Override
    protected void initializeFieldMap() {
        fieldMap = new FieldMapByName<>(errorLocale);
        fieldMap.setColumnOrderOnWrite(writeOrder);
    }

    @Override
    protected FieldMap<String, String, ? extends ComplexFieldMapEntry<String, String, T>, T> getFieldMap() {return fieldMap;}

    @Override
    public String findHeader(int col) {
        return headerIndex.getByPosition(col);
    }

    /**
     * Sets the {@link java.util.Comparator} to be used to sort columns when
     * writing beans to a CSV file.
     * Behavior of this method when used on a mapping strategy intended for
     * reading data from a CSV source is not defined.
     *
     * @param writeOrder The {@link java.util.Comparator} to use. May be
     *   {@code null}, in which case the natural ordering is used.
     * @since 4.3
     */
    public void setColumnOrderOnWrite(Comparator<String> writeOrder) {
        this.writeOrder = writeOrder;
        if(fieldMap != null) {
            fieldMap.setColumnOrderOnWrite(this.writeOrder);
        }
    }
}