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