JAXB 2.x : How to override an XmlElement annotation from parent class - Mission Impossible?
Asked Answered
T

4

8

Why is this not possible? It seems so simple but it does not behave as expected.

Summary: Class A uses an aggregated DataA bean whereas Class B (a subclass of Class A) is using an aggregated DataB bean (whereas DataB extends DataA).

I wrote these test classes to visualize and explain my question:

Class A:

package test;

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

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name="root")
public class A {

  private DataA source = new DataA();

  @XmlElement(name="source")
  public DataA getSource() {
    return source;
  }

  public void setSource(DataA source) {
    this.source = source;
  }

}

and its DataA class (I used the FIELD annotation so that all fields gets marshalled):

package test;

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

@XmlAccessorType(XmlAccessType.FIELD)
public class DataA {

    public String string1 = "1";
    public String string2 = "2";

}

And now the Class B (subclass of Class A): My goal is to reuse functionalities of A and also reuse the properties from the DataA bean by using the DataB bean:

package test;

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

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name="root")
public class B extends A {

  private DataB source = new DataB();

  public DataB getSource() {
    return this.source;
  }

  public void setSource(DataB source) {
    this.source = source;
  }

}

Its corresponding DataB bean looks like this:

package test;

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

@XmlAccessorType(XmlAccessType.FIELD)
public class DataB extends DataA {
    public String string3 = "3";
}

Now, when I marshall an instance of class A, it gives this output:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
  <source>
    <string1>1</string1>
    <string2>2</string2>
  </source>
</root>

When I marshall an instance of class B, I get the very same result:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
  <source>
    <string1>1</string1>
    <string2>2</string2>
  </source>
</root>

But I expected that also string3 would get marshalled, but it is only writing the properties of bean DataA! WHY? This is not really intuitive when thinking in terms of OOP.

When I set the @XmlElement annotation also on the Class B... like this:

@XmlElement
public DataB getSource() {
    return this.source;
}

... then the property gets marshalled twice because it is once annotated by the parent class as well as by the child class. This is also what I do not want:

The output now is:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
    <source xsi:type="dataB" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <string1>1</string1>
        <string2>2</string2>
        <string3>3</string3>
    </source>
    <source>
        <string1>1</string1>
        <string2>2</string2>
        <string3>3</string3>
    </source>
</root>

What I expected from JAXB as a result is the following XML:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
    <source>
        <string1>1</string1>
        <string2>2</string2>
        <string3>3</string3>
    </source>
</root>

Any hints how to tweak JAXB to produce the expected result?? Thanks for any feedback.

Teflon answered 11/1, 2011 at 18:32 Comment(4)
try putting @XMLElement over fields rather than public methodMachine
doesn't help. I also tried XmlAccessType.FIELD without any XmlElement annotation on the fields, and I also tried XmlAccessType.PROPERTY without any XmlElement annotation on the GETTER... is this a bug? like this is it not really useful.Teflon
... and as you can see I put the annotation @XmlAccessorType(XmlAccessType.NONE) to both classes... so only those elements which get annotated by XmlElement get marshalledTeflon
This has been fixed in the JRE trunk: java.net/jira/browse/JAXB-804Melyndamem
F
3

Just don't annotate the source property on class B. The source property was mapped on the parent class and should not be mapped again on the child class. Since you are annotating the get/set methods the appropriate get/set will be called on class B.

package test;

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

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name="root")
public class B extends A {

  private StringBuffer source = null;

  public String getSource() {
    return source.toString();
  }

  public void setSource(String source) {
    this.source = new StringBuffer(source);
  }
}

UPDATE

There may be a bug in the Metro JAXB (reference implementation). When I run this updated example with EclipseLink JAXB (MOXy) I get the following output:

<?xml version="1.0" encoding="UTF-8"?>
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <source>
      <string1>1</string1>
      <string2>2</string2>
   </source>
</root>
<?xml version="1.0" encoding="UTF-8"?>
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="b">
   <source xsi:type="dataB">
      <string1>1</string1>
      <string2>2</string2>
      <string3>3</string3>
   </source>
</root>

This can be reproduced with the following code:

package test;

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(A.class, B.class);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

        A a = new A();
        DataA da = new DataA();
        da.string1 = "1";
        da.string2 = "2";
        a.setSource(da);
        marshaller.marshal(a, System.out);

        B b = new B();
        DataB db = new DataB();
        db.string1 = "1";
        db.string2 = "2";
        db.string3 = "3";
        b.setSource(db);
        marshaller.marshal(b, System.out);
    }
}

To use MOXy as the JAXB implementation you need to supply a file named jaxb.properties in the model package (test) with the following entry:

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory
Fertility answered 11/1, 2011 at 18:48 Comment(18)
+1 nice to see you :) , 1 Q. in base class its private then how will it come to child class too ? is it due to overridden setter getters ?Machine
That's great and really helps!Teflon
I will compile another example where not only a String is used: Let's say class A uses Bean with 2 string properties whereas class B uses a Bean with 3 properties. If I do not annotate class B, only the 2 parameters are marshalled instead of 3... let me prepare the example...Teflon
Jigar Joshi - In this example it is the accessors that are annotated. So when populating the object, a JAXB impl will use set methods. These set methods can be different on the parent and child as long as they are named the same.Fertility
bzero - No problem, if this solves your problem please consider accepting and/or up voting this answer.Fertility
No it is not yet solved, I'll update my question then a better example with incorporating your initial hint.Teflon
These set methods can be different on the parent and child as long as they are named the same. You mean object of Method using reflection ?Machine
@Blaise: thanks! Actually I just use Java SE 6, forgot to mention that. I think there is even another JAXB impl in it compared to the RI. I will try out MOXyTeflon
HOW TO INSTALL: Is it possible to use only MOXy or do I need to download the whole EclipseLink JARs?Teflon
@bzero - Java SE 6 from Oracle (Sun) will contain the JAXB reference implementation Metro. I'm the MOXy tech lead.Fertility
You can use MOXy with either of the following configurations: 1) eclipselink.jar 2) Bundles: org.eclipse.persistence.asm, org.eclipse.persistence.core, and org.eclipse.persistence.moxy.Fertility
@Blaise: I see... So Oracle should update the SE6! I'm about to install the MOXy via Update Center in Eclipse... if I put the jaxb.properties into the package folder of my JAXB-beans only, I assume that there will be no impact on other XML binding libs in my application, right?Teflon
@bzero - SE6 receives regular updates with fixes to the JAXB RI. You should report this issue as a Metro bug at: java.net/jira/browse/JAXBFertility
@Blaise: it works now for me too with MOXy: <?xml version="1.0" encoding="UTF-8"?> <root xmlns:xsi="w3.org/2001/XMLSchema-instance" xsi:type="b"> <source> <string1>1</string1> <string2>2</string2> <string3>3</string3> </source> </root>Teflon
Why does it print out the xsi:type ?Teflon
@bzero - As far as JAXB is concerned the property is of type DataA, since this is the property type on the super class. When an instance of DataB is marshalled the xsi:type attribute is used to qualify it as an instance of a sub type. Check out: bdoughan.blogspot.com/2010/11/…Fertility
Can you comment on this: #4661763Teflon
@BlaiseDoughan: i'm upgrading from Java 1.6 to Java 1.8 now. Would you still recommend to go with Moxy?Teflon
M
1

you don't need to use MOXy.. Just change the Class B and use @XmlAlso.

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "root")
public class B extends A {
}


@XmlAccessorType(XmlAccessType.FIELD)
@XmlSeeAlso({ DataA.class, DataB.class })
@XmlRootElement(name = "source")
@XmlType(name = "source")

public class DataA {
   private String string1 = "1";
   private String string2 = "2";
.....//getters and setters here
}

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "source")
public class DataB extends DataA {
    private String string3 = "3";
.....//getters and setters here
}

@XmlAccessorType(XmlAccessType.FIELD) 
@XmlSeeAlso({ A.class, B.class }) 
@XmlRootElement(name = "root") 

public class A {

    private DataA source = new DataA();

    public DataA getSource() {
        return source;
    }

    public void setSource(DataA source) {
        this.source = source;
    }

}


 B b = new B();
        DataB db = new DataB();
        db.setString1("1");
        db.setString2("2");
        db.setString3("3");
        b.setSource(db);
        marshaller.marshal(b, System.out);

WILL FINALLY WRITE:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
    <source>
        <string1>1</string1>
        <string2>2</string2>
    </source>
</root>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
    <source xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="dataB">
        <string1>1</string1>
        <string2>2</string2>
        <string3>3</string3>
    </source>
</root>
Minesweeper answered 10/2, 2011 at 14:19 Comment(2)
Hi, that's an interesting approach but did you mix up something? Because there is class A and there is "B extends A", so A does not know anything about B. Applying an annotation in class A which actually references class B is breaking the object oriented data modeling... ?Teflon
Maybe you can apply the @Also only to class B? then it would be ok and I'd try it out.Teflon
T
0

Thanks a lot Blaise for all these hints. MOXy now works fine with my real application with real beans. nice!

The only drawback I currently have is that the last line in this configuration code does not work anymore because MOXy uses of course another namespace prefix mapping mechanism.

Do you have a pointer for this? I searched the MOXy documentation and search for namespace but nothing similar was found.

    NamespacePrefixMapper mapper = new PreferredMapper();
    Marshaller m = context.createMarshaller();
    m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE );
    m.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
    m.setProperty("com.sun.xml.internal.bind.namespacePrefixMapper", mapper);

    public static class PreferredMapper extends NamespacePrefixMapper {
      @Override
      public String getPreferredPrefix(String namespaceUri, String suggestion, boolean requirePrefix) {
        return "z";
    }
}
Teflon answered 11/1, 2011 at 20:42 Comment(3)
LINE NOT WORKING with MOXy: m.setProperty("com.sun.xml.internal.bind.namespacePrefixMapper", mapper);Teflon
MOXy does not support the namespacePrefixMapper, however you can do this much easier in MOXy. MOXy will use the prefixes as specified in the @XmlSchema annotation. For an example see: #3290144Fertility
Amazing, thanks! now it fully works as it should have worked 1 day ago.Teflon
U
0

You can use this method:

public interface Data {}

@XmlTransient
public abstract class A {
       private Data data;

       public Data getData(){
              return data;
       }

       public void setData(Data data){
              this.data = data;
       }
}

@XmlAccessorType(XmlAccessType.NONE)
public class ClassA extends A {

       // DataA implements Data
       @XmlElement(type=DataA.class)
       public Data getData(){
              return super.getData();
       }

       public void setData(DataA data){
              super.setData(data);
       }
}

@XmlAccessorType(XmlAccessType.NONE)
public class ClassB extends A {
       // DataB implements Data
       private DataB data;

       @XmlElement(type=DataB.class)
       public DataA getData(){
              return data;
       }

       public void setData(DataB data){
              this.data = data;
       }
}

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement
public class ClassC {
       @XmlElement
       private ClassA classA;
       @XmlElement
       private ClassB classB;

       public ClassA getClassA() {
              return classA;
       }

       public void setClassA(ClassA classA) {
              this.classA = classA;
       }

       public ClassB getClassB() {
              return classB;
       }

       public ClassB setClassB(ClassB classB) {
              this.classB = classB;
       }
}

this mapping worked for me.

Undersecretary answered 20/3, 2013 at 21:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.