Add lenient JsonParserStream 46/85346/1
authorserngawy <m.elserngawy@gmail.com>
Tue, 15 Oct 2019 20:00:59 +0000 (16:00 -0400)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 23 Oct 2019 10:57:07 +0000 (12:57 +0200)
It is beneficial to have the JSON parser ignore unknown constructs,
so that all other data gets parsed successfully.

This patch adds JsonParserStream.createLenient() to return such
parsers.

JIRA: YANGTOOLS-1034
Change-Id: I5ec9dbbf6dc52270bf83d7c4ed6c4d16181f74a7
Signed-off-by: serngawy <m.elserngawy@gmail.com>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
(cherry picked from commit 50ba8458e9b8eb48cd47ecf8d0a0f6fb1d83d812)

yang/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/JsonParserStream.java
yang/yang-data-codec-gson/src/test/java/org/opendaylight/yangtools/yang/data/codec/gson/JsonStreamToNormalizedNodeTest.java

index ede58a5a1805299fc1ed2c5e12593374eb83fefd..acfc3afa4e8106b2a2e818bd3aca91d27c271723 100644 (file)
@@ -53,6 +53,8 @@ import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Text;
@@ -65,16 +67,21 @@ import org.w3c.dom.Text;
 public final class JsonParserStream implements Closeable, Flushable {
     static final String ANYXML_ARRAY_ELEMENT_ID = "array-element";
 
+    private static final Logger LOG = LoggerFactory.getLogger(JsonParserStream.class);
     private final Deque<URI> namespaces = new ArrayDeque<>();
     private final NormalizedNodeStreamWriter writer;
     private final JSONCodecFactory codecs;
     private final DataSchemaNode parentNode;
 
+    // TODO: consider class specialization to remove this field
+    private final boolean lenient;
+
     private JsonParserStream(final NormalizedNodeStreamWriter writer, final JSONCodecFactory codecs,
-            final DataSchemaNode parentNode) {
+            final DataSchemaNode parentNode, final boolean lenient) {
         this.writer = requireNonNull(writer);
         this.codecs = requireNonNull(codecs);
         this.parentNode = parentNode;
+        this.lenient = lenient;
     }
 
     /**
@@ -89,7 +96,7 @@ public final class JsonParserStream implements Closeable, Flushable {
      */
     public static @NonNull JsonParserStream create(final @NonNull NormalizedNodeStreamWriter writer,
             final @NonNull JSONCodecFactory codecFactory) {
-        return new JsonParserStream(writer, codecFactory, codecFactory.getSchemaContext());
+        return new JsonParserStream(writer, codecFactory, codecFactory.getSchemaContext(), false);
     }
 
     /**
@@ -104,21 +111,55 @@ public final class JsonParserStream implements Closeable, Flushable {
      */
     public static @NonNull JsonParserStream create(final @NonNull NormalizedNodeStreamWriter writer,
             final @NonNull JSONCodecFactory codecFactory, final @NonNull SchemaNode parentNode) {
-        final DataSchemaNode parent;
-        if (parentNode instanceof DataSchemaNode) {
-            parent = (DataSchemaNode) parentNode;
-        } else if (parentNode instanceof OperationDefinition) {
-            parent = OperationAsContainer.of((OperationDefinition) parentNode);
-        } else {
-            throw new IllegalArgumentException("Illegal parent node " + requireNonNull(parentNode));
-        }
-        return new JsonParserStream(writer, codecFactory, parent);
+        return new JsonParserStream(writer, codecFactory, validateParent(parentNode), false);
+    }
+
+    /**
+     * Create a new {@link JsonParserStream} backed by specified {@link NormalizedNodeStreamWriter}
+     * and {@link JSONCodecFactory}. The stream will be logically rooted at the top of the SchemaContext associated
+     * with the specified codec factory.
+     *
+     * <p>
+     * Returned parser will treat incoming JSON data leniently:
+     * <ul>
+     *   <li>JSON elements referring to unknown constructs will be silently ignored</li>
+     * </ul>
+     *
+     * @param writer NormalizedNodeStreamWriter to use for instantiation of normalized nodes
+     * @param codecFactory {@link JSONCodecFactory} to use for parsing leaves
+     * @return A new {@link JsonParserStream}
+     * @throws NullPointerException if any of the arguments are null
+     */
+    public static @NonNull JsonParserStream createLenient(final @NonNull NormalizedNodeStreamWriter writer,
+            final @NonNull JSONCodecFactory codecFactory) {
+        return new JsonParserStream(writer, codecFactory, codecFactory.getSchemaContext(), true);
+    }
+
+    /**
+     * Create a new {@link JsonParserStream} backed by specified {@link NormalizedNodeStreamWriter}
+     * and {@link JSONCodecFactory}. The stream will be logically rooted at the specified parent node.
+     *
+     * <p>
+     * Returned parser will treat incoming JSON data leniently:
+     * <ul>
+     *   <li>JSON elements referring to unknown constructs will be silently ignored</li>
+     * </ul>
+     *
+     * @param writer NormalizedNodeStreamWriter to use for instantiation of normalized nodes
+     * @param codecFactory {@link JSONCodecFactory} to use for parsing leaves
+     * @param parentNode Logical root node
+     * @return A new {@link JsonParserStream}
+     * @throws NullPointerException if any of the arguments are null
+     */
+    public static @NonNull JsonParserStream createLenient(final @NonNull NormalizedNodeStreamWriter writer,
+            final @NonNull JSONCodecFactory codecFactory, final @NonNull SchemaNode parentNode) {
+        return new JsonParserStream(writer, codecFactory, validateParent(parentNode), true);
     }
 
     public JsonParserStream parse(final JsonReader reader) {
         // code copied from gson's JsonParser and Stream classes
 
-        final boolean lenient = reader.isLenient();
+        final boolean readerLenient = reader.isLenient();
         reader.setLenient(true);
         boolean isEmpty = true;
         try {
@@ -143,7 +184,7 @@ public final class JsonParserStream implements Closeable, Flushable {
         } catch (StackOverflowError | OutOfMemoryError e) {
             throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e);
         } finally {
-            reader.setLenient(lenient);
+            reader.setLenient(readerLenient);
         }
     }
 
@@ -245,7 +286,14 @@ public final class JsonParserStream implements Closeable, Flushable {
                     }
                     final Entry<String, URI> namespaceAndName = resolveNamespace(jsonElementName, parentSchema);
                     final String localName = namespaceAndName.getKey();
-                    addNamespace(namespaceAndName.getValue());
+                    final URI namespace = namespaceAndName.getValue();
+                    if (lenient && (localName == null || namespace == null)) {
+                        LOG.debug("Schema node with name {} was not found under {}", localName,
+                            parentSchema.getQName());
+                        in.skipValue();
+                        continue;
+                    }
+                    addNamespace(namespace);
                     if (!namesakes.add(jsonElementName)) {
                         throw new JsonSyntaxException("Duplicate name " + jsonElementName + " in JSON input.");
                     }
@@ -339,7 +387,7 @@ public final class JsonParserStream implements Closeable, Flushable {
             } else if (potentialUris.size() > 1) {
                 throw new IllegalStateException("Choose suitable module name for element " + nodeNamePart + ":"
                         + toModuleNames(potentialUris));
-            } else if (potentialUris.isEmpty()) {
+            } else if (potentialUris.isEmpty() && !lenient) {
                 throw new IllegalStateException("Schema node with name " + nodeNamePart + " was not found under "
                         + dataSchemaNode.getQName() + ".");
             }
@@ -383,6 +431,16 @@ public final class JsonParserStream implements Closeable, Flushable {
         return namespaces.peek();
     }
 
+    private static DataSchemaNode validateParent(final SchemaNode parent) {
+        if (parent instanceof DataSchemaNode) {
+            return (DataSchemaNode) parent;
+        } else if (parent instanceof OperationDefinition) {
+            return OperationAsContainer.of((OperationDefinition) parent);
+        } else {
+            throw new IllegalArgumentException("Illegal parent node " + requireNonNull(parent));
+        }
+    }
+
     @Override
     public void flush() throws IOException {
         writer.flush();
index 9402f989f6639c52ebc04de2c9d5d18132eea845..876d34db4e43d8bacbdc66e844c4f0aa4ab4e351 100644 (file)
@@ -168,6 +168,24 @@ public class JsonStreamToNormalizedNodeTest extends AbstractComplexJsonTest {
         }
     }
 
+    /**
+     * Should not fail as we set the parser to be lenient.
+     *
+     * <p>
+     * Json input contains element which doesn't exist in YANG schema
+     */
+    @Test
+    public void parsingSkipNotExistingElement() throws IOException, URISyntaxException {
+        final String inputJson = loadTextFile("/complexjson/not-existing-element.json");
+        final NormalizedNodeResult result = new NormalizedNodeResult();
+        final NormalizedNodeStreamWriter streamWriter = ImmutableNormalizedNodeStreamWriter.from(result);
+        final JsonParserStream jsonParser = JsonParserStream.createLenient(streamWriter,
+            JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02.getShared(schemaContext));
+        jsonParser.parse(new JsonReader(new StringReader(inputJson)));
+        final NormalizedNode<?, ?> transformedInput = result.getResult();
+        assertNotNull(transformedInput);
+    }
+
     @Test
     public void listItemWithoutArray() throws IOException, URISyntaxException {
         final String inputJson = loadTextFile("/complexjson/keyed-list-restconf-behaviour.json");