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