package org.opendaylight.netconf.sal.rest.impl;
+import static org.opendaylight.netconf.sal.restconf.impl.PATCHEditOperation.isPatchOperationWithValue;
+
import com.google.common.collect.ImmutableList;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
import org.opendaylight.netconf.sal.restconf.impl.PATCHContext;
-import org.opendaylight.netconf.sal.restconf.impl.PATCHEditOperation;
import org.opendaylight.netconf.sal.restconf.impl.PATCHEntity;
import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag;
import org.opendaylight.netconf.sal.restconf.impl.RestconfError.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.NormalizedNode;
import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
edit.setOperation(in.nextString());
break;
case "target" :
- edit.setTarget(codec.deserialize(codec.serialize(path.getInstanceIdentifier()) + in.nextString()));
- edit.setTargetSchemaNode(SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(),
- codec.getDataContextTree().getChild(edit.getTarget()).getDataSchemaNode().getPath()
- .getParent()));
+ // target can be specified completely in request URI
+ final String target = in.nextString();
+ if (target.equals("/")) {
+ edit.setTarget(path.getInstanceIdentifier());
+ edit.setTargetSchemaNode(path.getSchemaContext());
+ } else {
+ edit.setTarget(codec.deserialize(codec.serialize(path.getInstanceIdentifier()).concat(target)));
+ edit.setTargetSchemaNode(SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(),
+ codec.getDataContextTree().getChild(edit.getTarget()).getDataSchemaNode().getPath()
+ .getParent()));
+ }
+
break;
case "value" :
// save data defined in value node for next (later) processing, because target needs to be read
while (in.hasNext()) {
value.append("\"" + in.nextName() + "\"");
value.append(":");
- value.append("\"" + in.nextString() + "\"");
+
+ if (in.peek() == JsonToken.STRING) {
+ value.append("\"" + in.nextString() + "\"");
+ } else {
+ if (in.peek() == JsonToken.BEGIN_ARRAY) {
+ in.beginArray();
+ value.append("[");
+
+ while (in.hasNext()) {
+ readValueObject(value, in);
+ if (in.peek() != JsonToken.END_ARRAY) {
+ value.append(",");
+ }
+ }
+
+ in.endArray();
+ value.append("]");
+ } else {
+ readValueObject(value, in);
+ }
+ }
+
if (in.peek() != JsonToken.END_OBJECT) {
value.append(",");
}
if (edit.getOperation() != null && edit.getTargetSchemaNode() != null
&& checkDataPresence(edit.getOperation(), (edit.getData() != null))) {
if (isPatchOperationWithValue(edit.getOperation())) {
- return new PATCHEntity(edit.getId(), edit.getOperation(), edit.getTarget().getParent(), edit.getData());
+ // for lists allow to manipulate with list items through their parent
+ YangInstanceIdentifier targetNode;
+ if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
+ targetNode = edit.getTarget().getParent();
+ } else {
+ targetNode = edit.getTarget();
+ }
+
+ return new PATCHEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
} else {
return new PATCHEntity(edit.getId(), edit.getOperation(), edit.getTarget());
}
}
}
- /**
- * Check if operation requires data to be specified
- * @param operation Name of the operation to be checked
- * @return true if operation requires data, false otherwise
- */
- private boolean isPatchOperationWithValue(@Nonnull final String operation) {
- switch (PATCHEditOperation.valueOf(operation.toUpperCase())) {
- case CREATE:
- case MERGE:
- case REPLACE:
- case INSERT:
- return true;
- default:
- return false;
- }
- }
-
/**
* Helper class representing one patch edit
*/
import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
import org.opendaylight.yangtools.yang.common.QName;
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.NormalizedNode;
import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlUtils;
import org.opendaylight.yangtools.yang.data.impl.schema.transform.dom.parser.DomToNormalizedNodeParserFactory;
final Element element = (Element) editNodes.item(i);
final String operation = element.getElementsByTagName("operation").item(0).getFirstChild().getNodeValue();
final String editId = element.getElementsByTagName("edit-id").item(0).getFirstChild().getNodeValue();
- final String target = element.getElementsByTagName("target").item(0).getFirstChild().getNodeValue()
- .replaceFirst("/", "");
+ final String target = element.getElementsByTagName("target").item(0).getFirstChild().getNodeValue();
final List<Element> values = readValueNodes(element, operation);
final Element firstValueElement = values != null ? values.get(0) : null;
// get namespace according to schema node from path context or value
- String namespace = (firstValueElement == null) ?
+ final String namespace = (firstValueElement == null) ?
schemaNode.getQName().getNamespace().toString() : firstValueElement.getNamespaceURI();
// find module according to namespace
final StringModuleInstanceIdentifierCodec codec = new StringModuleInstanceIdentifierCodec(
pathContext.getSchemaContext(), module.getName());
- // find complete path to target
- final YangInstanceIdentifier targetII = codec.deserialize(codec.serialize(pathContext
- .getInstanceIdentifier()).concat(prepareNonCondXpath(schemaNode, target, firstValueElement,
- namespace, module.getQNameModule().getFormattedRevision())));
+ // find complete path to target and target schema node
+ // target can be also empty (only slash)
+ YangInstanceIdentifier targetII;
+ SchemaNode targetNode;
+ if (target.equals("/")) {
+ targetII = pathContext.getInstanceIdentifier();
+ targetNode = pathContext.getSchemaContext();
+ } else {
+ targetII = codec.deserialize(codec.serialize(pathContext.getInstanceIdentifier())
+ .concat(prepareNonCondXpath(schemaNode, target.replaceFirst("/", ""), firstValueElement,
+ namespace, module.getQNameModule().getFormattedRevision())));
- // move schema node and get target node
- schemaNode = (DataSchemaNode) SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
- codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath());
+ targetNode = SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
+ codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath().getParent());
- final SchemaNode targetNode = SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
- codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath().getParent());
+ // move schema node
+ schemaNode = (DataSchemaNode) SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
+ codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath());
+ }
if (targetNode == null) {
LOG.debug("Target node {} not found in path {} ", target, pathContext.getSchemaNode());
parsed = parserFactory.getMapNodeParser().parse(values, (ListSchemaNode) schemaNode);
}
- resultCollection.add(new PATCHEntity(editId, operation, targetII.getParent(), parsed));
+ // for lists allow to manipulate with list items through their parent
+ if (targetII.getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
+ targetII = targetII.getParent();
+ }
+
+ resultCollection.add(new PATCHEntity(editId, operation, targetII, parsed));
} else {
resultCollection.add(new PATCHEntity(editId, operation, targetII));
}
checkPATCHContext(returnValue);
}
+ /**
+ * Test of successful PATCH consisting of create and delete PATCH operations.
+ */
@Test
public void modulePATCHCreateAndDeleteTest() throws Exception {
final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1";
checkPATCHContext(returnValue);
}
+ /**
+ * Test trying to use PATCH create operation which requires value without value. Test should fail with
+ * {@link RestconfDocumentedException} with error code 400.
+ */
@Test
- public void modulePATCHValueMissingTest() throws Exception {
+ public void modulePATCHValueMissingNegativeTest() throws Exception {
final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1";
mockBodyReader(uri, jsonPATCHBodyReader, false);
try {
jsonPATCHBodyReader.readFrom(null, null, null, mediaType, null, inputStream);
fail("Test should return error 400 due to missing value node when attempt to invoke create operation");
- } catch (RestconfDocumentedException e) {
+ } catch (final RestconfDocumentedException e) {
assertEquals("Error code 400 expected", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
}
}
+ /**
+ * Test trying to use value with PATCH delete operation which does not support value. Test should fail with
+ * {@link RestconfDocumentedException} with error code 400.
+ */
@Test
- public void modulePATCHValueNotSupportedTest() throws Exception {
+ public void modulePATCHValueNotSupportedNegativeTest() throws Exception {
final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1";
mockBodyReader(uri, jsonPATCHBodyReader, false);
try {
jsonPATCHBodyReader.readFrom(null, null, null, mediaType, null, inputStream);
fail("Test should return error 400 due to present value node when attempt to invoke delete operation");
- } catch (RestconfDocumentedException e) {
+ } catch (final RestconfDocumentedException e) {
assertEquals("Error code 400 expected", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
}
}
+
+ /**
+ * Test using PATCH when target is completely specified in request URI and thus target leaf contains only '/' sign.
+ */
+ @Test
+ public void modulePATCHCompleteTargetInURITest() throws Exception {
+ final String uri = "instance-identifier-patch-module:patch-cont";
+ mockBodyReader(uri, jsonPATCHBodyReader, false);
+
+ final InputStream inputStream = TestJsonBodyReader.class
+ .getResourceAsStream("/instanceidentifier/json/jsonPATCHdataCompleteTargetInURI.json");
+
+ final PATCHContext returnValue = jsonPATCHBodyReader
+ .readFrom(null, null, null, mediaType, null, inputStream);
+ checkPATCHContext(returnValue);
+ }
}
.readFrom(null, null, null, mediaType, null, inputStream);
checkPATCHContext(returnValue);
}
+
+ /**
+ * Test using PATCH when target is completely specified in request URI and thus target leaf contains only '/' sign.
+ */
+ @Test
+ public void modulePATCHCompleteTargetInURITest() throws Exception {
+ final String uri = "instance-identifier-patch-module:patch-cont";
+ mockBodyReader(uri, xmlPATCHBodyReader, false);
+ final InputStream inputStream = TestXmlBodyReader.class
+ .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdataCompleteTargetInURI.xml");
+ final PATCHContext returnValue = xmlPATCHBodyReader
+ .readFrom(null, null, null, mediaType, null, inputStream);
+ checkPATCHContext(returnValue);
+ }
}
--- /dev/null
+{
+ "ietf-yang-patch:yang-patch" : {
+
+ "patch-id" : "test-patch",
+ "comment" : "Test to create and replace data in container directly using / sign as a target",
+ "edit" : [
+ {
+ "edit-id": "edit1",
+ "operation": "create",
+ "target": "/",
+ "value": {
+ "patch-cont": {
+ "my-list1": [
+ {
+ "name": "my-list1 - A",
+ "my-leaf11": "I am leaf11-0",
+ "my-leaf12": "I am leaf12-1"
+ },
+ {
+ "name": "my-list1 - B",
+ "my-leaf11": "I am leaf11-0",
+ "my-leaf12": "I am leaf12-1"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "edit-id": "edit2",
+ "operation": "replace",
+ "target": "/",
+ "value": {
+ "patch-cont": {
+ "my-list1": {
+ "name": "my-list1 - Replacing",
+ "my-leaf11": "I am leaf11-0",
+ "my-leaf12": "I am leaf12-1"
+ }
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+<!--
+ ~ Copyright (c) 2016 Cisco Systems, Inc. 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
+ -->
+<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
+ <patch-id>test-patch</patch-id>
+ <comment>Test to create and replace data in container directly using / sign as a target</comment>
+ <edit>
+ <edit-id>edit1</edit-id>
+ <operation>create</operation>
+ <target>/</target>
+ <value>
+ <patch-cont xmlns="instance:identifier:patch:module">
+ <my-list1>
+ <name>my-list1 - A</name>
+ <my-leaf11>I am leaf11-0</my-leaf11>
+ <my-leaf12>I am leaf12-1</my-leaf12>
+ </my-list1>
+ <my-list1>
+ <name>my-list1 - B</name>
+ <my-leaf11>I am leaf11-0</my-leaf11>
+ <my-leaf12>I am leaf12-1</my-leaf12>
+ </my-list1>
+ </patch-cont>
+ </value>
+ </edit>
+ <edit>
+ <edit-id>edit2</edit-id>
+ <operation>replace</operation>
+ <target>/</target>
+ <value>
+ <patch-cont xmlns="instance:identifier:patch:module">
+ <my-list1>
+ <name>my-list1 - Replacing</name>
+ <my-leaf11>I am leaf11-0</my-leaf11>
+ <my-leaf12>I am leaf12-1</my-leaf12>
+ </my-list1>
+ </patch-cont>
+ </value>
+ </edit>
+</yang-patch>
\ No newline at end of file