From 36c1f3025db6a33dc2ef1bd11a3c884ba88992d6 Mon Sep 17 00:00:00 2001 From: serngawy Date: Tue, 15 Oct 2019 16:00:59 -0400 Subject: [PATCH] Add lenient JsonParserStream 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 Signed-off-by: Robert Varga (cherry picked from commit 50ba8458e9b8eb48cd47ecf8d0a0f6fb1d83d812) (cherry picked from commit be62b4e0494ae2b551373760c4da1b03ff19df83) --- .../data/codec/gson/JsonParserStream.java | 88 +++++++++++++++---- .../gson/JsonStreamToNormalizedNodeTest.java | 18 ++++ 2 files changed, 91 insertions(+), 15 deletions(-) diff --git a/yang/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/JsonParserStream.java b/yang/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/JsonParserStream.java index 0ef409c10b..9b7c48fe61 100644 --- a/yang/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/JsonParserStream.java +++ b/yang/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/JsonParserStream.java @@ -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 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. + * + *

+ * Returned parser will treat incoming JSON data leniently: + *

    + *
  • JSON elements referring to unknown constructs will be silently ignored
  • + *
+ * + * @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. + * + *

+ * Returned parser will treat incoming JSON data leniently: + *

    + *
  • JSON elements referring to unknown constructs will be silently ignored
  • + *
+ * + * @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 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(); diff --git a/yang/yang-data-codec-gson/src/test/java/org/opendaylight/yangtools/yang/data/codec/gson/JsonStreamToNormalizedNodeTest.java b/yang/yang-data-codec-gson/src/test/java/org/opendaylight/yangtools/yang/data/codec/gson/JsonStreamToNormalizedNodeTest.java index 9402f989f6..876d34db4e 100644 --- a/yang/yang-data-codec-gson/src/test/java/org/opendaylight/yangtools/yang/data/codec/gson/JsonStreamToNormalizedNodeTest.java +++ b/yang/yang-data-codec-gson/src/test/java/org/opendaylight/yangtools/yang/data/codec/gson/JsonStreamToNormalizedNodeTest.java @@ -168,6 +168,24 @@ public class JsonStreamToNormalizedNodeTest extends AbstractComplexJsonTest { } } + /** + * Should not fail as we set the parser to be lenient. + * + *

+ * 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"); -- 2.36.6