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