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