--- /dev/null
+module module1 {
+ namespace "module:1";
+ prefix "mod1";
+ revision "2014-01-01";
+
+ rpc dummy-rpc1-module1 {
+ }
+
+ rpc dummy-rpc2-module1 {
+ }
+}
\ No newline at end of file
--- /dev/null
+module module2 {
+ namespace "module:2";
+ prefix "mod2";
+ revision "2014-01-02";
+
+ rpc dummy-rpc1-module2 {
+ }
+
+ rpc dummy-rpc2-module2 {
+ }
+}
\ No newline at end of file
<version>2.36.1</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-test-models</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
-import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.UriInfo;
import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
-import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
/**
* Container that provides access to the data-model specific operations supported by the server.
@Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
String getOperationsJSON();
+ /**
+ * 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
+ * @return A string containing a JSON document conforming to both RFC8040 and RFC7951.
+ */
+ @GET
+ @Path("/operations/{identifier:.+}")
+ @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
+ String getOperationJSON(@PathParam("identifier") String identifier);
+
/**
* 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.
+ * @return A string containing an XML document conforming to both RFC8040 section 11.3.1 and page 84.
*/
@GET
@Path("/operations")
String getOperationsXML();
/**
- * Valid for mount points. List of operations supported by the server.
+ * Retrieve list of operations and actions supported by the server or device in XML format.
*
- * @param identifier path parameter
- * @param uriInfo URI information
- * @return {@link NormalizedNodePayload}
+ * @param identifier 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:.+}")
- @Produces({
- MediaTypes.APPLICATION_YANG_DATA_JSON,
- MediaTypes.APPLICATION_YANG_DATA_XML,
- MediaType.APPLICATION_JSON,
- MediaType.APPLICATION_XML,
- MediaType.TEXT_XML
- })
- NormalizedNodePayload getOperations(@PathParam("identifier") String identifier, @Context UriInfo uriInfo);
+ @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+ String getOperationXML(@PathParam("identifier") String identifier);
}
import java.util.Map.Entry;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+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;
* @return Content of HTTP GET operation as a String
*/
public final @NonNull String bodyFor(final @Nullable EffectiveModelContext context) {
- if (context == null) {
- // Defensive, return empty content
+ if (isEmptyContext(context)) {
+ // No modules, or defensive return empty content
return emptyBody;
}
- final var modules = context.getModuleStatements();
- if (modules.isEmpty()) {
- // No modules, return empty content
+
+ final var moduleRpcs = getModuleRpcs(context, context.getModuleStatements());
+ return moduleRpcs.isEmpty() ? emptyBody : createBody(moduleRpcs);
+ }
+
+ /**
+ * Return content with RPCs and actions for a particular {@link InstanceIdentifierContext}.
+ *
+ * @param identifierContext InstanceIdentifierContext to use
+ * @return Content of HTTP GET operation as a String
+ */
+ public final @NonNull String bodyFor(final @NonNull InstanceIdentifierContext identifierContext) {
+ final var context = identifierContext.getSchemaContext();
+ if (isEmptyContext(context)) {
+ // No modules, or defensive return empty content
return emptyBody;
}
- final var moduleRpcs = 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();
+ final var stack = identifierContext.inference().toSchemaInferenceStack();
+ // empty stack == get all RPCs/actions
+ if (stack.isEmpty()) {
+ return createBody(getModuleRpcs(context, context.getModuleStatements()));
+ }
+
+ // get current module RPCs/actions by RPC/action name
+ final var currentModule = stack.currentModule();
+ final var currentModuleKey = Map.of(currentModule.localQNameModule(), currentModule);
+ final var rpcName = identifierContext.getSchemaNode().getQName().getLocalName();
+ return getModuleRpcs(context, currentModuleKey).stream()
+ .findFirst()
+ .map(e -> Map.entry(e.getKey(), e.getValue().stream().filter(rpcName::equals).toList()))
+ .map(e -> createBody(List.of(e)))
+ .orElse(emptyBody);
+ }
- return moduleRpcs.isEmpty() ? emptyBody : createBody(moduleRpcs);
+ private static boolean isEmptyContext(final EffectiveModelContext context) {
+ if (context == null) {
+ return true;
+ }
+ return context.getModuleStatements().isEmpty();
+ }
+
+ /**
+ * 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
+ */
+ private 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);
import static java.util.Objects.requireNonNull;
-import com.google.common.collect.ImmutableSet;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Map;
-import java.util.Map.Entry;
import java.util.Optional;
import javax.ws.rs.Path;
-import javax.ws.rs.core.UriInfo;
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-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.context.InstanceIdentifierContext;
-import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
-import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfOperationsService;
-import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
-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.data.api.YangInstanceIdentifier.NodeIdentifier;
-import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
-import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
-import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
-import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.Module;
-import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
-import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
}
@Override
- public String getOperationsXML() {
- return OperationsContent.XML.bodyFor(schemaContextHandler.get());
+ public String getOperationJSON(final String identifier) {
+ final var identifierContext = ParserIdentifier.toInstanceIdentifier(identifier,
+ schemaContextHandler.get(), Optional.of(mountPointService));
+ return OperationsContent.JSON.bodyFor(identifierContext);
}
@Override
- public NormalizedNodePayload getOperations(final String identifier, final UriInfo uriInfo) {
- if (!identifier.contains(RestconfConstants.MOUNT)) {
- final var errMsg = """
- URI has bad format. If operations behind mount point should be showed, URI has to end with %s.
- """.formatted(RestconfConstants.MOUNT);
- LOG.debug("{} for {}", errMsg, identifier);
- throw new RestconfDocumentedException(errMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
- }
-
- final InstanceIdentifierContext mountPointIdentifier = ParserIdentifier.toInstanceIdentifier(identifier,
- schemaContextHandler.get(), Optional.of(mountPointService));
- final DOMMountPoint mountPoint = mountPointIdentifier.getMountPoint();
- final var entry = contextForModelContext(modelContext(mountPoint), mountPoint);
- return NormalizedNodePayload.of(entry.getKey(), entry.getValue());
- }
-
- private static EffectiveModelContext modelContext(final DOMMountPoint mountPoint) {
- return mountPoint.getService(DOMSchemaService.class)
- .flatMap(svc -> Optional.ofNullable(svc.getGlobalContext()))
- .orElse(null);
+ public String getOperationsXML() {
+ return OperationsContent.XML.bodyFor(schemaContextHandler.get());
}
- // FIXME: remove this method and everything it uses
- @Deprecated(forRemoval = true, since = "4.0.0")
- private static @NonNull Entry<InstanceIdentifierContext, ContainerNode> contextForModelContext(
- final @NonNull EffectiveModelContext context, final @Nullable DOMMountPoint mountPoint) {
- // Determine which modules we need and construct leaf schemas to correspond to all RPC definitions
- final Collection<Module> modules = new ArrayList<>();
- final ArrayList<OperationsLeafSchemaNode> rpcLeafSchemas = new ArrayList<>();
- for (final Module m : context.getModules()) {
- final Collection<? extends RpcDefinition> rpcs = m.getRpcs();
- if (!rpcs.isEmpty()) {
- modules.add(new OperationsImportedModule(m));
- rpcLeafSchemas.ensureCapacity(rpcLeafSchemas.size() + rpcs.size());
- for (RpcDefinition rpc : rpcs) {
- rpcLeafSchemas.add(new OperationsLeafSchemaNode(rpc));
- }
- }
- }
-
- // Now generate a module for RESTCONF so that operations contain what they need
- final OperationsContainerSchemaNode operatationsSchema = new OperationsContainerSchemaNode(rpcLeafSchemas);
- modules.add(new OperationsRestconfModule(operatationsSchema));
-
- // Now build the operations container and combine it with the context
- final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> operationsBuilder = Builders.containerBuilder()
- .withNodeIdentifier(new NodeIdentifier(OperationsContainerSchemaNode.QNAME));
- for (final OperationsLeafSchemaNode leaf : rpcLeafSchemas) {
- operationsBuilder.withChild(ImmutableNodes.leafNode(leaf.getQName(), Empty.value()));
- }
-
- final var opContext = new OperationsEffectiveModuleContext(ImmutableSet.copyOf(modules));
- final var stack = SchemaInferenceStack.of(opContext);
- stack.enterSchemaTree(operatationsSchema.getQName());
-
- return Map.entry(InstanceIdentifierContext.ofStack(stack, mountPoint), operationsBuilder.build());
+ @Override
+ public String getOperationXML(final String identifier) {
+ final var identifierContext = ParserIdentifier.toInstanceIdentifier(identifier,
+ schemaContextHandler.get(), Optional.of(mountPointService));
+ return OperationsContent.XML.bodyFor(identifierContext);
}
-
}
import org.junit.BeforeClass;
import org.junit.Test;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.Revision;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
+import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
public class Netconf822Test {
- private static EffectiveModelContext CONTEXT;
+ private static InstanceIdentifierContext INSTANCE_IDENTIFIER_CONTEXT;
@BeforeClass
public static void beforeClass() {
- CONTEXT = YangParserTestUtils.parseYangResourceDirectory("/nc822");
+ final var context = YangParserTestUtils.parseYangResourceDirectory("/nc822");
+ final var stack = SchemaInferenceStack.of(context, Absolute.of(QName.create("foo", "new1",
+ Revision.of("2021-09-30"))));
+ INSTANCE_IDENTIFIER_CONTEXT = InstanceIdentifierContext.ofStack(stack);
}
@Test
public void testOperationsContentJSON() {
- assertEquals("{\n"
- + " \"ietf-restconf:operations\" : {\n"
- + " \"foo:new\": [null]\n"
- + " }\n"
- + "}", OperationsContent.JSON.bodyFor(CONTEXT));
+ assertEquals("""
+ {
+ "ietf-restconf:operations" : {
+ "foo:new": [null],
+ "foo:new1": [null]
+ }
+ }""", OperationsContent.JSON.bodyFor(INSTANCE_IDENTIFIER_CONTEXT.getSchemaContext()));
+ }
+
+ @Test
+ public void testOperationsContentByIdentifierJSON() {
+ assertEquals("""
+ {
+ "ietf-restconf:operations" : {
+ "foo:new1": [null]
+ }
+ }""", OperationsContent.JSON.bodyFor(INSTANCE_IDENTIFIER_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));
+ assertEquals("""
+ <?xml version="1.0" encoding="UTF-8"?>
+ <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"
+ xmlns:ns0="foo" >
+ <ns0:new/>
+ <ns0:new1/>
+ </operations>""", OperationsContent.XML.bodyFor(INSTANCE_IDENTIFIER_CONTEXT.getSchemaContext()));
+ }
+
+ @Test
+ public void testOperationsContentByIdentifierXML() {
+ assertEquals("""
+ <?xml version="1.0" encoding="UTF-8"?>
+ <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"
+ xmlns:ns0="foo" >
+ <ns0:new1/>
+ </operations>""", OperationsContent.XML.bodyFor(INSTANCE_IDENTIFIER_CONTEXT));
}
}
package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
-import java.io.IOException;
+import java.util.Optional;
+import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.mdsal.binding.runtime.spi.BindingRuntimeHelpers;
+import org.opendaylight.mdsal.dom.api.DOMDataBroker;
+import org.opendaylight.mdsal.dom.api.DOMMountPoint;
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
-import org.opendaylight.restconf.nb.rfc8040.TestRestconfUtils;
-import org.opendaylight.restconf.nb.rfc8040.TestUtils;
-import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
+import org.opendaylight.mdsal.dom.api.DOMSchemaService;
+import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
+import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfOperationsService;
+import org.opendaylight.yang.gen.v1.module._1.rev140101.Module1Data;
+import org.opendaylight.yang.gen.v1.module._2.rev140102.Module2Data;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NetworkTopology;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class RestconfOperationsServiceTest {
+ private static final String DEVICE_ID = "network-topology:network-topology/topology=topology-netconf/"
+ + "node=device/yang-ext:mount";
+ private static final String DEVICE_RPC1_MODULE1_ID = DEVICE_ID + "module1:dummy-rpc1-module1";
+ private static final String EXPECTED_JSON = """
+ {
+ "ietf-restconf:operations" : {
+ "module1:dummy-rpc1-module1": [null],
+ "module1:dummy-rpc2-module1": [null],
+ "module2:dummy-rpc1-module2": [null],
+ "module2:dummy-rpc2-module2": [null]
+ }
+ }""";
+ private static final String EXPECTED_XML = """
+ <?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" >
+ <ns0:dummy-rpc1-module1/>
+ <ns0:dummy-rpc2-module1/>
+ <ns1:dummy-rpc1-module2/>
+ <ns1:dummy-rpc2-module2/>
+ </operations>""";
+ private static RestconfOperationsService opService;
+
+ @BeforeClass
+ public static void startUp() {
+ final var runtimeContext = BindingRuntimeHelpers.createRuntimeContext(Module1Data.class, Module2Data.class,
+ NetworkTopology.class);
+ final var context = runtimeContext.getEffectiveModelContext();
+ final var mockMountPointService = mock(DOMMountPointService.class);
+ final var mockDomMountPoint = mock(DOMMountPoint.class);
+ final var mockDomSchemaService = mock(DOMSchemaService.class);
+ final var mockDataBroker = mock(DOMDataBroker.class);
+ doReturn(context).when(mockDomSchemaService).getGlobalContext();
+ doReturn(Optional.of(mockDomSchemaService)).when(mockDomMountPoint).getService(DOMSchemaService.class);
+ doReturn(Optional.of(mockDomMountPoint)).when(mockMountPointService).getMountPoint(any());
+ final var schemaContextHandler = new SchemaContextHandler(mockDataBroker, mockDomSchemaService);
+ schemaContextHandler.onModelContextUpdated(context);
+ opService = new RestconfOperationsServiceImpl(schemaContextHandler, mockMountPointService);
+ }
+
+ @Test
+ public void testOperationsJson() {
+ final var operationsJSON = opService.getOperationsJSON();
+ assertEquals(EXPECTED_JSON, operationsJSON);
+ }
+
+ @Test
+ public void testOperationsXml() {
+ final var operationsXML = opService.getOperationsXML();
+ assertEquals(EXPECTED_XML, operationsXML);
+ }
+
+ @Test
+ public void testMountPointOperationsJson() {
+ final var operationJSON = opService.getOperationJSON(DEVICE_ID);
+ assertEquals(EXPECTED_JSON, operationJSON);
+ }
+
+ @Test
+ public void testMountPointOperationsXml() {
+ final var operationXML = opService.getOperationXML(DEVICE_ID);
+ assertEquals(EXPECTED_XML, operationXML);
+ }
+
+ @Test
+ public void testMountPointSpecificOperationsJson() {
+ final var operationJSON = opService.getOperationJSON(DEVICE_RPC1_MODULE1_ID);
+ assertEquals("""
+ {
+ "ietf-restconf:operations" : {
+ "module1:dummy-rpc1-module1": [null]
+ }
+ }""", operationJSON);
+ }
+
@Test
- 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());
+ public void testMountPointSpecificOperationsXml() {
+ final var operationXML = opService.getOperationXML(DEVICE_RPC1_MODULE1_ID);
+ assertEquals("""
+ <?xml version="1.0" encoding="UTF-8"?>
+ <operations xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"
+ xmlns:ns0="module:1" >
+ <ns0:dummy-rpc1-module1/>
+ </operations>""", operationXML);
}
}
revision 2021-09-30;
rpc new;
+ rpc new1;
}