Java/Jaxb: Unmarshall Xml to Specific Subclass Based on an Attribute

Java/JAXB: Unmarshall Xml to specific subclass based on an attribute

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

Java/JAXB: Unmarshall XML attributes to specific Java object attributes

How about?

Introduce a common super class called Options:

import javax.xml.bind.annotation.XmlAttribute;

public abstract class Options {

private String name;

@XmlAttribute
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

}

Then on your class with the list of options (Configuration in this example), specify an @XmlJavaTypeAdapter on that property:

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement
public class Configuration {

private List<Options> section = new ArrayList<Options>();

@XmlJavaTypeAdapter(OptionsAdapter.class)
public List<Options> getSection() {
return section;
}

public void setSection(List<Options> section) {
this.section = section;
}

}

The XmlAdapter will look something like this:

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class OptionsAdapter extends XmlAdapter<AdaptedOptions, Options> {

@Override
public Options unmarshal(AdaptedOptions v) throws Exception {
if("default_options".equals(v.name)) {
DefaultOptions options = new DefaultOptions();
options.setName(v.getName());
options.setDefaultPort(Integer.valueOf(v.map.get("default_port")));
options.setLogLevel(v.map.get("log_level"));
return options;
} else {
CustomOptions options = new CustomOptions();
options.setName(v.getName());
options.setCompatibility(v.map.get("compatibility"));
options.setMemory(v.map.get("memory"));
return options;
}
}

@Override
public AdaptedOptions marshal(Options v) throws Exception {
AdaptedOptions adaptedOptions = new AdaptedOptions();
adaptedOptions.setName(v.getName());
if(DefaultOptions.class == v.getClass()) {
DefaultOptions options = (DefaultOptions) v;
adaptedOptions.map.put("default_port", String.valueOf(options.getDefaultPort()));
adaptedOptions.map.put("log_level", options.getLogLevel());
} else {
CustomOptions options = (CustomOptions) v;
adaptedOptions.map.put("compatibility", options.getCompatibility());
adaptedOptions.map.put("memory", options.getMemory());
}
return adaptedOptions;
}

}

AdaptedOptions looks like:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlValue;

public class AdaptedOptions extends Options {

@XmlAttribute String name;
@XmlElement List<Value> value = new ArrayList<Value>();
Map<String, String> map = new HashMap<String, String>();

public void beforeMarshal(Marshaller marshaller) {
for(Entry<String, String> entry : map.entrySet()) {
Value aValue = new Value();
aValue.name = entry.getKey();
aValue.value = entry.getValue();
value.add(aValue);
}
}

public void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
for(Value aValue : value) {
map.put(aValue.name, aValue.value);
}
}

private static class Value {
@XmlAttribute String name;
@XmlValue String value;
}

}

JAXB: Unmarshall to different classes based on an element's attribute value

Note: I'm the EclipseLink JAXB (MOXy) lead and a member of the JAXB 2 (JSR-222) expert group.

You could use MOXy's @XmlDescriminatorNode/@XmlDescriminatorValue JAXB extension.

Base

@XmlDiscriminatorNode("@name")
public abstract class Base {
}

Person

@XmlDiscriminatorValue("person")
public class Person extends Base {
}

Vehicle

@XmlDiscriminatorValue("vehicle")
public class Vehicle extends Base {
}

For More Information

  • http://blog.bdoughan.com/2010/11/jaxb-and-inheritance-moxy-extension.html
  • http://blog.bdoughan.com/2011/05/specifying-eclipselink-moxy-as-your.html

Below is an answer to a similar question that uses an older version of the MOXy API:

  • Java/JAXB: Unmarshall Xml to specific subclass based on an attribute

Unmarshalling to a subclass with JAXB

One of my previous answers to a similar question may help here. Essentially it is using the @XmlDescrimatorNode in EclipseLink JAXB (MOXy). Note I'm the MOXy tech lead.

  • Java/JAXB: Unmarshall Xml to specific subclass based on an attribute

You could also do this with an XmlAdapter. AdaptedEntity would have all the properties from Entity and it's subclasses.

  • JAXB @XmlElements, different types but same name?

Overwriting attributes in subclasses with jaxb

Are you looking to use the path attribute as the inheritance indicator? If so the following will help:

  • Stack Overflow: Java/JAXB: Unmarshall Xml to specific subclass based on an attribute
  • Blog Post: http://bdoughan.blogspot.com/2010/11/jaxb-and-inheritance-moxy-extension.html

I'm still not 100% sure I understand your use case but what about the following:

Stereotypes

import java.util.List;

import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Stereotypes {

private List<AbstractStereotype> sterotypes;

@XmlElementRef
public List<AbstractStereotype> getSterotypes() {
return sterotypes;
}

public void setSterotypes(List<AbstractStereotype> sterotypes) {
this.sterotypes = sterotypes;
}

}

AbstractStereotype

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlSeeAlso;

@XmlSeeAlso({Stereotype1.class, Stereotype2.class})
public abstract class AbstractStereotype {

@XmlAttribute
public abstract String getPath();

}

Stereotype1

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Stereotype1 extends AbstractStereotype {
public String getPath() {
return "Path1";
}
}

Stereotype2

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Stereotype2 extends AbstractStereotype {

public String getPath() {
return "Path2";
}

}

Demo

import java.io.File;

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 jc = JAXBContext.newInstance(Stereotypes.class);

Unmarshaller unmarshaller = jc.createUnmarshaller();
Stereotypes stereotypes = (Stereotypes) unmarshaller.unmarshal(new File("input.xml"));

Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(stereotypes, System.out);

}
}

input.xml

<?xml version="1.0" encoding="UTF-8"?>
<stereotypes>
<stereotype1/>
<stereotype2/>
</stereotypes>

Output

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<stereotypes>
<stereotype1 path="Path1"/>
<stereotype2 path="Path2"/>
</stereotypes>

For More Information

  • http://bdoughan.blogspot.com/2010/11/jaxb-and-inheritance-using-substitution.html

JAXB inheritance - unmarshalling to subtypes declared in annotations?

You can use the @XmlSeeAlso (javax.xml.bind.annotation) annotation for this.

@XmlDiscriminatorNode("@type")
@XmlSeeAlso({ CliActionDef.class, XsltActionDef.class, ... })
public static class ActionDef extends ContainerOfStackableDefs { ... }

UPDATE

The problem you are seeing in your latest updated is caused by the @XmlDescriminatorNode not being placed on the root class in your inheritance hierarchy. When I removed ContainerOfStackableDefs from the inheritance hierarchy using @XmlTransient everything worked fine (see: http://blog.bdoughan.com/2011/06/ignoring-inheritance-with-xmltransient.html).

Demo

import java.io.File;
import java.util.*;
import javax.xml.bind.*;
import javax.xml.bind.annotation.*;
import org.eclipse.persistence.oxm.annotations.*;

public class Demo {

@XmlTransient
public static class ContainerOfStackableDefs {
}

@XmlDiscriminatorNode("@type")
@XmlSeeAlso({ ManualActionDef.class, FileBasedActionDef.class, XsltActionDef.class })
public static class ActionDef extends ContainerOfStackableDefs {
//@XmlAttribute
//@XmlReadOnly
public String typeVal; // Was type, caused an exception.

//public List<PropertyBean> properties;
//@XmlAnyAttribute
public Map<String, String> attribs = new HashMap();
}

@XmlDiscriminatorValue("manual")
public static class ManualActionDef extends ActionDef {
}

@XmlDiscriminatorValue("fileBased")
public static class FileBasedActionDef extends ActionDef {
/** Path mask. Ant-like wildcards, EL. */
@XmlAttribute public String pathMask;

/** Where to store the result. May be a dir or a file. EL. */
@XmlAttribute public String dest;
}

@XmlDiscriminatorValue("xslt")
public static class XsltActionDef extends FileBasedActionDef {
/** XSLT template path. */
@XmlAttribute public String xslt;
}

@XmlRootElement
public static class Root {

@XmlElement(name="actionDef")
public List<ActionDef> actionDefs;

}

public static void main(String[] args) throws Exception {
JAXBContext jc = JAXBContext.newInstance(Root.class);

Unmarshaller unmarshaller = jc.createUnmarshaller();
File xml = new File("src/forum17453793/input.xml");
Root root = (Root) unmarshaller.unmarshal(xml);

Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(root, System.out);
}

}

input.xml/Output

<?xml version="1.0" encoding="UTF-8"?>
<root>
<actionDef type="manual"/>
<actionDef pathMask="PATH MASK" dest="DEST" type="fileBased"/>
<actionDef xslt="XSLT" type="xslt"/>
</root>

JAXB inheritance, unmarshal to subclass of marshaled class

You're using JAXB 2.0 right? (since JDK6)

There is a class:

javax.xml.bind.annotation.adapters.XmlAdapter<ValueType,BoundType>

which one can subclass, and override following methods:

public abstract BoundType unmarshal(ValueType v) throws Exception;
public abstract ValueType marshal(BoundType v) throws Exception;

Example:

public class YourNiceAdapter
extends XmlAdapter<ReceiverPerson,Person>{

@Override public Person unmarshal(ReceiverPerson v){
return v;
}
@Override public ReceiverPerson marshal(Person v){
return new ReceiverPerson(v); // you must provide such c-tor
}
}

Usage is done by as following:

@Your_favorite_JAXB_Annotations_Go_Here
class SomeClass{
@XmlJavaTypeAdapter(YourNiceAdapter.class)
Person hello; // field to unmarshal
}

I'm pretty sure, by using this concept you can control the marshalling/unmarshalling process by yourself (including the choice the correct [sub|super]type to construct).



Related Topics



Leave a reply



Submit