2 * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.yangtools.yang.data.codec.gson;
10 import static java.util.Objects.requireNonNull;
11 import static org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream.ANYXML_ARRAY_ELEMENT_ID;
12 import static org.w3c.dom.Node.ELEMENT_NODE;
13 import static org.w3c.dom.Node.TEXT_NODE;
15 import com.google.gson.stream.JsonWriter;
16 import java.io.IOException;
18 import java.util.regex.Pattern;
19 import javax.annotation.RegEx;
20 import javax.xml.transform.dom.DOMSource;
21 import org.opendaylight.yangtools.yang.common.QName;
22 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
24 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
25 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
26 import org.opendaylight.yangtools.yang.data.impl.codec.SchemaTracker;
27 import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode;
28 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
29 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
30 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
31 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
32 import org.w3c.dom.Node;
33 import org.w3c.dom.NodeList;
36 * This implementation will create JSON output as output stream.
39 * Values of leaf and leaf-list are NOT translated according to codecs.
41 public abstract class JSONNormalizedNodeStreamWriter implements NormalizedNodeStreamWriter {
42 private static final class Exclusive extends JSONNormalizedNodeStreamWriter {
43 Exclusive(final JSONCodecFactory codecFactory, final SchemaPath path, final JsonWriter writer,
44 final JSONStreamWriterRootContext rootContext) {
45 super(codecFactory, path, writer, rootContext);
49 public void close() throws IOException {
55 private static final class Nested extends JSONNormalizedNodeStreamWriter {
56 Nested(final JSONCodecFactory codecFactory, final SchemaPath path, final JsonWriter writer,
57 final JSONStreamWriterRootContext rootContext) {
58 super(codecFactory, path, writer, rootContext);
62 public void close() throws IOException {
64 // The caller "owns" the writer, let them close it
69 * RFC6020 deviation: we are not required to emit empty containers unless they
70 * are marked as 'presence'.
72 private static final boolean DEFAULT_EMIT_EMPTY_CONTAINERS = true;
75 private static final String NUMBER_STRING = "-?\\d+(\\.\\d+)?";
76 private static final Pattern NUMBER_PATTERN = Pattern.compile(NUMBER_STRING);
79 private static final String NOT_DECIMAL_NUMBER_STRING = "-?\\d+";
80 private static final Pattern NOT_DECIMAL_NUMBER_PATTERN = Pattern.compile(NOT_DECIMAL_NUMBER_STRING);
82 private final SchemaTracker tracker;
83 private final JSONCodecFactory codecs;
84 private final JsonWriter writer;
85 private JSONStreamWriterContext context;
87 JSONNormalizedNodeStreamWriter(final JSONCodecFactory codecFactory, final SchemaPath path, final JsonWriter writer,
88 final JSONStreamWriterRootContext rootContext) {
89 this.writer = requireNonNull(writer);
90 this.codecs = requireNonNull(codecFactory);
91 this.tracker = SchemaTracker.create(codecFactory.getSchemaContext(), path);
92 this.context = requireNonNull(rootContext);
96 * Create a new stream writer, which writes to the specified output stream.
99 * The codec factory can be reused between multiple writers.
102 * Returned writer is exclusive user of JsonWriter, which means it will start
103 * top-level JSON element and ends it.
106 * This instance of writer can be used only to emit one top level element,
107 * otherwise it will produce incorrect JSON. Closing this instance will close
110 * @param codecFactory JSON codec factory
111 * @param path Schema Path
112 * @param initialNs Initial namespace
113 * @param jsonWriter JsonWriter
114 * @return A stream writer instance
116 public static NormalizedNodeStreamWriter createExclusiveWriter(final JSONCodecFactory codecFactory,
117 final SchemaPath path, final URI initialNs, final JsonWriter jsonWriter) {
118 return new Exclusive(codecFactory, path, jsonWriter, new JSONStreamWriterExclusiveRootContext(initialNs));
122 * Create a new stream writer, which writes to the specified output stream.
125 * The codec factory can be reused between multiple writers.
128 * Returned writer can be used emit multiple top level element,
129 * but does not start / close parent JSON object, which must be done
130 * by user providing {@code jsonWriter} instance in order for
131 * JSON to be valid. Closing this instance <strong>will not</strong>
132 * close the wrapped writer; the caller must take care of that.
134 * @param codecFactory JSON codec factory
135 * @param path Schema Path
136 * @param initialNs Initial namespace
137 * @param jsonWriter JsonWriter
138 * @return A stream writer instance
140 public static NormalizedNodeStreamWriter createNestedWriter(final JSONCodecFactory codecFactory,
141 final SchemaPath path, final URI initialNs, final JsonWriter jsonWriter) {
142 return new Nested(codecFactory, path, jsonWriter, new JSONStreamWriterSharedRootContext(initialNs));
146 public final void leafNode(final NodeIdentifier name, final Object value) throws IOException {
147 final LeafSchemaNode schema = tracker.leafNode(name);
148 final JSONCodec<?> codec = codecs.codecFor(schema);
149 context.emittingChild(codecs.getSchemaContext(), writer);
150 context.writeChildJsonIdentifier(codecs.getSchemaContext(), writer, name.getNodeType());
151 writeValue(value, codec);
155 public final void startLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
156 tracker.startLeafSet(name);
157 context = new JSONStreamWriterListContext(context, name);
161 public final void leafSetEntryNode(final QName name, final Object value) throws IOException {
162 final LeafListSchemaNode schema = tracker.leafSetEntryNode(name);
163 final JSONCodec<?> codec = codecs.codecFor(schema);
164 context.emittingChild(codecs.getSchemaContext(), writer);
165 writeValue(value, codec);
169 public final void startOrderedLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
170 tracker.startLeafSet(name);
171 context = new JSONStreamWriterListContext(context, name);
175 * Warning suppressed due to static final constant which triggers a warning
176 * for the call to schema.isPresenceContainer().
178 @SuppressWarnings("unused")
180 public final void startContainerNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
181 final SchemaNode schema = tracker.startContainerNode(name);
183 // FIXME this code ignores presence for containers
184 // but datastore does as well and it needs be fixed first (2399)
185 context = new JSONStreamWriterNamedObjectContext(context, name, DEFAULT_EMIT_EMPTY_CONTAINERS);
189 public final void startUnkeyedList(final NodeIdentifier name, final int childSizeHint) throws IOException {
190 tracker.startList(name);
191 context = new JSONStreamWriterListContext(context, name);
195 public final void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException {
196 tracker.startListItem(name);
197 context = new JSONStreamWriterObjectContext(context, name, DEFAULT_EMIT_EMPTY_CONTAINERS);
201 public final void startMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
202 tracker.startList(name);
203 context = new JSONStreamWriterListContext(context, name);
207 public final void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint)
209 tracker.startListItem(identifier);
210 context = new JSONStreamWriterObjectContext(context, identifier, DEFAULT_EMIT_EMPTY_CONTAINERS);
214 public final void startOrderedMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
215 tracker.startList(name);
216 context = new JSONStreamWriterListContext(context, name);
220 public final void startChoiceNode(final NodeIdentifier name, final int childSizeHint) {
221 tracker.startChoiceNode(name);
222 context = new JSONStreamWriterInvisibleContext(context);
226 public final void startAugmentationNode(final AugmentationIdentifier identifier) {
227 tracker.startAugmentationNode(identifier);
228 context = new JSONStreamWriterInvisibleContext(context);
232 public final void anyxmlNode(final NodeIdentifier name, final Object value) throws IOException {
233 @SuppressWarnings("unused")
234 final AnyXmlSchemaNode schema = tracker.anyxmlNode(name);
235 // FIXME: should have a codec based on this :)
237 context.emittingChild(codecs.getSchemaContext(), writer);
238 context.writeChildJsonIdentifier(codecs.getSchemaContext(), writer, name.getNodeType());
240 writeAnyXmlValue((DOMSource) value);
244 public final void startYangModeledAnyXmlNode(final NodeIdentifier name, final int childSizeHint)
246 tracker.startYangModeledAnyXmlNode(name);
247 context = new JSONStreamWriterNamedObjectContext(context, name, true);
251 public final void endNode() throws IOException {
253 context = context.endNode(codecs.getSchemaContext(), writer);
255 if (context instanceof JSONStreamWriterRootContext) {
256 context.emitEnd(writer);
261 public final void flush() throws IOException {
265 final void closeWriter() throws IOException {
269 @SuppressWarnings("unchecked")
270 private void writeValue(final Object value, final JSONCodec<?> codec) throws IOException {
271 ((JSONCodec<Object>) codec).writeValue(writer, value);
274 private void writeAnyXmlValue(final DOMSource anyXmlValue) throws IOException {
275 final Node documentNode = anyXmlValue.getNode();
276 final Node firstChild = documentNode.getFirstChild();
277 if (ELEMENT_NODE == firstChild.getNodeType() && !ANYXML_ARRAY_ELEMENT_ID.equals(firstChild.getNodeName())) {
278 writer.beginObject();
279 traverseAnyXmlValue(documentNode);
282 traverseAnyXmlValue(documentNode);
286 private void traverseAnyXmlValue(final Node node) throws IOException {
287 final NodeList children = node.getChildNodes();
288 boolean inArray = false;
290 for (int i = 0, length = children.getLength(); i < length; i++) {
291 final Node childNode = children.item(i);
292 boolean inObject = false;
294 if (ELEMENT_NODE == childNode.getNodeType()) {
295 final Node firstChild = childNode.getFirstChild();
296 // beginning of an array
297 if (ANYXML_ARRAY_ELEMENT_ID.equals(childNode.getNodeName()) && !inArray) {
300 // object at the beginning of the array
301 if (isJsonObjectInArray(childNode, firstChild)) {
302 writer.beginObject();
305 // object in the array
306 } else if (isJsonObjectInArray(childNode, firstChild)) {
307 writer.beginObject();
310 } else if (isJsonObject(firstChild)) {
311 writer.name(childNode.getNodeName());
312 writer.beginObject();
315 } else if (!inArray) {
316 writer.name(childNode.getNodeName());
320 // text value, i.e. a number, string, boolean or null
321 if (TEXT_NODE == childNode.getNodeType()) {
322 final String childNodeText = childNode.getNodeValue();
323 if (NUMBER_PATTERN.matcher(childNodeText).matches()) {
324 writer.value(parseNumber(childNodeText));
325 } else if ("true".equals(childNodeText) || "false".equals(childNodeText)) {
326 writer.value(Boolean.parseBoolean(childNodeText));
327 } else if ("null".equals(childNodeText)) {
330 writer.value(childNodeText);
336 traverseAnyXmlValue(childNode);
348 // json numbers are 64 bit wide floating point numbers - in java terms it is either long or double
349 private static Number parseNumber(final String numberText) {
350 if (NOT_DECIMAL_NUMBER_PATTERN.matcher(numberText).matches()) {
351 return Long.valueOf(numberText);
354 return Double.valueOf(numberText);
357 private static boolean isJsonObject(final Node firstChild) {
358 return !ANYXML_ARRAY_ELEMENT_ID.equals(firstChild.getNodeName()) && TEXT_NODE != firstChild.getNodeType();
361 private static boolean isJsonObjectInArray(final Node node, final Node firstChild) {
362 return ANYXML_ARRAY_ELEMENT_ID.equals(node.getNodeName())
363 && !ANYXML_ARRAY_ELEMENT_ID.equals(firstChild.getNodeName())
364 && TEXT_NODE != firstChild.getNodeType();