Introduction

This page demonstrates the #DEFINE functionality of the Expanded Context feature of the Object to CSV mapper. The #DEFINE functionality allows one to introduce arbitrary Java Objects to the Expanded Context.

Examples

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 pair of Java Objects that look like this:

 public class Person {
        private String firstName;
        private String lastName;
        private Map<String, Address> addresses;
        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 Map<String, Address> getAddresses() {
                return addresses;
        }
        
        public void setAddresses(Map<String, Address> addresses) {
                this.addresses = addresses;
        }
        
        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;
        }
 }
 
 public class Address {
        private String streetAddress;
        private String city;
        private String state;
        private Integer zipCode;
        
        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;
        }
 }

We also have a pre-existing mapping file which we want to expand with mapping information for converting Java Objects into CSV. 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" />
                                <entry key="addressMappingDefinition" value-ref="addressMappingDefinition" />
                        </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="objectToCsvExpression" value="person.getFirstName()" />
                                        <property name="columnIndex" value="0" />
                                        <property name="csvFieldHeader" value="First Name" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setLastName(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="person.getLastName()" />
                                        <property name="columnIndex" value="1" />
                                        <property name="csvFieldHeader" value="Last Name" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setAddress(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="goodAddress" />
                                        <property name="beanName" value="addressMappingDefinition" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setHomePhoneNumber(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="person.getHomePhoneNumber()" />
                                        <property name="columnIndex" value="6" />
                                        <property name="formatter" ref="phoneNumberFormatter" />
                                        <property name="validationCommand" ref="phoneNumberValidation" />
                                        <property name="csvFieldHeader" value="Home Phone Number" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setCellPhoneNumber(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="person.getCellPhoneNumber()" />
                                        <property name="columnIndex" value="7" />
                                        <property name="formatter" ref="phoneNumberFormatter" />
                                        <property name="validationCommand" ref="phoneNumberValidation" />
                                        <property name="csvFieldHeader" value="Cell Phone Number" />
                                </bean>
                        </list>
                </property>
                <property name="beanClassName" value="Person" />
                <property name="expectedNumberOfFields" value="8" />
                <property name="beanVariableName" value="person" />
                <property name="extendedContext">
                        <map>
                                <entry key="goodAddress" value="person.getAddresses().values().iterator().next();" />
                                <entry key="goodAddress" value="foreach (address in person.getAddresses()) { if (address.getState == 'MN') { address; } } goodAdress;" />
                        </map>
                </property>
        </bean>
        <bean id="addressMappingDefinition" class="com.projectnine.csvmapper.CsvMappingDefinition">
                <property name="fieldMappings">
                        <list>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setStreetAddress(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="address.getStreetAddress()" />
                                        <property name="columnIndex" value="2" />
                                        <property name="csvFieldHeader" value="Street Address" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setCity(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="address.getCity()" />
                                        <property name="columnIndex" value="3" />
                                        <property name="csvFieldHeader" value="City" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setState(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="address.getState()" />
                                        <property name="columnIndex" value="4" />
                                        <property name="csvFieldHeader" value="State" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setZipCode(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="address.getZipCode()" />
                                        <property name="columnIndex" value="5" />
                                        <property name="formatter" ref="integerFormatter" />
                                        <property name="validationCommand" ref="numericalValidator" />
                                        <property name="csvFieldHeader" value="Zip Code" />
                                </bean>
                        </list>
                </property>
                <property name="beanClassName" value="Address" />
                <property name="expectedNumberOfFields" value="8" />
                <property name="beanVariableName" value="address" />
        </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>
        
        <bean id="tenCharacterValidator" class="com.projectnine.csvmapper.RegularExpressionCsvFieldValidator">
                <property name="regularExpressions">
                        <list>
                                <value>.{10}</value>
                        </list>
                </property>
                <property name="required" value="true" />
        </bean>
        <bean id="numericalValidator" class="com.projectnine.csvmapper.RegularExpressionCsvFieldValidator">
                <property name="regularExpressions">
                        <list>
                                <value>\d+</value>
                        </list>
                </property>
        </bean>
        <bean id="phoneNumberValidation" class="org.apache.commons.chain.impl.ChainBase">
                <constructor-arg>
                        <list>
                                <ref bean="tenCharacterValidator" />
                                <ref bean="numericalValidator" />
                        </list>
                </constructor-arg>
        </bean>
 </beans>

Requirements

We are changing our address rule. Now, in odd numbered years, I want "home" address to be used, and in even numbered years, I want the "work" address to be used.

Configuration

We will change our objectToCsvExpressions for personMappingDefinition as follows:

 ...
        <bean id="personMappingDefinition" class="com.projectnine.csvmapper.CsvMappingDefinition">
 ...
                <property name="extendedContext">
                        <map>
                                <entry key="" value="'#DEFINE currentDate = java.util.Date'" />
                                <entry key="" value="'#DEFINE sdf = java.text.SimpleDateFormat |yyyy|'" />
                                <entry key="year" value="sdf.format(currentDate)" />
                                <entry key="goodAddress" value="if (year % 2 != 0) { person.getAddress().get('work'); } else { person.getAddress().get('home'); }" />
                        </map>
                </property>
        </bean>
 ...

Here's the deal. I found that I needed to instantiate arbitrary objects to act on the data that would be going into the CSV when translating from Object to CSV. Yes, a custom formatter could have handled that task, but what if the way the data would be handled ever changed? The answer is that I would have to recompile and redeploy. Ugh. So I devised a declarative mechanism for handling "formatting."

At first blush, the implementation seems kludgy. You would not be mistaken in making that assessment. However, the rules are very simple, and kludgy as it seems, it works consistently. Just blame it on environmental restrictions.

Here is the basic form:

 <entry key="" value="'#DEFINE sdf = java.text.SimpleDateFormat |MM/dd/yyyy|'" />

First note that the key is empty. The key can be any value, but resist the urge to give the key a value that represents a variable that has already been defined since the content of the variable will be overwritten by nonsense. For instance, if I were to type the following:

 <entry key="sdf" value="'#DEFINE sdf = java.text.SimpleDateFormat |MM/dd/yyyy|'" />

"sdf" would literally be equal to "#DEFINE sdf = java.text.SimpleDateFormat |MM/dd/yyyy|". That is NOT what I want. I want "sdf" to be a java.text.SimpleDateFormat. I'm getting ahead of myself, though.

Note that the whole expression is wrapped in single quotes. This is necessary otherwise JEXL would try to interpret the line, and since JEXL doesn't know what "#DEFINE" means, an Exception will be thrown.

Now, "#DEFINE" simply instructs the pre-processor to get ready to instantiate a new Object. "sdf" is the variable name of the new Object. "java.text.SimpleDateFormat" is the type of the new Object. "MM/dd/yyyy" is the argument of the constructor. Further, since "MM/dd/yyyy" is a string literal, it must be bounded in "|" characters (I have already used up all of my levels of nested quotes for XML).

You may use more than just Strings for arguments, though. Additionally, you may have more than one argument:

 <entry key="" value="'#DEFINE in = java.io.FileInputStream |test.csv|'" />
 <entry key="" value="'#DEFINE bufferedIn = java.io.BufferedInputStream in,1024'" />

Why would you ever need a BufferedInputStream here? I don't know. Just pretend that it's practical for some reason. Anyway, note that "in" is a new FileInputStream, opening the file "test.csv" in the current directory. Once "in" has been successfully instantiated, we move on to "bufferedIn". "bufferedIn" is a new BufferedInputStream taking the FileInputStream, "in", and the int, 1024, as arguments in the constructor. Notice that "in" and 1024 are separated by a comma. THERE IS NOT A SPACE AFTER THE COMMA. If you include a space, something bad will happen. So NO SPACE!!!

The balance of this is that any variable defined in a #DEFINE statement is accessible by any other expression that is subsequently executed.

Cool, huh?

Our final configuration 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" />
                                <entry key="addressMappingDefinition" value-ref="addressMappingDefinition" />
                        </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="objectToCsvExpression" value="person.getFirstName()" />
                                        <property name="columnIndex" value="0" />
                                        <property name="csvFieldHeader" value="First Name" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setLastName(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="person.getLastName()" />
                                        <property name="columnIndex" value="1" />
                                        <property name="csvFieldHeader" value="Last Name" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setAddress(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="goodAddress" />
                                        <property name="beanName" value="addressMappingDefinition" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setHomePhoneNumber(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="person.getHomePhoneNumber()" />
                                        <property name="columnIndex" value="6" />
                                        <property name="formatter" ref="phoneNumberFormatter" />
                                        <property name="validationCommand" ref="phoneNumberValidation" />
                                        <property name="csvFieldHeader" value="Home Phone Number" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setCellPhoneNumber(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="person.getCellPhoneNumber()" />
                                        <property name="columnIndex" value="7" />
                                        <property name="formatter" ref="phoneNumberFormatter" />
                                        <property name="validationCommand" ref="phoneNumberValidation" />
                                        <property name="csvFieldHeader" value="Cell Phone Number" />
                                </bean>
                        </list>
                </property>
                <property name="beanClassName" value="Person" />
                <property name="expectedNumberOfFields" value="8" />
                <property name="beanVariableName" value="person" />
                <property name="extendedContext">
                        <map>
                                <entry key="" value="'#DEFINE currentDate = java.util.Date'" />
                                <entry key="" value="'#DEFINE sdf = java.text.SimpleDateFormat |yyyy|'" />
                                <entry key="year" value="sdf.format(currentDate)" />
                                <entry key="goodAddress" value="if (year % 2 != 0) { person.getAddress().get('work'); } else { person.getAddress().get('home'); }" />
                        </map>
                </property>
        </bean>
        <bean id="addressMappingDefinition" class="com.projectnine.csvmapper.CsvMappingDefinition">
                <property name="fieldMappings">
                        <list>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setStreetAddress(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="address.getStreetAddress()" />
                                        <property name="columnIndex" value="2" />
                                        <property name="csvFieldHeader" value="Street Address" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setCity(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="address.getCity()" />
                                        <property name="columnIndex" value="3" />
                                        <property name="csvFieldHeader" value="City" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setState(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="address.getState()" />
                                        <property name="columnIndex" value="4" />
                                        <property name="csvFieldHeader" value="State" />
                                </bean>
                                <bean class="com.projectnine.csvmapper.CsvFieldMapping">
                                        <property name="csvToObjectExpression" value="setZipCode(%ARGUMENT%)" />
                                        <property name="objectToCsvExpression" value="address.getZipCode()" />
                                        <property name="columnIndex" value="5" />
                                        <property name="formatter" ref="integerFormatter" />
                                        <property name="validationCommand" ref="numericalValidator" />
                                        <property name="csvFieldHeader" value="Zip Code" />
                                </bean>
                        </list>
                </property>
                <property name="beanClassName" value="Address" />
                <property name="expectedNumberOfFields" value="8" />
                <property name="beanVariableName" value="address" />
        </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>
        
        <bean id="tenCharacterValidator" class="com.projectnine.csvmapper.RegularExpressionCsvFieldValidator">
                <property name="regularExpressions">
                        <list>
                                <value>.{10}</value>
                        </list>
                </property>
                <property name="required" value="true" />
        </bean>
        <bean id="numericalValidator" class="com.projectnine.csvmapper.RegularExpressionCsvFieldValidator">
                <property name="regularExpressions">
                        <list>
                                <value>\d+</value>
                        </list>
                </property>
        </bean>
        <bean id="phoneNumberValidation" class="org.apache.commons.chain.impl.ChainBase">
                <constructor-arg>
                        <list>
                                <ref bean="tenCharacterValidator" />
                                <ref bean="numericalValidator" />
                        </list>
                </constructor-arg>
        </bean>
 </beans>