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