Using JAXB to cross reference XmlIDs from two XML files

I'm trying to marshal/unmarshal from two different XML files to POJOS. The first XML file looks like this:

--Network.xml--
<Network>
  <Nodes>
    <Node id="ROD" />
    <Node id="KFI" />
    <Node id="JND" />
  </Nodes>
  <Arcs>
    <Arc fromNode="ROD" />
    <Arc fromNode="JND" />
  </Arcs>
</Network>
---------

Using @XmlID and @XmlIDREF annotations, I can successfully populate the Arc classes to point to the correct Node which it references.

However, I also have to parse this XML:

--NetworkInputs.xml--
<NetworkInputs>
  <Flows>
    <Flow toNode="JND" />
    <Flow toNode="ROD" />
  </Flows>
</NetworkInputs>
------

Currently, my program unmarshals the Network object successfully, but there's no connection between Network and NetworkInputs that allows JAXB to "see" the nodes that exist in Network. I want my Flow objects to point to the correct Node in the Network class.

I basically want to do this: http://old.nabble.com/JAXB-Unmarshalling-and-XmlIDREF-using-different-stores-td14035248.html

I tried implementing this: http://weblogs.java.net/blog/kohsuke/archive/2005/08/pluggable_ididr.html and it just doesn't work, because I can't get the Node data for my populated Network from a static context.

Is it even possible to do something like this?


Solution 1:

This can be done with an XmlAdapter. The trick is the XmlAdapter will need to be initialized with all of the Nodes from Network.xml and passed to the Unmarshaller used with NetworkInputs.xml:

import java.io.File;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Network.class, NetworkInputs.class);

        File networkXML = new File("Network.xml");
        Unmarshaller unmarshaller = jc.createUnmarshaller();
        Network network = (Network) unmarshaller.unmarshal(networkXML);

        File networkInputsXML = new File("NetworkInputs.xml");
        Unmarshaller unmarshaller2 = jc.createUnmarshaller();
        NodeAdapter nodeAdapter = new NodeAdapter();
        for(Node node : network.getNodes()) {
            nodeAdapter.getNodes().put(node.getId(), node);
        }
        unmarshaller2.setAdapter(nodeAdapter);
        NetworkInputs networkInputs = (NetworkInputs) unmarshaller2.unmarshal(networkInputsXML);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(networkInputs, System.out);
    }
}

The trick is to map the toNode property on Flow with an XmlAdapter:

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

public class Flow {

    private Node toNode;

    @XmlAttribute
    @XmlJavaTypeAdapter(NodeAdapter.class)
    public Node getToNode() {
        return toNode;
    }

    public void setToNode(Node toNode) {
        this.toNode = toNode;
    }

}

The adapter will look like the following. The trick is that we will pass a configured XmlAdapter that knows about all the Nodes to the unmarshaller:

import java.util.HashMap;
import java.util.Map;

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class NodeAdapter extends XmlAdapter<String, Node>{

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

    public Map<String, Node> getNodes() {
        return nodes;
    }

    @Override
    public Node unmarshal(String v) throws Exception {
        return nodes.get(v);
    }

    @Override
    public String marshal(Node v) throws Exception {
        return v.getId();
    }

}

Solution 2:

My solution: the ID resolution is handled by an (unfortunately) internal class (com.sun.xml.internal.bind.IDResolver ) that can be set from external.

final Unmarshaller unmarshaller = context.createUnmarshaller();
unmarshaller.setProperty(IDResolver.class.getName(), resolver);

Where the resolver could be used over many inststance of the unmarshaller. But the point is that the resolver will not clear itsself on startDocument as the default implementation of com.sun.xml.internal.bind.v2.runtime.unmarshaller.DefaultIDResolver does:

import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;

import org.xml.sax.SAXException;

import com.sun.xml.internal.bind.IDResolver;

public final class IDResolverExtension extends IDResolver {
    public static final class CallableImplementation implements Callable<Object> {
        private final Object value;

        private CallableImplementation(final Object value) {
            this.value = value;
        }

        @Override
        public Object call() {
            return value;
        }
    }

    private final Map<KeyAndClass, Object> m = new HashMap<KeyAndClass, Object>();

    @SuppressWarnings("rawtypes")
    @Override
    public synchronized CallableImplementation resolve(final String key0, final Class clazz) throws SAXException {
        assert clazz != null;
        assert key0 != null;
        final KeyAndClass key = new KeyAndClass(clazz, key0);
        final Object value = m.get(key);
        return new CallableImplementation(value);
    }

    static class KeyAndClass {
        public final Class<?> clazz;
        public final String key;

        public KeyAndClass(final Class<?> clazz, final String key) {
            this.clazz = clazz;
            this.key = key;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + clazz.hashCode();
            result = prime * result + key.hashCode();
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final KeyAndClass other = (KeyAndClass) obj;
            if (!clazz.equals(other.clazz)) {
                return false;
            }
            if (!key.equals(other.key)) {
                return false;
            }
            return true;
        }

    }

    @Override
    public synchronized void bind(final String key0, final Object value) throws SAXException {
        assert key0 != null;
        assert value != null;
        Class<? extends Object> clazz = value.getClass();
        assert clazz != null;
        final KeyAndClass key = new KeyAndClass(clazz, key0);
        final Object oldValue = m.put(key, value);
        if (oldValue != null) {
            final String message = MessageFormat.format("duplicated key ''{0}'' => ''{1}'' - old: ''{2}''", key, value,
                    oldValue);
            throw new AssertionError(message);
        }
    }
}