Eliminate NormalizedNodePayload 59/111359/4
authorRobert Varga <robert.varga@pantheon.tech>
Thu, 11 Apr 2024 19:18:07 +0000 (21:18 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Thu, 11 Apr 2024 22:00:34 +0000 (00:00 +0200)
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 <robert.varga@pantheon.tech>
27 files changed:
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JaxRsRestconf.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/AbstractNormalizedNodeBodyWriter.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyWriter.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyWriter.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/NormalizedNodePayload.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/legacy/WriterParameters.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalNormalizedNodeWriterFactory.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetResult.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/RestconfServer.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DataFormattableBody.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/DefaultNormalizedNodeWriterFactory.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/MaxDepthNormalizedNodeWriterFactory.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/NormalizedFormattableBody.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/NormalizedNodeWriterFactory.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/RootFormattableBody.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/YangLibraryVersionResource.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/AbstractRestconfTest.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/jaxrs/RestconfDataGetTest.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/AbstractInstanceIdentifierTest.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyWriterTest.java [deleted file]
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/spi/NormalizedFormattableBodyTest.java [new file with mode: 0644]
restconf/restconf-nb/src/test/resources/instanceidentifier/yang/bar-module.yang
restconf/restconf-nb/src/test/resources/instanceidentifier/yang/foo-module.yang

index d018f0f79bae18840389b771daa66b970dab255b..8e8135ad4ef4d803d8a79d46d6aadaa66170e2cc 100644 (file)
@@ -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<DataGetResult> future, final AsyncResponse ar) {
+    @NonNullByDefault
+    private static void completeDataGET(final RestconfFuture<DataGetResult> 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
index 2e78aba2d0f3d2444efa1db5fa83a2af70124c5f..965204df032cf59b9f615074550868e1473dd4d2 100644 (file)
@@ -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<Class<?>> getClasses() {
-                            return Set.of(JsonNormalizedNodeBodyWriter.class, XmlNormalizedNodeBodyWriter.class);
-                        }
-
                         @Override
                         public Set<Object> 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 (file)
index 8bf81af..0000000
+++ /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<NormalizedNodePayload> {
-    @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<String, Object> 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 (file)
index 6bb73b1..0000000
+++ /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 (file)
index 8da763c..0000000
+++ /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 (file)
index 4e62f79..0000000
+++ /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 (file)
index 3e8a8b7..0000000
+++ /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<Set<QName>> 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 (file)
index 0000000..f2b63ca
--- /dev/null
@@ -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<Set<QName>> fields;
+
+    MdsalNormalizedNodeWriterFactory(final List<Set<QName>> 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);
+    }
+}
index 06a68f3dd93e44caa3696567d0b4e469fead5834..01c34331724b871aac37a0a709e866095630d1bf 100644 (file)
@@ -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<DataGetResult> 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
index e06d3ea265f4b6c2a59a52dac7f242bb1c89eac8..fe5ec4b5d31de6024f69119a3160fc569dad6e85 100644 (file)
@@ -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<DataGetResult> dataGET(final ServerRequest request, final Data path, final DataGetParams params) {
-        final var inference = path.inference();
         final var fields = params.fields();
         final List<YangInstanceIdentifier> fieldPaths;
         if (fields != null) {
             final List<YangInstanceIdentifier> 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
index a091c955bf0debfed2cc55ad65743de47efb2b44..074f466284b637f31de40bb5f6c2a74cd9616411 100644 (file)
@@ -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<DataGetResult> dataGET(ServerRequest request, Data path, DataGetParams params);
 
-    static final @NonNull RestconfFuture<DataGetResult> completeDataGET(final PrettyPrintParam prettyPrint,
-            final Inference inference, final WriterParameters writerParams, final @Nullable NormalizedNode node,
-            final @Nullable ConfigurationMetadata metadata) {
+    @NonNullByDefault
+    static final RestconfFuture<DataGetResult> 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()));
     }
 
     /**
index 3a8ed7ba9e1fbdc8bc164adc64f5015f23e61619..5664c4bb7b7f1c7ce44f6143339bcabc5b084fc9 100644 (file)
@@ -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
  * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.3">RFC8040 section 4.3</a>.
  *
- * @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);
     }
 }
index c4d637d95ae760a037ddc70b3268a3a9b13abfbe..0385dd611c2a51a45036394e65a7a6143e510e32 100644 (file)
@@ -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 {
      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.3.3">RFC8040 {+restconf}/yang-library-version</a>.
      *
      * @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<FormattableBody> 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 (file)
index 0000000..4f4b14f
--- /dev/null
@@ -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<N extends NormalizedNode> extends NormalizedFormattableBody<N> {
+    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<? extends DataTreeEffectiveStatement<?>> 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 (file)
index 0000000..0cc6e1b
--- /dev/null
@@ -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 (file)
index 0000000..b5cf1ef
--- /dev/null
@@ -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
index 9ccd06efb4bb1b54beedee402141abed3fc0ef4a..f11030bc29b57239b70d5fce684094b25e3777a6 100644 (file)
  */
 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<N extends NormalizedNode> extends DatabindFormattableBody {
-    private final Inference parent;
+public abstract sealed class NormalizedFormattableBody<N extends NormalizedNode> 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<? extends DataTreeEffectiveStatement<?>> 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 (file)
index 0000000..8a8975e
--- /dev/null
@@ -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 (file)
index 0000000..b8dfe47
--- /dev/null
@@ -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<ContainerNode> {
+    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();
+    }
+}
index 3b567fbe039ffadbdebc8fc437497720cb283dc8..99f38045bf5296dce0b02baeb335d7f93ea61a4a 100644 (file)
@@ -71,7 +71,7 @@ public record YangLibraryVersionResource(DatabindContext databind, Inference res
 
     @Override
     public RestconfFuture<FormattableBody> httpGET(final ServerRequest request) {
-        return RestconfFuture.of(new NormalizedFormattableBody<>(databind, restconf, leaf));
+        return RestconfFuture.of(new DataFormattableBody<>(databind, restconf, leaf));
     }
 
     @Override
index 2520e9b0c51d2e2440aa7b89bc90e25ae2204bfb..5632434bfe4e8cdd051f672e9adb8554dc62e655 100644 (file)
@@ -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 <N extends NormalizedNode> NormalizedFormattableBody<N> assertNormalizedBody(final int status,
+            final Consumer<AsyncResponse> invocation) {
+        return assertInstanceOf(NormalizedFormattableBody.class, assertFormattableBody(status, invocation));
+    }
+
     static final FormattableBody assertFormattableBody(final int status, final Consumer<AsyncResponse> 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<AsyncResponse> invocation) {
-        return assertNormalizedNodePayload(status, invocation).data();
-    }
-
-    static final NormalizedNodePayload assertNormalizedNodePayload(final int status,
-            final Consumer<AsyncResponse> invocation) {
-        return assertEntity(NormalizedNodePayload.class, status, invocation);
-    }
-
     static final <T> T assertEntity(final Class<T> expectedType, final int expectedStatus,
             final Consumer<AsyncResponse> invocation) {
         return assertInstanceOf(expectedType, assertEntity(expectedStatus, invocation));
index ff75a2c7eaa134f28689d882178d3c50884ae08c..74848baf1ddf9ca5aaed8bccaea12d88af781f21 100644 (file)
@@ -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("""
+            <jukebox xmlns="http://example.com/ns/example-jukebox">
+              <player>
+                <gap>0.2</gap>
+              </player>
+            </jukebox>""", 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("""
+            <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+              <jukebox xmlns="http://example.com/ns/example-jukebox">
+                <library/>
+                <player>
+                  <gap>0.2</gap>
+                </player>
+              </jukebox>
+            </data>""", 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("""
+            <jukebox xmlns="http://example.com/ns/example-jukebox">
+              <library/>
+              <player>
+                <gap>0.2</gap>
+              </player>
+            </jukebox>""", 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("""
+            <jukebox xmlns="http://example.com/ns/example-jukebox">
+              <library/>
+              <player>
+                <gap>0.2</gap>
+              </player>
+            </jukebox>""", 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("""
+            <jukebox xmlns="http://example.com/ns/example-jukebox">
+              <player>
+                <gap>0.2</gap>
+              </player>
+            </jukebox>""", body::formatToXML, true);
     }
 }
index 2b4753bc886fef63ad5f2fd3d9f9d16ab10a4bd1..1aef225e80669fc575415c72a29e89297752ecb4 100644 (file)
@@ -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 (file)
index c19e53c..0000000
+++ /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("<data xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"></data>",
-            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("""
-            <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">\
-            <foo-bar-container xmlns="bar:module"></foo-bar-container>\
-            <foo-bar-container xmlns="foo:module"></foo-bar-container>\
-            </data>""", 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 (file)
index 0000000..3758eef
--- /dev/null
@@ -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("<data xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"></data>",
+            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("""
+            <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">\
+            <foo-bar-container xmlns="bar:module"></foo-bar-container>\
+            <foo-bar-container xmlns="foo:module"></foo-bar-container>\
+            </data>""", body::formatToXML, false);
+        AbstractJukeboxTest.assertFormat("""
+            {"bar-module:foo-bar-container":{}}""", body::formatToJSON, false);
+    }
+}
\ No newline at end of file
index 90de08547271eaaebcbb9726e8d0960562ec271f..122834ed23b333b0ba4bc6ac7577539e4232ef20 100644 (file)
@@ -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
+}
index 16b8e7f3551e2a611de8b1cca21548bfde2d83a3..34b067781cb03ebc9574fdf1920cd8f722f03169 100644 (file)
@@ -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;
+}