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