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.util;
17  
18  import com.opencsv.ICSVParser;
19  import com.opencsv.bean.*;
20  import com.opencsv.bean.exceptionhandler.CsvExceptionHandler;
21  import com.opencsv.exceptions.CsvBadConverterException;
22  import com.opencsv.exceptions.CsvChainedException;
23  import com.opencsv.exceptions.CsvException;
24  import org.apache.commons.lang3.StringUtils;
25  import org.apache.commons.lang3.reflect.FieldUtils;
26  
27  import java.util.*;
28  import java.util.concurrent.BlockingQueue;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  import java.util.regex.PatternSyntaxException;
32  import java.util.stream.Stream;
33  
34  /**
35   * This class is meant to be a collection of general purpose static methods
36   * useful in internal processing for opencsv.
37   *
38   * @author Andrew Rucker Jones
39   * @since 3.9
40   */
41  public final class OpencsvUtils {
42  
43      /** This class can't be instantiated. */
44      private OpencsvUtils() {}
45  
46      /**
47       * Determines which mapping strategy is appropriate for this bean.
48       * The algorithm is:<ol>
49       * <li>If annotations {@link CsvBindByPosition},
50       * {@link CsvCustomBindByPosition}, {@link CsvBindAndSplitByPosition} or
51       * {@link CsvBindAndJoinByPosition} are present,
52       * {@link ColumnPositionMappingStrategy} is chosen.</li>
53       * <li>Otherwise, {@link HeaderColumnNameMappingStrategy} is chosen. If
54       * annotations are present, they will be used, otherwise the field names
55       * will be used as the column names.</li></ol>
56       *
57       * @param <T> The type of the bean for which the mapping strategy is sought
58       * @param type The class of the bean for which the mapping strategy is sought
59       * @param errorLocale The locale to use for all error messages. If null, the
60       *   default locale is used.
61       * @param profile The profile to use when configuring bean fields
62       * @return A functional mapping strategy for the bean in question
63       */
64      public static <T> MappingStrategy<T> determineMappingStrategy(
65              Class<? extends T> type, Locale errorLocale, String profile) {
66          // Check for annotations
67          boolean positionAnnotationsPresent = Stream.of(FieldUtils.getAllFields(type)).anyMatch(
68                  f -> f.isAnnotationPresent(CsvBindByPosition.class)
69                  || f.isAnnotationPresent(CsvBindAndSplitByPosition.class)
70                  || f.isAnnotationPresent(CsvBindAndJoinByPosition.class)
71                  || f.isAnnotationPresent(CsvCustomBindByPosition.class));
72  
73          // Set the mapping strategy according to what we've found.
74          MappingStrategy<T> mappingStrategy = positionAnnotationsPresent ?
75                  new ColumnPositionMappingStrategy<>() :
76                  new HeaderColumnNameMappingStrategy<>();
77          mappingStrategy.setErrorLocale(errorLocale);
78          mappingStrategy.setProfile(profile);
79          mappingStrategy.setType(type);
80          return mappingStrategy;
81      }
82  
83      /**
84       * I find it annoying that when I want to queue something in a blocking
85       * queue, the thread might be interrupted and I have to try again; this
86       * method fixes that.
87       * @param <E> The type of the object to be queued
88       * @param queue The queue the object should be added to
89       * @param object The object to be queued
90       * @since 4.0
91       */
92      public static <E> void queueRefuseToAcceptDefeat(BlockingQueue<E> queue, E object) {
93          boolean interrupted = true;
94          while(interrupted) {
95              try {
96                  queue.put(object);
97                  interrupted = false;
98              }
99              catch(InterruptedException ie) {/* Do nothing. */}
100         }
101     }
102 
103     /**
104      * A function to consolidate code common to handling exceptions thrown
105      * during reading or writing of CSV files.
106      * The proper line number is set for the exception, the exception handler
107      * is run, and the exception is queued or thrown as necessary.
108      *
109      * @param e The exception originally thrown
110      * @param lineNumber The line or record number that caused the exception
111      * @param exceptionHandler The exception handler
112      * @param queue The queue for captured exceptions
113      * @since 5.2
114      */
115     public static synchronized void handleException(
116             CsvException e, long lineNumber,
117             CsvExceptionHandler exceptionHandler, BlockingQueue<OrderedObject<CsvException>> queue) {
118         e.setLineNumber(lineNumber);
119         CsvException capturedException = null;
120         List<CsvException> exceptionList = e instanceof CsvChainedException ?
121                 Collections.<CsvException>unmodifiableList(((CsvChainedException)e).getExceptionChain()) :
122                 Collections.singletonList(e);
123         for (CsvException iteratedException : exceptionList) {
124             try {
125                 capturedException = exceptionHandler.handleException(iteratedException);
126             } catch (CsvException csve) {
127                 capturedException = csve;
128                 throw new RuntimeException(csve);
129             } finally {
130                 if (capturedException != null) {
131                     queueRefuseToAcceptDefeat(queue,
132                             new OrderedObject<>(lineNumber, capturedException));
133                 }
134             }
135         }
136     }
137 
138     /**
139      * Compiles a regular expression into a {@link java.util.regex.Pattern},
140      * throwing an exception that is proper in the context of opencsv if the
141      * regular expression is not valid, or if it does not have at least one
142      * capturing group.
143      *
144      * @param regex The regular expression to be compiled. May be {@code null}
145      *              or an empty string, in which case {@code null} is returned.
146      *              Must have at least one capturing group if not {@code null}
147      *              or empty.
148      * @param regexFlags Flags for compiling the regular expression, as in
149      *                   {@link java.util.regex.Pattern#compile(String, int)}.
150      * @param callingClass The class from which this method is being called.
151      *                     Used for generating helpful exceptions.
152      * @param errorLocale  The locale to be used for error messages. If
153      *                     {@code null}, the default locale is used.
154      * @return A compiled pattern, or {@code null} if the input was null or
155      * empty
156      * @throws CsvBadConverterException If the regular expression is not empty
157      * but invalid or valid but does not have at least one capturing group
158      * @since 4.3
159      */
160     public static Pattern compilePatternAtLeastOneGroup(String regex, int regexFlags, Class<?> callingClass, Locale errorLocale)
161             throws CsvBadConverterException {
162         Pattern tempPattern = compilePattern(regex, regexFlags, callingClass, errorLocale);
163         Locale exceptionLocale = errorLocale == null ? Locale.getDefault() : errorLocale;
164 
165         // Verify that the pattern has at least one capture group. This does
166         // not appear to be possible without matching a string first.
167         if(tempPattern != null) {
168             Matcher m = tempPattern.matcher(StringUtils.EMPTY);
169             if(m.groupCount() < 1) {
170                 throw new CsvBadConverterException(callingClass,
171                         String.format(ResourceBundle.getBundle(
172                                 ICSVParser.DEFAULT_BUNDLE_NAME,
173                                 exceptionLocale).getString("regex.without.capture.group"), regex));
174             }
175         }
176 
177         return tempPattern;
178     }
179 
180     /**
181      * Compiles a regular expression into a {@link java.util.regex.Pattern},
182      * throwing an exception that is proper in the context of opencsv if the
183      * regular expression is not valid.
184      * This method may be used by custom converters if they are required to
185      * compile regular expressions that are unknown at compile time.
186      *
187      * @param regex The regular expression to be compiled. May be {@code null}
188      *              or an empty string, in which case {@code null} is returned.
189      * @param regexFlags Flags for compiling the regular expression, as in
190      *                   {@link java.util.regex.Pattern#compile(String, int)}.
191      * @param callingClass The class from which this method is being called.
192      *                     Used for generating helpful exceptions.
193      * @param errorLocale  The locale to be used for error messages. If
194      *                     {@code null}, the default locale is used.
195      * @return A compiled pattern, or {@code null} if the input was null or
196      * empty
197      * @throws CsvBadConverterException If the regular expression is not empty
198      * but invalid
199      * @since 4.3
200      */
201     public static Pattern compilePattern(String regex, int regexFlags, Class<?> callingClass, Locale errorLocale)
202             throws CsvBadConverterException {
203         Pattern tempPattern = null;
204         Locale exceptionLocale = errorLocale == null ? Locale.getDefault() : errorLocale;
205 
206         // Set up the regular expression for extraction of the value to be
207         // converted
208         if(StringUtils.isNotEmpty(regex)) {
209             try {
210                 tempPattern = Pattern.compile(regex, regexFlags);
211             }
212             catch(PatternSyntaxException e) {
213                 CsvBadConverterException csve = new CsvBadConverterException(
214                         callingClass,
215                         String.format(ResourceBundle.getBundle(
216                                 ICSVParser.DEFAULT_BUNDLE_NAME,
217                                 exceptionLocale).getString("invalid.regex"), regex));
218                 csve.initCause(e);
219                 throw csve;
220             }
221         }
222         return tempPattern;
223     }
224 
225     /**
226      * Verifies that the given format string works with one string parameter.
227      *
228      * @param format A format string for {@link java.lang.String#format(String, Object...)}
229      * @param callingClass The class from which this method is being called.
230      *                     Used for generating helpful exceptions.
231      * @param errorLocale  The locale to be used for error messages. If
232      *                     {@code null}, the default locale is used.
233      */
234     public static void verifyFormatString(String format, Class<?> callingClass, Locale errorLocale) {
235         Locale exceptionLocale = errorLocale == null ? Locale.getDefault() : errorLocale;
236         try {
237             if(StringUtils.isNotEmpty(format)) {
238                 String okayToIgnore = String.format(format, StringUtils.SPACE);
239             }
240         }
241         catch(IllegalFormatException e) {
242             CsvBadConverterException csve = new CsvBadConverterException(
243                     callingClass,
244                     String.format(ResourceBundle.getBundle(
245                             ICSVParser.DEFAULT_BUNDLE_NAME,
246                             exceptionLocale).getString("invalid.one.parameter.format.string"), format));
247             csve.initCause(e);
248             throw csve;
249         }
250     }
251 }