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