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