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 ?
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}]
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With