August 25, 2010

@XmlTransformation - Going Beyond XmlAdapter

XML Adapter is my favourite JAXB feature, which is why it was the topic of my first post XmlAdapter - JAXB's Secret Weapon.  With it there is no such thing as an unmappable object in JAXB.  However today I came across a use case where the MOXy's transformation mapping was a much better fit.


The Use Case

The user wanted to map the following XML to objects.  The interesting point is that they wanted to combine the values of the DATE/TIME elements into one java.util.Date object. 

<ELEM_B> 
    <B_DATE>20100825</B_DATE> 
    <B_TIME>153000</B_TIME> 
    <NUM>123</NUM> 
    <C_DATE>20100825</C_DATE> 
    <C_TIME>154500</C_TIME> 
</ELEM_B> 

XmlAdapter would normally be a good fit here except for the following factors:
  • The DATE/TIME pairings are repeated throughout the document, but each time the element names change (e.g.  B_DATE/B_TIME, and C_DATE/C_TIME).
  • Each pairing is missing a grouping element.  This means we would need to adapt the entire ElemB class.

The Solution

In this case the transformation mapping concept in MOXy will make this use case much easier to map.  Let's look at how this looks:

import java.util.Date;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import org.eclipse.persistence.oxm.annotations.XmlReadTransformer;
import org.eclipse.persistence.oxm.annotations.XmlTransformation;
import org.eclipse.persistence.oxm.annotations.XmlWriteTransformer;
import org.eclipse.persistence.oxm.annotations.XmlWriteTransformers;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name="ELEM_B")
public class ElemB {

    @XmlReadTransformer(transformerClass=DateAttributeTransformer.class)
    @XmlWriteTransformers({
        @XmlWriteTransformer(xmlPath="B_DATE/text()", transformerClass=DateFieldTransformer.class),
        @XmlWriteTransformer(xmlPath="B_TIME/text()", transformerClass=TimeFieldTransformer.class),
    })
    private Date bDate;

    @XmlElement(name="NUM")
    private int num;

    @XmlReadTransformer(transformerClass=DateAttributeTransformer.class)
    @XmlWriteTransformers({
        @XmlWriteTransformer(xmlPath="C_DATE/text()", transformerClass=DateFieldTransformer.class),
        @XmlWriteTransformer(xmlPath="C_TIME/text()", transformerClass=TimeFieldTransformer.class),
    })
    private Date cDate;

}
XML Read Transformer

An XML read transformer is responsible for constructing the object value from XML.

import java.text.ParseException;
import java.text.SimpleDateFormat;

import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.mappings.foundation.AbstractTransformationMapping;
import org.eclipse.persistence.mappings.transformers.AttributeTransformer;
import org.eclipse.persistence.sessions.Record;
import org.eclipse.persistence.sessions.Session;

public class DateAttributeTransformer implements AttributeTransformer {

    private AbstractTransformationMapping mapping;
    private SimpleDateFormat yyyyMMddHHmmss = new SimpleDateFormat("yyyyMMddHHmmss");

    public void initialize(AbstractTransformationMapping mapping) {
        this.mapping = mapping;
    }

    public Object buildAttributeValue(Record record, Object instance, Session session) {
        try {
            String dateString = null;
            String timeString = null;
            
            for(DatabaseField field : mapping.getFields()) {
                if(field.getName().contains("DATE")) {
                    dateString = (String) record.get(field);
                } else {
                    timeString = (String) record.get(field);
                }
            }
            return yyyyMMddHHmmss.parseObject(dateString + timeString);
        } catch(ParseException e) {
            throw new RuntimeException(e);
        }
    }

}

XML Write Transformer(s)

An XML write transformer is responsible for constructing an XML value from the object.  A transformation mapping may have multiple write transformers.  For this example we will have two.

The first XML write transformer is responsible for writing out the year, month, and day information in the format yyMMdd.

import java.text.SimpleDateFormat;
import java.util.Date;

import org.eclipse.persistence.mappings.foundation.AbstractTransformationMapping;
import org.eclipse.persistence.mappings.transformers.FieldTransformer;
import org.eclipse.persistence.sessions.Session;

public class DateFieldTransformer implements FieldTransformer {

    private AbstractTransformationMapping mapping;
    private SimpleDateFormat yyyyMMdd = new SimpleDateFormat("yyyyMMdd");

    public void initialize(AbstractTransformationMapping mapping) {
        this.mapping = mapping;
    }

    public Object buildFieldValue(Object instance, String xPath, Session session) {
        Date date = (Date) mapping.getAttributeValueFromObject(instance);
        return yyyyMMdd.format(date);
    }

}

The second XML write transformer is responsible for writing out the hour, minute, and second information in the format HHmmss.


import java.text.SimpleDateFormat;
import java.util.Date;

import org.eclipse.persistence.mappings.foundation.AbstractTransformationMapping;
import org.eclipse.persistence.mappings.transformers.FieldTransformer;
import org.eclipse.persistence.sessions.Session;

public class TimeFieldTransformer implements FieldTransformer {

    private AbstractTransformationMapping mapping;
    private SimpleDateFormat HHmmss = new SimpleDateFormat("HHmmss");

    public void initialize(AbstractTransformationMapping mapping) {
        this.mapping = mapping;
    }

    public Object buildFieldValue(Object instance, String xPath, Session session) {
        Date date = (Date) mapping.getAttributeValueFromObject(instance);
        return HHmmss.format(date);
    }

}

jaxb.properties

In order to use the MOXy JAXB implementation you need to add a jaxb.properties file in which your model classes with the following entry:
javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory
Demo

The following class can be used to demonstrate the mapping:
import java.io.File;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(ElemB.class);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        File xml = new File("input.xml");
        ElemB elemB = (ElemB) unmarshaller.unmarshal(xml);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(elemB, System.out);
    }

}
Please Note

For this example I have specified the transformation mapping using the annotations that will be available in the upcoming EclipseLink 2.2 release.  You can try these out today by dowloading one of the nightly builds:
Equivalent functionality is available in all released versions of EclipseLink, if you are interested in how to configure it please contact me or leave a comment.

15 comments:

  1. Hi Blaise,

    I get the error at Demo-Start:

    Exception in thread "main" com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException: 2 counts of IllegalAnnotationExceptions
    javax.persistence.criteria.Root is an interface, and JAXB can't handle interfaces.
    this problem is related to the following location:
    at javax.persistence.criteria.Root
    javax.persistence.criteria.Root does not have a no-arg default constructor.
    this problem is related to the following location:
    at javax.persistence.criteria.Root

    at com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException$Builder.check(IllegalAnnotationsException.java:91)
    at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.getTypeInfoSet(JAXBContextImpl.java:436)
    at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.(JAXBContextImpl.java:277)
    at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$JAXBContextBuilder.build(JAXBContextImpl.java:1100)
    at com.sun.xml.internal.bind.v2.ContextFactory.createContext(ContextFactory.java:143)
    at com.sun.xml.internal.bind.v2.ContextFactory.createContext(ContextFactory.java:110)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:202)
    at javax.xml.bind.ContextFinder.find(ContextFinder.java:376)
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:574)
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:522)
    at de.dailab.demo.Demo.main(Demo.java:14)

    eclipselink Build ID: 20110106

    Could you help me?
    Thank you in advance.
    Best regards

    ReplyDelete
  2. Sorry, I've forgotten.
    The error occurs in Demo.java row 10 JAXBContext jc = JAXBContext.newInstance(Root.class);
    (at me the row was 14. Sorry.)

    ReplyDelete
  3. Hi Sipungora,

    I have corrected the Demo class. The JAXBContext should be created on ElemB.class and not Root.class.

    You will also need to ensure that you have the jaxb.properties file included with the ElemB class that specifies that EclipseLink MOXy should be used as the JAXB implementation. From your stack trace I see that the reference implementation is being used.

    -Blaise

    ReplyDelete
  4. Hi Blaise,

    thank you very much for your answer. It works, but I have for ElemB object such result:



    123


    What can I do wrong?

    Thank you in advance,
    Best regards,
    -Sipungora

    ReplyDelete
  5. Hi Sipungora,

    I have just gone through and fixed the example. It will work best if you are using the latest EclipseLink 2.2 nightly download.

    -Blaise

    ReplyDelete
  6. Hello Blaise,

    I'm using an @XmlReadTransformer to transform 4 separate XML elements to a Joda time Interval, but I'm getting an exception:

    The class org.joda.time.DateTime$Property requires a zero argument constructor or a specified factory method. Note that non-static inner classes do not have zero argument constructors and are not supported.

    So somehow it is inspecting the whole Joda Time class structure. In my opinion this is not needed since I'm using the XmlReadTransformer to construct the Interval.

    -Paul

    ReplyDelete
  7. Hi Paul,

    Have you correctly specified EclipseLink JAXB (MOXy) as the JAXB provider? This is done via a jaxb.properties file:

    - Specifying EclipseLink MOXy as Your JAXB Provider

    -Blaise

    ReplyDelete
  8. I get an error similar to Paul's: "The class org.joda.time.LocalDate$Property requires a zero argument constructor or a specified factory method." My JAXBContext is specified in a haxb.properties file and I've verified that it is org.eclipse.persistence.jaxb.JAXBContext at runtime.

    ReplyDelete
  9. Hi Paul & Ross,

    I have been able to confirm that the behaviour that you are seeing is a bug in MOXy. We are currently working on a fix.
    - https://bugs.eclipse.org/347470

    -Blaise

    ReplyDelete
  10. Hi Paul & Ross,

    The fix for this issue has been included in the EclipseLink 2.4.0 stream. A nightly download can be obtained from:

    http://www.eclipse.org/eclipselink/downloads/nightly.php

    We will also include this fix in EclipseLink 2.3.1 once that stream becomes available for check in (which will happen once 2.3.0 releases).

    -Blaise

    ReplyDelete
  11. Is it possible to use @XmlTransformation to create a list of objects? Let's say you have xml like:

    <DATES>
      <DATE>
        <B_DATE>20100825</B_DATE>
        <B_TIME>153000</B_TIME>
      </DATE>
      <DATE>
        <B_DATE>20100425</B_DATE>
        <B_TIME>154300</B_TIME>
      </DATE>
    </DATES>

    Could I get a List<Date> using @XmlTransformation?

    ReplyDelete
  12. Hi Paul;

    For this particular use case I would just use an XmlAdapter:

    package dateadapter;

    import java.text.SimpleDateFormat;
    import java.util.Date;

    import javax.xml.bind.annotation.XmlElement;
    import javax.xml.bind.annotation.adapters.XmlAdapter;

    public class DateAdapter extends XmlAdapter<DateAdapter.AdaptedDate, Date> {

        private SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
        private SimpleDateFormat timeFormat = new SimpleDateFormat("HHmmss");

        public static class AdaptedDate {
            @XmlElement(name="B_DATE") public String date;
            @XmlElement(name="B_TIME") public String time;
        }

        @Override
        public AdaptedDate marshal(Date date) throws Exception {
            AdaptedDate adaptedDate = new AdaptedDate();
            adaptedDate.date = dateFormat.format(date);
            adaptedDate.time = timeFormat.format(date);
            return adaptedDate;
        }

        @Override
        public Date unmarshal(AdaptedDate adaptedDate) throws Exception {
            return dateTimeFormat.parse(adaptedDate.date + adaptedDate.time);
        }

    }

    -Blaise

    ReplyDelete
  13. For what it's worth, If you try to pass the JAXBContext constructor the DateTime class from JodaTime, you'll get the same exception, where it complains about DateTime$Property, and public constructors. Just put an XMLJavaTypeAdapter in front of that DateTime, DON'T pass DateTime.class to the JAXBContext constructor, and you'll be fine. You'll tell mOXY you've got the marshalling, to just let you handle it. It is worth noting that when you write your XMLJavaTypeAdapter, or your XMLTransformer, you'll want to check for null before trying to format null.

    ReplyDelete
  14. If you are interested in Joda-Time here is an example of how those types can be handled with an XmlAdapter:
    - JAXB and Joda-Time: Dates and Times

    -Blaise

    ReplyDelete

Note: Only a member of this blog may post a comment.