0

Sample of the xml being parsed

<llsd>
    <map>
    <key>agents</key>
        <map>
        <key>0008c41e-3298-449c-8abd-4929a0eeae0e</key>
            <map>
            <key>display_name</key>
                <string>kettelynin</string>
            <key>display_name_expires</key>
                <date>2020-06-24T23:53:44.05Z</date>
            <key>display_name_next_update</key>
                <date>1970-01-01T00:00:00Z</date>
            <key>is_display_name_default</key>
                <boolean>1</boolean>
            <key>legacy_first_name</key>
                <string>kettelynin</string>
            <key>legacy_last_name</key>
                <string>Resident</string>
            <key>username</key>
                <string>username1</string>
            </map>
        <key>000bd88c-562f-423e-af63-55b9f0b17e10</key>
            <map>
            <key>display_name</key>
                <string>ϯ Mary Baker Pitbull Darkϯ  </string>
            <key>display_name_expires</key>
                <date>2020-06-25T00:24:25.63Z</date>
            <key>display_name_next_update</key>
                <date>1970-01-01T00:00:00Z</date>
            <key>is_display_name_default</key>
                <boolean>0</boolean>
            <key>legacy_first_name</key>
                <string>maryunasilva</string>
            <key>legacy_last_name</key>
                <string>Resident</string>
            <key>username</key>
                <string>username2</string>
            </map>
        </map>
    </map>
</llsd>

As I understand it, the structure is some nested maps with String keys and with various Objects as the final values. So, I have this class.

import javax.xml.bind.annotation.XmlRootElement;
import java.util.Map;

@XmlRootElement
public class llsd {
    Map<String, Map<String, Object>> agents;

    llsd() {
    }

    public Map<String, Map<String, Object>> getAgents() {
        return agents;
    }
}

I'm running the following code snippet to test it

JAXBContext newInstance = JAXBContext.newInstance(llsd.class);
Unmarshaller unmarshall = newInstance.createUnmarshaller();
llsd newParsed = (llsd) unmarshall.unmarshal(new File("C:/path/to/my.xml"));

I don't get any errors while running it, but when I run the debugger, I can see that the newParsed value for agents is null. Clearly I've got my binding set up wrong, but I can't figure out why.

ssteph
  • 89
  • 1
  • 9

1 Answers1

0

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.

andrewJames
  • 19,570
  • 8
  • 19
  • 51