The structure of your XML has more nesting levels than accounted for by Map<String, Map<String, Object>>
. You can take a look at some approaches suggested in other JAXB-related questions, such as here and here for general approaches.
If you are not familiar with xjc
that can also help you to build the objects needed by JAXB - see notes here.
However, for a structure such as the specific one in your question, I would consider a different (non-JAXB) approach. One of the reasons is because your data has ordered pairs of tags, such as:
<key>display_name_expires</key>
<date>2020-06-25T00:24:25.63Z</date>
You have the field name in one tag, and then a related data type and value in the following tag. I don't know any clean way to handle this in JAXB (there may be a way, of course).
My alternative is to use Java's xPath classes to parse the XML in a targeted way, and to build a list of Agent
data beans. It's a manual process, but fairly straightforward.
The Agent
Bean:
import java.util.UUID;
import java.time.Instant;
public class Agent {
private UUID id;
private String displayName;
private Instant displayNameExpires;
private Instant displayNameNextUpdate;
private boolean isDisplayNameDefault;
private String legacyFirstName;
private String legacyLastName;
private String userName;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public Instant getDisplayNameExpires() {
return displayNameExpires;
}
public void setDisplayNameExpires(String displayNameExpires) {
this.displayNameExpires = Instant.parse(displayNameExpires);
}
public Instant getDisplayNameNextUpdate() {
return displayNameNextUpdate;
}
public void setDisplayNameNextUpdate(String displayNameNextUpdate) {
this.displayNameNextUpdate = Instant.parse(displayNameNextUpdate);
}
public boolean getIsDisplayNameDefault() {
return isDisplayNameDefault;
}
public void setIsDisplayNameDefault(String isDefault) {
this.isDisplayNameDefault = isDefault.equals("1") ? Boolean.TRUE
: Boolean.FALSE;
}
public String getLegacyFirstName() {
return legacyFirstName;
}
public void setLegacyFirstName(String legacyFirstName) {
this.legacyFirstName = legacyFirstName;
}
public String getLegacyLastName() {
return legacyLastName;
}
public void setLegacyLastName(String legacyLastName) {
this.legacyLastName = legacyLastName;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
In the above class, the only thing to note is the inclusion of data conversions in the relevant getters, from String
to other types, where needed (instants and booleans).
The XML Processor:
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.xml.sax.SAXException;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
public class AgentReader {
private final List<Agent> agents = new ArrayList();
public void processAgents() throws FileNotFoundException, ParserConfigurationException,
SAXException, IOException, XPathExpressionException {
FileInputStream fis = new FileInputStream(new File("/path/to/llsd.xml"));
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = builderFactory.newDocumentBuilder();
Document xmlDocument = builder.parse(fis);
xmlDocument.normalizeDocument();
XPathExpression idPath = XPathFactory.newInstance().newXPath()
.compile("/llsd/map/map/key");
XPathExpression attrMapPath = XPathFactory.newInstance().newXPath()
.compile("/llsd/map/map/map");
// This is relative to the attrMapPath defined above:
XPathExpression attrsPath = XPathFactory.newInstance().newXPath()
.compile("./*");
NodeList idNodes = (NodeList) idPath.evaluate(xmlDocument, XPathConstants.NODESET);
NodeList attrLists = (NodeList) attrMapPath.evaluate(xmlDocument, XPathConstants.NODESET);
for (int i = 0; i < idNodes.getLength(); i++) {
Node idNode = idNodes.item(i);
Node attrList = attrLists.item(i);
agents.add(buildAgent(idNode, attrList, attrsPath));
}
}
private Agent buildAgent(Node idNode, Node attrList, XPathExpression attrsPath)
throws XPathExpressionException {
Agent agent = new Agent();
String id = idNode.getTextContent();
NodeList attrNodes = (NodeList) attrsPath.evaluate(attrList, XPathConstants.NODESET);
String fieldName = null;
for (int i = 0; i < attrNodes.getLength(); i++) {
// The data comes in pairs of tags - a field name and a related value:
if (i % 2 == 0) {
fieldName = attrNodes.item(i).getTextContent();
} else {
String value = attrNodes.item(i).getTextContent();
agent = addValue(fieldName, value, agent);
}
}
return agent;
}
private Agent addValue(String fieldName, String value, Agent agent) {
switch (fieldName) {
case "display_name":
agent.setDisplayName(value);
break;
case "display_name_expires":
agent.setDisplayNameExpires(value);
break;
case "display_name_next_update":
agent.setDisplayNameNextUpdate(value);
break;
case "is_display_name_default":
agent.setIsDisplayNameDefault(value);
break;
case "legacy_first_name":
agent.setLegacyFirstName(value);
break;
case "legacy_last_name":
agent.setLegacyLastName(value);
break;
case "username":
agent.setUserName(value);
break;
}
return agent;
}
}
In processAgents()
, the xPaths allow us to ignore the outer nesting tags, and go directly to the data.
Because these tags are at the same level, we use 2 xPaths in parallel - one for the set of <key>
tags, and one for the related set of <map>
tags:
<key>0008c41e-3298-449c-8abd-4929a0eeae0e</key>
<map>...</map>
The related xPath selectors are /llsd/map/map/key
and /llsd/map/map/map
.
The assumption here is that there is always the same number of each tag (two of each in your sample). You could add some defensive coding to check this.
The buildAgent()
method handles each set of data for one agent. It uses the switch statement in addValue()
to call the relevant setter in the Agent
bean.
The end result from your sample data is List<Agent>
containing 2 objects.