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 com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Preconditions.checkState;
12 import static java.util.Objects.requireNonNull;
14 import com.google.common.annotations.Beta;
15 import com.google.gson.JsonIOException;
16 import com.google.gson.JsonParseException;
17 import com.google.gson.JsonSyntaxException;
18 import com.google.gson.stream.JsonReader;
19 import com.google.gson.stream.MalformedJsonException;
20 import java.io.Closeable;
21 import java.io.EOFException;
22 import java.io.Flushable;
23 import java.io.IOException;
25 import java.util.AbstractMap.SimpleImmutableEntry;
26 import java.util.ArrayDeque;
27 import java.util.Deque;
28 import java.util.HashSet;
29 import java.util.Iterator;
30 import java.util.Map.Entry;
32 import javax.xml.transform.dom.DOMSource;
33 import org.eclipse.jdt.annotation.NonNull;
34 import org.opendaylight.yangtools.odlext.model.api.YangModeledAnyxmlSchemaNode;
35 import org.opendaylight.yangtools.util.xml.UntrustedXML;
36 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
37 import org.opendaylight.yangtools.yang.data.util.AbstractNodeDataWithSchema;
38 import org.opendaylight.yangtools.yang.data.util.AnyXmlNodeDataWithSchema;
39 import org.opendaylight.yangtools.yang.data.util.CompositeNodeDataWithSchema;
40 import org.opendaylight.yangtools.yang.data.util.LeafListEntryNodeDataWithSchema;
41 import org.opendaylight.yangtools.yang.data.util.LeafListNodeDataWithSchema;
42 import org.opendaylight.yangtools.yang.data.util.LeafNodeDataWithSchema;
43 import org.opendaylight.yangtools.yang.data.util.ListEntryNodeDataWithSchema;
44 import org.opendaylight.yangtools.yang.data.util.ListNodeDataWithSchema;
45 import org.opendaylight.yangtools.yang.data.util.OperationAsContainer;
46 import org.opendaylight.yangtools.yang.data.util.ParserStreamUtils;
47 import org.opendaylight.yangtools.yang.data.util.SimpleNodeDataWithSchema;
48 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
51 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.Module;
53 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
54 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
55 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
56 import org.w3c.dom.Document;
57 import org.w3c.dom.Element;
58 import org.w3c.dom.Text;
61 * This class parses JSON elements from a GSON JsonReader. It disallows multiple elements of the same name unlike the
62 * default GSON JsonParser.
65 public final class JsonParserStream implements Closeable, Flushable {
66 static final String ANYXML_ARRAY_ELEMENT_ID = "array-element";
68 private final Deque<URI> namespaces = new ArrayDeque<>();
69 private final NormalizedNodeStreamWriter writer;
70 private final JSONCodecFactory codecs;
71 private final DataSchemaNode parentNode;
73 private JsonParserStream(final NormalizedNodeStreamWriter writer, final JSONCodecFactory codecs,
74 final DataSchemaNode parentNode) {
75 this.writer = requireNonNull(writer);
76 this.codecs = requireNonNull(codecs);
77 this.parentNode = parentNode;
81 * Create a new {@link JsonParserStream} backed by specified {@link NormalizedNodeStreamWriter}
82 * and {@link JSONCodecFactory}. The stream will be logically rooted at the top of the SchemaContext associated
83 * with the specified codec factory.
85 * @param writer NormalizedNodeStreamWriter to use for instantiation of normalized nodes
86 * @param codecFactory {@link JSONCodecFactory} to use for parsing leaves
87 * @return A new {@link JsonParserStream}
88 * @throws NullPointerException if any of the arguments are null
90 public static @NonNull JsonParserStream create(final @NonNull NormalizedNodeStreamWriter writer,
91 final @NonNull JSONCodecFactory codecFactory) {
92 return new JsonParserStream(writer, codecFactory, codecFactory.getSchemaContext());
96 * Create a new {@link JsonParserStream} backed by specified {@link NormalizedNodeStreamWriter}
97 * and {@link JSONCodecFactory}. The stream will be logically rooted at the specified parent node.
99 * @param writer NormalizedNodeStreamWriter to use for instantiation of normalized nodes
100 * @param codecFactory {@link JSONCodecFactory} to use for parsing leaves
101 * @param parentNode Logical root node
102 * @return A new {@link JsonParserStream}
103 * @throws NullPointerException if any of the arguments are null
105 public static @NonNull JsonParserStream create(final @NonNull NormalizedNodeStreamWriter writer,
106 final @NonNull JSONCodecFactory codecFactory, final @NonNull SchemaNode parentNode) {
107 final DataSchemaNode parent;
108 if (parentNode instanceof DataSchemaNode) {
109 parent = (DataSchemaNode) parentNode;
110 } else if (parentNode instanceof OperationDefinition) {
111 parent = OperationAsContainer.of((OperationDefinition) parentNode);
113 throw new IllegalArgumentException("Illegal parent node " + requireNonNull(parentNode));
115 return new JsonParserStream(writer, codecFactory, parent);
118 public JsonParserStream parse(final JsonReader reader) {
119 // code copied from gson's JsonParser and Stream classes
121 final boolean lenient = reader.isLenient();
122 reader.setLenient(true);
123 boolean isEmpty = true;
127 final CompositeNodeDataWithSchema<?> compositeNodeDataWithSchema =
128 new CompositeNodeDataWithSchema<>(parentNode);
129 read(reader, compositeNodeDataWithSchema);
130 compositeNodeDataWithSchema.write(writer);
133 } catch (final EOFException e) {
137 // The stream ended prematurely so it is likely a syntax error.
138 throw new JsonSyntaxException(e);
139 } catch (final MalformedJsonException | NumberFormatException e) {
140 throw new JsonSyntaxException(e);
141 } catch (final IOException e) {
142 throw new JsonIOException(e);
143 } catch (StackOverflowError | OutOfMemoryError e) {
144 throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e);
146 reader.setLenient(lenient);
150 private void traverseAnyXmlValue(final JsonReader in, final Document doc, final Element parentElement)
155 Text textNode = doc.createTextNode(in.nextString());
156 parentElement.appendChild(textNode);
159 textNode = doc.createTextNode(Boolean.toString(in.nextBoolean()));
160 parentElement.appendChild(textNode);
164 textNode = doc.createTextNode("null");
165 parentElement.appendChild(textNode);
169 while (in.hasNext()) {
170 final Element childElement = doc.createElement(ANYXML_ARRAY_ELEMENT_ID);
171 parentElement.appendChild(childElement);
172 traverseAnyXmlValue(in, doc, childElement);
178 while (in.hasNext()) {
179 final Element childElement = doc.createElement(in.nextName());
180 parentElement.appendChild(childElement);
181 traverseAnyXmlValue(in, doc, childElement);
190 private void readAnyXmlValue(final JsonReader in, final AnyXmlNodeDataWithSchema parent,
191 final String anyXmlObjectName) throws IOException {
192 final String anyXmlObjectNS = getCurrentNamespace().toString();
193 final Document doc = UntrustedXML.newDocumentBuilder().newDocument();
194 final Element rootElement = doc.createElementNS(anyXmlObjectNS, anyXmlObjectName);
195 doc.appendChild(rootElement);
196 traverseAnyXmlValue(in, doc, rootElement);
198 final DOMSource domSource = new DOMSource(doc.getDocumentElement());
199 parent.setValue(domSource);
202 public void read(final JsonReader in, AbstractNodeDataWithSchema<?> parent) throws IOException {
206 setValue(parent, in.nextString());
209 setValue(parent, Boolean.toString(in.nextBoolean()));
213 setValue(parent, null);
217 while (in.hasNext()) {
218 if (parent instanceof LeafNodeDataWithSchema) {
221 final AbstractNodeDataWithSchema<?> newChild = newArrayEntry(parent);
228 final Set<String> namesakes = new HashSet<>();
231 * This allows parsing of incorrectly /as showcased/
232 * in testconf nesting of list items - eg.
233 * lists with one value are sometimes serialized
234 * without wrapping array.
237 if (isArray(parent)) {
238 parent = newArrayEntry(parent);
240 while (in.hasNext()) {
241 final String jsonElementName = in.nextName();
242 DataSchemaNode parentSchema = parent.getSchema();
243 if (parentSchema instanceof YangModeledAnyxmlSchemaNode) {
244 parentSchema = ((YangModeledAnyxmlSchemaNode) parentSchema).getSchemaOfAnyXmlData();
246 final Entry<String, URI> namespaceAndName = resolveNamespace(jsonElementName, parentSchema);
247 final String localName = namespaceAndName.getKey();
248 addNamespace(namespaceAndName.getValue());
249 if (!namesakes.add(jsonElementName)) {
250 throw new JsonSyntaxException("Duplicate name " + jsonElementName + " in JSON input.");
253 final Deque<DataSchemaNode> childDataSchemaNodes =
254 ParserStreamUtils.findSchemaNodeByNameAndNamespace(parentSchema, localName,
255 getCurrentNamespace());
256 checkState(!childDataSchemaNodes.isEmpty(),
257 "Schema for node with name %s and namespace %s does not exist at %s",
258 localName, getCurrentNamespace(), parentSchema.getPath());
261 final AbstractNodeDataWithSchema<?> newChild = ((CompositeNodeDataWithSchema<?>) parent)
262 .addChild(childDataSchemaNodes);
263 if (newChild instanceof AnyXmlNodeDataWithSchema) {
264 readAnyXmlValue(in, (AnyXmlNodeDataWithSchema) newChild, jsonElementName);
277 private static boolean isArray(final AbstractNodeDataWithSchema<?> parent) {
278 return parent instanceof ListNodeDataWithSchema || parent instanceof LeafListNodeDataWithSchema;
281 private static AbstractNodeDataWithSchema<?> newArrayEntry(final AbstractNodeDataWithSchema<?> parent) {
282 AbstractNodeDataWithSchema<?> newChild;
283 if (parent instanceof ListNodeDataWithSchema) {
284 newChild = ListEntryNodeDataWithSchema.forSchema(((ListNodeDataWithSchema) parent).getSchema());
285 } else if (parent instanceof LeafListNodeDataWithSchema) {
286 newChild = new LeafListEntryNodeDataWithSchema(((LeafListNodeDataWithSchema) parent).getSchema());
288 throw new IllegalStateException("Found an unexpected array nested under " + parent.getSchema().getQName());
290 ((CompositeNodeDataWithSchema<?>) parent).addChild(newChild);
294 private void setValue(final AbstractNodeDataWithSchema<?> parent, final String value) {
295 checkArgument(parent instanceof SimpleNodeDataWithSchema, "Node %s is not a simple type",
296 parent.getSchema().getQName());
297 final SimpleNodeDataWithSchema<?> parentSimpleNode = (SimpleNodeDataWithSchema<?>) parent;
298 checkArgument(parentSimpleNode.getValue() == null, "Node '%s' has already set its value to '%s'",
299 parentSimpleNode.getSchema().getQName(), parentSimpleNode.getValue());
301 final Object translatedValue = translateValueByType(value, parentSimpleNode.getSchema());
302 parentSimpleNode.setValue(translatedValue);
305 private Object translateValueByType(final String value, final DataSchemaNode node) {
306 checkArgument(node instanceof TypedDataSchemaNode);
307 return codecs.codecFor((TypedDataSchemaNode) node).parseValue(null, value);
310 private void removeNamespace() {
314 private void addNamespace(final URI namespace) {
315 namespaces.push(namespace);
318 private Entry<String, URI> resolveNamespace(final String childName, final DataSchemaNode dataSchemaNode) {
319 final int lastIndexOfColon = childName.lastIndexOf(':');
320 String moduleNamePart = null;
321 String nodeNamePart = null;
322 URI namespace = null;
323 if (lastIndexOfColon != -1) {
324 moduleNamePart = childName.substring(0, lastIndexOfColon);
325 nodeNamePart = childName.substring(lastIndexOfColon + 1);
327 final Iterator<Module> m = codecs.getSchemaContext().findModules(moduleNamePart).iterator();
328 namespace = m.hasNext() ? m.next().getNamespace() : null;
330 nodeNamePart = childName;
333 if (namespace == null) {
334 final Set<URI> potentialUris = resolveAllPotentialNamespaces(nodeNamePart, dataSchemaNode);
335 if (potentialUris.contains(getCurrentNamespace())) {
336 namespace = getCurrentNamespace();
337 } else if (potentialUris.size() == 1) {
338 namespace = potentialUris.iterator().next();
339 } else if (potentialUris.size() > 1) {
340 throw new IllegalStateException("Choose suitable module name for element " + nodeNamePart + ":"
341 + toModuleNames(potentialUris));
342 } else if (potentialUris.isEmpty()) {
343 throw new IllegalStateException("Schema node with name " + nodeNamePart + " was not found under "
344 + dataSchemaNode.getQName() + ".");
348 return new SimpleImmutableEntry<>(nodeNamePart, namespace);
351 private String toModuleNames(final Set<URI> potentialUris) {
352 final StringBuilder builder = new StringBuilder();
353 for (final URI potentialUri : potentialUris) {
354 builder.append('\n');
355 //FIXME how to get information about revision from JSON input? currently first available is used.
356 builder.append(codecs.getSchemaContext().findModules(potentialUri).iterator().next().getName());
358 return builder.toString();
361 private Set<URI> resolveAllPotentialNamespaces(final String elementName, final DataSchemaNode dataSchemaNode) {
362 final Set<URI> potentialUris = new HashSet<>();
363 final Set<ChoiceSchemaNode> choices = new HashSet<>();
364 if (dataSchemaNode instanceof DataNodeContainer) {
365 for (final DataSchemaNode childSchemaNode : ((DataNodeContainer) dataSchemaNode).getChildNodes()) {
366 if (childSchemaNode instanceof ChoiceSchemaNode) {
367 choices.add((ChoiceSchemaNode)childSchemaNode);
368 } else if (childSchemaNode.getQName().getLocalName().equals(elementName)) {
369 potentialUris.add(childSchemaNode.getQName().getNamespace());
373 for (final ChoiceSchemaNode choiceNode : choices) {
374 for (final CaseSchemaNode concreteCase : choiceNode.getCases().values()) {
375 potentialUris.addAll(resolveAllPotentialNamespaces(elementName, concreteCase));
379 return potentialUris;
382 private URI getCurrentNamespace() {
383 return namespaces.peek();
387 public void flush() throws IOException {
392 public void close() throws IOException {