package org.exist.util.serializer.json; import java.io.IOException; import java.io.Writer; import java.util.ArrayDeque; import java.util.Deque; import java.util.Properties; import javax.xml.transform.OutputKeys; import javax.xml.transform.TransformerException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; 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 { private final static Logger LOG = LogManager.getLogger(JSONWriter.class); private final static String ARRAY = "array"; private final static String LITERAL = "literal"; private final static String VALUE = "value"; private final static String NAME = "name"; private final static String JSON_ARRAY = "json:" + ARRAY; private final static String JSON_LITERAL = "json:" + LITERAL; private final static String JSON_VALUE = "json:" + VALUE; private final static String JSON_NAME = "json:" + NAME; public final static String JASON_NS = "http://www.json.org"; protected JSONNode root; protected final Deque<JSONObject> stack = new ArrayDeque<>(); protected boolean useNSPrefix = false; protected boolean prefixAttributes = false; private String jsonp = null; private boolean indent = false; public JSONWriter() { // empty } public JSONWriter(final Writer writer) { super(writer); } @Override protected void resetObjectState() { super.resetObjectState(); stack.clear(); root = null; } @Override public void setOutputProperties(final Properties properties) { super.setOutputProperties(properties); final String useNSProp = properties.getProperty(EXistOutputKeys.JSON_OUTPUT_NS_PREFIX, "no"); useNSPrefix = useNSProp.equalsIgnoreCase("yes"); final String prefixForAttr = properties.getProperty(EXistOutputKeys.JSON_PREFIX_ATTRIBUTES, "no"); prefixAttributes = prefixForAttr.equalsIgnoreCase("yes"); jsonp = properties.getProperty(EXistOutputKeys.JSONP); indent = properties.getProperty(OutputKeys.INDENT, "no").equalsIgnoreCase("yes"); } @Override public void startDocument() throws TransformerException { } @Override public void endDocument() throws TransformerException { try { if(root != null) { if(jsonp != null) { getWriter().write(jsonp + "("); } root.serialize(getWriter(), true); if(jsonp != null) { getWriter().write(")"); } } } catch(final IOException ioe) { LOG.error(ioe.getMessage(), ioe); } } @Override public void startElement(final String namespaceURI, final String localName, final 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(final QName qname) throws TransformerException { if(JASON_NS.equals(qname.getNamespaceURI()) && VALUE.equals(qname.getLocalPart())) { processStartValue(); } else if(useNSPrefix) { processStartElement(qname.getPrefix() + '_' + qname.getLocalPart(), false); } else { processStartElement(qname.getLocalPart(), false); } } private void processStartElement(final String localName, boolean simpleValue) { final JSONObject obj = new JSONObject(localName); obj.setIndent(indent); if(root == null) { root = obj; stack.push(obj); } else { final JSONObject parent = stack.peek(); parent.addObject(obj); stack.push(obj); } } private void processStartValue() throws TransformerException { // a json:value is stored as an unnamed object final JSONObject obj = new JSONObject(); obj.setIndent(indent); if(root == null) { root = obj; stack.push(obj); } else { final JSONObject parent = stack.peek(); parent.addObject(obj); stack.push(obj); } } @Override public void endElement(final String namespaceUri, final String localName, final String qname) throws TransformerException { stack.pop(); } @Override public void endElement(final QName qname) throws TransformerException { stack.pop(); } @Override public void namespace(final String prefix, final String nsURI) throws TransformerException { } @Override public void attribute(final String qname, final String value) throws TransformerException { final JSONObject parent = stack.peek(); switch (qname) { case JSON_ARRAY: parent.setSerializationType(JSONNode.SerializationType.AS_ARRAY); break; case JSON_LITERAL: parent.setSerializationDataType(JSONNode.SerializationDataType.AS_LITERAL); break; case JSON_NAME: parent.setName(value); break; default: final String name = prefixAttributes ? "@" + qname : qname; final JSONSimpleProperty obj = new JSONSimpleProperty(name, value); obj.setIndent(indent); parent.addObject(obj); break; } } @Override public void attribute(final QName qname, final String value) throws TransformerException { attribute(qname.toString(), value); } @Override public void characters(final CharSequence chars) throws TransformerException { final JSONObject parent = stack.peek(); final JSONNode value = new JSONValue(chars.toString()); value.setIndent(indent); value.setSerializationType(parent.getSerializationType()); value.setSerializationDataType(parent.getSerializationDataType()); parent.addObject(value); } @Override public void characters(final char[] ch, final int start, final int len) throws TransformerException { characters(new String(ch, start, len)); } @Override public void processingInstruction(final String target, final String data) throws TransformerException { // skip } @Override public void comment(final CharSequence data) throws TransformerException { // skip } @Override public void cdataSection(final char[] ch, final int start, final int len) throws TransformerException { // treat as string content characters(ch, start, len); } @Override public void documentType(final String name, final String publicId, final String systemId) throws TransformerException { // skip } }