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