View Javadoc
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 }