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 }