com.opencsv.bean.StatefulBeanToCsv Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of opencsv Show documentation
Show all versions of opencsv Show documentation
A simple library for reading and writing CSV in Java
/*
* Copyright 2016 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.CSVWriter;
import com.opencsv.ICSVParser;
import com.opencsv.ICSVWriter;
import com.opencsv.bean.concurrent.BeanExecutor;
import com.opencsv.bean.concurrent.ProcessCsvBean;
import com.opencsv.bean.exceptionhandler.CsvExceptionHandler;
import com.opencsv.bean.exceptionhandler.ExceptionHandlerThrow;
import com.opencsv.bean.util.OpencsvUtils;
import com.opencsv.bean.util.OrderedObject;
import com.opencsv.exceptions.CsvDataTypeMismatchException;
import com.opencsv.exceptions.CsvException;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
import com.opencsv.exceptions.CsvRuntimeException;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.iterators.PeekingIterator;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
* This class writes beans out in CSV format to a {@link java.io.Writer},
* keeping state information and making an intelligent guess at the mapping
* strategy to be applied.
* This class implements multi-threading on writing more than one bean, so
* there should be no need to use it across threads in an application. As such,
* it is not thread-safe.
*
* @param Type of the bean to be written
* @author Andrew Rucker Jones
* @see OpencsvUtils#determineMappingStrategy(java.lang.Class, java.util.Locale, java.lang.String)
* @since 3.9
*/
public class StatefulBeanToCsv {
private static final char NO_CHARACTER = '\0';
/**
* The beans being written are counted in the order they are written.
*/
private int lineNumber = 0;
private final char separator;
private final char quotechar;
private final char escapechar;
private final String lineEnd;
private boolean headerWritten = false;
private MappingStrategy mappingStrategy;
private final Writer writer;
private ICSVWriter csvwriter;
private final CsvExceptionHandler exceptionHandler;
private List capturedExceptions = new ArrayList<>();
private boolean orderedResults = true;
private BeanExecutor executor = null;
private Locale errorLocale = Locale.getDefault();
private final boolean applyQuotesToAll;
private final MultiValuedMap, Field> ignoredFields;
private final String profile;
/**
* Constructor used when supplying a Writer instead of a CsvWriter class.
* It is defined as package protected to ensure that {@link StatefulBeanToCsvBuilder} is always used.
*
* @param escapechar The escape character to use when writing a CSV file
* @param lineEnd The line ending to use when writing a CSV file
* @param mappingStrategy The mapping strategy to use when writing a CSV file
* @param quotechar The quote character to use when writing a CSV file
* @param separator The field separator to use when writing a CSV file
* @param exceptionHandler Determines the exception handling behavior
* @param writer A {@link java.io.Writer} for writing the beans as a CSV to
* @param applyQuotesToAll Whether all output fields should be quoted
* @param ignoredFields The fields to ignore during processing. May be {@code null}.
* @param profile The profile to use when configuring how to interpret bean fields
*/
StatefulBeanToCsv(char escapechar, String lineEnd,
MappingStrategy mappingStrategy, char quotechar, char separator,
CsvExceptionHandler exceptionHandler, Writer writer, boolean applyQuotesToAll,
MultiValuedMap, Field> ignoredFields, String profile) {
this.escapechar = escapechar;
this.lineEnd = lineEnd;
this.mappingStrategy = mappingStrategy;
this.quotechar = quotechar;
this.separator = separator;
this.exceptionHandler = exceptionHandler;
this.writer = writer;
this.applyQuotesToAll = applyQuotesToAll;
this.ignoredFields = ignoredFields;
this.profile = StringUtils.defaultString(profile);
}
/**
* Constructor used to allow building of a {@link com.opencsv.bean.StatefulBeanToCsv}
* with a user-supplied {@link com.opencsv.ICSVWriter} class.
*
* @param mappingStrategy The mapping strategy to use when writing a CSV file
* @param exceptionHandler Determines the exception handling behavior
* @param applyQuotesToAll Whether all output fields should be quoted
* @param csvWriter An user-supplied {@link com.opencsv.ICSVWriter} for writing beans to a CSV output
* @param ignoredFields The fields to ignore during processing. May be {@code null}.
* @param profile The profile to use when configuring how to interpret bean fields
*/
public StatefulBeanToCsv(MappingStrategy mappingStrategy,
CsvExceptionHandler exceptionHandler, boolean applyQuotesToAll,
ICSVWriter csvWriter,
MultiValuedMap, Field> ignoredFields, String profile) {
this.mappingStrategy = mappingStrategy;
this.exceptionHandler = exceptionHandler;
this.applyQuotesToAll = applyQuotesToAll;
this.csvwriter = csvWriter;
this.escapechar = NO_CHARACTER;
this.lineEnd = "";
this.quotechar = NO_CHARACTER;
this.separator = NO_CHARACTER;
this.writer = null;
this.ignoredFields = ignoredFields;
this.profile = StringUtils.defaultString(profile);
}
/**
* Custodial tasks that must be performed before beans are written to a CSV
* destination for the first time.
*
* @param bean Any bean to be written. Used to determine the mapping
* strategy automatically. The bean itself is not written to the output by
* this method.
* @throws CsvRequiredFieldEmptyException If a required header is missing
* while attempting to write. Since every other header is hard-wired
* through the bean fields and their associated annotations, this can only
* happen with multi-valued fields.
*/
private void beforeFirstWrite(T bean) throws CsvRequiredFieldEmptyException {
// Determine mapping strategy
if (mappingStrategy == null) {
mappingStrategy = OpencsvUtils.determineMappingStrategy(
(Class) bean.getClass(), errorLocale, profile);
}
// Ignore fields. It's possible the mapping strategy has already been
// primed, so only pass on our data if the user actually gave us
// something.
if(!ignoredFields.isEmpty()) {
mappingStrategy.ignoreFields(ignoredFields);
}
// Build CSVWriter
if (csvwriter == null) {
csvwriter = new CSVWriter(writer, separator, quotechar, escapechar, lineEnd);
}
// Write the header
String[] header = mappingStrategy.generateHeader(bean);
if (header.length > 0) {
csvwriter.writeNext(header, applyQuotesToAll);
}
headerWritten = true;
}
/**
* Writes a bean out to the {@link java.io.Writer} provided to the
* constructor.
*
* @param bean A bean to be written to a CSV destination
* @throws CsvDataTypeMismatchException If a field of the bean is
* annotated improperly or an unsupported data type is supposed to be
* written
* @throws CsvRequiredFieldEmptyException If a field is marked as required,
* but the source is null
*/
public void write(T bean) throws CsvDataTypeMismatchException,
CsvRequiredFieldEmptyException {
// Write header
if (bean != null) {
if (!headerWritten) {
beforeFirstWrite(bean);
}
// Process the bean
BlockingQueue> resultantLineQueue = new ArrayBlockingQueue<>(1);
BlockingQueue> thrownExceptionsQueue = new LinkedBlockingQueue<>();
ProcessCsvBean proc = new ProcessCsvBean<>(++lineNumber,
mappingStrategy, bean, resultantLineQueue,
thrownExceptionsQueue, new TreeSet<>(), exceptionHandler);
try {
proc.run();
} catch (RuntimeException re) {
if (re.getCause() != null) {
if (re.getCause() instanceof CsvRuntimeException) {
// Can't currently happen, but who knows what might be
// in the future? I'm certain we wouldn't want to wrap
// these in another RuntimeException.
throw (CsvRuntimeException) re.getCause();
}
if (re.getCause() instanceof CsvDataTypeMismatchException) {
throw (CsvDataTypeMismatchException) re.getCause();
}
if (re.getCause() instanceof CsvRequiredFieldEmptyException) {
throw (CsvRequiredFieldEmptyException) re.getCause();
}
}
throw re;
}
// Write out the result
if (!thrownExceptionsQueue.isEmpty()) {
OrderedObject o = thrownExceptionsQueue.poll();
while (o != null && o.getElement() != null) {
capturedExceptions.add(o.getElement());
o = thrownExceptionsQueue.poll();
}
} else {
// No exception, so there really must always be a string
OrderedObject result = resultantLineQueue.poll();
if (result != null && result.getElement() != null) {
csvwriter.writeNext(result.getElement(), applyQuotesToAll);
}
}
}
}
private void submitAllLines(Iterator beans) throws InterruptedException {
while (beans.hasNext()) {
T bean = beans.next();
if (bean != null) {
executor.submitBean(++lineNumber, mappingStrategy, bean, exceptionHandler);
}
}
executor.complete();
}
/**
* Writes a list of beans out to the {@link java.io.Writer} provided to the
* constructor.
*
* @param beans A list of beans to be written to a CSV destination
* @throws CsvDataTypeMismatchException If a field of the beans is
* annotated improperly or an unsupported data type is supposed to be
* written
* @throws CsvRequiredFieldEmptyException If a field is marked as required,
* but the source is null
*/
public void write(List beans) throws CsvDataTypeMismatchException,
CsvRequiredFieldEmptyException {
if (CollectionUtils.isNotEmpty(beans)) {
write(beans.iterator());
}
}
/**
* Writes an iterator of beans out to the {@link java.io.Writer} provided to the
* constructor.
*
* @param iBeans An iterator of beans to be written to a CSV destination
* @throws CsvDataTypeMismatchException If a field of the beans is annotated improperly or an unsupported
* data type is supposed to be written
* @throws CsvRequiredFieldEmptyException If a field is marked as required, but the source is null
*/
public void write(Iterator iBeans) throws CsvDataTypeMismatchException, CsvRequiredFieldEmptyException {
PeekingIterator beans = new PeekingIterator<>(iBeans);
T firstBean = beans.peek();
if (!beans.hasNext()) {
return;
}
// Write header
if (!headerWritten) {
beforeFirstWrite(firstBean);
}
executor = new BeanExecutor<>(orderedResults, errorLocale);
executor.prepare();
// Process the beans
try {
submitAllLines(beans);
} catch (RejectedExecutionException e) {
// An exception in one of the bean writing threads prompted the
// executor service to shutdown before we were done.
if (executor.getTerminalException() instanceof RuntimeException) {
throw (RuntimeException) executor.getTerminalException();
}
if (executor.getTerminalException() instanceof CsvDataTypeMismatchException) {
throw (CsvDataTypeMismatchException) executor.getTerminalException();
}
if (executor.getTerminalException() instanceof CsvRequiredFieldEmptyException) {
throw (CsvRequiredFieldEmptyException) executor
.getTerminalException();
}
throw new RuntimeException(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
.getString("error.writing.beans"), executor.getTerminalException());
} catch (Exception e) {
// Exception during parsing. Always unrecoverable.
// I can't find a way to create this condition in the current
// code, but we must have a catch-all clause.
executor.shutdownNow();
if (executor.getTerminalException() instanceof RuntimeException) {
throw (RuntimeException) executor.getTerminalException();
}
throw new RuntimeException(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
.getString("error.writing.beans"), e);
}
finally {
capturedExceptions.addAll(executor.getCapturedExceptions());
}
StreamSupport.stream(executor, false)
.forEach(l -> csvwriter.writeNext(l, applyQuotesToAll));
}
/**
* Writes a stream of beans out to the {@link java.io.Writer} provided to the
* constructor.
*
* @param beans A stream of beans to be written to a CSV destination
* @throws CsvDataTypeMismatchException If a field of the beans is annotated improperly or an unsupported
* data type is supposed to be written
* @throws CsvRequiredFieldEmptyException If a field is marked as required, but the source is null
*/
public void write(Stream beans) throws CsvDataTypeMismatchException, CsvRequiredFieldEmptyException {
write(beans.iterator());
}
/**
* Sets whether or not results must be written in the same order in which
* they appear in the list of beans provided as input.
* The default is that order is preserved. If your data do not need to be
* ordered, you can get a slight performance boost by setting
* {@code orderedResults} to {@code false}. The lack of ordering then also
* applies to any captured exceptions, if you have chosen not to have
* exceptions thrown.
*
* @param orderedResults Whether or not the lines written are in the same
* order they appeared in the input
* @since 4.0
*/
public void setOrderedResults(boolean orderedResults) {
this.orderedResults = orderedResults;
}
/**
* @return Whether or not exceptions are thrown. If they are not thrown,
* they are captured and returned later via {@link #getCapturedExceptions()}.
* @deprecated There is simply no need for this method.
*/
@Deprecated
public boolean isThrowExceptions() {
return exceptionHandler instanceof ExceptionHandlerThrow;
}
/**
* Any exceptions captured during writing of beans to a CSV destination can
* be retrieved through this method.
* Reads from the list are destructive! Calling this method will
* clear the list of captured exceptions. However, calling
* {@link #write(java.util.List)} or {@link #write(java.lang.Object)}
* multiple times with no intervening call to this method will not clear the
* list of captured exceptions, but rather add to it if further exceptions
* are thrown.
*
* @return A list of exceptions that would have been thrown during any and
* all read operations since the last call to this method
*/
public List getCapturedExceptions() {
List intermediate = capturedExceptions;
capturedExceptions = new ArrayList<>();
intermediate.sort(Comparator.comparingLong(CsvException::getLineNumber));
return intermediate;
}
/**
* Sets the locale for all error messages.
*
* @param errorLocale Locale for error messages. If null, the default locale
* is used.
* @since 4.0
*/
public void setErrorLocale(Locale errorLocale) {
this.errorLocale = ObjectUtils.defaultIfNull(errorLocale, Locale.getDefault());
}
}