/* * 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><alice>bob<edgar/></alice></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><?xml-multiple element-name?></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> { 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 boolean documentArray = false; /** * Create writer instance. * * @param target stream target * @param multiplePI whether to consume <code><xml-multiple?></code> PIs to trigger array start * @param namespaceSeparator namespace prefix separator * @param namespaceDeclarations whether to write namespace declarations */ 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; } 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 (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()) { target.value(null); } } catch (IOException e) { throw new XMLStreamException("Cannot write end element: " + getFieldName(getScope().getPrefix(), getScope().getLocalName()), e); } } @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); } }