Merge "BUG-2350: do encapsulte null snapshot"
[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 com.google.common.annotations.Beta;
11 import com.google.common.base.Optional;
12 import com.google.common.base.Preconditions;
13 import com.google.gson.JsonIOException;
14 import com.google.gson.JsonParseException;
15 import com.google.gson.JsonSyntaxException;
16 import com.google.gson.stream.JsonReader;
17 import com.google.gson.stream.MalformedJsonException;
18
19 import java.io.Closeable;
20 import java.io.EOFException;
21 import java.io.Flushable;
22 import java.io.IOException;
23 import java.net.URI;
24 import java.util.ArrayDeque;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.Deque;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Set;
31
32 import org.opendaylight.yangtools.yang.common.QName;
33 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
34 import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode;
36 import org.opendaylight.yangtools.yang.model.api.ChoiceNode;
37 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
38 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.Module;
42 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
43 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
45
46 /**
47  * This class parses JSON elements from a GSON JsonReader. It disallows multiple elements of the same name unlike the
48  * default GSON JsonParser.
49  */
50 @Beta
51 public final class JsonParserStream implements Closeable, Flushable {
52     private final Deque<URI> namespaces = new ArrayDeque<>();
53     private final NormalizedNodeStreamWriter writer;
54     private final JSONCodecFactory codecs;
55     private final SchemaContext schema;
56     private final DataSchemaNode parentNode;
57
58     private JsonParserStream(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext, final DataSchemaNode parentNode) {
59         this.schema = Preconditions.checkNotNull(schemaContext);
60         this.writer = Preconditions.checkNotNull(writer);
61         this.codecs = JSONCodecFactory.create(schemaContext);
62         this.parentNode = parentNode;
63     }
64
65     public static JsonParserStream create(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext, final SchemaNode parentNode ) {
66         Preconditions.checkArgument(parentNode instanceof DataSchemaNode, "Instance of DataSchemaNode class awaited.");
67         return new JsonParserStream(writer, schemaContext, (DataSchemaNode) parentNode);
68     }
69
70     public static JsonParserStream create(final NormalizedNodeStreamWriter writer, final SchemaContext schemaContext) {
71         return new JsonParserStream(writer, schemaContext, schemaContext);
72     }
73
74     public JsonParserStream parse(final JsonReader reader) throws JsonIOException, JsonSyntaxException {
75         // code copied from gson's JsonParser and Stream classes
76
77         boolean lenient = reader.isLenient();
78         reader.setLenient(true);
79         boolean isEmpty = true;
80         try {
81             reader.peek();
82             isEmpty = false;
83             CompositeNodeDataWithSchema compositeNodeDataWithSchema = new CompositeNodeDataWithSchema(parentNode);
84             read(reader, compositeNodeDataWithSchema);
85             compositeNodeDataWithSchema.write(writer);
86
87             return this;
88             // return read(reader);
89         } catch (EOFException e) {
90             if (isEmpty) {
91                 return this;
92                 // return JsonNull.INSTANCE;
93             }
94             // The stream ended prematurely so it is likely a syntax error.
95             throw new JsonSyntaxException(e);
96         } catch (MalformedJsonException e) {
97             throw new JsonSyntaxException(e);
98         } catch (IOException e) {
99             throw new JsonIOException(e);
100         } catch (NumberFormatException e) {
101             throw new JsonSyntaxException(e);
102         } catch (StackOverflowError | OutOfMemoryError e) {
103             throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e);
104         } finally {
105             reader.setLenient(lenient);
106         }
107     }
108
109     private final void setValue(final AbstractNodeDataWithSchema parent, final String value) {
110         Preconditions.checkArgument(parent instanceof SimpleNodeDataWithSchema, "Node %s is not a simple type", parent);
111
112         final Object translatedValue = translateValueByType(value, parent.getSchema());
113         ((SimpleNodeDataWithSchema) parent).setValue(translatedValue);
114     }
115
116     public void read(final JsonReader in, final AbstractNodeDataWithSchema parent) throws IOException {
117         switch (in.peek()) {
118         case STRING:
119         case NUMBER:
120             setValue(parent, in.nextString());
121             break;
122         case BOOLEAN:
123             setValue(parent, Boolean.toString(in.nextBoolean()));
124             break;
125         case NULL:
126             in.nextNull();
127             setValue(parent, null);
128             break;
129         case BEGIN_ARRAY:
130             in.beginArray();
131             while (in.hasNext()) {
132                 AbstractNodeDataWithSchema newChild = null;
133                 if (parent instanceof ListNodeDataWithSchema) {
134                     newChild = new ListEntryNodeDataWithSchema(parent.getSchema());
135                     ((CompositeNodeDataWithSchema) parent).addChild(newChild);
136                 } else if (parent instanceof LeafListNodeDataWithSchema) {
137                     newChild = new LeafListEntryNodeDataWithSchema(parent.getSchema());
138                     ((CompositeNodeDataWithSchema) parent).addChild(newChild);
139                 }
140                 read(in, newChild);
141             }
142             in.endArray();
143             return;
144         case BEGIN_OBJECT:
145             Set<String> namesakes = new HashSet<>();
146             in.beginObject();
147             while (in.hasNext()) {
148                 final String jsonElementName = in.nextName();
149                 final NamespaceAndName namespaceAndName = resolveNamespace(jsonElementName);
150                 final String localName = namespaceAndName.getName();
151                 addNamespace(namespaceAndName.getUri());
152                 if (namesakes.contains(jsonElementName)) {
153                     throw new JsonSyntaxException("Duplicate name " + jsonElementName + " in JSON input.");
154                 }
155                 namesakes.add(jsonElementName);
156                 final Deque<DataSchemaNode> childDataSchemaNodes = findSchemaNodeByNameAndNamespace(parent.getSchema(),
157                         localName, getCurrentNamespace());
158                 if (childDataSchemaNodes.isEmpty()) {
159                     throw new IllegalStateException("Schema for node with name " + localName + " and namespace "
160                             + getCurrentNamespace() + " doesn't exist.");
161                 }
162
163                 AbstractNodeDataWithSchema newChild;
164                 newChild = ((CompositeNodeDataWithSchema) parent).addChild(childDataSchemaNodes);
165 //                FIXME:anyxml data shouldn't be skipped but should be loaded somehow. will be specified after 17AUG2014
166                 if (newChild instanceof AnyXmlNodeDataWithSchema) {
167                     in.skipValue();
168                 } else {
169                     read(in, newChild);
170                 }
171                 removeNamespace();
172             }
173             in.endObject();
174             return;
175         case END_DOCUMENT:
176         case NAME:
177         case END_OBJECT:
178         case END_ARRAY:
179             break;
180         }
181     }
182
183     private Object translateValueByType(final String value, final DataSchemaNode node) {
184         final TypeDefinition<? extends Object> typeDefinition = typeDefinition(node);
185         if (typeDefinition == null) {
186             return value;
187         }
188
189         return codecs.codecFor(typeDefinition).deserialize(value);
190     }
191
192     private static TypeDefinition<? extends Object> typeDefinition(final DataSchemaNode node) {
193         TypeDefinition<?> baseType = null;
194         if (node instanceof LeafListSchemaNode) {
195             baseType = ((LeafListSchemaNode) node).getType();
196         } else if (node instanceof LeafSchemaNode) {
197             baseType = ((LeafSchemaNode) node).getType();
198         } else if (node instanceof AnyXmlSchemaNode) {
199             return null;
200         } else {
201             throw new IllegalArgumentException("Unhandled parameter types: " + Arrays.<Object> asList(node).toString());
202         }
203
204         if (baseType != null) {
205             while (baseType.getBaseType() != null) {
206                 baseType = baseType.getBaseType();
207             }
208         }
209         return baseType;
210     }
211
212     private void removeNamespace() {
213         namespaces.pop();
214     }
215
216     private void addNamespace(final Optional<URI> namespace) {
217         if (!namespace.isPresent()) {
218             if (namespaces.isEmpty()) {
219                 throw new IllegalStateException("Namespace has to be specified at top level.");
220             } else {
221                 namespaces.push(namespaces.peek());
222             }
223         } else {
224             namespaces.push(namespace.get());
225         }
226     }
227
228     private NamespaceAndName resolveNamespace(final String childName) {
229         int lastIndexOfColon = childName.lastIndexOf(':');
230         String moduleNamePart = null;
231         String nodeNamePart = null;
232         URI namespace = null;
233         if (lastIndexOfColon != -1) {
234             moduleNamePart = childName.substring(0, lastIndexOfColon);
235             nodeNamePart = childName.substring(lastIndexOfColon + 1);
236
237             final Module m = schema.findModuleByName(moduleNamePart, null);
238             namespace = m == null ? null : m.getNamespace();
239         } else {
240             nodeNamePart = childName;
241         }
242
243         Optional<URI> namespaceOpt = namespace == null ? Optional.<URI> absent() : Optional.of(namespace);
244         return new NamespaceAndName(nodeNamePart, namespaceOpt);
245     }
246
247     private URI getCurrentNamespace() {
248         return namespaces.peek();
249     }
250
251     /**
252      * Returns stack of schema nodes via which it was necessary to pass to get schema node with specified
253      * {@code childName} and {@code namespace}
254      *
255      * @param dataSchemaNode
256      * @param childName
257      * @param namespace
258      * @return stack of schema nodes via which it was passed through. If found schema node is direct child then stack
259      *         contains only one node. If it is found under choice and case then stack should contains 2*n+1 element
260      *         (where n is number of choices through it was passed)
261      */
262     private Deque<DataSchemaNode> findSchemaNodeByNameAndNamespace(final DataSchemaNode dataSchemaNode,
263             final String childName, final URI namespace) {
264         final Deque<DataSchemaNode> result = new ArrayDeque<>();
265         List<ChoiceNode> childChoices = new ArrayList<>();
266         if (dataSchemaNode instanceof DataNodeContainer) {
267             for (DataSchemaNode childNode : ((DataNodeContainer) dataSchemaNode).getChildNodes()) {
268                 if (childNode instanceof ChoiceNode) {
269                     childChoices.add((ChoiceNode) childNode);
270                 } else {
271                     final QName childQName = childNode.getQName();
272                     if (childQName.getLocalName().equals(childName) && childQName.getNamespace().equals(namespace)) {
273                         result.push(childNode);
274                         return result;
275                     }
276                 }
277             }
278         }
279         // try to find data schema node in choice (looking for first match)
280         for (ChoiceNode choiceNode : childChoices) {
281             for (ChoiceCaseNode concreteCase : choiceNode.getCases()) {
282                 Deque<DataSchemaNode> resultFromRecursion = findSchemaNodeByNameAndNamespace(concreteCase, childName,
283                         namespace);
284                 if (!resultFromRecursion.isEmpty()) {
285                     resultFromRecursion.push(concreteCase);
286                     resultFromRecursion.push(choiceNode);
287                     return resultFromRecursion;
288                 }
289             }
290         }
291         return result;
292     }
293
294     private static class NamespaceAndName {
295         private final Optional<URI> uri;
296         private final String name;
297
298         public NamespaceAndName(final String name, final Optional<URI> uri) {
299             this.name = name;
300             this.uri = uri;
301         }
302
303         public String getName() {
304             return name;
305         }
306
307         public Optional<URI> getUri() {
308             return uri;
309         }
310     }
311
312     @Override
313     public void flush() throws IOException {
314         writer.flush();
315     }
316
317     @Override
318     public void close() throws IOException {
319         writer.flush();
320         writer.close();
321     }
322 }