1 /***
2 * Copyright (C) 2008 rweber <quietgenie@users.sourceforge.net>
3 *
4 * This file is part of CsvObjectMapper.
5 *
6 * CsvObjectMapper is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * CsvObjectMapper is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with CsvObjectMapper. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 /***
21 *
22 */
23 package com.projectnine.csvmapper;
24
25 import java.io.BufferedReader;
26 import java.io.FileInputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.InputStreamReader;
30 import java.lang.reflect.InvocationTargetException;
31 import java.util.List;
32 import java.util.Map;
33
34 import net.sf.csv4j.CSVReader;
35 import net.sf.csv4j.ParseException;
36
37 import org.apache.commons.beanutils.BeanUtils;
38 import org.apache.commons.jexl.Expression;
39 import org.apache.commons.jexl.ExpressionFactory;
40 import org.apache.commons.jexl.JexlContext;
41 import org.apache.commons.jexl.JexlHelper;
42 import org.apache.commons.logging.Log;
43 import org.apache.commons.logging.LogFactory;
44 import org.springframework.core.io.InputStreamResource;
45 import org.springframework.core.io.Resource;
46
47 /***
48 * This class is the only class in which users should be directly interested.
49 *
50 * First of all, if you don't want to go insane, use a spring application
51 * context to configure your {@link CsvMappingDefinition}s and add them the
52 * {@link #csvMappingDefinitions} {@link Map}.
53 *
54 * Check out the test cases for examples (src/test).
55 *
56 * Not thread safe. May never be. We'll see.
57 *
58 * @author robweber
59 *
60 */
61 public class CsvToObjectMapper {
62 private static final Log log = LogFactory.getLog(CsvToObjectMapper.class);
63
64 /***
65 * JEXL expressions for property names in the {@link CsvFieldMapping} should
66 * include this token somewhere. The token is a placeholder for the adjusted
67 * RAW CSV Field String value.
68 *
69 * So if you want your JEXL expression to do something like this:
70 *
71 * getFooMap().put('bar', adjustedCsvValue)
72 *
73 * you really want to type this:
74 *
75 * getFooMap().put('bar', %ARGUMENT%)
76 */
77 public static final String ARGUMENT_TOKEN = "%ARGUMENT%";
78
79 /***
80 * You don't really care about this.
81 *
82 * Fine.
83 *
84 * This is the name in the JEXL context of the Object into which the RAW CSV
85 * Field String has been transformed. Happy?
86 */
87 private static final String ARGUMENT_VALUE = "finalPropertyValue";
88
89 /***
90 * All of the mapping definitions that are available to you.
91 */
92 protected static Map<String, CsvMappingDefinition> csvMappingDefinitions;
93
94 /***
95 * The name of the mapping definition in which THIS
96 * {@link CsvToObjectMapper} is interested.
97 */
98 private String mappingDefinition;
99
100 /***
101 * This guy does a little of this and a little of that.
102 *
103 * Mainly, though, I just use this to read a CSV file. Huh?
104 */
105 private CSVReader csvReader;
106
107 /***
108 * A list containing all of the CSV field values in the line we are
109 * currently processing.
110 */
111 private List<String> line;
112
113 /***
114 * Default constructor. Mainly, it just does nothing.
115 *
116 * Do not forget to {@link #init(Resource, boolean, String)}!!!
117 */
118 public CsvToObjectMapper() {
119 }
120
121 /***
122 * This constructor is pretty useful. It initializes the object without you
123 * having to do it explicitly.
124 *
125 * @param csvResource
126 * The {@link Resource} containing the CSV file.
127 * @param containsHeader
128 * Does this CSV file contain a header? I need to know so that I
129 * can skip the header if it's there. There might be problems
130 * otherwise...
131 * @param mappingDefinition
132 * The name of the mapping definition that we are using from the
133 * {@link #csvMappingDefinitions}.
134 */
135 public CsvToObjectMapper(Resource csvResource, boolean containsHeader,
136 String mappingDefinition) {
137 init(csvResource, containsHeader, mappingDefinition);
138 }
139
140 /***
141 * Initializes the object by setting the {@link #mappingDefinition} and
142 * instantiating the {@link #csvReader}.
143 *
144 * @see #CsvToObjectMapper(Resource, boolean, String)
145 */
146 public void init(Resource csvResource, boolean containsHeader,
147 String mappingDefinition) {
148 this.mappingDefinition = mappingDefinition;
149
150 try {
151 csvReader = new CSVReader(new BufferedReader(new InputStreamReader(
152 csvResource.getInputStream())));
153 if (containsHeader) {
154 csvReader.readLine();
155 }
156 } catch (Exception e) {
157 log.error(
158 "Unable to load the specified CSV resource: "
159 + (csvResource != null ? csvResource.getFilename()
160 : "NULL") + ".", e);
161 throw new RuntimeException(e);
162 }
163 }
164
165 public static void main(String[] args) throws Exception {
166 try {
167 InputStream in = new FileInputStream("temp");
168 Resource resource = new InputStreamResource(in);
169 CSVReader csvReader = new CSVReader(new BufferedReader(
170 new InputStreamReader(resource.getInputStream())));
171 List<String> list = csvReader.readLine();
172 while (list.size() > 0) {
173 System.out.println(list);
174 list = csvReader.readLine();
175 }
176
177 } catch (Throwable t) {
178 log.warn("Error", t);
179 throw new Exception(t);
180 }
181 }
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247 /***
248 * Generate the next Object from CSV.
249 *
250 * @return An Object; null if there are no more records; throws an Exception
251 * if there is a problem loading the next record. Note that a thrown
252 * Exception does necessarily indicate a show stopper.
253 */
254 public Object generateNextObjectFromCsv() throws Exception {
255 Object o = null;
256
257 try {
258 if (loadNextRecord()) {
259 o = generateObjectFromCurrentCsvRecord();
260 }
261 } catch (Exception e) {
262 log
263 .warn(
264 "An error occurred while loading the CSV line. Maybe the next line is good?",
265 e);
266 throw e;
267 }
268
269 return o;
270 }
271
272 /***
273 * Assuming that you have called {@link #loadNextRecord()} at least once,
274 * this method will... do... something... what?
275 */
276 private Object generateObjectFromCurrentCsvRecord() {
277 CsvMappingDefinition csvMappingDefinition = getCsvMappingDefinition(mappingDefinition);
278 Object generatedObject = null;
279 if (csvMappingDefinition == null) {
280 log.warn("The specified mapping is undefined. Returning null.");
281 } else {
282
283 try {
284 if (csvMappingDefinition.getExpectedNumberOfFields() == -1
285 || csvMappingDefinition.getExpectedNumberOfFields() == line
286 .size()) {
287 log.debug("The line to parse is " + line);
288
289 generatedObject = populateBean(csvMappingDefinition
290 .getNewBeanInstance(), csvMappingDefinition, line);
291
292 } else if (line.size() != 0) {
293 throw new ValidationException("The line (#"
294 + getCurrentLineNumber() + ") read contains "
295 + (line != null ? line.size() : -1) + " items. "
296 + csvMappingDefinition.getExpectedNumberOfFields()
297 + " fields are expected:\n\t" + line);
298 }
299 } catch (Exception e) {
300 log
301 .error(
302 "An error occurred while converting the CSV to Object.",
303 e);
304 throw new RuntimeException(e);
305 }
306 }
307
308 log.debug("The generated object, "
309 + generatedObject
310 + ", is of class, "
311 + (generatedObject != null ? generatedObject.getClass()
312 .getName() : "null"));
313 return generatedObject;
314 }
315
316 /***
317 * Load the next record from the CSV file.
318 *
319 * @return true if a record is loaded successfully; false if there are no
320 * more records
321 * @throws RuntimeException
322 * when an error occurs during record load.
323 */
324 private boolean loadNextRecord() {
325 boolean nextRecordLoaded = false;
326 try {
327 line = csvReader.readLine();
328 if (line.size() > 0) {
329 nextRecordLoaded = true;
330 }
331 } catch (ParseException e) {
332 log.fatal("An error occurred while parsing the CSV file.", e);
333 throw new RuntimeException(e);
334 } catch (IOException e) {
335 log.fatal("An error occurred while accessing the CSV file.", e);
336 throw new RuntimeException(e);
337 }
338
339 return nextRecordLoaded;
340 }
341
342 /***
343 * Returns a {@link CsvMappingDefinition} that corresponds to the given
344 * mapping definition string.
345 *
346 * @param mappingDefinition
347 * @return
348 */
349 private CsvMappingDefinition getCsvMappingDefinition(
350 String mappingDefinition) {
351 return csvMappingDefinitions.get(mappingDefinition);
352 }
353
354 /***
355 * Fills the bean with the good stuff.
356 *
357 * @param generatedObject
358 * @param csvMappingDefinition
359 * @param line
360 * @return
361 * @throws Exception
362 */
363 @SuppressWarnings("unchecked")
364 private Object populateBean(Object generatedObject,
365 CsvMappingDefinition csvMappingDefinition, List<String> line)
366 throws Exception {
367 if (line.size() > 0) {
368 List<CsvFieldMapping> csvFieldMappingList = csvMappingDefinition
369 .getFieldMappings();
370 for (int i = 0; i < csvFieldMappingList.size(); i++) {
371 CsvFieldMapping csvFieldMapping = csvFieldMappingList.get(i);
372 String propertyName = csvFieldMapping
373 .getCsvToObjectExpression();
374 Object finalPropertyValue = null;
375
376 if (csvFieldMapping.getBeanName() == null) {
377 finalPropertyValue = csvFieldMapping
378 .getObjectValueFromCsvField(line
379 .get(csvFieldMapping.getColumnIndex()),
380 generatedObject, line);
381 } else {
382 log.debug("Attempting to retrieve a mapping called "
383 + csvFieldMapping.getBeanName());
384
385 CsvMappingDefinition newMapping = getCsvMappingDefinition(csvFieldMapping
386 .getBeanName());
387 Object newBeanInstance = newMapping.getNewBeanInstance();
388 finalPropertyValue = populateBean(newBeanInstance,
389 newMapping, line);
390 }
391
392 log.debug("After bean population, the final property value is "
393 + finalPropertyValue
394 + " | "
395 + (finalPropertyValue != null ? finalPropertyValue
396 .getClass().getName() : "null"));
397
398 if (propertyName.contains(ARGUMENT_TOKEN)) {
399 JexlContext jexlContext = JexlHelper.createContext();
400 jexlContext.getVars().put("generatedObject",
401 generatedObject);
402 jexlContext.getVars().put(ARGUMENT_VALUE,
403 finalPropertyValue);
404 String expressionString = propertyName.replace(
405 ARGUMENT_TOKEN, ARGUMENT_VALUE);
406 Expression expression = ExpressionFactory
407 .createExpression("generatedObject."
408 + expressionString);
409 expression.evaluate(jexlContext);
410 } else {
411
412
413
414 log.warn("Using legacy property setting method.");
415 doOldPropertySetting(generatedObject, csvFieldMapping,
416 propertyName, finalPropertyValue);
417 }
418 }
419 }
420 return generatedObject;
421 }
422
423 /***
424 * This method encapsulates the property setting logic that was incorporated
425 * into the {@link CsvToObjectMapper} pre JEXL. It is included here for
426 * compatibility only, and it will be removed in a future release.
427 *
428 * @param generatedObject
429 * @param csvFieldMapping
430 * @param propertyName
431 * @param finalPropertyValue
432 * @throws IllegalAccessException
433 * @throws InvocationTargetException
434 * @throws NoSuchMethodException
435 * @throws Exception
436 * @deprecated This will be removed in a future release.
437 */
438 private void doOldPropertySetting(Object generatedObject,
439 CsvFieldMapping csvFieldMapping, String propertyName,
440 Object finalPropertyValue) throws IllegalAccessException,
441 InvocationTargetException, NoSuchMethodException, Exception {
442
443
444
445
446
447
448 BeanUtils.getProperty(generatedObject, propertyName);
449
450
451 BeanUtils
452 .setProperty(generatedObject, propertyName, finalPropertyValue);
453 log.debug("Set a property called " + propertyName + " to a value of "
454 + finalPropertyValue + " to an object of class "
455 + generatedObject.getClass());
456
457
458
459
460 }
461
462 /***
463 * @param csvMappingDefinitions
464 * the csvMappingDefinitions to set
465 */
466 public void setCsvMappingDefinitions(
467 Map<String, CsvMappingDefinition> csvMappingDefinitions) {
468 CsvToObjectMapper.csvMappingDefinitions = csvMappingDefinitions;
469 }
470
471 /***
472 * On what line number is the csvReader?
473 *
474 * @return
475 */
476 public synchronized long getCurrentLineNumber() {
477 return csvReader.getLineNumber();
478 }
479
480 /***
481 * Move the cursor of the csvReader to the specified line number.
482 *
483 * @param parserPosition
484 */
485 public synchronized void seek(long parserPosition) {
486 try {
487 if (parserPosition > csvReader.getLineNumber()) {
488 long linesToSkip = parserPosition - csvReader.getLineNumber();
489 for (int i = 0; i < linesToSkip; i++) {
490 List<String> list = csvReader.readLine();
491 if (list.size() == 0) {
492
493 break;
494 }
495 }
496 }
497 } catch (Exception e) {
498 throw new RuntimeException(e);
499 }
500 }
501 }