# AbstractXMLStreamWriter.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.base;

    import javax.xml.XMLConstants;
    import javax.xml.namespace.NamespaceContext;
    import javax.xml.stream.XMLStreamConstants;
    import javax.xml.stream.XMLStreamException;
    import javax.xml.stream.XMLStreamWriter;

    /**
     * Abstract XML stream writer.
     */
    public abstract class AbstractXMLStreamWriter<T> implements XMLStreamWriter {
        private final boolean repairingNamespaces;
        private XMLStreamWriterScope<T> scope;
        private boolean startDocumentWritten;

        /**
         * Create writer instance.
         *
         * @param rootInfo root scope information
         */
        public AbstractXMLStreamWriter(T rootInfo, boolean repaireNamespaces) {
            scope = new XMLStreamWriterScope<T>(XMLConstants.NULL_NS_URI, rootInfo);
            startDocumentWritten = false;
            repairingNamespaces = repaireNamespaces;
        }

        private void ensureStartTagClosed() throws XMLStreamException {
            if (!scope.isStartTagClosed()) {
                if (repairingNamespaces) { // missing declaration?
                    String namespaceURI = getScope().getNamespaceURI();
                    if (!XMLConstants.NULL_NS_URI.equals(namespaceURI)) {
                        String prefix = getScope().getPrefix();
                        if (!namespaceURI.equals(getScope().getNamespaceURI(prefix))) {
                            writeNamespace(prefix, namespaceURI);
                        }
                    }
                }
                writeStartElementTagEnd();
                scope.setStartTagClosed(true);
                if (scope.isEmptyElement()) {
                    scope = scope.getParent();
                }
            }
        }

        private void writeStartElement(String prefix, String localPart, String namespaceURI, boolean emptyElement) throws XMLStreamException {
            if (localPart == null) {
                throw new XMLStreamException("Local name must not be null");
            }
            boolean writeNamespace = false;
            if (prefix == null) {
                if (repairingNamespaces) {
                    if (XMLConstants.NULL_NS_URI.equals(getScope().getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX))) { // try empty prefix
                        prefix = XMLConstants.DEFAULT_NS_PREFIX;
                    } else { // find unused prefix
                        int index = 1;
                        while (getScope().getNamespaceURI("ns" + index) == null) {
                            index++;
                        }
                        prefix = index == 1 ? "ns" : "ns" + index;
                    }
                    writeNamespace = true;
                } else {
                    throw new XMLStreamException("Namespace URI has not been bound to a prefix: " + namespaceURI);
                }
            }
            ensureStartTagClosed();
            T scopeInfo = writeStartElementTag(prefix, localPart, namespaceURI);
            scope = new XMLStreamWriterScope<T>(scope, prefix, localPart, namespaceURI, emptyElement);
            scope.setInfo(scopeInfo);
            if (writeNamespace) {
                writeNamespace(prefix, namespaceURI);
            }
        }

        /**
         * @return current scope
         */
        protected XMLStreamWriterScope<T> getScope() {
            return scope;
        }

        /**
         * @return <code>true</code> if <code>START_DOCUMENT</code> event has been written
         */
        protected boolean isStartDocumentWritten() {
            return startDocumentWritten;
        }

        /**
         * Write open start element tag.
         * The returned scope info is stored in the new scope and will be available via
         * <code>getScope().getInfo()</code>.
         *
         * @param prefix       element prefix (may be <code>XMLConstants.DEFAULT_NS_PREFIX</code>)
         * @param localPart    local name
         * @param namespaceURI namespace URI
         * @return new scope info
         * @throws XMLStreamException
         */
        protected abstract T writeStartElementTag(String prefix, String localPart, String namespaceURI) throws XMLStreamException;

        /**
         * Write close start element tag.
         *
         * @throws XMLStreamException
         */
        protected abstract void writeStartElementTagEnd() throws XMLStreamException;

        /**
         * Write end element tag.
         *
         * @throws XMLStreamException
         */
        protected abstract void writeEndElementTag() throws XMLStreamException;

        /**
         * Write attribute.
         *
         * @param prefix       attribute prefix (may be <code>XMLConstants.DEFAULT_NS_PREFIX</code>)
         * @param localName    local name
         * @param namespaceURI namespace URI
         * @param value        attribute value
         * @throws XMLStreamException
         */
        protected abstract void writeAttr(String prefix, String localName, String namespaceURI, String value) throws XMLStreamException;

        /**
         * Write namespace declaration.
         *
         * @param prefix       namespace prefix
         * @param namespaceURI namespace URI
         * @throws XMLStreamException
         */
        protected abstract void writeNsDecl(String prefix, String namespaceURI) throws XMLStreamException;

        /**
         * Write characters/comment/dtd/entity data.
         *
         * @param data text/data
         * @param type one of <code>CHARACTERS, COMMENT, CDATA, DTD, ENTITY_REFERENCE, SPACE</code>
         * @throws XMLStreamException
         */
        protected abstract void writeData(Object data, int type) throws XMLStreamException;

        /**
         * Read processing instruction.
         *
         * @param target PI target
         * @param data   PI data (may be <code>null</code>)
         * @throws XMLStreamException
         */
        protected abstract void writePI(String target, String data) throws XMLStreamException;

        @Override
        public void writeStartElement(String localName) throws XMLStreamException {
            String namespaceURI = scope.getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX);
            if (namespaceURI == null) {
                throw new XMLStreamException("Default namespace URI has not been set");
            }
            writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, localName, namespaceURI, false);
        }

        @Override
        public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException {
            if (namespaceURI == null) {
                throw new XMLStreamException("Namespace URI must not be null");
            }
            String prefix = scope.getPrefix(namespaceURI);
            if (prefix == null && !repairingNamespaces) {
                throw new XMLStreamException("Namespace URI has not been bound to a prefix: " + namespaceURI);
            }
            writeStartElement(prefix, localName, namespaceURI, false);
        }

        @Override
        public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException {
            if (prefix == null) {
                throw new XMLStreamException("Prefix must not be null");
            }
            if (namespaceURI == null) {
                throw new XMLStreamException("Namespace URI must not be null");
            }
            writeStartElement(prefix, localName, namespaceURI, false);
        }

        @Override
        public void writeEndElement() throws XMLStreamException {
            ensureStartTagClosed();
            if (scope.isRoot()) {
                throw new XMLStreamException("Cannot write end element in root scope");
            }
            writeEndElementTag();
            scope = scope.getParent();
        }

        @Override
        public void writeEmptyElement(String localName) throws XMLStreamException {
            String namespaceURI = scope.getNamespaceURI(XMLConstants.DEFAULT_NS_PREFIX);
            if (namespaceURI == null) {
                throw new XMLStreamException("Default namespace URI has not been set");
            }
            writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, localName, namespaceURI, true);
        }

        @Override
        public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException {
            if (namespaceURI == null) {
                throw new XMLStreamException("Namespace URI must not be null");
            }
            String prefix = scope.getPrefix(namespaceURI);
            if (prefix == null && !repairingNamespaces) {
                throw new XMLStreamException("Namespace URI has not been bound to a prefix: " + namespaceURI);
            }
            writeStartElement(prefix, localName, namespaceURI, true);
        }

        @Override
        public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException {
            if (prefix == null) {
                throw new XMLStreamException("Prefix must not be null");
            }
            if (namespaceURI == null) {
                throw new XMLStreamException("Namespace URI must not be null");
            }
            writeStartElement(prefix, localName, namespaceURI, true);
        }

        @Override
        public void writeAttribute(String localName, String value) throws XMLStreamException {
            writeAttribute(null, XMLConstants.NULL_NS_URI, localName, value);
        }

        @Override
        public void writeAttribute(String namespaceURI, String localName, String value) throws XMLStreamException {
            writeAttribute(null, namespaceURI, localName, value);
        }

        @Override
        public void writeAttribute(String prefix, String namespaceURI, String localName, String value) throws XMLStreamException {
            if (scope.isStartTagClosed()) {
                throw new XMLStreamException("Cannot write attribute: element has children or text");
            }
            if (namespaceURI == null) {
                throw new XMLStreamException("Namespace URI must not be null");
            } else if (XMLConstants.NULL_NS_URI.equals(namespaceURI)) { // no namespace -> no prefix
                if (prefix == null) {
                    prefix = XMLConstants.DEFAULT_NS_PREFIX;
                } else if (!XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
                    throw new XMLStreamException("Cannot write prefixed attribute without a namespace URI: " + prefix);
                }
            } else { // namespace -> prefixed attribute
                if (prefix == null) { // lookup prefix
                    prefix = scope.getNonEmptyPrefix(namespaceURI);
                    if (prefix == null) {
                        throw new XMLStreamException("Namespace URI has not been bound to a prefix: " + namespaceURI);
                    }
                } else if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
                    throw new XMLStreamException("Cannot write attribute without prefix for namespace URI: " + namespaceURI);
                }
            }
            scope.addAttribute(prefix, localName, namespaceURI, value);
            writeAttr(prefix, localName, namespaceURI, value);
        }

        protected final void writeCharacters(Object data, int type) throws XMLStreamException {
            ensureStartTagClosed();
            writeData(data, type);
        }

        @Override
        public void writeCharacters(String text) throws XMLStreamException {
            writeCharacters(text, XMLStreamConstants.CHARACTERS);
        }

        @Override
        public void writeCharacters(char[] text, int start, int length) throws XMLStreamException {
            writeCharacters(new String(text, start, length));
        }

        @Override
        public void writeCData(String data) throws XMLStreamException {
            writeCharacters(data, XMLStreamConstants.CDATA);
        }

        @Override
        public void writeStartDocument() throws XMLStreamException {
            writeStartDocument("UTF-8", null);
        }

        @Override
        public void writeStartDocument(String version) throws XMLStreamException {
            writeStartDocument(null, version);
        }

        @Override
        public void writeStartDocument(String encoding, String version) throws XMLStreamException {
            if (startDocumentWritten || !scope.isRoot()) {
                throw new XMLStreamException("Cannot start document");
            }
            if (version == null) {
                version = "1.0";
            }
            if (encoding == null) {
                writePI("xml", String.format("version=\"%s\"", version));
            } else {
                writePI("xml", String.format("version=\"%s\" encoding=\"%s\"", version, encoding));
            }
            startDocumentWritten = true;
        }

        @Override
        public void writeEndDocument() throws XMLStreamException {
            if (!startDocumentWritten) {
                throw new XMLStreamException("Cannot end document");
            }
            if (!scope.isRoot()) {
                ensureStartTagClosed();
                while (!scope.isRoot()) {
                    writeEndElement();
                }
            }
            startDocumentWritten = false;
        }

        @Override
        public void close() throws XMLStreamException {
            ensureStartTagClosed();
            flush();
        }

        @Override
        public void writeNamespace(String prefix, String namespaceURI) throws XMLStreamException {
            if (scope.isStartTagClosed()) {
                throw new XMLStreamException("Cannot write namespace: element has children or text");
            }
            writeNsDecl(prefix, namespaceURI);
            scope.setPrefix(prefix, namespaceURI);
        }

        @Override
        public void writeDefaultNamespace(String namespaceURI) throws XMLStreamException {
            writeNamespace(XMLConstants.DEFAULT_NS_PREFIX, namespaceURI);
        }

        @Override
        public String getPrefix(String namespaceURI) throws XMLStreamException {
            return scope.getPrefix(namespaceURI);
        }

        @Override
        public void setPrefix(String prefix, String namespaceURI) throws XMLStreamException {
            scope.setPrefix(prefix, namespaceURI);
        }

        @Override
        public void setDefaultNamespace(String namespaceURI) throws XMLStreamException {
            setPrefix(XMLConstants.DEFAULT_NS_PREFIX, namespaceURI);
        }

        @Override
        public void setNamespaceContext(NamespaceContext context) throws XMLStreamException {
            if (!scope.isRoot()) {
                throw new XMLStreamException("This method may only be called once at the start of the document");
            }
            scope = new XMLStreamWriterScope<T>(context, scope.getInfo());
        }

        @Override
        public NamespaceContext getNamespaceContext() {
            return scope;
        }

        @Override
        public void writeComment(String data) throws XMLStreamException {
            ensureStartTagClosed();
            writeData(data, XMLStreamConstants.COMMENT);
        }

        @Override
        public void writeProcessingInstruction(String target) throws XMLStreamException {
            ensureStartTagClosed();
            writePI(target, null);
        }

        @Override
        public void writeProcessingInstruction(String target, String data) throws XMLStreamException {
            ensureStartTagClosed();
            writePI(target, data);
        }

        @Override
        public void writeDTD(String dtd) throws XMLStreamException {
            ensureStartTagClosed();
            writeData(dtd, XMLStreamConstants.DTD);
        }

        @Override
        public void writeEntityRef(String name) throws XMLStreamException {
            ensureStartTagClosed();
            writeData(name, XMLStreamConstants.ENTITY_REFERENCE);
        }

        @Override
        public Object getProperty(String name) throws IllegalArgumentException {
            throw new IllegalArgumentException("Unsupported property: " + name);
        }
    }