问题
I am trying to parse an XML file using JAXB which contains a list of items. The class of the items depends on the value of an element in the XML.
This is a legacy system and I can't easily change the input format.
For example, given the following XML and class definitions:
<root>
<type>a</type>
<item>
<a>a1</a>
</item>
<item>
<a>a2</a>
</item>
</root>
@XmlRootElement(name = "root")
public class Root {
@XmlElement
String type;
@XmlElement(name="item")
List<Item> items;
}
public class Item {}
public class ItemA extends Item {
@XmlElement
String a;
}
public class ItemB extends Item {
@XmlElement
String b;
}
As it works now, the items list contains two Item objects.
I need the items list in the resulting Root object to contain two ItemA objects, one with a="a1" and the other with a="a2".
If the type element is "b", I need the items list to contain ItemB objects.
There will only be one type element specified in a single XML file.
I have seen several solutions using attribute values, but none using element values.
回答1:
Following the tip of Blaise Doughan you could create an XmlAdapter. Unfortunately Adapters are not available on the root level so you would have to add a bit extra code during un-/marshalling.
Root/Item/ItemA/ItemB are plain POJOs without any annotations here.
The Adapter with the adapted types:
public class RootAdapter extends XmlAdapter<AdaptedRoot, Root>
{
@Override
public Root unmarshal( AdaptedRoot v ) throws Exception
{
Root root = new Root();
root.type = v.type;
for ( AdaptedItem adaptedItem : v.items )
{
if ( v.type.equals( "a" ) )
{
ItemA a = new ItemA();
a.a = adaptedItem.a;
root.items.add( a );
}
if ( v.type.equals( "b" ) )
{
ItemB b = new ItemB();
b.b = adaptedItem.b;
root.items.add( b );
}
}
return root;
}
@Override
public AdaptedRoot marshal( Root v ) throws Exception
{
AdaptedRoot adapted = new AdaptedRoot();
adapted.type = v.type;
for ( Item item : v.items )
{
AdaptedItem adaptedItem = new AdaptedItem();
if ( v.type.equals( "a" ) )
{
adaptedItem.a = ((ItemA) item).a;
}
if ( v.type.equals( "b" ) )
{
adaptedItem.b = ((ItemB) item).b;
}
adapted.items.add( adaptedItem );
}
return adapted;
}
@XmlRootElement( name = "root" )
public static class AdaptedRoot
{
@XmlElement
String type;
@XmlElement( name = "item" )
List<AdaptedItem> items = new ArrayList<>();
}
public static class AdaptedItem
{
@XmlElement
String a;
@XmlElement
String b;
}
}
Un-/marshalling could be done like this:
public static void main( String[] args ) throws Exception
{
String rawRootA = "<root><type>a</type><item><a>a1</a></item><item><a>a2</a></item></root>";
String rawRootB = "<root><type>b</type><item><b>b1</b></item><item><b>b2</b></item></root>";
Root rootA = unmarshal( rawRootA );
for ( Item item : rootA.items )
{
System.out.println( item.getClass().getSimpleName() );
}
print( rootA );
Root rootB = unmarshal( rawRootB );
for ( Item item : rootB.items )
{
System.out.println( item.getClass().getSimpleName() );
}
print( rootB );
}
public static Root unmarshal( String xml ) throws Exception
{
JAXBContext context = JAXBContext.newInstance( AdaptedRoot.class );
Unmarshaller unmarshaller = context.createUnmarshaller();
XmlAdapter<AdaptedRoot, Root> adapter = new RootAdapter();
AdaptedRoot adapted = (AdaptedRoot) unmarshaller.unmarshal( new StringReader( xml ) );
return adapter.unmarshal( adapted );
}
public static void print( Root root ) throws Exception
{
JAXBContext context = JAXBContext.newInstance( AdaptedRoot.class );
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, true );
XmlAdapter<AdaptedRoot, Root> adapter = new RootAdapter();
AdaptedRoot adaptedRoot = adapter.marshal( root );
marshaller.marshal( adaptedRoot, System.out );
}
with the expected output:
ItemA
ItemA
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
<type>a</type>
<item>
<a>a1</a>
</item>
<item>
<a>a2</a>
</item>
</root>
ItemB
ItemB
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
<type>b</type>
<item>
<b>b1</b>
</item>
<item>
<b>b2</b>
</item>
</root>
回答2:
I came up with a solution using @XmlAnyElement and a technique described by Blaise Doughan here.
The Root class becomes:
@XmlRootElement(name = "root")
public class Root {
@XmlElement
String type;
@XmlAnyElement
List<Element> other;
List<Item> items = new ArrayList<Item>();
}
Then after unmarshaling the Root object, I iterate over the other Elements and populate my items list by unmarshaling each Element for the proper class:
Class unmarshalClass = null;
switch (root.type.toLowerCase()) {
case "a":
unmarshalClass = ItemA.class;
break;
case "b":
unmarshalClass = ItemB.class;
break;
default:
throw new Exception("Unknown type " + root.type);
}
for (Element element : root.other) {
if (element.getNodeName().equalsIgnoreCase("item")) {
root.items.add(
(Item)jaxbUnmarshaller
.unmarshal(element, unmarshalClass).getValue());
}
}
来源:https://stackoverflow.com/questions/20797405/jaxb-unmarshal-to-subclass-based-on-element-value