Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I modify the JAXB marshalling output stream to include arbitrary inline XML?

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.

like image 537
Gary Rowe Avatar asked Nov 19 '10 11:11

Gary Rowe


2 Answers

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.

like image 166
bdoughan Avatar answered Oct 21 '22 23:10

bdoughan


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.

like image 23
Gary Rowe Avatar answered Oct 21 '22 22:10

Gary Rowe