Split out operation output serialization 71/111271/4
authorRobert Varga <robert.varga@pantheon.tech>
Thu, 4 Apr 2024 15:16:45 +0000 (17:16 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Fri, 5 Apr 2024 07:23:02 +0000 (09:23 +0200)
NormalizedNodePayload is quite overloaded, handling a ton of different
options.

This patch renames AbstractBody to RequestBody and creates its
counterpart ReplyBody. The JAX-RS serialization bit is handled through a
pair of MessageBodyWriters, which end up just calling ReplyBody methods.

We then define an OperationOutputBody, which handles the case of
RPC/action invocation result and stop using NormalizedNodePayload for
that purpose.

This also speeds up the entire process, as we can reuse JSON codecs
and not look them up again.

JIRA: NETCONF-773
Change-Id: Ib711451498f9bad485b69d14f6ae8195038ff506
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
22 files changed:
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JaxRsRestconf.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JsonReplyBodyWriter.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/ReplyBodyWriter.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/XmlReplyBodyWriter.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/AbstractNormalizedNodeBodyWriter.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyWriter.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/ParameterAwareNormalizedNodeWriter.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyWriter.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ChildBody.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataPostBody.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataPostResult.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationInputBody.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationOutputBody.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/PatchBody.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ReplyBody.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/RequestBody.java [moved from restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/AbstractBody.java with 86% similarity]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ResourceBody.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/AbstractRestconfTest.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/Netconf799Test.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfOperationsPostTest.java

index 9ed12ea8c7e4fa6fd9f4c632f0668faf2999262a..7ad3f20b393daee1aefcb44eee65e51f2aa8600b 100644 (file)
@@ -69,6 +69,7 @@ import org.opendaylight.restconf.server.api.JsonPatchBody;
 import org.opendaylight.restconf.server.api.JsonResourceBody;
 import org.opendaylight.restconf.server.api.ModulesGetResult;
 import org.opendaylight.restconf.server.api.OperationInputBody;
+import org.opendaylight.restconf.server.api.OperationOutputBody;
 import org.opendaylight.restconf.server.api.OperationsGetResult;
 import org.opendaylight.restconf.server.api.OperationsPostResult;
 import org.opendaylight.restconf.server.api.RestconfServer;
@@ -761,7 +762,7 @@ public final class JaxRsRestconf implements ParamConverterProvider {
                 Response transform(final OperationsPostResult result) {
                     final var body = result.output();
                     return body == null ? Response.noContent().build()
-                        : Response.ok().entity(new NormalizedNodePayload(result.path().inference(), body)).build();
+                        : Response.ok().entity(new OperationOutputBody(result.path(), body, false)).build();
                 }
             });
     }
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JsonReplyBodyWriter.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JsonReplyBodyWriter.java
new file mode 100644 (file)
index 0000000..751ef05
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * 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.restconf.nb.jaxrs;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.ext.Provider;
+import org.opendaylight.restconf.api.MediaTypes;
+import org.opendaylight.restconf.server.api.ReplyBody;
+
+@Provider
+@Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
+public final class JsonReplyBodyWriter extends ReplyBodyWriter {
+    @Override
+    void writeTo(final ReplyBody body, final OutputStream out) throws IOException {
+        body.writeJSON(out);
+    }
+}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/ReplyBodyWriter.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/ReplyBodyWriter.java
new file mode 100644 (file)
index 0000000..ad28271
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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.restconf.nb.jaxrs;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.restconf.server.api.ReplyBody;
+
+abstract sealed class ReplyBodyWriter implements MessageBodyWriter<ReplyBody>
+        permits JsonReplyBodyWriter, XmlReplyBodyWriter {
+    @Override
+    public final boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations,
+            final MediaType mediaType) {
+        return ReplyBody.class.isAssignableFrom(type);
+    }
+
+    @Override
+    public final void writeTo(final ReplyBody body, final Class<?> type, final Type genericType,
+            final Annotation[] annotations, final MediaType mediaType, final MultivaluedMap<String, Object> httpHeaders,
+            final OutputStream entityStream) throws IOException {
+        writeTo(requireNonNull(body), requireNonNull(entityStream));
+    }
+
+    abstract void writeTo(@NonNull ReplyBody body, @NonNull OutputStream out) throws IOException;
+}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/XmlReplyBodyWriter.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/XmlReplyBodyWriter.java
new file mode 100644 (file)
index 0000000..15b8d3a
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * 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.restconf.nb.jaxrs;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.ext.Provider;
+import org.opendaylight.restconf.api.MediaTypes;
+import org.opendaylight.restconf.server.api.ReplyBody;
+
+@Provider
+@Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+public final class XmlReplyBodyWriter extends ReplyBodyWriter {
+    @Override
+    void writeTo(final ReplyBody body, final OutputStream out) throws IOException {
+        body.writeXML(out);
+    }
+}
index 2a0a81f452c50297d25337d0b6f2b3f7ef927039..13b62fb0855dabe2aa544e68389bf45780642d9c 100644 (file)
@@ -21,6 +21,8 @@ import org.opendaylight.aaa.web.WebServer;
 import org.opendaylight.aaa.web.servlet.ServletSupport;
 import org.opendaylight.restconf.nb.jaxrs.JaxRsRestconf;
 import org.opendaylight.restconf.nb.jaxrs.JaxRsWebHostMetadata;
+import org.opendaylight.restconf.nb.jaxrs.JsonReplyBodyWriter;
+import org.opendaylight.restconf.nb.jaxrs.XmlReplyBodyWriter;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.JsonNormalizedNodeBodyWriter;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.JsonPatchStatusBodyWriter;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.XmlNormalizedNodeBodyWriter;
@@ -68,6 +70,7 @@ public final class JaxRsNorthbound implements AutoCloseable {
                         @Override
                         public Set<Object> getSingletons() {
                             return Set.of(
+                                new JsonReplyBodyWriter(), new XmlReplyBodyWriter(),
                                 new RestconfDocumentedExceptionMapper(databindProvider),
                                 new JaxRsRestconf(server));
                         }
index c4fbef3cef26b3d05f45c61743a9b8576f49b1dd..a4a7357f3bc1d6b27296be6d802299badc6ed597 100644 (file)
@@ -19,10 +19,7 @@ import javax.ws.rs.ext.MessageBodyWriter;
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
-import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
-import org.opendaylight.yangtools.yang.model.api.stmt.ActionEffectiveStatement;
-import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
 
 abstract class AbstractNormalizedNodeBodyWriter implements MessageBodyWriter<NormalizedNodePayload> {
@@ -36,28 +33,10 @@ abstract class AbstractNormalizedNodeBodyWriter implements MessageBodyWriter<Nor
     public final void writeTo(final NormalizedNodePayload context, final Class<?> type, final Type genericType,
             final Annotation[] annotations, final MediaType mediaType, final MultivaluedMap<String, Object> httpHeaders,
             final OutputStream entityStream) throws IOException {
-        final var output = requireNonNull(entityStream);
-        final var stack = context.inference().toSchemaInferenceStack();
-        // FIXME: this dispatch is here to handle codec transition to 'output', but that should be completely okay with
-        //        the instantiation path we are using (based in Inference).
-        if (!stack.isEmpty()) {
-            final var stmt = stack.currentStatement();
-            if (stmt instanceof RpcEffectiveStatement rpc) {
-                stack.enterSchemaTree(rpc.output().argument());
-                writeOperationOutput(stack, context.writerParameters(), (ContainerNode) context.data(), output);
-                return;
-            } else if (stmt instanceof ActionEffectiveStatement action) {
-                stack.enterSchemaTree(action.output().argument());
-                writeOperationOutput(stack, context.writerParameters(), (ContainerNode) context.data(), output);
-                return;
-            }
-        }
-        writeData(stack, context.writerParameters(), context.data(), output);
+        writeData(context.inference().toSchemaInferenceStack(), context.writerParameters(), context.data(),
+            requireNonNull(entityStream));
     }
 
-    abstract void writeOperationOutput(@NonNull SchemaInferenceStack stack, @NonNull QueryParameters writerParameters,
-        @NonNull ContainerNode output, @NonNull OutputStream entityStream) throws IOException;
-
     abstract void writeData(@NonNull SchemaInferenceStack stack, @NonNull QueryParameters writerParameters,
         @NonNull NormalizedNode data, @NonNull OutputStream entityStream) throws IOException;
 }
index 0cbe81238eadb7246b93c362cce9b1395f451a53..b4c58e1211b44b667066f23a69828f993ca384ec 100644 (file)
@@ -22,7 +22,6 @@ import org.opendaylight.restconf.nb.rfc8040.jersey.providers.api.RestconfNormali
 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
 import org.opendaylight.yangtools.yang.common.XMLNamespace;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
-import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
@@ -37,24 +36,6 @@ import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference
 public final class JsonNormalizedNodeBodyWriter extends AbstractNormalizedNodeBodyWriter {
     private static final int DEFAULT_INDENT_SPACES_NUM = 2;
 
-    @Override
-    void writeOperationOutput(final SchemaInferenceStack stack, final QueryParameters writerParameters,
-            final ContainerNode output, final OutputStream entityStream) throws IOException {
-        // RpcDefinition/ActionDefinition is not supported as initial codec in JSONStreamWriter, so we need to emit
-        // initial output declaration
-        try (var jsonWriter = createJsonWriter(entityStream, writerParameters.prettyPrint())) {
-            final var module = stack.currentModule();
-            jsonWriter.beginObject().name(module.argument().getLocalName() + ":output").beginObject();
-
-            final var nnWriter = createNormalizedNodeWriter(stack.toInference(), jsonWriter, writerParameters,
-                module.namespace().argument());
-            writeChildren(nnWriter, output);
-            nnWriter.flush();
-
-            jsonWriter.endObject().endObject();
-        }
-    }
-
     @Override
     void writeData(final SchemaInferenceStack stack, final QueryParameters writerParameters, final NormalizedNode data,
             final OutputStream entityStream) throws IOException {
@@ -81,13 +62,6 @@ public final class JsonNormalizedNodeBodyWriter extends AbstractNormalizedNodeBo
         }
     }
 
-    private static void writeChildren(final RestconfNormalizedNodeWriter nnWriter, final ContainerNode data)
-            throws IOException {
-        for (var child : data.body()) {
-            nnWriter.write(child);
-        }
-    }
-
     private static RestconfNormalizedNodeWriter createNormalizedNodeWriter(final Inference inference,
             final JsonWriter jsonWriter, final QueryParameters writerParameters,
             final @Nullable XMLNamespace initialNamespace) {
index 7370b229fb6f2260f82cd13a15ac22b9f554bf57..21eec1321c80af704542c20d422f91b609d5be83 100644 (file)
@@ -16,6 +16,7 @@ import java.util.List;
 import java.util.Map.Entry;
 import java.util.Set;
 import javax.xml.transform.dom.DOMSource;
+import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.restconf.api.query.DepthParam;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.api.RestconfNormalizedNodeWriter;
 import org.opendaylight.yangtools.yang.common.Ordering;
@@ -73,7 +74,7 @@ public class ParameterAwareNormalizedNodeWriter implements RestconfNormalizedNod
      * @param fields Selected child nodes to write
      * @return A new instance.
      */
-    public static ParameterAwareNormalizedNodeWriter forStreamWriter(
+    public static @NonNull ParameterAwareNormalizedNodeWriter forStreamWriter(
             final NormalizedNodeStreamWriter writer, final DepthParam maxDepth, final List<Set<QName>> fields) {
         return forStreamWriter(writer, true,  maxDepth, fields);
     }
@@ -93,10 +94,8 @@ public class ParameterAwareNormalizedNodeWriter implements RestconfNormalizedNod
      * @param fields Selected child nodes to write
      * @return A new instance.
      */
-    public static ParameterAwareNormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer,
-                                                                     final boolean orderKeyLeaves,
-                                                                     final DepthParam depth,
-                                                                     final List<Set<QName>> fields) {
+    public static @NonNull ParameterAwareNormalizedNodeWriter forStreamWriter(final NormalizedNodeStreamWriter writer,
+            final boolean orderKeyLeaves, final DepthParam depth, final List<Set<QName>> fields) {
         return orderKeyLeaves ? new OrderedParameterAwareNormalizedNodeWriter(writer, depth, fields)
                 : new ParameterAwareNormalizedNodeWriter(writer, depth, fields);
     }
index 4b3dbc61992078199b23ab540d5f6f917b5734c5..93afdb798da6cf65bfd0a04367cb301555f12a24 100644 (file)
@@ -44,17 +44,6 @@ public final class XmlNormalizedNodeBodyWriter extends AbstractNormalizedNodeBod
         XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
     }
 
-    @Override
-    void writeOperationOutput(final SchemaInferenceStack stack, final QueryParameters writerParameters,
-            final ContainerNode output, final OutputStream entityStream) throws IOException {
-        // RpcDefinition/ActionDefinition is not supported as initial codec in XMLStreamWriter, so we need to emit
-        // initial output declaration.
-        final var xmlWriter = createXmlWriter(entityStream, writerParameters.prettyPrint());
-        final var nnWriter = createNormalizedNodeWriter(xmlWriter, stack.toInference(), writerParameters);
-        writeElements(xmlWriter, nnWriter, output);
-        nnWriter.flush();
-    }
-
     @Override
     void writeData(final SchemaInferenceStack stack, final QueryParameters writerParameters, final NormalizedNode data,
             final OutputStream entityStream) throws IOException {
@@ -134,22 +123,4 @@ public final class XmlNormalizedNodeBodyWriter extends AbstractNormalizedNodeBod
             throw new IOException("Failed to write elements", e);
         }
     }
-
-    private static void writeElements(final XMLStreamWriter xmlWriter, final RestconfNormalizedNodeWriter nnWriter,
-            final ContainerNode data) throws IOException {
-        final QName nodeType = data.name().getNodeType();
-        final String namespace = nodeType.getNamespace().toString();
-        try {
-            xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, nodeType.getLocalName(), namespace);
-            xmlWriter.writeDefaultNamespace(namespace);
-            for (var child : data.body()) {
-                nnWriter.write(child);
-            }
-            nnWriter.flush();
-            xmlWriter.writeEndElement();
-            xmlWriter.flush();
-        } catch (final XMLStreamException e) {
-            throw new IOException("Failed to write elements", e);
-        }
-    }
 }
index 48bd554633175dd5301af92983a3615b9280c696..3972d8ead8f58148c0187b22d5fc170b3de51690 100644 (file)
@@ -86,6 +86,7 @@ import org.opendaylight.restconf.server.api.DatabindPath.Data;
 import org.opendaylight.restconf.server.api.DatabindPath.InstanceReference;
 import org.opendaylight.restconf.server.api.DatabindPath.Rpc;
 import org.opendaylight.restconf.server.api.OperationInputBody;
+import org.opendaylight.restconf.server.api.OperationOutputBody;
 import org.opendaylight.restconf.server.api.OperationsGetResult;
 import org.opendaylight.restconf.server.api.OperationsPostResult;
 import org.opendaylight.restconf.server.api.PatchBody;
@@ -1453,7 +1454,8 @@ public abstract class RestconfStrategy {
         return ret;
     }
 
-    private @NonNull RestconfFuture<InvokeOperation> dataInvokePOST(final Action path, final OperationInputBody body) {
+    private @NonNull RestconfFuture<InvokeOperation> dataInvokePOST(final @NonNull Action path,
+            final @NonNull OperationInputBody body) {
         final ContainerNode input;
         try {
             input = body.toContainerNode(path);
@@ -1470,7 +1472,7 @@ public abstract class RestconfStrategy {
         final var future = dataInvokePOST(actionService, path, input);
         return future.transform(result -> result.getOutput()
             .flatMap(output -> output.isEmpty() ? Optional.empty()
-                : Optional.of(new InvokeOperation(new NormalizedNodePayload(path.inference(), output))))
+                : Optional.of(new InvokeOperation(new OperationOutputBody(path, output, false))))
             .orElse(InvokeOperation.EMPTY));
     }
 
index 9772a55d2df5f43779b3801c6107e5ccbfc48460..ed853d559d8539c0e10d2f780454aeadcbcd3c47 100644 (file)
@@ -15,7 +15,7 @@ import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 
-public abstract sealed class ChildBody extends AbstractBody permits JsonChildBody, XmlChildBody {
+public abstract sealed class ChildBody extends RequestBody permits JsonChildBody, XmlChildBody {
     public record PrefixAndBody(@NonNull ImmutableList<PathArgument> prefix, @NonNull NormalizedNode body) {
         public PrefixAndBody {
             requireNonNull(prefix);
index 48096f1311cbd0db5626e6febee88ac1f2a41291..2db07c9550c8993ffcb698df3bd3e1da17dd5a8c 100644 (file)
@@ -15,7 +15,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
  * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.4">RFC8040 section 4.4</a>.
  */
 @NonNullByDefault
-public abstract sealed class DataPostBody extends AbstractBody permits JsonDataPostBody, XmlDataPostBody {
+public abstract sealed class DataPostBody extends RequestBody permits JsonDataPostBody, XmlDataPostBody {
     DataPostBody(final InputStream inputStream) {
         super(inputStream);
     }
index 35a4540275c30e34dd628b3bc4b7526839730d95..a78f842053259ecea304e22beb27ef2cb0a9f0c6 100644 (file)
@@ -12,7 +12,6 @@ import static java.util.Objects.requireNonNull;
 import java.time.Instant;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
-import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 
 /**
  * Result of a {@code POST} request as defined in
@@ -47,7 +46,7 @@ public sealed interface DataPostResult {
      *
      * @param output Non-empty operation output, or {@code null}
      */
-    record InvokeOperation(@Nullable NormalizedNodePayload output) implements DataPostResult {
-        public static final InvokeOperation EMPTY = new InvokeOperation(null);
+    record InvokeOperation(@Nullable OperationOutputBody output) implements DataPostResult {
+        public static final @NonNull InvokeOperation EMPTY = new InvokeOperation(null);
     }
 }
index de171abd60db0b93d3bd032f71640203e49dec7e..391b1a3eee43ddc47faa69a291e73a7aa22db1ac 100644 (file)
@@ -22,7 +22,7 @@ import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
 /**
  * Access to an {code rpc}'s or an {@code action}'s input.
  */
-public abstract sealed class OperationInputBody extends AbstractBody
+public abstract sealed class OperationInputBody extends RequestBody
         permits JsonOperationInputBody, XmlOperationInputBody {
     OperationInputBody(final InputStream inputStream) {
         super(inputStream);
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationOutputBody.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationOutputBody.java
new file mode 100644 (file)
index 0000000..25754f2
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * 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.restconf.server.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.io.OutputStream;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.restconf.nb.rfc8040.jersey.providers.ParameterAwareNormalizedNodeWriter;
+import org.opendaylight.restconf.server.api.DatabindPath.OperationPath;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
+
+/**
+ * A {@link ReplyBody} corresponding to a {@code rpc} or {@code action} invocation.
+ */
+@NonNullByDefault
+public final class OperationOutputBody extends ReplyBody {
+    private final OperationPath path;
+    private final ContainerNode output;
+
+    public OperationOutputBody(final OperationPath path, final ContainerNode output, final boolean prettyPrint) {
+        super(prettyPrint);
+        this.path = requireNonNull(path);
+        this.output = requireNonNull(output);
+        if (output.isEmpty()) {
+            throw new IllegalArgumentException("output may not be empty");
+        }
+    }
+
+    @VisibleForTesting
+    public ContainerNode output() {
+        return output;
+    }
+
+    @Override
+    void writeJSON(final OutputStream out, final boolean prettyPrint) throws IOException {
+        final var stack = prepareStack();
+
+        // RpcDefinition/ActionDefinition is not supported as initial codec in JSONStreamWriter, so we need to emit
+        // initial output declaration
+        try (var jsonWriter = createJsonWriter(out, prettyPrint)) {
+            final var module = stack.currentModule();
+            jsonWriter.beginObject().name(module.argument().getLocalName() + ":output").beginObject();
+
+            final var nnWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+                JSONNormalizedNodeStreamWriter.createNestedWriter(path.databind().jsonCodecs(), stack.toInference(),
+                    module.namespace().argument(), jsonWriter), null, null);
+            for (var child : output.body()) {
+                nnWriter.write(child);
+            }
+            nnWriter.flush();
+
+            jsonWriter.endObject().endObject();
+        }
+    }
+
+    @Override
+    void writeXML(final OutputStream out, final boolean prettyPrint) throws IOException {
+        final var stack = prepareStack();
+
+        // RpcDefinition/ActionDefinition is not supported as initial codec in XMLStreamWriter, so we need to emit
+        // initial output declaration.
+        final var xmlWriter = createXmlWriter(out, prettyPrint);
+        final var nnWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
+            XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, stack.toInference()), null, null);
+
+        writeElements(xmlWriter, nnWriter, output);
+        nnWriter.flush();
+    }
+
+    private SchemaInferenceStack prepareStack() {
+        final var stack = path.inference().toSchemaInferenceStack();
+        stack.enterSchemaTree(path.outputStatement().argument());
+        return stack;
+    }
+}
index b7466b676e675de17b547bc8a010025ac117c688..d6b8c89d7313a2e738e8735d085ed906ee7f407d 100644 (file)
@@ -26,7 +26,7 @@ import org.opendaylight.yangtools.yang.common.ErrorType;
 /**
  * A YANG Patch body.
  */
-public abstract sealed class PatchBody extends AbstractBody permits JsonPatchBody, XmlPatchBody {
+public abstract sealed class PatchBody extends RequestBody permits JsonPatchBody, XmlPatchBody {
     /**
      * Resource context needed to completely resolve a {@link PatchBody}.
      */
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ReplyBody.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ReplyBody.java
new file mode 100644 (file)
index 0000000..44c74db
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * 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.restconf.server.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import javanet.staxutils.IndentingXMLStreamWriter;
+import javax.xml.XMLConstants;
+import javax.xml.stream.FactoryConfigurationError;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.restconf.nb.rfc8040.jersey.providers.api.RestconfNormalizedNodeWriter;
+import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
+
+/**
+ * A response counterpart to {@link RequestBody}. It is inherently immutable and exposes methods to write the content
+ * to an {@link OutputStream}.
+ */
+@NonNullByDefault
+public abstract class ReplyBody implements Immutable {
+    private static final XMLOutputFactory XML_FACTORY = XMLOutputFactory.newFactory();
+    private static final String PRETTY_PRINT_INDENT = "  ";
+
+    private final boolean prettyPrint;
+
+    ReplyBody(final boolean prettyPrint) {
+        this.prettyPrint = prettyPrint;
+    }
+
+    /**
+     * Write the content of this body as a JSON document.
+     *
+     * @param out output stream
+     * @throws IOException if an IO error occurs.
+     */
+    public final void writeJSON(final OutputStream out) throws IOException {
+        writeJSON(requireNonNull(out), prettyPrint);
+    }
+
+    abstract void writeJSON(OutputStream out, boolean prettyPrint) throws IOException;
+
+    /**
+     * Write the content of this body as an XML document.
+     *
+     * @param out output stream
+     * @throws IOException if an IO error occurs.
+     */
+    public final void writeXML(final OutputStream out) throws IOException {
+        writeXML(requireNonNull(out), prettyPrint);
+    }
+
+    abstract void writeXML(OutputStream out, boolean prettyPrint) throws IOException;
+
+    @Override
+    public final String toString() {
+        return addToStringAttributes(MoreObjects.toStringHelper(this)).toString();
+    }
+
+    ToStringHelper addToStringAttributes(final ToStringHelper helper) {
+        return helper.add("prettyPrint", prettyPrint);
+    }
+
+    static final JsonWriter createJsonWriter(final OutputStream out, final boolean prettyPrint) {
+        final var ret = JsonWriterFactory.createJsonWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
+        ret.setIndent(prettyPrint ? PRETTY_PRINT_INDENT : "");
+        return ret;
+    }
+
+    static final XMLStreamWriter createXmlWriter(final OutputStream out, final boolean prettyPrint) throws IOException {
+        final var xmlWriter = createXmlWriter(out);
+        return prettyPrint ? new IndentingXMLStreamWriter(xmlWriter) : xmlWriter;
+    }
+
+    private static XMLStreamWriter createXmlWriter(final OutputStream out) throws IOException {
+        try {
+            return XML_FACTORY.createXMLStreamWriter(out, StandardCharsets.UTF_8.name());
+        } catch (XMLStreamException | FactoryConfigurationError e) {
+            throw new IOException(e);
+        }
+    }
+
+    static final void writeElements(final XMLStreamWriter xmlWriter, final RestconfNormalizedNodeWriter nnWriter,
+            final ContainerNode data) throws IOException {
+        final QName nodeType = data.name().getNodeType();
+        final String namespace = nodeType.getNamespace().toString();
+        try {
+            xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, nodeType.getLocalName(), namespace);
+            xmlWriter.writeDefaultNamespace(namespace);
+            for (var child : data.body()) {
+                nnWriter.write(child);
+            }
+            nnWriter.flush();
+            xmlWriter.writeEndElement();
+            xmlWriter.flush();
+        } catch (final XMLStreamException e) {
+            throw new IOException("Failed to write elements", e);
+        }
+    }
+}
similarity index 86%
rename from restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/AbstractBody.java
rename to restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/RequestBody.java
index 9922ec3aea406165a8b896f4a47b1d3e7f91478d..fd38649aa0dd6859c3cff091af27223e6ea400c7 100644 (file)
@@ -18,23 +18,25 @@ import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfError;
+import org.opendaylight.yangtools.concepts.Mutable;
 import org.opendaylight.yangtools.yang.data.api.YangNetconfError;
 import org.opendaylight.yangtools.yang.data.api.YangNetconfErrorAware;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * An abstract request body backed by an {@link InputStream}.
+ * An abstract request body backed by an {@link InputStream}. In controls the access to input stream, so that it can
+ * only be taken once.
  */
-public abstract sealed class AbstractBody implements AutoCloseable
+public abstract sealed class RequestBody implements AutoCloseable, Mutable
         permits ChildBody, DataPostBody, OperationInputBody, PatchBody, ResourceBody {
-    private static final Logger LOG = LoggerFactory.getLogger(AbstractBody.class);
+    private static final Logger LOG = LoggerFactory.getLogger(RequestBody.class);
 
     private static final VarHandle INPUT_STREAM;
 
     static {
         try {
-            INPUT_STREAM = MethodHandles.lookup().findVarHandle(AbstractBody.class, "inputStream", InputStream.class);
+            INPUT_STREAM = MethodHandles.lookup().findVarHandle(RequestBody.class, "inputStream", InputStream.class);
         } catch (NoSuchFieldException | IllegalAccessException e) {
             throw new ExceptionInInitializerError(e);
         }
@@ -44,7 +46,7 @@ public abstract sealed class AbstractBody implements AutoCloseable
     @SuppressFBWarnings(value = "URF_UNREAD_FIELD", justification = "https://github.com/spotbugs/spotbugs/issues/2749")
     private volatile InputStream inputStream;
 
-    AbstractBody(final InputStream inputStream) {
+    RequestBody(final InputStream inputStream) {
         this.inputStream = requireNonNull(inputStream);
     }
 
@@ -68,7 +70,6 @@ public abstract sealed class AbstractBody implements AutoCloseable
         return is;
     }
 
-
     /**
      * Throw a {@link RestconfDocumentedException} if the specified exception has a {@link YangNetconfError} attachment.
      *
index bc9937999c8904e2599467b69c7dedd4a1f8d41b..68634b2d8711b7724df423134f019860aa12b365 100644 (file)
@@ -28,7 +28,7 @@ import org.slf4j.LoggerFactory;
  * The body of a resource identified in the request URL, i.e. a {@code PUT} or a plain {@code PATCH} request on RESTCONF
  * data service.
  */
-public abstract sealed class ResourceBody extends AbstractBody permits JsonResourceBody, XmlResourceBody {
+public abstract sealed class ResourceBody extends RequestBody permits JsonResourceBody, XmlResourceBody {
     private static final Logger LOG = LoggerFactory.getLogger(ResourceBody.class);
     private static final NodeIdentifier DATA_NID = NodeIdentifier.create(Data.QNAME);
 
index f5eff3718990270e08546a525f884459afcef7ac..7ffeb191905630cc98391b72dd51a32d9c64ca72 100644 (file)
@@ -46,8 +46,10 @@ import org.opendaylight.restconf.nb.rfc8040.AbstractJukeboxTest;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.JsonNormalizedNodeBodyWriter;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.XmlNormalizedNodeBodyWriter;
 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
+import org.opendaylight.restconf.server.api.OperationOutputBody;
 import org.opendaylight.restconf.server.mdsal.MdsalDatabindProvider;
 import org.opendaylight.restconf.server.mdsal.MdsalRestconfServer;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 
@@ -86,10 +88,30 @@ abstract class AbstractRestconfTest extends AbstractJukeboxTest {
             MediaTypes.APPLICATION_YANG_DATA_JSON);
     }
 
+    static final void assertJson(final String expectedJson, final OperationOutputBody payload) {
+        final var baos = new ByteArrayOutputStream();
+        try {
+            payload.writeJSON(baos);
+        } catch (IOException e) {
+            throw new AssertionError(e);
+        }
+        assertEquals(expectedJson, baos.toString(StandardCharsets.UTF_8));
+    }
+
     static final void assertXml(final String expectedXml, final NormalizedNodePayload payload) {
         assertPayload(expectedXml, payload, new XmlNormalizedNodeBodyWriter(), MediaTypes.APPLICATION_YANG_DATA_XML);
     }
 
+    static final void assertXml(final String expectedXml, final OperationOutputBody payload) {
+        final var baos = new ByteArrayOutputStream();
+        try {
+            payload.writeXML(baos);
+        } catch (IOException e) {
+            throw new AssertionError(e);
+        }
+        assertEquals(expectedXml, baos.toString(StandardCharsets.UTF_8));
+    }
+
     private static void assertPayload(final String expected, final NormalizedNodePayload payload,
             final MessageBodyWriter<NormalizedNodePayload> writer, final String mediaType) {
         final var baos = new ByteArrayOutputStream();
@@ -101,6 +123,15 @@ abstract class AbstractRestconfTest extends AbstractJukeboxTest {
         assertEquals(expected, baos.toString(StandardCharsets.UTF_8));
     }
 
+    static final ContainerNode assertOperationOutput(final int status, final Consumer<AsyncResponse> invocation) {
+        return assertOperationOutputBody(status, invocation).output();
+    }
+
+    static final OperationOutputBody assertOperationOutputBody(final int status,
+            final Consumer<AsyncResponse> invocation) {
+        return assertEntity(OperationOutputBody.class, status, invocation);
+    }
+
     static final NormalizedNode assertNormalizedNode(final int status, final Consumer<AsyncResponse> invocation) {
         return assertNormalizedNodePayload(status, invocation).data();
     }
index 672b6e19f530105d7dbe1f2631864142124cfd77..fd0f2222fb2d6b27093f55ab1eb4433a126abcbf 100644 (file)
@@ -33,7 +33,7 @@ import org.opendaylight.mdsal.dom.spi.FixedDOMSchemaService;
 import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
 import org.opendaylight.restconf.api.ApiPath;
 import org.opendaylight.restconf.nb.rfc8040.AbstractInstanceIdentifierTest;
-import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
+import org.opendaylight.restconf.server.api.OperationOutputBody;
 import org.opendaylight.restconf.server.mdsal.MdsalDatabindProvider;
 import org.opendaylight.restconf.server.mdsal.MdsalRestconfServer;
 import org.opendaylight.yangtools.yang.common.QName;
@@ -108,7 +108,7 @@ class Netconf799Test extends AbstractInstanceIdentifierTest {
         final var response = captor.getValue();
         assertEquals(200, response.getStatus());
 
-        final var payload = assertInstanceOf(NormalizedNodePayload.class, response.getEntity());
+        final var payload = assertInstanceOf(OperationOutputBody.class, response.getEntity());
         AbstractRestconfTest.assertJson("""
             {"instance-identifier-module:output":{"timestamp":"somevalue"}}""", payload);
         AbstractRestconfTest.assertXml("""
index f95542e0740cf89fd433d40bbf32832f370ff68a..346bc49f0d889166117594ab7f811d5b8a35f7e8 100644 (file)
@@ -77,7 +77,7 @@ class RestconfOperationsPostTest extends AbstractRestconfTest {
         doReturn(false).when(result).isEmpty();
 
         prepNNC(result);
-        assertSame(result, assertNormalizedNode(200, ar -> restconf.operationsXmlPOST(
+        assertSame(result, assertOperationOutput(200, ar -> restconf.operationsXmlPOST(
             apiPath("invoke-rpc-module:rpc-test"), stringInputStream("""
                 <input xmlns="invoke:rpc:module"/>"""), uriInfo, ar)));
     }
@@ -101,7 +101,7 @@ class RestconfOperationsPostTest extends AbstractRestconfTest {
         doReturn(Futures.immediateFuture(new DefaultDOMRpcResult(OUTPUT, List.of()))).when(rpcService)
             .invokeRpc(RPC, INPUT);
 
-        assertEquals(OUTPUT, assertNormalizedNode(200, ar -> restconf.operationsXmlPOST(
+        assertEquals(OUTPUT, assertOperationOutput(200, ar -> restconf.operationsXmlPOST(
             apiPath("invoke-rpc-module:rpc-test"), stringInputStream("""
                 <input xmlns="invoke:rpc:module">
                   <cont>
@@ -145,7 +145,7 @@ class RestconfOperationsPostTest extends AbstractRestconfTest {
         doReturn(Futures.immediateFuture(new DefaultDOMRpcResult(OUTPUT, List.of()))).when(rpcService)
             .invokeRpc(RPC, INPUT);
 
-        assertEquals(OUTPUT, assertNormalizedNode(200,
+        assertEquals(OUTPUT, assertOperationOutput(200,
             ar -> restconf.operationsJsonPOST(
                 apiPath("ietf-yang-library:modules-state/yang-ext:mount/invoke-rpc-module:rpc-test"),
                 stringInputStream("""
@@ -188,7 +188,7 @@ class RestconfOperationsPostTest extends AbstractRestconfTest {
         doReturn(Futures.immediateFuture(new DefaultDOMRpcResult(OUTPUT, List.of())))
             .when(rpcService).invokeRpc(RPC, INPUT);
 
-        final var payload = assertNormalizedNodePayload(200, ar -> restconf.operationsJsonPOST(
+        final var body = assertOperationOutputBody(200, ar -> restconf.operationsJsonPOST(
             apiPath("invoke-rpc-module:rpc-test"),
             stringInputStream("""
                 {
@@ -198,12 +198,11 @@ class RestconfOperationsPostTest extends AbstractRestconfTest {
                     }
                   }
                 }"""), uriInfo, ar));
-        assertEquals(OUTPUT, payload.data());
+        assertEquals(OUTPUT, body.output());
         assertJson("""
-            {"invoke-rpc-module:output":{"cont-out":{"lf-out":"operation result"}}}""", payload);
+            {"invoke-rpc-module:output":{"cont-out":{"lf-out":"operation result"}}}""", body);
         assertXml("""
-            <output xmlns="invoke:rpc:module"><cont-out><lf-out>operation result</lf-out></cont-out></output>""",
-            payload);
+            <output xmlns="invoke:rpc:module"><cont-out><lf-out>operation result</lf-out></cont-out></output>""", body);
     }
 
     private void prepNNC(final ContainerNode result) {