--- /dev/null
+/*
+ * 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.common;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.HashBasedTable;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.common.Revision;
+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://datatracker.ietf.org/doc/html/rfc8040#section-3.3.2">RFC8040</a>.
+ */
+// FIXME: when bierman02 is gone, this should be folded to nb-rfc8040, as it is a server-side thing.
+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();
+ }
+ };
+
+ private final @NonNull String emptyBody;
+
+ OperationsContent(final String emptyBody) {
+ this.emptyBody = requireNonNull(emptyBody);
+ }
+
+ /**
+ * Return the content for a particular {@link EffectiveModelContext}.
+ *
+ * @param context Context to use
+ * @return Content of HTTP GET operation as a String
+ */
+ public final @NonNull String bodyFor(final @Nullable EffectiveModelContext context) {
+ if (context == null) {
+ return emptyBody;
+ }
+ final var modules = context.getModuleStatements();
+ if (modules.isEmpty()) {
+ return emptyBody;
+ }
+
+ // Index into prefix -> revision -> module table
+ final var prefixRevModule = HashBasedTable.<String, Optional<Revision>, ModuleEffectiveStatement>create();
+ for (var module : modules.values()) {
+ prefixRevModule.put(prefix(module), module.localQNameModule().getRevision(), module);
+ }
+
+ // Now extract RPC names for each module with highest revision. This needed so we expose the right set of RPCs,
+ // as we always pick the latest revision to resolve prefix (or module name)
+ // TODO: Simplify this once we have yangtools-7.0.9+
+ final var moduleRpcs = new ArrayList<Entry<String, List<String>>>();
+ for (var moduleEntry : prefixRevModule.rowMap().entrySet()) {
+ final var revisions = new ArrayList<>(moduleEntry.getValue().keySet());
+ revisions.sort(Revision::compare);
+ final var selectedRevision = revisions.get(revisions.size() - 1);
+
+ final var rpcNames = moduleEntry.getValue().get(selectedRevision)
+ .streamEffectiveSubstatements(RpcEffectiveStatement.class)
+ .map(rpc -> rpc.argument().getLocalName())
+ .collect(Collectors.toUnmodifiableList());
+ if (!rpcNames.isEmpty()) {
+ moduleRpcs.add(Map.entry(moduleEntry.getKey(), rpcNames));
+ }
+ }
+
+ if (moduleRpcs.isEmpty()) {
+ // No RPCs, return empty content
+ return emptyBody;
+ }
+
+ // Ensure stability: sort by prefix
+ moduleRpcs.sort(Comparator.comparing(Entry::getKey));
+
+ return modules.isEmpty() ? emptyBody : createBody(moduleRpcs);
+ }
+
+ abstract @NonNull String createBody(List<Entry<String, List<String>>> rpcsByPrefix);
+
+ abstract @NonNull String prefix(ModuleEffectiveStatement module);
+}
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+// FIXME: remove this class
public final class OperationsResourceUtils {
private OperationsResourceUtils() {
// Hidden on purpose
--- /dev/null
+/*
+ * 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.common;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
+
+public class Netconf822Test {
+ private static EffectiveModelContext CONTEXT;
+
+ @BeforeClass
+ public static void beforeClass() {
+ CONTEXT = YangParserTestUtils.parseYangResourceDirectory("/nc822");
+ }
+
+ @Test
+ public void testOperationsContentJSON() {
+ assertEquals("{\n"
+ + " \"ietf-restconf:operations\" : {\n"
+ + " \"foo:new\": [null]\n"
+ + " }\n"
+ + "}", OperationsContent.JSON.bodyFor(CONTEXT));
+ }
+
+ @Test
+ public void testOperationsContentXML() {
+ assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"\n"
+ + " xmlns:ns0=\"foo\" >\n"
+ + " <ns0:new/>\n"
+ + "</operations>", OperationsContent.XML.bodyFor(CONTEXT));
+ }
+}
--- /dev/null
+module foo {
+ prefix foo;
+ namespace foo;
+ revision 2021-09-29;
+
+ rpc old;
+}
--- /dev/null
+module foo {
+ prefix foo;
+ namespace foo;
+ revision 2021-09-30;
+
+ rpc new;
+}
/**
* List of rpc or action operations supported by the server.
*
- * @param uriInfo
- * URI information
- * @return {@link NormalizedNodeContext}
+ * @return A JSON document string
* @deprecated do not use this method. It will be replaced by
* RestconfOperationsService#getOperations(UriInfo)
*/
@Deprecated
@GET
@Path("/operations")
- @Produces({
- Draft02.MediaTypes.API + JSON,
- Draft02.MediaTypes.API + XML,
- MediaType.APPLICATION_JSON,
- MediaType.APPLICATION_XML,
- MediaType.TEXT_XML
- })
- NormalizedNodeContext getOperations(@Context UriInfo uriInfo);
+ @Produces({ Draft02.MediaTypes.API + JSON, MediaType.APPLICATION_JSON })
+ String getOperationsJSON();
+
+ /**
+ * List of rpc or action operations supported by the server.
+ *
+ * @return A XML document string
+ * @deprecated do not use this method. It will be replaced by
+ * RestconfOperationsService#getOperations(UriInfo)
+ */
+ @Deprecated
+ @GET
+ @Path("/operations")
+ @Produces({ Draft02.MediaTypes.API + XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+ String getOperationsXML();
/**
* Valid for mount points. List of operations supported by the server.
}
@Override
- public NormalizedNodeContext getOperations(final UriInfo uriInfo) {
- return this.restconf.getOperations(uriInfo);
+ public String getOperationsJSON() {
+ return this.restconf.getOperationsJSON();
+ }
+
+ @Override
+ public String getOperationsXML() {
+ return this.restconf.getOperationsXML();
}
@Override
import org.opendaylight.netconf.sal.streams.listeners.NotificationListenerAdapter;
import org.opendaylight.netconf.sal.streams.listeners.Notificator;
import org.opendaylight.netconf.sal.streams.websockets.WebSocketServer;
+import org.opendaylight.restconf.common.OperationsContent;
import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
import org.opendaylight.restconf.common.context.NormalizedNodeContext;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
@Override
@Deprecated
- public NormalizedNodeContext getOperations(final UriInfo uriInfo) {
- return OperationsResourceUtils.contextForModelContext(controllerContext.getGlobalSchema(), null);
+ public String getOperationsJSON() {
+ return OperationsContent.JSON.bodyFor(controllerContext.getGlobalSchema());
+ }
+
+ @Override
+ @Deprecated
+ public String getOperationsXML() {
+ return OperationsContent.XML.bodyFor(controllerContext.getGlobalSchema());
}
@Override
}
@Override
- public NormalizedNodeContext getOperations(final UriInfo uriInfo) {
- return this.delegate.getOperations(uriInfo);
+ public String getOperationsJSON() {
+ return this.delegate.getOperationsJSON();
+ }
+
+ @Override
+ public String getOperationsXML() {
+ return this.delegate.getOperationsXML();
}
@Override
*/
public interface RestconfOperationsService {
/**
- * List of rpc or action operations supported by the server.
+ * List RPC and action operations in RFC7951 format.
*
- * @param uriInfo URI information
- * @return {@link NormalizedNodeContext}
+ * @return A string containing a JSON document conforming to both RFC8040 and RFC7951.
*/
@GET
@Path("/operations")
- @Produces({
- MediaTypes.APPLICATION_YANG_DATA_JSON,
- MediaTypes.APPLICATION_YANG_DATA_XML,
- MediaType.APPLICATION_JSON,
- MediaType.APPLICATION_XML,
- MediaType.TEXT_XML
- })
- NormalizedNodeContext getOperations(@Context UriInfo uriInfo);
+ @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
+ String getOperationsJSON();
+
+ /**
+ * List RPC and action operations in RFC8040 XML format.
+ *
+ * @return A string containing a JSON document conforming to both RFC8040 section 11.3.1 and page 84.
+ */
+ @GET
+ @Path("/operations")
+ @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+ String getOperationsXML();
/**
* Valid for mount points. List of operations supported by the server.
import org.opendaylight.mdsal.dom.api.DOMMountPoint;
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
import org.opendaylight.mdsal.dom.api.DOMSchemaService;
+import org.opendaylight.restconf.common.OperationsContent;
import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
import org.opendaylight.restconf.common.context.NormalizedNodeContext;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
}
@Override
- public NormalizedNodeContext getOperations(final UriInfo uriInfo) {
- return OperationsResourceUtils.contextForModelContext(schemaContextHandler.get(), null);
+ public String getOperationsJSON() {
+ return OperationsContent.JSON.bodyFor(schemaContextHandler.get());
+ }
+
+ @Override
+ public String getOperationsXML() {
+ return OperationsContent.XML.bodyFor(schemaContextHandler.get());
}
@Override
package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
-import com.google.common.collect.ImmutableSet;
-import java.util.Set;
-import javax.ws.rs.core.UriInfo;
-import org.junit.Before;
+import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
-import org.opendaylight.restconf.common.context.NormalizedNodeContext;
import org.opendaylight.restconf.nb.rfc8040.TestRestconfUtils;
import org.opendaylight.restconf.nb.rfc8040.TestUtils;
-import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
-import org.opendaylight.yangtools.yang.common.Empty;
-import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.common.QNameModule;
-import org.opendaylight.yangtools.yang.common.XMLNamespace;
-import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
-import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class RestconfOperationsServiceTest {
-
- @Mock
- private DOMMountPointService domMountPointService;
-
- @Mock
- private UriInfo uriInfo;
-
- private EffectiveModelContext schemaContext;
- private SchemaContextHandler schemaContextHandler;
-
- private Set<QName> listOfRpcsNames;
-
- @Before
- public void init() throws Exception {
- this.schemaContext = YangParserTestUtils.parseYangFiles(TestRestconfUtils.loadFiles("/modules"));
- this.schemaContextHandler = TestUtils.newSchemaContextHandler(schemaContext);
-
- final QNameModule module1 = QNameModule.create(XMLNamespace.of("module:1"));
- final QNameModule module2 = QNameModule.create(XMLNamespace.of("module:2"));
-
- this.listOfRpcsNames = ImmutableSet.of(QName.create(module1, "dummy-rpc1-module1"),
- QName.create(module1, "dummy-rpc2-module1"), QName.create(module2, "dummy-rpc1-module2"),
- QName.create(module2, "dummy-rpc2-module2"));
- }
-
@Test
- public void getOperationsTest() {
- final RestconfOperationsServiceImpl oper =
- new RestconfOperationsServiceImpl(this.schemaContextHandler, this.domMountPointService);
- final NormalizedNodeContext operations = oper.getOperations(this.uriInfo);
- final ContainerNode data = (ContainerNode) operations.getData();
- assertEquals("urn:ietf:params:xml:ns:yang:ietf-restconf",
- data.getIdentifier().getNodeType().getNamespace().toString());
- assertEquals("operations", data.getIdentifier().getNodeType().getLocalName());
-
- assertEquals(4, data.body().size());
-
- for (final DataContainerChild child : data.body()) {
- assertEquals(Empty.getInstance(), child.body());
-
- final QName qname = child.getIdentifier().getNodeType().withoutRevision();
- assertTrue(this.listOfRpcsNames.contains(qname));
- }
+ public void getOperationsTest() throws IOException {
+ final var oper = new RestconfOperationsServiceImpl(
+ TestUtils.newSchemaContextHandler(
+ YangParserTestUtils.parseYangFiles(TestRestconfUtils.loadFiles("/modules"))),
+ mock(DOMMountPointService.class));
+
+ assertEquals("{\n"
+ + " \"ietf-restconf:operations\" : {\n"
+ + " \"module1:dummy-rpc1-module1\": [null],\n"
+ + " \"module1:dummy-rpc2-module1\": [null],\n"
+ + " \"module2:dummy-rpc1-module2\": [null],\n"
+ + " \"module2:dummy-rpc2-module2\": [null]\n"
+ + " }\n"
+ + "}", oper.getOperationsJSON());
+ assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ + "<operations xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"\n"
+ + " xmlns:ns0=\"module:1\"\n"
+ + " xmlns:ns1=\"module:2\" >\n"
+ + " <ns0:dummy-rpc1-module1/>\n"
+ + " <ns0:dummy-rpc2-module1/>\n"
+ + " <ns1:dummy-rpc1-module2/>\n"
+ + " <ns1:dummy-rpc2-module2/>\n"
+ + "</operations>", oper.getOperationsXML());
}
}