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