Bridge {Notification,Operation}Definition to ContainerLike
[yangtools.git] / codec / yang-data-codec-gson / src / main / java / org / opendaylight / yangtools / yang / data / codec / gson / JsonParserStream.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.yangtools.yang.data.codec.gson;
9
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;
13
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;
24 import java.util.AbstractMap.SimpleImmutableEntry;
25 import java.util.ArrayDeque;
26 import java.util.Deque;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.Map.Entry;
30 import java.util.Set;
31 import javax.xml.transform.dom.DOMSource;
32 import org.eclipse.jdt.annotation.NonNull;
33 import org.opendaylight.yangtools.util.xml.UntrustedXML;
34 import org.opendaylight.yangtools.yang.common.QName;
35 import org.opendaylight.yangtools.yang.common.XMLNamespace;
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.CompositeNodeDataWithSchema.ChildReusePolicy;
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.ListNodeDataWithSchema;
44 import org.opendaylight.yangtools.yang.data.util.MultipleEntryDataWithSchema;
45 import org.opendaylight.yangtools.yang.data.util.ParserStreamUtils;
46 import org.opendaylight.yangtools.yang.data.util.SimpleNodeDataWithSchema;
47 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
50 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference;
52 import org.opendaylight.yangtools.yang.model.api.Module;
53 import org.opendaylight.yangtools.yang.model.api.NotificationDefinition;
54 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
55 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
57 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60 import org.w3c.dom.Document;
61 import org.w3c.dom.Element;
62 import org.w3c.dom.Text;
63
64 /**
65  * This class parses JSON elements from a GSON JsonReader. It disallows multiple elements of the same name unlike the
66  * default GSON JsonParser.
67  */
68 @Beta
69 public final class JsonParserStream implements Closeable, Flushable {
70     static final String ANYXML_ARRAY_ELEMENT_ID = "array-element";
71
72     private static final Logger LOG = LoggerFactory.getLogger(JsonParserStream.class);
73     private final Deque<XMLNamespace> namespaces = new ArrayDeque<>();
74     private final NormalizedNodeStreamWriter writer;
75     private final JSONCodecFactory codecs;
76     private final DataSchemaNode parentNode;
77
78     private final SchemaInferenceStack stack;
79
80     // TODO: consider class specialization to remove this field
81     private final boolean lenient;
82
83     private JsonParserStream(final NormalizedNodeStreamWriter writer, final JSONCodecFactory codecs,
84             final SchemaInferenceStack stack, final boolean lenient) {
85         this.writer = requireNonNull(writer);
86         this.codecs = requireNonNull(codecs);
87         this.stack = requireNonNull(stack);
88         this.lenient = lenient;
89
90         if (!stack.isEmpty()) {
91             final EffectiveStatement<?, ?> parent = stack.currentStatement();
92             if (parent instanceof DataSchemaNode data) {
93                 parentNode = data;
94             } else if (parent instanceof OperationDefinition oper) {
95                 parentNode = oper.toContainerLike();
96             } else if (parent instanceof NotificationDefinition notif) {
97                 parentNode = notif.toContainerLike();
98             } else {
99                 throw new IllegalArgumentException("Illegal parent node " + parent);
100             }
101         } else {
102             parentNode = stack.getEffectiveModelContext();
103         }
104     }
105
106     /**
107      * Create a new {@link JsonParserStream} backed by specified {@link NormalizedNodeStreamWriter}
108      * and {@link JSONCodecFactory}. The stream will be logically rooted at the top of the SchemaContext associated
109      * with the specified codec factory.
110      *
111      * @param writer NormalizedNodeStreamWriter to use for instantiation of normalized nodes
112      * @param codecFactory {@link JSONCodecFactory} to use for parsing leaves
113      * @return A new {@link JsonParserStream}
114      * @throws NullPointerException if any of the arguments are null
115      */
116     public static @NonNull JsonParserStream create(final @NonNull NormalizedNodeStreamWriter writer,
117             final @NonNull JSONCodecFactory codecFactory) {
118         return new JsonParserStream(writer, codecFactory,
119             SchemaInferenceStack.of(codecFactory.getEffectiveModelContext()), false);
120     }
121
122     /**
123      * Create a new {@link JsonParserStream} backed by specified {@link NormalizedNodeStreamWriter}
124      * and {@link JSONCodecFactory}. The stream will be logically rooted at the specified parent node.
125      *
126      * @param writer NormalizedNodeStreamWriter to use for instantiation of normalized nodes
127      * @param codecFactory {@link JSONCodecFactory} to use for parsing leaves
128      * @param parentNode Logical root node
129      * @return A new {@link JsonParserStream}
130      * @throws NullPointerException if any of the arguments are null
131      */
132     public static @NonNull JsonParserStream create(final @NonNull NormalizedNodeStreamWriter writer,
133             final @NonNull JSONCodecFactory codecFactory, final @NonNull EffectiveStatementInference parentNode) {
134         return new JsonParserStream(writer, codecFactory, SchemaInferenceStack.ofInference(parentNode), false);
135     }
136
137     /**
138      * Create a new {@link JsonParserStream} backed by specified {@link NormalizedNodeStreamWriter}
139      * and {@link JSONCodecFactory}. The stream will be logically rooted at the top of the SchemaContext associated
140      * with the specified codec factory.
141      *
142      * <p>
143      * Returned parser will treat incoming JSON data leniently:
144      * <ul>
145      *   <li>JSON elements referring to unknown constructs will be silently ignored</li>
146      * </ul>
147      *
148      * @param writer NormalizedNodeStreamWriter to use for instantiation of normalized nodes
149      * @param codecFactory {@link JSONCodecFactory} to use for parsing leaves
150      * @return A new {@link JsonParserStream}
151      * @throws NullPointerException if any of the arguments are null
152      */
153     public static @NonNull JsonParserStream createLenient(final @NonNull NormalizedNodeStreamWriter writer,
154             final @NonNull JSONCodecFactory codecFactory) {
155         return new JsonParserStream(writer, codecFactory,
156             SchemaInferenceStack.of(codecFactory.getEffectiveModelContext()), true);
157     }
158
159     /**
160      * Create a new {@link JsonParserStream} backed by specified {@link NormalizedNodeStreamWriter}
161      * and {@link JSONCodecFactory}. The stream will be logically rooted at the specified parent node.
162      *
163      * <p>
164      * Returned parser will treat incoming JSON data leniently:
165      * <ul>
166      *   <li>JSON elements referring to unknown constructs will be silently ignored</li>
167      * </ul>
168      *
169      * @param writer NormalizedNodeStreamWriter to use for instantiation of normalized nodes
170      * @param codecFactory {@link JSONCodecFactory} to use for parsing leaves
171      * @param parentNode Logical root node
172      * @return A new {@link JsonParserStream}
173      * @throws NullPointerException if any of the arguments are null
174      */
175     public static @NonNull JsonParserStream createLenient(final @NonNull NormalizedNodeStreamWriter writer,
176             final @NonNull JSONCodecFactory codecFactory, final @NonNull EffectiveStatementInference parentNode) {
177         return new JsonParserStream(writer, codecFactory, SchemaInferenceStack.ofInference(parentNode), true);
178     }
179
180     public JsonParserStream parse(final JsonReader reader) {
181         // code copied from gson's JsonParser and Stream classes
182
183         final boolean readerLenient = reader.isLenient();
184         reader.setLenient(true);
185         boolean isEmpty = true;
186         try {
187             reader.peek();
188             isEmpty = false;
189             // FIXME: this has a special-case bypass for SchemaContext, where we end up emitting just the child while
190             //        the usual of() would result in SchemaContext.NAME being the root
191             final var compositeNodeDataWithSchema = new CompositeNodeDataWithSchema<>(parentNode);
192             read(reader, compositeNodeDataWithSchema);
193             compositeNodeDataWithSchema.write(writer);
194
195             return this;
196         } catch (final EOFException e) {
197             if (isEmpty) {
198                 return this;
199             }
200             // The stream ended prematurely so it is likely a syntax error.
201             throw new JsonSyntaxException(e);
202         } catch (final MalformedJsonException | NumberFormatException e) {
203             throw new JsonSyntaxException(e);
204         } catch (final IOException e) {
205             throw new JsonIOException(e);
206         } catch (StackOverflowError | OutOfMemoryError e) {
207             throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e);
208         } finally {
209             reader.setLenient(readerLenient);
210         }
211     }
212
213     private void traverseAnyXmlValue(final JsonReader in, final Document doc, final Element parentElement)
214             throws IOException {
215         switch (in.peek()) {
216             case STRING:
217             case NUMBER:
218                 Text textNode = doc.createTextNode(in.nextString());
219                 parentElement.appendChild(textNode);
220                 break;
221             case BOOLEAN:
222                 textNode = doc.createTextNode(Boolean.toString(in.nextBoolean()));
223                 parentElement.appendChild(textNode);
224                 break;
225             case NULL:
226                 in.nextNull();
227                 textNode = doc.createTextNode("null");
228                 parentElement.appendChild(textNode);
229                 break;
230             case BEGIN_ARRAY:
231                 in.beginArray();
232                 while (in.hasNext()) {
233                     final Element childElement = doc.createElement(ANYXML_ARRAY_ELEMENT_ID);
234                     parentElement.appendChild(childElement);
235                     traverseAnyXmlValue(in, doc, childElement);
236                 }
237                 in.endArray();
238                 break;
239             case BEGIN_OBJECT:
240                 in.beginObject();
241                 while (in.hasNext()) {
242                     final Element childElement = doc.createElement(in.nextName());
243                     parentElement.appendChild(childElement);
244                     traverseAnyXmlValue(in, doc, childElement);
245                 }
246                 in.endObject();
247                 break;
248             default:
249                 break;
250         }
251     }
252
253     private void readAnyXmlValue(final JsonReader in, final AnyXmlNodeDataWithSchema parent,
254             final String anyXmlObjectName) throws IOException {
255         final String anyXmlObjectNS = getCurrentNamespace().toString();
256         final Document doc = UntrustedXML.newDocumentBuilder().newDocument();
257         final Element rootElement = doc.createElementNS(anyXmlObjectNS, anyXmlObjectName);
258         doc.appendChild(rootElement);
259         traverseAnyXmlValue(in, doc, rootElement);
260
261         final DOMSource domSource = new DOMSource(doc.getDocumentElement());
262         parent.setValue(domSource);
263     }
264
265     private void read(final JsonReader in, AbstractNodeDataWithSchema<?> parent) throws IOException {
266         switch (in.peek()) {
267             case STRING:
268             case NUMBER:
269                 setValue(parent, in.nextString());
270                 break;
271             case BOOLEAN:
272                 setValue(parent, Boolean.toString(in.nextBoolean()));
273                 break;
274             case NULL:
275                 in.nextNull();
276                 setValue(parent, null);
277                 break;
278             case BEGIN_ARRAY:
279                 in.beginArray();
280                 while (in.hasNext()) {
281                     if (parent instanceof LeafNodeDataWithSchema) {
282                         read(in, parent);
283                     } else {
284                         final AbstractNodeDataWithSchema<?> newChild = newArrayEntry(parent);
285                         read(in, newChild);
286                     }
287                 }
288                 in.endArray();
289                 return;
290             case BEGIN_OBJECT:
291                 final Set<String> namesakes = new HashSet<>();
292                 in.beginObject();
293                 /*
294                  * This allows parsing of incorrectly /as showcased/
295                  * in testconf nesting of list items - eg.
296                  * lists with one value are sometimes serialized
297                  * without wrapping array.
298                  *
299                  */
300                 if (isArray(parent)) {
301                     parent = newArrayEntry(parent);
302                 }
303                 while (in.hasNext()) {
304                     final String jsonElementName = in.nextName();
305                     final DataSchemaNode parentSchema = parent.getSchema();
306                     final Entry<String, XMLNamespace> namespaceAndName =
307                         resolveNamespace(jsonElementName, parentSchema);
308                     final String localName = namespaceAndName.getKey();
309                     final XMLNamespace namespace = namespaceAndName.getValue();
310                     if (lenient && (localName == null || namespace == null)) {
311                         LOG.debug("Schema node with name {} was not found under {}", localName,
312                             parentSchema.getQName());
313                         in.skipValue();
314                         continue;
315                     }
316                     addNamespace(namespace);
317                     if (!namesakes.add(jsonElementName)) {
318                         throw new JsonSyntaxException("Duplicate name " + jsonElementName + " in JSON input.");
319                     }
320
321                     final Deque<DataSchemaNode> childDataSchemaNodes =
322                             ParserStreamUtils.findSchemaNodeByNameAndNamespace(parentSchema, localName,
323                                 getCurrentNamespace());
324                     checkState(!childDataSchemaNodes.isEmpty(),
325                         "Schema for node with name %s and namespace %s does not exist at %s",
326                         localName, getCurrentNamespace(), parentSchema);
327
328                     final QName qname = childDataSchemaNodes.peekLast().getQName();
329                     final AbstractNodeDataWithSchema<?> newChild = ((CompositeNodeDataWithSchema<?>) parent)
330                             .addChild(childDataSchemaNodes, ChildReusePolicy.NOOP);
331                     if (newChild instanceof AnyXmlNodeDataWithSchema anyxml) {
332                         readAnyXmlValue(in, anyxml, jsonElementName);
333                     } else {
334                         stack.enterDataTree(qname);
335                         read(in, newChild);
336                         stack.exit();
337                     }
338                     removeNamespace();
339                 }
340                 in.endObject();
341                 return;
342             default:
343                 break;
344         }
345     }
346
347     private static boolean isArray(final AbstractNodeDataWithSchema<?> parent) {
348         return parent instanceof ListNodeDataWithSchema || parent instanceof LeafListNodeDataWithSchema;
349     }
350
351     private static AbstractNodeDataWithSchema<?> newArrayEntry(final AbstractNodeDataWithSchema<?> parent) {
352         if (parent instanceof MultipleEntryDataWithSchema<?> multiple) {
353             return multiple.newChildEntry();
354         }
355         throw new IllegalStateException("Found an unexpected array nested under " + parent.getSchema().getQName());
356     }
357
358     private void setValue(final AbstractNodeDataWithSchema<?> parent, final String value) {
359         checkArgument(parent instanceof SimpleNodeDataWithSchema, "Node %s is not a simple type",
360                 parent.getSchema().getQName());
361         final SimpleNodeDataWithSchema<?> parentSimpleNode = (SimpleNodeDataWithSchema<?>) parent;
362         checkArgument(parentSimpleNode.getValue() == null, "Node '%s' has already set its value to '%s'",
363                 parentSimpleNode.getSchema().getQName(), parentSimpleNode.getValue());
364
365         final Object translatedValue = translateValueByType(value, parentSimpleNode.getSchema());
366         parentSimpleNode.setValue(translatedValue);
367     }
368
369     private Object translateValueByType(final String value, final DataSchemaNode node) {
370         checkArgument(node instanceof TypedDataSchemaNode);
371         return codecs.codecFor((TypedDataSchemaNode) node, stack).parseValue(null, value);
372     }
373
374     private void removeNamespace() {
375         namespaces.pop();
376     }
377
378     private void addNamespace(final XMLNamespace namespace) {
379         namespaces.push(namespace);
380     }
381
382     private Entry<String, XMLNamespace> resolveNamespace(final String childName, final DataSchemaNode dataSchemaNode) {
383         final int lastIndexOfColon = childName.lastIndexOf(':');
384         String moduleNamePart = null;
385         String nodeNamePart = null;
386         XMLNamespace namespace = null;
387         if (lastIndexOfColon != -1) {
388             moduleNamePart = childName.substring(0, lastIndexOfColon);
389             nodeNamePart = childName.substring(lastIndexOfColon + 1);
390
391             final Iterator<? extends Module> m = codecs.getEffectiveModelContext().findModules(moduleNamePart)
392                     .iterator();
393             namespace = m.hasNext() ? m.next().getNamespace() : null;
394         } else {
395             nodeNamePart = childName;
396         }
397
398         if (namespace == null) {
399             final Set<XMLNamespace> potentialUris = resolveAllPotentialNamespaces(nodeNamePart, dataSchemaNode);
400             if (potentialUris.contains(getCurrentNamespace())) {
401                 namespace = getCurrentNamespace();
402             } else if (potentialUris.size() == 1) {
403                 namespace = potentialUris.iterator().next();
404             } else if (potentialUris.size() > 1) {
405                 throw new IllegalStateException("Choose suitable module name for element " + nodeNamePart + ":"
406                         + toModuleNames(potentialUris));
407             } else if (potentialUris.isEmpty() && !lenient) {
408                 throw new IllegalStateException("Schema node with name " + nodeNamePart + " was not found under "
409                         + dataSchemaNode.getQName() + ".");
410             }
411         }
412
413         return new SimpleImmutableEntry<>(nodeNamePart, namespace);
414     }
415
416     private String toModuleNames(final Set<XMLNamespace> potentialUris) {
417         final StringBuilder builder = new StringBuilder();
418         for (final XMLNamespace potentialUri : potentialUris) {
419             builder.append('\n');
420             //FIXME how to get information about revision from JSON input? currently first available is used.
421             builder.append(codecs.getEffectiveModelContext().findModules(potentialUri).iterator().next().getName());
422         }
423         return builder.toString();
424     }
425
426     private Set<XMLNamespace> resolveAllPotentialNamespaces(final String elementName,
427             final DataSchemaNode dataSchemaNode) {
428         final Set<XMLNamespace> potentialUris = new HashSet<>();
429         final Set<ChoiceSchemaNode> choices = new HashSet<>();
430         if (dataSchemaNode instanceof DataNodeContainer container) {
431             for (final DataSchemaNode childSchemaNode : container.getChildNodes()) {
432                 if (childSchemaNode instanceof ChoiceSchemaNode choice) {
433                     choices.add(choice);
434                 } else if (childSchemaNode.getQName().getLocalName().equals(elementName)) {
435                     potentialUris.add(childSchemaNode.getQName().getNamespace());
436                 }
437             }
438
439             for (final ChoiceSchemaNode choiceNode : choices) {
440                 for (final CaseSchemaNode concreteCase : choiceNode.getCases()) {
441                     potentialUris.addAll(resolveAllPotentialNamespaces(elementName, concreteCase));
442                 }
443             }
444         }
445         return potentialUris;
446     }
447
448     private XMLNamespace getCurrentNamespace() {
449         return namespaces.peek();
450     }
451
452     @Override
453     public void flush() throws IOException {
454         writer.flush();
455     }
456
457     @Override
458     public void close() throws IOException {
459         writer.flush();
460         writer.close();
461     }
462 }