/**
* Copyright (C) 2008 rweber <quietgenie@users.sourceforge.net>
*
* This file is part of CsvObjectMapper.
*
* CsvObjectMapper is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CsvObjectMapper is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CsvObjectMapper. If not, see <http://www.gnu.org/licenses/>.
*/
/**
*
*/
package com.projectnine.csvmapper;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
import net.sf.csv4j.CSVReader;
import net.sf.csv4j.ParseException;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.jexl.Expression;
import org.apache.commons.jexl.ExpressionFactory;
import org.apache.commons.jexl.JexlContext;
import org.apache.commons.jexl.JexlHelper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
/**
* This class is the only class in which users should be directly interested.
*
* First of all, if you don't want to go insane, use a spring application
* context to configure your {@link CsvMappingDefinition}s and add them the
* {@link #csvMappingDefinitions} {@link Map}.
*
* Check out the test cases for examples (src/test).
*
* Not thread safe. May never be. We'll see.
*
* @author robweber
*
*/
public class CsvToObjectMapper {
private static final Log log = LogFactory.getLog(CsvToObjectMapper.class);
/**
* JEXL expressions for property names in the {@link CsvFieldMapping} should
* include this token somewhere. The token is a placeholder for the adjusted
* RAW CSV Field String value.
*
* So if you want your JEXL expression to do something like this:
*
* getFooMap().put('bar', adjustedCsvValue)
*
* you really want to type this:
*
* getFooMap().put('bar', %ARGUMENT%)
*/
public static final String ARGUMENT_TOKEN = "%ARGUMENT%";
/**
* You don't really care about this.
*
* Fine.
*
* This is the name in the JEXL context of the Object into which the RAW CSV
* Field String has been transformed. Happy?
*/
private static final String ARGUMENT_VALUE = "finalPropertyValue";
/**
* All of the mapping definitions that are available to you.
*/
protected static Map<String, CsvMappingDefinition> csvMappingDefinitions;
/**
* The name of the mapping definition in which THIS
* {@link CsvToObjectMapper} is interested.
*/
private String mappingDefinition;
/**
* This guy does a little of this and a little of that.
*
* Mainly, though, I just use this to read a CSV file. Huh?
*/
private CSVReader csvReader;
/**
* A list containing all of the CSV field values in the line we are
* currently processing.
*/
private List<String> line;
/**
* Default constructor. Mainly, it just does nothing.
*
* Do not forget to {@link #init(Resource, boolean, String)}!!!
*/
public CsvToObjectMapper() {
}
/**
* This constructor is pretty useful. It initializes the object without you
* having to do it explicitly.
*
* @param csvResource
* The {@link Resource} containing the CSV file.
* @param containsHeader
* Does this CSV file contain a header? I need to know so that I
* can skip the header if it's there. There might be problems
* otherwise...
* @param mappingDefinition
* The name of the mapping definition that we are using from the
* {@link #csvMappingDefinitions}.
*/
public CsvToObjectMapper(Resource csvResource, boolean containsHeader,
String mappingDefinition) {
init(csvResource, containsHeader, mappingDefinition);
}
/**
* Initializes the object by setting the {@link #mappingDefinition} and
* instantiating the {@link #csvReader}.
*
* @see #CsvToObjectMapper(Resource, boolean, String)
*/
public void init(Resource csvResource, boolean containsHeader,
String mappingDefinition) {
this.mappingDefinition = mappingDefinition;
try {
csvReader = new CSVReader(new BufferedReader(new InputStreamReader(
csvResource.getInputStream())));
if (containsHeader) {
csvReader.readLine();
}
} catch (Exception e) {
log.error(
"Unable to load the specified CSV resource: "
+ (csvResource != null ? csvResource.getFilename()
: "NULL") + ".", e);
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws Exception {
try {
InputStream in = new FileInputStream("temp");
Resource resource = new InputStreamResource(in);
CSVReader csvReader = new CSVReader(new BufferedReader(
new InputStreamReader(resource.getInputStream())));
List<String> list = csvReader.readLine();
while (list.size() > 0) {
System.out.println(list);
list = csvReader.readLine();
}
} catch (Throwable t) {
log.warn("Error", t);
throw new Exception(t);
}
}
// @SuppressWarnings("unchecked")
// public static String generateCsvLineFromObject(Object objectToConvert,
// String csvMappingName) throws Exception {
// CsvMappingDefinition mappingDefinition = csvMappingDefinitions
// .get(csvMappingName);
// if (mappingDefinition == null) {
// return null;
// }
//
// if (!Class.forName(mappingDefinition.beanClassName).isAssignableFrom(
// objectToConvert.getClass())) {
// return null;
// }
//
// Map<Integer, String> fieldMap = MapUtils
// .orderedMap(convertObjectToFieldMap(objectToConvert,
// mappingDefinition));
//
// int numberOfFields = mappingDefinition.getExpectedNumberOfFields();
// // If the number of fields is not explicitly stated, we guess based on
// // the column number of the last mapped value.
// if (numberOfFields <= 0) {
// numberOfFields = fieldMap.keySet().toArray(
// new Integer[fieldMap.size()])[fieldMap.size() - 1];
// }
//
// StringBuffer lineBuffer = new StringBuffer();
// for (int i = 0; i < numberOfFields; i++) {
// String mapValue = fieldMap.get(new Integer(i));
// lineBuffer.append((mapValue != null ? mapValue : ""));
// lineBuffer.append((i < numberOfFields - 1 ? "," : "\n"));
// }
//
// return lineBuffer.toString();
// }
// private static Map<Integer, String> convertObjectToFieldMap(
// Object objectToConvert, CsvMappingDefinition mappingDefinition)
// throws Exception {
// Map<Integer, String> map = new HashMap<Integer, String>();
//
// List<CsvFieldMapping> list = mappingDefinition.getFieldMappings();
// for (int i = 0; i < list.size(); i++) {
// CsvFieldMapping csvFieldMapping = list.get(i);
// if (csvFieldMapping.getBeanName() == null) {
// map.put(new Integer(csvFieldMapping.getColumnIndex()),
// BeanUtils.getProperty(objectToConvert, csvFieldMapping
// .getPropertyName()));
// } else if (csvFieldMapping.isComplexProperty()) {
// map.putAll(convertObjectToFieldMap(CsvPropertyUtil
// .getComplexProperty(objectToConvert, csvFieldMapping
// .getPropertyName()), csvMappingDefinitions
// .get(csvFieldMapping.getBeanName())));
// } else {
// map.putAll(convertObjectToFieldMap(CsvPropertyUtil
// .getSimpleProperty(objectToConvert, csvFieldMapping
// .getPropertyName()), csvMappingDefinitions
// .get(csvFieldMapping.getBeanName())));
// }
// }
//
// return map;
// }
/**
* Generate the next Object from CSV.
*
* @return An Object; null if there are no more records; throws an Exception
* if there is a problem loading the next record. Note that a thrown
* Exception does necessarily indicate a show stopper.
*/
public Object generateNextObjectFromCsv() throws Exception {
Object o = null;
try {
if (loadNextRecord()) {
o = generateObjectFromCurrentCsvRecord();
}
} catch (Exception e) {
log
.warn(
"An error occurred while loading the CSV line. Maybe the next line is good?",
e);
throw e;
}
return o;
}
/**
* Assuming that you have called {@link #loadNextRecord()} at least once,
* this method will... do... something... what?
*/
private Object generateObjectFromCurrentCsvRecord() {
CsvMappingDefinition csvMappingDefinition = getCsvMappingDefinition(mappingDefinition);
Object generatedObject = null;
if (csvMappingDefinition == null) {
log.warn("The specified mapping is undefined. Returning null.");
} else {
try {
if (csvMappingDefinition.getExpectedNumberOfFields() == -1
|| csvMappingDefinition.getExpectedNumberOfFields() == line
.size()) {
log.debug("The line to parse is " + line);
generatedObject = populateBean(csvMappingDefinition
.getNewBeanInstance(), csvMappingDefinition, line);
} else if (line.size() != 0) {
throw new ValidationException("The line (#"
+ getCurrentLineNumber() + ") read contains "
+ (line != null ? line.size() : -1) + " items. "
+ csvMappingDefinition.getExpectedNumberOfFields()
+ " fields are expected:\n\t" + line);
}
} catch (Exception e) {
log
.error(
"An error occurred while converting the CSV to Object.",
e);
throw new RuntimeException(e);
}
}
log.debug("The generated object, "
+ generatedObject
+ ", is of class, "
+ (generatedObject != null ? generatedObject.getClass()
.getName() : "null"));
return generatedObject;
}
/**
* Load the next record from the CSV file.
*
* @return true if a record is loaded successfully; false if there are no
* more records
* @throws RuntimeException
* when an error occurs during record load.
*/
private boolean loadNextRecord() {
boolean nextRecordLoaded = false;
try {
line = csvReader.readLine();
if (line.size() > 0) {
nextRecordLoaded = true;
}
} catch (ParseException e) {
log.fatal("An error occurred while parsing the CSV file.", e);
throw new RuntimeException(e);
} catch (IOException e) {
log.fatal("An error occurred while accessing the CSV file.", e);
throw new RuntimeException(e);
}
return nextRecordLoaded;
}
/**
* Returns a {@link CsvMappingDefinition} that corresponds to the given
* mapping definition string.
*
* @param mappingDefinition
* @return
*/
private CsvMappingDefinition getCsvMappingDefinition(
String mappingDefinition) {
return csvMappingDefinitions.get(mappingDefinition);
}
/**
* Fills the bean with the good stuff.
*
* @param generatedObject
* @param csvMappingDefinition
* @param line
* @return
* @throws Exception
*/
@SuppressWarnings("unchecked")
private Object populateBean(Object generatedObject,
CsvMappingDefinition csvMappingDefinition, List<String> line)
throws Exception {
if (line.size() > 0) {
List<CsvFieldMapping> csvFieldMappingList = csvMappingDefinition
.getFieldMappings();
for (int i = 0; i < csvFieldMappingList.size(); i++) {
CsvFieldMapping csvFieldMapping = csvFieldMappingList.get(i);
String propertyName = csvFieldMapping
.getCsvToObjectExpression();
Object finalPropertyValue = null;
if (csvFieldMapping.getBeanName() == null) {
finalPropertyValue = csvFieldMapping
.getObjectValueFromCsvField(line
.get(csvFieldMapping.getColumnIndex()),
generatedObject, line);
} else {
log.debug("Attempting to retrieve a mapping called "
+ csvFieldMapping.getBeanName());
CsvMappingDefinition newMapping = getCsvMappingDefinition(csvFieldMapping
.getBeanName());
Object newBeanInstance = newMapping.getNewBeanInstance();
finalPropertyValue = populateBean(newBeanInstance,
newMapping, line);
}
log.debug("After bean population, the final property value is "
+ finalPropertyValue
+ " | "
+ (finalPropertyValue != null ? finalPropertyValue
.getClass().getName() : "null"));
if (propertyName.contains(ARGUMENT_TOKEN)) {
JexlContext jexlContext = JexlHelper.createContext();
jexlContext.getVars().put("generatedObject",
generatedObject);
jexlContext.getVars().put(ARGUMENT_VALUE,
finalPropertyValue);
String expressionString = propertyName.replace(
ARGUMENT_TOKEN, ARGUMENT_VALUE);
Expression expression = ExpressionFactory
.createExpression("generatedObject."
+ expressionString);
expression.evaluate(jexlContext);
} else {
// TODO Throw an Exception here since not including the
// ARGUMENT_TOKEN in the expression is a violation of the
// updated specification.
log.warn("Using legacy property setting method.");
doOldPropertySetting(generatedObject, csvFieldMapping,
propertyName, finalPropertyValue);
}
}
}
return generatedObject;
}
/**
* This method encapsulates the property setting logic that was incorporated
* into the {@link CsvToObjectMapper} pre JEXL. It is included here for
* compatibility only, and it will be removed in a future release.
*
* @param generatedObject
* @param csvFieldMapping
* @param propertyName
* @param finalPropertyValue
* @throws IllegalAccessException
* @throws InvocationTargetException
* @throws NoSuchMethodException
* @throws Exception
* @deprecated This will be removed in a future release.
*/
private void doOldPropertySetting(Object generatedObject,
CsvFieldMapping csvFieldMapping, String propertyName,
Object finalPropertyValue) throws IllegalAccessException,
InvocationTargetException, NoSuchMethodException, Exception {
// if (!csvFieldMapping.isComplexProperty()) {
// Note that if the property does not exist on the specified
// object, nothing bad happens when we try to set it using
// BeanUtils :-O
// So we try to get the property first!
BeanUtils.getProperty(generatedObject, propertyName);
// Then, when no Exception is thrown, we set the property!
BeanUtils
.setProperty(generatedObject, propertyName, finalPropertyValue);
log.debug("Set a property called " + propertyName + " to a value of "
+ finalPropertyValue + " to an object of class "
+ generatedObject.getClass());
// } else {
// CsvPropertyUtil.setProperty(generatedObject, propertyName,
// finalPropertyValue);
// }
}
/**
* @param csvMappingDefinitions
* the csvMappingDefinitions to set
*/
public void setCsvMappingDefinitions(
Map<String, CsvMappingDefinition> csvMappingDefinitions) {
CsvToObjectMapper.csvMappingDefinitions = csvMappingDefinitions;
}
/**
* On what line number is the csvReader?
*
* @return
*/
public synchronized long getCurrentLineNumber() {
return csvReader.getLineNumber();
}
/**
* Move the cursor of the csvReader to the specified line number.
*
* @param parserPosition
*/
public synchronized void seek(long parserPosition) {
try {
if (parserPosition > csvReader.getLineNumber()) {
long linesToSkip = parserPosition - csvReader.getLineNumber();
for (int i = 0; i < linesToSkip; i++) {
List<String> list = csvReader.readLine();
if (list.size() == 0) {
// Reached the end of the file.
break;
}
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}