Disconnect JSONCodec from JsonWriter 74/111174/4
authorRobert Varga <robert.varga@pantheon.tech>
Tue, 2 Apr 2024 08:49:01 +0000 (10:49 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Tue, 2 Apr 2024 12:18:08 +0000 (14:18 +0200)
We have a use case where we would like to JSON-encode values without a
JsonWriter. Introduce JSONValueWriter to enable the values to be
intercepted.

JIRA: YANGTOOLS-1569
Change-Id: I8cabe490a78effb78d6d4bfaf0bb62238bd1b809
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
15 files changed:
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/BooleanJSONCodec.java
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/DefaultJSONValueWriter.java [new file with mode: 0644]
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/EmptyJSONCodec.java
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/IdentityrefJSONCodec.java
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/JSONCodec.java
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/JSONInstanceIdentifierCodec.java
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/JSONNormalizedNodeStreamWriter.java
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/JSONValueWriter.java [new file with mode: 0644]
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/NullJSONCodec.java
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/NumberJSONCodec.java
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/QuotedJSONCodec.java
codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/UnionJSONCodec.java
codec/yang-data-codec-gson/src/test/java/org/opendaylight/yangtools/yang/data/codec/gson/Bug8083Test.java
codec/yang-data-codec-gson/src/test/java/org/opendaylight/yangtools/yang/data/codec/gson/YT1473Test.java
codec/yang-data-codec-gson/src/test/java/org/opendaylight/yangtools/yang/data/codec/gson/YT1542Test.java

index 4f04e6166eff944778f68a6976d299e879cd2ab0..401465b9291e91597e8c4941708f95069eab122e 100644 (file)
@@ -7,7 +7,6 @@
  */
 package org.opendaylight.yangtools.yang.data.codec.gson;
 
-import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import org.opendaylight.yangtools.yang.data.impl.codec.DataStringCodec;
 
@@ -20,7 +19,7 @@ final class BooleanJSONCodec extends AbstractJSONCodec<Boolean> {
     }
 
     @Override
-    public void writeValue(final JsonWriter ctx, final Boolean value) throws IOException {
-        ctx.value(value.booleanValue());
+    public void writeValue(final JSONValueWriter ctx, final Boolean value) throws IOException {
+        ctx.writeBoolean(value);
     }
 }
diff --git a/codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/DefaultJSONValueWriter.java b/codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/DefaultJSONValueWriter.java
new file mode 100644 (file)
index 0000000..e6d188a
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.codec.gson;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+
+/**
+ * A {@link JSONValueWriter} backed by a {@link JsonWriter}.
+ */
+public final class DefaultJSONValueWriter implements JSONValueWriter {
+    private final JsonWriter writer;
+
+    public DefaultJSONValueWriter(final JsonWriter writer) {
+        this.writer = requireNonNull(writer);
+    }
+
+    @Override
+    public void writeBoolean(final boolean value) throws IOException {
+        writer.value(value);
+    }
+
+    @Override
+    public void writeEmpty() throws IOException {
+        writer.beginArray().nullValue().endArray();
+    }
+
+    @Override
+    public void writeNumber(final Number value) throws IOException {
+        writer.value(requireNonNull(value));
+    }
+
+    @Override
+    public void writeString(final String value) throws IOException {
+        writer.value(requireNonNull(value));
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this).add("writer", writer).toString();
+    }
+}
index fa9d79060d10a523972e502fbd98d2b4cf79fab8..46f33cb2d3d1ae0a96090e91374a46006797a10b 100644 (file)
@@ -5,10 +5,8 @@
  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
  * and is available at http://www.eclipse.org/legal/epl-v10.html
  */
-
 package org.opendaylight.yangtools.yang.data.codec.gson;
 
-import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import org.opendaylight.yangtools.yang.common.Empty;
 
@@ -30,9 +28,7 @@ final class EmptyJSONCodec implements JSONCodec<Empty> {
     }
 
     @Override
-    public void writeValue(final JsonWriter ctx, final Empty value) throws IOException {
-        ctx.beginArray();
-        ctx.nullValue();
-        ctx.endArray();
+    public void writeValue(final JSONValueWriter ctx, final Empty value) throws IOException {
+        ctx.writeEmpty();
     }
 }
index cf4164ed4b29c105f253841e2e37e92fa6780981..3c3dad959cba8795eccd35600ed14459907811e4 100644 (file)
@@ -10,7 +10,6 @@ package org.opendaylight.yangtools.yang.data.codec.gson;
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Objects.requireNonNull;
 
-import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.yang.common.QName;
@@ -46,15 +45,9 @@ final class IdentityrefJSONCodec implements JSONCodec<QName> {
         }).getQName();
     }
 
-    /**
-     * Serialize QName with specified JsonWriter.
-     *
-     * @param writer JsonWriter
-     * @param value QName
-     */
     @Override
-    public void writeValue(final JsonWriter writer, final QName value) throws IOException {
-        writer.value(QNameCodecUtil.encodeQName(value, uri -> context.findModuleStatement(uri)
+    public void writeValue(final JSONValueWriter ctx, final QName value) throws IOException {
+        ctx.writeString(QNameCodecUtil.encodeQName(value, uri -> context.findModuleStatement(uri)
             .map(module -> module.argument().getLocalName())
             .orElseThrow(() -> new IllegalArgumentException("Cannot find module for " + uri))));
     }
index f03805a31210924e728414a1266cab2bd5045342..908709d88fa3bc9f57ed15a4ca13c01216f009e7 100644 (file)
@@ -24,9 +24,22 @@ public sealed interface JSONCodec<T> extends TypeAwareCodec<T, Object, JsonWrite
      * {@inheritDoc}.
      *
      * @throws IOException if the write fails
+     * @deprecated Use {@link #writeValue(JSONValueWriter, Object)} instead.
      */
     @Override
-    void writeValue(JsonWriter ctx, T value) throws IOException;
+    @Deprecated(since = "13.0.3", forRemoval = true)
+    default void writeValue(final JsonWriter writer, final T value) throws IOException {
+        writeValue(new DefaultJSONValueWriter(writer), value);
+    }
+
+    /**
+     * Serialize specified value with specified {@link JSONValueWriter}.
+     *
+     * @param ctx Write context
+     * @param value Value in native format
+     * @throws IOException if the write fails
+     */
+    void writeValue(JSONValueWriter ctx, T value) throws IOException;
 
     /**
      * {@inheritDoc}.
index 85db6d3e065dcfbd7859b41b721b3c70fca5f0b3..ed7ec5bb01022cd89cb7678d2bee4124add4745e 100644 (file)
@@ -10,7 +10,6 @@ package org.opendaylight.yangtools.yang.data.codec.gson;
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Objects.requireNonNull;
 
-import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.yang.common.QName;
@@ -102,13 +101,13 @@ abstract sealed class JSONInstanceIdentifierCodec extends AbstractStringInstance
     }
 
     @Override
-    public final void writeValue(final JsonWriter ctx, final YangInstanceIdentifier value) throws IOException {
+    public final void writeValue(final JSONValueWriter ctx, final YangInstanceIdentifier value) throws IOException {
         final String str;
         try {
             str = serialize(value);
         } catch (IllegalArgumentException e) {
             throw new IOException("Failed to encode instance-identifier", e);
         }
-        ctx.value(str);
+        ctx.writeString(str);
     }
 }
index 5439f14af251d0591d7464743ab5e0a534cf4dab..465b895f2078362a1b8c8c1f2873a2544c0732d6 100644 (file)
@@ -93,6 +93,7 @@ public abstract class JSONNormalizedNodeStreamWriter implements NormalizedNodeSt
     private final NormalizedNodeStreamWriterStack tracker;
     private final JSONCodecFactory codecs;
     private final JsonWriter writer;
+    private final DefaultJSONValueWriter valueWriter;
     private JSONStreamWriterContext context;
 
     JSONNormalizedNodeStreamWriter(final JSONCodecFactory codecFactory, final NormalizedNodeStreamWriterStack tracker,
@@ -101,6 +102,7 @@ public abstract class JSONNormalizedNodeStreamWriter implements NormalizedNodeSt
         codecs = requireNonNull(codecFactory);
         this.tracker = requireNonNull(tracker);
         context = requireNonNull(rootContext);
+        valueWriter = new DefaultJSONValueWriter(writer);
     }
 
     /**
@@ -454,7 +456,7 @@ public abstract class JSONNormalizedNodeStreamWriter implements NormalizedNodeSt
 
     @SuppressWarnings("unchecked")
     private void writeValue(final Object value, final JSONCodec<?> codec) throws IOException {
-        ((JSONCodec<Object>) codec).writeValue(writer, value);
+        ((JSONCodec<Object>) codec).writeValue(valueWriter, value);
     }
 
     private void writeAnydataValue(final Object value) throws IOException {
diff --git a/codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/JSONValueWriter.java b/codec/yang-data-codec-gson/src/main/java/org/opendaylight/yangtools/yang/data/codec/gson/JSONValueWriter.java
new file mode 100644 (file)
index 0000000..c2e0a11
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.codec.gson;
+
+import java.io.IOException;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * A handler used to write out JSON-encoded values.
+ */
+@NonNullByDefault
+public interface JSONValueWriter {
+    /**
+     * Write a {@code boolean} value, as per
+     * <a href="https://www.rfc-editor.org/rfc/rfc7951#section-6.3">RFC7951, section 6.3</a>.
+     *
+     * @param value Value to write
+     * @throws IOException when an IO error occurs
+     */
+    void writeBoolean(boolean value) throws IOException;
+
+    /**
+     * Write an {@code empty} value, as per
+     * <a href="https://www.rfc-editor.org/rfc/rfc7951#section-6.9">RFC7951, section 6.9</a>.
+     *
+     * @throws IOException when an IO error occurs
+     */
+    void writeEmpty() throws IOException;
+
+    /**
+     * Write a numeric value, as per
+     * <a href="https://www.rfc-editor.org/rfc/rfc7951#section-6.1">RFC7951, section 6.1</a>.
+     *
+     * @param value Value to write
+     * @throws IOException when an IO error occurs
+     */
+    void writeNumber(Number value) throws IOException;
+
+    /**
+     * Write a string value, as per
+     * <a href="https://www.rfc-editor.org/rfc/rfc7951#section-6.2">RFC7951, section 6.2</a> and other types which have
+     * are represented as JSON strings.
+     *
+     * @param value Value to write
+     * @throws IOException when an IO error occurs
+     */
+    void writeString(String value) throws IOException;
+}
index a4fa1fcb7afa15e15999dfeb8fb1a76a82c48b32..3ab79f517ce11e909e6d9cb3fa6880ba2deb5a5f 100644 (file)
@@ -7,8 +7,6 @@
  */
 package org.opendaylight.yangtools.yang.data.codec.gson;
 
-import com.google.gson.stream.JsonWriter;
-import java.io.IOException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -32,7 +30,7 @@ final class NullJSONCodec implements JSONCodec<Object> {
     }
 
     @Override
-    public void writeValue(final JsonWriter ctx, final Object value) throws IOException {
+    public void writeValue(final JSONValueWriter ctx, final Object value) {
         // NOOP since codec is unknown.
         LOG.warn("Call of the serializeToWriter method on null codec. No operation performed.");
     }
index 9800476b7a4aa755f1508194292567fdb8b070a7..6d4a4bf008ed212948e7198221059a3c910a36fe 100644 (file)
@@ -7,7 +7,6 @@
  */
 package org.opendaylight.yangtools.yang.data.codec.gson;
 
-import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import org.opendaylight.yangtools.yang.data.impl.codec.DataStringCodec;
 
@@ -22,7 +21,7 @@ final class NumberJSONCodec<T extends Number> extends AbstractJSONCodec<T> {
     }
 
     @Override
-    public void writeValue(final JsonWriter ctx, final T value) throws IOException {
-        ctx.value(value);
+    public void writeValue(final JSONValueWriter ctx, final T value) throws IOException {
+        ctx.writeNumber(value);
     }
 }
\ No newline at end of file
index 6d3a107fd06d0f43a70616770bd0468db44f2479..9eec9d4038498e88f1b6f9a03b4d827c16dc0daf 100644 (file)
@@ -7,7 +7,6 @@
  */
 package org.opendaylight.yangtools.yang.data.codec.gson;
 
-import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import org.opendaylight.yangtools.yang.data.impl.codec.DataStringCodec;
 
@@ -22,7 +21,7 @@ final class QuotedJSONCodec<T> extends AbstractJSONCodec<T> {
     }
 
     @Override
-    public void writeValue(final JsonWriter ctx, final T value) throws IOException {
-        ctx.value(serialize(value));
+    public void writeValue(final JSONValueWriter ctx, final T value) throws IOException {
+        ctx.writeString(serialize(value));
     }
 }
\ No newline at end of file
index 7991fc5152397f87843bf30acdeacb35d166e692..8f5b54132f10f1e48bf5ca968f51348713509833 100644 (file)
@@ -11,7 +11,6 @@ import static com.google.common.base.Verify.verify;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
-import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import java.util.Iterator;
 import java.util.List;
@@ -97,7 +96,7 @@ abstract sealed class UnionJSONCodec<T> implements JSONCodec<T> {
 
     @Override
     @SuppressWarnings("checkstyle:illegalCatch")
-    public final void writeValue(final JsonWriter ctx, final T value) throws IOException {
+    public final void writeValue(final JSONValueWriter ctx, final T value) throws IOException {
         for (var codec : codecs) {
             if (!codec.getDataType().isInstance(value)) {
                 LOG.debug("Codec {} cannot accept input {}, skipping it", codec, value);
index ef9de558fd7db80c2447ab89c023a6e6f5695f3c..7116e05154f76cc1851f48c9163318889d456ae5 100644 (file)
@@ -11,14 +11,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.opendaylight.yangtools.yang.data.codec.gson.TestUtils.loadTextFile;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import java.io.StringReader;
 import java.net.URISyntaxException;
@@ -250,12 +249,13 @@ class Bug8083Test {
     }
 
     private static String writeInstanceIdentifier(final JSONCodecFactorySupplier supplier) throws IOException {
-        final var writer = mock(JsonWriter.class);
-        final var captor = ArgumentCaptor.forClass(String.class);
-        doReturn(writer).when(writer).value(captor.capture());
+        final var writer = mock(JSONValueWriter.class);
+        doNothing().when(writer).writeString(any());
 
         getCodec(supplier).writeValue(writer, TEST_IID);
-        verify(writer).value(any(String.class));
+
+        final var captor = ArgumentCaptor.forClass(String.class);
+        verify(writer).writeString(captor.capture());
         return captor.getValue();
     }
 }
index b3a0904fc05204188bc358d8793d8622bf475f94..3d8d477c8ab70d8d3090bf8f30ebd0544054a9d4 100644 (file)
@@ -9,13 +9,12 @@ package org.opendaylight.yangtools.yang.data.codec.gson;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doReturn;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
 import com.google.common.collect.ImmutableSet;
-import com.google.gson.stream.JsonWriter;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -211,11 +210,12 @@ class YT1473Test {
     }
 
     private static void assertSerdes(final String expected, final YangInstanceIdentifier id) throws Exception {
-        final var writer = mock(JsonWriter.class);
-        final var captor = ArgumentCaptor.forClass(String.class);
-        doReturn(writer).when(writer).value(anyString());
+        final var writer = mock(JSONValueWriter.class);
+        doNothing().when(writer).writeString(any());
         CODEC.writeValue(writer, id);
-        verify(writer).value(captor.capture());
+
+        final var captor = ArgumentCaptor.forClass(String.class);
+        verify(writer).writeString(captor.capture());
 
         assertEquals(expected, captor.getValue());
         assertEquals(id, CODEC.parseValue(expected));
index 9a5521e02b5f1592022113a90bf4d54d7194351d..ef41e887b8f50d5948a71053a68fe8945361cd6f 100644 (file)
@@ -22,7 +22,7 @@ class YT1542Test {
     void writeInstanceIdentifierReportsIOException() {
         final var codec = JSONCodecFactorySupplier.RFC7951.createSimple(YangParserTestUtils.parseYang())
             .instanceIdentifierCodec();
-        final var ex = assertThrows(IOException.class, () -> codec.writeValue(null,
+        final var ex = assertThrows(IOException.class, () -> codec.writeValue((JSONValueWriter) null,
             YangInstanceIdentifier.of(QName.create("foo", "bar"))));
         assertEquals("Failed to encode instance-identifier", ex.getMessage());
         assertInstanceOf(IllegalArgumentException.class, ex.getCause());