Refactor pretty printing 89/111289/1
authorRobert Varga <robert.varga@pantheon.tech>
Fri, 5 Apr 2024 15:56:49 +0000 (17:56 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Fri, 5 Apr 2024 18:49:51 +0000 (20:49 +0200)
PrettyPrintParam is a common operation, introduce FormatParameters
to expose it. This interface will be useful for controlling other
parameters as well.

This really makes ReplyBody a FormattableBody, so we perform this
renaming as well and make FormatParameters a required argument.

This flushes out the fact we are not accepting this parameter for invoke
operations. Correctly propagate these parameters, which forces us to
clean up the RPC/action invocation paths -- further simplifying the
interface and creating proper place where these parameters get applied.

JIRA: NETCONF-773
Change-Id: I3382b27c0b4f7d727d792ead5c1ad364bd69624c
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
30 files changed:
protocol/restconf-api/src/main/java/org/opendaylight/restconf/api/query/PrettyPrintParam.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/FormattableBodyWriter.java [moved from restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/ReplyBodyWriter.java with 70% similarity]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JaxRsRestconf.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JsonFormattableBody.java [moved from restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JsonReplyBodyWriter.java with 72% similarity]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/XmlFormattableBody.java [moved from restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/XmlReplyBodyWriter.java with 73% similarity]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/Insert.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/jaxrs/QueryParams.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/XmlNormalizedNodeBodyWriter.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/QueryParameters.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/DataGetParams.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/EventStreamGetParams.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/FormatParameters.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/FormattableBody.java [moved from restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ReplyBody.java with 75% similarity]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/InvokeParams.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/InvokeResult.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationOutputBody.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationsPostResult.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/RestconfServer.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/MdsalRestconfServer.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/devnotif/SubscribeDeviceNotificationRpc.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/dtcl/CreateDataChangeEventSubscriptionRpc.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/notif/CreateNotificationStreamRpc.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/RpcImplementation.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/RestconfOperationsPostTest.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/mdsal/streams/dtcl/CreateNotificationStreamRpcTest.java

index a2cdead7e7ad7c958e0d8f420eed033d16ceaab5..6a70d3788327a41ecac1a21fe82a9ff45c38c389 100644 (file)
@@ -19,10 +19,11 @@ public final class PrettyPrintParam implements RestconfQueryParam<PrettyPrintPar
     @SuppressWarnings("checkstyle:ConstantName")
     public static final String uriName = "odl-pretty-print";
 
+    public static final @NonNull PrettyPrintParam FALSE = new PrettyPrintParam(false);
+    public static final @NonNull PrettyPrintParam TRUE = new PrettyPrintParam(true);
+
     private static final @NonNull URI CAPABILITY =
         URI.create("urn:opendaylight:params:restconf:capability:pretty-print:1.0");
-    private static final @NonNull PrettyPrintParam FALSE = new PrettyPrintParam(false);
-    private static final @NonNull PrettyPrintParam TRUE = new PrettyPrintParam(true);
 
     private final boolean value;
 
similarity index 70%
rename from restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/ReplyBodyWriter.java
rename to restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/FormattableBodyWriter.java
index ad2827102b9a565c482ce6b1015d534d76be0646..559a1d055a110e957d997059dd558440f8e56314 100644 (file)
@@ -17,22 +17,22 @@ 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;
+import org.opendaylight.restconf.server.api.FormattableBody;
 
-abstract sealed class ReplyBodyWriter implements MessageBodyWriter<ReplyBody>
-        permits JsonReplyBodyWriter, XmlReplyBodyWriter {
+abstract sealed class FormattableBodyWriter implements MessageBodyWriter<FormattableBody>
+        permits JsonFormattableBody, XmlFormattableBody {
     @Override
     public final boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations,
             final MediaType mediaType) {
-        return ReplyBody.class.isAssignableFrom(type);
+        return FormattableBody.class.isAssignableFrom(type);
     }
 
     @Override
-    public final void writeTo(final ReplyBody body, final Class<?> type, final Type genericType,
+    public final void writeTo(final FormattableBody 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;
+    abstract void writeTo(@NonNull FormattableBody body, @NonNull OutputStream out) throws IOException;
 }
index 7ad3f20b393daee1aefcb44eee65e51f2aa8600b..9009e092d721a93c8ff09e8e20808b98bd88436e 100644 (file)
@@ -59,9 +59,9 @@ import org.opendaylight.restconf.server.api.DataGetResult;
 import org.opendaylight.restconf.server.api.DataPatchResult;
 import org.opendaylight.restconf.server.api.DataPostResult;
 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
-import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
 import org.opendaylight.restconf.server.api.DataPutResult;
 import org.opendaylight.restconf.server.api.DataYangPatchResult;
+import org.opendaylight.restconf.server.api.InvokeResult;
 import org.opendaylight.restconf.server.api.JsonChildBody;
 import org.opendaylight.restconf.server.api.JsonDataPostBody;
 import org.opendaylight.restconf.server.api.JsonOperationInputBody;
@@ -69,9 +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;
 import org.opendaylight.restconf.server.api.XmlChildBody;
 import org.opendaylight.restconf.server.api.XmlDataPostBody;
@@ -520,7 +518,7 @@ public final class JaxRsRestconf implements ParamConverterProvider {
                     fillConfigurationMetadata(builder, createResource);
                     return builder.build();
                 }
-                if (result instanceof InvokeOperation invokeOperation) {
+                if (result instanceof InvokeResult invokeOperation) {
                     final var output = invokeOperation.output();
                     return output == null ? Response.status(Status.NO_CONTENT).build()
                         : Response.status(Status.OK).entity(output).build();
@@ -756,13 +754,13 @@ public final class JaxRsRestconf implements ParamConverterProvider {
 
     private void operationsPOST(final ApiPath identifier, final UriInfo uriInfo, final AsyncResponse ar,
             final OperationInputBody body) {
-        server.operationsPOST(uriInfo.getBaseUri(), identifier, body)
-            .addCallback(new JaxRsRestconfCallback<OperationsPostResult>(ar) {
+        server.operationsPOST(uriInfo.getBaseUri(), identifier, QueryParams.normalize(uriInfo), body)
+            .addCallback(new JaxRsRestconfCallback<>(ar) {
                 @Override
-                Response transform(final OperationsPostResult result) {
+                Response transform(final InvokeResult result) {
                     final var body = result.output();
                     return body == null ? Response.noContent().build()
-                        : Response.ok().entity(new OperationOutputBody(result.path(), body, false)).build();
+                        : Response.ok().entity(body).build();
                 }
             });
     }
similarity index 72%
rename from restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JsonReplyBodyWriter.java
rename to restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JsonFormattableBody.java
index 751ef05768bf2bedc40bb30ff2bd524ee81237cc..b1c5d6155987ce481a8dd09aedeabd3a0904c61b 100644 (file)
@@ -13,13 +13,13 @@ 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;
+import org.opendaylight.restconf.server.api.FormattableBody;
 
 @Provider
 @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
-public final class JsonReplyBodyWriter extends ReplyBodyWriter {
+public final class JsonFormattableBody extends FormattableBodyWriter {
     @Override
-    void writeTo(final ReplyBody body, final OutputStream out) throws IOException {
-        body.writeJSON(out);
+    void writeTo(final FormattableBody body, final OutputStream out) throws IOException {
+        body.formatToJSON(out);
     }
 }
similarity index 73%
rename from restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/XmlReplyBodyWriter.java
rename to restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/XmlFormattableBody.java
index 15b8d3a97e70c094129340dfdfc020e78608c5bf..c276bfddb7312735b081890701621336ccfcf17c 100644 (file)
@@ -13,13 +13,13 @@ 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;
+import org.opendaylight.restconf.server.api.FormattableBody;
 
 @Provider
 @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
-public final class XmlReplyBodyWriter extends ReplyBodyWriter {
+public final class XmlFormattableBody extends FormattableBodyWriter {
     @Override
-    void writeTo(final ReplyBody body, final OutputStream out) throws IOException {
-        body.writeXML(out);
+    void writeTo(final FormattableBody body, final OutputStream out) throws IOException {
+        body.formatToXML(out);
     }
 }
index 3959a74867c1c071b64763bdde83a4cbffa630c0..96f813bc2dec0e3f09896e5e4726429ffa58664b 100644 (file)
@@ -8,7 +8,7 @@
 package org.opendaylight.restconf.nb.rfc8040;
 
 import static java.util.Objects.requireNonNull;
-import static org.opendaylight.restconf.server.api.EventStreamGetParams.optionalParam;
+import static org.opendaylight.restconf.server.api.EventStreamGetParams.mandatoryParam;
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.MoreObjects;
@@ -69,10 +69,10 @@ public final class Insert implements Immutable {
 
             switch (paramName) {
                 case InsertParam.uriName:
-                    insert = optionalParam(InsertParam::forUriValue, paramName, paramValue);
+                    insert = mandatoryParam(InsertParam::forUriValue, paramName, paramValue);
                     break;
                 case PointParam.uriName:
-                    point = optionalParam(PointParam::forUriValue, paramName, paramValue);
+                    point = mandatoryParam(PointParam::forUriValue, paramName, paramValue);
                     break;
                 default:
                     throw new IllegalArgumentException("Invalid parameter: " + paramName);
index 13b62fb0855dabe2aa544e68389bf45780642d9c..36feabf53991560e3c9cdd84bb59d32986326649 100644 (file)
@@ -21,8 +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.jaxrs.JsonFormattableBody;
+import org.opendaylight.restconf.nb.jaxrs.XmlFormattableBody;
 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;
@@ -70,7 +70,7 @@ public final class JaxRsNorthbound implements AutoCloseable {
                         @Override
                         public Set<Object> getSingletons() {
                             return Set.of(
-                                new JsonReplyBodyWriter(), new XmlReplyBodyWriter(),
+                                new JsonFormattableBody(), new XmlFormattableBody(),
                                 new RestconfDocumentedExceptionMapper(databindProvider),
                                 new JaxRsRestconf(server));
                         }
index af27ce9e2ebf8aa3ae12ac6323475dbd5d45a885..1b9c83871ed2c2d85cb9168012f32445baf262cd 100644 (file)
@@ -93,7 +93,7 @@ public final class QueryParams {
         DepthParam depth = null;
         FieldsParam fields = null;
         WithDefaultsParam withDefaults = null;
-        PrettyPrintParam prettyPrint = null;
+        PrettyPrintParam prettyPrint = PrettyPrintParam.FALSE;
 
         for (var entry : uriInfo.getQueryParameters().entrySet()) {
             final var paramName = entry.getKey();
index b4c58e1211b44b667066f23a69828f993ca384ec..92be29921d02781f6893a534b7afe6d5e01eb623 100644 (file)
@@ -10,23 +10,20 @@ package org.opendaylight.restconf.nb.rfc8040.jersey.providers;
 import com.google.gson.stream.JsonWriter;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.nio.charset.StandardCharsets;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.ext.Provider;
 import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.restconf.api.MediaTypes;
-import org.opendaylight.restconf.api.query.PrettyPrintParam;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.api.RestconfNormalizedNodeWriter;
 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
+import org.opendaylight.restconf.server.api.FormattableBody;
 import org.opendaylight.yangtools.yang.common.XMLNamespace;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 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;
 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
-import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
@@ -34,8 +31,6 @@ import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference
 @Provider
 @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
 public final class JsonNormalizedNodeBodyWriter extends AbstractNormalizedNodeBodyWriter {
-    private static final int DEFAULT_INDENT_SPACES_NUM = 2;
-
     @Override
     void writeData(final SchemaInferenceStack stack, final QueryParameters writerParameters, final NormalizedNode data,
             final OutputStream entityStream) throws IOException {
@@ -51,7 +46,7 @@ public final class JsonNormalizedNodeBodyWriter extends AbstractNormalizedNodeBo
                 .build()
                 : data;
 
-        try (var jsonWriter = createJsonWriter(entityStream, writerParameters.prettyPrint())) {
+        try (var jsonWriter = FormattableBody.createJsonWriter(entityStream, writerParameters)) {
             jsonWriter.beginObject();
 
             final var nnWriter = createNormalizedNodeWriter(stack.toInference(), jsonWriter, writerParameters, null);
@@ -71,12 +66,4 @@ public final class JsonNormalizedNodeBodyWriter extends AbstractNormalizedNodeBo
             JSONNormalizedNodeStreamWriter.createNestedWriter(codecs, inference,
                 initialNamespace, jsonWriter), writerParameters.depth(), writerParameters.fields());
     }
-
-    private static JsonWriter createJsonWriter(final OutputStream entityStream,
-            final @Nullable PrettyPrintParam prettyPrint) {
-        final var outputWriter = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8);
-        return prettyPrint != null && prettyPrint.value()
-            ? JsonWriterFactory.createJsonWriter(outputWriter, DEFAULT_INDENT_SPACES_NUM)
-                : JsonWriterFactory.createJsonWriter(outputWriter);
-    }
 }
index 93afdb798da6cf65bfd0a04367cb301555f12a24..7f0c5183164c53a3a0100fad78c6e036f606af86 100644 (file)
@@ -9,21 +9,16 @@ package org.opendaylight.restconf.nb.rfc8040.jersey.providers;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
-import javanet.staxutils.IndentingXMLStreamWriter;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.ext.Provider;
 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.Nullable;
 import org.opendaylight.restconf.api.MediaTypes;
-import org.opendaylight.restconf.api.query.PrettyPrintParam;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.api.RestconfNormalizedNodeWriter;
 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
+import org.opendaylight.restconf.server.api.FormattableBody;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
@@ -37,13 +32,6 @@ import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference
 @Provider
 @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
 public final class XmlNormalizedNodeBodyWriter extends AbstractNormalizedNodeBodyWriter {
-    private static final XMLOutputFactory XML_FACTORY;
-
-    static {
-        XML_FACTORY = XMLOutputFactory.newFactory();
-        XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
-    }
-
     @Override
     void writeData(final SchemaInferenceStack stack, final QueryParameters writerParameters, final NormalizedNode data,
             final OutputStream entityStream) throws IOException {
@@ -55,7 +43,7 @@ public final class XmlNormalizedNodeBodyWriter extends AbstractNormalizedNodeBod
             isRoot = true;
         }
 
-        final var xmlWriter = createXmlWriter(entityStream, writerParameters.prettyPrint());
+        final var xmlWriter = FormattableBody.createXmlWriter(entityStream, writerParameters);
         final var nnWriter = createNormalizedNodeWriter(xmlWriter, stack.toInference(), writerParameters);
         if (data instanceof MapEntryNode mapEntry) {
             // Restconf allows returning one list item. We need to wrap it
@@ -76,18 +64,6 @@ public final class XmlNormalizedNodeBodyWriter extends AbstractNormalizedNodeBod
         nnWriter.flush();
     }
 
-    private static XMLStreamWriter createXmlWriter(final OutputStream entityStream,
-            final @Nullable PrettyPrintParam prettyPrint) throws IOException {
-        final XMLStreamWriter xmlWriter;
-        try {
-            xmlWriter = XML_FACTORY.createXMLStreamWriter(entityStream, StandardCharsets.UTF_8.name());
-        } catch (XMLStreamException | FactoryConfigurationError e) {
-            throw new IOException(e);
-        }
-
-        return prettyPrint != null && prettyPrint.value() ? new IndentingXMLStreamWriter(xmlWriter) : xmlWriter;
-    }
-
     private static RestconfNormalizedNodeWriter createNormalizedNodeWriter(final XMLStreamWriter xmlWriter,
             final Inference inference, final QueryParameters writerParameters) {
         return ParameterAwareNormalizedNodeWriter.forStreamWriter(
index 65cc2e0d5daed4e27c700a85d9b7884b3ab2b34f..bc6a6ab81e8e525acee15ad1521ae61b625fdd0d 100644 (file)
@@ -7,6 +7,8 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.legacy;
 
+import static java.util.Objects.requireNonNull;
+
 import com.google.common.annotations.Beta;
 import java.util.List;
 import java.util.Set;
@@ -15,6 +17,7 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.restconf.api.query.DepthParam;
 import org.opendaylight.restconf.api.query.PrettyPrintParam;
 import org.opendaylight.restconf.server.api.DataGetParams;
+import org.opendaylight.restconf.server.api.FormatParameters;
 import org.opendaylight.yangtools.yang.common.QName;
 
 /**
@@ -26,14 +29,18 @@ import org.opendaylight.yangtools.yang.common.QName;
 // FIXME: this probably needs to be renamed back to WriterParams, or somesuch
 public record QueryParameters(
         @Nullable DepthParam depth,
-        @Nullable PrettyPrintParam prettyPrint,
-        @Nullable List<Set<QName>> fields) {
-    public static final @NonNull QueryParameters EMPTY = new QueryParameters(null, null, null);
+        @NonNull PrettyPrintParam prettyPrint,
+        @Nullable List<Set<QName>> fields) implements FormatParameters {
+    public static final @NonNull QueryParameters EMPTY = new QueryParameters(null, PrettyPrintParam.FALSE, null);
+
+    public QueryParameters {
+        requireNonNull(prettyPrint);
+    }
 
     public static @NonNull QueryParameters of(final DataGetParams params) {
         final var depth = params.depth();
         final var prettyPrint = params.prettyPrint();
-        return depth == null && prettyPrint == null ? EMPTY : new QueryParameters(depth, prettyPrint, null);
+        return depth == null && !prettyPrint.value() ? EMPTY : new QueryParameters(depth, prettyPrint, null);
     }
 
     public static @NonNull QueryParameters of(final DataGetParams params, final List<Set<QName>> fields) {
index 3972d8ead8f58148c0187b22d5fc170b3de51690..68814d857ac84deb9c50181925c1c88564e69bda 100644 (file)
@@ -76,7 +76,6 @@ import org.opendaylight.restconf.server.api.DataPatchResult;
 import org.opendaylight.restconf.server.api.DataPostBody;
 import org.opendaylight.restconf.server.api.DataPostResult;
 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
-import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
 import org.opendaylight.restconf.server.api.DataPutResult;
 import org.opendaylight.restconf.server.api.DataYangPatchResult;
 import org.opendaylight.restconf.server.api.DatabindContext;
@@ -84,11 +83,13 @@ import org.opendaylight.restconf.server.api.DatabindPath;
 import org.opendaylight.restconf.server.api.DatabindPath.Action;
 import org.opendaylight.restconf.server.api.DatabindPath.Data;
 import org.opendaylight.restconf.server.api.DatabindPath.InstanceReference;
+import org.opendaylight.restconf.server.api.DatabindPath.OperationPath;
 import org.opendaylight.restconf.server.api.DatabindPath.Rpc;
+import org.opendaylight.restconf.server.api.InvokeParams;
+import org.opendaylight.restconf.server.api.InvokeResult;
 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;
 import org.opendaylight.restconf.server.api.ResourceBody;
 import org.opendaylight.restconf.server.spi.ApiPathCanonizer;
@@ -1285,8 +1286,8 @@ public abstract class RestconfStrategy {
         return RestconfFuture.of(new OperationsGetResult.Leaf(rpc.inference().modelContext(), rpc.rpc().argument()));
     }
 
-    public @NonNull RestconfFuture<OperationsPostResult> operationsPOST(final URI restconfURI, final ApiPath apiPath,
-            final OperationInputBody body) {
+    public @NonNull RestconfFuture<InvokeResult> operationsPOST(final URI restconfURI, final ApiPath apiPath,
+            final Map<String, String> queryParameters, final OperationInputBody body) {
         final Rpc path;
         try {
             path = pathNormalizer.normalizeRpcPath(apiPath);
@@ -1294,6 +1295,14 @@ public abstract class RestconfStrategy {
             return RestconfFuture.failed(e);
         }
 
+        final InvokeParams params;
+        try {
+            params = InvokeParams.ofQueryParameters(queryParameters);
+        } catch (IllegalArgumentException e) {
+            return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
+                ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
+        }
+
         final ContainerNode data;
         try {
             data = body.toContainerNode(path);
@@ -1306,7 +1315,8 @@ public abstract class RestconfStrategy {
         final var type = path.rpc().argument();
         final var local = localRpcs.get(type);
         if (local != null) {
-            return local.invoke(restconfURI, new OperationInput(path, data));
+            return local.invoke(restconfURI, new OperationInput(path, data))
+                .transform(output -> outputToInvokeResult(path, params, output));
         }
         if (rpcService == null) {
             LOG.debug("RPC invocation is not available");
@@ -1314,13 +1324,13 @@ public abstract class RestconfStrategy {
                 ErrorType.PROTOCOL, ErrorTag.OPERATION_NOT_SUPPORTED));
         }
 
-        final var ret = new SettableRestconfFuture<OperationsPostResult>();
+        final var ret = new SettableRestconfFuture<InvokeResult>();
         Futures.addCallback(rpcService.invokeRpc(type, data), new FutureCallback<DOMRpcResult>() {
             @Override
             public void onSuccess(final DOMRpcResult response) {
                 final var errors = response.errors();
                 if (errors.isEmpty()) {
-                    ret.set(new OperationsPostResult(path, response.value()));
+                    ret.set(outputToInvokeResult(path, params, response.value()));
                 } else {
                     LOG.debug("RPC invocation reported {}", response.errors());
                     ret.setFailure(new RestconfDocumentedException("RPC implementation reported errors", null,
@@ -1343,6 +1353,12 @@ public abstract class RestconfStrategy {
         return ret;
     }
 
+    private static @NonNull InvokeResult outputToInvokeResult(final @NonNull OperationPath path,
+            final @NonNull InvokeParams params, final @Nullable ContainerNode value) {
+        return value == null || value.isEmpty() ? InvokeResult.EMPTY
+            : new InvokeResult(new OperationOutputBody(params, path, value));
+    }
+
     public @NonNull RestconfFuture<CharSource> resolveSource(final SourceIdentifier source,
             final Class<? extends SourceRepresentation> representation) {
         final var src = requireNonNull(source);
@@ -1419,7 +1435,7 @@ public abstract class RestconfStrategy {
         }
         if (path instanceof Action actionPath) {
             try (var inputBody = body.toOperationInput()) {
-                return dataInvokePOST(actionPath, inputBody);
+                return dataInvokePOST(actionPath, inputBody, queryParameters);
             }
         }
         // Note: this should never happen
@@ -1454,8 +1470,16 @@ public abstract class RestconfStrategy {
         return ret;
     }
 
-    private @NonNull RestconfFuture<InvokeOperation> dataInvokePOST(final @NonNull Action path,
-            final @NonNull OperationInputBody body) {
+    private @NonNull RestconfFuture<InvokeResult> dataInvokePOST(final @NonNull Action path,
+            final @NonNull OperationInputBody body, final Map<String, String> queryParameters) {
+        final InvokeParams params;
+        try {
+            params = InvokeParams.ofQueryParameters(queryParameters);
+        } catch (IllegalArgumentException e) {
+            return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
+                ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
+        }
+
         final ContainerNode input;
         try {
             input = body.toContainerNode(path);
@@ -1469,11 +1493,8 @@ public abstract class RestconfStrategy {
             return RestconfFuture.failed(new RestconfDocumentedException("DOMActionService is missing."));
         }
 
-        final var future = dataInvokePOST(actionService, path, input);
-        return future.transform(result -> result.getOutput()
-            .flatMap(output -> output.isEmpty() ? Optional.empty()
-                : Optional.of(new InvokeOperation(new OperationOutputBody(path, output, false))))
-            .orElse(InvokeOperation.EMPTY));
+        return dataInvokePOST(actionService, path, input)
+            .transform(result -> outputToInvokeResult(path, params, result.getOutput().orElse(null)));
     }
 
     /**
index fc375ab62681aef6db1f3ec395af766cb0b558c2..023717ee4d514ed83adcca5fdeab07f50ba24fac 100644 (file)
@@ -16,7 +16,6 @@ import org.opendaylight.restconf.api.query.DepthParam;
 import org.opendaylight.restconf.api.query.FieldsParam;
 import org.opendaylight.restconf.api.query.PrettyPrintParam;
 import org.opendaylight.restconf.api.query.WithDefaultsParam;
-import org.opendaylight.yangtools.concepts.Immutable;
 
 /**
  * Supported query parameters of {@code /data} {@code GET} HTTP operation, as defined in
@@ -27,10 +26,12 @@ public record DataGetParams(
         @Nullable DepthParam depth,
         @Nullable FieldsParam fields,
         @Nullable WithDefaultsParam withDefaults,
-        @Nullable PrettyPrintParam prettyPrint) implements Immutable {
-    public static final @NonNull DataGetParams EMPTY = new DataGetParams(ContentParam.ALL, null, null, null, null);
+        @NonNull PrettyPrintParam prettyPrint) implements FormatParameters {
+    public static final @NonNull DataGetParams EMPTY =
+        new DataGetParams(ContentParam.ALL, null, null, null, PrettyPrintParam.FALSE);
 
     public DataGetParams {
         requireNonNull(content);
+        requireNonNull(prettyPrint);
     }
 }
index a78f842053259ecea304e22beb27ef2cb0a9f0c6..349c5b20a451527c191fab6ae2c81a2c2204451c 100644 (file)
@@ -17,7 +17,7 @@ import org.eclipse.jdt.annotation.Nullable;
  * Result of a {@code POST} request as defined in
  * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.4">RFC8040 section 4.4</a>.
  */
-public sealed interface DataPostResult {
+public sealed interface DataPostResult permits DataPostResult.CreateResource, InvokeResult {
     /**
      * Result of a {@code POST} request in as defined in
      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.4.1">RFC8040 Create Resource Mode</a>.
@@ -39,14 +39,4 @@ public sealed interface DataPostResult {
             this(createdPath, null, null);
         }
     }
-
-    /**
-     * Result of a {@code POST} request as defined in
-     * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.4.2">RFC8040 Invoke Operation Mode</a>.
-     *
-     * @param output Non-empty operation output, or {@code null}
-     */
-    record InvokeOperation(@Nullable OperationOutputBody output) implements DataPostResult {
-        public static final @NonNull InvokeOperation EMPTY = new InvokeOperation(null);
-    }
 }
index 79f1ac3ad1facd0a8b3c30aca961c542eeeaa5ef..3004bacf2f31e377cd06d9edc7af768473be6bd1 100644 (file)
@@ -14,7 +14,6 @@ import com.google.common.base.MoreObjects;
 import java.util.Map;
 import java.util.function.Function;
 import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.restconf.api.query.ChangedLeafNodesOnlyParam;
 import org.opendaylight.restconf.api.query.ChildNodesOnlyParam;
 import org.opendaylight.restconf.api.query.FilterParam;
@@ -75,27 +74,27 @@ public record EventStreamGetParams(
 
             switch (paramName) {
                 case FilterParam.uriName:
-                    filter = optionalParam(FilterParam::forUriValue, paramName, paramValue);
+                    filter = mandatoryParam(FilterParam::forUriValue, paramName, paramValue);
                     break;
                 case StartTimeParam.uriName:
-                    startTime = optionalParam(StartTimeParam::forUriValue, paramName, paramValue);
+                    startTime = mandatoryParam(StartTimeParam::forUriValue, paramName, paramValue);
                     break;
                 case StopTimeParam.uriName:
-                    stopTime = optionalParam(StopTimeParam::forUriValue, paramName, paramValue);
+                    stopTime = mandatoryParam(StopTimeParam::forUriValue, paramName, paramValue);
                     break;
                 case LeafNodesOnlyParam.uriName:
-                    leafNodesOnly = optionalParam(LeafNodesOnlyParam::forUriValue, paramName, paramValue);
+                    leafNodesOnly = mandatoryParam(LeafNodesOnlyParam::forUriValue, paramName, paramValue);
                     break;
                 case SkipNotificationDataParam.uriName:
-                    skipNotificationData = optionalParam(SkipNotificationDataParam::forUriValue, paramName,
+                    skipNotificationData = mandatoryParam(SkipNotificationDataParam::forUriValue, paramName,
                         paramValue);
                     break;
                 case ChangedLeafNodesOnlyParam.uriName:
-                    changedLeafNodesOnly = optionalParam(ChangedLeafNodesOnlyParam::forUriValue, paramName,
+                    changedLeafNodesOnly = mandatoryParam(ChangedLeafNodesOnlyParam::forUriValue, paramName,
                         paramValue);
                     break;
                 case ChildNodesOnlyParam.uriName:
-                    childNodesOnly = optionalParam(ChildNodesOnlyParam::forUriValue, paramName, paramValue);
+                    childNodesOnly = mandatoryParam(ChildNodesOnlyParam::forUriValue, paramName, paramValue);
                     break;
                 default:
                     throw new IllegalArgumentException("Invalid parameter: " + paramName);
@@ -134,7 +133,7 @@ public record EventStreamGetParams(
     }
 
     // FIXME: find a better place for this method
-    public static <T> @Nullable T optionalParam(final Function<String, @NonNull T> factory, final String name,
+    public static <T> @NonNull T mandatoryParam(final Function<String, @NonNull T> factory, final String name,
             final String value) {
         try {
             return factory.apply(requireNonNull(value));
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/FormatParameters.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/FormatParameters.java
new file mode 100644 (file)
index 0000000..aff9341
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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 org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.restconf.api.query.PrettyPrintParam;
+import org.opendaylight.restconf.api.query.RestconfQueryParam;
+import org.opendaylight.yangtools.concepts.Immutable;
+
+/**
+ * The set of {@link RestconfQueryParam}s governing output formatting.
+ */
+@NonNullByDefault
+public interface FormatParameters extends Immutable {
+    /**
+     * Return the {@link PrettyPrintParam} parameter.
+     *
+     * @return the {@link PrettyPrintParam} parameter
+     */
+    PrettyPrintParam prettyPrint();
+}
similarity index 75%
rename from restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ReplyBody.java
rename to restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/FormattableBody.java
index 44c74db24a59763893088e60fedc7890c01684a0..fbc9ba18df5bd80c70fb796e145600b71288290f 100644 (file)
@@ -34,14 +34,14 @@ import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
  * to an {@link OutputStream}.
  */
 @NonNullByDefault
-public abstract class ReplyBody implements Immutable {
+public abstract class FormattableBody implements Immutable {
     private static final XMLOutputFactory XML_FACTORY = XMLOutputFactory.newFactory();
     private static final String PRETTY_PRINT_INDENT = "  ";
 
-    private final boolean prettyPrint;
+    private final FormatParameters format;
 
-    ReplyBody(final boolean prettyPrint) {
-        this.prettyPrint = prettyPrint;
+    FormattableBody(final FormatParameters format) {
+        this.format = requireNonNull(format);
     }
 
     /**
@@ -50,11 +50,11 @@ public abstract class ReplyBody implements Immutable {
      * @param out output stream
      * @throws IOException if an IO error occurs.
      */
-    public final void writeJSON(final OutputStream out) throws IOException {
-        writeJSON(requireNonNull(out), prettyPrint);
+    public final void formatToJSON(final OutputStream out) throws IOException {
+        formatToJSON(requireNonNull(out), format);
     }
 
-    abstract void writeJSON(OutputStream out, boolean prettyPrint) throws IOException;
+    abstract void formatToJSON(OutputStream out, FormatParameters format) throws IOException;
 
     /**
      * Write the content of this body as an XML document.
@@ -62,11 +62,11 @@ public abstract class ReplyBody implements Immutable {
      * @param out output stream
      * @throws IOException if an IO error occurs.
      */
-    public final void writeXML(final OutputStream out) throws IOException {
-        writeXML(requireNonNull(out), prettyPrint);
+    public final void formatToXML(final OutputStream out) throws IOException {
+        formatToXML(requireNonNull(out), format);
     }
 
-    abstract void writeXML(OutputStream out, boolean prettyPrint) throws IOException;
+    abstract void formatToXML(OutputStream out, FormatParameters format) throws IOException;
 
     @Override
     public final String toString() {
@@ -74,18 +74,19 @@ public abstract class ReplyBody implements Immutable {
     }
 
     ToStringHelper addToStringAttributes(final ToStringHelper helper) {
-        return helper.add("prettyPrint", prettyPrint);
+        return helper.add("prettyPrint", format.prettyPrint().value());
     }
 
-    static final JsonWriter createJsonWriter(final OutputStream out, final boolean prettyPrint) {
+    public static final JsonWriter createJsonWriter(final OutputStream out, final FormatParameters format) {
         final var ret = JsonWriterFactory.createJsonWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
-        ret.setIndent(prettyPrint ? PRETTY_PRINT_INDENT : "");
+        ret.setIndent(format.prettyPrint().value() ? PRETTY_PRINT_INDENT : "");
         return ret;
     }
 
-    static final XMLStreamWriter createXmlWriter(final OutputStream out, final boolean prettyPrint) throws IOException {
+    public static final XMLStreamWriter createXmlWriter(final OutputStream out, final FormatParameters format)
+            throws IOException {
         final var xmlWriter = createXmlWriter(out);
-        return prettyPrint ? new IndentingXMLStreamWriter(xmlWriter) : xmlWriter;
+        return format.prettyPrint().value() ? new IndentingXMLStreamWriter(xmlWriter) : xmlWriter;
     }
 
     private static XMLStreamWriter createXmlWriter(final OutputStream out) throws IOException {
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/InvokeParams.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/InvokeParams.java
new file mode 100644 (file)
index 0000000..e157b28
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021 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 static org.opendaylight.restconf.server.api.EventStreamGetParams.mandatoryParam;
+
+import java.util.Map;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.restconf.api.query.PrettyPrintParam;
+
+/**
+ * Supported query parameters of {@code POST} HTTP operation when the request is to invoke a YANG operation, be it
+ * {@code rpc} or {@code action}. There is no such thing in RFC8040, but we support pretty-printing of the resulting
+ * {@code output} container.
+ */
+public record InvokeParams(@NonNull PrettyPrintParam prettyPrint) implements FormatParameters {
+    public static final @NonNull InvokeParams EMPTY = new InvokeParams(PrettyPrintParam.FALSE);
+
+    public InvokeParams {
+        requireNonNull(prettyPrint);
+    }
+
+    /**
+     * Return {@link InvokeParams} for specified query parameters.
+     *
+     * @param queryParameters Parameters and their values
+     * @return A {@link InvokeParams}
+     * @throws NullPointerException if {@code queryParameters} is {@code null}
+     * @throws IllegalArgumentException if the parameters are invalid
+     */
+    public static @NonNull InvokeParams ofQueryParameters(final Map<String, String> queryParameters) {
+        if (queryParameters.isEmpty()) {
+            return EMPTY;
+        }
+
+        PrettyPrintParam prettyPrint = PrettyPrintParam.FALSE;
+
+        for (var entry : queryParameters.entrySet()) {
+            final var paramName = entry.getKey();
+            final var paramValue = entry.getValue();
+
+            prettyPrint = switch (paramName) {
+                case PrettyPrintParam.uriName -> mandatoryParam(PrettyPrintParam::forUriValue, paramName, paramValue);
+                default -> throw new IllegalArgumentException("Invalid parameter: " + paramName);
+            };
+        }
+
+        return new InvokeParams(requireNonNull(prettyPrint));
+    }
+}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/InvokeResult.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/InvokeResult.java
new file mode 100644 (file)
index 0000000..88f7d72
--- /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.server.api;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Result of a {@code POST} request resulting in an operation invocation, as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.6">RFC8040 Operation Resource</a> and
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.4.2">RFC8040 Invoke Operation Mode</a>.
+ *
+ * @param output Non-empty operation output, or {@code null}
+ */
+public record InvokeResult(@Nullable OperationOutputBody output) implements DataPostResult {
+    /**
+     * Empty instance. Prefer this to creating one with {@code output}.
+     */
+    public static final @NonNull InvokeResult EMPTY = new InvokeResult(null);
+}
\ No newline at end of file
index 25754f293dc3dd816cea1a468727a48b8c7efd88..42dee85fe49bf3f021bb5e16dffaf6daf5a9ef67 100644 (file)
@@ -10,6 +10,7 @@ package org.opendaylight.restconf.server.api;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects.ToStringHelper;
 import java.io.IOException;
 import java.io.OutputStream;
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -21,15 +22,15 @@ import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStr
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
 
 /**
- * A {@link ReplyBody} corresponding to a {@code rpc} or {@code action} invocation.
+ * A {@link FormattableBody} corresponding to a {@code rpc} or {@code action} invocation.
  */
 @NonNullByDefault
-public final class OperationOutputBody extends ReplyBody {
+public final class OperationOutputBody extends FormattableBody {
     private final OperationPath path;
     private final ContainerNode output;
 
-    public OperationOutputBody(final OperationPath path, final ContainerNode output, final boolean prettyPrint) {
-        super(prettyPrint);
+    public OperationOutputBody(final FormatParameters format, final OperationPath path, final ContainerNode output) {
+        super(format);
         this.path = requireNonNull(path);
         this.output = requireNonNull(output);
         if (output.isEmpty()) {
@@ -43,12 +44,12 @@ public final class OperationOutputBody extends ReplyBody {
     }
 
     @Override
-    void writeJSON(final OutputStream out, final boolean prettyPrint) throws IOException {
+    void formatToJSON(final OutputStream out, final FormatParameters format) 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)) {
+        try (var jsonWriter = createJsonWriter(out, format)) {
             final var module = stack.currentModule();
             jsonWriter.beginObject().name(module.argument().getLocalName() + ":output").beginObject();
 
@@ -65,12 +66,12 @@ public final class OperationOutputBody extends ReplyBody {
     }
 
     @Override
-    void writeXML(final OutputStream out, final boolean prettyPrint) throws IOException {
+    void formatToXML(final OutputStream out, final FormatParameters format) 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 xmlWriter = createXmlWriter(out, format);
         final var nnWriter = ParameterAwareNormalizedNodeWriter.forStreamWriter(
             XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, stack.toInference()), null, null);
 
@@ -78,6 +79,11 @@ public final class OperationOutputBody extends ReplyBody {
         nnWriter.flush();
     }
 
+    @Override
+    ToStringHelper addToStringAttributes(final ToStringHelper helper) {
+        return super.addToStringAttributes(helper.add("path", path).add("output", output.prettyTree()));
+    }
+
     private SchemaInferenceStack prepareStack() {
         final var stack = path.inference().toSchemaInferenceStack();
         stack.enterSchemaTree(path.outputStatement().argument());
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationsPostResult.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationsPostResult.java
deleted file mode 100644 (file)
index 6794c94..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (c) 2023 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 org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-import org.opendaylight.restconf.server.api.DatabindPath.OperationPath;
-import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
-
-/**
- * RESTCONF {@code /operations} content for a {@code POST} operation as per
- * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.6">RFC8040 Operation Resource</a> and
- * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.4.2">RFC8040 Invoke Operation Mode</a>.
- *
- * @param path associated {@link OperationPath}
- * @param output Operation output, or {@code null} if output would be empty
- */
-public record OperationsPostResult(@NonNull OperationPath path, @Nullable ContainerNode output) {
-    public OperationsPostResult {
-        requireNonNull(path);
-        if (output != null && output.isEmpty()) {
-            output = null;
-        }
-    }
-}
\ No newline at end of file
index 864eebfb17d5699063259a152b5263195b5068a8..3686d13acc586bbd11641cec0cf48724a782d49d 100644 (file)
@@ -121,7 +121,7 @@ public interface RestconfServer {
     RestconfFuture<DataPutResult> dataPUT(ApiPath identifier, ResourceBody body, Map<String, String> queryParameters);
 
     /**
-     * Return the set of supported RPCs supported by {@link #operationsPOST(URI, ApiPath, OperationInputBody)},
+     * Return the set of supported RPCs supported by {@link #operationsPOST(URI, ApiPath, Map, OperationInputBody)},
      * as expressed in the <a href="https://www.rfc-editor.org/rfc/rfc8040#page-84">ietf-restconf.yang</a>
      * {@code container operations} statement.
      *
@@ -146,12 +146,14 @@ public interface RestconfServer {
      *
      * @param restconfURI Base URI of the request
      * @param operation {@code <operation>} path, really an {@link ApiPath} to an {@code rpc}
+     * @param queryParameters query parameters
      * @param body RPC operation
-     * @return A {@link RestconfFuture} completing with {@link OperationsPostResult}
+     * @return A {@link RestconfFuture} completing with {@link InvokeResult}
      */
     // FIXME: 'operation' should really be an ApiIdentifier with non-null module, but we also support yang-ext:mount,
     //        and hence it is a path right now
-    RestconfFuture<OperationsPostResult> operationsPOST(URI restconfURI, ApiPath operation, OperationInputBody body);
+    RestconfFuture<InvokeResult> operationsPOST(URI restconfURI, ApiPath operation, Map<String, String> queryParameters,
+        OperationInputBody body);
 
     /**
      * Return the revision of {@code ietf-yang-library} module implemented by this server, as defined in
index 43ed84c7afbc02920881b9af35cb92ece0dbdafb..c2768bbb70f87b8fcbfc79134b0540f2a3958e56 100644 (file)
@@ -46,10 +46,10 @@ import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
 import org.opendaylight.restconf.server.api.DataPutResult;
 import org.opendaylight.restconf.server.api.DataYangPatchResult;
 import org.opendaylight.restconf.server.api.DatabindContext;
+import org.opendaylight.restconf.server.api.InvokeResult;
 import org.opendaylight.restconf.server.api.ModulesGetResult;
 import org.opendaylight.restconf.server.api.OperationInputBody;
 import org.opendaylight.restconf.server.api.OperationsGetResult;
-import org.opendaylight.restconf.server.api.OperationsPostResult;
 import org.opendaylight.restconf.server.api.PatchBody;
 import org.opendaylight.restconf.server.api.ResourceBody;
 import org.opendaylight.restconf.server.api.RestconfServer;
@@ -337,8 +337,8 @@ public final class MdsalRestconfServer implements RestconfServer, AutoCloseable
     }
 
     @Override
-    public RestconfFuture<OperationsPostResult> operationsPOST(final URI restconfURI, final ApiPath apiPath,
-            final OperationInputBody body) {
+    public RestconfFuture<InvokeResult> operationsPOST(final URI restconfURI, final ApiPath apiPath,
+            final Map<String, String> queryParameters, final OperationInputBody body) {
         final StrategyAndTail strategyAndTail;
         try {
             strategyAndTail = localStrategy().resolveStrategy(apiPath);
@@ -346,7 +346,7 @@ public final class MdsalRestconfServer implements RestconfServer, AutoCloseable
             return RestconfFuture.failed(e);
         }
         final var strategy = strategyAndTail.strategy();
-        return strategy.operationsPOST(restconfURI, strategyAndTail.tail(), body);
+        return strategy.operationsPOST(restconfURI, strategyAndTail.tail(), queryParameters, body);
     }
 
     @Override
index 7238b0b900e759fcc5c080f0910f71a25bb3e416..75529b3f60bd22732857f5a5763d71488032f6a4 100644 (file)
@@ -15,7 +15,6 @@ import javax.inject.Singleton;
 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
-import org.opendaylight.restconf.server.api.OperationsPostResult;
 import org.opendaylight.restconf.server.spi.ApiPathCanonizer;
 import org.opendaylight.restconf.server.spi.OperationInput;
 import org.opendaylight.restconf.server.spi.RestconfStream;
@@ -29,6 +28,7 @@ import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
@@ -58,7 +58,7 @@ public final class SubscribeDeviceNotificationRpc extends RpcImplementation {
     }
 
     @Override
-    public RestconfFuture<OperationsPostResult> invoke(final URI restconfURI, final OperationInput input) {
+    public RestconfFuture<ContainerNode> invoke(final URI restconfURI, final OperationInput input) {
         final var body = input.input();
         final var pathLeaf = body.childByArg(DEVICE_NOTIFICATION_PATH_NODEID);
         if (pathLeaf == null) {
@@ -79,14 +79,12 @@ public final class SubscribeDeviceNotificationRpc extends RpcImplementation {
                 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE));
         }
 
-        final var operPath = input.path();
-
         return streamRegistry.createStream(restconfURI, new DeviceNotificationSource(mountPointService, path),
             "All YANG notifications occuring on mount point /"
-                + new ApiPathCanonizer(operPath.databind()).dataToApiPath(path).toString())
-            .transform(stream -> new OperationsPostResult(operPath, ImmutableNodes.newContainerBuilder()
+                + new ApiPathCanonizer(input.path().databind()).dataToApiPath(path).toString())
+            .transform(stream -> ImmutableNodes.newContainerBuilder()
                 .withNodeIdentifier(new NodeIdentifier(SubscribeDeviceNotificationOutput.QNAME))
                 .withChild(ImmutableNodes.leafNode(DEVICE_NOTIFICATION_STREAM_NAME_NODEID, stream.name()))
-                .build()));
+                .build());
     }
 }
index be3f6396b53356c1e9a52c61765b3bbae6351697..e12d7867e4b06cacbbf805de257dd22eab9dca6f 100644 (file)
@@ -18,7 +18,6 @@ import org.opendaylight.mdsal.dom.api.DOMDataBroker;
 import org.opendaylight.mdsal.dom.api.DOMDataBroker.DataTreeChangeExtension;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
-import org.opendaylight.restconf.server.api.OperationsPostResult;
 import org.opendaylight.restconf.server.spi.ApiPathCanonizer;
 import org.opendaylight.restconf.server.spi.DatabindProvider;
 import org.opendaylight.restconf.server.spi.OperationInput;
@@ -33,6 +32,7 @@ import org.opendaylight.yangtools.yang.common.ErrorType;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
@@ -96,7 +96,7 @@ public final class CreateDataChangeEventSubscriptionRpc extends RpcImplementatio
      *     </pre>
      */
     @Override
-    public RestconfFuture<OperationsPostResult> invoke(final URI restconfURI, final OperationInput input) {
+    public RestconfFuture<ContainerNode> invoke(final URI restconfURI, final OperationInput input) {
         final var body = input.input();
         final var datastoreName = leaf(body, DATASTORE_NODEID, String.class);
         final var datastore = datastoreName != null ? LogicalDatastoreType.valueOf(datastoreName)
@@ -108,15 +108,13 @@ public final class CreateDataChangeEventSubscriptionRpc extends RpcImplementatio
                 new RestconfDocumentedException("missing path", ErrorType.APPLICATION, ErrorTag.MISSING_ELEMENT));
         }
 
-        final var operPath = input.path();
-
         return streamRegistry.createStream(restconfURI,
             new DataTreeChangeSource(databindProvider, changeService, datastore, path),
             "Events occuring in " + datastore + " datastore under /"
-                + new ApiPathCanonizer(operPath.databind()).dataToApiPath(path).toString())
-            .transform(stream -> new OperationsPostResult(operPath, ImmutableNodes.newContainerBuilder()
+                + new ApiPathCanonizer(input.path().databind()).dataToApiPath(path).toString())
+            .transform(stream -> ImmutableNodes.newContainerBuilder()
                 .withNodeIdentifier(OUTPUT_NODEID)
                 .withChild(ImmutableNodes.leafNode(STREAM_NAME_NODEID, stream.name()))
-                .build()));
+                .build());
     }
 }
index da45ff5fec09b315ccd399f1faf5926e8b75be84..24f6d3162c14c38b88eddbb559213cee547ddfce 100644 (file)
@@ -16,7 +16,6 @@ import javax.inject.Singleton;
 import org.opendaylight.mdsal.dom.api.DOMNotificationService;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
-import org.opendaylight.restconf.server.api.OperationsPostResult;
 import org.opendaylight.restconf.server.spi.DatabindProvider;
 import org.opendaylight.restconf.server.spi.OperationInput;
 import org.opendaylight.restconf.server.spi.RestconfStream;
@@ -28,6 +27,7 @@ import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
 import org.opendaylight.yangtools.yang.common.QName;
 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.LeafSetEntryNode;
 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
@@ -65,7 +65,7 @@ public final class CreateNotificationStreamRpc extends RpcImplementation {
     }
 
     @Override
-    public RestconfFuture<OperationsPostResult> invoke(final URI restconfURI, final OperationInput input) {
+    public RestconfFuture<ContainerNode> invoke(final URI restconfURI, final OperationInput input) {
         final var body = input.input();
         final var qnames = ((LeafSetNode<String>) body.getChildByArg(NOTIFICATIONS)).body().stream()
             .map(LeafSetEntryNode::body)
@@ -73,8 +73,7 @@ public final class CreateNotificationStreamRpc extends RpcImplementation {
             .sorted()
             .collect(ImmutableSet.toImmutableSet());
 
-        final var operPath = input.path();
-        final var modelContext = operPath.databind().modelContext();
+        final var modelContext = input.path().databind().modelContext();
         final var description = new StringBuilder("YANG notifications matching any of {");
         var haveFirst = false;
         for (var qname : qnames) {
@@ -106,9 +105,9 @@ public final class CreateNotificationStreamRpc extends RpcImplementation {
 
         return streamRegistry.createStream(restconfURI,
             new NotificationSource(databindProvider, notificationService, qnames), description.toString())
-            .transform(stream -> new OperationsPostResult(operPath, ImmutableNodes.newContainerBuilder()
+            .transform(stream -> ImmutableNodes.newContainerBuilder()
                 .withNodeIdentifier(SAL_REMOTE_OUTPUT_NODEID)
                 .withChild(ImmutableNodes.leafNode(STREAM_NAME_NODEID, stream.name()))
-                .build()));
+                .build());
     }
 }
index af810b4512861700fe6ebc0cf8834dd77ae9bf0f..f7dd89bd9cafa16fd1129327a3e31bf2229d9798 100644 (file)
@@ -14,7 +14,6 @@ import java.net.URI;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
-import org.opendaylight.restconf.server.api.OperationsPostResult;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
@@ -48,7 +47,7 @@ public abstract class RpcImplementation {
      * @param input RPC input
      * @return Future RPC output
      */
-    public abstract RestconfFuture<OperationsPostResult> invoke(URI restconfURI, OperationInput input);
+    public abstract RestconfFuture<ContainerNode> invoke(URI restconfURI, OperationInput input);
 
     @Override
     public final String toString() {
index 7ffeb191905630cc98391b72dd51a32d9c64ca72..e640e3a807ef6862a491b33d38e1063920188eb2 100644 (file)
@@ -91,7 +91,7 @@ abstract class AbstractRestconfTest extends AbstractJukeboxTest {
     static final void assertJson(final String expectedJson, final OperationOutputBody payload) {
         final var baos = new ByteArrayOutputStream();
         try {
-            payload.writeJSON(baos);
+            payload.formatToJSON(baos);
         } catch (IOException e) {
             throw new AssertionError(e);
         }
@@ -105,7 +105,7 @@ abstract class AbstractRestconfTest extends AbstractJukeboxTest {
     static final void assertXml(final String expectedXml, final OperationOutputBody payload) {
         final var baos = new ByteArrayOutputStream();
         try {
-            payload.writeXML(baos);
+            payload.formatToXML(baos);
         } catch (IOException e) {
             throw new AssertionError(e);
         }
@@ -160,19 +160,23 @@ abstract class AbstractRestconfTest extends AbstractJukeboxTest {
 
     static final List<RestconfError> assertErrors(final Consumer<AsyncResponse> invocation) {
         final var ar = mock(AsyncResponse.class);
-        final var captor = ArgumentCaptor.forClass(RestconfDocumentedException.class);
-        doReturn(true).when(ar).resume(captor.capture());
+        doReturn(true).when(ar).resume(any(RestconfDocumentedException.class));
+
         invocation.accept(ar);
-        verify(ar).resume(any(RestconfDocumentedException.class));
+
+        final var captor = ArgumentCaptor.forClass(RestconfDocumentedException.class);
+        verify(ar).resume(captor.capture());
         return captor.getValue().getErrors();
     }
 
     static final Response assertResponse(final int expectedStatus, final Consumer<AsyncResponse> invocation) {
         final var ar = mock(AsyncResponse.class);
-        final var captor = ArgumentCaptor.forClass(Response.class);
-        doReturn(true).when(ar).resume(captor.capture());
+        doReturn(true).when(ar).resume(any(Response.class));
+
         invocation.accept(ar);
-        verify(ar).resume(any(Response.class));
+
+        final var captor = ArgumentCaptor.forClass(Response.class);
+        verify(ar).resume(captor.capture());
         final var response = captor.getValue();
         assertEquals(expectedStatus, response.getStatus());
         return response;
index 346bc49f0d889166117594ab7f811d5b8a35f7e8..30ba800edfb4a93b1a22006377a6a1c9c1f46a0b 100644 (file)
@@ -18,6 +18,8 @@ import static org.mockito.Mockito.mock;
 import com.google.common.util.concurrent.Futures;
 import java.util.List;
 import java.util.Optional;
+import javax.ws.rs.core.MultivaluedHashMap;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
@@ -71,6 +73,11 @@ class RestconfOperationsPostTest extends AbstractRestconfTest {
         return MODEL_CONTEXT;
     }
 
+    @BeforeEach
+    void setupUriInfo() {
+        doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
+    }
+
     @Test
     void testInvokeRpcWithNonEmptyOutput() {
         final var result = mock(ContainerNode.class);
index 4bfac3b84c6dc194cc5a24aee05ed7c2d2005c54..37e6cc43c6dfd7f720abc88d1961b36b0adb3bf9 100644 (file)
@@ -98,7 +98,7 @@ class CreateNotificationStreamRpcTest {
         doReturn(CommitInfo.emptyFluentFuture()).when(tx).commit();
 
         final var output = assertInstanceOf(ContainerNode.class, rpc.invoke(RESTCONF_URI, createInput("path", TOASTER))
-            .getOrThrow().output());
+            .getOrThrow());
 
         assertEquals(new NodeIdentifier(CreateDataChangeEventSubscriptionOutput.QNAME), output.name());
         assertEquals(1, output.size());