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