Dynamic root element JAXB?
Asked Answered
S

2

8

I'm trying to integrate with a third-party system and depending on the type of object, the root element of the returned XML document changes. I am using the JAXB library for Marshalling/unmarshalling.

Root1:

<?xml version="1.0" encoding="UTF-8"?>
<root1 id='1'>
   <MOBILE>9831138683</MOBILE>
   <A>1</A>
   <B>2</B>
</root1>

Root2:

<?xml version="1.0" encoding="UTF-8"?>
<root2 id='3'>
   <MOBILE>9831138683</MOBILE>
   <specific-attr1>1</specific-attr1>
   <specific-attr2>2</specific-attr2>
</root2>

I am consuming all the different XML's mapping them to a generic object:

 @XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "ROW")
public class Row {

    @XmlAttribute
    private int id;
    @XmlElement(name = "MOBILE")
    private int mobileNo;

    @XmlMixed
    @XmlAnyElement
    @XmlJavaTypeAdapter(MyMapAdapter.class)
    private Map<String, String> otherElements;
}

And the adapter for turning the uknown values into a map:

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.parsers.DocumentBuilderFactory;
import java.util.HashMap;
import java.util.Map;

public class MyMapAdapter extends XmlAdapter<Element, Map<String, String>> {

    private Map<String, String> hashMap = new HashMap<>();

    @Override
    public Element marshal(Map<String, String> map) throws Exception {
        // expensive, but keeps the example simpler
        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();

        Element root = document.createElement("dynamic-elements");

        for(Map.Entry<String, String> entry : map.entrySet()) {
            Element element = document.createElement(entry.getKey());
            element.setTextContent(entry.getValue());
            root.appendChild(element);

        }

        return root;
    }


    @Override
    public Map<String, String> unmarshal(Element element) {
        String tagName = element.getTagName();
        String elementValue = element.getChildNodes().item(0).getNodeValue();
        hashMap.put(tagName, elementValue);

        return hashMap;
    }
}

This will put id and mobile number in the fields, and the rest, the unknown into a map.

This works if the Root Element is fixed to ROW as in the above example.

How to make this work such that root element will be different in each XML? A way to maybe just be agnostic to root element while unmarshalling ?

Strainer answered 14/11, 2019 at 6:31 Comment(4)
I think is generic beyond all use. There's no contract here. You can return anything you want. You make your users guess. I'd either make a better API that had explicit methods for each type returned or drop this requirement.Tripletail
Atleast do you know what are all possible root elements?Chancellorsville
@HarshalKhachane Yes I know the set!Strainer
It the set is not too long, then you can simply use inheritance and move all XMLElements in parent class and creating Child classes for every root.Chancellorsville
H
3

Forget about JAXB for this, and parse it yourself with StAX.

In the code below, I've changed field mobileNo from int to String, since value 9831138683 is too large for an int.

private static Row parse(String xml) throws XMLStreamException {
    XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory();
    XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(new StringReader(xml));
    reader.nextTag(); // read root element
    Row row = new Row(Integer.parseInt(reader.getAttributeValue(null, "id")));
    while (reader.nextTag() == XMLStreamConstants.START_ELEMENT) {
        String tagName = reader.getLocalName();
        if (tagName.equals("MOBILE")) {
            row.setMobileNo(reader.getElementText());
        } else {
            row.addOtherElement(tagName, reader.getElementText());
        }
    }
    return row;
}
public class Row {
    private int id;
    private String mobileNo;
    private Map<String, String> otherElements = new LinkedHashMap<>();

    public Row(int id) {
        this.id = id;
    }
    public void setMobileNo(String mobileNo) {
        this.mobileNo = mobileNo;
    }
    public void addOtherElement(String name, String value) {
        this.otherElements.put(name, value);
    }

    // getters here

    @Override
    public String toString() {
        return "Row[id=" + this.id + ", mobileNo=" + this.mobileNo +
                 ", otherElements=" + this.otherElements + "]";
    }
}

Test

public static void main(String[] args) throws Exception {
    test("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
         "<root1 id='1'>\n" +
         "   <MOBILE>9831138683</MOBILE>\n" +
         "   <A>1</A>\n" +
         "   <B>2</B>\n" +
         "</root1>");
    test("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
         "<root2 id='3'>\n" +
         "   <MOBILE>9831138683</MOBILE>\n" +
         "   <specific-attr1>1</specific-attr1>\n" +
         "   <specific-attr2>2</specific-attr2>\n" +
         "</root2>");
}
private static void test(String xml) throws XMLStreamException {
    System.out.println(parse(xml));
}

Output

Row[id=1, mobileNo=9831138683, otherElements={A=1, B=2}]
Row[id=3, mobileNo=9831138683, otherElements={specific-attr1=1, specific-attr2=2}]
Hylomorphism answered 23/11, 2019 at 8:1 Comment(4)
I am not sure about whether working at a lower level of abstraction (via StAX) for this types of xml vs unmarhshalling via JAXB (for known data objects where the XSD is defined), which would be better. Even if I use StAX, I need to serialize back the Row object to proper xml with matching root element.Strainer
In addition I have to take care of both XML and JSON inputs.Strainer
@SiddharthTrikha If you need to retain the root element name when serializing back to XML, you need to have the Row class remember the name. --- If you need to take care of JSON input, use a JSON parser.Hylomorphism
@Hylomorphism excellent point on making mobileNo a String. Even if int was large enough to store a phone number, the value doesn't represent an amount. IMO, this warrants it to be treated as a string even if the value is numeric.Inexcusable
I
2

I don't think there is a way to do what you are asking. In XML, the root node (the document) must have a defined element (or class). In other words xs:any only works for sub-elements. Even if there was a way to achieve this, this is a bad decision. Instead of creating a variable ("dynamic") root element, you should add a name attribute to the same element to distinguish the XML files. For example:

<?xml version="1.0" encoding="UTF-8"?>
<ROW id='1' name="me">
   <MOBILE>9831138683</MOBILE>
   <specific-attr1>1</specific-attr1>
   <specific-attr2>2</specific-attr2>
</ROW>


<?xml version="1.0" encoding="UTF-8"?>
<ROW id='2' name="you">
   <MOBILE>123456790</MOBILE>
   <specific-attr1>3</specific-attr1>
   <specific-attr2>4</specific-attr2>
</ROW>

For this, all you need to do is add a name attribute to your existing element:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "ROW")
public class Row {

    @XmlAttribute
    private int id;

    @XmlAttribute(name = "name", required=true)
    private String name;

    @XmlElement(name = "MOBILE")
    private int mobileNo;

    @XmlMixed
    @XmlAnyElement
    @XmlJavaTypeAdapter(MyMapAdapter.class)
    private Map<String, String> otherElements;
}
Inexcusable answered 18/11, 2019 at 20:44 Comment(3)
I am consuming XML from a client, so can't change the XML structure. I tried removing XmlRootElement from Row and unmarshalling to JAXBElement by : JAXBElement<Row> element = unmarshaller.unmarshal(new StreamSource(xml), Row.class); Row root = element.getValue(); . This gives the populating of all fields and consuming XMLs with different root elements.Strainer
@SiddharthTrikha in that case, you need multiple base classes to represent your root element which will be a mess and most likely won't scale up well. Your root node cannot be xs:any. I tried this with XML schema as well. One you set your root element to be basically anything, your schema breaks. In other words, xs:any can only be used in the context of a child node. Lastly, you need to go to your client and show them why this is a bad implementation.Inexcusable
@SiddharthTrikha I understand that removing XmlRootElement from Row will work when unmarshaling. My question to you is, how would you marshal it if you have to? You can do it with other mechanisms, but not with JAXB. The disadvantage of this approach is that you will then need to write your own validators. Doable, but you lose the native capabilities of JAXB to do so.Inexcusable

© 2022 - 2024 — McMap. All rights reserved.