This page demonstrates how to use the CsvFieldFormatter classes to format a String in a CSV file and convert it to a different type of Object at parse time.
Note that any CSV file should be valid as long as it complies with RFC 4180. If you wish to process non-compliant CSV files, you will have to wait until the next release of the CSV Object Mapper.
For the purposes of this demo, let's say that we have a CSV file with the following content:
"First Name", "Last Name", "Street Address", "City", "State", "Zip Code", "Home Phone Number", "Cell Phone Number" "John", "Doe", "123 Test Dr.", "Test City", "HI", "11111", 1231231234, 5554443333 "Jane", "Doe", "321 Tset Dr.", "Tset City", "IA", "99999", 4324324321, 3334445555
Note that the Java Objects to which you map your CSV may be POJOs employing the standard [set/get]ter properties. However, your Objects may be much more "fancy" if you like.
For the purposes of this demo, we want to map to a Java Object that looks like this:
public class Person { private String firstName; private String lastName; private String streetAddress; private String city; private String state; private Integer zipCode; private String homePhoneNumber; private String cellPhoneNumber; public void setFirstName(String firstName) { this.firstName = firstName; } public String getFirstName() { return firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getLastName() { return lastName; } public void setStreetAddress(String streetAddress) { this.streetAddress = streetAddress; } public String getStreetAddress() { return streetAddress; } public void setCity(String city) { this.city = city; } public String getCity() { return city; } public void setState(String state) { this.state = state; } public String getState() { return state; } public void setZipCode(Integer zipCode) { this.zipCode = zipCode; } public Integer getZipCode() { return zipCode; } public void setHomePhoneNumber(String homePhoneNumber) { this.homePhoneNumber = homePhoneNumber; } public String getHomePhoneNumber() { return homePhoneNumber; } public void setCellPhoneNumber(String cellPhoneNumber) { this.cellPhoneNumber = cellPhoneNumber; } public String getCellPhoneNumber() { return cellPhoneNumber; } }
We also have a pre-existing mapping file to which we want to add formatter definitions. It looks like this:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"> <bean id="csvToObjectMapper" class="com.projectnine.csvmapper.CsvToObjectMapper"> <property name="csvMappingDefinitions"> <map> <entry key="personMappingDefinition" value-ref="personMappingDefinition" /> </map> </property> </bean> <bean id="personMappingDefinition" class="com.projectnine.csvmapper.CsvMappingDefinition"> <property name="fieldMappings"> <list> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setFirstName(%ARGUMENT%)" /> <property name="columnIndex" value="0" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setLastName(%ARGUMENT%)" /> <property name="columnIndex" value="1" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setStreetAddress(%ARGUMENT%)" /> <property name="columnIndex" value="2" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setCity(%ARGUMENT%)" /> <property name="columnIndex" value="3" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setState(%ARGUMENT%)" /> <property name="columnIndex" value="4" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setZipCode(%ARGUMENT%)" /> <property name="columnIndex" value="5" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setHomePhoneNumber(%ARGUMENT%)" /> <property name="columnIndex" value="6" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setCellPhoneNumber(%ARGUMENT%)" /> <property name="columnIndex" value="7" /> </bean> </list> </property> <property name="beanClassName" value="Person" /> <property name="expectedNumberOfFields" value="8" /> </bean> </beans>
First of all, notice that the zipCode is not a String. We would of course never do this in real life, but at least I'm not using Cats and Dogs.
Anyway, the zipCode comes to us as a String, so if we were to attempt to run the CsvToObjectMapper using our Person class, we would get ClassCastExceptions because a String is not an Integer.
Additionally, our phone numbers are currently stored as "9999999999", and we want them to be "999.999.9999" instead.
This is where the CsvFieldFormatter comes in.
The configuration of a formatter occurs in two parts: definition and usage. For instance, I would define my Integer formatter as such:
... <bean id="integerFormatter" class="com.projectnine.csvmapper.example.IntegerFormatter"> </bean> ...
And I would use it in my mapping for the zipCode like so:
... <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setZipCode(%ARGUMENT%)" /> <property name="columnIndex" value="5" /> <property name="formatter" ref="integerFormatter" /> </bean> ...
Formatters implement the com.projectnine.csvmapper.CsvFieldFormatter interface. That interface has two methods: formatObject and formatString. At this time, we are only interested in formatString, so formatObject will be a no-op:
package com.projectnine.csvmapper.examples; import com.projectnine.csvmapper.CsvFieldFormatter; public class IntegerFormatter implements CsvFieldFormatter { public String formatObject(Object object) { return null; } public Object formatString(String rawPropertyValue) { return new Integer(rawPropertyValue); } }
The same applies to our custom phone number formatter:
package com.projectnine.csvmapper.examples; import com.projectnine.csvmapper.CsvFieldFormatter; public class PhoneNumberFormatter implements CsvFieldFormatter { private static String separator; public void setSeparator(String separator) { PhoneNumberFormatter.separator = separator; } public String formatObject(Object object) { return null; } public Object formatString(String rawPropertyValue) { StringBuffer buffer = new StringBuffer(); buffer.append(rawPropertyValue.substring(0, 3)).append(separator); buffer.append(rawPropertyValue.substring(3, 6)).append(separator); buffer.append(rawPropertyValue.substring(6)); return buffer.toString(); } }
The type of the phone number won't change, but the value will! Also, I can use the ApplicationContext to cause any String I want to be the digit separator.
Here is the final configuration:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"> <bean id="csvToObjectMapper" class="com.projectnine.csvmapper.CsvToObjectMapper"> <property name="csvMappingDefinitions"> <map> <entry key="personMappingDefinition" value-ref="personMappingDefinition" /> </map> </property> </bean> <bean id="personMappingDefinition" class="com.projectnine.csvmapper.CsvMappingDefinition"> <property name="fieldMappings"> <list> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setFirstName(%ARGUMENT%)" /> <property name="columnIndex" value="0" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setLastName(%ARGUMENT%)" /> <property name="columnIndex" value="1" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setStreetAddress(%ARGUMENT%)" /> <property name="columnIndex" value="2" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setCity(%ARGUMENT%)" /> <property name="columnIndex" value="3" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setState(%ARGUMENT%)" /> <property name="columnIndex" value="4" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setZipCode(%ARGUMENT%)" /> <property name="columnIndex" value="5" /> <property name="formatter" ref="integerFormatter" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setHomePhoneNumber(%ARGUMENT%)" /> <property name="columnIndex" value="6" /> <property name="formatter" ref="phoneNumberFormatter" /> </bean> <bean class="com.projectnine.csvmapper.CsvFieldMapping"> <property name="csvToObjectExpression" value="setCellPhoneNumber(%ARGUMENT%)" /> <property name="columnIndex" value="7" /> <property name="formatter" ref="phoneNumberFormatter" /> </bean> </list> </property> <property name="beanClassName" value="Person" /> <property name="expectedNumberOfFields" value="8" /> </bean> <bean id="integerFormatter" class="com.projectnine.csvmapper.example.IntegerFormatter"> </bean> <bean id="phoneNumberFormatter" class="com.projectnine.csvmapper.examples.PhoneNumberFormatter"> <property name="separator" value="." /> </bean> </beans>