Felipe Gaúcho works as senior software engineer at Netcetera AG in Switzerland. He is a well known Brazilian JUG leader and open-source evangelist. Felipe works with Java since its early versions and has plans to keep that Java tradition as it is. When he is not coding, he prefers to listen reggae and travel around with his lovely wife Alena and his son Rodrigo. Felipe is a DZone MVB and is not an employee of DZone and has posted 29 posts at DZone. View Full User Profile

JAXB Customization of xsd:dateTime

12.07.2009
| 12724 views |
  • submit to reddit

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 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 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>
    
  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 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 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 :)

From http://weblogs.java.net/blog/felipegaucho

Published at DZone with permission of Felipe Gaúcho, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags:

Comments

Karthik Venkatesan replied on Sat, 2010/09/18 - 1:16pm

Has anyone been able to download the complete project?

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.