BeanFieldSplit.java

/*
 * Copyright 2017 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.bean.util.OpencsvUtils;
import com.opencsv.exceptions.CsvBadConverterException;
import com.opencsv.exceptions.CsvBeanIntrospectionException;
import com.opencsv.exceptions.CsvConstraintViolationException;
import com.opencsv.exceptions.CsvDataTypeMismatchException;
import org.apache.commons.collections4.Bag;
import org.apache.commons.collections4.SortedBag;
import org.apache.commons.collections4.bag.HashBag;
import org.apache.commons.collections4.bag.TreeBag;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import java.lang.reflect.Field;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class concerns itself with handling collection-valued bean fields.
 * 
 * @param <T> The type of the bean being populated
 * @param <I> Type of the index into a multivalued field
 * @author Andrew Rucker Jones
 * @since 4.2
 */
public class BeanFieldSplit<T, I> extends AbstractBeanField<T, I> {
    
    private final Pattern splitOn, capture;
    private final String writeDelimiter, writeFormat;
    private final Class<? extends Collection> collectionType;
    private final Class<?> elementType;
    
    /**
     * The only valid constructor.
     *
     * @param type The type of the class in which this field is found. This is
     *             the type as instantiated by opencsv, and not necessarily the
     *             type in which the field is declared in the case of
     *             inheritance.
     * @param field A {@link java.lang.reflect.Field} object.
     * @param required Whether or not this field is required in input
     * @param errorLocale The errorLocale to use for error messages.
     * @param converter The converter to be used to perform the actual data
     *   conversion
     * @param splitOn See {@link CsvBindAndSplitByName#splitOn()}
     * @param writeDelimiter See {@link CsvBindAndSplitByName#writeDelimiter()}
     * @param collectionType  See {@link CsvBindAndSplitByName#collectionType()}
     * @param elementType See {@link CsvBindAndSplitByName#elementType()}
     * @param capture See {@link CsvBindAndSplitByName#capture()}
     * @param format The format string used for packaging values to be written.
     *               If {@code null} or empty, it is ignored.
     */
    public BeanFieldSplit(
            Class<?> type, Field field, boolean required, Locale errorLocale,
            CsvConverter converter, String splitOn, String writeDelimiter,
            Class<? extends Collection> collectionType, Class<?> elementType,
            String capture, String format) {
        
        // Simple assignments
        super(type, field, required, errorLocale, converter);
        this.writeDelimiter = writeDelimiter;
        this.writeFormat = format;
        this.elementType = elementType;
        
        // Check that we really have a collection
        if(!Collection.class.isAssignableFrom(field.getType())) {
            throw new CsvBadConverterException(
                    BeanFieldSplit.class,
                    String.format(
                            ResourceBundle.getBundle(
                                    ICSVParser.DEFAULT_BUNDLE_NAME,
                                    this.errorLocale).getString("invalid.collection.type"),
                            field.getType().toString()));
        }
        
        // Check the regular expressions for validity and compile once for speed
        this.splitOn = OpencsvUtils.compilePattern(splitOn, 0,
                BeanFieldSplit.class, this.errorLocale);
        this.capture = OpencsvUtils.compilePatternAtLeastOneGroup(capture, 0,
                BeanFieldSplit.class, this.errorLocale);

        // Verify that the format string works as expected
        OpencsvUtils.verifyFormatString(this.writeFormat, BeanFieldSplit.class, this.errorLocale);

        // Determine the Collection implementation that should be instantiated
        // for every bean.
        Class<?> fieldType = field.getType();
        if(!fieldType.isInterface()) {
            this.collectionType = (Class<Collection>)field.getType();
        }
        else if(!collectionType.isInterface()) {
            this.collectionType = collectionType;
        }
        else {
            if(Collection.class.equals(fieldType) || List.class.equals(fieldType)) {
                this.collectionType = ArrayList.class;
            }
            else if(Set.class.equals(fieldType)) {
                if(fieldType.isEnum()) {
                    this.collectionType = EnumSet.class;
                }
                else {
                    this.collectionType = HashSet.class;
                }
            }
            else if(SortedSet.class.equals(fieldType) || NavigableSet.class.equals(fieldType)) {
                this.collectionType = TreeSet.class;
            }
            else if(Queue.class.equals(fieldType) || Deque.class.equals(fieldType)) {
                this.collectionType = ArrayDeque.class;
            }
            else if(Bag.class.equals(fieldType)) {
                this.collectionType = HashBag.class;
            }
            else if(SortedBag.class.equals(fieldType)) {
                this.collectionType = TreeBag.class;
            }
            else {
                this.collectionType = null;
                throw new CsvBadConverterException(
                        BeanFieldSplit.class,
                        String.format(
                                ResourceBundle.getBundle(
                                        ICSVParser.DEFAULT_BUNDLE_NAME,
                                        this.errorLocale).getString("invalid.collection.type"),
                                collectionType.toString()));
            }
        }
        
        // Now that we know what type we want to assign, run one last check
        // that assignment is truly possible
        if(!field.getType().isAssignableFrom(this.collectionType)) {
            throw new CsvBadConverterException(
                    BeanFieldSplit.class,
                    String.format(
                            ResourceBundle.getBundle(
                                    ICSVParser.DEFAULT_BUNDLE_NAME,
                                    this.errorLocale).getString("unassignable.collection.type"),
                            collectionType.getName(), field.getType().getName()));
        }
    }

    /**
     * This method manages the collection being created as well as splitting the
     * data.
     * Once the data are split, they are sent to the converter for the actual
     * conversion.
     * 
     * @see ConverterPrimitiveTypes#convertToRead(java.lang.String) 
     * @see ConverterDate#convertToRead(java.lang.String)
     * @see ConverterNumber#convertToRead(String) 
     */
    // The rest of the Javadoc is inherited
    @Override
    protected Object convert(String value) throws CsvDataTypeMismatchException, CsvConstraintViolationException {
        Collection<Object> collection;
        try {
            if(collectionType.equals(EnumSet.class)) {
                collection = (Collection)EnumSet.noneOf((Class<Enum>)elementType);
            }
            else {
                collection = collectionType.newInstance();
            }
        }
        catch(InstantiationException | IllegalAccessException e) {
            CsvBeanIntrospectionException csve = new CsvBeanIntrospectionException(
                    String.format(
                            ResourceBundle
                                    .getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
                                    .getString("collection.cannot.be.instantiated"),
                            collectionType.getCanonicalName()));
            csve.initCause(e);
            throw csve;
        }
        
        String[] splitValues = value == null ? ArrayUtils.EMPTY_STRING_ARRAY : splitOn.split(value);
        for(String s : splitValues) {
            if(capture != null) {
                Matcher m = capture.matcher(s);
                if(m.matches()) {
                    s = m.group(1);
                }
                // Otherwise s remains intentionally unchanged
            }
            collection.add(converter.convertToRead(s));
        }
        return collection;
    }

    /**
     * Manages converting a collection of values into a single string.
     * The conversion of each individual value is performed by the converter.
     */
    // The rest of the Javadoc is inherited
    @Override
    protected String convertToWrite(Object value)
            throws CsvDataTypeMismatchException {
        String retval = StringUtils.EMPTY;
        if(value != null) {
            @SuppressWarnings("unchecked") Collection<Object> collection = (Collection<Object>) value;
            String[] convertedValue = new String[collection.size()];
            int i = 0;
            for(Object o : collection) {
                convertedValue[i] = converter.convertToWrite(o);
                if(StringUtils.isNotEmpty(this.writeFormat)
                        && StringUtils.isNotEmpty(convertedValue[i])) {
                    convertedValue[i] = String.format(this.writeFormat, convertedValue[i]);
                }
                i++;
            }
            retval = StringUtils.join(convertedValue, writeDelimiter); // String.join() make null into "null"
        }
        return retval;
    }
    
    /**
     * Checks that {@code value} is not null and not an empty
     * {@link java.util.Collection}.
     */
    // The rest of the Javadoc is inherited
    @SuppressWarnings("unchecked")
    @Override
    protected boolean isFieldEmptyForWrite(Object value) {
        return super.isFieldEmptyForWrite(value) || ((Collection<Object>)value).isEmpty();
    }
}