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