# JsonXMLStreamReader.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.AbstractXMLStreamReader;
    import org.apache.synapse.commons.staxon.core.base.XMLStreamReaderScope;
    import org.apache.synapse.commons.staxon.core.json.stream.JsonStreamSource;
    import org.apache.synapse.commons.staxon.core.json.stream.JsonStreamSource.Value;
    import org.apache.synapse.commons.staxon.core.json.stream.JsonStreamToken;

    /**
     * JSON XML stream reader.
     * <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>
     * </ul>
     * <p/>
     * <p>The reader may produce processing instructions
     * <code>&lt;?xml-multiple element-name?&gt;</code>
     * to indicate array starts (<code>'['</code>).</p>
     */
    public class JsonXMLStreamReader extends AbstractXMLStreamReader<JsonXMLStreamReader.ScopeInfo> {
        static class ScopeInfo extends JsonXMLStreamScopeInfo {
            private String currentTagName;
        }

        private final JsonStreamSource source;
        private final boolean multiplePI;
        private final char namespaceSeparator;

        private final boolean xmlNilReadWriteEnabled;

        private boolean documentArray = false;

        /**
         * Create reader instance.
         *
         * @param source             stream source
         * @param multiplePI         whether to produce <code>&lt;xml-multiple?&gt;</code> PIs to signal array start
         * @param namespaceSeparator namespace prefix separator
         * @throws XMLStreamException
         */
        public JsonXMLStreamReader(JsonStreamSource source, boolean multiplePI, char namespaceSeparator) throws XMLStreamException {
            super(new ScopeInfo(), source);
            this.source = source;
            this.multiplePI = multiplePI;
            this.namespaceSeparator = namespaceSeparator;
            xmlNilReadWriteEnabled = false;
            initialize();
        }

        /**
         * Create reader instance.
         *
         * @param source             stream source
         * @param multiplePI         whether to produce <code>&lt;xml-multiple?&gt;</code> PIs to signal array start
         * @param namespaceSeparator namespace prefix separator
         * @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.
         * @throws XMLStreamException
         */
        public JsonXMLStreamReader(JsonStreamSource source, boolean multiplePI, char namespaceSeparator,
                                   boolean xmlNilReadWriteEnabled) throws XMLStreamException {
            super(new ScopeInfo(), source);
            this.source = source;
            this.multiplePI = multiplePI;
            this.namespaceSeparator = namespaceSeparator;
            this.xmlNilReadWriteEnabled = xmlNilReadWriteEnabled;
            initialize();
        }

        private void readStartElementTag(String name) throws XMLStreamException {
            int separator = name.indexOf(namespaceSeparator);
            if (separator < 0) {
                readStartElementTag(XMLConstants.DEFAULT_NS_PREFIX, name, null, new ScopeInfo());
            } else {
                readStartElementTag(name.substring(0, separator), name.substring(separator + 1), null, new ScopeInfo());
            }
        }

        private void readAttrNsDecl(String name, String value) throws XMLStreamException {
            int separator = name.indexOf(namespaceSeparator);
            if (separator < 0) {
                if (XMLConstants.XMLNS_ATTRIBUTE.equals(name)) {
                    readNsDecl(XMLConstants.DEFAULT_NS_PREFIX, value);
                } else {
                    readAttr(XMLConstants.DEFAULT_NS_PREFIX, name, null, value);
                }
            } else {
                if (name.startsWith(XMLConstants.XMLNS_ATTRIBUTE) && separator == XMLConstants.XMLNS_ATTRIBUTE.length()) {
                    readNsDecl(name.substring(separator + 1), value);
                } else {
                    readAttr(name.substring(0, separator), name.substring(separator + 1), null, value);
                }
            }
        }

        private void readData(Value value, int type) throws XMLStreamException {
            readData(value.text, value.data, type);
        }

        private void consumeName(ScopeInfo info) throws XMLStreamException, IOException {
            String fieldName = source.name();
            if (fieldName.startsWith("@")) {
                fieldName = fieldName.substring(1);
                if (source.peek() == JsonStreamToken.VALUE) {
                    readAttrNsDecl(fieldName, source.value().text);
                } else if (XMLConstants.XMLNS_ATTRIBUTE.equals(fieldName)) { // badgerfish
                    source.startObject();
                    while (source.peek() == JsonStreamToken.NAME) {
                        String prefix = source.name();
                        if ("$".equals(prefix)) {
                            readNsDecl(XMLConstants.DEFAULT_NS_PREFIX, source.value().text);
                        } else {
                            readNsDecl(prefix, source.value().text);
                        }
                    }
                    source.endObject();
                } else if (source.peek() == JsonStreamToken.START_OBJECT) {
                    source.startObject();
                    while (source.peek() == JsonStreamToken.NAME) {
                        readAttrNsDecl(source.name(), source.value().text);
                    }
                    source.endObject();
                } else {
                    throw new IllegalStateException("Expected attribute value");
                }
            } else if ("$".equals(fieldName)) {
                readData(source.value(), XMLStreamConstants.CHARACTERS);
            } else {
                info.currentTagName = fieldName;
            }
        }

        @Override
        protected boolean consume() throws XMLStreamException, IOException {
            XMLStreamReaderScope<ScopeInfo> scope = getScope();
            switch (source.peek()) {
                case NAME:
                    consumeName(scope.getInfo());
                    return consume();
                case START_ARRAY:
                    source.startArray();
                    if (scope.getInfo().isArray()) {
                        throw new IOException("Array start inside array");
                    }
                    if (scope.isRoot() && !isStartDocumentRead()) {
                        documentArray = true;
                    } else {
                        if (scope.getInfo().currentTagName == null) {
                            throw new IOException("Array name missing");
                        }
                        scope.getInfo().startArray(scope.getInfo().currentTagName);
                    }
                    if (multiplePI) {
                        readPI(JsonXMLStreamConstants.MULTIPLE_PI_TARGET, scope.getInfo().currentTagName);
                    }
                    return consume();
                case START_OBJECT:
                    source.startObject();
                    if (scope.isRoot() && !isStartDocumentRead()) {
                        readStartDocument(null, null, null);
                    } else {
                        if (scope.getInfo().isArray()) {
                            scope.getInfo().incArraySize();
                        }
                        if (scope.getInfo().currentTagName != null) {
                            readStartElementTag(scope.getInfo().currentTagName);
                        }
                    }
                    return consume();
                case END_OBJECT:
                    source.endObject();
                    if (scope.isRoot() && isStartDocumentRead()) {
                        readEndDocument();
                        return documentArray;
                    } else {
                        readEndElementTag();
                        return true;
                    }
                case VALUE:
                    String name = scope.getInfo().currentTagName;
                    if (scope.getInfo().isArray()) {
                        scope.getInfo().incArraySize();
                        name = scope.getInfo().getArrayName();
                    }
                    if (getScope().isRoot() && !isStartDocumentRead()) { // hack: allow to read simple value
                        readData(source.value(), XMLStreamConstants.CHARACTERS);
                    } else {
                        readStartElementTag(name);
                        Value value = source.value();
                        if (value != JsonStreamSource.NULL) {
                            readData(value, XMLStreamConstants.CHARACTERS);
                        }
                        if (value == JsonStreamSource.NULL && xmlNilReadWriteEnabled) {
                            readAttrNsDecl("nil", "true");
                        }
                        readEndElementTag();
                    }
                    return consume();
                case END_ARRAY:
                    source.endArray();
                    if (getScope().isRoot() && documentArray) {
                        return false;
                    }
                    if (!scope.getInfo().isArray()) {
                        throw new IllegalStateException("Array end without matching start");
                    }
                    scope.getInfo().endArray();
                    return true;
                case NONE:
                    return false;
                default:
                    throw new IOException("Unexpected token: " + source.peek());
            }
        }

        /**
         * @return <code>true</code> iff the current event data is a number primitive
         */
        public boolean hasNumber() {
            return getEventData() instanceof Number;
        }

        /**
         * @return number primitive
         * @throws ClassCastException
         */
        public Number getNumber() {
            return (Number) getEventData();
        }

        /**
         * @return <code>true</code> iff the current event data is a boolean primitive
         */
        public boolean hasBoolean() {
            return getEventData() instanceof Boolean;
        }

        /**
         * @return boolean primitive
         * @throws ClassCastException
         */
        public Boolean getBoolean() {
            return (Boolean) getEventData();
        }

        @Override
        public void close() throws XMLStreamException {
            super.close();
            try {
                source.close();
            } catch (IOException e) {
                throw new XMLStreamException(e);
            }
        }
    }