1 /*
2 * Copyright 2016 Andrew Rucker Jones.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.opencsv.bean;
17
18 import com.opencsv.CSVWriter;
19 import com.opencsv.ICSVParser;
20 import com.opencsv.ICSVWriter;
21 import com.opencsv.bean.concurrent.BeanExecutor;
22 import com.opencsv.bean.concurrent.ProcessCsvBean;
23 import com.opencsv.bean.exceptionhandler.CsvExceptionHandler;
24 import com.opencsv.bean.exceptionhandler.ExceptionHandlerThrow;
25 import com.opencsv.bean.util.OpencsvUtils;
26 import com.opencsv.bean.util.OrderedObject;
27 import com.opencsv.exceptions.CsvDataTypeMismatchException;
28 import com.opencsv.exceptions.CsvException;
29 import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
30 import com.opencsv.exceptions.CsvRuntimeException;
31 import org.apache.commons.collections4.CollectionUtils;
32 import org.apache.commons.collections4.MultiValuedMap;
33 import org.apache.commons.collections4.iterators.PeekingIterator;
34 import org.apache.commons.lang3.ObjectUtils;
35 import org.apache.commons.lang3.StringUtils;
36
37 import java.io.Writer;
38 import java.lang.reflect.Field;
39 import java.util.*;
40 import java.util.concurrent.ArrayBlockingQueue;
41 import java.util.concurrent.BlockingQueue;
42 import java.util.concurrent.LinkedBlockingQueue;
43 import java.util.concurrent.RejectedExecutionException;
44 import java.util.stream.Stream;
45 import java.util.stream.StreamSupport;
46
47 /**
48 * This class writes beans out in CSV format to a {@link java.io.Writer},
49 * keeping state information and making an intelligent guess at the mapping
50 * strategy to be applied.
51 * <p>This class implements multi-threading on writing more than one bean, so
52 * there should be no need to use it across threads in an application. As such,
53 * it is not thread-safe.</p>
54 *
55 * @param <T> Type of the bean to be written
56 * @author Andrew Rucker Jones
57 * @see OpencsvUtils#determineMappingStrategy(java.lang.Class, java.util.Locale, java.lang.String)
58 * @since 3.9
59 */
60 public class StatefulBeanToCsv<T> {
61 private static final char NO_CHARACTER = '\0';
62 /**
63 * The beans being written are counted in the order they are written.
64 */
65 private int lineNumber = 0;
66
67 private final char separator;
68 private final char quotechar;
69 private final char escapechar;
70 private final String lineEnd;
71 private boolean headerWritten = false;
72 private MappingStrategy<T> mappingStrategy;
73 private final Writer writer;
74 private ICSVWriter csvwriter;
75 private final CsvExceptionHandler exceptionHandler;
76 private List<CsvException> capturedExceptions = new ArrayList<>();
77 private boolean orderedResults = true;
78 private BeanExecutor<T> executor = null;
79 private Locale errorLocale = Locale.getDefault();
80 private final boolean applyQuotesToAll;
81 private final MultiValuedMap<Class<?>, Field> ignoredFields;
82 private final String profile;
83
84 /**
85 * Constructor used when supplying a Writer instead of a CsvWriter class.
86 * It is defined as package protected to ensure that {@link StatefulBeanToCsvBuilder} is always used.
87 *
88 * @param escapechar The escape character to use when writing a CSV file
89 * @param lineEnd The line ending to use when writing a CSV file
90 * @param mappingStrategy The mapping strategy to use when writing a CSV file
91 * @param quotechar The quote character to use when writing a CSV file
92 * @param separator The field separator to use when writing a CSV file
93 * @param exceptionHandler Determines the exception handling behavior
94 * @param writer A {@link java.io.Writer} for writing the beans as a CSV to
95 * @param applyQuotesToAll Whether all output fields should be quoted
96 * @param ignoredFields The fields to ignore during processing. May be {@code null}.
97 * @param profile The profile to use when configuring how to interpret bean fields
98 */
99 StatefulBeanToCsv(char escapechar, String lineEnd,
100 MappingStrategy<T> mappingStrategy, char quotechar, char separator,
101 CsvExceptionHandler exceptionHandler, Writer writer, boolean applyQuotesToAll,
102 MultiValuedMap<Class<?>, Field> ignoredFields, String profile) {
103 this.escapechar = escapechar;
104 this.lineEnd = lineEnd;
105 this.mappingStrategy = mappingStrategy;
106 this.quotechar = quotechar;
107 this.separator = separator;
108 this.exceptionHandler = exceptionHandler;
109 this.writer = writer;
110 this.applyQuotesToAll = applyQuotesToAll;
111 this.ignoredFields = ignoredFields;
112 this.profile = StringUtils.defaultString(profile);
113 }
114
115 /**
116 * Constructor used to allow building of a {@link com.opencsv.bean.StatefulBeanToCsv}
117 * with a user-supplied {@link com.opencsv.ICSVWriter} class.
118 *
119 * @param mappingStrategy The mapping strategy to use when writing a CSV file
120 * @param exceptionHandler Determines the exception handling behavior
121 * @param applyQuotesToAll Whether all output fields should be quoted
122 * @param csvWriter An user-supplied {@link com.opencsv.ICSVWriter} for writing beans to a CSV output
123 * @param ignoredFields The fields to ignore during processing. May be {@code null}.
124 * @param profile The profile to use when configuring how to interpret bean fields
125 */
126 public StatefulBeanToCsv(MappingStrategy<T> mappingStrategy,
127 CsvExceptionHandler exceptionHandler, boolean applyQuotesToAll,
128 ICSVWriter csvWriter,
129 MultiValuedMap<Class<?>, Field> ignoredFields, String profile) {
130 this.mappingStrategy = mappingStrategy;
131 this.exceptionHandler = exceptionHandler;
132 this.applyQuotesToAll = applyQuotesToAll;
133 this.csvwriter = csvWriter;
134
135 this.escapechar = NO_CHARACTER;
136 this.lineEnd = "";
137 this.quotechar = NO_CHARACTER;
138 this.separator = NO_CHARACTER;
139 this.writer = null;
140 this.ignoredFields = ignoredFields;
141 this.profile = StringUtils.defaultString(profile);
142 }
143
144 /**
145 * Custodial tasks that must be performed before beans are written to a CSV
146 * destination for the first time.
147 *
148 * @param bean Any bean to be written. Used to determine the mapping
149 * strategy automatically. The bean itself is not written to the output by
150 * this method.
151 * @throws CsvRequiredFieldEmptyException If a required header is missing
152 * while attempting to write. Since every other header is hard-wired
153 * through the bean fields and their associated annotations, this can only
154 * happen with multi-valued fields.
155 */
156 private void beforeFirstWrite(T bean) throws CsvRequiredFieldEmptyException {
157
158 // Determine mapping strategy
159 if (mappingStrategy == null) {
160 mappingStrategy = OpencsvUtils.determineMappingStrategy(
161 (Class<T>) bean.getClass(), errorLocale, profile);
162 }
163
164 // Ignore fields. It's possible the mapping strategy has already been
165 // primed, so only pass on our data if the user actually gave us
166 // something.
167 if(!ignoredFields.isEmpty()) {
168 mappingStrategy.ignoreFields(ignoredFields);
169 }
170
171 // Build CSVWriter
172 if (csvwriter == null) {
173 csvwriter = new CSVWriter(writer, separator, quotechar, escapechar, lineEnd);
174 }
175
176 // Write the header
177 String[] header = mappingStrategy.generateHeader(bean);
178 if (header.length > 0) {
179 csvwriter.writeNext(header, applyQuotesToAll);
180 }
181 headerWritten = true;
182 }
183
184
185 /**
186 * Writes a bean out to the {@link java.io.Writer} provided to the
187 * constructor.
188 *
189 * @param bean A bean to be written to a CSV destination
190 * @throws CsvDataTypeMismatchException If a field of the bean is
191 * annotated improperly or an unsupported data type is supposed to be
192 * written
193 * @throws CsvRequiredFieldEmptyException If a field is marked as required,
194 * but the source is null
195 */
196 public void write(T bean) throws CsvDataTypeMismatchException,
197 CsvRequiredFieldEmptyException {
198
199 // Write header
200 if (bean != null) {
201 if (!headerWritten) {
202 beforeFirstWrite(bean);
203 }
204
205 // Process the bean
206 BlockingQueue<OrderedObject<String[]>> resultantLineQueue = new ArrayBlockingQueue<>(1);
207 BlockingQueue<OrderedObject<CsvException>> thrownExceptionsQueue = new LinkedBlockingQueue<>();
208 ProcessCsvBean<T> proc = new ProcessCsvBean<>(++lineNumber,
209 mappingStrategy, bean, resultantLineQueue,
210 thrownExceptionsQueue, new TreeSet<>(), exceptionHandler);
211 try {
212 proc.run();
213 } catch (RuntimeException re) {
214 if (re.getCause() != null) {
215 if (re.getCause() instanceof CsvRuntimeException) {
216 // Can't currently happen, but who knows what might be
217 // in the future? I'm certain we wouldn't want to wrap
218 // these in another RuntimeException.
219 throw (CsvRuntimeException) re.getCause();
220 }
221 if (re.getCause() instanceof CsvDataTypeMismatchException) {
222 throw (CsvDataTypeMismatchException) re.getCause();
223 }
224 if (re.getCause() instanceof CsvRequiredFieldEmptyException) {
225 throw (CsvRequiredFieldEmptyException) re.getCause();
226 }
227 }
228 throw re;
229 }
230
231 // Write out the result
232 if (!thrownExceptionsQueue.isEmpty()) {
233 OrderedObject<CsvException> o = thrownExceptionsQueue.poll();
234 while (o != null && o.getElement() != null) {
235 capturedExceptions.add(o.getElement());
236 o = thrownExceptionsQueue.poll();
237 }
238 } else {
239 // No exception, so there really must always be a string
240 OrderedObject<String[]> result = resultantLineQueue.poll();
241 if (result != null && result.getElement() != null) {
242 csvwriter.writeNext(result.getElement(), applyQuotesToAll);
243 }
244 }
245 }
246 }
247
248 private void submitAllLines(Iterator<T> beans) throws InterruptedException {
249 while (beans.hasNext()) {
250 T bean = beans.next();
251 if (bean != null) {
252 executor.submitBean(++lineNumber, mappingStrategy, bean, exceptionHandler);
253 }
254 }
255 executor.complete();
256 }
257
258 /**
259 * Writes a list of beans out to the {@link java.io.Writer} provided to the
260 * constructor.
261 *
262 * @param beans A list of beans to be written to a CSV destination
263 * @throws CsvDataTypeMismatchException If a field of the beans is
264 * annotated improperly or an unsupported data type is supposed to be
265 * written
266 * @throws CsvRequiredFieldEmptyException If a field is marked as required,
267 * but the source is null
268 */
269 public void write(List<T> beans) throws CsvDataTypeMismatchException,
270 CsvRequiredFieldEmptyException {
271 if (CollectionUtils.isNotEmpty(beans)) {
272 write(beans.iterator());
273 }
274 }
275
276 /**
277 * Writes an iterator of beans out to the {@link java.io.Writer} provided to the
278 * constructor.
279 *
280 * @param iBeans An iterator of beans to be written to a CSV destination
281 * @throws CsvDataTypeMismatchException If a field of the beans is annotated improperly or an unsupported
282 * data type is supposed to be written
283 * @throws CsvRequiredFieldEmptyException If a field is marked as required, but the source is null
284 */
285 public void write(Iterator<T> iBeans) throws CsvDataTypeMismatchException, CsvRequiredFieldEmptyException {
286
287 PeekingIterator<T> beans = new PeekingIterator<>(iBeans);
288 T firstBean = beans.peek();
289
290 if (!beans.hasNext()) {
291 return;
292 }
293
294 // Write header
295 if (!headerWritten) {
296 beforeFirstWrite(firstBean);
297 }
298
299 executor = new BeanExecutor<>(orderedResults, errorLocale);
300 executor.prepare();
301
302 // Process the beans
303 try {
304 submitAllLines(beans);
305 } catch (RejectedExecutionException e) {
306 // An exception in one of the bean writing threads prompted the
307 // executor service to shutdown before we were done.
308 if (executor.getTerminalException() instanceof RuntimeException) {
309 throw (RuntimeException) executor.getTerminalException();
310 }
311 if (executor.getTerminalException() instanceof CsvDataTypeMismatchException) {
312 throw (CsvDataTypeMismatchException) executor.getTerminalException();
313 }
314 if (executor.getTerminalException() instanceof CsvRequiredFieldEmptyException) {
315 throw (CsvRequiredFieldEmptyException) executor
316 .getTerminalException();
317 }
318 throw new RuntimeException(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
319 .getString("error.writing.beans"), executor.getTerminalException());
320 } catch (Exception e) {
321 // Exception during parsing. Always unrecoverable.
322 // I can't find a way to create this condition in the current
323 // code, but we must have a catch-all clause.
324 executor.shutdownNow();
325 if (executor.getTerminalException() instanceof RuntimeException) {
326 throw (RuntimeException) executor.getTerminalException();
327 }
328 throw new RuntimeException(ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
329 .getString("error.writing.beans"), e);
330 }
331 finally {
332 capturedExceptions.addAll(executor.getCapturedExceptions());
333 }
334
335 StreamSupport.stream(executor, false)
336 .forEach(l -> csvwriter.writeNext(l, applyQuotesToAll));
337 }
338
339 /**
340 * Writes a stream of beans out to the {@link java.io.Writer} provided to the
341 * constructor.
342 *
343 * @param beans A stream of beans to be written to a CSV destination
344 * @throws CsvDataTypeMismatchException If a field of the beans is annotated improperly or an unsupported
345 * data type is supposed to be written
346 * @throws CsvRequiredFieldEmptyException If a field is marked as required, but the source is null
347 */
348 public void write(Stream<T> beans) throws CsvDataTypeMismatchException, CsvRequiredFieldEmptyException {
349 write(beans.iterator());
350 }
351
352 /**
353 * Sets whether or not results must be written in the same order in which
354 * they appear in the list of beans provided as input.
355 * The default is that order is preserved. If your data do not need to be
356 * ordered, you can get a slight performance boost by setting
357 * {@code orderedResults} to {@code false}. The lack of ordering then also
358 * applies to any captured exceptions, if you have chosen not to have
359 * exceptions thrown.
360 *
361 * @param orderedResults Whether or not the lines written are in the same
362 * order they appeared in the input
363 * @since 4.0
364 */
365 public void setOrderedResults(boolean orderedResults) {
366 this.orderedResults = orderedResults;
367 }
368
369 /**
370 * @return Whether or not exceptions are thrown. If they are not thrown,
371 * they are captured and returned later via {@link #getCapturedExceptions()}.
372 * @deprecated There is simply no need for this method.
373 */
374 @Deprecated
375 public boolean isThrowExceptions() {
376 return exceptionHandler instanceof ExceptionHandlerThrow;
377 }
378
379 /**
380 * Any exceptions captured during writing of beans to a CSV destination can
381 * be retrieved through this method.
382 * <p><em>Reads from the list are destructive!</em> Calling this method will
383 * clear the list of captured exceptions. However, calling
384 * {@link #write(java.util.List)} or {@link #write(java.lang.Object)}
385 * multiple times with no intervening call to this method will not clear the
386 * list of captured exceptions, but rather add to it if further exceptions
387 * are thrown.</p>
388 *
389 * @return A list of exceptions that would have been thrown during any and
390 * all read operations since the last call to this method
391 */
392 public List<CsvException> getCapturedExceptions() {
393 List<CsvException> intermediate = capturedExceptions;
394 capturedExceptions = new ArrayList<>();
395 intermediate.sort(Comparator.comparingLong(CsvException::getLineNumber));
396 return intermediate;
397 }
398
399 /**
400 * Sets the locale for all error messages.
401 *
402 * @param errorLocale Locale for error messages. If null, the default locale
403 * is used.
404 * @since 4.0
405 */
406 public void setErrorLocale(Locale errorLocale) {
407 this.errorLocale = ObjectUtils.defaultIfNull(errorLocale, Locale.getDefault());
408 }
409 }