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 }