Refactor OperationsContent 02/109002/5
authorRobert Varga <robert.varga@pantheon.tech>
Thu, 16 Nov 2023 23:26:47 +0000 (00:26 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Sat, 18 Nov 2023 08:00:58 +0000 (09:00 +0100)
Turn the enumeration into a plain DTO which can serialize itself into
XML/JSON -- allowing it to be promoted to restconf.server.api.

While we are in the area, fix the semantics here:
- do not consider ActionEffectiveStatement, as that is handled through
  a different service
- report 404 when the operation points to a non-RPC

JIRA: NETCONF-773
Change-Id: I69fd5865c89ea2b727befdf4cedb8cddf8418002
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/OperationsContent.java [deleted file]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfOperationsServiceImpl.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationsContent.java [new file with mode: 0644]
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/Netconf822Test.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfOperationsServiceImplTest.java

index daa56217736d6248421c2e90b2fb4e3b3fa3bbc6..1c2ab7b39cdf7207dd3e95e8cfec98bca9c0b4ab 100644 (file)
@@ -11,14 +11,18 @@ import static com.google.common.base.Verify.verifyNotNull;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Maps;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.lang.invoke.VarHandle;
 import java.net.URI;
+import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
+import java.util.Map.Entry;
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import org.eclipse.jdt.annotation.NonNull;
@@ -37,6 +41,7 @@ import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
+import org.opendaylight.restconf.server.api.OperationsContent;
 import org.opendaylight.restconf.server.api.RestconfServer;
 import org.opendaylight.restconf.server.spi.OperationInput;
 import org.opendaylight.restconf.server.spi.OperationOutput;
@@ -47,13 +52,14 @@ import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.librar
 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.QNameModule;
+import org.opendaylight.yangtools.yang.common.Revision;
+import org.opendaylight.yangtools.yang.common.XMLNamespace;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 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.ActionEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
-import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
@@ -103,10 +109,10 @@ public final class MdsalRestconfServer implements RestconfServer {
         this.localRpcs = Maps.uniqueIndex(localRpcs, RpcImplementation::qname);
     }
 
-    public MdsalRestconfServer(final DatabindProvider databind, final DOMDataBroker dataBroker,
+    public MdsalRestconfServer(final DatabindProvider databindProvider, final DOMDataBroker dataBroker,
             final DOMRpcService rpcService, final DOMMountPointService mountPointService,
             final RpcImplementation... localRpcs) {
-        this(databind, dataBroker, rpcService, mountPointService, List.of(localRpcs));
+        this(databindProvider, dataBroker, rpcService, mountPointService, List.of(localRpcs));
     }
 
     @NonNull InstanceIdentifierContext bindRequestPath(final String identifier) {
@@ -132,52 +138,57 @@ public final class MdsalRestconfServer implements RestconfServer {
     }
 
     @Override
-    public String operationsGET(final OperationsContent contentType) {
-        return operationsGET(contentType, bindRequestRoot().inference());
+    public OperationsContent operationsGET() {
+        return operationsGET(databindProvider.currentContext().modelContext());
     }
 
     @Override
-    public String operationsGET(final OperationsContent contentType, final String operation) {
-        return operationsGET(contentType, bindRequestPath(operation).inference());
+    public OperationsContent operationsGET(final String operation) {
+        // get current module RPCs/actions by RPC/action name
+        final var inference = bindRequestPath(operation).inference();
+        if (inference.isEmpty()) {
+            return operationsGET(inference.getEffectiveModelContext());
+        }
+
+        final var stmt = inference.toSchemaInferenceStack().currentStatement();
+        if (stmt instanceof RpcEffectiveStatement rpc) {
+            final var qname = rpc.argument();
+            return new OperationsContent(inference.getEffectiveModelContext(),
+                ImmutableSetMultimap.of(qname.getModule(), qname));
+        }
+        LOG.debug("Operation '{}' resulted in non-RPC {}", operation, stmt);
+        return null;
     }
 
-    @VisibleForTesting
-    static @NonNull String operationsGET(final OperationsContent contentType, final @NonNull Inference inference) {
-        final var modelContext = inference.getEffectiveModelContext();
-        if (modelContext.getModuleStatements().isEmpty()) {
+    private static @NonNull OperationsContent operationsGET(final EffectiveModelContext modelContext) {
+        final var modules = modelContext.getModuleStatements();
+        if (modules.isEmpty()) {
             // No modules, or defensive return empty content
-            return contentType.emptyBody;
+            return new OperationsContent(modelContext, ImmutableSetMultimap.of());
         }
-        if (inference.isEmpty()) {
-            // empty stack == get all RPCs/actions
-            return contentType.createBody(contentType.getModuleRpcs(modelContext, modelContext.getModuleStatements()));
-        }
-
-        // get current module RPCs/actions by RPC/action name
-        final var stack = inference.toSchemaInferenceStack();
-        final var currentModule = stack.currentModule();
-        final var currentModuleKey = Map.of(currentModule.localQNameModule(), currentModule);
 
-        final QName qname;
-        final var stmt = stack.currentStatement();
-        if (stmt instanceof RpcEffectiveStatement rpc) {
-            qname = rpc.argument();
-        } else if (stmt instanceof ActionEffectiveStatement action) {
-            qname = action.argument();
-        } else {
-            throw new IllegalArgumentException("Unhandled statement " + stmt);
+        // RPCs by their XMLNamespace/Revision
+        final var table = HashBasedTable.<XMLNamespace, Revision, ImmutableSet<QName>>create();
+        for (var entry : modules.entrySet()) {
+            final var module = entry.getValue();
+            final var rpcNames = module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
+                .map(RpcEffectiveStatement::argument)
+                .collect(ImmutableSet.toImmutableSet());
+            if (!rpcNames.isEmpty()) {
+                final var namespace = entry.getKey();
+                table.put(namespace.getNamespace(), namespace.getRevision().orElse(null), rpcNames);
+            }
         }
 
-        final var operName = qname.getLocalName();
-        // FIXME: This is weird: it only handles rpc statements, not action statements. What is going on here?!
-        //        There is a reason this sort of method should handle both RPCs and actions, which is the invocation
-        //        remapping -- e.g. RFC8528 specifies how 'action' invocation is mappend to 'rpc' invocation.
-        //        There is something fishy going on here and we either have a bug, or the spec needs to be clarified.
-        return contentType.getModuleRpcs(modelContext, currentModuleKey).stream()
-            .findFirst()
-            .map(e -> Map.entry(e.getKey(), e.getValue().stream().filter(operName::equals).toList()))
-            .map(e -> contentType.createBody(List.of(e)))
-            .orElse(contentType.emptyBody);
+        // Now pick the latest revision for each namespace
+        final var rpcs = ImmutableSetMultimap.<QNameModule, QName>builder();
+        for (var entry : table.rowMap().entrySet()) {
+            entry.getValue().entrySet().stream()
+                .sorted(Comparator.comparing(Entry::getKey, (first, second) -> Revision.compare(second, first)))
+                .findFirst()
+                .ifPresent(row -> rpcs.putAll(QNameModule.create(entry.getKey(), row.getKey()), row.getValue()));
+        }
+        return new OperationsContent(modelContext, rpcs.build());
     }
 
     @Override
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/OperationsContent.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/OperationsContent.java
deleted file mode 100644 (file)
index 443d49e..0000000
+++ /dev/null
@@ -1,128 +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.rests.services.impl;
-
-import static java.util.Objects.requireNonNull;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import org.eclipse.jdt.annotation.NonNull;
-import org.opendaylight.yangtools.yang.common.QNameModule;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
-import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
-
-/**
- * RESTCONF {@code /operations} content for a {@code GET} operation as per
- * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.3.2">RFC8040</a>.
- */
-public enum OperationsContent {
-    JSON("{ \"ietf-restconf:operations\" : { } }") {
-        @Override
-        String createBody(final List<Entry<String, List<String>>> rpcsByPrefix) {
-            final var sb = new StringBuilder("{\n"
-                + "  \"ietf-restconf:operations\" : {\n");
-            var entryIt = rpcsByPrefix.iterator();
-            var entry = entryIt.next();
-            var nameIt = entry.getValue().iterator();
-            while (true) {
-                sb.append("    \"").append(entry.getKey()).append(':').append(nameIt.next()).append("\": [null]");
-                if (nameIt.hasNext()) {
-                    sb.append(",\n");
-                    continue;
-                }
-
-                if (entryIt.hasNext()) {
-                    sb.append(",\n");
-                    entry = entryIt.next();
-                    nameIt = entry.getValue().iterator();
-                    continue;
-                }
-
-                break;
-            }
-
-            return sb.append("\n  }\n}").toString();
-        }
-
-        @Override
-        String prefix(final ModuleEffectiveStatement module) {
-            return module.argument().getLocalName();
-        }
-    },
-
-    XML("<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"/>") {
-        @Override
-        String createBody(final List<Entry<String, List<String>>> rpcsByPrefix) {
-            // Header with namespace declarations for each module
-            final var sb = new StringBuilder("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
-                + "<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"");
-            for (int i = 0; i < rpcsByPrefix.size(); ++i) {
-                final var prefix = "ns" + i;
-                sb.append("\n            xmlns:").append(prefix).append("=\"").append(rpcsByPrefix.get(i).getKey())
-                    .append("\"");
-            }
-            sb.append(" >");
-
-            // Second pass: emit all leaves
-            for (int i = 0; i < rpcsByPrefix.size(); ++i) {
-                final var prefix = "ns" + i;
-                for (var localName : rpcsByPrefix.get(i).getValue()) {
-                    sb.append("\n  <").append(prefix).append(':').append(localName).append("/>");
-                }
-            }
-
-            return sb.append("\n</operations>").toString();
-        }
-
-        @Override
-        String prefix(final ModuleEffectiveStatement module) {
-            return module.localQNameModule().getNamespace().toString();
-        }
-    };
-
-    public final @NonNull String emptyBody;
-
-    OperationsContent(final String emptyBody) {
-        this.emptyBody = requireNonNull(emptyBody);
-    }
-
-    /**
-     * Returns a list of entries, where each entry contains a module prefix and a list of RPC names.
-     *
-     * @param context the effective model context
-     * @param modules the map of QNameModule to ModuleEffectiveStatement
-     * @return a list of entries, where each entry contains a module prefix and a list of RPC names
-     */
-    public List<Entry<@NonNull String, List<String>>> getModuleRpcs(final EffectiveModelContext context,
-            final Map<QNameModule, ModuleEffectiveStatement> modules) {
-        return modules.values().stream()
-                // Extract XMLNamespaces
-                .map(module -> module.localQNameModule().getNamespace())
-                // Make sure each is XMLNamespace unique
-                .distinct()
-                // Find the most recent module with that namespace. This needed so we expose the right set of RPCs,
-                // as we always pick the latest revision to resolve prefix (or module name).
-                .map(namespace -> context.findModuleStatements(namespace).iterator().next())
-                // Convert to module prefix + List<String> with RPC names
-                .map(module -> Map.entry(prefix(module),
-                        module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
-                        .map(rpc -> rpc.argument().getLocalName())
-                        .toList()))
-                // Skip prefixes which do not have any RPCs
-                .filter(entry -> !entry.getValue().isEmpty())
-                // Ensure stability: sort by prefix
-                .sorted(Entry.comparingByKey())
-                .toList();
-    }
-
-    abstract @NonNull String createBody(List<Entry<String, List<String>>> rpcsByPrefix);
-
-    abstract @NonNull String prefix(ModuleEffectiveStatement module);
-}
index bce5f4fc909051f3e13a431809c26e61b8df9fbb..e1db1b4498627c8fbb6a3cb5ad1403b348fe9855 100644 (file)
@@ -13,6 +13,7 @@ import java.io.InputStream;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.Encoded;
 import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
@@ -23,12 +24,14 @@ import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
+import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
 import org.opendaylight.restconf.nb.rfc8040.databind.JsonOperationInputBody;
 import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
 import org.opendaylight.restconf.nb.rfc8040.databind.XmlOperationInputBody;
 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
+import org.opendaylight.restconf.server.api.OperationsContent;
 import org.opendaylight.restconf.server.spi.OperationOutput;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 
@@ -57,20 +60,20 @@ public final class RestconfOperationsServiceImpl {
     @Path("/operations")
     @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
     public String getOperationsJSON() {
-        return server.operationsGET(OperationsContent.JSON);
+        return server.operationsGET().toJSON();
     }
 
     /**
      * Retrieve list of operations and actions supported by the server or device in JSON format.
      *
-     * @param identifier path parameter to identify device and/or operation
+     * @param operation path parameter to identify device and/or operation
      * @return A string containing a JSON document conforming to both RFC8040 and RFC7951.
      */
     @GET
-    @Path("/operations/{identifier:.+}")
+    @Path("/operations/{operation:.+}")
     @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
-    public String getOperationJSON(@PathParam("identifier") final String identifier) {
-        return server.operationsGET(OperationsContent.JSON, identifier);
+    public String getOperationJSON(@PathParam("operation") final String operation) {
+        return operationsGET(operation).toJSON();
     }
 
     /**
@@ -82,20 +85,28 @@ public final class RestconfOperationsServiceImpl {
     @Path("/operations")
     @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
     public String operationsGetXML() {
-        return server.operationsGET(OperationsContent.XML);
+        return server.operationsGET().toXML();
     }
 
     /**
      * Retrieve list of operations and actions supported by the server or device in XML format.
      *
-     * @param identifier path parameter to identify device and/or operation
+     * @param operation path parameter to identify device and/or operation
      * @return A string containing an XML document conforming to both RFC8040 section 11.3.1 and page 84.
      */
     @GET
-    @Path("/operations/{identifier:.+}")
+    @Path("/operations/{operation:.+}")
     @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
-    public String operationsGetXML(@PathParam("identifier") final String identifier) {
-        return server.operationsGET(OperationsContent.XML, identifier);
+    public String operationsGetXML(@PathParam("operation") final String operation) {
+        return operationsGET(operation).toXML();
+    }
+
+    private @NonNull OperationsContent operationsGET(final String operation) {
+        final var content = server.operationsGET(operation);
+        if (content == null) {
+            throw new NotFoundException();
+        }
+        return content;
     }
 
     /**
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationsContent.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/OperationsContent.java
new file mode 100644 (file)
index 0000000..c115410
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.server.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableSetMultimap;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+
+/**
+ * RESTCONF {@code /operations} content for a {@code GET} operation as per
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.3.2">RFC8040</a>.
+ */
+@NonNullByDefault
+public record OperationsContent(
+        EffectiveModelContext modelContext,
+        ImmutableSetMultimap<QNameModule, QName> operations) {
+    public OperationsContent {
+        requireNonNull(modelContext);
+        requireNonNull(operations);
+    }
+
+    public String toJSON() {
+        final var sb = new StringBuilder("""
+            {
+              "ietf-restconf:operations" : {\
+            """);
+
+        if (!operations.isEmpty()) {
+            final var entryIt = operations.asMap().entrySet().stream()
+                .map(entry -> Map.entry(
+                    modelContext.findModuleStatement(entry.getKey()).orElseThrow().argument().getLocalName(),
+                    entry.getValue()))
+                .sorted(Comparator.comparing(Entry::getKey))
+                .iterator();
+            var entry = entryIt.next();
+            var nameIt = entry.getValue().iterator();
+            while (true) {
+                final var rpcName = nameIt.next().getLocalName();
+                sb.append("\n    \"").append(entry.getKey()).append(':').append(rpcName).append("\": [null]");
+                if (nameIt.hasNext()) {
+                    sb.append(',');
+                    continue;
+                }
+
+                if (entryIt.hasNext()) {
+                    sb.append(',');
+                    entry = entryIt.next();
+                    nameIt = entry.getValue().iterator();
+                    continue;
+                }
+
+                break;
+            }
+        }
+
+        return sb.append("\n  }\n}").toString();
+    }
+
+    public String toXML() {
+        // Header with namespace declarations for each module
+        final var sb = new StringBuilder("""
+            <?xml version="1.0" encoding="UTF-8"?>
+            <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"\
+            """);
+        if (operations.isEmpty()) {
+            return sb.append("/>").toString();
+        }
+
+        // We perform two passes:
+        // - first we emit namespace declarations
+        // - then we emit individual leaves
+        final var entries = operations.asMap().entrySet().stream()
+            .sorted(Comparator.comparing(Entry::getKey))
+            .toList();
+
+        for (int i = 0; i < entries.size(); ++i) {
+            sb.append("\n            xmlns:ns").append(i).append("=\"").append(entries.get(i).getKey().getNamespace())
+                .append('"');
+        }
+        sb.append('>');
+
+        for (int i = 0; i < entries.size(); ++i) {
+            for (var rpc : entries.get(i).getValue()) {
+                sb.append("\n  <ns").append(i).append(':').append(rpc.getLocalName()).append("/>");
+            }
+        }
+
+        return sb.append("\n</operations>").toString();
+    }
+}
index 435959c55c85d577d2268bbb5df7bfa79bbb6041..92f57ad1798455a345bbd5be012a7383ce7b3a02 100644 (file)
@@ -9,11 +9,11 @@ package org.opendaylight.restconf.server.api;
 
 import java.net.URI;
 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.databind.OperationInputBody;
 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
-import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.OperationsContent;
 import org.opendaylight.restconf.server.spi.OperationOutput;
 
 /**
@@ -36,24 +36,22 @@ public interface RestconfServer {
     /**
      * Return the set of supported RPCs supported by {@link #operationsPOST(URI, String, OperationInputBody)}.
      *
-     * @param contentType Formatting type
-     * @return A formatted string
+     * @return An {@link OperationsContent}
      */
-    String operationsGET(OperationsContent contentType);
+    OperationsContent operationsGET();
 
     /*
      * Return the details about a particular operation supported by
      * {@link #operationsPOST(URI, String, OperationInputBody)}, as expressed in the
      * <a href="https://www.rfc-editor.org/rfc/rfc8040#page-84>RFC8040<a> {@code container operations} statement.
      *
-     * @param contentType Formatting type
      * @param operation An operation
-     * @return A formatted string
+     * @return An {@link OperationsContent}, or {@code null} if {@code operation} does not point to an {@code rpc}
      */
     // FIXME: 'operation' should really be an ApiIdentifier with non-null module, but we also support ang-ext:mount,
     //        and hence it is a path right now
     // FIXME: use ApiPath instead of String
-    String operationsGET(OperationsContent contentType, String operation);
+    @Nullable OperationsContent operationsGET(String operation);
 
     /**
      * Invoke an RPC operation, as defined in
index 1f0ba07a667f6df7c5aebdab87486b1c7b9caa6f..1fbda387766884d9bcbc4803a72c06f8f13ae073 100644 (file)
@@ -7,71 +7,74 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
-import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
+import org.eclipse.jdt.annotation.NonNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.opendaylight.mdsal.dom.api.DOMDataBroker;
+import org.opendaylight.mdsal.dom.api.DOMMountPointService;
+import org.opendaylight.mdsal.dom.api.DOMRpcService;
+import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
 
-public class Netconf822Test {
-    private static final Absolute NEW1 = Absolute.of(QName.create("foo", "2021-09-30", "new1"));
+@ExtendWith(MockitoExtension.class)
+class Netconf822Test {
+    private static final @NonNull DatabindContext DATABIND =
+        DatabindContext.ofModel(YangParserTestUtils.parseYangResourceDirectory("/nc822"));
 
-    private static EffectiveModelContext SCHEMA;
+    @Mock
+    private DOMDataBroker dataBroker;
+    @Mock
+    private DOMRpcService rpcService;
+    @Mock
+    private DOMMountPointService mountPointService;
 
-    @BeforeClass
-    public static void beforeClass() {
-        SCHEMA = YangParserTestUtils.parseYangResourceDirectory("/nc822");
+    private MdsalRestconfServer server;
+
+    @BeforeEach
+    void beforeEach() {
+        server = new MdsalRestconfServer(() -> DATABIND, dataBroker, rpcService, mountPointService);
     }
 
     @Test
-    public void testOperationsContentJSON() {
+    void testOperationsContent() {
+        final var content = server.operationsGET();
         assertEquals("""
             {
               "ietf-restconf:operations" : {
                 "foo:new": [null],
                 "foo:new1": [null]
               }
-            }""",
-            MdsalRestconfServer.operationsGET(OperationsContent.JSON, SchemaInferenceStack.of(SCHEMA).toInference()));
-    }
-
-    @Test
-    public void testOperationsContentByIdentifierJSON() {
-        assertEquals("""
-            {
-              "ietf-restconf:operations" : {
-                "foo:new1": [null]
-              }
-            }""",
-            MdsalRestconfServer.operationsGET(OperationsContent.JSON,
-                SchemaInferenceStack.of(SCHEMA, NEW1).toInference()));
-    }
-
-    @Test
-    public void testOperationsContentXML() {
+            }""", content.toJSON());
         assertEquals("""
             <?xml version="1.0" encoding="UTF-8"?>
             <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"
-                        xmlns:ns0="foo" >
+                        xmlns:ns0="foo">
               <ns0:new/>
               <ns0:new1/>
-            </operations>""",
-            MdsalRestconfServer.operationsGET(OperationsContent.XML, SchemaInferenceStack.of(SCHEMA).toInference()));
+            </operations>""", content.toXML());
     }
 
     @Test
-    public void testOperationsContentByIdentifierXML() {
+    void testOperationsContentByIdentifier() {
+        final var content = server.operationsGET("foo:new1");
+        assertNotNull(content);
+        assertEquals("""
+            {
+              "ietf-restconf:operations" : {
+                "foo:new1": [null]
+              }
+            }""", content.toJSON());
         assertEquals("""
             <?xml version="1.0" encoding="UTF-8"?>
             <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"
-                        xmlns:ns0="foo" >
+                        xmlns:ns0="foo">
               <ns0:new1/>
-            </operations>""",
-            MdsalRestconfServer.operationsGET(OperationsContent.XML,
-                SchemaInferenceStack.of(SCHEMA, NEW1).toInference()));
+            </operations>""", content.toXML());
     }
 }
index e50b171744573509c748b13088ee7a68d580d74a..c2eaa9961396c453a98f24060f1bc567471d295a 100644 (file)
@@ -49,7 +49,7 @@ public class RestconfOperationsServiceImplTest {
         <?xml version="1.0" encoding="UTF-8"?>
         <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"
                     xmlns:ns0="module:1"
-                    xmlns:ns1="module:2" >
+                    xmlns:ns1="module:2">
           <ns0:dummy-rpc1-module1/>
           <ns0:dummy-rpc2-module1/>
           <ns1:dummy-rpc1-module2/>
@@ -129,7 +129,7 @@ public class RestconfOperationsServiceImplTest {
         assertEquals("""
             <?xml version="1.0" encoding="UTF-8"?>
             <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"
-                        xmlns:ns0="module:1" >
+                        xmlns:ns0="module:1">
               <ns0:dummy-rpc1-module1/>
             </operations>""", operationXML);
     }