XOP Converter
My most recent task at work involves XOP, or XML-binary Optimized Packaging. Basically, if you have an XML document that needs to have binary data in it, like images, you have two ways of doing it. You can base64 encode the data and put it directly in an XML element, or you can use XOP to keep the data in binary form and attach the file to the message using MIME. XOP is usually used to represent SOAP messages with MTOM (Message Transmission Optimization Mechanism).
My app uses JiBX to convert my Java objects into XML documents, and base64 encodes them. But I got a new requirement to also support XOP encoding. So to minimize impact on the app, I decided to write a converter that would take in a XML document with base64 encoded data and spit out a properly encoded XOP document. Then it would also be useful to convert existing documents and creating new ones. The module that helped me do this was Apache Axiom, a component of the Apache Axis2 web services engine.
The main problem with Axiom is its poor documentation. Almost all of the docs I could find about Axiom and Axis2 were from a server-side perspective, and I am writing a client-side app. So I thought I’d share the bit of code I wrote to do this conversion.
public class XOPConverter { @SuppressWarnings("rawtypes") public static void convertToXOP(String xml, OutputStream os) throws XMLStreamException, JaxenException, JiBXException, IOException { // set up all the AXIOM objects XMLInputFactory factory = XMLInputFactory.newInstance(); XMLStreamReader parser = factory.createXMLStreamReader(new StringReader(xml)); StAXOMBuilder builder = new StAXOMBuilder(parser); OMDocument document = builder.getDocument(); OMElement documentElement = builder.getDocumentElement(); OMFactory fac = OMAbstractFactory.getOMFactory(); // find all the BinaryBase64Object nodes AXIOMXPath xpathExpression = new AXIOMXPath("//niem-nc:BinaryBase64Object"); xpathExpression.addNamespace("niem-nc", "http://niem.gov/niem/niem-core/2.0"); List nodeList = xpathExpression.selectNodes(documentElement); for (Iterator i = nodeList.iterator(); i.hasNext(); ) { Object obj = i.next(); if (!(obj instanceof OMElement)) { continue; } OMElement element = (OMElement)obj; // get bytes data out of that node String base64data = element.getText(); // decode base 64 encoding byte[] data = Base64Serializer.deserializeBase64(base64data); // create ByteArrayDataSource ByteArrayDataSource ds = new ByteArrayDataSource(data); // set the MIME type ds.setType("image/" + XOPConverter.getMIMEType(data)); // create OMText object OMText textElement = fac.createOMText(new DataHandler(ds), true); // replace BinaryBase64Object node text with OMText object element.setText(""); element.addChild(textElement); } // do the XOP conversion OMOutputFormat format = new OMOutputFormat(); format.setDoOptimize(true); format.setMimeBoundary("MIME"); format.setContentType("Multipart/Related"); MTOMXMLStreamWriter mtomWriter = new MTOMXMLStreamWriter(os, format); mtomWriter.setDoOptimize(true); document.serializeAndConsume(mtomWriter); document.close(false); } /** * @param args * @throws IOException * @throws XMLStreamException * @throws JaxenException * @throws JiBXException */ public static void main(String[] args) throws IOException, XMLStreamException, JaxenException, JiBXException { if (args.length == 0) { System.out.println("Usage: <filename>"); return; } String inputFileName = args[0]; String outputFileName = inputFileName.substring(0, inputFileName.lastIndexOf(".")) + ".xop"; boolean overwrite = false; if (args.length > 1) { String overwriteStr = args[1]; overwrite = Boolean.parseBoolean(overwriteStr); } // check if output file exists if (new java.io.File(outputFileName).exists() && !overwrite) { System.out.println("ERROR: Output file " + outputFileName + " exists, please move it."); return; } java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(args[0])); String line = null; StringBuilder stringBuilder = new StringBuilder(); String ls = System.getProperty("line.separator"); while ((line = reader.readLine()) != null) { stringBuilder.append(line); stringBuilder.append(ls); } String xml = stringBuilder.toString(); FileOutputStream os = new FileOutputStream(outputFileName); convertToXOP(xml, os); System.out.println("done"); } }
Now I’ll step through the code and explain it a bit.
The convertToXOP method takes the XML text as a String, and an OutputStream where the XOP is written. In my original document, all the base64 encoded objects I need to extract and attach are in elements labeled . So I cook up an XPath expression to find all those elements for processing. The code gets the data out of the element and un-base64 encodes it. I didn’t write that code, so you’ll have to do that yourself. Then we put the byte array in a ByteArrayDataSource and tag it with the MIME type we want to use. In a later post I’ll show how to figure out what encoding an image uses. Then use the OMFactory to create a new OMText element containing the byte array and set it to be optimized. Add it to the original element and reset its text, clearing out the base64 encoded data.
The key to doing the conversion is Axiom’s MTOMXMLStreamWriter class. You send the MTOMXMLStreamWriter the OutputStream where you want the XOP document to go, and an OMOutputFormat object. Setting the doOptimize flag on the OMOutputFormat object enables the optimization step that moves the binary objects to MIME attachments. Sending the MTOMXMLStreamWriter to the OMDocument’s serializeAndConsume method writes the XOP output.
I hope this code and explanation has been helpful to you.
I’ve been following your blog since you started. You have made amazing progress. This site is an inspiration for all pursuing a long transition versus the big chop.
– Rob
Excellent ! Thanks for sharing!