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 com.google.common.annotations.Beta;
11 import com.google.common.base.Preconditions;
12 import com.google.gson.JsonIOException;
13 import com.google.gson.JsonParseException;
14 import com.google.gson.JsonSyntaxException;
15 import com.google.gson.stream.JsonReader;
16 import com.google.gson.stream.MalformedJsonException;
17 import java.io.Closeable;
18 import java.io.EOFException;
19 import java.io.Flushable;
20 import java.io.IOException;
22 import java.util.AbstractMap.SimpleImmutableEntry;
23 import java.util.ArrayDeque;
24 import java.util.Collections;
25 import java.util.Deque;
26 import java.util.HashSet;
27 import java.util.Map.Entry;
29 import javax.xml.transform.dom.DOMSource;
30 import org.opendaylight.yangtools.util.xml.UntrustedXML;
31 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
32 import org.opendaylight.yangtools.yang.data.util.AbstractNodeDataWithSchema;
33 import org.opendaylight.yangtools.yang.data.util.AnyXmlNodeDataWithSchema;
34 import org.opendaylight.yangtools.yang.data.util.CompositeNodeDataWithSchema;
35 import org.opendaylight.yangtools.yang.data.util.LeafListEntryNodeDataWithSchema;
36 import org.opendaylight.yangtools.yang.data.util.LeafListNodeDataWithSchema;
37 import org.opendaylight.yangtools.yang.data.util.LeafNodeDataWithSchema;
38 import org.opendaylight.yangtools.yang.data.util.ListEntryNodeDataWithSchema;
39 import org.opendaylight.yangtools.yang.data.util.ListNodeDataWithSchema;
40 import org.opendaylight.yangtools.yang.data.util.ParserStreamUtils;
41 import org.opendaylight.yangtools.yang.data.util.RpcAsContainer;
42 import org.opendaylight.yangtools.yang.data.util.SimpleNodeDataWithSchema;
43 import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode;
44 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
46 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
47 import org.opendaylight.yangtools.yang.model.api.Module;
48 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
49 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
50 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.TypedSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.YangModeledAnyXmlSchemaNode;
53 import org.w3c.dom.Document;
54 import org.w3c.dom.Element;
55 import org.w3c.dom.Text;
58 * This class parses JSON elements from a GSON JsonReader. It disallows multiple elements of the same name unlike the
59 * default GSON JsonParser.
62 public final class JsonParserStream implements Closeable, Flushable {
63 static final String ANYXML_ARRAY_ELEMENT_ID = "array-element";
65 private final Deque<URI> namespaces = new ArrayDeque<>();
66 private final NormalizedNodeStreamWriter writer;
67 private final JSONCodecFactory codecs;
68 private final SchemaContext schema;
69 private final DataSchemaNode parentNode;
71 private JsonParserStream(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext,
72 final JSONCodecFactory codecs, final DataSchemaNode parentNode) {
73 this.schema = Preconditions.checkNotNull(schemaContext);
74 this.writer = Preconditions.checkNotNull(writer);
75 this.codecs = Preconditions.checkNotNull(codecs);
76 this.parentNode = parentNode;
79 private JsonParserStream(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext,
80 final DataSchemaNode parentNode) {
81 this(writer, schemaContext, JSONCodecFactory.getShared(schemaContext), parentNode);
84 public static JsonParserStream create(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext,
85 final SchemaNode parentNode) {
86 if (parentNode instanceof RpcDefinition) {
87 return new JsonParserStream(writer, schemaContext, new RpcAsContainer((RpcDefinition) parentNode));
89 Preconditions.checkArgument(parentNode instanceof DataSchemaNode, "Instance of DataSchemaNode class awaited.");
90 return new JsonParserStream(writer, schemaContext, (DataSchemaNode) parentNode);
93 public static JsonParserStream create(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext) {
94 return new JsonParserStream(writer, schemaContext, schemaContext);
97 public JsonParserStream parse(final JsonReader reader) {
98 // code copied from gson's JsonParser and Stream classes
100 final boolean lenient = reader.isLenient();
101 reader.setLenient(true);
102 boolean isEmpty = true;
106 final CompositeNodeDataWithSchema compositeNodeDataWithSchema = new CompositeNodeDataWithSchema(parentNode);
107 read(reader, compositeNodeDataWithSchema);
108 compositeNodeDataWithSchema.write(writer);
111 } catch (final EOFException e) {
115 // The stream ended prematurely so it is likely a syntax error.
116 throw new JsonSyntaxException(e);
117 } catch (final MalformedJsonException | NumberFormatException e) {
118 throw new JsonSyntaxException(e);
119 } catch (final IOException e) {
120 throw new JsonIOException(e);
121 } catch (StackOverflowError | OutOfMemoryError e) {
122 throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e);
124 reader.setLenient(lenient);
128 private void traverseAnyXmlValue(final JsonReader in, final Document doc, final Element parentElement)
133 Text textNode = doc.createTextNode(in.nextString());
134 parentElement.appendChild(textNode);
137 textNode = doc.createTextNode(Boolean.toString(in.nextBoolean()));
138 parentElement.appendChild(textNode);
142 textNode = doc.createTextNode("null");
143 parentElement.appendChild(textNode);
147 while (in.hasNext()) {
148 final Element childElement = doc.createElement(ANYXML_ARRAY_ELEMENT_ID);
149 parentElement.appendChild(childElement);
150 traverseAnyXmlValue(in, doc, childElement);
156 while (in.hasNext()) {
157 final Element childElement = doc.createElement(in.nextName());
158 parentElement.appendChild(childElement);
159 traverseAnyXmlValue(in, doc, childElement);
168 private void readAnyXmlValue(final JsonReader in, final AnyXmlNodeDataWithSchema parent,
169 final String anyXmlObjectName) throws IOException {
170 final String anyXmlObjectNS = getCurrentNamespace().toString();
171 final Document doc = UntrustedXML.newDocumentBuilder().newDocument();
172 final Element rootElement = doc.createElementNS(anyXmlObjectNS, anyXmlObjectName);
173 doc.appendChild(rootElement);
174 traverseAnyXmlValue(in, doc, rootElement);
176 final DOMSource domSource = new DOMSource(doc.getDocumentElement());
177 parent.setValue(domSource);
180 public void read(final JsonReader in, AbstractNodeDataWithSchema parent) throws IOException {
184 setValue(parent, in.nextString());
187 setValue(parent, Boolean.toString(in.nextBoolean()));
191 setValue(parent, null);
195 while (in.hasNext()) {
196 if (parent instanceof LeafNodeDataWithSchema) {
199 final AbstractNodeDataWithSchema newChild = newArrayEntry(parent);
206 final Set<String> namesakes = new HashSet<>();
209 * This allows parsing of incorrectly /as showcased/
210 * in testconf nesting of list items - eg.
211 * lists with one value are sometimes serialized
212 * without wrapping array.
215 if (isArray(parent)) {
216 parent = newArrayEntry(parent);
218 while (in.hasNext()) {
219 final String jsonElementName = in.nextName();
220 DataSchemaNode parentSchema = parent.getSchema();
221 if (parentSchema instanceof YangModeledAnyXmlSchemaNode) {
222 parentSchema = ((YangModeledAnyXmlSchemaNode) parentSchema).getSchemaOfAnyXmlData();
224 final Entry<String, URI> namespaceAndName = resolveNamespace(jsonElementName, parentSchema);
225 final String localName = namespaceAndName.getKey();
226 addNamespace(namespaceAndName.getValue());
227 if (!namesakes.add(jsonElementName)) {
228 throw new JsonSyntaxException("Duplicate name " + jsonElementName + " in JSON input.");
231 final Deque<DataSchemaNode> childDataSchemaNodes =
232 ParserStreamUtils.findSchemaNodeByNameAndNamespace(parentSchema, localName,
233 getCurrentNamespace());
234 Preconditions.checkState(!childDataSchemaNodes.isEmpty(),
235 "Schema for node with name %s and namespace %s does not exist.", localName,
236 getCurrentNamespace());
238 final AbstractNodeDataWithSchema newChild = ((CompositeNodeDataWithSchema) parent)
239 .addChild(childDataSchemaNodes);
240 if (newChild instanceof AnyXmlNodeDataWithSchema) {
241 readAnyXmlValue(in, (AnyXmlNodeDataWithSchema) newChild, jsonElementName);
254 private static boolean isArray(final AbstractNodeDataWithSchema parent) {
255 return parent instanceof ListNodeDataWithSchema || parent instanceof LeafListNodeDataWithSchema;
258 private static AbstractNodeDataWithSchema newArrayEntry(final AbstractNodeDataWithSchema parent) {
259 AbstractNodeDataWithSchema newChild;
260 if (parent instanceof ListNodeDataWithSchema) {
261 newChild = new ListEntryNodeDataWithSchema(parent.getSchema());
262 } else if (parent instanceof LeafListNodeDataWithSchema) {
263 newChild = new LeafListEntryNodeDataWithSchema(parent.getSchema());
265 throw new IllegalStateException("Found an unexpected array nested under " + parent.getSchema().getQName());
267 ((CompositeNodeDataWithSchema) parent).addChild(newChild);
271 private void setValue(final AbstractNodeDataWithSchema parent, final String value) {
272 Preconditions.checkArgument(parent instanceof SimpleNodeDataWithSchema, "Node %s is not a simple type",
273 parent.getSchema().getQName());
274 final SimpleNodeDataWithSchema parentSimpleNode = (SimpleNodeDataWithSchema) parent;
275 Preconditions.checkArgument(parentSimpleNode.getValue() == null, "Node '%s' has already set its value to '%s'",
276 parentSimpleNode.getSchema().getQName(), parentSimpleNode.getValue());
278 final Object translatedValue = translateValueByType(value, parentSimpleNode.getSchema());
279 parentSimpleNode.setValue(translatedValue);
282 private Object translateValueByType(final String value, final DataSchemaNode node) {
283 Preconditions.checkArgument(node instanceof TypedSchemaNode);
284 return codecs.codecFor((TypedSchemaNode) node).parseValue(null, value);
287 private void removeNamespace() {
291 private void addNamespace(final URI namespace) {
292 namespaces.push(namespace);
295 private Entry<String, URI> resolveNamespace(final String childName, final DataSchemaNode dataSchemaNode) {
296 final int lastIndexOfColon = childName.lastIndexOf(':');
297 String moduleNamePart = null;
298 String nodeNamePart = null;
299 URI namespace = null;
300 if (lastIndexOfColon != -1) {
301 moduleNamePart = childName.substring(0, lastIndexOfColon);
302 nodeNamePart = childName.substring(lastIndexOfColon + 1);
304 final Module m = schema.findModuleByName(moduleNamePart, null);
305 namespace = m == null ? null : m.getNamespace();
307 nodeNamePart = childName;
310 if (namespace == null) {
311 Set<URI> potentialUris = Collections.emptySet();
312 potentialUris = resolveAllPotentialNamespaces(nodeNamePart, dataSchemaNode);
313 if (potentialUris.contains(getCurrentNamespace())) {
314 namespace = getCurrentNamespace();
315 } else if (potentialUris.size() == 1) {
316 namespace = potentialUris.iterator().next();
317 } else if (potentialUris.size() > 1) {
318 throw new IllegalStateException("Choose suitable module name for element " + nodeNamePart + ":"
319 + toModuleNames(potentialUris));
320 } else if (potentialUris.isEmpty()) {
321 throw new IllegalStateException("Schema node with name " + nodeNamePart + " was not found under "
322 + dataSchemaNode.getQName() + ".");
326 return new SimpleImmutableEntry<>(nodeNamePart, namespace);
329 private String toModuleNames(final Set<URI> potentialUris) {
330 final StringBuilder builder = new StringBuilder();
331 for (final URI potentialUri : potentialUris) {
332 builder.append('\n');
333 //FIXME how to get information about revision from JSON input? currently first available is used.
334 builder.append(schema.findModuleByNamespace(potentialUri).iterator().next().getName());
336 return builder.toString();
339 private Set<URI> resolveAllPotentialNamespaces(final String elementName, final DataSchemaNode dataSchemaNode) {
340 final Set<URI> potentialUris = new HashSet<>();
341 final Set<ChoiceSchemaNode> choices = new HashSet<>();
342 if (dataSchemaNode instanceof DataNodeContainer) {
343 for (final DataSchemaNode childSchemaNode : ((DataNodeContainer) dataSchemaNode).getChildNodes()) {
344 if (childSchemaNode instanceof ChoiceSchemaNode) {
345 choices.add((ChoiceSchemaNode)childSchemaNode);
346 } else if (childSchemaNode.getQName().getLocalName().equals(elementName)) {
347 potentialUris.add(childSchemaNode.getQName().getNamespace());
351 for (final ChoiceSchemaNode choiceNode : choices) {
352 for (final ChoiceCaseNode concreteCase : choiceNode.getCases()) {
353 potentialUris.addAll(resolveAllPotentialNamespaces(elementName, concreteCase));
357 return potentialUris;
360 private URI getCurrentNamespace() {
361 return namespaces.peek();
365 public void flush() throws IOException {
370 public void close() throws IOException {