Java/JAXB: Unmarshall Xml to specific subclass based on an attribute
Is it possible to use JAXB to unmarshall xml to a specific Java class based on an attribute of the xml?
<shapes>
<shape type="square" points="4" square-specific-attribute="foo" />
<shape type="triangle" points="3" triangle-specific-attribute="bar" />
</shapes>
I would like to have a List of Shape objects containing a triangle and a square, each with their own shape-specific attribute. IE:
abstract class Shape {
int points;
//...etc
}
class Square extends Shape {
String sq开发者_C百科uare-specific-attribute;
//...etc
}
class Triangle extends Shape {
String triangle-specific-attribute;
//...etc
}
I'm currently just putting all attributes in one big "Shape" class and it's less than ideal.
I could get this to work if the shapes were properly named xml elements, but unfortunately I don't have control of the xml I'm retrieving.
Thanks!
JAXB is a spec, specific implementations will provide extension points to do things such as this. If you are using EclipseLink JAXB (MOXy) you could modify the Shape class as follows:
import javax.xml.bind.annotation.XmlAttribute;
import org.eclipse.persistence.oxm.annotations.XmlCustomizer;
@XmlCustomizer(ShapeCustomizer.class)
public abstract class Shape {
int points;
@XmlAttribute
public int getPoints() {
return points;
}
public void setPoints(int points) {
this.points = points;
}
}
Then using the MOXy @XMLCustomizer you could access the InheritancePolicy and change the class indicator field from "@xsi:type" to just "type":
import org.eclipse.persistence.config.DescriptorCustomizer;
import org.eclipse.persistence.descriptors.ClassDescriptor;
public class ShapeCustomizer implements DescriptorCustomizer {
@Override
public void customize(ClassDescriptor descriptor) throws Exception {
descriptor.getInheritancePolicy().setClassIndicatorFieldName("@type");
}
}
You will need to ensure that you have a jaxb.properties file in with you model classes (Shape, Square, etc) with the following entry specifying the EclipseLink MOXy JAXB implementation:
javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory
Below is the rest of the model classes:
Shapes
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Shapes {
private List<Shape> shape = new ArrayList<Shape>();;
public List<Shape> getShape() {
return shape;
}
public void setShape(List<Shape> shape) {
this.shape = shape;
}
}
Square
import javax.xml.bind.annotation.XmlAttribute;
public class Square extends Shape {
private String squareSpecificAttribute;
@XmlAttribute(name="square-specific-attribute")
public String getSquareSpecificAttribute() {
return squareSpecificAttribute;
}
public void setSquareSpecificAttribute(String s) {
this.squareSpecificAttribute = s;
}
}
Triangle
import javax.xml.bind.annotation.XmlAttribute;
public class Triangle extends Shape {
private String triangleSpecificAttribute;
@XmlAttribute(name="triangle-specific-attribute")
public String getTriangleSpecificAttribute() {
return triangleSpecificAttribute;
}
public void setTriangleSpecificAttribute(String t) {
this.triangleSpecificAttribute = t;
}
}
Below is a demo program to check that everything works:
import java.io.StringReader;
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 jaxbContext = JAXBContext.newInstance(Shapes.class, Triangle.class, Square.class);
StringReader xml = new StringReader("<shapes><shape square-specific-attribute='square stuff' type='square'><points>4</points></shape><shape triangle-specific-attribute='triangle stuff' type='triangle'><points>3</points></shape></shapes>");
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
Shapes root = (Shapes) unmarshaller.unmarshal(xml);
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(root, System.out);
}
}
I hope this helps.
For more information on EclipseLink MOXy see:
- http://www.eclipse.org/eclipselink/moxy.php
EDIT
In EclipseLink 2.2 we're making this easier to configure, check out the following article for more information:
- http://bdoughan.blogspot.com/2010/11/jaxb-and-inheritance-moxy-extension.html
The annotation @XmlElements enables you to specify which tag corresponds with which subclass.
@XmlElements({
@XmlElement(name="square", type=Square.class),
@XmlElement(name="triangle", type=Triangle.class)
})
public List<Shape> getShape() {
return shape;
}
Also see javadoc for @XmlElements
AFAIK, you'll have to write an XmlAdapter which knows how to handle the marshal/unmarshalling of the Shape.
No, I'm afraid that's not an option, JAXB isn't that flexible.
The best I can suggest is that you put a method on the Shape
class which instantiates the "correct" type based on the attribute. The client code would invoke that factory method to obtain it.
Best I can come up with, sorry.
There is @XmlSeeAlso annotation to tell to bind subclasses.
For example, with the following class definitions:
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
The user would be required to create JAXBContext as JAXBContext.newInstance(Dog.class,Cat.class) (Animal will be automatically picked up since Dog and Cat refers to it.)
XmlSeeAlso annotation would allow you to write:
@XmlSeeAlso({Dog.class,Cat.class})
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
精彩评论