*/
package org.opendaylight.yangtools.yang.data.codec.gson;
-import com.google.common.base.CharMatcher;
+import static org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream.ANYXML_ARRAY_ELEMENT_ID;
+import static org.w3c.dom.Node.ELEMENT_NODE;
+import static org.w3c.dom.Node.TEXT_NODE;
+
import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.net.URI;
+import java.util.regex.Pattern;
+import javax.annotation.RegEx;
+import javax.xml.transform.dom.DOMSource;
+import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
import org.opendaylight.yangtools.yang.data.impl.codec.SchemaTracker;
import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
import org.opendaylight.yangtools.yang.model.api.SchemaPath;
-
-import java.io.IOException;
-import java.io.Writer;
-import java.net.URI;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
/**
* This implementation will create JSON output as output stream.
*
* Values of leaf and leaf-list are NOT translated according to codecs.
*
- * FIXME: rewrite this in terms of {@link JsonWriter}.
*/
-public class JSONNormalizedNodeStreamWriter implements NormalizedNodeStreamWriter {
+public final class JSONNormalizedNodeStreamWriter implements NormalizedNodeStreamWriter {
/**
* RFC6020 deviation: we are not required to emit empty containers unless they
* are marked as 'presence'.
*/
private static final boolean DEFAULT_EMIT_EMPTY_CONTAINERS = true;
- /**
- * Matcher used to check if a string needs to be escaped.
- */
- private static final CharMatcher JSON_ILLEGAL_STRING_CHARACTERS = CharMatcher.anyOf("\\\"\n\r");
+ @RegEx
+ private static final String NUMBER_STRING = "-?\\d+(\\.\\d+)?";
+ private static final Pattern NUMBER_PATTERN = Pattern.compile(NUMBER_STRING);
+
+ @RegEx
+ private static final String NOT_DECIMAL_NUMBER_STRING = "-?\\d+";
+ private static final Pattern NOT_DECIMAL_NUMBER_PATTERN = Pattern.compile(NOT_DECIMAL_NUMBER_STRING);
private final SchemaTracker tracker;
private final JSONCodecFactory codecs;
- private final Writer writer;
- private final String indent;
+ private final JsonWriter writer;
private JSONStreamWriterContext context;
- private JSONNormalizedNodeStreamWriter(final JSONCodecFactory codecFactory, final SchemaPath path,
- final Writer writer, final URI initialNs, final int indentSize) {
- this.writer = Preconditions.checkNotNull(writer);
-
- Preconditions.checkArgument(indentSize >= 0, "Indent size must be non-negative");
- if (indentSize != 0) {
- indent = Strings.repeat(" ", indentSize);
- } else {
- indent = null;
- }
+ private JSONNormalizedNodeStreamWriter(final JSONCodecFactory codecFactory, final SchemaPath path, final JsonWriter JsonWriter, final JSONStreamWriterRootContext rootContext) {
+ this.writer = Preconditions.checkNotNull(JsonWriter);
this.codecs = Preconditions.checkNotNull(codecFactory);
this.tracker = SchemaTracker.create(codecFactory.getSchemaContext(), path);
- this.context = new JSONStreamWriterRootContext(initialNs);
+ this.context = Preconditions.checkNotNull(rootContext);
}
/**
- * Create a new stream writer, which writes to the specified {@link Writer}.
+ * Create a new stream writer, which writes to the specified output stream.
*
- * @param schemaContext Schema context
- * @param writer Output writer
- * @return A stream writer instance
- */
- public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final Writer writer) {
- return new JSONNormalizedNodeStreamWriter(JSONCodecFactory.create(schemaContext), SchemaPath.ROOT, writer, null, 0);
- }
-
- /**
- * Create a new stream writer, which writes to the specified {@link Writer}.
+ * The codec factory can be reused between multiple writers.
*
- * @param schemaContext Schema context
- * @param path Root schemapath
- * @param writer Output writer
- * @return A stream writer instance
- */
- public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final SchemaPath path, final Writer writer) {
- return new JSONNormalizedNodeStreamWriter(JSONCodecFactory.create(schemaContext), path, writer, null, 0);
- }
-
- /**
- * Create a new stream writer, which writes to the specified {@link Writer}.
+ * Returned writer is exclusive user of JsonWriter, which means it will start
+ * top-level JSON element and ends it.
+ *
+ * This instance of writer can be used only to emit one top level element,
+ * otherwise it will produce incorrect JSON.
*
- * @param schemaContext Schema context
- * @param path Root schemapath
- * @param writer Output writer
+ * @param codecFactory JSON codec factory
+ * @param path Schema Path
* @param initialNs Initial namespace
+ * @param jsonWriter JsonWriter
* @return A stream writer instance
*/
- public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final SchemaPath path,
- final URI initialNs, final Writer writer) {
- return new JSONNormalizedNodeStreamWriter(JSONCodecFactory.create(schemaContext), path, writer, initialNs, 0);
+ public static NormalizedNodeStreamWriter createExclusiveWriter(final JSONCodecFactory codecFactory, final SchemaPath path, final URI initialNs, final JsonWriter jsonWriter) {
+ return new JSONNormalizedNodeStreamWriter(codecFactory, path, jsonWriter, new JSONStreamWriterExclusiveRootContext(initialNs));
}
/**
* Create a new stream writer, which writes to the specified output stream.
*
- * @param schemaContext Schema context
- * @param writer Output writer
- * @param indentSize indentation size
- * @return A stream writer instance
- */
- public static NormalizedNodeStreamWriter create(final SchemaContext schemaContext, final Writer writer, final int indentSize) {
- return new JSONNormalizedNodeStreamWriter(JSONCodecFactory.create(schemaContext), SchemaPath.ROOT, writer, null, indentSize);
- }
-
- /**
- * Create a new stream writer, which writes to the specified output stream. The codec factory
- * can be reused between multiple writers.
+ * The codec factory can be reused between multiple writers.
+ *
+ * Returned writer can be used emit multiple top level element,
+ * but does not start / close parent JSON object, which must be done
+ * by user providing {@code jsonWriter} instance in order for
+ * JSON to be valid.
*
- * @param codecFactor JSON codec factory
- * @param writer Output writer
- * @param indentSize indentation size
+ * @param codecFactory JSON codec factory
+ * @param path Schema Path
+ * @param initialNs Initial namespace
+ * @param jsonWriter JsonWriter
* @return A stream writer instance
*/
- public static NormalizedNodeStreamWriter create(final JSONCodecFactory codecFactory, final Writer writer, final int indentSize) {
- return new JSONNormalizedNodeStreamWriter(codecFactory, SchemaPath.ROOT, writer, null, indentSize);
+ public static NormalizedNodeStreamWriter createNestedWriter(final JSONCodecFactory codecFactory, final SchemaPath path, final URI initialNs, final JsonWriter jsonWriter) {
+ return new JSONNormalizedNodeStreamWriter(codecFactory, path, jsonWriter, new JSONStreamWriterSharedRootContext(initialNs));
}
@Override
public void leafNode(final NodeIdentifier name, final Object value) throws IOException {
final LeafSchemaNode schema = tracker.leafNode(name);
- final JSONCodec<Object> codec = codecs.codecFor(schema.getType());
-
- context.emittingChild(codecs.getSchemaContext(), writer, indent);
+ final JSONCodec<?> codec = codecs.codecFor(schema);
+ context.emittingChild(codecs.getSchemaContext(), writer);
context.writeChildJsonIdentifier(codecs.getSchemaContext(), writer, name.getNodeType());
- writeValue(codec.serialize(value), codec.needQuotes());
+ writeValue(value, codec);
}
@Override
}
@Override
- public void leafSetEntryNode(final Object value) throws IOException {
- final LeafListSchemaNode schema = tracker.leafSetEntryNode();
- final JSONCodec<Object> codec = codecs.codecFor(schema.getType());
+ public void leafSetEntryNode(final QName name, final Object value) throws IOException {
+ final LeafListSchemaNode schema = tracker.leafSetEntryNode(name);
+ final JSONCodec<?> codec = codecs.codecFor(schema);
+ context.emittingChild(codecs.getSchemaContext(), writer);
+ writeValue(value, codec);
+ }
- context.emittingChild(codecs.getSchemaContext(), writer, indent);
- writeValue(codec.serialize(value), codec.needQuotes());
+ @Override
+ public void startOrderedLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
+ tracker.startLeafSet(name);
+ context = new JSONStreamWriterListContext(context, name);
}
/*
@SuppressWarnings("unused")
@Override
public void startContainerNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
- final ContainerSchemaNode schema = tracker.startContainerNode(name);
- context = new JSONStreamWriterNamedObjectContext(context, name, DEFAULT_EMIT_EMPTY_CONTAINERS || schema.isPresenceContainer());
+ final SchemaNode schema = tracker.startContainerNode(name);
+
+ // FIXME this code ignores presence for containers
+ // but datastore does as well and it needs be fixed first (2399)
+ context = new JSONStreamWriterNamedObjectContext(context, name, DEFAULT_EMIT_EMPTY_CONTAINERS);
}
@Override
final AnyXmlSchemaNode schema = tracker.anyxmlNode(name);
// FIXME: should have a codec based on this :)
- context.emittingChild(codecs.getSchemaContext(), writer, indent);
+ context.emittingChild(codecs.getSchemaContext(), writer);
context.writeChildJsonIdentifier(codecs.getSchemaContext(), writer, name.getNodeType());
- writeValue(String.valueOf(value), true);
+
+ writeAnyXmlValue((DOMSource) value);
+ }
+
+ @Override
+ public void startYangModeledAnyXmlNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
+ tracker.startYangModeledAnyXmlNode(name);
+ context = new JSONStreamWriterNamedObjectContext(context, name, true);
}
@Override
public void endNode() throws IOException {
tracker.endNode();
- context = context.endNode(codecs.getSchemaContext(), writer, indent);
+ context = context.endNode(codecs.getSchemaContext(), writer);
+
+ if (context instanceof JSONStreamWriterRootContext) {
+ context.emitEnd(writer);
+ }
}
- private void writeValue(final String str, final boolean needQuotes) throws IOException {
- if (needQuotes) {
- writer.append('"');
+ @SuppressWarnings("unchecked")
+ private void writeValue(final Object value, final JSONCodec<?> codec)
+ throws IOException {
+ try {
+ ((JSONCodec<Object>) codec).writeValue(writer, value);
+ } catch (IOException e) {
+ throw e;
+ } catch (Exception e) {
+ Throwables.propagateIfPossible(e);
+ throw new RuntimeException(e);
+ }
+ }
- final int needEscape = JSON_ILLEGAL_STRING_CHARACTERS.countIn(str);
- if (needEscape != 0) {
- final char[] escaped = new char[str.length() + needEscape];
- int offset = 0;
+ private void writeAnyXmlValue(final DOMSource anyXmlValue) throws IOException {
+ final Node documentNode = anyXmlValue.getNode();
+ final Node firstChild = documentNode.getFirstChild();
+ if (ELEMENT_NODE == firstChild.getNodeType() && !ANYXML_ARRAY_ELEMENT_ID.equals(firstChild.getNodeName())) {
+ writer.beginObject();
+ traverseAnyXmlValue(documentNode);
+ writer.endObject();
+ } else {
+ traverseAnyXmlValue(documentNode);
+ }
+ }
- for (int i = 0; i < str.length(); i++) {
- final char c = str.charAt(i);
- if (JSON_ILLEGAL_STRING_CHARACTERS.matches(c)) {
- escaped[offset++] = '\\';
+ private void traverseAnyXmlValue(final Node node) throws IOException {
+ final NodeList children = node.getChildNodes();
+ boolean inArray = false;
+
+ for (int i = 0, length = children.getLength(); i < length; i++) {
+ final Node childNode = children.item(i);
+ boolean inObject = false;
+
+ if (ELEMENT_NODE == childNode.getNodeType()) {
+ final Node firstChild = childNode.getFirstChild();
+ // beginning of an array
+ if (ANYXML_ARRAY_ELEMENT_ID.equals(childNode.getNodeName()) && !inArray) {
+ writer.beginArray();
+ inArray = true;
+ // object at the beginning of the array
+ if (isJsonObjectInArray(childNode, firstChild)) {
+ writer.beginObject();
+ inObject = true;
}
- escaped[offset++] = c;
+ // object in the array
+ } else if (isJsonObjectInArray(childNode, firstChild)) {
+ writer.beginObject();
+ inObject = true;
+ // object
+ } else if (isJsonObject(firstChild)) {
+ writer.name(childNode.getNodeName());
+ writer.beginObject();
+ inObject = true;
+ // name
+ } else if (!inArray){
+ writer.name(childNode.getNodeName());
}
- writer.write(escaped);
- } else {
- writer.append(str);
}
- writer.append('"');
- } else {
- writer.append(str);
+ // text value, i.e. a number, string, boolean or null
+ if (TEXT_NODE == childNode.getNodeType()) {
+ final String childNodeText = childNode.getNodeValue();
+ if (NUMBER_PATTERN.matcher(childNodeText).matches()) {
+ writer.value(parseNumber(childNodeText));
+ } else if ("true".equals(childNodeText) || "false".equals(childNodeText)) {
+ writer.value(Boolean.parseBoolean(childNodeText));
+ } else if ("null".equals(childNodeText)) {
+ writer.nullValue();
+ } else {
+ writer.value(childNodeText);
+ }
+
+ return;
+ }
+
+ traverseAnyXmlValue(childNode);
+
+ if (inObject) {
+ writer.endObject();
+ }
+ }
+
+ if (inArray) {
+ writer.endArray();
}
}
+ // json numbers are 64 bit wide floating point numbers - in java terms it is either long or double
+ private static Number parseNumber(final String numberText) {
+ if (NOT_DECIMAL_NUMBER_PATTERN.matcher(numberText).matches()) {
+ return Long.valueOf(numberText);
+ }
+
+ return Double.valueOf(numberText);
+ }
+
+ private static boolean isJsonObject(final Node firstChild) {
+ return !ANYXML_ARRAY_ELEMENT_ID.equals(firstChild.getNodeName()) && TEXT_NODE != firstChild.getNodeType();
+ }
+
+ private static boolean isJsonObjectInArray(final Node node, final Node firstChild) {
+ return ANYXML_ARRAY_ELEMENT_ID.equals(node.getNodeName())
+ && !ANYXML_ARRAY_ELEMENT_ID.equals(firstChild.getNodeName())
+ && TEXT_NODE != firstChild.getNodeType();
+ }
+
@Override
public void flush() throws IOException {
writer.flush();
@Override
public void close() throws IOException {
- writer.flush();
+ flush();
writer.close();
}
-
}