views:

34

answers:

5

Hi

I've just started working on web services using JAXB to unmarshall the incoming SOAP documents to our domain classes. I've run into a technical challenge, which is dictated by the OIO XML format used within danish govermental institutions. The format states among other things, that it is not allowed to use the xml schema attribute nillable for an xml element declation. I thus have to find another solution to my challenge.

Description We have a few numbers and dates which could be send by a webservice client to update the application. Those numbers and dates are mapped to POJO fields of equivalent types. The challenge is how to reset the value of such a POJO field, by constructing and sending correct XML.

Sending 12:31:34T01-01-2010..... will update the POJO field to the specified value.

I can however not reset the field by sending as it's not allowed for datetime elements.

I can also not send as it is not allowed by OIO XML standard.

I therefore as a grim workaround plan to send as it should not be disallowed by the OIO XML standard.

This how leads a challenge, that if a startTime element contains a delete="true" attribute, then should the corresponding POJO field be set to null; if a does not have a delete attribute, then transfer the valid element value to the POJO field.

The @XMLElement annotation only allows me to map the startTime value, e.g.

class MyClass{
   @XMLElement
   private Date startTime;
}

How can I force the delete attribute to also affect the value of the MyClass.startTime field?

Best regards, Jesper

+1  A: 

You could use an XmlAdapter for this. The adapter will convert to/from a date object. The adapted object will have two properties. The first a date propert annotated with @XmlValue and the a boolean property annotated with @XmlAttribute. I'll post a full example tomorrow.

MyClass

We will annotate the startTime property with @XmlJavaTypeAdapter.

import java.util.Date;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement
class MyClass{

    private Date startTime;

    @XmlJavaTypeAdapter(DateAdapter.class)
    public Date getStartTime() {
        return startTime;
    }

    public void setStartTime(Date startTime) {
        this.startTime = startTime;
    }

}

DateAdapter

In the DateAdapter class we will convert between an instance of Date and a class with two properties (date & boolean).

import java.util.Date;
import javax.xml.bind.annotation.adapters.XmlAdapter;

public class DateAdapter extends XmlAdapter<AdaptedDate, Date> {

    @Override
    public AdaptedDate marshal(Date date) throws Exception {
        AdaptedDate adaptedDate = new AdaptedDate();
        adaptedDate.setDate(date);
        return adaptedDate;
    }

    @Override
    public Date unmarshal(AdaptedDate adaptedDate) throws Exception {
        return adaptedDate.getDate();
    }

}

AdaptedDate

This is the two property representation of our date information. It is constructed by the XmlAdapter.

import java.util.Date;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlValue;

public class AdaptedDate {

    private Date date;

    @XmlValue
    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    @XmlAttribute
    public Boolean isDelete() {
        if(null == date) {
            return true;
        }
        return null;
    }

}

Demo

The following demo code can be used to demonstrate this code:

import java.util.Date;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(MyClass.class);
        System.out.println(jc);
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

        MyClass myClass1 = new MyClass();
        marshaller.marshal(myClass1, System.out);

        MyClass myClass2 = new MyClass();
        myClass2.setStartTime(new Date());
        marshaller.marshal(myClass2, System.out);
    }

}

Further extension:

  • You can control the element name for the startTime attribute by adding the @XmlElement(name="foo") annotation.
  • You can further control the XML representation using MOXy's @XmlPath("foo/bar") annotation like @XmlPath()

For more info see:

Blaise Doughan
+1  A: 

Hi Blaise

Thanks a lot for your answer. It seems to work for a very simple scenario, but I have no success for slightly more complex scenarios.

I have tried to mimic you code in my own domain, only am I ummarshalling data rather than marshalling.

Adapter class:

public class DeleteStringAdapter extends XmlAdapter<DeletableString, String> {

@Override
public DeletableString marshal(String value) throws Exception {
    System.out.println("MARSHALL: " + value);
    return new DeletableString(value);
}

@Override
public String unmarshal(DeletableString v) throws Exception {
    System.out.println("UNMARSHALL: "  + v);

    if(v.isDelete() != null && v.isDelete()){
        return null;
    }

    return v.getValue();    
}

}

Adapter type:

public class DeletableString {

public DeletableString() {
}

public DeletableString(String value) {
    this.value = value;
}

private Boolean delete;

private String value;

@XmlAttribute
public Boolean isDelete() {
    return delete;
}

public void setDelete(Boolean delete) {
    this.delete = delete;
}   

@XmlValue
public String getValue() {
    return value;
}

public void setValue(String value) {
    this.value = value;
}

@Override
public String toString() {
    return DeletableString.class.getSimpleName() + "[delete=" + isDelete() + ", value=" + value + "]";
}
}

Working domain class with annotations:

@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlRootElement(name = "xml-fragment")
public class SimpleNavne implements Serializable{

private String forNavn = "";

private String fornavneMrkKode = "";

@XmlElement(name="PersonGivenName")
@XmlJavaTypeAdapter(value = DeleteStringAdapter.class)
public String getForNavn() {
    return forNavn;
}

public void setForNavn(String forNavn) {
    this.forNavn = forNavn;
}

@Override
public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append(SimpleNavne.class.getSimpleName() + "[");
    builder.append("forNavn=");
    builder.append(forNavn);
    builder.append("]");
    return builder.toString();
}

}

Demo

public class AppTest {

@Test
public void testApp() throws Exception {

    System.setProperty("jaxb.debug", "true");

    try{
        JAXBContext jaxbContext = JAXBContext.newInstance(SimpleNavne.class);
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        Object ps =  unmarshaller.unmarshal(new File("./personname-test4.xml"));

        System.out.println(ps);

    }catch(Exception e){
        e.printStackTrace();
    }
}
}

File:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns:xml-fragment xmlns:ns="http://cpr.csc.com/navne"&gt;
<ns:PersonGivenName delete="false">010</ns:PersonGivenName>
</ns:xml-fragment>

The above seems to work fine if concluded from the output

SimpleNavne[forNavn=010]

I however get problems when introducing fields annotated with @XMLPath in the SimpleName domain class.

Modified domain class

@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlRootElement(name = "xml-fragment")
public class SimpleNavne implements Serializable{

private String forNavn = "";

private String fornavneMrkKode = "";

@XmlElement(name="PersonGivenName")
@XmlJavaTypeAdapter(value = DeleteStringAdapter.class)
public String getForNavn() {
    return forNavn;
}

public void setForNavn(String forNavn) {
    this.forNavn = forNavn;
}

@XmlPath("/PersonGivenNameMarkingStructure/MarkingCode/text()")
public String getFornavneMrkKode() {
    return fornavneMrkKode;
}

public void setFornavneMrkKode(String forNavnMrk) {
    this.fornavneMrkKode = forNavnMrk;
}

@Override
public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append(SimpleNavne.class.getSimpleName() + "[");
    builder.append(", forNavn=");
    builder.append(forNavn);
    builder.append(", fornavneMrkKode=");
    builder.append(fornavneMrkKode);
    builder.append("]");
    return builder.toString();
}

}

Modified file:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns:xml-fragment xmlns:ns="http://cpr.csc.com/navne"&gt;
<ns:PersonGivenName delete="true">010</ns:PersonGivenName>
<ns:PersonGivenNameMarkingStructure>
    <ns:MarkingCode>011</ns:MarkingCode>
</ns:PersonGivenNameMarkingStructure>
</ns:xml-fragment>

The output is:

SimpleNavne[forNavn=010, fornavneMrkKode=]

But should have been:

SimpleNavne[forNavn=010, fornavneMrkKode=011]

Am I doing something completely wrong or is MOXy not supporting this scenario?

PS. I have tried using MOXy 2.1.1 and 2.2.0-M3 with same result

Jesper Tejlgaard Pedersen
+1  A: 

In response to your second question:

When I run your code (using EclipseLink 2.1.1) I get the following output:

20-Oct-2010 2:50:46 PM javax.xml.bind.ContextFinder find
FINE: Trying to locate forum80/jaxb.properties
20-Oct-2010 2:50:46 PM javax.xml.bind.ContextFinder loadJAXBProperties
FINE: loading props from file:/C:/Workspaces/EclipseLink-Trunk/SCRATCH/bin/forum80/jaxb.properties
20-Oct-2010 2:50:46 PM javax.xml.bind.ContextFinder find
FINE:   found
20-Oct-2010 2:50:46 PM javax.xml.bind.ContextFinder safeLoadClass
FINE: Trying to load org.eclipse.persistence.jaxb.JAXBContextFactory
20-Oct-2010 2:50:46 PM javax.xml.bind.ContextFinder newInstance
FINE: loaded org.eclipse.persistence.jaxb.JAXBContextFactory from jar:file:/C:/Workspaces/EclipseLink-2.1/eclipselink.jar!/org/eclipse/persistence/jaxb/JAXBContextFactory.class
UNMARSHALL: DeletableString[delete=true, value=010]
SimpleNavne[, forNavn=null, fornavneMrkKode=011]

This result is different from what you are seeing:

SimpleNavne[forNavn=010, fornavneMrkKode=]

We may need to tweak the logic in the XML Adapter to handle your particular XML sample. In your XML fragment you specify both delete="true" and a value for the "PersonGivenName" element which maps to the "forNavn" property, should the null or value win in this case?

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns:xml-fragment xmlns:ns="http://cpr.csc.com/navne"&gt;
    <ns:PersonGivenName delete="true">010</ns:PersonGivenName>
    <ns:PersonGivenNameMarkingStructure>
        <ns:MarkingCode>011</ns:MarkingCode>
    </ns:PersonGivenNameMarkingStructure>
</ns:xml-fragment>

I can make the "fornavneMrkKode" property return empty string, if the JAXB RI is used. Is there a chance you do not have the jaxb.properties correctly specified to use EclipseLink MOXy?

The jaxb.properties file should live in the same package as your model classes with the following entry:

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory

UPDATE:

I have emailed you the Java project I am using to debug your issue, would you mind running it to see if you get the same result? Hopefully you'll get the same output and we can figure out the delta that is causing the incorrect behaviour.

Blaise Doughan
+1  A: 

Hi Blaise

Thank you very much for spending time on this.

As a response to your last response: http://stackoverflow.com/questions/3973080/can-jaxb-moxy-map-element-value-and-element-attribute-to-same-pojo-field/3981371#3981371

I am using the jaxb.properties file with the factory configuration. My output looks almost identical to yours except for the missing value:

21-10-2010 09:47:08 javax.xml.bind.ContextFinder find
FINE: Trying to locate com/csc/cpr/ws/domain/jaxb.properties
21-10-2010 09:47:08 javax.xml.bind.ContextFinder loadJAXBProperties
FINE: loading props from file:/C:/Ajour/ajourworkspace/jaxb-test-with-schema/target/classes/com/csc/cpr/ws/domain/jaxb.properties
21-10-2010 09:47:08 javax.xml.bind.ContextFinder find
FINE:   found
21-10-2010 09:47:08 javax.xml.bind.ContextFinder safeLoadClass
FINE: Trying to load org.eclipse.persistence.jaxb.JAXBContextFactory
21-10-2010 09:47:08 javax.xml.bind.ContextFinder newInstance
FINE: loaded org.eclipse.persistence.jaxb.JAXBContextFactory from jar:file:/C:/Users/jpedersen22/.m2/repository/org/eclipse/persistence/eclipselink/2.1.1/eclipselink-2.1.1.jar!/org/eclipse/persistence/jaxb/JAXBContextFactory.class
UNMARSHALL: DeletableString[delete=true, value=010]
SimpleNavne[forNavn=null, fornavneMrkKode=]

As you can see, then MOXy is the JAXB implementation in use. I have also used the @XMLPath annotation with success, when the DeleteStringAdapter is not in use. I.e. if I remove the @XmlJavaTypeAdapter(value = DeleteStringAdapter.class) from getFornavn, then I get the following:

21-10-2010 10:00:02 javax.xml.bind.ContextFinder find
FINE: Trying to locate com/csc/cpr/ws/domain/jaxb.properties
21-10-2010 10:00:02 javax.xml.bind.ContextFinder loadJAXBProperties
FINE: loading props from file:/C:/Ajour/ajourworkspace/jaxb-test-with-schema/target/classes/com/csc/cpr/ws/domain/jaxb.properties
21-10-2010 10:00:02 javax.xml.bind.ContextFinder find
FINE:   found
21-10-2010 10:00:02 javax.xml.bind.ContextFinder safeLoadClass
FINE: Trying to load org.eclipse.persistence.jaxb.JAXBContextFactory
21-10-2010 10:00:02 javax.xml.bind.ContextFinder newInstance
FINE: loaded org.eclipse.persistence.jaxb.JAXBContextFactory from jar:file:/C:/Users/jpedersen22/.m2/repository/org/eclipse/persistence/eclipselink/2.1.1/eclipselink-2.1.1.jar!/org/eclipse/persistence/jaxb/JAXBContextFactory.class
SimpleNavne[forNavn=010, fornavneMrkKode=011]

With respect to the XML 010, then it was made to make my life easier when testing different scenarios (with or without attribute). In any case, then my DeleteStringAdapter lets the delete attribute take presedence, which is exactly what I want.

Jesper Tejlgaard Pedersen
I have emailed you the Java project I am using to debug your issue, would you mind running it to see if you get the same result? Hopefully you'll get the same output and we can figure out the delta that is causing the incorrect behaviour.
Blaise Doughan
A: 

Hi Blaise

I ran your project with great success! I also located my own problem by comparing our projects. I had placed the DeletableString class in a separate package, but forgotten to add a package-info.java file. Adding the file with below contents makes the whole thing work:

@XmlSchema(namespace = "http://cpr.csc.com/navne", elementFormDefault = XmlNsForm.QUALIFIED)
package com.csc.cpr.ws.domain.customtypes;

import javax.xml.bind.annotation.XmlNsForm;
import javax.xml.bind.annotation.XmlSchema;

Thanks a lot for all your help. It's been of great value

Best regards, Jesper

Jesper Tejlgaard Pedersen
No problem, glad I could help.
Blaise Doughan