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