In this post, you will create a Spring project and use Castor XML mapping for marshalling/unmarshalling Java objects into an XML document.
If you would like to become an active contributor to this project please follow these simple steps:
- Fork it
- Create your feature branch
- Commit your changes
- Push to the branch
- Create new Pull Request
Source code can be downloaded from github.
- About 40 minutes
- A favorite IDE. In this post, we use:Eclipse IDE for Java DevelopersVersion: Mars.2 Release (4.5.2) Build id: 20160218-0600
- JDK 7 or later. It can be made to work with JDK6, but it will need configuration tweaks. Please check the Spring Boot documentation
- An empty Spring project. You can follow the steps from here.
Sometimes, you might come to need an easy way to read or write an XML file, or a Web service reply in the form of XML. When this happens, Castor comes very handy.
Castor XML is an XML data binding framework, which deals data in an XML documents as object models which represent that data. This convertion from data to object and viceversa is refered as marshalling/unmarshalling.
For those who are not familiar with the above teminology, "marshalling" means converting an object to a stream or sequence of bytes. While "unmarshalling" means converting a stream to an object.
The conversion between Java object and XML is done by the XML data binding framework, and consists of the following classes:
org.exolab.castor.xml.Marshaller
: Worker class for converting a Java object to XML document.org.exolab.castor.xml.Unmarshaller
: Worker class for converting XML document to Java object.org.exolab.castor.xml.XMLContext
: A bootstrap class used for configuration of the XML data binding framework and instantiation of the two worker objects.
There are three modes in which Castor XML can be used for marshalling/unmarshalling data to and from XML:
- introspection mode
- mapping mode
- descriptor mode (aka generation mode)
In this post, we will focus only in the mapping mode, in which a user-defined mapping file is provided to Castor XML. This user-defined mapping file is also an XML that allows the whole or partial definition of a customized mapping between Java classes (and their properties) and XML.
For our example, we will read from an XML file and that same information will be written to a new XML document. We will do it in three samples:
- Marshal/Unmarshal a very simple XML file composed of one root element and several child elements.
- Marshal/Unmarshal a complex XML file composed of one root element and several child elements, but one of the child elements is a list of elements.
- Marshal/Unmarshal a complex XML file, but this time using custom field handler.
Even though the project has three examples, we will focus on explaining the most complex example: marshalling/unmarshalling a complex XML file composed of one root element and several child elements, and one of the child elements is a list of elements. During the unmarshalling process, we will use two custom field handler.
src/main/java | +- com | +- canchitodev | +- example | +- MarshallingXmlUsingOxMappersApplication.java | +- MarshallingXmlUsingOxMappersApplication.java | +- MarshallingXmlUsingOxMappersApplication.java | | | +- domain | | +- basic | | +- Platform.java | | | | +- complex | | +- Game.java | | +- Games.java | | +- Platform.java | | | +- field | +- handlers | | +- DateHandler.java | | +- PriceHandler.java | | | | +- domain | +- Game.java | +- Games.java | +- Platform.java src/main/resources | +- basic | +- basic-example.xml | +- basic-mapping.xml | +- complex | +- complex-example.xml | +- complex-mapping.xml | +- field-convertion | +- field-convertion-example.xml | +- field-convertion-mapping.xml | src/test/java | +- com +- canchitodev +- example +- MarshallingXmlUsingOxMappersApplicationTests.java
So let's get started!
Once you have created an empty project and imported into your favorite IDE, it is time to modify the pom.xml
file. If you have not created the project yet, you can follow the steps described in here.
Let's open the pom.xml
file, and add the dependencies needed by Castor.
First we add Castor's and Xerces' version, that we will be using, as a property. Notice that we will use the latest current version at the moment of writing this post.
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <castor.version>1.4.1</castor.version> <xerces.version>2.11.0</xerces.version> </properties>
Next, we add three dependencies (Spring Oxm, Castor & Xerces). But do not remove the previously added ones.
<dependencies> <!-- Marshalling XML using O/X Mappers --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-oxm</artifactId> </dependency> <!-- Marshalling XML using O/X Mappers --> <!-- Castor - Data binding made easy --> <dependency> <groupId>org.codehaus.castor</groupId> <artifactId>castor-xml</artifactId> <version>${castor.version}</version> </dependency> <dependency> <groupId>org.codehaus.castor</groupId> <artifactId>castor-codegen</artifactId> <version>${castor.version}</version> </dependency> <!-- Castor need this --> <dependency> <groupId>xerces</groupId> <artifactId>xercesImpl</artifactId> <version>${xerces.version}</version> </dependency> <!-- Castor - Data binding made easy --> </dependencies>
That's it. You have successfully included Castor into your project.
Here is a representation of the XML file that we will be unmarshalling. You can find it at src/java/resources/field-convertion
.
<?xml version="1.0" encoding="UTF-8"?> <Platform> <Name>Playstation 4</Name> <Developer>Sony Interactive Entertainment</Developer> <Manufacturer>Sony, Foxconn</Manufacturer> <ReleaseDate>29/11/2013</ReleaseDate> <Price>€399.99</Price> <Website>http://playstation.com/ps4/</Website> <Games> <Game> <Name>Uncharted 4: A Thief's End</Name> <ReleaseDate>10/05/2016</ReleaseDate> <Price>€35.90</Price> <URL>https://www.unchartedthegame.com/en-us/</URL> <Developer>Naughty Dog</Developer> <Publisher>Sony Computer Entertainment</Publisher> </Game> <Game> <Name>Batman: Arkham Knight</Name> <ReleaseDate>23/06/2015</ReleaseDate> <Price>€17.99</Price> <URL>https://www.batmanarkhamknight.com/</URL> <Developer>Rocksteady Studios</Developer> <Publisher>Warner Bros. Interactive Entertainment</Publisher> </Game> <Game> <Name>Wolfenstein II: The New Colossus</Name> <ReleaseDate>27/10/2017</ReleaseDate> <Price>€69.99</Price> <URL/> <Developer>MachineGames</Developer> <Publisher>Bethesda Softworks</Publisher> </Game> </Games> </Platform>
As you might remember, we will focus this post on explaining how to marshal/unmarshal a complex XML file composed of one root element and several child elements, and one of the child elements is a list of elements. During the unmarshalling process, we will use two custom field handler. All of the Java object that will be used are located in package com.canchitodev.example.field.handlers.domain
.
public class Platform {private String name; private String developer; private String manufacturer; private Date releaseDate; private String price; private String website; private Games games;
public Platform() {}
public Platform(String name, String developer, String manufacturer, Date releaseDate, String price, String website, Games games) { this.name = name; this.developer = developer; this.manufacturer = manufacturer; this.releaseDate = releaseDate; this.price = price; this.website = website; this.games = games; }
// Getters and Setters // ... }
public class Games { private List<Game> game; public Games() { this.game = new ArrayList<Game>(); } public Games(List<Game> game) { this.game = game; } // Getters and Setters // ... }
public class Game { private String name; private Date releaseDate; private String price; private String url; private String developer; private String publisher; public Game() {} public Game(String name, Date releaseDate, String price, String url, String developer, String publisher) { this.name = name; this.releaseDate = releaseDate; this.price = price; this.url = url; this.developer = developer; this.publisher = publisher; } // Getters and Setters // ... }
If you pay attention, we have created an object for each XML's element that has child elements.
In order to use the XML mapping technique, we have to first define the mapping information. This is an XML document, which describes how the properties of the Java Object have to be translated into XML. However, the only contraint for the mapping file is that Castor must unambiguously infer from it, how a given XML element/attribute has to be translated into the object model during unmarshalling.
Put into simple words, the XML mapping file explains for each Java object how each of its fields have to be mapped into XML. For this, Castor considers each field as an abstraction of an object's property. And by it, it can be accessed via public class variable or using accessor methods (setters and getters). Whenever Castor has to handle an object or XML data which information is not in the mapping file, it will follow its default behavior, by using Java's Reflection API to introspect the Java object and determine what to do. Both methods can be simultaneously used.Note: Castor can’t handle all possible mappings. In some complex cases, it may be necessary to rely on an XSL transformation in conjunction with Castor to adapt the XML document to a more friendly format.
<?xml version="1.0"?> <!DOCTYPE mapping PUBLIC "-//EXOLAB/Castor Mapping DTD Version 1.0//EN" "http://castor.org/mapping.dtd"> <mapping> <description> marshalling-xml-using-ox-mappers: Demo project for marshalling XML using O/X Mappers with Castos </description> <class name="com.canchitodev.example.field.handlers.domain.Platform"> <map-to xml="Platform" /> <field name="name" type="string"> <bind-xml name="Name" node="element" /> </field> <field name="developer" type="string"> <bind-xml name="Developer" node="element" /> </field> <field name="manufacturer" type="string"> <bind-xml name="Manufacturer" node="element" /> </field> <field name="releaseDate" type="string" handler="com.canchitodev.example.field.handlers.DateHandler"> <bind-xml name="ReleaseDate" node="element" /> </field> <field name="price" type="string" handler="com.canchitodev.example.field.handlers.PriceHandler"> <bind-xml name="Price" node="element" /> </field> <field name="website" type="string"> <bind-xml name="Website" node="element" /> </field> <field name="Games" type="com.canchitodev.example.field.handlers.domain.Games"> <bind-xml name="Games" node="element" /> </field> </class> <class name="com.canchitodev.example.field.handlers.domain.Games"> <map-to xml="Games" /> <field name="Game" type="com.canchitodev.example.field.handlers.domain.Game" collection="arraylist"> <bind-xml name="Game" node="element" /> </field> </class> <class name="com.canchitodev.example.field.handlers.domain.Game"> <map-to xml="Game" /> <field name="name" type="string"> <bind-xml name="Name" node="element" /> </field> <field name="releaseDate" type="string" handler="com.canchitodev.example.field.handlers.DateHandler"> <bind-xml name="ReleaseDate" node="element" /> </field> <field name="price" type="string" handler="com.canchitodev.example.field.handlers.PriceHandler"> <bind-xml name="Price" node="element" /> </field> <field name="url" type="string"> <bind-xml name="URL" node="element" /> </field> <field name="developer" type="string"> <bind-xml name="Developer" node="element" /> </field> <field name="publisher" type="string"> <bind-xml name="Publisher" node="element" /> </field> </class> </mapping>
When using Castor's XML framework, each XML element has to map to a Java class. Everytime Castor marshals an object, it will:
- if present, use the mapping information to find the name of the element to create; or
- create a name using the name of the class, which is its default behavior
Afterwards, the fields' information from the mapping file is used to decide the way a particular object's property has to converted into into one only one of the following:
- an attribute
- an element
- text content
- nothing, as we can choose to ignore a particular field
If no inforamtion for a given class in found in the mapping XML file, by default, Castor will introspect the class and apply a set of predefined rules to guess the fields and marshal them. The predefined rules are as follows:
- All primitive types, including the primitive type wrappers (Boolean, Short, etc…) are marshalled as attributes.
- All other objects are marshalled as elements with either text content or element content.
During the unmarshalling process, if Castor finds an element, it will try to use the information found in the XML mapping file to determine which object to instantiate. When no mapping information is found, Castor will try to guess the name of the class to instantiate, by using the element's name. Afterwards, it will use the field information of the mapping file to handle the content of the element.
If no inforamtion for a given class in found in the mapping XML file, by default, Castor will introspect the class in order to find out if there any method of the form getXxxYyy()/setXxxYyy(<type> x). This accessor will be associated with XML element/attribute named ‘xxx-yyy’.
Before continuing, it is important to understand the Castor's marshalling/unmarshalling behavior. Castor's XML framework is very well documented. I recommend reading the following links: (1) XML Framework and (2) XML Mapping.
First we define the class for handling marshalling/unmarshalling of the XML. Notice that this class has both methods.public class XMLMarshalUtil {Second, we define the bean that will return us an instance of the class for manipulating the XML.private Marshaller marshaller; private Unmarshaller unmarshaller;
public XMLMarshalUtil() {} public void setMarshaller(Marshaller marshaller) { this.marshaller = marshaller; } public void setUnmarshaller(Unmarshaller unmarshaller) { this.unmarshaller = unmarshaller; } //Converts Object to XML file public void doMarshaling(String filename, Object graph) throws IOException { OutputStream fos = null; try { fos = new FileOutputStream(filename); marshaller.marshal(graph, new StreamResult(fos)); } finally { if(fos != null) fos.close(); } } //Converts Object to XML file public void doMarshaling(OutputStream fos, Object graph) throws IOException { try { marshaller.marshal(graph, new StreamResult(fos)); } finally { if(fos != null) fos.close(); } } //Converts XML to Java Object public Object doUnMarshaling(String filename) throws IOException { InputStream fis = null; try { fis = new FileInputStream(filename); return unmarshaller.unmarshal(new StreamSource(fis)); } finally { if(fis != null) fis.close(); } } //Converts XML to Java Object public Object doUnMarshaling(InputStream fis) throws IOException { try { return unmarshaller.unmarshal(new StreamSource(fis)); } finally { if(fis != null) fis.close(); } }
}
@Configuration public class XMLMarshalBean { @Bean(name = "xmlFieldConvertionHandler") public XMLMarshalUtil getXmlFieldConvertionHandler() throws IOException{ XMLMarshalUtil handler = new XMLMarshalUtil(); handler.setMarshaller(getXmlFieldConvertionMarshaller()); handler.setUnmarshaller(getXmlFieldConvertionMarshaller()); return handler; } @Bean(name = "xmlFieldConvertionMarshaller") public CastorMarshaller getXmlFieldConvertionMarshaller() throws IOException { CastorMarshaller castorMarshaller = new CastorMarshaller(); castorMarshaller.setCastorProperties(this.castorProperties()); castorMarshaller.setMappingLocation( new ClassPathResource("field-convertion/field-convertion-mapping.xml") ); return castorMarshaller; } private HashMap<String, String> castorProperties() { HashMap<String, String> properties = new HashMap<String, String>(); properties.put("org.exolab.castor.indent", "true"); properties.put("org.exolab.castor.debug", "true"); return properties; } }
There are occassions in which we need to deal with data format that are not nativably support by Castor, or manipulate fields to get the desired output without changing the object model. To deal with this, Castor has several interesting interfaces. For this post, we will focus on org.exolab.castor.mapping.GeneralizedFieldHandler
interface. We have implemented two field handlers: (1) for converting a date from English date format to Spanish date format, and (2) for converting the price form Euro (€) to US Dollars (US$). Both field handlers can be found in package com.canchitodev.example.field.handlers
.
/** * The GeneralizedFieldHandler for the Date class * A org.exolab.castor.mapping.GeneralizedFieldHandler is an extension of FieldHandler interface where we simply write the conversion * methods and Castor will automatically handle the underlying get/set operations. This allows us to re-use the same FieldHandler for * fields from different classes that require the same conversion. **/ public class DateHandler extends GeneralizedFieldHandler {And here is the class for doing the currency convertion:private static final String SOURCE_FORMAT = "dd/MM/yyyy";
/** * Creates a new DateHandler instance **/ public DateHandler() { super(); }
/** * This method is used to convert the value when the getValue method is called. The getValue method will * obtain the actual field value from given 'parent' object. * * This convert method is then invoked with the field's value. The value returned from this method will be * the actual value returned by getValue method. * * @param value the object value to convert after performing a get operation * @return the converted value. **/ @Override public Object convertUponGet(Object value) { SimpleDateFormat formatter = new SimpleDateFormat(SOURCE_FORMAT); if (value == null) return formatter.format(new Date()); Date date = (Date)value; return formatter.format(date); }
/** * This method is used to convert the value when the setValue method is called. The setValue method will * call this method to obtain the converted value. * * The converted value will then be used as the value to set for the field. * * @param value the object value to convert before performing a set operation * @return the converted value. **/ @Override public Object convertUponSet(Object value) { SimpleDateFormat formatter = new SimpleDateFormat(SOURCE_FORMAT); Date date = null; try { date = formatter.parse((String)value); } catch(ParseException px) { throw new IllegalArgumentException(px.getMessage()); } return date; }
/** * Returns the class type for the field that this * GeneralizedFieldHandler converts to and from. This should be the type that is used in the object model. * * @return the class type of of the field **/ @SuppressWarnings("rawtypes") @Override public Class getFieldType() { return Date.class; }
/** * Creates a new instance of the object described by this field. * * @param parent The object for which the field is created * @return A new instance of the field's value * @throws IllegalStateException This field is a simple type and cannot be instantiated **/ public Object newInstance(Object parent) throws IllegalStateException{ //-- Since it's marked as a string...just return null, //-- it's not needed. return null; } }
/** * The GeneralizedFieldHandler for the Date class * A org.exolab.castor.mapping.GeneralizedFieldHandler is an extension of FieldHandler interface where we simply write the conversion * methods and Castor will automatically handle the underlying get/set operations. This allows us to re-use the same FieldHandler for * fields from different classes that require the same conversion. **/ public class PriceHandler extends GeneralizedFieldHandler { private static final String EUROS_SIGN = "€"; private static final String DOLLARS_SIGN = "$"; /** * Creates a new DateHandler instance **/ public PriceHandler() { super(); } /** * This method is used to convert the value when the getValue method is called. The getValue method will * obtain the actual field value from given 'parent' object. * * This convert method is then invoked with the field's value. The value returned from this method will be * the actual value returned by getValue method. * * @param value the object value to convert after performing a get operation * @return the converted value. **/ @Override public Object convertUponGet(Object value) { if (value == null) return "$0.00"; String price = (String) value; price = price.replace(this.EUROS_SIGN, ""); Double priceInDollars = (double) Math.round(Double.parseDouble(price) * 1.2); return this.DOLLARS_SIGN + priceInDollars.toString(); } /** * This method is used to convert the value when the setValue method is called. The setValue method will * call this method to obtain the converted value. * * The converted value will then be used as the value to set for the field. * * @param value the object value to convert before performing a set operation * @return the converted value. **/ @Override public Object convertUponSet(Object value) { return value; } /** * Returns the class type for the field that this * GeneralizedFieldHandler converts to and from. This should be the type that is used in the object model. * * @return the class type of of the field **/ @SuppressWarnings("rawtypes") @Override public Class getFieldType() { return String.class; } /** * Creates a new instance of the object described by this field. * * @param parent The object for which the field is created * @return A new instance of the field's value * @throws IllegalStateException This field is a simple type and cannot be instantiated **/ public Object newInstance(Object parent) throws IllegalStateException{ //-- Since it's marked as a string...just return null, //-- it's not needed. return null; } }
Castor's XML framework is very well documented. I recommend reading the following link in order to understand much better how to write custom field handlers.
The only thing left to do it to test our code. You can find a jUnit class with three functions for testing our three examples. Just remember to comment the @Ignore
annotation.
In this post, you will learned:
- Get familier with Castor XML mapping for reading/writing XML documents.
- Convert an XML document to a Java object using Castor Unmarshaller.
- Write Java objects to an XML document using Castor Marshaller.
- Writing Castor XML custom field handlers.
Hope you enjoyed this post as much as I did writing it. Please leave your comments and feedback.