From a2746edcfec11feaf5dcd0096909b8cdc7ba94ed Mon Sep 17 00:00:00 2001 From: Robert Varga Date: Fri, 5 Apr 2024 17:56:49 +0200 Subject: [PATCH] Refactor pretty printing 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 --- .../restconf/api/query/PrettyPrintParam.java | 5 +- ...Writer.java => FormattableBodyWriter.java} | 12 ++-- .../restconf/nb/jaxrs/JaxRsRestconf.java | 14 ++--- ...dyWriter.java => JsonFormattableBody.java} | 8 +-- ...odyWriter.java => XmlFormattableBody.java} | 8 +-- .../restconf/nb/rfc8040/Insert.java | 6 +- .../restconf/nb/rfc8040/JaxRsNorthbound.java | 6 +- .../rfc8040/databind/jaxrs/QueryParams.java | 2 +- .../JsonNormalizedNodeBodyWriter.java | 17 +----- .../XmlNormalizedNodeBodyWriter.java | 28 +--------- .../nb/rfc8040/legacy/QueryParameters.java | 15 +++-- .../rests/transactions/RestconfStrategy.java | 51 ++++++++++++----- .../restconf/server/api/DataGetParams.java | 7 ++- .../restconf/server/api/DataPostResult.java | 12 +--- .../server/api/EventStreamGetParams.java | 17 +++--- .../restconf/server/api/FormatParameters.java | 26 +++++++++ .../{ReplyBody.java => FormattableBody.java} | 31 +++++----- .../restconf/server/api/InvokeParams.java | 56 +++++++++++++++++++ .../restconf/server/api/InvokeResult.java | 25 +++++++++ .../server/api/OperationOutputBody.java | 22 +++++--- .../server/api/OperationsPostResult.java | 32 ----------- .../restconf/server/api/RestconfServer.java | 8 ++- .../server/mdsal/MdsalRestconfServer.java | 8 +-- .../SubscribeDeviceNotificationRpc.java | 12 ++-- .../CreateDataChangeEventSubscriptionRpc.java | 12 ++-- .../notif/CreateNotificationStreamRpc.java | 11 ++-- .../server/spi/RpcImplementation.java | 3 +- .../nb/jaxrs/AbstractRestconfTest.java | 20 ++++--- .../nb/jaxrs/RestconfOperationsPostTest.java | 7 +++ .../dtcl/CreateNotificationStreamRpcTest.java | 2 +- 30 files changed, 276 insertions(+), 207 deletions(-) rename restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/{ReplyBodyWriter.java => FormattableBodyWriter.java} (70%) rename restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/{JsonReplyBodyWriter.java => JsonFormattableBody.java} (72%) rename restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/{XmlReplyBodyWriter.java => XmlFormattableBody.java} (73%) create mode 100644 restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/FormatParameters.java rename restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/{ReplyBody.java => FormattableBody.java} (75%) create mode 100644 restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/InvokeParams.java create mode 100644 restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/InvokeResult.java delete mode 100644 restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationsPostResult.java diff --git a/protocol/restconf-api/src/main/java/org/opendaylight/restconf/api/query/PrettyPrintParam.java b/protocol/restconf-api/src/main/java/org/opendaylight/restconf/api/query/PrettyPrintParam.java index a2cdead7e7..6a70d37883 100644 --- a/protocol/restconf-api/src/main/java/org/opendaylight/restconf/api/query/PrettyPrintParam.java +++ b/protocol/restconf-api/src/main/java/org/opendaylight/restconf/api/query/PrettyPrintParam.java @@ -19,10 +19,11 @@ public final class PrettyPrintParam implements RestconfQueryParam - permits JsonReplyBodyWriter, XmlReplyBodyWriter { +abstract sealed class FormattableBodyWriter implements MessageBodyWriter + 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 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; } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JaxRsRestconf.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JaxRsRestconf.java index 7ad3f20b39..9009e092d7 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JaxRsRestconf.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JaxRsRestconf.java @@ -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(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(); } }); } 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/JsonFormattableBody.java 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 751ef05768..b1c5d61559 100644 --- 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/JsonFormattableBody.java @@ -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); } } 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/XmlFormattableBody.java 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 15b8d3a97e..c276bfddb7 100644 --- 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/XmlFormattableBody.java @@ -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); } } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/Insert.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/Insert.java index 3959a74867..96f813bc2d 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/Insert.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/Insert.java @@ -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); diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java index 13b62fb085..36feabf539 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java @@ -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 getSingletons() { return Set.of( - new JsonReplyBodyWriter(), new XmlReplyBodyWriter(), + new JsonFormattableBody(), new XmlFormattableBody(), new RestconfDocumentedExceptionMapper(databindProvider), new JaxRsRestconf(server)); } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/jaxrs/QueryParams.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/jaxrs/QueryParams.java index af27ce9e2e..1b9c83871e 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/jaxrs/QueryParams.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/jaxrs/QueryParams.java @@ -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(); diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyWriter.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyWriter.java index b4c58e1211..92be29921d 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyWriter.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyWriter.java @@ -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); - } } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyWriter.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyWriter.java index 93afdb798d..7f0c518316 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyWriter.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyWriter.java @@ -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( diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/QueryParameters.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/QueryParameters.java index 65cc2e0d5d..bc6a6ab81e 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/QueryParameters.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/QueryParameters.java @@ -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> fields) { - public static final @NonNull QueryParameters EMPTY = new QueryParameters(null, null, null); + @NonNull PrettyPrintParam prettyPrint, + @Nullable List> 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> fields) { diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java index 3972d8ead8..68814d857a 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java @@ -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 operationsPOST(final URI restconfURI, final ApiPath apiPath, - final OperationInputBody body) { + public @NonNull RestconfFuture operationsPOST(final URI restconfURI, final ApiPath apiPath, + final Map 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(); + final var ret = new SettableRestconfFuture(); Futures.addCallback(rpcService.invokeRpc(type, data), new FutureCallback() { @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 resolveSource(final SourceIdentifier source, final Class 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 dataInvokePOST(final @NonNull Action path, - final @NonNull OperationInputBody body) { + private @NonNull RestconfFuture dataInvokePOST(final @NonNull Action path, + final @NonNull OperationInputBody body, final Map 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))); } /** diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetParams.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetParams.java index fc375ab626..023717ee4d 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetParams.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetParams.java @@ -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); } } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataPostResult.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataPostResult.java index a78f842053..349c5b20a4 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataPostResult.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataPostResult.java @@ -17,7 +17,7 @@ import org.eclipse.jdt.annotation.Nullable; * Result of a {@code POST} request as defined in * RFC8040 section 4.4. */ -public sealed interface DataPostResult { +public sealed interface DataPostResult permits DataPostResult.CreateResource, InvokeResult { /** * Result of a {@code POST} request in as defined in * RFC8040 Create Resource Mode. @@ -39,14 +39,4 @@ public sealed interface DataPostResult { this(createdPath, null, null); } } - - /** - * Result of a {@code POST} request as defined in - * RFC8040 Invoke Operation Mode. - * - * @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); - } } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/EventStreamGetParams.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/EventStreamGetParams.java index 79f1ac3ad1..3004bacf2f 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/EventStreamGetParams.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/EventStreamGetParams.java @@ -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 @Nullable T optionalParam(final Function factory, final String name, + public static @NonNull T mandatoryParam(final Function 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 index 0000000000..aff9341ad9 --- /dev/null +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/FormatParameters.java @@ -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(); +} 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/FormattableBody.java 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 44c74db24a..fbc9ba18df 100644 --- 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/FormattableBody.java @@ -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 index 0000000000..e157b28e44 --- /dev/null +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/InvokeParams.java @@ -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 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 index 0000000000..88f7d72640 --- /dev/null +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/InvokeResult.java @@ -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 + * RFC8040 Operation Resource and + * RFC8040 Invoke Operation Mode. + * + * @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 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 index 25754f293d..42dee85fe4 100644 --- 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 @@ -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 index 6794c9415b..0000000000 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationsPostResult.java +++ /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 - * RFC8040 Operation Resource and - * RFC8040 Invoke Operation Mode. - * - * @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 diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/RestconfServer.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/RestconfServer.java index 864eebfb17..3686d13acc 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/RestconfServer.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/RestconfServer.java @@ -121,7 +121,7 @@ public interface RestconfServer { RestconfFuture dataPUT(ApiPath identifier, ResourceBody body, Map 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 ietf-restconf.yang * {@code container operations} statement. * @@ -146,12 +146,14 @@ public interface RestconfServer { * * @param restconfURI Base URI of the request * @param operation {@code } 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 operationsPOST(URI restconfURI, ApiPath operation, OperationInputBody body); + RestconfFuture operationsPOST(URI restconfURI, ApiPath operation, Map queryParameters, + OperationInputBody body); /** * Return the revision of {@code ietf-yang-library} module implemented by this server, as defined in diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/MdsalRestconfServer.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/MdsalRestconfServer.java index 43ed84c7af..c2768bbb70 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/MdsalRestconfServer.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/MdsalRestconfServer.java @@ -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 operationsPOST(final URI restconfURI, final ApiPath apiPath, - final OperationInputBody body) { + public RestconfFuture operationsPOST(final URI restconfURI, final ApiPath apiPath, + final Map 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 diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/devnotif/SubscribeDeviceNotificationRpc.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/devnotif/SubscribeDeviceNotificationRpc.java index 7238b0b900..75529b3f60 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/devnotif/SubscribeDeviceNotificationRpc.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/devnotif/SubscribeDeviceNotificationRpc.java @@ -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 invoke(final URI restconfURI, final OperationInput input) { + public RestconfFuture 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()); } } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/dtcl/CreateDataChangeEventSubscriptionRpc.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/dtcl/CreateDataChangeEventSubscriptionRpc.java index be3f6396b5..e12d7867e4 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/dtcl/CreateDataChangeEventSubscriptionRpc.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/dtcl/CreateDataChangeEventSubscriptionRpc.java @@ -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 * */ @Override - public RestconfFuture invoke(final URI restconfURI, final OperationInput input) { + public RestconfFuture 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()); } } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/notif/CreateNotificationStreamRpc.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/notif/CreateNotificationStreamRpc.java index da45ff5fec..24f6d3162c 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/notif/CreateNotificationStreamRpc.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/notif/CreateNotificationStreamRpc.java @@ -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 invoke(final URI restconfURI, final OperationInput input) { + public RestconfFuture invoke(final URI restconfURI, final OperationInput input) { final var body = input.input(); final var qnames = ((LeafSetNode) 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()); } } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/RpcImplementation.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/RpcImplementation.java index af810b4512..f7dd89bd9c 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/RpcImplementation.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/RpcImplementation.java @@ -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 invoke(URI restconfURI, OperationInput input); + public abstract RestconfFuture invoke(URI restconfURI, OperationInput input); @Override public final String toString() { diff --git a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/AbstractRestconfTest.java b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/AbstractRestconfTest.java index 7ffeb19190..e640e3a807 100644 --- a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/AbstractRestconfTest.java +++ b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/AbstractRestconfTest.java @@ -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 assertErrors(final Consumer 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 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; diff --git a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfOperationsPostTest.java b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfOperationsPostTest.java index 346bc49f0d..30ba800edf 100644 --- a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfOperationsPostTest.java +++ b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfOperationsPostTest.java @@ -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); diff --git a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/mdsal/streams/dtcl/CreateNotificationStreamRpcTest.java b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/mdsal/streams/dtcl/CreateNotificationStreamRpcTest.java index 4bfac3b84c..37e6cc43c6 100644 --- a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/mdsal/streams/dtcl/CreateNotificationStreamRpcTest.java +++ b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/mdsal/streams/dtcl/CreateNotificationStreamRpcTest.java @@ -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()); -- 2.36.6