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