Project

Profile

Help

JSON-XML round trip using java API

Added by Rob Brown almost 2 years ago

I'm implementing some logic from XProc and want to round trip between JSON and XML using the java API only (not from within a stylesheet). I've got half of the task working, but the trip back causes an exception.

I've pasted a test class below. It was adapted from JsonToXMLFn and XMLToJsonFn and I've commented out some code related to options.

Going from JSON to XML works as expected, but converting XML to JSON causes an IllegalStateException in JsonReceiver.

Can someone help get this working? Any advice on how to improve the code in general would also be appreciated.

package saxon.demo;

import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger;

import javax.xml.transform.dom.DOMSource;

import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node;

import net.sf.saxon.Configuration; import net.sf.saxon.dom.NodeOverNodeInfo; import net.sf.saxon.event.PipelineConfiguration; import net.sf.saxon.expr.XPathContextMajor; import net.sf.saxon.expr.instruct.Executable; import net.sf.saxon.expr.parser.ExplicitLocation; import net.sf.saxon.ma.json.JsonHandlerXML; import net.sf.saxon.ma.json.JsonParser; import net.sf.saxon.ma.json.JsonReceiver; import net.sf.saxon.om.Item; import net.sf.saxon.s9api.DocumentBuilder; import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.Serializer; import net.sf.saxon.s9api.XdmNode; import net.sf.saxon.trans.XPathException; import net.sf.saxon.tree.tiny.TinyDocumentImpl;

public class JsonXmlRoundTrip_main {

public static void main(String[] args) {
	
	Logger logger = Logger.getAnonymousLogger();
	
	String json = "{\"foo\":\"bar\"}";

	Element convertedElement = convertJsonToXml(json, logger);
	if (convertedElement == null) {
		return;
	}
	String markup = serializeToString(convertedElement, logger);
	System.out.println("convertedElement:");
	System.out.println(markup);

	String convertedJson = convertXmlToJson(convertedElement, logger);
	if (convertedJson == null) {
		return;
	}
	System.out.println("convertedJson:");
	System.out.println(convertedJson);

	System.out.println("compare json:");
	System.out.println(json.equals(convertedJson));
}

private static Element convertJsonToXml(String json, Logger logger) {
	
	//adapted from saxon JsonToXMLFn
	
    JsonParser parser = new JsonParser();
    int flags = 0;

// Map<String, Sequence> checkedOptions = null; // if (options != null) { // checkedOptions = getDetails().optionDetails.processSuppliedOptions(options, context); // flags = JsonParser.getFlags(checkedOptions, true, context.getController().getExecutable().isSchemaAware()); // if ((flags & JsonParser.DUPLICATES_LAST) != 0) { // throw new XPathException("json-to-xml: duplicates=use-last is not allowed", "FOJS0005"); // } // if ((flags & JsonParser.DUPLICATES_SPECIFIED) == 0) { // if ((flags & JsonParser.VALIDATE) != 0) { // flags |= JsonParser.DUPLICATES_REJECTED; // } else { // flags |= JsonParser.DUPLICATES_RETAINED; // } // } // } else { flags = JsonParser.DUPLICATES_RETAINED; // }

	Processor proc = new Processor(false);
	XPathContextMajor context = createDisposableXpathContext(proc);
    String baseUri = null;
	try {
		JsonHandlerXML handler = new JsonHandlerXML(context, baseUri, flags);

// if (options != null) { // handler.setFallbackFunction(checkedOptions, context); // } parser.parse(json, flags, handler, context); Item<?> resultItem = handler.getResult(); if (resultItem instanceof TinyDocumentImpl) { Node domNode = convertTinyDocToDom((TinyDocumentImpl)resultItem); if (domNode instanceof Document) { return ((Document)domNode).getDocumentElement(); } else if (domNode instanceof Element) { return (Element)domNode; } } logger.severe("unable to convert JSON to XML"); return null; } catch (XPathException e) { logger.log(Level.SEVERE, "an error occurred while converting JSON to XML", e); return null; } }

private static String convertXmlToJson(Node node, Logger logger) {
	
	//adapted from saxon XMLToJsonFn

	Processor proc = new Processor(false);
	XdmNode item = proc.newDocumentBuilder().wrap(node);
	XPathContextMajor context = createDisposableXpathContext(proc);
	PipelineConfiguration pipe = context.getController().makePipelineConfiguration();
    pipe.setXPathContext(context);
	JsonReceiver receiver = new JsonReceiver(pipe);
    receiver.setIndenting(true);

// if (options.numberFormatter != null) { // receiver.setNumberFormatter(options.numberFormatter); // } // Receiver r = receiver; // if (xml.getNodeKind() == Type.DOCUMENT) { // r = new DocumentValidator(r, "FOJS0006"); // }

    try {
    	receiver.open();
    	
    	//this causes IllegalStateException in JsonReceiver
		item.getUnderlyingNode().copy(
				receiver, 
				0, 
				new ExplicitLocation(null, -1, -1));
		
		receiver.close();
		return receiver.getJsonString();
	} catch (XPathException e) {
		logger.log(Level.SEVERE, "unable to convert XML to JSON", e);
		return null;
	}
}

private static XPathContextMajor createDisposableXpathContext(Processor proc) {
	
	Executable exec = new Executable(proc.getUnderlyingConfiguration());
	return new XPathContextMajor(null, exec);
}

private static String serializeToString(
		Node node,
		Logger logger) {
	
	Configuration config = new Configuration();
	Processor processor = new Processor(config);
	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	Serializer serializer = processor.newSerializer(baos);
	boolean success = serialize(
			node, 
			processor.newDocumentBuilder(),
			serializer, 
			logger);
	if (success) {
		return new String(baos.toByteArray(), StandardCharsets.UTF_8);
	} else {
		return null;
	}
}

private static boolean serialize(
		Node node,
		DocumentBuilder docBldr,
		Serializer serializer,
		Logger logger) {
	
	try {
		XdmNode xdmNode = docBldr.build(new DOMSource(node));
		serializer.setOutputProperty(Serializer.Property.INDENT, "yes");
		serializer.setOutputProperty(Serializer.Property.OMIT_XML_DECLARATION, "yes");
		serializer.setOutputProperty(Serializer.Property.ENCODING, "UTF-8");
		serializer.serializeNode(xdmNode);
		return true;
	} catch (SaxonApiException e) {
		logger.log(Level.SEVERE, "an error occurred while saving data", e);
		return false;
	}
}

private static Node convertTinyDocToDom(TinyDocumentImpl tinyDoc) {
	
	//saxon no longer implements w3 dom in TinyTree, so NodeOverNodeInfo
	//provides a read-only w3 dom api over tiny node
	//https://www.saxonica.com/documentation9.5/changes/v8.3/external-object-models.html
	return NodeOverNodeInfo.wrap(tinyDoc.getRootNode());
}

}


Replies (7)

Please register to reply

RE: JSON-XML round trip using java API - Added by Rob Brown almost 2 years ago

Sorry, I should have attached the class as a file which I've done here.

RE: JSON-XML round trip using java API - Added by Martin Honnen almost 2 years ago

Just to make sure, your code is attempting some JSON <--> XML conversion different from what the W3C json-to-xml and xml-to-json functions provide? Or could you just call those functions (you don't need XSLT for that, they are callable from XPath or even accessible directly using XdmFunctionItem.getSystemFunction)?

RE: JSON-XML round trip using java API - Added by Rob Brown almost 2 years ago

Great idea, except I want to implement using Saxon-HE so I can freely distribute.

When I try using XdmFunctionItem.getSystemFunction() I get:

Dynamic functions require Saxon-PE or higher

RE: JSON-XML round trip using java API - Added by Martin Honnen almost 2 years ago

The current versions of Saxon are 10 and 11 where I think that higher-order/dynamic function are supported by HE. Is there any need to use an earlier version?

RE: JSON-XML round trip using java API - Added by Martin Honnen almost 2 years ago

For Saxon 10, I think, as an oversight, the feature (XdmFunctionItem.getSystemFunction) was initially not enabled but that got fixed as https://saxonica.plan.io/issues/4775 in 10.3.

RE: JSON-XML round trip using java API - Added by Rob Brown almost 2 years ago

I confirmed that getSystemFunction() works in 10.3 and 11.3. Unfortunately, my code is stuck on 9.9 for the time being.

I was also able to get it working in 9.9 using the XPath API, so I'm happy.

Thanks very much for the tips!

RE: JSON-XML round trip using java API - Added by Michael Kay almost 2 years ago

Although we make many internal interfaces like JsonParser public (and write Javadoc for them), they aren't really designed for external use and they typically aren't robust enough to handle all possible usage scenarios. If you use such interfaces, you need to be prepared to study the source code and debug any failures you get.

As Martin suggests, it's better to go in via public APIs: either by executing an XPath expression, or by getSystemFunction(). I suspect that in 9.9 you can achieve the equivalent of getSystemFunction() by executing an XPath expression such as fn:parse-json#1.

    (1-7/7)

    Please register to reply