package org.exist.util.serializer.json; import java.io.IOException; import java.io.Writer; import java.util.Properties; import java.util.Stack; import javax.xml.transform.TransformerException; import org.exist.dom.QName; import org.exist.storage.serializers.EXistOutputKeys; import org.exist.util.serializer.XMLWriter; /** * This class plugs into eXist's serialization to transform XML to JSON. It is used * if the serialization property "method" is set to "json". * * The following rules apply for the mapping of XML to JSON: * * <ul> * <li>The root element will be absorbed, i.e. <root>text</root> becomes "root".</li> * <li>Sibling elements with the same name are added to an array.</li> * <li>If an element has attribute and text content, the text content becomes a * property, e.g. '#text': 'my text'.</li> * <li>In mixed content nodes, text nodes will be dropped.</li> * <li>An empty element becomes 'null', i.e. <e/> becomes {"e": null}.</li> * <li>An element with a single text child becomes a property with the value of the text child, i.e. * <e>text</e> becomes {"e": "text"}<li> * <li>An element with name "json:value" is serialized as a simple value, not an object, i.e. * <json:value>value</json:value> just becomes "value".</li> * </ul> * * Namespace prefixes will be dropped from element and attribute names by default. If the serialization * property {@link EXistOutputKeys#JSON_OUTPUT_NS_PREFIX} is set to "yes", namespace prefixes will be * added to the resulting JSON property names, replacing the ":" with a "_", i.e. <foo:node> becomes * "foo_node". * * If an attribute json:array is present on an element it will always be serialized as an array, even if there * are no other sibling elements with the same name. * * The attribute json:literal indicates that the element's text content should be serialized literally. This is * handy for writing boolean or numeric values. By default, text content is serialized as a Javascript string. * * @author wolf * */ public class JSONWriter extends XMLWriter { public final static String JASON_NS = "http://www.json.org"; protected JSONNode root; protected Stack<JSONObject> stack = new Stack<JSONObject>(); protected boolean useNSPrefix = false; public JSONWriter() { // empty } public JSONWriter(Writer writer) { super(writer); } @Override protected void reset() { super.reset(); stack.clear(); root = null; } @Override public void setOutputProperties(Properties properties) { super.setOutputProperties(properties); String useNSProp = properties.getProperty(EXistOutputKeys.JSON_OUTPUT_NS_PREFIX, "no"); useNSPrefix = useNSProp.equalsIgnoreCase("yes"); } @Override public void startDocument() throws TransformerException { } @Override public void endDocument() throws TransformerException { try { if (root != null) root.serialize(writer, true); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Override public void startElement(String qname) throws TransformerException { if (qname.equals("json:value")) processStartValue(); else if (useNSPrefix) processStartElement(qname.replace(':', '_'), false); else processStartElement(QName.extractLocalName(qname), false); } @Override public void startElement(QName qname) throws TransformerException { if (JASON_NS.equals(qname.getNamespaceURI()) && "value".equals(qname.getLocalName())) processStartValue(); else if (useNSPrefix) processStartElement(qname.getPrefix() + '_' + qname.getLocalName(), false); else processStartElement(qname.getLocalName(), false); } private void processStartElement(String localName, boolean simpleValue) { JSONObject obj = new JSONObject(localName); if (root == null) { root = obj; stack.push(obj); } else { JSONObject parent = stack.peek(); parent.addObject(obj); stack.push(obj); } } private void processStartValue() throws TransformerException { // a json:value is stored as an unnamed object JSONObject obj = new JSONObject(); if (root == null) { root = obj; stack.push(obj); } else { JSONObject parent = stack.peek(); parent.addObject(obj); stack.push(obj); } } @Override public void endElement(String qname) throws TransformerException { stack.pop(); } @Override public void endElement(QName qname) throws TransformerException { stack.pop(); } @Override public void namespace(String prefix, String nsURI) throws TransformerException { } @Override public void attribute(String qname, String value) throws TransformerException { JSONObject parent = stack.peek(); if (qname.equals("json:array")) { parent.setSerializationType(JSONNode.SerializationType.AS_ARRAY); } else if (qname.equals("json:literal")) { parent.setSerializationType(JSONNode.SerializationType.AS_LITERAL); } else { JSONSimpleProperty obj = new JSONSimpleProperty(qname, value); parent.addObject(obj); } } @Override public void attribute(QName qname, String value) throws TransformerException { attribute(qname.toString(), value); } @Override public void characters(CharSequence chars) throws TransformerException { JSONObject parent = stack.peek(); JSONNode value = new JSONValue(chars.toString()); value.setSerializationType(parent.getSerializationType()); parent.addObject(value); } @Override public void characters(char[] ch, int start, int len) throws TransformerException { characters(new String(ch, start, len)); } @Override public void processingInstruction(String target, String data) throws TransformerException { // skip } @Override public void comment(CharSequence data) throws TransformerException { // skip } @Override public void cdataSection(char[] ch, int start, int len) throws TransformerException { // treat as string content characters(ch, start, len); } @Override public void documentType(String name, String publicId, String systemId) throws TransformerException { // skip } }