I'd like to modify the output stream for a JAXB marshalling operation to include some arbitrary XML. Here's an example to clarify the situation.
I have an arbitrary Product
domain object with JAXB annotations that currently look like this:
@XmlRootElement(name="Product")
public class Product {
@XmlElement(name="CommonProperty")
private String commonProperty="Something";
@XmlElement(name="ExtraXml")
private String extraXml="Something extra";
}
Which would typically marshal into this:
<Product>
<CommonProperty>Something</CommonProperty>
<ExtraXml>Something else</ExtraXml>
</Product>
Now, what if the extraXml
field contained some additional XML (of arbitrary complexity) that was to be included inline with the final marshalled result?
Say, extraXml
contained "<abc><def>Something extra</def></abc>
", I would really like a solution that enabled me to marshal Product
like this (formatting optional):
<Product>
<CommonProperty>Something</CommonProperty>
<abc>
<def>Something extra</def>
</abc>
</Product>
I've looked at this related question but it didn't quite yield the result I'm after since it seems more geared towards an overall format change rather than DOM insertion.
The extraXml
property is just there for illustration, it could be marked as @XmlTransient
or into a single specialised class. The only criteria is that it can somehow get a String
containing completely arbitrary XML content for appending to the individual Product
marshalled output.
I should also mention that the consumers of the output from this are able to parse the arbitrary content in a manner that suits them. The purpose here is to simplify processing on the server side.
Thanks in advance for any help you can offer.
You can represent the extra XML as a DOM node instead of a String.
Change the extraXML property to be an org.w3c.dom.Node instead of a String, and annotate it with @XmlAnyElement.
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import org.w3c.dom.Node;
@XmlRootElement(name="Product")
@XmlAccessorType(XmlAccessType.FIELD)
public class Product {
@XmlElement(name="CommonProperty")
private String commonProperty="Something";
@XmlAnyElement
private Node extraXml;
}
Then when you umarshal an XML document such as:
<Product>
<CommonProperty>Something</CommonProperty>
<abc>
<def>Something extra</def>
</abc>
</Product>
With the following code:
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(Product.class);
Unmarshaller unmarshaller = jc.createUnmarshaller();
File xml = new File("input.xml");
Product product = (Product) unmarshaller.unmarshal(xml);
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(product, System.out);
}
}
The "extra XML" will be kept as a DOM node.
With Blaise Doughan's help here is the final solution that I settled on. This includes some extra demonstration code to help others who may find themselves in this situation.
The classes
ProductList (a new wrapping class to show how this works for multiple Product entries)
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.ArrayList;
import java.util.List;
@XmlRootElement(name="ProductList")
public class ProductList {
@XmlElementWrapper(name="Products")
@XmlElement(name="Product")
public List<Product> products = new ArrayList<Product>();
}
Product (with Blaise's modifications)
import org.w3c.dom.Node;
import javax.xml.bind.annotation.*;
@XmlRootElement(name="Product")
@XmlAccessorType(XmlAccessType.FIELD)
public class Product {
@XmlElement(name="CommonProperty")
public String commonProperty="Something";
@XmlAnyElement
public Node extraXml;
}
Main (some demonstration code)
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;
public class Main {
public static void main(String[] args) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// Build some arbitrary extra XML and prepare an InputStream
String fragment1 = "<abc><def>Some extra 1</def></abc>";
String fragment2 = "<ghi><jkl>Some extra 2</jkl></ghi>";
Document document1 = factory.newDocumentBuilder().parse(new InputSource(new StringReader(fragment1)));
Document document2 = factory.newDocumentBuilder().parse(new InputSource(new StringReader(fragment2)));
Product product1 = new Product();
product1.commonProperty = "Hello 1";
product1.extraXml=document1.getFirstChild();
Product product2 = new Product();
product2.commonProperty = "Hello 2";
product2.extraXml=document2.getFirstChild();
ProductList productList = new ProductList();
productList.products.add(product1);
productList.products.add(product2);
JAXBContext jc = JAXBContext.newInstance(ProductList.class, Product.class);
Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(productList, System.out);
}
}
The final output:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ProductList>
<Products>
<Product>
<CommonProperty>Hello 1</CommonProperty>
<abc>
<def>Some extra 1</def>
</abc>
</Product>
<Product>
<CommonProperty>Hello 2</CommonProperty>
<ghi>
<jkl>Some extra 2</jkl>
</ghi>
</Product>
</Products>
</ProductList>
Result!
But it doesn't work in JBoss...
If you try the above in JBoss 4.2.3.GA or 4.3 you may find that you get an
class com.sun.org.apache.xerces.internal.dom.DocumentFragmentImpl nor any of its super class is known to this context.
exception being reported. This is (probably) due to the xercesImpl.jar
in the JBoss lib
and /lib/endorsed
folders using the Java META-INF/Services
override facility to prevent the JDK from marshalling using internal classes. You may need to specify an alternative DocumentBuilderFactory
directly using the following approach:
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance("oracle.xml.jaxp.JXDocumentBuilderFactory", this
.getClass().getClassLoader());
The Oracle implementation seems to alleviate these issues, perhaps because it keeps awareness of the DOM Node classes within the JAXB context. These classes are found in the xdb.jar and xmlparserv2.jar shipped with the Oracle client.
Hope it helps.
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