JAXB Customization of xsd:dateTime
Join the DZone community and get the full member experience.
Join For FreeA small JAXB puzzle: how to define a custom element to serialize Date objects with the TimeZone information? Piece of cake, isn't it? Try it yourself and you will be surprised with the tricky details.
A friend of mine gave me a JAXB challenge this week: his company already uses a customization of the xsd:date type in a legacy code - mapped to a proprietary type instead of the default Calendar type. Now they also need to represent Calendar objects in their application schema, so they need to model the date objects as a custom type. My first thought was about a five minutes hack, just defining an element based on the xsd:date and use the JAXB customization to map the new type to the Java Calendar type. After my five minutes I got few issues:
-
The default customization of Calendar in JAXB doesn't serialize the Time information of a date. Ok, let's create a custom binder class and hack the way we want to write and read our data.
-
If you use xsd:dateTime instead of a simple xsd:date, the default adapter of JAXB doesn't work anymore.
-
Other surprise: you can't use the java.text.SimpleDateFormat to serialize Date objects because the String representation of the TimeZone provided by Java is not compatible with the XML specification.
- new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") produces 2009-12-06T15:59:34+0100 - the expected format for the Schema xsd:dateTime type is 2009-12-06T15:59:34+01:00 You got the difference? Yes, the stupid missed colon in the time zone representation makes the output of the SimpleDateFormat incompatible with the XSD Schema specification. Yes, unbelievable but you need to handle that detail programatically.
You can try by yourself but instead of proving you the details I
wrote down my hack solution. If you know a more elegant
solution, please give me your feedback. Remember the original problem:
to not use the xsd:dateTime directly since it is already in
use by other customization. Also: your customization should support a
date and time representation, including the time zone.
Below you find a transcription of the sample project I created to illustrate the solution, to facilitate the copy paste and also to allow you to check the solution in case you don't want or you can't compile and run the project. Otherwise, just download the complete project. To compile and run the project, open a terminal and type the following line commands in the folder you unzipped the project:
mvn clean compile test eclipse:eclipse
The sample Maven project
-
First step, to create the maven project and configure the JAXB plugin in the pom.xml. To create the project I used the Maven default J2SE archetype:
mvn archetype:create -DgroupId=cejug.org -DartifactId=jaxb-example mvn compile eclipse:eclipse
-
Then you can import the project in your preferred IDE and configure the JAXB plugin in the pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cejug.org</groupId> <artifactId>jaxb-example</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>jaxb-example</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> <pluginRepositories> <pluginRepository> <id>maven2-repository.dev.java.net</id> <name>Java.net Maven 2 Repository</name> <url>http://download.java.net/maven/2 </url> </pluginRepository> </pluginRepositories> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> <plugin> <!-- <a href="https://jaxb.dev.java.net/jaxb-maven2-plugin/" title="https://jaxb.dev.java.net/jaxb-maven2-plugin/">https://jaxb.dev.java.net/jaxb-maven2-plugin/</a> --> <groupId>org.jvnet.jaxb2.maven2</groupId> <artifactId>maven-jaxb2-plugin</artifactId> <executions> <execution> <goals> <goal>generate</goal> </goals> </execution> </executions> <configuration> <schemaDirectory>${basedir}/src/main/resources/schema</schemaDirectory> <!-- generateDirectory>${basedir}/src/main/java</generateDirectory--> <includeSchemas> <includeSchema>**/*.xsd</includeSchema> </includeSchemas> <strict>true</strict> <verbose>false</verbose> <extension>true</extension> <readOnly>yes</readOnly> <removeOldOutput>true</removeOldOutput> </configuration> </plugin> </plugins> </build> </project>
-
After that, I created the sample schema /jaxb-example/src/main/resources/schema/sample-binding.xsd:
<?xml version="1.0" encoding="UTF-8"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.w3.org/2001/XMLSchema http://www.w3.org/2001/XMLSchema.xsd" targetNamespace="http://cejug.org/sample" xmlns:sample="http://cejug.org/sample" elementFormDefault="qualified" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb" xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc" jaxb:extensionBindingPrefixes="xjc" jaxb:version="2.1"> <xsd:annotation> <xsd:appinfo> <jaxb:globalBindings> <xjc:serializable uid="-6026937020915831338" /> <jaxb:javaType name="java.util.Date" xmlType="sample:sample.date" parseMethod="org.cejug.binder.XSDateTimeCustomBinder.parseDateTime" printMethod="org.cejug.binder.XSDateTimeCustomBinder.printDateTime" /> </jaxb:globalBindings> </xsd:appinfo> </xsd:annotation> <xsd:element name="element" type="sample:element.type" /> <xsd:complexType name="element.type"> <xsd:sequence minOccurs="1"> <xsd:element name="jdate" type="sample:sample.date" /> </xsd:sequence> </xsd:complexType> <xsd:simpleType name="sample.date"> <xsd:restriction base="xsd:dateTime" /> </xsd:simpleType> </xsd:schema>
-
Inspired by this blog I created the custom binder org.cejug.binder.XSDateTimeCustomBinder:
package org.cejug.binder; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class XSDateTimeCustomBinder { public static Date parseDateTime(String s) { DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); try { return formatter.parse(s); } catch (ParseException e) { return null; } } // crazy hack because the 'Z' formatter produces an output incompatible with the xsd:dateTime public static String printDateTime(Date dt) { DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); DateFormat tzFormatter = new SimpleDateFormat("Z"); String timezone = tzFormatter.format(dt); return formatter.format(dt) + timezone.substring(0, 3) + ":" + timezone.substring(3); } }
- Then I created a JUnit class with the following test method:
package cejug.org; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; import org.cejug.sample.ElementType; import org.cejug.sample.ObjectFactory; import org.xml.sax.SAXException; public class JaxbSampleTest extends TestCase { private static final String UTF_8 = "UTF-8"; private static final File TEST_FILE = new File("target/test.xml"); public JaxbSampleTest(String testName) { super(testName); } public static Test suite() { return new TestSuite(JaxbSampleTest.class); } @Override protected void setUp() throws Exception { super.setUp(); if (TEST_FILE.exists()) { if (!TEST_FILE.delete()) { fail("impossible to delete the test file, please release it and run the test again"); } } } public void testApp() { ObjectFactory xmlFactory = new ObjectFactory(); ElementType type = new ElementType(); Date calendar = GregorianCalendar.getInstance(TimeZone.getDefault()) .getTime(); type.setJdate(calendar); JAXBElement<ElementType> element = xmlFactory.createElement(type); try { writeXml(element, TEST_FILE); JAXBElement<ElementType> result = read(TEST_FILE); assertEquals(calendar.toString(), result.getValue().getJdate().toString()); } catch (Exception e) { fail(e.getMessage()); } } private void writeXml(JAXBElement<ElementType> sample, File file) throws JAXBException, IOException { FileWriter writer = new FileWriter(file); try { JAXBContext jc = JAXBContext.newInstance(ElementType.class .getPackage().getName(), Thread.currentThread() .getContextClassLoader()); Marshaller m = jc.createMarshaller(); m.setProperty(Marshaller.JAXB_ENCODING, UTF_8); m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); m.marshal(sample, writer); } finally { writer.close(); } } @SuppressWarnings("unchecked") public JAXBElement<ElementType> read(File file) throws JAXBException, SAXException, IOException { InputStreamReader reader = new InputStreamReader(new FileInputStream( file)); try { JAXBContext jc = JAXBContext.newInstance(ElementType.class .getPackage().getName(), Thread.currentThread() .getContextClassLoader()); Unmarshaller unmarshaller = jc.createUnmarshaller(); SchemaFactory sf = SchemaFactory .newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI); Schema schema = sf.newSchema(Thread.currentThread() .getContextClassLoader().getResource( "../classes/schema/sample-binding.xsd")); unmarshaller.setSchema(schema); JAXBElement<ElementType> element = (JAXBElement<ElementType>) unmarshaller .unmarshal(reader); return element; } finally { reader.close(); } } }
That's it, I hope it can save your next five minutes of hack :)
Opinions expressed by DZone contributors are their own.
Comments