JAXB 2.x : How to override an XmlElement annotation from parent class - Mission Impossible?
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开发者_Go百科>3</string3>
</source>
</root>
Any hints how to tweak JAXB to produce the expected result?? Thanks for any feedback.
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
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>
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";
}
}
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.
精彩评论