Using Jaxb to Cross Reference Xmlids from Two Xml Files

Using JAXB to cross reference XmlIDs from two XML files

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();
}

}

jaxb reference xmlID between xml files

The problem arises from class Company that corresponds to an XML document containing employees and departments. However, you've got two separate documents. Apparently you want one final class containing both lists.

(1) You could define a class EmployeeList for employees only, similar to the one for departments (DepartmentList). This will still let you write an application class Company into which you set the references for both lists.

(2) Change the annotation for Company.departments

@XmlTransient
private List<Department> departments;

marshal like you do now, and set the List with the reference you have from unmarshalling the corresponding XML into the returned object.

How to force JAXB marshaller to use xlink references?

You can implement an XLink approach in JAXB using an XmlAdapter. Below are links to various similar answers.

  • Serialize a JAXB object via its ID?
  • Using JAXB to cross reference XmlIDs from two XML files
  • Can JAXB marshal by containment at first then marshal by @XmlIDREF for subsequent references?

I lead the EclipseLink JAXB (MOXy) implementation, and we have the @XmlInverseReference extension for mapping bidirectional relationship that you may be interested in:

  • http://blog.bdoughan.com/2010/07/jpa-entities-to-xml-bidirectional.html

JaxB reference resolving

To better illustrate the point I have modified the question to fit his answer. There is now a generic base class Person and I am trying to use it as per Can generic XmlAdapter be written

I solved the issue of being able to actually make sure the Adapters are used by writing specific derived Classes and using them with @XmlJavaTypeAdapter. I preregister the adapters using:

JAXBContext context = JAXBContext.newInstance(type);
Unmarshaller u = context.createUnmarshaller();
u.setAdapter(Worker.WorkerAdapter.class,new Worker.WorkerAdapter());
u.setAdapter(Trainer.TrainerAdapter.class,new Trainer.TrainerAdapter());

and then unmarshalling twice. The debug shows that the Adapter instance for both passes is the same. Still the lookup somehow seemed to fail ... The reason was the way the @XmlJavaTypeAdapter annotation works see:

What package-info do I annotate with XmlJavaTypeAdapters?

There seem to be multiple modes for @XmlJavaTypeAdapter:

  • it can be an annotation for a class
  • it can be an annotation for a field (getter)
  • it can be used in a package-info.java file to annotate a whole package

At this point I am using all three annotations and now have to debug which ones are necessary. I assume the global annotations (class,package) are not working as expected. The reason might be the type= usage in the @XmlElementWrapper which explicitly calls for a type. Personally I do not understand what is going on yet. At least things are now working as expected.

the local field annotation is now e.g.:

@XmlElementWrapper(name="workers")
@XmlElement(name="Worker", type=Worker.class)
@XmlJavaTypeAdapter(WorkerAdapter.class)

the package-info.java annotation is:

@XmlJavaTypeAdapters({
@XmlJavaTypeAdapter(value=WorkerAdapter.class,type=Worker.class),
@XmlJavaTypeAdapter(value=TrainerAdapter.class,type=Trainer.class),
})
package com.bitplan.jaxb.refidtest;

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

the class annotation is:

@XmlJavaTypeAdapter(Worker.WorkerAdapter.class)
public class Worker extends Person {

...

/**
* Worker Adapter
* @author wf
*
*/
public static class WorkerAdapter extends PersonAdapter<Worker>{
@Override
public Worker marshal(Worker me)
throws Exception {
return super.marshal(me);
}

@Override
public Worker unmarshal(Worker me) throws Exception {
return super.unmarshal(me);
}
}

/**
* https://stackoverflow.com/questions/7587095/can-jaxb-marshal-by-containment-at-first-then-marshal-by-xmlidref-for-subsequen/7587727#7587727
* @author wf
*
*/
public class PersonAdapter<T extends Person> extends XmlAdapter<T, T>{

public boolean debug=true;

/**
* keep track of the elements already seen
*/
public Map<String,T> lookup=new HashMap<String,T>();

@Override
public T marshal(T me)
throws Exception {
return me;
}

/**
* show debug information
* @param title
* @param key
* @param me
* @param found
*/
public void showDebug(String title,String key,T me, T found) {
String deref="?";
if (found!=null)
deref="->"+found.getId()+"("+found.getClass().getSimpleName()+")";
if (debug)
System.err.println(title+": "+key+"("+me.getClass().getSimpleName()+")"+deref+" - "+this);
}

@Override
public T unmarshal(T me) throws Exception {
if (me.getId()!=null) {
showDebug("id",me.getId(),me,null);
lookup.put(me.getId(), me);
return me;
}
if (me.getRef()!=null) {
if (lookup.containsKey(me.getRef())) {
T meRef=lookup.get(me.getRef());
showDebug("ref",me.getRef(),me,meRef);
me.setRef(null);
return meRef;
} else {
if (debug)
showDebug("ref",me.getRef(),me,null);
}
}
return me;
}

}


Related Topics



Leave a reply



Submit