JAXB Customization of xsd:dateTime
A 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:dateTimeinstead of a simplexsd:date, the default adapter of JAXB doesn't work anymore. -
Other surprise: you can't use the
java.text.SimpleDateFormatto 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> <!-- https://jaxb.dev.java.net/jaxb-maven2-plugin/ --> <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 :)
| Attachment | Size |
|---|---|
| jaxb-example.zip | 7.33 KB |
- Printer-friendly version
- felipegaucho's blog
- 10201 reads






Comments
TimeZone adjustment in parseDateTime
by djhagberg - 2009-12-07 11:31
I also noticed that the SimpleDateFormat instance created in parseDateTime does not have its TimeZone set, so it will default to parsing in the TimeZone of the JVM rather than what's indicated on the input string.From your example: 2009-12-06T15:59:34+0100
I think the method would need to look something like the following (sorry, formatting gets stripped by java.net):
import java.util.regex.*;
import java.util.TimeZone;
. . .
private static final Pattern TZ_REGEX = Pattern.compile("([+-][0-9][0-9]):?([0-9][0-9])$");
. . .
public static Date parseDateTime(String s) {
Matcher mat = TZ_REGEX.matcher(s);
TimeZone tz = null;
if( mat.find ) {
String tzCode = "GMT"+mat.group(1)+mat.group(2); // eg "GMT+0100"
tz = TimeZone.getTimeZone(tzCode);
}
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
if( tz != null ) {
formatter.setTimeZone(tz);
}
try {
return formatter.parse(s);
} catch (ParseException e) {
return null;
}
}
\o/ Thanks my friend!!!!
by rodrigolopes - 2009-12-07 03:29
\o/ Thanks my friend!!!!SimpleDateFormat is NOT thread safe
by anthavio - 2009-12-06 16:30
I see bug in parseDateTime. Fire few threads and you will see...yes, non thread safe
by felipegaucho - 2009-12-07 00:19
Thank you anthavio, I was so focused on the damn problem with JAXB that didn't consider concurrency........ I will review the code.......