FieldMapByName.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.exceptions.CsvRequiredFieldEmptyException;
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.lang3.ArrayUtils;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
/**
* This class maintains a mapping from header names out of a CSV file to bean
* fields.
* Simple entries are matched using string equality. Complex entries are matched
* using regular expressions.
*
* @param <T> Type of the bean being converted
* @author Andrew Rucker Jones
* @since 4.2
*/
public class FieldMapByName<T> extends AbstractFieldMap<String, String, RegexToBeanField<T>, T> {
/** Holds a {@link java.util.Comparator} to sort columns on writing. */
private Comparator<String> writeOrder = null;
/**
* Initializes this {@link FieldMap}.
*
* @param errorLocale The locale to be used for error messages
*/
public FieldMapByName(final Locale errorLocale) {
super(errorLocale);
}
/**
* @param key A regular expression matching header names
*/
// The rest of the Javadoc is inherited
@Override
public void putComplex(final String key, final BeanField<T, String> value) {
complexMapList.add(new RegexToBeanField<>(key, value, errorLocale));
}
/**
* Returns a list of required headers that were not present in the input.
*
* @param headersPresent An array of all headers present from the input
* @return A list of name + field for all the required headers that were
* not found
*/
public List<FieldMapByNameEntry<T>> determineMissingRequiredHeaders(final String[] headersPresent) {
// Start with collections of all required headers
final List<String> requiredStringList = simpleMap.entrySet().stream()
.filter(e -> e.getValue().isRequired())
.map(Map.Entry::getKey)
.collect(Collectors.toCollection(LinkedList::new));
final List<ComplexFieldMapEntry<String, String, T>> requiredRegexList = complexMapList.stream()
.filter(r -> r.getBeanField().isRequired())
.collect(Collectors.toList());
// Now remove the ones we found
for(String h : headersPresent) {
if(!requiredStringList.remove(h.toUpperCase().trim())) {
final ListIterator<ComplexFieldMapEntry<String, String, T>> requiredRegexListIterator = requiredRegexList.listIterator();
boolean found = false;
while(!found && requiredRegexListIterator.hasNext()) {
final ComplexFieldMapEntry<String, String, T> r = requiredRegexListIterator.next();
if(r.contains(h)) {
found = true;
requiredRegexListIterator.remove();
}
}
}
}
// Repackage what remains
List<FieldMapByNameEntry<T>> missingRequiredHeaders = new LinkedList<>();
for(String s : requiredStringList) {
missingRequiredHeaders.add(new FieldMapByNameEntry<>(s, simpleMap.get(s), false));
}
for(ComplexFieldMapEntry<String, String, T> r : requiredRegexList) {
missingRequiredHeaders.add(new FieldMapByNameEntry<>(r.getInitializer(), r.getBeanField(), true));
}
return missingRequiredHeaders;
}
/**
* This method generates a header that can be used for writing beans of the
* type provided back to a file.
* <p>The ordering of the headers is determined by the
* {@link java.util.Comparator} passed in to
* {@link #setColumnOrderOnWrite(Comparator)}, should that method be called,
* otherwise the natural ordering is used (alphabetically ascending).</p>
* <p>This implementation will not write headers discovered in multi-valued
* bean fields if the headers would not be matched by the bean field on
* reading. There are two reasons for this:</p>
* <ol><li>opencsv always tries to create data that are round-trip
* equivalent, and that would not be the case if it generated data on
* writing that it would discard on reading.</li>
* <li>As the code is currently written, the header name is used on writing
* each bean field to determine the appropriate {@link BeanField} for
* information concerning conversions, locales, necessity (whether or not
* the field is required). Without this information, conversion is
* impossible, and every value written under the unmatched header is blank,
* regardless of the contents of the bean.</li></ol>
*/
// The rest of the Javadoc is inherited.
@Override
public String[] generateHeader(final T bean) throws CsvRequiredFieldEmptyException {
final List<Field> missingRequiredHeaders = new LinkedList<>();
final List<String> headerList = new ArrayList<>(simpleMap.keySet());
for(ComplexFieldMapEntry<String, String, T> r : complexMapList) {
@SuppressWarnings("unchecked")
final MultiValuedMap<String,T> m = (MultiValuedMap<String,T>) r.getBeanField().getFieldValue(bean);
if(m != null && !m.isEmpty()) {
headerList.addAll(m.entries().stream()
.map(Map.Entry::getKey)
.filter(r::contains)
.collect(Collectors.toList()));
}
else {
if(r.getBeanField().isRequired()) {
missingRequiredHeaders.add(r.getBeanField().getField());
}
}
}
// Report headers that should have been present
if(!missingRequiredHeaders.isEmpty()) {
String errorMessage = String.format(
ResourceBundle
.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
.getString("header.required.field.absent"),
missingRequiredHeaders.stream()
.map(Field::getName)
.collect(Collectors.joining(" ")),
String.join(" ", headerList));
throw new CsvRequiredFieldEmptyException(bean.getClass(), missingRequiredHeaders, errorMessage);
}
// Sort and return
headerList.sort(writeOrder);
return headerList.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
}
/**
* Sets the {@link java.util.Comparator} to be used to sort columns when
* writing beans to a CSV file.
*
* @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;
}
}