# JsonXMLStreamWriter.java

    /*
     * Copyright 2011, 2012 Odysseus Software GmbH
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    package org.apache.synapse.commons.staxon.core.json;

    import java.io.IOException;

    import javax.xml.XMLConstants;
    import javax.xml.stream.XMLStreamConstants;
    import javax.xml.stream.XMLStreamException;

    import org.apache.synapse.commons.staxon.core.base.AbstractXMLStreamWriter;
    import org.apache.synapse.commons.staxon.core.json.stream.JsonStreamTarget;

    /**
     * JSON XML stream writer.
     * <p/>
     * <h4>Limitations</h4>
     * <ul>
     * <li>Mixed content (e.g. <code>&lt;alice&gt;bob&lt;edgar/&gt;&lt;/alice&gt;</code>) is not supported.</li>
     * <li><code>writeDTD(...)</code> and <code>writeEntityRef(...)</code> are not supported.</li>
     * <li><code>writeCData(...)</code> delegates to writeCharacters(...).</li>
     * <li><code>writeComment(...)</code> does nothing.</li>
     * <li><code>writeProcessingInstruction(...)</code> does nothing (except for target <code>xml-multiple</code>, see below).</li>
     * </ul>
     * <p/>
     * <p>The writer may consume processing instructions
     * (e.g. <code>&lt;?xml-multiple element-name?&gt;</code>) to properly insert JSON array tokens (<code>'['</code>
     * and <code>']'</code>). The client provides this instruction through the
     * {@link #writeProcessingInstruction(String, String)} method,
     * passing the (possibly prefixed) field name as data e.g.</p>
     * <pre>
     *   ...
     *   writer.writeProcessingInstruction("xml-multiple", "item");
     *   for (Item item : items) {
     *     writer.writeStartElement("item");
     *     ...
     *     writer.writeEndElement();
     *   }
     *   ...
     * </pre>
     * <p>The element name passed as processing instruction data is optional.
     * If omitted, the next element within the current scope will start an array. Note, that this method
     * does not allow to create empty arrays (in fact, the above code sample could create unexpected results,
     * if the name would have been omitted and collection were empty).</p>
     */
    public class JsonXMLStreamWriter extends AbstractXMLStreamWriter<JsonXMLStreamWriter.ScopeInfo> {
        private static final String XML_SCHEMA_NIL_VALUE = "org.apache.synapse.commons.staxon.core.json.JsonXMLStreamWriter.{http://www.w3.org/2001/XMLSchema-instance}#nil=true_";
        private static final String XSI_NIL = "nil";
        private static final String XSI_NS = "http://www.w3.org/2001/XMLSchema-instance";

        static class ScopeInfo extends JsonXMLStreamScopeInfo {
            private Object leadData = null;
            private StringBuilder builder = null;
            boolean startObjectWritten = false;
            boolean pendingStartArray = false;

            void addText(String data) {
                if (leadData == null) { // first event?
                    this.leadData = data;
                } else {
                    if (builder == null) { // second event?
                        builder = new StringBuilder(leadData.toString());
                    }
                    builder.append(data);
                }
            }

            boolean hasData() {
                return leadData != null;
            }

            Object getData() {
                return builder == null ? (hasData() ? leadData : null) : builder.toString();
            }

            void setData(Object data) {
                this.leadData = data;
                this.builder = null;
            }
        }

        static boolean isWhitespace(Object data) {
            if (data == null) {
                return false;
            }
            String text = data.toString();
            for (int i = 0; i < text.length(); i++) {
                if (!Character.isWhitespace(text.charAt(i))) {
                    return false;
                }
            }
            return true;
        }

        private final JsonStreamTarget target;
        private final boolean multiplePI;
        private final boolean autoEndArray;
        private final boolean skipSpace;
        private final char namespaceSeparator;
        private final boolean namespaceDeclarations;
        private final boolean xmlNilReadWriteEnabled;
        private final boolean xmlWriteNullForEmptyElement;

        private boolean documentArray = false;

        /**
         * Create writer instance.
         *
         * @param target                stream target
         * @param multiplePI            whether to consume <code>&lt;xml-multiple?&gt;</code> PIs to trigger array start
         * @param namespaceSeparator    namespace prefix separator
         * @param namespaceDeclarations whether to write namespace declarations
         * @param repairNamespaces      repair Namespaces
         */
        public JsonXMLStreamWriter(JsonStreamTarget target, boolean repairNamespaces, boolean multiplePI, char namespaceSeparator, boolean namespaceDeclarations) {
            super(new ScopeInfo(), repairNamespaces);
            this.target = target;
            this.multiplePI = multiplePI;
            this.namespaceSeparator = namespaceSeparator;
            this.namespaceDeclarations = namespaceDeclarations;
            this.autoEndArray = true;
            this.skipSpace = true;
            this.xmlNilReadWriteEnabled = false;
            this.xmlWriteNullForEmptyElement = true;
        }

        /**
         * Create writer instance.
         *
         * @param target                stream target
         * @param multiplePI            whether to consume <code>&lt;xml-multiple?&gt;</code> PIs to trigger array start
         * @param namespaceSeparator    namespace prefix separator
         * @param namespaceDeclarations whether to write namespace declarations
         * @param xmlNilReadWriteEnabled    Supports reading and writing of XML Nil elements as defined by http://www.w3.org/TR/xmlschema-1/#xsi_nil when the XML/JSON inputs contains nil/null values.
         * @param repairNamespaces      repair Namespaces
         * @param xmlWriteNullForEmptyElement   write null or "" for XML empty elements without nil attribute
         */
        public JsonXMLStreamWriter(JsonStreamTarget target, boolean repairNamespaces, boolean multiplePI, char namespaceSeparator, boolean namespaceDeclarations,
                                   boolean xmlNilReadWriteEnabled, boolean xmlWriteNullForEmptyElement) {
            super(new ScopeInfo(), repairNamespaces);
            this.target = target;
            this.multiplePI = multiplePI;
            this.namespaceSeparator = namespaceSeparator;
            this.namespaceDeclarations = namespaceDeclarations;
            this.autoEndArray = true;
            this.skipSpace = true;
            this.xmlNilReadWriteEnabled = xmlNilReadWriteEnabled;
            this.xmlWriteNullForEmptyElement = xmlWriteNullForEmptyElement;
        }

        private String getFieldName(String prefix, String localName) {
            return XMLConstants.DEFAULT_NS_PREFIX.equals(prefix) ? localName : prefix + namespaceSeparator + localName;
        }

        @Override
        protected ScopeInfo writeStartElementTag(String prefix, String localName, String namespaceURI) throws XMLStreamException {
            ScopeInfo parentInfo = getScope().getInfo();
            if (parentInfo.hasData()) {
                if (!skipSpace || !isWhitespace(parentInfo.getData())) {
                    throw new XMLStreamException("Mixed content is not supported: '" + parentInfo.getData() + "'");
                }
                parentInfo.setData(null);
            }
            String fieldName = getFieldName(prefix, localName);
            if (getScope().isRoot() && getScope().getLastChild() != null && !documentArray) {
                if (!fieldName.equals(parentInfo.getArrayName())) {
                    throw new XMLStreamException("Multiple roots within document");
                }
            }
            if (parentInfo.pendingStartArray) {
                writeStartArray(fieldName);
            }
            try {
                if (!parentInfo.isArray()) {
                    if (!parentInfo.startObjectWritten) {
                        target.startObject();
                        parentInfo.startObjectWritten = true;
                    }
                } else if (autoEndArray && !fieldName.equals(parentInfo.getArrayName())) {
                    writeEndArray();
                }
                if (!parentInfo.isArray()) {
                    target.name(fieldName);
                } else {
                    parentInfo.incArraySize();
                }
            } catch (IOException e) {
                throw new XMLStreamException("Cannot write start element: " + fieldName, e);
            }
            return new ScopeInfo();
        }

        @Override
        protected void writeStartElementTagEnd() throws XMLStreamException {
            if (getScope().isEmptyElement()) {
                writeEndElementTag();
            }
        }

        @Override
        protected void writeEndElementTag() throws XMLStreamException {
            try {
                if (getScope().getInfo().hasData()) {
                    if (xmlNilReadWriteEnabled && XML_SCHEMA_NIL_VALUE.equals(getScope().getInfo().getData())) {
                        target.value(null);
                        return;
                    }
                    if (getScope().getInfo().startObjectWritten) {
                        target.name("$");
                    }
                    target.value(getScope().getInfo().getData());
                }
                if (autoEndArray && getScope().getInfo().isArray()) {
                    writeEndArray();
                }
                if (getScope().getInfo().startObjectWritten) {
                    target.endObject();
                } else if (!getScope().getInfo().hasData()) {
                    if (xmlWriteNullForEmptyElement) {
                        target.value(null);
                    } else {
                        target.value("");
                    }
                }
            } catch (IOException e) {
                throw new XMLStreamException("Cannot write end element: " + getFieldName(getScope().getPrefix(), getScope().getLocalName()), e);
            }
        }

        public void writeAttribute(String prefix, String namespaceURI, String localName, String value) throws XMLStreamException {
            if (xmlNilReadWriteEnabled && (XSI_NS.equals(namespaceURI) || "".equals(namespaceURI)) &&
                    XSI_NIL.equals(localName) &&
                    "true".equals(value)) {
                getScope().getInfo().setData(XML_SCHEMA_NIL_VALUE);
                return;
            }
            super.writeAttribute(prefix, namespaceURI, localName, value);
        }

        public void writeNamespace(String prefix, String namespaceURI) throws XMLStreamException {
            if (xmlNilReadWriteEnabled && XSI_NS.equals(namespaceURI)) {
                return;
            }
            super.writeNamespace(prefix, namespaceURI);
        }

        @Override
        protected void writeAttr(String prefix, String localName, String namespaceURI, String value) throws XMLStreamException {
            String name = XMLConstants.DEFAULT_NS_PREFIX.equals(prefix) ? localName : prefix + namespaceSeparator + localName;
            try {
                if (!getScope().getInfo().startObjectWritten) {
                    target.startObject();
                    getScope().getInfo().startObjectWritten = true;
                }
                target.name('@' + name);
                target.value(value);
            } catch (IOException e) {
                throw new XMLStreamException("Cannot write attribute: " + name, e);
            }
        }

        @Override
        protected void writeNsDecl(String prefix, String namespaceURI) throws XMLStreamException {
            if (namespaceDeclarations) {
                try {
                    if (!getScope().getInfo().startObjectWritten) {
                        target.startObject();
                        getScope().getInfo().startObjectWritten = true;
                    }
                    if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
                        target.name('@' + XMLConstants.XMLNS_ATTRIBUTE);
                    } else {
                        target.name('@' + XMLConstants.XMLNS_ATTRIBUTE + namespaceSeparator + prefix);
                    }
                    target.value(namespaceURI);
                } catch (IOException e) {
                    throw new XMLStreamException("Cannot write namespace declaration: " + namespaceURI, e);
                }
            }
        }

        @Override
        protected void writeData(Object data, int type) throws XMLStreamException {
            switch (type) {
                case XMLStreamConstants.CHARACTERS:
                case XMLStreamConstants.CDATA:
                    if (getScope().isRoot() && !isStartDocumentWritten()) { // hack: allow to write simple value
                        try {
                            target.value(data);
                        } catch (IOException e) {
                            throw new XMLStreamException("Cannot write data", e);
                        }
                    } else {
                        if (data == null) {
                            throw new XMLStreamException("Cannot write null data");
                        }
                        if (getScope().getLastChild() != null) {
                            if (!skipSpace || !isWhitespace(data)) {
                                throw new XMLStreamException("Mixed content is not supported: '" + data + "'");
                            }
                        } else if (getScope().getInfo().hasData()) {
                            if (data instanceof String) {
                                getScope().getInfo().addText(data.toString());
                            } else {
                                throw new XMLStreamException("Cannot append primitive data: " + data);
                            }
                        } else {
                            getScope().getInfo().setData(data);
                        }
                    }
                    break;
                case XMLStreamConstants.COMMENT: // ignore comments
                    break;
                default:
                    throw new UnsupportedOperationException("Cannot write data of type " + type);
            }
        }

        @Override
        public void writeStartDocument(String encoding, String version) throws XMLStreamException {
            super.writeStartDocument(encoding, version);
            try {
                target.startObject();
            } catch (IOException e) {
                throw new XMLStreamException("Cannot start document", e);
            }
            getScope().getInfo().startObjectWritten = true;
        }

        @Override
        public void writeEndDocument() throws XMLStreamException {
            super.writeEndDocument();
            try {
                if (getScope().getInfo().isArray()) {
                    target.endArray();
                }
                target.endObject();
            } catch (IOException e) {
                throw new XMLStreamException("Cannot end document", e);
            }
            getScope().getInfo().startObjectWritten = false;
        }

        public void writeStartArray(String fieldName) throws XMLStreamException {
            if (autoEndArray && getScope().getInfo().isArray()) {
                writeEndArray();
            }
            getScope().getInfo().startArray(fieldName);
            getScope().getInfo().pendingStartArray = false;
            try {
                if (!getScope().getInfo().startObjectWritten) {
                    target.startObject();
                    getScope().getInfo().startObjectWritten = true;
                }
                target.name(fieldName);
                target.startArray();
            } catch (IOException e) {
                throw new XMLStreamException("Cannot start array: " + fieldName, e);
            }
        }

        public void writeEndArray() throws XMLStreamException {
            getScope().getInfo().endArray();
            try {
                target.endArray();
            } catch (IOException e) {
                throw new XMLStreamException("Cannot end array: " + getScope().getInfo().getArrayName(), e);
            }
        }

        @Override
        public void close() throws XMLStreamException {
            super.close();
            try {
                if (documentArray) {
                    target.endArray();
                }
                target.close();
            } catch (IOException e) {
                throw new XMLStreamException("Close failed", e);
            }
        }

        @Override
        public void flush() throws XMLStreamException {
            try {
                target.flush();
            } catch (IOException e) {
                throw new XMLStreamException("Flush failed", e);
            }
        }

        @Override
        protected void writePI(String target, String data) throws XMLStreamException {
            if (multiplePI && JsonXMLStreamConstants.MULTIPLE_PI_TARGET.equals(target)) {
                if (getScope().isRoot() && !isStartDocumentWritten()) {
                    if (data == null || data.trim().isEmpty()) {
                        try {
                            this.target.startArray();
                            this.documentArray = true;
                        } catch (IOException e) {
                            throw new XMLStreamException("Cannot start document array", e);
                        }
                    } else {
                        throw new XMLStreamException("Cannot specify name in document array: " + data);
                    }
                } else {
                    if (data == null || data.trim().isEmpty()) {
                        getScope().getInfo().pendingStartArray = true;
                    } else {
                        writeStartArray(data.trim());
                    }
                }
            }
        }

        /**
         * Write number value.
         *
         * @param value
         * @throws XMLStreamException
         */
        public void writeNumber(Number value) throws XMLStreamException {
            if (getScope().getInfo().hasData()) {
                throw new XMLStreamException("Cannot write number value");
            }
            super.writeCharacters(value, XMLStreamConstants.CHARACTERS);
        }

        /**
         * Write boolean value.
         *
         * @param value
         * @throws XMLStreamException
         */
        public void writeBoolean(Boolean value) throws XMLStreamException {
            if (getScope().getInfo().hasData()) {
                throw new XMLStreamException("Cannot write boolean value");
            }
            super.writeCharacters(value, XMLStreamConstants.CHARACTERS);
        }
    }