From: Robert Varga Date: Thu, 11 Apr 2024 19:18:07 +0000 (+0200) Subject: Eliminate NormalizedNodePayload X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=netconf.git;a=commitdiff_plain;h=4c54c44f4642490b2d0687f1ee2d6a7356e6b752 Eliminate NormalizedNodePayload NormalizedNodePayload is only used in RestconfStrategy.dataGET() and is one of the two last places where we use old JAX-RS serialization. This patch introduces {Data,Root}FormattableBody class to act as a combination of NormalizedFormattableBody and NormalizedNodePayload. This also refactors the interface towards RestconfNormalizedNodeWriter by introducing a factory which is under control of the RestconfStrategy which produces the result. JIRA: NETCONF-773 Change-Id: I867944ab260828fb19e3ba829d1188a215e0bd9f Signed-off-by: Robert Varga --- 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 d018f0f79b..8e8135ad4e 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 @@ -42,8 +42,10 @@ import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.ParamConverter; import javax.ws.rs.ext.ParamConverterProvider; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.restconf.api.ApiPath; +import org.opendaylight.restconf.api.FormattableBody; import org.opendaylight.restconf.api.HttpStatusCode; import org.opendaylight.restconf.api.MediaTypes; import org.opendaylight.restconf.api.QueryParameters; @@ -53,7 +55,6 @@ import org.opendaylight.restconf.common.errors.RestconfError; import org.opendaylight.restconf.common.errors.RestconfFuture; import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping; import org.opendaylight.restconf.nb.rfc8040.URLConstants; -import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload; import org.opendaylight.restconf.server.api.ConfigurationMetadata; import org.opendaylight.restconf.server.api.CreateResourceResult; import org.opendaylight.restconf.server.api.DataGetResult; @@ -187,7 +188,8 @@ public final class JaxRsRestconf implements ParamConverterProvider { MediaType.TEXT_XML }) public void dataGET(@Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) { - completeDataGET(server.dataGET(requestOf(uriInfo)), ar); + final var request = requestOf(uriInfo); + completeDataGET(server.dataGET(request), request.prettyPrint(), ar); } /** @@ -208,14 +210,19 @@ public final class JaxRsRestconf implements ParamConverterProvider { }) public void dataGET(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) { - completeDataGET(server.dataGET(requestOf(uriInfo), identifier), ar); + final var request = requestOf(uriInfo); + completeDataGET(server.dataGET(request, identifier), request.prettyPrint(), ar); } - private static void completeDataGET(final RestconfFuture future, final AsyncResponse ar) { + @NonNullByDefault + private static void completeDataGET(final RestconfFuture future, final PrettyPrintParam prettyPrint, + final AsyncResponse ar) { future.addCallback(new JaxRsRestconfCallback<>(ar) { @Override Response transform(final DataGetResult result) { - final var builder = Response.ok().entity(result.payload()).cacheControl(NO_CACHE); + final var builder = Response.ok() + .entity(new JaxRsFormattableBody(result.body(), prettyPrint)) + .cacheControl(NO_CACHE); fillConfigurationMetadata(builder, result); return builder.build(); } @@ -695,7 +702,7 @@ public final class JaxRsRestconf implements ParamConverterProvider { * @param identifier module name and rpc identifier string for the desired operation * @param body the body of the operation * @param uriInfo URI info - * @param ar {@link AsyncResponse} which needs to be completed with a {@link NormalizedNodePayload} output + * @param ar {@link AsyncResponse} which needs to be completed with a {@link FormattableBody} output */ @POST // FIXME: identifier is just a *single* QName @@ -725,7 +732,7 @@ public final class JaxRsRestconf implements ParamConverterProvider { * @param identifier module name and rpc identifier string for the desired operation * @param body the body of the operation * @param uriInfo URI info - * @param ar {@link AsyncResponse} which needs to be completed with a {@link NormalizedNodePayload} output + * @param ar {@link AsyncResponse} which needs to be completed with a {@link FormattableBody} output */ @POST // FIXME: identifier is just a *single* QName 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 2e78aba2d0..965204df03 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 @@ -23,8 +23,6 @@ import org.opendaylight.restconf.nb.jaxrs.JaxRsRestconf; import org.opendaylight.restconf.nb.jaxrs.JaxRsWebHostMetadata; import org.opendaylight.restconf.nb.jaxrs.JsonJaxRsFormattableBodyWriter; import org.opendaylight.restconf.nb.jaxrs.XmlJaxRsFormattableBodyWriter; -import org.opendaylight.restconf.nb.rfc8040.jersey.providers.JsonNormalizedNodeBodyWriter; -import org.opendaylight.restconf.nb.rfc8040.jersey.providers.XmlNormalizedNodeBodyWriter; import org.opendaylight.restconf.nb.rfc8040.jersey.providers.errors.RestconfDocumentedExceptionMapper; import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStreamServletFactory; import org.opendaylight.restconf.server.api.RestconfServer; @@ -58,11 +56,6 @@ public final class JaxRsNorthbound implements AutoCloseable { .addUrlPattern("/*") .servlet(servletSupport.createHttpServletBuilder( new Application() { - @Override - public Set> getClasses() { - return Set.of(JsonNormalizedNodeBodyWriter.class, XmlNormalizedNodeBodyWriter.class); - } - @Override public Set getSingletons() { final var errorTagMapping = servletFactory.errorTagMapping(); diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/AbstractNormalizedNodeBodyWriter.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/AbstractNormalizedNodeBodyWriter.java deleted file mode 100644 index 8bf81af721..0000000000 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/AbstractNormalizedNodeBodyWriter.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.nb.rfc8040.jersey.providers; - -import static java.util.Objects.requireNonNull; - -import java.io.IOException; -import java.io.OutputStream; -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.ext.MessageBodyWriter; -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.opendaylight.restconf.api.query.PrettyPrintParam; -import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload; -import org.opendaylight.restconf.nb.rfc8040.legacy.WriterParameters; -import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; -import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; - -abstract class AbstractNormalizedNodeBodyWriter implements MessageBodyWriter { - @Override - public final boolean isWriteable(final Class type, final Type genericType, final Annotation[] annotations, - final MediaType mediaType) { - return type.equals(NormalizedNodePayload.class); - } - - @Override - public final void writeTo(final NormalizedNodePayload context, final Class type, final Type genericType, - final Annotation[] annotations, final MediaType mediaType, final MultivaluedMap httpHeaders, - final OutputStream entityStream) throws IOException { - writeData(context.inference().toSchemaInferenceStack(), context.data(), context.writerParameters(), - context.prettyPrint(), requireNonNull(entityStream)); - } - - @NonNullByDefault - abstract void writeData(SchemaInferenceStack stack, NormalizedNode data, WriterParameters writerParameters, - PrettyPrintParam prettyPrint, OutputStream out) throws IOException; -} 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 deleted file mode 100644 index 6bb73b1111..0000000000 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyWriter.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.restconf.nb.rfc8040.jersey.providers; - -import com.google.gson.stream.JsonWriter; -import java.io.IOException; -import java.io.OutputStream; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.ext.Provider; -import org.eclipse.jdt.annotation.Nullable; -import org.opendaylight.restconf.api.MediaTypes; -import org.opendaylight.restconf.api.query.PrettyPrintParam; -import org.opendaylight.restconf.nb.rfc8040.legacy.WriterParameters; -import org.opendaylight.restconf.server.spi.FormattableBodySupport; -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.spi.node.ImmutableNodes; -import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; -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 { - @Override - void writeData(final SchemaInferenceStack stack, final NormalizedNode data, final WriterParameters writerParameters, - final PrettyPrintParam prettyPrint, final OutputStream out) throws IOException { - if (!stack.isEmpty()) { - stack.exit(); - } - - // RESTCONF allows returning one list item. We need to wrap it in map node in order to serialize it properly - final var toSerialize = data instanceof MapEntryNode mapEntry - ? ImmutableNodes.newSystemMapBuilder() - .withNodeIdentifier(new NodeIdentifier(data.name().getNodeType())) - .withChild(mapEntry) - .build() - : data; - - try (var jsonWriter = FormattableBodySupport.createJsonWriter(out, prettyPrint)) { - jsonWriter.beginObject(); - - final var nnWriter = createNormalizedNodeWriter(stack.toInference(), jsonWriter, writerParameters, null); - nnWriter.write(toSerialize); - nnWriter.flush(); - - jsonWriter.endObject().flush(); - } - } - - private static RestconfNormalizedNodeWriter createNormalizedNodeWriter(final Inference inference, - final JsonWriter jsonWriter, final WriterParameters writerParameters, - final @Nullable XMLNamespace initialNamespace) { - // TODO: Performance: Cache JSON Codec factory and schema context - final var codecs = JSONCodecFactorySupplier.RFC7951.getShared(inference.modelContext()); - return RestconfNormalizedNodeWriter.forStreamWriter( - JSONNormalizedNodeStreamWriter.createNestedWriter(codecs, inference, - initialNamespace, jsonWriter), writerParameters.depth(), writerParameters.fields()); - } -} 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 deleted file mode 100644 index 8da763cfcb..0000000000 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyWriter.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.restconf.nb.rfc8040.jersey.providers; - -import java.io.IOException; -import java.io.OutputStream; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.ext.Provider; -import javax.xml.XMLConstants; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; -import org.opendaylight.restconf.api.MediaTypes; -import org.opendaylight.restconf.api.query.PrettyPrintParam; -import org.opendaylight.restconf.nb.rfc8040.legacy.WriterParameters; -import org.opendaylight.restconf.server.spi.FormattableBodySupport; -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.MapEntryNode; -import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; -import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter; -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; - -@Provider -@Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML }) -public final class XmlNormalizedNodeBodyWriter extends AbstractNormalizedNodeBodyWriter { - @Override - void writeData(final SchemaInferenceStack stack, final NormalizedNode data, final WriterParameters writerParameters, - final PrettyPrintParam prettyPrint, final OutputStream out) throws IOException { - final boolean isRoot; - if (!stack.isEmpty()) { - stack.exit(); - isRoot = false; - } else { - isRoot = true; - } - - final var xmlWriter = FormattableBodySupport.createXmlWriter(out, prettyPrint); - final var nnWriter = createNormalizedNodeWriter(xmlWriter, stack.toInference(), writerParameters); - if (data instanceof MapEntryNode mapEntry) { - // Restconf allows returning one list item. We need to wrap it - // in map node in order to serialize it properly - nnWriter.write(ImmutableNodes.newSystemMapBuilder() - .withNodeIdentifier(new NodeIdentifier(data.name().getNodeType())) - .addChild(mapEntry) - .build()); - } else if (isRoot) { - if (data instanceof ContainerNode container && container.isEmpty()) { - writeEmptyDataNode(xmlWriter, container); - } else { - writeAndWrapInDataNode(xmlWriter, nnWriter, data); - } - } else { - nnWriter.write(data); - } - nnWriter.flush(); - } - - private static RestconfNormalizedNodeWriter createNormalizedNodeWriter(final XMLStreamWriter xmlWriter, - final Inference inference, final WriterParameters writerParameters) { - return RestconfNormalizedNodeWriter.forStreamWriter( - XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, inference), - writerParameters.depth(), writerParameters.fields()); - } - - private static void writeAndWrapInDataNode(final XMLStreamWriter xmlWriter, - final RestconfNormalizedNodeWriter nnWriter, final NormalizedNode data) throws IOException { - final QName nodeType = data.name().getNodeType(); - final String namespace = nodeType.getNamespace().toString(); - try { - xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, nodeType.getLocalName(), namespace); - xmlWriter.writeDefaultNamespace(namespace); - nnWriter.write(data); - xmlWriter.writeEndElement(); - xmlWriter.flush(); - } catch (XMLStreamException e) { - throw new IOException("Failed to write elements", e); - } - } - - private static void writeEmptyDataNode(final XMLStreamWriter xmlWriter, final ContainerNode data) - throws IOException { - final QName nodeType = data.name().getNodeType(); - final String namespace = nodeType.getNamespace().toString(); - try { - xmlWriter.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, nodeType.getLocalName(), namespace); - xmlWriter.writeDefaultNamespace(namespace); - xmlWriter.writeEndElement(); - xmlWriter.flush(); - } catch (XMLStreamException e) { - throw new IOException("Failed to write elements", e); - } - } -} diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/NormalizedNodePayload.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/NormalizedNodePayload.java deleted file mode 100644 index 4e62f79e9b..0000000000 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/NormalizedNodePayload.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.nb.rfc8040.legacy; - -import static java.util.Objects.requireNonNull; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.opendaylight.restconf.api.query.PrettyPrintParam; -import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; -import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; - -/** - * A RFC8040 overlay from our marriage to NormalizedNodeContext. This represents a NormalizedNode along with further - * messy details needed to deal with the payload. - */ -@NonNullByDefault -public record NormalizedNodePayload( - Inference inference, - NormalizedNode data, - WriterParameters writerParameters, - PrettyPrintParam prettyPrint) { - public NormalizedNodePayload { - requireNonNull(inference); - requireNonNull(data); - requireNonNull(writerParameters); - requireNonNull(prettyPrint); - } -} diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/WriterParameters.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/WriterParameters.java deleted file mode 100644 index 3e8a8b7c89..0000000000 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/WriterParameters.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.nb.rfc8040.legacy; - -import com.google.common.annotations.Beta; -import java.util.List; -import java.util.Set; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; -import org.opendaylight.restconf.api.query.DepthParam; -import org.opendaylight.yangtools.yang.common.QName; - -/** - * This holds various options acquired from a requests's query part. This class needs to be further split up to make - * sense of it, as parts of it pertain to how a {@link NormalizedNodePayload} should be created while others how it - * needs to be processed (for example filtered). - */ -@Beta -public record WriterParameters(@Nullable DepthParam depth, @Nullable List> fields) { - public static final @NonNull WriterParameters EMPTY = new WriterParameters(null, null); - - public static @NonNull WriterParameters of(final @Nullable DepthParam depth) { - return depth == null ? EMPTY : new WriterParameters(depth, null); - } -} diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalNormalizedNodeWriterFactory.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalNormalizedNodeWriterFactory.java new file mode 100644 index 0000000000..f2b63ca323 --- /dev/null +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalNormalizedNodeWriterFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.restconf.nb.rfc8040.rests.transactions; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.MoreObjects.ToStringHelper; +import java.util.List; +import java.util.Set; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.opendaylight.restconf.api.query.DepthParam; +import org.opendaylight.restconf.nb.rfc8040.jersey.providers.RestconfNormalizedNodeWriter; +import org.opendaylight.restconf.server.spi.NormalizedNodeWriterFactory; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; + +/** + * A {@link NormalizedNodeWriterFactory} which restricts output to specified fields and potentially the specified depth. + */ +@NonNullByDefault +final class MdsalNormalizedNodeWriterFactory extends NormalizedNodeWriterFactory { + private final @Nullable DepthParam depth; + private final List> fields; + + MdsalNormalizedNodeWriterFactory(final List> fields, final @Nullable DepthParam depth) { + this.fields = requireNonNull(fields); + this.depth = depth; + } + + @Override + protected RestconfNormalizedNodeWriter newWriter(final NormalizedNodeStreamWriter streamWriter) { + return RestconfNormalizedNodeWriter.forStreamWriter(streamWriter, depth, fields); + } + + @Override + protected ToStringHelper addToStringAttributes(final ToStringHelper helper) { + final var local = depth; + if (local != null) { + helper.add("depth", local.value()); + } + return helper.add("fields", fields); + } +} diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java index 06a68f3dd9..01c3433172 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java @@ -39,13 +39,13 @@ import org.opendaylight.restconf.common.errors.RestconfDocumentedException; import org.opendaylight.restconf.common.errors.RestconfFuture; import org.opendaylight.restconf.common.errors.SettableRestconfFuture; import org.opendaylight.restconf.nb.rfc8040.jersey.providers.RestconfNormalizedNodeWriter; -import org.opendaylight.restconf.nb.rfc8040.legacy.WriterParameters; import org.opendaylight.restconf.server.api.DataGetParams; import org.opendaylight.restconf.server.api.DataGetResult; import org.opendaylight.restconf.server.api.DatabindContext; import org.opendaylight.restconf.server.api.DatabindPath.Data; import org.opendaylight.restconf.server.api.ServerRequest; import org.opendaylight.restconf.server.spi.HttpGetResource; +import org.opendaylight.restconf.server.spi.NormalizedNodeWriterFactory; import org.opendaylight.restconf.server.spi.RpcImplementation; import org.opendaylight.restconf.server.spi.YangLibraryVersionResource; import org.opendaylight.yangtools.yang.common.Empty; @@ -130,13 +130,14 @@ public final class MdsalRestconfStrategy extends RestconfStrategy { @Override RestconfFuture dataGET(final ServerRequest request, final Data path, final DataGetParams params) { - final var inference = path.inference(); + final var depth = params.depth(); final var fields = params.fields(); - return completeDataGET(request.prettyPrint(), inference, - fields == null ? WriterParameters.of(params.depth()) - : new WriterParameters(params.depth(), - translateFieldsParam(inference.modelContext(), path.schema(), fields)), - readData(params.content(), path.instance(), params.withDefaults()), null); + final var writerFactory = fields == null ? NormalizedNodeWriterFactory.of(depth) + : new MdsalNormalizedNodeWriterFactory( + translateFieldsParam(path.inference().modelContext(), path.schema(), fields), depth); + + return completeDataGET(readData(params.content(), path.instance(), params.withDefaults()), path, writerFactory, + null); } @Override diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java index e06d3ea265..fe5ec4b5d3 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java @@ -40,12 +40,12 @@ import org.opendaylight.restconf.api.query.WithDefaultsParam; import org.opendaylight.restconf.common.errors.RestconfDocumentedException; import org.opendaylight.restconf.common.errors.RestconfFuture; import org.opendaylight.restconf.common.errors.SettableRestconfFuture; -import org.opendaylight.restconf.nb.rfc8040.legacy.WriterParameters; import org.opendaylight.restconf.server.api.DataGetParams; import org.opendaylight.restconf.server.api.DataGetResult; import org.opendaylight.restconf.server.api.DatabindContext; import org.opendaylight.restconf.server.api.DatabindPath.Data; import org.opendaylight.restconf.server.api.ServerRequest; +import org.opendaylight.restconf.server.spi.NormalizedNodeWriterFactory; import org.opendaylight.yangtools.yang.common.Empty; import org.opendaylight.yangtools.yang.common.ErrorTag; import org.opendaylight.yangtools.yang.common.ErrorType; @@ -103,13 +103,12 @@ public final class NetconfRestconfStrategy extends RestconfStrategy { @Override RestconfFuture dataGET(final ServerRequest request, final Data path, final DataGetParams params) { - final var inference = path.inference(); final var fields = params.fields(); final List fieldPaths; if (fields != null) { final List tmp; try { - tmp = fieldsParamToPaths(inference.modelContext(), path.schema(), fields); + tmp = fieldsParamToPaths(path.inference().modelContext(), path.schema(), fields); } catch (RestconfDocumentedException e) { return RestconfFuture.failed(e); } @@ -124,8 +123,7 @@ public final class NetconfRestconfStrategy extends RestconfStrategy { } else { node = readData(params.content(), path.instance(), params.withDefaults()); } - - return completeDataGET(request.prettyPrint(), inference, WriterParameters.of(params.depth()), node, null); + return completeDataGET(node, path, NormalizedNodeWriterFactory.of(params.depth()), null); } @Override 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 a091c955bf..074f466284 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 @@ -52,7 +52,6 @@ import org.opendaylight.netconf.dom.api.NetconfDataTreeService; import org.opendaylight.restconf.api.ApiPath; import org.opendaylight.restconf.api.FormattableBody; import org.opendaylight.restconf.api.query.ContentParam; -import org.opendaylight.restconf.api.query.PrettyPrintParam; import org.opendaylight.restconf.api.query.WithDefaultsParam; import org.opendaylight.restconf.common.errors.RestconfDocumentedException; import org.opendaylight.restconf.common.errors.RestconfError; @@ -61,8 +60,6 @@ import org.opendaylight.restconf.common.errors.SettableRestconfFuture; import org.opendaylight.restconf.common.patch.PatchContext; import org.opendaylight.restconf.nb.rfc8040.Insert; import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags; -import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload; -import org.opendaylight.restconf.nb.rfc8040.legacy.WriterParameters; import org.opendaylight.restconf.server.api.ChildBody; import org.opendaylight.restconf.server.api.ConfigurationMetadata; import org.opendaylight.restconf.server.api.CreateResourceResult; @@ -91,6 +88,8 @@ import org.opendaylight.restconf.server.spi.ApiPathCanonizer; import org.opendaylight.restconf.server.spi.ApiPathNormalizer; import org.opendaylight.restconf.server.spi.DefaultResourceContext; import org.opendaylight.restconf.server.spi.HttpGetResource; +import org.opendaylight.restconf.server.spi.NormalizedFormattableBody; +import org.opendaylight.restconf.server.spi.NormalizedNodeWriterFactory; import org.opendaylight.restconf.server.spi.OperationInput; import org.opendaylight.restconf.server.spi.OperationOutputBody; import org.opendaylight.restconf.server.spi.OperationsResource; @@ -138,7 +137,6 @@ import org.opendaylight.yangtools.yang.model.api.source.YangTextSource; import org.opendaylight.yangtools.yang.model.api.source.YinTextSource; import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement; import org.opendaylight.yangtools.yang.model.api.stmt.SubmoduleEffectiveStatement; -import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -843,18 +841,19 @@ public abstract class RestconfStrategy { abstract @NonNull RestconfFuture dataGET(ServerRequest request, Data path, DataGetParams params); - static final @NonNull RestconfFuture completeDataGET(final PrettyPrintParam prettyPrint, - final Inference inference, final WriterParameters writerParams, final @Nullable NormalizedNode node, - final @Nullable ConfigurationMetadata metadata) { + @NonNullByDefault + static final RestconfFuture completeDataGET(final @Nullable NormalizedNode node, final Data path, + final NormalizedNodeWriterFactory writerFactory, final @Nullable ConfigurationMetadata metadata) { + // Non-existing data if (node == null) { return RestconfFuture.failed(new RestconfDocumentedException( "Request could not be completed because the relevant data model content does not exist", ErrorType.PROTOCOL, ErrorTag.DATA_MISSING)); } - final var payload = new NormalizedNodePayload(inference, node, writerParams, prettyPrint); - return RestconfFuture.of(metadata == null ? new DataGetResult(payload) - : new DataGetResult(payload, metadata.entityTag(), metadata.lastModified())); + final var body = NormalizedFormattableBody.of(path, node, writerFactory); + return RestconfFuture.of(metadata == null ? new DataGetResult(body) + : new DataGetResult(body, metadata.entityTag(), metadata.lastModified())); } /** diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetResult.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetResult.java index 3a8ed7ba9e..5664c4bb7b 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetResult.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetResult.java @@ -12,25 +12,24 @@ import static java.util.Objects.requireNonNull; import java.time.Instant; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; -import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload; /** * Result of a {@code GET} request as defined in * RFC8040 section 4.3. * - * @param payload Resulting payload + * @param body Resulting body * @param entityTag response {@code ETag} header, or {@code null} if not applicable * @param lastModified response {@code Last-Modified} header, or {@code null} if not applicable */ public record DataGetResult( - @NonNull NormalizedNodePayload payload, + @NonNull DatabindFormattableBody body, @Nullable EntityTag entityTag, @Nullable Instant lastModified) implements ConfigurationMetadata { public DataGetResult { - requireNonNull(payload); + requireNonNull(body); } - public DataGetResult(final @NonNull NormalizedNodePayload payload) { - this(payload, null, null); + public DataGetResult(final @NonNull DatabindFormattableBody body) { + this(body, null, null); } } 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 c4d637d95a..0385dd611c 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 @@ -13,7 +13,6 @@ import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.restconf.api.ApiPath; import org.opendaylight.restconf.api.FormattableBody; import org.opendaylight.restconf.common.errors.RestconfFuture; -import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload; import org.opendaylight.yangtools.yang.common.Empty; /** @@ -166,7 +165,7 @@ public interface RestconfServer { * RFC8040 {+restconf}/yang-library-version. * * @param request {@link ServerRequest} for this request - * @return A {@link RestconfFuture} completing with {@link NormalizedNodePayload} containing a single + * @return A {@link RestconfFuture} completing with {@link FormattableBody} containing a single * {@code yang-library-version} leaf element. */ RestconfFuture yangLibraryVersionGET(ServerRequest request); diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DataFormattableBody.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DataFormattableBody.java new file mode 100644 index 0000000000..4f4b14f2fd --- /dev/null +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DataFormattableBody.java @@ -0,0 +1,74 @@ +/* + * 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.spi; + +import static com.google.common.base.Verify.verify; +import static java.util.Objects.requireNonNull; + +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import javax.xml.stream.XMLStreamWriter; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.opendaylight.restconf.server.api.DatabindContext; +import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode; +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.JSONCodecFactory; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.xml.XmlCodecFactory; +import org.opendaylight.yangtools.yang.model.api.stmt.DataTreeEffectiveStatement; +import org.opendaylight.yangtools.yang.model.api.stmt.LeafListEffectiveStatement; +import org.opendaylight.yangtools.yang.model.api.stmt.ListEffectiveStatement; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; + +/** + * A {@link NormalizedFormattableBody} representing a data resource. + */ +@NonNullByDefault +final class DataFormattableBody extends NormalizedFormattableBody { + private final Inference parent; + + DataFormattableBody(final DatabindContext databind, final Inference parent, final N data, + final NormalizedNodeWriterFactory writerFactory) { + super(databind, writerFactory, data); + this.parent = requireNonNull(parent); + + // RESTCONF allows returning one list item. We need to wrap it in map node in order to serialize it properly, + // which is where the notion of "parent Inference" may be confusing. We mean + // 'inference to the parent NormalizedNode', which for *EntryNode ends up the corresponding statement + if (data instanceof MapEntryNode) { + verifyParent(parent, ListEffectiveStatement.class, data); + } else if (data instanceof LeafSetEntryNode) { + verifyParent(parent, LeafListEffectiveStatement.class, data); + } + } + + DataFormattableBody(final DatabindContext databind, final Inference parent, final N data) { + this(databind, parent, data, NormalizedNodeWriterFactory.of()); + } + + private static void verifyParent(final Inference inference, + final Class> expectedStmt, final NormalizedNode data) { + // Let's not bother with niceties of error report -- if we trip here, the caller is doing the wrong + final var qname = expectedStmt.cast(inference.toSchemaInferenceStack().currentStatement()).argument(); + verify(qname.equals(data.name().getNodeType())); + } + + @Override + protected void formatToJSON(final JSONCodecFactory codecs, final N data, final JsonWriter writer) + throws IOException { + writeTo(data, JSONNormalizedNodeStreamWriter.createExclusiveWriter(codecs, parent, null, writer)); + } + + @Override + protected void formatToXML(final XmlCodecFactory codecs, final N data, final XMLStreamWriter xmlWriter) + throws IOException { + writeTo(data, XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, parent)); + } +} diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DefaultNormalizedNodeWriterFactory.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DefaultNormalizedNodeWriterFactory.java new file mode 100644 index 0000000000..0cc6e1bc6f --- /dev/null +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DefaultNormalizedNodeWriterFactory.java @@ -0,0 +1,35 @@ +/* + * 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.spi; + +import com.google.common.base.MoreObjects.ToStringHelper; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.opendaylight.restconf.nb.rfc8040.jersey.providers.RestconfNormalizedNodeWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; + +/** + * Default {@link NormalizedNodeWriterFactory}. + */ +@NonNullByDefault +final class DefaultNormalizedNodeWriterFactory extends NormalizedNodeWriterFactory { + static final DefaultNormalizedNodeWriterFactory INSTANCE = new DefaultNormalizedNodeWriterFactory(); + + private DefaultNormalizedNodeWriterFactory() { + // Hidden on purpose + } + + @Override + protected RestconfNormalizedNodeWriter newWriter(final NormalizedNodeStreamWriter streamWriter) { + return RestconfNormalizedNodeWriter.forStreamWriter(streamWriter, null); + } + + @Override + protected ToStringHelper addToStringAttributes(final ToStringHelper helper) { + return helper; + } +} \ No newline at end of file diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/MaxDepthNormalizedNodeWriterFactory.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/MaxDepthNormalizedNodeWriterFactory.java new file mode 100644 index 0000000000..b5cf1eff52 --- /dev/null +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/MaxDepthNormalizedNodeWriterFactory.java @@ -0,0 +1,40 @@ +/* + * 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.spi; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.MoreObjects.ToStringHelper; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.opendaylight.restconf.api.query.DepthParam; +import org.opendaylight.restconf.nb.rfc8040.jersey.providers.RestconfNormalizedNodeWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; + +/** + * A {@link NormalizedNodeWriterFactory} returning a {@link RestconfNormalizedNodeWriter} which emits the data only to + * a certain + * depth. + */ +@NonNullByDefault +final class MaxDepthNormalizedNodeWriterFactory extends NormalizedNodeWriterFactory { + private final DepthParam depth; + + MaxDepthNormalizedNodeWriterFactory(final DepthParam depth) { + this.depth = requireNonNull(depth); + } + + @Override + protected RestconfNormalizedNodeWriter newWriter(final NormalizedNodeStreamWriter streamWriter) { + return RestconfNormalizedNodeWriter.forStreamWriter(streamWriter, depth); + } + + @Override + protected ToStringHelper addToStringAttributes(final ToStringHelper helper) { + return helper.add("depth", depth.value()); + } +} \ No newline at end of file diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/NormalizedFormattableBody.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/NormalizedFormattableBody.java index 9ccd06efb4..f11030bc29 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/NormalizedFormattableBody.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/NormalizedFormattableBody.java @@ -7,86 +7,122 @@ */ package org.opendaylight.restconf.server.spi; -import static com.google.common.base.Verify.verify; import static java.util.Objects.requireNonNull; import com.google.common.base.MoreObjects.ToStringHelper; +import com.google.common.base.VerifyException; +import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.io.OutputStream; import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; import org.eclipse.jdt.annotation.NonNullByDefault; import org.opendaylight.restconf.api.FormattableBody; import org.opendaylight.restconf.api.query.PrettyPrintParam; +import org.opendaylight.restconf.nb.rfc8040.jersey.providers.RestconfNormalizedNodeWriter; import org.opendaylight.restconf.server.api.DatabindContext; import org.opendaylight.restconf.server.api.DatabindFormattableBody; +import org.opendaylight.restconf.server.api.DatabindPath.Data; +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.MapEntryNode; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode; import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; -import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter; -import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter; -import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter; -import org.opendaylight.yangtools.yang.model.api.stmt.DataTreeEffectiveStatement; -import org.opendaylight.yangtools.yang.model.api.stmt.LeafListEffectiveStatement; -import org.opendaylight.yangtools.yang.model.api.stmt.ListEffectiveStatement; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory; +import org.opendaylight.yangtools.yang.data.codec.xml.XmlCodecFactory; import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; /** * A {@link FormattableBody} representing a data resource. */ @NonNullByDefault -public final class NormalizedFormattableBody extends DatabindFormattableBody { - private final Inference parent; +public abstract sealed class NormalizedFormattableBody extends DatabindFormattableBody + permits DataFormattableBody, RootFormattableBody { + private final NormalizedNodeWriterFactory writerFactory; private final N data; - public NormalizedFormattableBody(final DatabindContext databind, final Inference parent, final N data) { + NormalizedFormattableBody(final DatabindContext databind, final NormalizedNodeWriterFactory writerFactory, + final N data) { super(databind); - this.parent = requireNonNull(parent); + this.writerFactory = requireNonNull(writerFactory); this.data = requireNonNull(data); - // RESTCONF allows returning one list item. We need to wrap it in map node in order to serialize it properly, - // which is where the notion of "parent Inference" may be confusing. We mean - // 'inference to the parent NormalizedNode', which for *EntryNode ends up the corresponding statement - if (data instanceof MapEntryNode) { - verifyParent(parent, ListEffectiveStatement.class, data); - } else if (data instanceof LeafSetEntryNode) { - verifyParent(parent, LeafListEffectiveStatement.class, data); + } + + public static NormalizedFormattableBody of(final Data path, final NormalizedNode data, + final NormalizedNodeWriterFactory writerFactory) { + final var inference = path.inference(); + if (inference.isEmpty()) { + // Read of the entire /data resource + if (data instanceof ContainerNode container) { + return new RootFormattableBody(path.databind(), writerFactory, container); + } + throw new VerifyException("Unexpected root data contract " + data.contract()); + } + + // Read of a sub-resource. We need to adjust the inference to point to the NormalizedNode parent of the node + // being output. + final Inference parentInference; + if (data instanceof MapEntryNode || data instanceof LeafSetEntryNode || data instanceof UnkeyedListEntryNode) { + parentInference = inference; + } else { + final var stack = inference.toSchemaInferenceStack(); + stack.exitToDataTree(); + parentInference = stack.toInference(); } + + return new DataFormattableBody<>(path.databind(), parentInference, data, writerFactory); } - private static void verifyParent(final Inference inference, - final Class> expectedStmt, final NormalizedNode data) { - // Let's not bother with niceties of error report -- if we trip here, the caller is doing the wrong - final var qname = expectedStmt.cast(inference.toSchemaInferenceStack().currentStatement()).argument(); - verify(qname.equals(data.name().getNodeType())); + /** + * Return data. + * + * @return data + */ + public final N data() { + return data; } @Override - protected void formatToJSON(final DatabindContext databind, final PrettyPrintParam prettyPrint, + protected final void formatToJSON(final DatabindContext databind, final PrettyPrintParam prettyPrint, final OutputStream out) throws IOException { - writeTo(JSONNormalizedNodeStreamWriter.createExclusiveWriter(databind.jsonCodecs(), parent, null, - FormattableBodySupport.createJsonWriter(out, prettyPrint))); + try (var writer = FormattableBodySupport.createJsonWriter(out, prettyPrint)) { + formatToJSON(databind.jsonCodecs(), data, writer); + } } + protected abstract void formatToJSON(JSONCodecFactory codecs, N data, JsonWriter writer) throws IOException; + @Override - protected void formatToXML(final DatabindContext databind, final PrettyPrintParam prettyPrint, + protected final void formatToXML(final DatabindContext databind, final PrettyPrintParam prettyPrint, final OutputStream out) throws IOException { - final var xmlWriter = FormattableBodySupport.createXmlWriter(out, prettyPrint); - writeTo(XMLStreamNormalizedNodeStreamWriter.create(xmlWriter, parent)); + final var writer = FormattableBodySupport.createXmlWriter(out, prettyPrint); try { - xmlWriter.close(); + formatToXML(databind.xmlCodecs(), data, writer); + writer.close(); } catch (XMLStreamException e) { throw new IOException("Failed to write data", e); } } + protected abstract void formatToXML(XmlCodecFactory codecs, N data, XMLStreamWriter writer) + throws IOException, XMLStreamException; + + protected final RestconfNormalizedNodeWriter newWriter(final NormalizedNodeStreamWriter streamWriter) { + return writerFactory.newWriter(streamWriter); + } + + final void writeTo(final NormalizedNode toWrite, final NormalizedNodeStreamWriter streamWriter) + throws IOException { + try (var writer = newWriter(streamWriter)) { + writer.write(toWrite); + } + } + @Override protected ToStringHelper addToStringAttributes(final ToStringHelper helper) { return helper.add("body", data.prettyTree()); } - private void writeTo(final NormalizedNodeStreamWriter streamWriter) throws IOException { - try (var writer = NormalizedNodeWriter.forStreamWriter(streamWriter)) { - writer.write(data); - } - } + } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/NormalizedNodeWriterFactory.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/NormalizedNodeWriterFactory.java new file mode 100644 index 0000000000..8a8975eba6 --- /dev/null +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/NormalizedNodeWriterFactory.java @@ -0,0 +1,51 @@ +/* + * 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.spi; + +import com.google.common.base.MoreObjects; +import com.google.common.base.MoreObjects.ToStringHelper; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.opendaylight.restconf.api.query.DepthParam; +import org.opendaylight.restconf.nb.rfc8040.jersey.providers.RestconfNormalizedNodeWriter; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; + +/** + * Interface for instantiating a {@link RestconfNormalizedNodeWriter} to handle the data writeout to a + * {@link NormalizedNodeStreamWriter}. + */ +@NonNullByDefault +public abstract class NormalizedNodeWriterFactory { + /** + * Return the default {@link NormalizedNodeWriterFactory}. + * + * @return the default {@link NormalizedNodeWriterFactory} + */ + public static final NormalizedNodeWriterFactory of() { + return DefaultNormalizedNodeWriterFactory.INSTANCE; + } + + public static final NormalizedNodeWriterFactory of(final @Nullable DepthParam depth) { + return depth == null ? of() : new MaxDepthNormalizedNodeWriterFactory(depth); + } + + /** + * Create a new {@link RestconfNormalizedNodeWriter} for specified {@link NormalizedNodeStreamWriter}. + * + * @param streamWriter target {@link NormalizedNodeStreamWriter} + * @return A {@link RestconfNormalizedNodeWriter} + */ + protected abstract RestconfNormalizedNodeWriter newWriter(NormalizedNodeStreamWriter streamWriter); + + protected abstract ToStringHelper addToStringAttributes(ToStringHelper helper); + + @Override + public final String toString() { + return addToStringAttributes(MoreObjects.toStringHelper(this).omitNullValues()).toString(); + } +} \ No newline at end of file diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/RootFormattableBody.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/RootFormattableBody.java new file mode 100644 index 0000000000..b8dfe474ba --- /dev/null +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/RootFormattableBody.java @@ -0,0 +1,66 @@ +/* + * 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.spi; + +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import javax.xml.XMLConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.opendaylight.restconf.server.api.DatabindContext; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.xml.XmlCodecFactory; + +/** + * A {@link NormalizedFormattableBody} for a data root. + */ +@NonNullByDefault +final class RootFormattableBody extends NormalizedFormattableBody { + RootFormattableBody(final DatabindContext databind, final NormalizedNodeWriterFactory writerFactory, + final ContainerNode data) { + super(databind, writerFactory, data); + } + + @Override + protected void formatToJSON(final JSONCodecFactory codecs, final ContainerNode data, final JsonWriter writer) + throws IOException { + writer.beginObject(); + + final var nnWriter = newWriter(JSONNormalizedNodeStreamWriter.createNestedWriter(codecs, writer, null)); + // FIXME: we should be remapping namespace here + nnWriter.write(data); + nnWriter.flush(); + + writer.endObject(); + } + + @Override + protected void formatToXML(final XmlCodecFactory codecs, final ContainerNode data, final XMLStreamWriter writer) + throws IOException, XMLStreamException { + // FIXME: we should be remapping namespace here + final QName nodeType = data.name().getNodeType(); + final String namespace = nodeType.getNamespace().toString(); + + writer.writeStartElement(XMLConstants.DEFAULT_NS_PREFIX, nodeType.getLocalName(), namespace); + writer.writeDefaultNamespace(namespace); + + if (!data.isEmpty()) { + final var nnWriter = newWriter( + XMLStreamNormalizedNodeStreamWriter.create(writer, codecs.modelContext())); + nnWriter.write(data); + nnWriter.flush(); + } + + writer.writeEndElement(); + } +} diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/YangLibraryVersionResource.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/YangLibraryVersionResource.java index 3b567fbe03..99f38045bf 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/YangLibraryVersionResource.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/YangLibraryVersionResource.java @@ -71,7 +71,7 @@ public record YangLibraryVersionResource(DatabindContext databind, Inference res @Override public RestconfFuture httpGET(final ServerRequest request) { - return RestconfFuture.of(new NormalizedFormattableBody<>(databind, restconf, leaf)); + return RestconfFuture.of(new DataFormattableBody<>(databind, restconf, leaf)); } @Override 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 2520e9b0c5..5632434bfe 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 @@ -25,6 +25,7 @@ import javax.ws.rs.container.AsyncResponse; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -43,9 +44,9 @@ import org.opendaylight.restconf.common.errors.RestconfDocumentedException; import org.opendaylight.restconf.common.errors.RestconfError; import org.opendaylight.restconf.nb.rfc8040.AbstractJukeboxTest; import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping; -import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload; import org.opendaylight.restconf.server.mdsal.MdsalDatabindProvider; import org.opendaylight.restconf.server.mdsal.MdsalRestconfServer; +import org.opendaylight.restconf.server.spi.NormalizedFormattableBody; import org.opendaylight.restconf.server.spi.OperationOutputBody; import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; @@ -102,6 +103,12 @@ abstract class AbstractRestconfTest extends AbstractJukeboxTest { assertEquals(expectedXml, baos.toString(StandardCharsets.UTF_8)); } + @NonNullByDefault + static final NormalizedFormattableBody assertNormalizedBody(final int status, + final Consumer invocation) { + return assertInstanceOf(NormalizedFormattableBody.class, assertFormattableBody(status, invocation)); + } + static final FormattableBody assertFormattableBody(final int status, final Consumer invocation) { return assertEntity(JaxRsFormattableBody.class, status, invocation).body(); } @@ -115,15 +122,6 @@ abstract class AbstractRestconfTest extends AbstractJukeboxTest { return assertEntity(OperationOutputBody.class, status, invocation); } - static final NormalizedNode assertNormalizedNode(final int status, final Consumer invocation) { - return assertNormalizedNodePayload(status, invocation).data(); - } - - static final NormalizedNodePayload assertNormalizedNodePayload(final int status, - final Consumer invocation) { - return assertEntity(NormalizedNodePayload.class, status, invocation); - } - static final T assertEntity(final Class expectedType, final int expectedStatus, final Consumer invocation) { return assertInstanceOf(expectedType, assertEntity(expectedStatus, invocation)); diff --git a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfDataGetTest.java b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfDataGetTest.java index ff75a2c7ea..74848baf1d 100644 --- a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfDataGetTest.java +++ b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfDataGetTest.java @@ -74,7 +74,22 @@ class RestconfDataGetTest extends AbstractRestconfTest { doReturn(immediateFluentFuture(Optional.empty())) .when(tx).read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID); - assertEquals(EMPTY_JUKEBOX, assertNormalizedNode(200, ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar))); + final var body = assertNormalizedBody(200, ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar)); + assertEquals(EMPTY_JUKEBOX, body.data()); + assertFormat(""" + { + "example-jukebox:jukebox": { + "player": { + "gap": "0.2" + } + } + }""", body::formatToJSON, true); + assertFormat(""" + + + 0.2 + + """, body::formatToXML, true); } @Test @@ -87,12 +102,30 @@ class RestconfDataGetTest extends AbstractRestconfTest { .when(tx) .read(LogicalDatastoreType.OPERATIONAL, YangInstanceIdentifier.of()); - final var data = assertInstanceOf(ContainerNode.class, - assertNormalizedNode(200, ar -> restconf.dataGET(uriInfo, ar))); + final var body = assertNormalizedBody(200, ar -> restconf.dataGET(uriInfo, ar)); + final var data = assertInstanceOf(ContainerNode.class, body.data()); final var rootNodes = data.body(); assertEquals(1, rootNodes.size()); final var allDataChildren = assertInstanceOf(ContainerNode.class, rootNodes.iterator().next()).body(); assertEquals(3, allDataChildren.size()); + + assertFormat(""" + { + "example-jukebox:jukebox": { + "player": { + "gap": "0.2" + } + } + }""", body::formatToJSON, true); + assertFormat(""" + + + + + 0.2 + + + """, body::formatToXML, true); } private static ContainerNode wrapNodeByDataRootContainer(final DataContainerChild data) { @@ -125,13 +158,29 @@ class RestconfDataGetTest extends AbstractRestconfTest { doReturn(Optional.empty()).when(mountPoint).getService(NetconfDataTreeService.class); // response must contain all child nodes from config and operational containers merged in one container - final var data = assertInstanceOf(ContainerNode.class, - assertNormalizedNode(200, ar -> restconf.dataGET( - apiPath("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox"), uriInfo, ar))); + final var body = assertNormalizedBody(200, ar -> restconf.dataGET( + apiPath("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox"), uriInfo, ar)); + final var data = assertInstanceOf(ContainerNode.class, body.data()); assertEquals(3, data.size()); assertNotNull(data.childByArg(CONT_PLAYER.name())); assertNotNull(data.childByArg(LIBRARY_NID)); assertNotNull(data.childByArg(PLAYLIST_NID)); + + assertFormat(""" + { + "example-jukebox:jukebox": { + "player": { + "gap": "0.2" + } + } + }""", body::formatToJSON, true); + assertFormat(""" + + + + 0.2 + + """, body::formatToXML, true); } @Test @@ -162,13 +211,29 @@ class RestconfDataGetTest extends AbstractRestconfTest { .read(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID); // response must contain only config data - final var data = assertInstanceOf(ContainerNode.class, - assertNormalizedNode(200, ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar))); + final var body = assertNormalizedBody(200, ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar)); + final var data = assertInstanceOf(ContainerNode.class, body.data()); // config data present assertNotNull(data.childByArg(CONT_PLAYER.name())); assertNotNull(data.childByArg(LIBRARY_NID)); // state data absent assertNull(data.childByArg(PLAYLIST_NID)); + + assertFormat(""" + { + "example-jukebox:jukebox": { + "player": { + "gap": "0.2" + } + } + }""", body::formatToJSON, true); + assertFormat(""" + + + + 0.2 + + """, body::formatToXML, true); } /** @@ -184,13 +249,28 @@ class RestconfDataGetTest extends AbstractRestconfTest { .read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID); // response must contain only operational data - final var data = assertInstanceOf(ContainerNode.class, assertNormalizedNode(200, - ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar))); + final var body = assertNormalizedBody(200, ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar)); + final var data = assertInstanceOf(ContainerNode.class, body.data()); // state data present assertNotNull(data.childByArg(CONT_PLAYER.name())); assertNotNull(data.childByArg(PLAYLIST_NID)); // config data absent assertNull(data.childByArg(LIBRARY_NID)); + + assertFormat(""" + { + "example-jukebox:jukebox": { + "player": { + "gap": "0.2" + } + } + }""", body::formatToJSON, true); + assertFormat(""" + + + 0.2 + + """, body::formatToXML, true); } } diff --git a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/AbstractInstanceIdentifierTest.java b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/AbstractInstanceIdentifierTest.java index 2b4753bc88..1aef225e80 100644 --- a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/AbstractInstanceIdentifierTest.java +++ b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/AbstractInstanceIdentifierTest.java @@ -10,6 +10,7 @@ package org.opendaylight.restconf.nb.rfc8040; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import org.eclipse.jdt.annotation.NonNull; import org.opendaylight.restconf.server.api.DatabindContext; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.common.QNameModule; @@ -48,9 +49,9 @@ public abstract class AbstractInstanceIdentifierTest { protected static final QName MY_LEAF11_QNAME = QName.create(PATCH_CONT_QNAME, "my-leaf11"); protected static final QName MY_LEAF12_QNAME = QName.create(PATCH_CONT_QNAME, "my-leaf12"); - protected static final EffectiveModelContext IID_SCHEMA = + protected static final @NonNull EffectiveModelContext IID_SCHEMA = YangParserTestUtils.parseYangResourceDirectory("/instanceidentifier/yang"); - protected static final DatabindContext IID_DATABIND = DatabindContext.ofModel(IID_SCHEMA); + protected static final @NonNull DatabindContext IID_DATABIND = DatabindContext.ofModel(IID_SCHEMA); protected static final InputStream stringInputStream(final String str) { return new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8)); diff --git a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyWriterTest.java b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyWriterTest.java deleted file mode 100644 index c19e53c9c3..0000000000 --- a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyWriterTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.restconf.nb.rfc8040.jersey.providers; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import javax.ws.rs.core.MediaType; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; -import org.opendaylight.restconf.api.query.PrettyPrintParam; -import org.opendaylight.restconf.nb.rfc8040.AbstractInstanceIdentifierTest; -import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload; -import org.opendaylight.restconf.nb.rfc8040.legacy.WriterParameters; -import org.opendaylight.yangtools.yang.common.QName; -import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; -import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes; -import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; -import org.opendaylight.yangtools.yang.model.api.SchemaContext; -import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference; - -@RunWith(MockitoJUnitRunner.StrictStubs.class) -public class XmlNormalizedNodeBodyWriterTest extends AbstractInstanceIdentifierTest { - @Test - public void testWriteEmptyRootContainer() throws IOException { - final EffectiveModelContext schemaContext = mock(EffectiveModelContext.class); - - final NormalizedNodePayload nodePayload = new NormalizedNodePayload(Inference.ofDataTreePath(schemaContext), - ImmutableNodes.newContainerBuilder().withNodeIdentifier(new NodeIdentifier(SchemaContext.NAME)).build(), - WriterParameters.EMPTY, PrettyPrintParam.FALSE); - - final ByteArrayOutputStream output = new ByteArrayOutputStream(); - final XmlNormalizedNodeBodyWriter xmlWriter = new XmlNormalizedNodeBodyWriter(); - xmlWriter.writeTo(nodePayload, null, null, null, MediaType.APPLICATION_XML_TYPE, null, output); - - // FIXME: NETCONF-855: this is wrong, the namespace should be 'urn:ietf:params:xml:ns:yang:ietf-restconf' - assertEquals("", - output.toString(StandardCharsets.UTF_8)); - } - - @Test - public void testRootContainerWrite() throws IOException { - final NormalizedNodePayload nodePayload = new NormalizedNodePayload( - Inference.ofDataTreePath(IID_SCHEMA), - ImmutableNodes.newContainerBuilder() - .withNodeIdentifier(new NodeIdentifier(SchemaContext.NAME)) - .withChild(ImmutableNodes.newContainerBuilder() - .withNodeIdentifier(new NodeIdentifier( - QName.create("foo:module", "2016-09-29", "foo-bar-container"))) - .build()) - .withChild(ImmutableNodes.newContainerBuilder() - .withNodeIdentifier(new NodeIdentifier( - QName.create("bar:module", "2016-09-29", "foo-bar-container"))) - .build()) - .build(), WriterParameters.EMPTY, PrettyPrintParam.FALSE); - - final ByteArrayOutputStream output = new ByteArrayOutputStream(); - final XmlNormalizedNodeBodyWriter xmlWriter = new XmlNormalizedNodeBodyWriter(); - xmlWriter.writeTo(nodePayload, null, null, null, MediaType.APPLICATION_XML_TYPE, null, output); - - assertEquals(""" - \ - \ - \ - """, output.toString(StandardCharsets.UTF_8)); - } -} \ No newline at end of file diff --git a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/spi/NormalizedFormattableBodyTest.java b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/spi/NormalizedFormattableBodyTest.java new file mode 100644 index 0000000000..3758eef034 --- /dev/null +++ b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/spi/NormalizedFormattableBodyTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 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.spi; + +import org.junit.jupiter.api.Test; +import org.opendaylight.restconf.nb.rfc8040.AbstractInstanceIdentifierTest; +import org.opendaylight.restconf.nb.rfc8040.AbstractJukeboxTest; +import org.opendaylight.restconf.server.api.DatabindPath.Data; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; + +class NormalizedFormattableBodyTest extends AbstractInstanceIdentifierTest { + @Test + void testWriteEmptyRootContainer() throws Exception { + final var body = NormalizedFormattableBody.of(new Data(IID_DATABIND), ImmutableNodes.newContainerBuilder() + .withNodeIdentifier(new NodeIdentifier(SchemaContext.NAME)) + .build(), NormalizedNodeWriterFactory.of()); + + // FIXME: NETCONF-855: this is wrong, the namespace should be 'urn:ietf:params:xml:ns:yang:ietf-restconf' + AbstractJukeboxTest.assertFormat("", + body::formatToXML, false); + AbstractJukeboxTest.assertFormat("{}", body::formatToJSON, false); + } + + @Test + void testRootContainerWrite() throws Exception { + final var body = NormalizedFormattableBody.of(new Data(IID_DATABIND), ImmutableNodes.newContainerBuilder() + .withNodeIdentifier(new NodeIdentifier(SchemaContext.NAME)) + .withChild(ImmutableNodes.newContainerBuilder() + .withNodeIdentifier(new NodeIdentifier( + QName.create("foo:module", "2016-09-29", "foo-bar-container"))) + .build()) + .withChild(ImmutableNodes.newContainerBuilder() + .withNodeIdentifier(new NodeIdentifier( + QName.create("bar:module", "2016-09-29", "foo-bar-container"))) + .build()) + .build(), NormalizedNodeWriterFactory.of()); + + // FIXME: NETCONF-855: this is wrong, the namespace should be 'urn:ietf:params:xml:ns:yang:ietf-restconf' + AbstractJukeboxTest.assertFormat(""" + \ + \ + \ + """, body::formatToXML, false); + AbstractJukeboxTest.assertFormat(""" + {"bar-module:foo-bar-container":{}}""", body::formatToJSON, false); + } +} \ No newline at end of file diff --git a/restconf/restconf-nb/src/test/resources/instanceidentifier/yang/bar-module.yang b/restconf/restconf-nb/src/test/resources/instanceidentifier/yang/bar-module.yang index 90de085472..122834ed23 100644 --- a/restconf/restconf-nb/src/test/resources/instanceidentifier/yang/bar-module.yang +++ b/restconf/restconf-nb/src/test/resources/instanceidentifier/yang/bar-module.yang @@ -7,5 +7,6 @@ module bar-module { /* This container has the same name as container in foo-module */ container foo-bar-container { + presence "has semantic meaning"; } -} \ No newline at end of file +} diff --git a/restconf/restconf-nb/src/test/resources/instanceidentifier/yang/foo-module.yang b/restconf/restconf-nb/src/test/resources/instanceidentifier/yang/foo-module.yang index 16b8e7f355..34b067781c 100644 --- a/restconf/restconf-nb/src/test/resources/instanceidentifier/yang/foo-module.yang +++ b/restconf/restconf-nb/src/test/resources/instanceidentifier/yang/foo-module.yang @@ -6,6 +6,5 @@ module foo-module { } /* This container has the same name as container in bar-module */ - container foo-bar-container { - } -} \ No newline at end of file + container foo-bar-container; +}