Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 square-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!

like image 747
Frothy Avatar asked Jun 07 '10 18:06

Frothy


2 Answers

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
like image 184
bdoughan Avatar answered Sep 29 '22 10:09

bdoughan


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

like image 32
Barmak Avatar answered Sep 29 '22 10:09

Barmak