View Javadoc

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     // @SuppressWarnings("unchecked")
184     // public static String generateCsvLineFromObject(Object objectToConvert,
185     // String csvMappingName) throws Exception {
186     // CsvMappingDefinition mappingDefinition = csvMappingDefinitions
187     // .get(csvMappingName);
188     // if (mappingDefinition == null) {
189     // return null;
190     // }
191     //
192     // if (!Class.forName(mappingDefinition.beanClassName).isAssignableFrom(
193     // objectToConvert.getClass())) {
194     // return null;
195     // }
196     //
197     // Map<Integer, String> fieldMap = MapUtils
198     // .orderedMap(convertObjectToFieldMap(objectToConvert,
199     // mappingDefinition));
200     //
201     // int numberOfFields = mappingDefinition.getExpectedNumberOfFields();
202     // // If the number of fields is not explicitly stated, we guess based on
203     // // the column number of the last mapped value.
204     // if (numberOfFields <= 0) {
205     // numberOfFields = fieldMap.keySet().toArray(
206     // new Integer[fieldMap.size()])[fieldMap.size() - 1];
207     // }
208     //
209     // StringBuffer lineBuffer = new StringBuffer();
210     // for (int i = 0; i < numberOfFields; i++) {
211     // String mapValue = fieldMap.get(new Integer(i));
212     // lineBuffer.append((mapValue != null ? mapValue : ""));
213     // lineBuffer.append((i < numberOfFields - 1 ? "," : "\n"));
214     // }
215     //
216     // return lineBuffer.toString();
217     // }
218 
219     // private static Map<Integer, String> convertObjectToFieldMap(
220     // Object objectToConvert, CsvMappingDefinition mappingDefinition)
221     // throws Exception {
222     // Map<Integer, String> map = new HashMap<Integer, String>();
223     //
224     // List<CsvFieldMapping> list = mappingDefinition.getFieldMappings();
225     // for (int i = 0; i < list.size(); i++) {
226     // CsvFieldMapping csvFieldMapping = list.get(i);
227     // if (csvFieldMapping.getBeanName() == null) {
228     // map.put(new Integer(csvFieldMapping.getColumnIndex()),
229     // BeanUtils.getProperty(objectToConvert, csvFieldMapping
230     // .getPropertyName()));
231     // } else if (csvFieldMapping.isComplexProperty()) {
232     // map.putAll(convertObjectToFieldMap(CsvPropertyUtil
233     // .getComplexProperty(objectToConvert, csvFieldMapping
234     // .getPropertyName()), csvMappingDefinitions
235     // .get(csvFieldMapping.getBeanName())));
236     // } else {
237     // map.putAll(convertObjectToFieldMap(CsvPropertyUtil
238     // .getSimpleProperty(objectToConvert, csvFieldMapping
239     // .getPropertyName()), csvMappingDefinitions
240     // .get(csvFieldMapping.getBeanName())));
241     // }
242     // }
243     //
244     // return map;
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 		    // TODO Throw an Exception here since not including the
412 		    // ARGUMENT_TOKEN in the expression is a violation of the
413 		    // updated specification.
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 	// if (!csvFieldMapping.isComplexProperty()) {
443 	// Note that if the property does not exist on the specified
444 	// object, nothing bad happens when we try to set it using
445 	// BeanUtils :-O
446 
447 	// So we try to get the property first!
448 	BeanUtils.getProperty(generatedObject, propertyName);
449 
450 	// Then, when no Exception is thrown, we set the property!
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 	// } else {
457 	// CsvPropertyUtil.setProperty(generatedObject, propertyName,
458 	// finalPropertyValue);
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 			// Reached the end of the file.
493 			break;
494 		    }
495 		}
496 	    }
497 	} catch (Exception e) {
498 	    throw new RuntimeException(e);
499 	}
500     }
501 }