Skip to main content

JAXB Customization of xsd:dateTime

Posted by felipegaucho on December 6, 2009 at 7:32 AM PST

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:

  1. 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.

  2. If you use xsd:dateTime instead of a simple xsd:date,
    the default adapter of JAXB doesn't work anymore.

  3. 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 href="http://weblogs.java.net/sites/default/files/jaxb-example.zip">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

  1. 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
  2. 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 " title="http://maven.apache.org/maven-v4_0_0.xsd">
    ">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>
  3. 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 " title="http://www.w3.org/2001/XMLSchema.xsd"
    ">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>
  4. Inspired by href="http://eskatos.wordpress.com/2007/11/24/jaxb-custom-binding-for-joda-time/">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);
        }
    }
  5. 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 :)

AttachmentSize
jaxb-example.zip7.33 KB

Comments

TimeZone adjustment in parseDateTime

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!!!!

\o/ Thanks my friend!!!!

SimpleDateFormat is NOT thread safe

I see bug in parseDateTime. Fire few threads and you will see...

yes, non thread safe

Thank you anthavio,

I was so focused on the damn problem with JAXB that didn't consider concurrency........ I will review the code.......