/*******************************************************************************
* Copyright (c) 2006-2010 eBay Inc. All Rights Reserved.
* 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
*******************************************************************************/
package org.ebayopensource.turmeric.runtime.binding.impl.parser.json;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import org.ebayopensource.turmeric.runtime.binding.BindingConstants;
import org.ebayopensource.turmeric.runtime.binding.DataBindingOptions;
import org.ebayopensource.turmeric.runtime.binding.impl.parser.BaseStreamWriter;
import org.ebayopensource.turmeric.runtime.binding.impl.parser.IndexedQName;
import org.ebayopensource.turmeric.runtime.binding.impl.parser.NamespaceConvention;
import org.ebayopensource.turmeric.runtime.binding.schema.DataElementSchema;
import org.ebayopensource.turmeric.runtime.binding.utils.BufferedCharWriter;
import org.ebayopensource.turmeric.runtime.binding.utils.CollectionUtils;
/**
*
* @author wdeng
*
*/
public class JSONStreamWriter extends BaseStreamWriter {
public static final String KEY_USE_SCHEMA_INFO = "useSchemaInfo";
public static final String KEY_FORMAT_OUTPUT = "formatOutput";
public static final String KEY_VALUE_KEY = "valueKey";
private static final String KEY_NO_ROOT = DataBindingOptions.NoRoot.getOptionName();
private boolean m_useSchemaInfo = true;
private boolean m_formatOutput = false;
private String m_valueKey = BindingConstants.JSON_VALUE_KEY;
private boolean m_noRoot = false; //true;
private String m_lastWritten=null;
private int m_nestedLevel = 0;
private final BufferedCharWriter m_os;
private boolean m_hasNSDefinition = false;
private boolean m_shouldOutputNamespaceDefs = true;
private String m_defaultNamespace = null;
private boolean m_singleNamespace = false;
private NodeInfo m_currentNodeInfo; // This is the key structure of the writer. See notes on NodeInfo.
private DataElementSchema m_rootEleSchema;
private boolean m_justSkippedRoot = false;
public JSONStreamWriter(NamespaceConvention convention, Charset charset, OutputStream os)
throws XMLStreamException {
this(convention, null, charset, os, CollectionUtils.EMPTY_STRING_MAP);
}
public JSONStreamWriter(NamespaceConvention convention, DataElementSchema rootEleSchema,
Charset charset, OutputStream os, Map<String, String> options)
throws XMLStreamException
{
super(convention);
m_os = new BufferedCharWriter(os, charset, 2048);
m_defaultNamespace = convention.getSingleNamespace();
m_singleNamespace = convention.isSingleNamespace();
setupOptions(options);
if (null == rootEleSchema) {
m_useSchemaInfo = false;
}
if (m_useSchemaInfo) {
m_rootEleSchema = rootEleSchema;
}
}
@Override
public void close() throws XMLStreamException {
try {
m_os.close();
} catch (IOException e) {
throw new XMLStreamException(e);
}
}
@Override
public void flush() throws XMLStreamException {
try {
m_os.flush();
} catch (IOException e) {
throw new XMLStreamException(e);
}
}
@Override
public void writeCharacters(String value) throws XMLStreamException {
try {
m_currentNodeInfo.setHasCharacters(true);
boolean isArray = true;
if (m_useSchemaInfo) {
DataElementSchema elementSchema = m_currentNodeInfo.getElementSchema();
isArray = elementSchema.getMaxOccurs() == -1;
}
if (m_currentNodeInfo.hasAttribute()) {
writeToStream(",");
if (m_formatOutput) {
writeIndentation(m_nestedLevel);
}
writeToStream("\"");
writeToStream(m_valueKey);
writeToStream("\":");
} else if (isArray) {
NodeInfo parent = m_currentNodeInfo.getParent();
if (null == parent || parent.isLastKnownChildWithNewName()) {
writeToStream("[");
}
}
if (value != null) {
writeToStream("\"");
writeToStream(encodeValue(value));
writeToStream("\"");
} else {
writeToStream(BindingConstants.NULL_VALUE_STR);
}
} catch (IOException ioe) {
throw new XMLStreamException(ioe);
}
}
@Override
public void writeAttribute(String prefix, String nsURI, String localName,
String value) throws XMLStreamException {
writeElementName(prefix, localName, nsURI, true);
try {
writeToStream("\"");
writeToStream(encodeValue(value));
writeToStream("\"");
} catch (IOException ioe) {
throw new XMLStreamException(ioe);
}
m_currentNodeInfo.setHasAttribute(true);
}
@Override
public void writeStartElement(String prefix, String localName,
String namespaceURI) throws XMLStreamException
{
if (null == localName) {
throw new XMLStreamException(
"writeStartElement expects non-null local name.");
}
String prefix2 = m_convention.getPrefix(namespaceURI);
writeElementName(prefix2, localName, namespaceURI, false);
createNodeInfo(localName, namespaceURI);
}
@Override
public void writeEndElement() throws XMLStreamException {
try {
closeChildArrayElementsWithBlanket();
// The case that root element is reduced.
if (m_noRoot && m_currentNodeInfo.getParent() == null) {
if (!m_useSchemaInfo && m_currentNodeInfo.getLastKnownChild() == null && (m_currentNodeInfo.hasAttribute() || m_currentNodeInfo.hasCharacters())) {
writeToStream("]");
}
return;
}
NodeInfo lastKnownSibling = getLastKnownChildOfCurrentNode();
if (lastKnownSibling != null) {
writeIndentation(m_nestedLevel-1);
writeToStream("}");
// Break the reference to grand child to release memory
lastKnownSibling.addChild(null);
// Handling case that the element has attribute.
} else if (m_currentNodeInfo.hasAttribute()) {
writeIndentation(m_nestedLevel-1);
writeToStream("}");
}
} catch (IOException ioe) {
throw new XMLStreamException(ioe);
} finally {
// Move m_currentNodeInfo up one level.
m_currentNodeInfo = m_currentNodeInfo.getParent();
m_nestedLevel--;
}
}
public void writeStartDocument() throws XMLStreamException {
try {
writeToStream("{");
writeIndentation(0);
if (m_shouldOutputNamespaceDefs) {
m_shouldOutputNamespaceDefs = false;
if (m_singleNamespace) {
return;
}
// Write the namespace prefix definitions.
Map<String, String> prefixToNSMap = m_convention.getPrefixToNamespaceMap();
Iterator<Entry<String, String>> iter = prefixToNSMap.entrySet().iterator();
while (iter.hasNext()) {
m_hasNSDefinition = true;
Entry<String, String> entry = iter.next();
String prefix = (String)entry.getKey();
String ns = (String)entry.getValue();
writeToStream("\"");
writeToStream(JSONConstants.JSON_NAMESPACE_DEF_PREFIX);
if (!(prefix == null) && !("".equals(prefix))) {
writeToStream(".");
writeToStream(prefix);
}
writeToStream("\":\"");
writeToStream(ns);
writeToStream("\"");
if (iter.hasNext()) {
writeToStream(",");
writeIndentation(0);
}
}
}
} catch (IOException ioe) {
throw new XMLStreamException(ioe);
}
}
@Override
public void writeEndDocument() throws XMLStreamException {
try {
// When schema info is not available, we already need to close the blankets when there is an attribute.
if (!m_useSchemaInfo && !m_noRoot) {
writeToStream("]");
}
if (m_formatOutput) {
writeToStream("\n}\n");
} else {
writeToStream("}");
}
} catch (IOException ioe) {
throw new XMLStreamException(ioe);
}
}
private void writeElementName(String prefix, String localName, String namespaceURI, boolean isAttribute) throws XMLStreamException {
try {
NodeInfo lastKnownSibling = getLastKnownChildOfCurrentNode();
String prevNS = lastKnownSibling == null ? "" : lastKnownSibling.getNamespaceURI();
String prevLocalName = lastKnownSibling == null ? "" : lastKnownSibling.getLocalPart();
if (m_currentNodeInfo == null) {
// Handles the first top level element
if (m_hasNSDefinition) {
writeToStream(",");
}
if( ! m_noRoot ) {
writeCurrentElement(prefix, localName, namespaceURI, isAttribute);
} else {
m_justSkippedRoot = true;
}
} else {
if (!(localName.equals(prevLocalName) && namespaceURI.equals(prevNS)))
{
if( m_justSkippedRoot ) {
m_justSkippedRoot = false;
} else {
closeSiblingArrayElementsWithBlanket();
prepareToStartNewElement();
}
writeCurrentElement(prefix, localName, namespaceURI, isAttribute);
} else { // Array element continuing with the same QName
writeToStream(",");
writeIndentation(m_nestedLevel);
}
}
} catch (IOException ioe) {
throw new XMLStreamException(ioe);
}
}
private void createNodeInfo(String localName, String namespaceURI) throws XMLStreamException {
DataElementSchema eleSchema = getElementSchema(namespaceURI, localName, m_currentNodeInfo);
String prefix;
prefix = m_convention.getPrefix(namespaceURI);
int index = 0;
QName nodeName = createQName(m_convention, prefix, localName);
if (null != m_currentNodeInfo && m_currentNodeInfo.sameQName(nodeName)) {
index = m_currentNodeInfo.getIndex() + 1;
}
NodeInfo currentNodeInfo = new NodeInfo(nodeName, index, eleSchema, m_currentNodeInfo);
m_nestedLevel++;
if (null != m_currentNodeInfo) {
m_currentNodeInfo.addChild(currentNodeInfo);
}
m_currentNodeInfo = currentNodeInfo;
}
private void writeIndentation(int level) throws IOException {
if (m_formatOutput) {
writeToStream("\n");
for (int i = 0; i <= level; i++) {
writeToStream("\t");
}
}
}
private void prepareToStartNewElement() throws IOException, XMLStreamException {
if (m_currentNodeInfo == null || m_currentNodeInfo.getLastKnownChild() == null) {
// Processing elements at one level higher.
// For element that has attribute;
if (m_currentNodeInfo != null && m_currentNodeInfo.hasAttribute() ) {
writeToStream(",");
return;
}
// For elements without attribute;
NodeInfo parent = m_currentNodeInfo == null ? null : m_currentNodeInfo.getParent();
boolean isFirstArrayElement = parent == null ? true : parent.isLastKnownChildWithNewName();
if (m_useSchemaInfo) {
// Use Schema Info to reduce the use of blankets
DataElementSchema currentNodeSchema = m_currentNodeInfo.getElementSchema();
isFirstArrayElement = isFirstArrayElement && (currentNodeSchema == null ? false : (currentNodeSchema.getMaxOccurs() == -1));
}
if (isFirstArrayElement && !m_currentNodeInfo.hasAttribute()){
writeToStream("[{");
} else {
writeToStream("{");
}
return;
}
// For elements at the same level
writeToStream(",");
}
private NodeInfo getLastKnownChildOfCurrentNode() {
if (m_currentNodeInfo == null) {
return null;
}
return m_currentNodeInfo.getLastKnownChild();
}
private void writeCurrentElement(String prefix, String localName, String namespaceURI, boolean isAttribute) throws IOException {
writeIndentation(m_nestedLevel);
writeToStream("\"");
if ((!m_singleNamespace || !m_defaultNamespace.equals(namespaceURI) || isAttribute)
&& prefix != null && prefix.length() != 0) {
writeToStream(prefix);
writeToStream(".");
}
if (isAttribute) {
writeToStream(BindingConstants.ATTRIBUTE_MARK);
}
writeToStream(localName);
writeToStream("\":");
}
static QName createQName(NamespaceConvention convention, String prefix, String name) {
QName qname = null;
if (prefix != null) {
// TODO: use function that checks for no namespace
String xns = convention.getNamespaceUriNoChecks(prefix);
if (xns == null) {
qname = new QName("", name, prefix);
} else {
qname = new QName(xns, name, prefix);
}
} else {
qname = new QName(name);
}
return qname;
}
private void setupOptions(Map<String, String>options) {
String useSchemaInfoOption = options.get(KEY_USE_SCHEMA_INFO);
if (null != useSchemaInfoOption) {
m_useSchemaInfo = Boolean.parseBoolean(useSchemaInfoOption);
}
String formatOutput = options.get(KEY_FORMAT_OUTPUT);
if (null != formatOutput) {
m_formatOutput = Boolean.parseBoolean(formatOutput);
}
String valueKey = options.get(KEY_VALUE_KEY);
if (null != valueKey && valueKey.length() > 0) {
m_valueKey = valueKey;
}
String noRootValue = options.get(KEY_NO_ROOT);
if( noRootValue != null ) {
m_noRoot = Boolean.parseBoolean(noRootValue);
}
}
private DataElementSchema getElementSchema(String namespaceURI, String localName, NodeInfo parentNode) throws XMLStreamException {
DataElementSchema eleSchema = null;
if (!m_useSchemaInfo) {
return null;
}
if (parentNode == null) {
eleSchema = m_rootEleSchema;
} else {
DataElementSchema parentEleSchema = parentNode.getElementSchema();
eleSchema = parentEleSchema.getChild(namespaceURI, localName);
}
if (eleSchema == null) {
throw new XMLStreamException("Unable to load schema information for: " + localName);
}
return eleSchema;
}
public static final String encodeValue(String s) {
if(s == null)
return "";
StringBuffer sbuf = new StringBuffer();
for(int i=0; i<s.length(); i++) {
char c = s.charAt(i);
switch(c) {
case '"':
sbuf.append("\\\"");
break;
case '\\':
sbuf.append("\\\\");
break;
case '/':
sbuf.append("\\/");
break;
case '\b':
sbuf.append("\\b");
break;
case '\t':
sbuf.append("\\t");
break;
case '\n':
sbuf.append("\\n");
break;
case '\f':
sbuf.append("\\f");
break;
case '\r':
sbuf.append("\\r");
break;
default:
if( c < ' ' || (c >= '\u0080' && c < '\u00a0') ||
(c >= '\u2000' && c < '\u2100') ) {
String s1 = Integer.toHexString(c);
while(s1.length()<4) {
s1 = "0" + s1;
}
sbuf.append("\\u" + s1);
} else {
sbuf.append(c);
}
break;
}
}
return sbuf.toString();
}
// This is to close the sibling element before we start with the element currently started to be written.
private void closeSiblingArrayElementsWithBlanket() throws IOException {
NodeInfo lastKnownSibling = getLastKnownChildOfCurrentNode();
if (lastKnownSibling == null) {
// This is the first node at this level. nothing to close.
return;
}
if (m_useSchemaInfo) {
if (lastKnownSibling.getElementSchema().getMaxOccurs() == -1) {
writeToStream("]");
}
return;
}
// When schema info is not available, we already need to close the blankets when there is an attribute.
if (lastKnownSibling.hasAttribute()) {
writeToStream("]");
return;
}
if (lastKnownSibling.getLastKnownChild() != null || lastKnownSibling.hasCharacters() || lastKnownSibling.hasAttribute()) {
writeToStream("]");
}
}
private void closeChildArrayElementsWithBlanket() throws IOException {
NodeInfo lastKnownChild = getLastKnownChildOfCurrentNode();
if (lastKnownChild == null ) {
return;
}
if (m_useSchemaInfo) {
if (lastKnownChild.getElementSchema().getMaxOccurs() == -1) {
writeToStream("]");
}
return;
}
writeToStream("]");
return;
}
private void writeToStream(String str) throws IOException{
//check for exceptional cases
if (m_lastWritten!=null){
if (m_lastWritten.endsWith(":") &&
( str.startsWith(",") || str.startsWith("\n}") || str.startsWith("}")) ){
m_os.write("null");
}
}
m_os.write(str);
m_lastWritten = str;
}
/**
* This is the key structure for the writer. Through parent variable,
* it keep a stack of all the nested nodes. Through lastKnownChild variable, it keeps track
* of the last node that has been processed. When the stack level retreat, we break the
* lastKnownChild link, so no DOM is maintained.
*
* @author wdeng
*
*/
private static final class NodeInfo extends IndexedQName {
NodeInfo m_lastKnownChild;
boolean m_isLastKnownChildWithNewName = true;
boolean m_hasAttribute = false;
boolean m_hasCharacters = false;
DataElementSchema m_elementSchema;
NodeInfo m_parent;
NodeInfo(QName qName, int index, DataElementSchema schema, NodeInfo parent) {
super(qName, index);
m_lastKnownChild = null;
m_elementSchema = schema;
m_parent = parent;
}
NodeInfo getParent() {
return m_parent;
}
boolean isLastKnownChildWithNewName() {
return m_isLastKnownChildWithNewName;
}
NodeInfo getLastKnownChild() {
return m_lastKnownChild;
}
DataElementSchema getElementSchema() {
return m_elementSchema;
}
void addChild(NodeInfo child) {
// If the newly added child has new QName than the previous child, set
// m_isLastKnownChildWithNewName to true.
if (null == child) {
m_isLastKnownChildWithNewName = false;
m_lastKnownChild = null;
return;
}
if (m_lastKnownChild == null) {
m_isLastKnownChildWithNewName = true;
} else {
m_isLastKnownChildWithNewName = !child.getNamespaceURI().equals(m_lastKnownChild.getNamespaceURI())
|| !child.getLocalPart().equals(m_lastKnownChild.getLocalPart());
}
m_lastKnownChild = child;
}
boolean hasAttribute() {
return m_hasAttribute;
}
void setHasAttribute(boolean attribute) {
m_hasAttribute = attribute;
}
boolean hasCharacters() {
return m_hasCharacters;
}
void setHasCharacters(boolean hasCharacters) {
this.m_hasCharacters = hasCharacters;
}
@Override
public int hashCode() {
return super.hashCode() ^ m_lastKnownChild.hashCode() ^ m_elementSchema.hashCode()
^ m_parent.hashCode() ^ (m_isLastKnownChildWithNewName ? 0 : 1)
^ (m_hasCharacters ? 0 : 2)
^ (m_hasAttribute ? 0 : 8);
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (other == null || !(other instanceof NodeInfo)) {
return false;
}
NodeInfo other2 = (NodeInfo)other;
if (! equals (m_parent, other2.m_parent)) {
return false;
}
if (! equals (m_elementSchema, other2.m_elementSchema)) {
return false;
}
if (! equals (m_lastKnownChild, other2.m_lastKnownChild)) {
return false;
}
if (m_isLastKnownChildWithNewName != other2.m_isLastKnownChildWithNewName) {
return false;
}
if (m_hasAttribute != other2.m_hasAttribute) {
return false;
}
if (m_hasCharacters != other2.m_hasCharacters) {
return false;
}
return super.equals(other);
}
private boolean equals(Object o1, Object o2) {
if (o1 == null) {
return o2 == null;
}
return o1.equals(o2);
}
}
}