*/
package org.opendaylight.restconf.nb.rfc8040.jersey.providers.patch;
+import static com.google.common.base.Verify.verify;
+
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
import org.opendaylight.restconf.common.patch.PatchContext;
}
static final YangInstanceIdentifier parsePatchTarget(final InstanceIdentifierContext context, final String target) {
- final var schemaContext = context.getSchemaContext();
final var urlPath = context.getInstanceIdentifier();
+ if (target.equals("/")) {
+ verify(!urlPath.isEmpty(),
+ "target resource of URI must not be a datastore resource when target is '/'");
+ return urlPath;
+ }
+
+ final var schemaContext = context.getSchemaContext();
final String targetUrl;
if (urlPath.isEmpty()) {
targetUrl = target.startsWith("/") ? target.substring(1) : target;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
import org.opendaylight.yangtools.yang.model.api.SchemaNode;
import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
-import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
break;
case "target":
// target can be specified completely in request URI
- final String target = in.nextString();
- if (target.equals("/")) {
- edit.setTarget(path.getInstanceIdentifier());
- edit.setTargetSchemaNode(SchemaInferenceStack.of(path.getSchemaContext()).toInference());
- } else {
- edit.setTarget(parsePatchTarget(path, target));
-
- final var stack = schemaTree.enterPath(edit.getTarget()).orElseThrow().stack();
- if (!stack.isEmpty()) {
- stack.exit();
- }
+ edit.setTarget(parsePatchTarget(path, in.nextString()));
+ final var stack = schemaTree.enterPath(edit.getTarget()).orElseThrow().stack();
+ if (!stack.isEmpty()) {
+ stack.exit();
+ }
- if (!stack.isEmpty()) {
- final EffectiveStatement<?, ?> parentStmt = stack.currentStatement();
- verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt);
- }
- edit.setTargetSchemaNode(stack.toInference());
+ if (!stack.isEmpty()) {
+ final EffectiveStatement<?, ?> parentStmt = stack.currentStatement();
+ verify(parentStmt instanceof SchemaNode, "Unexpected parent %s", parentStmt);
}
+ edit.setTargetSchemaNode(stack.toInference());
break;
case "value":
import org.opendaylight.yangtools.util.xml.UntrustedXML;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
import org.opendaylight.yangtools.yang.data.impl.schema.NormalizationResultHolder;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
-import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
final Element firstValueElement = values != null ? values.get(0) : null;
// find complete path to target, it can be also empty (only slash)
- final YangInstanceIdentifier targetII;
- final Inference inference;
- if (target.equals("/")) {
- targetII = pathContext.getInstanceIdentifier();
- inference = pathContext.inference();
- } else {
- // interpret as simple context
- targetII = parsePatchTarget(pathContext, target);
-
- // move schema node
- final var lookup = DataSchemaContextTree.from(pathContext.getSchemaContext())
- .enterPath(targetII).orElseThrow();
-
- final var stack = lookup.stack();
- inference = stack.toInference();
- if (!stack.isEmpty()) {
- stack.exit();
- }
+ final var targetII = parsePatchTarget(pathContext, target);
+ // move schema node
+ final var lookup = DataSchemaContextTree.from(pathContext.getSchemaContext())
+ .enterPath(targetII).orElseThrow();
+
+ final var stack = lookup.stack();
+ final var inference = stack.toInference();
+ if (!stack.isEmpty()) {
+ stack.exit();
}
if (requiresValue(oper)) {
checkPatchContext(returnValue);
}
+ /**
+ * Test of Yang Patch on the top-level container with the full path in the URI and "/" in 'target'.
+ */
+ @Test
+ public void modulePatchTargetTopLevelContainerWithFullPathURITest() throws Exception {
+ mockBodyReader("instance-identifier-patch-module:patch-cont", jsonToPatchBodyReader, false);
+ final var inputStream = new ByteArrayInputStream("""
+ {
+ "ietf-yang-patch:yang-patch": {
+ "patch-id": "test-patch",
+ "comment": "Test patch applied to the top-level container with '/' in target",
+ "edit": [
+ {
+ "edit-id": "edit1",
+ "operation": "replace",
+ "target": "/",
+ "value": {
+ "patch-cont": {
+ "my-list1": [
+ {
+ "name": "my-leaf-set",
+ "my-leaf11": "leaf-a",
+ "my-leaf12": "leaf-b"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ }""".getBytes(StandardCharsets.UTF_8));
+ final PatchContext returnValue = jsonToPatchBodyReader.readFrom(null, null, null, mediaType, null, inputStream);
+ checkPatchContext(returnValue);
+ assertEquals(Builders.containerBuilder()
+ .withNodeIdentifier(new NodeIdentifier(PATCH_CONT_QNAME))
+ .withChild(Builders.mapBuilder()
+ .withNodeIdentifier(new NodeIdentifier(MY_LIST1_QNAME))
+ .withChild(Builders.mapEntryBuilder()
+ .withNodeIdentifier(NodeIdentifierWithPredicates.of(MY_LIST1_QNAME, LEAF_NAME_QNAME, "my-leaf-set"))
+ .withChild(ImmutableNodes.leafNode(LEAF_NAME_QNAME, "my-leaf-set"))
+ .withChild(ImmutableNodes.leafNode(MY_LEAF11_QNAME, "leaf-a"))
+ .withChild(ImmutableNodes.leafNode(MY_LEAF12_QNAME, "leaf-b"))
+ .build())
+ .build())
+ .build(), returnValue.getData().get(0).getNode());
+ }
+
+ /**
+ * Test of Yang Patch on the second-level list with the full path in the URI and "/" in 'target'.
+ */
+ @Test
+ public void modulePatchTargetSecondLevelListWithFullPathURITest() throws Exception {
+ mockBodyReader("instance-identifier-patch-module:patch-cont/my-list1=my-leaf-set",
+ jsonToPatchBodyReader, false);
+ final var inputStream = new ByteArrayInputStream("""
+ {
+ "ietf-yang-patch:yang-patch": {
+ "patch-id": "test-patch",
+ "comment": "Test patch applied to the second-level list with '/' in target",
+ "edit": [
+ {
+ "edit-id": "edit1",
+ "operation": "replace",
+ "target": "/",
+ "value": {
+ "my-list1": [
+ {
+ "name": "my-leaf-set",
+ "my-leaf11": "leaf-a",
+ "my-leaf12": "leaf-b"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }""".getBytes(StandardCharsets.UTF_8));
+ final PatchContext returnValue = jsonToPatchBodyReader.readFrom(null, null, null, mediaType, null, inputStream);
+ checkPatchContext(returnValue);
+ assertEquals(Builders.mapBuilder()
+ .withNodeIdentifier(new NodeIdentifier(MY_LIST1_QNAME))
+ .withChild(Builders.mapEntryBuilder()
+ .withNodeIdentifier(NodeIdentifierWithPredicates.of(
+ MY_LIST1_QNAME, LEAF_NAME_QNAME, "my-leaf-set"))
+ .withChild(ImmutableNodes.leafNode(LEAF_NAME_QNAME, "my-leaf-set"))
+ .withChild(ImmutableNodes.leafNode(MY_LEAF11_QNAME, "leaf-a"))
+ .withChild(ImmutableNodes.leafNode(MY_LEAF12_QNAME, "leaf-b"))
+ .build())
+ .build(), returnValue.getData().get(0).getNode());
+ }
+
/**
* Test of Yang Patch on the top augmented element.
*/
import org.junit.BeforeClass;
import org.junit.Test;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.patch.PatchContext;
import org.opendaylight.restconf.nb.rfc8040.jersey.providers.test.AbstractBodyReaderTest;
import org.opendaylight.restconf.nb.rfc8040.jersey.providers.test.XmlBodyReaderTest;
import org.opendaylight.yangtools.yang.common.ErrorTag;
checkPatchContext(xmlToPatchBodyReader.readFrom(null, null, null, mediaType, null, inputStream));
}
+ /**
+ * Test of Yang Patch on the top-level container with the full path in the URI and "/" in 'target'.
+ */
+ @Test
+ public void modulePatchTargetTopLevelContainerWithFullPathURITest() throws Exception {
+ mockBodyReader("instance-identifier-patch-module:patch-cont", xmlToPatchBodyReader, false);
+ final var inputStream = new ByteArrayInputStream("""
+ <yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
+ <patch-id>test-patch</patch-id>
+ <comment>Test patch applied to the top-level container with '/' in target</comment>
+ <edit>
+ <edit-id>edit1</edit-id>
+ <operation>replace</operation>
+ <target>/</target>
+ <value>
+ <patch-cont xmlns="instance:identifier:patch:module">
+ <my-list1>
+ <name>my-leaf-set</name>
+ <my-leaf11>leaf-a</my-leaf11>
+ <my-leaf12>leaf-b</my-leaf12>
+ </my-list1>
+ </patch-cont>
+ </value>
+ </edit>
+ </yang-patch>
+ """.getBytes(StandardCharsets.UTF_8));
+ final PatchContext returnValue = xmlToPatchBodyReader.readFrom(null, null, null, mediaType, null, inputStream);
+ checkPatchContext(returnValue);
+ assertEquals(Builders.containerBuilder()
+ .withNodeIdentifier(new NodeIdentifier(PATCH_CONT_QNAME))
+ .withChild(Builders.mapBuilder()
+ .withNodeIdentifier(new NodeIdentifier(MY_LIST1_QNAME))
+ .withChild(Builders.mapEntryBuilder()
+ .withNodeIdentifier(NodeIdentifierWithPredicates.of(MY_LIST1_QNAME, LEAF_NAME_QNAME, "my-leaf-set"))
+ .withChild(ImmutableNodes.leafNode(LEAF_NAME_QNAME, "my-leaf-set"))
+ .withChild(ImmutableNodes.leafNode(MY_LEAF11_QNAME, "leaf-a"))
+ .withChild(ImmutableNodes.leafNode(MY_LEAF12_QNAME, "leaf-b"))
+ .build())
+ .build())
+ .build(), returnValue.getData().get(0).getNode());
+ }
+
+ /**
+ * Test of Yang Patch on the second-level list with the full path in the URI and "/" in 'target'.
+ */
+ @Test
+ public void modulePatchTargetSecondLevelListWithFullPathURITest() throws Exception {
+ mockBodyReader("instance-identifier-patch-module:patch-cont/my-list1=my-leaf-set",
+ xmlToPatchBodyReader, false);
+ final var inputStream = new ByteArrayInputStream("""
+ <yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
+ <patch-id>test-patch</patch-id>
+ <comment>Test patch applied to the second-level list with '/' in target</comment>
+ <edit>
+ <edit-id>edit1</edit-id>
+ <operation>replace</operation>
+ <target>/</target>
+ <value>
+ <my-list1 xmlns="instance:identifier:patch:module">
+ <name>my-leaf-set</name>
+ <my-leaf11>leaf-a</my-leaf11>
+ <my-leaf12>leaf-b</my-leaf12>
+ </my-list1>
+ </value>
+ </edit>
+ </yang-patch>
+ """.getBytes(StandardCharsets.UTF_8));
+ final PatchContext returnValue = xmlToPatchBodyReader.readFrom(null, null, null, mediaType, null, inputStream);
+ checkPatchContext(returnValue);
+ assertEquals(Builders.mapBuilder()
+ .withNodeIdentifier(new NodeIdentifier(MY_LIST1_QNAME))
+ .withChild(Builders.mapEntryBuilder()
+ .withNodeIdentifier(NodeIdentifierWithPredicates.of(MY_LIST1_QNAME, LEAF_NAME_QNAME, "my-leaf-set"))
+ .withChild(ImmutableNodes.leafNode(LEAF_NAME_QNAME, "my-leaf-set"))
+ .withChild(ImmutableNodes.leafNode(MY_LEAF11_QNAME, "leaf-a"))
+ .withChild(ImmutableNodes.leafNode(MY_LEAF12_QNAME, "leaf-b"))
+ .build())
+ .build(), returnValue.getData().get(0).getNode());
+ }
+
/**
* Test of Yang Patch on the top augmented element.
*/
protected static final QName LIST_LEAF2_QNAME = QName.create("list:ns", "leaf2").intern();
protected static final QName CHOICE_CONT_QNAME = QName.create("choice:ns", "case-cont1").intern();
protected static final QName CASE_LEAF1_QNAME = QName.create("choice:ns", "case-leaf1").intern();
+ protected static final QName PATCH_CONT_QNAME = QName.create("instance:identifier:patch:module",
+ "2015-11-21", "patch-cont").intern();
+ protected static final QName MY_LIST1_QNAME = QName.create("instance:identifier:patch:module",
+ "2015-11-21", "my-list1").intern();
protected static final QName LEAF_NAME_QNAME = QName.create("instance:identifier:patch:module",
"2015-11-21", "name").intern();
+ protected static final QName MY_LEAF11_QNAME = QName.create("instance:identifier:patch:module",
+ "2015-11-21", "my-leaf11").intern();
+ protected static final QName MY_LEAF12_QNAME = QName.create("instance:identifier:patch:module",
+ "2015-11-21", "my-leaf12").intern();
protected final MediaType mediaType;
protected final DatabindProvider databindProvider;