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; }
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); } } }
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); } }
javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory
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); } }
Hi Blaise,
ReplyDeleteI 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
Sorry, I've forgotten.
ReplyDeleteThe error occurs in Demo.java row 10 JAXBContext jc = JAXBContext.newInstance(Root.class);
(at me the row was 14. Sorry.)
Hi Sipungora,
ReplyDeleteI 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
Hi Blaise,
ReplyDeletethank 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
Hi Sipungora,
ReplyDeleteI have just gone through and fixed the example. It will work best if you are using the latest EclipseLink 2.2 nightly download.
-Blaise
Hello Blaise,
ReplyDeleteI'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
Hi Paul,
ReplyDeleteHave 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
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.
ReplyDeleteHi Paul & Ross,
ReplyDeleteI 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
Hi Paul & Ross,
ReplyDeleteThe 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
Thanks for the fix Blaise!
ReplyDeleteIs it possible to use @XmlTransformation to create a list of objects? Let's say you have xml like:
ReplyDelete<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?
Hi Paul;
ReplyDeleteFor 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
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.
ReplyDeleteIf you are interested in Joda-Time here is an example of how those types can be handled with an XmlAdapter:
ReplyDelete- JAXB and Joda-Time: Dates and Times
-Blaise