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