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