Introduce asynchronous RestconfServer.readData() 08/109008/3
authorRobert Varga <robert.varga@pantheon.tech>
Fri, 17 Nov 2023 15:49:45 +0000 (16:49 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Sat, 18 Nov 2023 08:00:58 +0000 (09:00 +0100)
Refactor logic so as to reside in MdsalRestconfServer, interfaced with
RestconfFuture -- which is forcing us to use AsyncResponse.

JIRA: NETCONF-773
JIRA: NETCONF-1155
Change-Id: Idc8207f0c2ac4486f9382d1649131852e76e5965
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/MdsalRestconfServer.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.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/RestconfServer.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImplTest.java

index 13fb7dad9d73fa34f169f3afd2ff41a87976a5bd..15fa365122f720356709363d5844e3bb1fefb248 100644 (file)
@@ -44,9 +44,11 @@ import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
 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.ReadDataParams;
 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
 import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
+import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
@@ -69,6 +71,7 @@ import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
 import org.opendaylight.yangtools.yang.common.XMLNamespace;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
@@ -132,6 +135,39 @@ public final class MdsalRestconfServer implements RestconfServer {
         this(databindProvider, dataBroker, rpcService, actionService, mountPointService, List.of(localRpcs));
     }
 
+    @Override
+    public RestconfFuture<NormalizedNodePayload> dataGET(final ReadDataParams readParams) {
+        return readData(bindRequestRoot(), readParams);
+    }
+
+    @Override
+    public RestconfFuture<NormalizedNodePayload> dataGET(final String identifier, final ReadDataParams readParams) {
+        return readData(bindRequestPath(identifier), readParams);
+    }
+
+    private @NonNull RestconfFuture<NormalizedNodePayload> readData(final InstanceIdentifierContext reqPath,
+            final ReadDataParams readParams) {
+        final var queryParams = QueryParams.newQueryParameters(readParams, reqPath);
+        final var fieldPaths = queryParams.fieldPaths();
+        final var strategy = getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
+        final NormalizedNode node;
+        if (fieldPaths != null && !fieldPaths.isEmpty()) {
+            node = strategy.readData(readParams.content(), reqPath.getInstanceIdentifier(),
+                readParams.withDefaults(), fieldPaths);
+        } else {
+            node = strategy.readData(readParams.content(), reqPath.getInstanceIdentifier(),
+                readParams.withDefaults());
+        }
+        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));
+        }
+
+        return RestconfFuture.of(new NormalizedNodePayload(reqPath.inference(), node, queryParams));
+    }
+
+    // FIXME: should follow the same pattern as operationsPOST() does
     RestconfFuture<DOMActionResult> dataInvokePOST(final InstanceIdentifierContext reqPath,
             final OperationInputBody body) {
         final var yangIIdContext = reqPath.getInstanceIdentifier();
index 738ef9be9035db8b215a02bf95af0d7196c5ad67..03186c9aea985cdaf4d25477ef06a481a51a69b7 100644 (file)
@@ -39,6 +39,7 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.mdsal.dom.api.DOMActionResult;
 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
 import org.opendaylight.restconf.common.patch.PatchContext;
 import org.opendaylight.restconf.common.patch.PatchStatusContext;
 import org.opendaylight.restconf.common.patch.PatchStatusEntity;
@@ -66,7 +67,6 @@ import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
 import org.opendaylight.yangtools.yang.common.Empty;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
-import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
@@ -100,7 +100,7 @@ public final class RestconfDataServiceImpl {
      * Get target data resource from data root.
      *
      * @param uriInfo URI info
-     * @return {@link NormalizedNodePayload}
+     * @param ar {@link AsyncResponse} which needs to be completed
      */
     @GET
     @Path("/data")
@@ -111,9 +111,9 @@ public final class RestconfDataServiceImpl {
         MediaType.APPLICATION_XML,
         MediaType.TEXT_XML
     })
-    public Response readData(@Context final UriInfo uriInfo) {
+    public void dataGET(@Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
         final var readParams = QueryParams.newReadDataParams(uriInfo);
-        return readData(server.bindRequestRoot(), readParams);
+        completeDataGET(server.dataGET(readParams), readParams, ar);
     }
 
     /**
@@ -121,7 +121,7 @@ public final class RestconfDataServiceImpl {
      *
      * @param identifier path to target
      * @param uriInfo URI info
-     * @return {@link NormalizedNodePayload}
+     * @param ar {@link AsyncResponse} which needs to be completed
      */
     @GET
     @Path("/data/{identifier:.+}")
@@ -132,44 +132,32 @@ public final class RestconfDataServiceImpl {
         MediaType.APPLICATION_XML,
         MediaType.TEXT_XML
     })
-    public Response readData(@Encoded @PathParam("identifier") final String identifier,
-            @Context final UriInfo uriInfo) {
+    public void dataGET(@Encoded @PathParam("identifier") final String identifier,
+            @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
         final var readParams = QueryParams.newReadDataParams(uriInfo);
-        return readData(server.bindRequestPath(identifier), readParams);
+        completeDataGET(server.dataGET(identifier, readParams), readParams, ar);
     }
 
-    private Response readData(final InstanceIdentifierContext reqPath, final ReadDataParams readParams) {
-        final var queryParams = QueryParams.newQueryParameters(readParams, reqPath);
-        final var fieldPaths = queryParams.fieldPaths();
-        final var strategy = server.getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
-        final NormalizedNode node;
-        if (fieldPaths != null && !fieldPaths.isEmpty()) {
-            node = strategy.readData(readParams.content(), reqPath.getInstanceIdentifier(),
-                readParams.withDefaults(), fieldPaths);
-        } else {
-            node = strategy.readData(readParams.content(), reqPath.getInstanceIdentifier(),
-                readParams.withDefaults());
-        }
-        if (node == null) {
-            throw new RestconfDocumentedException(
-                "Request could not be completed because the relevant data model content does not exist",
-                ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
-        }
-
-        return switch (readParams.content()) {
-            case ALL, CONFIG -> {
-                final QName type = node.name().getNodeType();
-                yield Response.status(Status.OK)
-                    .entity(new NormalizedNodePayload(reqPath.inference(), node, queryParams))
-                    .header("ETag", '"' + type.getModule().getRevision().map(Revision::toString).orElse(null) + "-"
-                        + type.getLocalName() + '"')
-                    .header("Last-Modified", FORMATTER.format(LocalDateTime.now(Clock.systemUTC())))
-                    .build();
+    private static void completeDataGET(final RestconfFuture<NormalizedNodePayload> future,
+            final ReadDataParams readParams, final AsyncResponse ar) {
+        future.addCallback(new JaxRsRestconfCallback<>(ar) {
+            @Override
+            Response transform(final NormalizedNodePayload result) {
+                return switch (readParams.content()) {
+                    case ALL, CONFIG -> {
+                        final var type = result.data().name().getNodeType();
+                        yield Response.status(Status.OK)
+                            .entity(result)
+                            // FIXME: is this ETag okay?
+                            .header("ETag", '"' + type.getModule().getRevision().map(Revision::toString).orElse(null)
+                                + "-" + type.getLocalName() + '"')
+                            .header("Last-Modified", FORMATTER.format(LocalDateTime.now(Clock.systemUTC())))
+                            .build();
+                    }
+                    case NONCONFIG -> Response.status(Status.OK).entity(result).build();
+                };
             }
-            case NONCONFIG -> Response.status(Status.OK)
-                .entity(new NormalizedNodePayload(reqPath.inference(), node, queryParams))
-                .build();
-        };
+        });
     }
 
     /**
index a755b139e4b6e1f3962157209833b68705f4a43d..f7dcd7bc26e1d59aa1f67c92cdab5cc147f2cbbd 100644 (file)
@@ -614,6 +614,7 @@ public abstract class RestconfStrategy {
      * @param defaultsMode value of with-defaults parameter
      * @return {@link NormalizedNode}
      */
+    // FIXME: NETCONF-1155: this method should asynchronous
     public @Nullable NormalizedNode readData(final @NonNull ContentParam content,
             final @NonNull YangInstanceIdentifier path, final WithDefaultsParam defaultsMode) {
         return switch (content) {
@@ -645,6 +646,7 @@ public abstract class RestconfStrategy {
      * @param fields   paths to selected subtrees which should be read, relative to to the parent path
      * @return {@link NormalizedNode}
      */
+    // FIXME: NETCONF-1155: this method should asynchronous
     public @Nullable NormalizedNode readData(final @NonNull ContentParam content,
             final @NonNull YangInstanceIdentifier path, final @Nullable WithDefaultsParam withDefa,
             final @NonNull List<YangInstanceIdentifier> fields) {
index 4fe667a284aa012d13dc0fbb8dd08e1ae62a73ab..ac3b1479eb7fcac838c2091f7482a9cb3a91f270 100644 (file)
@@ -12,6 +12,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.restconf.api.ApiPath;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
 import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 import org.opendaylight.restconf.server.spi.OperationOutput;
@@ -22,6 +23,23 @@ import org.opendaylight.restconf.server.spi.OperationOutput;
  */
 @NonNullByDefault
 public interface RestconfServer {
+    /**
+     * Return the content of the datastore.
+     *
+     * @param readParams {@link ReadDataParams} for this request
+     * @return A {@link RestconfFuture} of the {@link NormalizedNodePayload} content
+     */
+    RestconfFuture<NormalizedNodePayload> dataGET(ReadDataParams readParams);
+
+    /**
+     * Return the content of a resource.
+     *
+     * @param identifier resource identifier
+     * @param readParams {@link ReadDataParams} for this request
+     * @return A {@link RestconfFuture} of the {@link NormalizedNodePayload} content
+     */
+    RestconfFuture<NormalizedNodePayload> dataGET(String identifier, ReadDataParams readParams);
+
     /**
      * Return the set of supported RPCs supported by {@link #operationsPOST(URI, String, OperationInputBody)}.
      *
index 385bb2952c331a4bcfab6e08ef2df0fb54955878..29cd849a9e4c63a1a01f654b06e67a4e30cb59da 100644 (file)
@@ -12,7 +12,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doNothing;
@@ -26,7 +25,6 @@ import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
-import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
@@ -70,7 +68,6 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
-import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 
@@ -151,8 +148,10 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
                 .read(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
         doReturn(immediateFluentFuture(Optional.empty()))
                 .when(read).read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
-        final Response response = dataService.readData("example-jukebox:jukebox", uriInfo);
-        assertNotNull(response);
+
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+        dataService.dataGET("example-jukebox:jukebox", uriInfo, asyncResponse);
+        final var response = responseCaptor.getValue();
         assertEquals(200, response.getStatus());
         assertEquals(EMPTY_JUKEBOX, ((NormalizedNodePayload) response.getEntity()).data());
     }
@@ -166,15 +165,16 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
         doReturn(immediateFluentFuture(Optional.of(wrapNodeByDataRootContainer(OPER_JUKEBOX))))
                 .when(read)
                 .read(LogicalDatastoreType.OPERATIONAL, YangInstanceIdentifier.of());
-        final Response response = dataService.readData(uriInfo);
-        assertNotNull(response);
+
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+        dataService.dataGET(uriInfo, asyncResponse);
+        final var response = responseCaptor.getValue();
         assertEquals(200, response.getStatus());
 
-        final NormalizedNode data = ((NormalizedNodePayload) response.getEntity()).data();
-        assertTrue(data instanceof ContainerNode);
-        final Collection<DataContainerChild> rootNodes = ((ContainerNode) data).body();
+        final var data = assertInstanceOf(ContainerNode.class, ((NormalizedNodePayload) response.getEntity()).data());
+        final var rootNodes = data.body();
         assertEquals(1, rootNodes.size());
-        final Collection<DataContainerChild> allDataChildren = ((ContainerNode) rootNodes.iterator().next()).body();
+        final var allDataChildren = assertInstanceOf(ContainerNode.class, rootNodes.iterator().next()).body();
         assertEquals(3, allDataChildren.size());
     }
 
@@ -197,19 +197,17 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
         doReturn(immediateFluentFuture(Optional.of(OPER_JUKEBOX))).when(read)
                 .read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
 
-        final Response response = dataService.readData(
-                "example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox", uriInfo);
-
-        assertNotNull(response);
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+        dataService.dataGET("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox", uriInfo, asyncResponse);
+        final var response = responseCaptor.getValue();
         assertEquals(200, response.getStatus());
 
         // response must contain all child nodes from config and operational containers merged in one container
-        final NormalizedNode data = ((NormalizedNodePayload) response.getEntity()).data();
-        assertTrue(data instanceof ContainerNode);
-        assertEquals(3, ((ContainerNode) data).size());
-        assertNotNull(((ContainerNode) data).childByArg(CONT_PLAYER.name()));
-        assertNotNull(((ContainerNode) data).childByArg(LIBRARY_NID));
-        assertNotNull(((ContainerNode) data).childByArg(PLAYLIST_NID));
+        final var data = assertInstanceOf(ContainerNode.class, ((NormalizedNodePayload) response.getEntity()).data());
+        assertEquals(3, data.size());
+        assertNotNull(data.childByArg(CONT_PLAYER.name()));
+        assertNotNull(data.childByArg(LIBRARY_NID));
+        assertNotNull(data.childByArg(PLAYLIST_NID));
     }
 
     @Test
@@ -220,8 +218,11 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
         doReturn(immediateFluentFuture(Optional.empty()))
                 .when(read).read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
 
-        final var errors = assertThrows(RestconfDocumentedException.class,
-            () -> dataService.readData("example-jukebox:jukebox", uriInfo)).getErrors();
+        final var rdeCaptor = ArgumentCaptor.forClass(RestconfDocumentedException.class);
+        doReturn(true).when(asyncResponse).resume(rdeCaptor.capture());
+        dataService.dataGET("example-jukebox:jukebox", uriInfo, asyncResponse);
+
+        final var errors = rdeCaptor.getValue().getErrors();
         assertEquals(1, errors.size());
         final var error = errors.get(0);
         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
@@ -242,20 +243,20 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
         doReturn(immediateFluentFuture(Optional.of(CONFIG_JUKEBOX))).when(read)
                 .read(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
 
-        final Response response = dataService.readData("example-jukebox:jukebox", uriInfo);
-
-        assertNotNull(response);
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+        dataService.dataGET("example-jukebox:jukebox", uriInfo, asyncResponse);
+        final var response = responseCaptor.getValue();
         assertEquals(200, response.getStatus());
 
         // response must contain only config data
-        final NormalizedNode data = ((NormalizedNodePayload) response.getEntity()).data();
+        final var data = assertInstanceOf(ContainerNode.class, ((NormalizedNodePayload) response.getEntity()).data());
 
         // config data present
-        assertNotNull(((ContainerNode) data).childByArg(CONT_PLAYER.name()));
-        assertNotNull(((ContainerNode) data).childByArg(LIBRARY_NID));
+        assertNotNull(data.childByArg(CONT_PLAYER.name()));
+        assertNotNull(data.childByArg(LIBRARY_NID));
 
         // state data absent
-        assertNull(((ContainerNode) data).childByArg(PLAYLIST_NID));
+        assertNull(data.childByArg(PLAYLIST_NID));
     }
 
     /**
@@ -270,20 +271,21 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
         doReturn(immediateFluentFuture(Optional.of(OPER_JUKEBOX))).when(read)
                 .read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
 
-        final Response response = dataService.readData("example-jukebox:jukebox", uriInfo);
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+        dataService.dataGET("example-jukebox:jukebox", uriInfo, asyncResponse);
+        final var response = responseCaptor.getValue();
 
-        assertNotNull(response);
         assertEquals(200, response.getStatus());
 
         // response must contain only operational data
-        final NormalizedNode data = ((NormalizedNodePayload) response.getEntity()).data();
+        final var data = assertInstanceOf(ContainerNode.class, ((NormalizedNodePayload) response.getEntity()).data());
 
         // state data present
-        assertNotNull(((ContainerNode) data).childByArg(CONT_PLAYER.name()));
-        assertNotNull(((ContainerNode) data).childByArg(PLAYLIST_NID));
+        assertNotNull(data.childByArg(CONT_PLAYER.name()));
+        assertNotNull(data.childByArg(PLAYLIST_NID));
 
         // config data absent
-        assertNull(((ContainerNode) data).childByArg(LIBRARY_NID));
+        assertNull(data.childByArg(LIBRARY_NID));
     }
 
     @Test