final NormalizedNode data, final @Nullable Insert insert) {
final ListenableFuture<? extends CommitInfo> future;
if (insert != null) {
- final var parentPath = path.coerceParent();
- checkListAndOrderedType(parentPath);
- future = insertAndCommitPost(path, data, insert, parentPath);
+ checkListAndOrderedType(path);
+ future = insertAndCommitPost(path, data, insert);
} else {
future = createAndCommit(prepareWriteExecution(), path, data);
}
}
private ListenableFuture<? extends CommitInfo> insertAndCommitPost(final YangInstanceIdentifier path,
- final NormalizedNode data, final @NonNull Insert insert, final YangInstanceIdentifier parent) {
- final var grandParent = parent.coerceParent();
+ final NormalizedNode data, final @NonNull Insert insert) {
final var tx = prepareWriteExecution();
return switch (insert.insert()) {
case FIRST -> {
- final var readData = tx.readList(grandParent);
+ final var readData = tx.readList(path);
if (readData == null || readData.isEmpty()) {
tx.replace(path, data);
} else {
- checkItemDoesNotExists(exists(path), path);
- tx.remove(grandParent);
+ checkListDataDoesNotExist(path, data);
+ tx.remove(path);
tx.replace(path, data);
- tx.replace(grandParent, readData);
+ tx.replace(path, readData);
}
yield tx.commit();
}
case LAST -> createAndCommit(tx, path, data);
case BEFORE -> {
- final var readData = tx.readList(grandParent);
+ final var readData = tx.readList(path);
if (readData == null || readData.isEmpty()) {
tx.replace(path, data);
} else {
- checkItemDoesNotExists(exists(path), path);
- insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, grandParent, true);
+ checkListDataDoesNotExist(path, data);
+ insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, true);
}
yield tx.commit();
}
case AFTER -> {
- final var readData = tx.readList(grandParent);
+ final var readData = tx.readList(path);
if (readData == null || readData.isEmpty()) {
tx.replace(path, data);
} else {
- checkItemDoesNotExists(exists(path), path);
- insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, grandParent, false);
+ checkListDataDoesNotExist(path, data);
+ insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, false);
}
yield tx.commit();
}
private void insertWithPointPost(final RestconfTransaction tx, final YangInstanceIdentifier path,
final NormalizedNode data, final PathArgument pointArg, final NormalizedNodeContainer<?> readList,
- final YangInstanceIdentifier grandParentPath, final boolean before) {
- tx.remove(grandParentPath);
+ final boolean before) {
+ tx.remove(path);
int lastItemPosition = 0;
for (var nodeChild : readList.body()) {
}
int lastInsertedPosition = 0;
- final var emptySubtree = fromInstanceId(modelContext(), grandParentPath);
- tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
for (var nodeChild : readList.body()) {
if (lastInsertedPosition == lastItemPosition) {
tx.replace(path, data);
}
- tx.replace(grandParentPath.node(nodeChild.name()), nodeChild);
+ tx.replace(path.node(nodeChild.name()), nodeChild);
lastInsertedPosition++;
}
return tx.commit();
}
+ /**
+ * Check if child items do NOT already exists in List at specified {@code path}.
+ *
+ * @param data Data to be checked
+ * @param path Path to be checked
+ * @throws RestconfDocumentedException if data already exists.
+ */
+ private void checkListDataDoesNotExist(final YangInstanceIdentifier path, final NormalizedNode data) {
+ if (data instanceof NormalizedNodeContainer<?> dataNode) {
+ for (final var node : dataNode.body()) {
+ checkItemDoesNotExists(exists(path.node(node.name())), path.node(node.name()));
+ }
+ } else {
+ throw new RestconfDocumentedException("Unexpected node type: " + data.getClass().getName());
+ }
+ }
+
/**
* Check if items do NOT already exists at specified {@code path}.
*
import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFalseFluentFuture;
+import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFluentFuture;
import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateTrueFluentFuture;
import java.net.URI;
+import java.util.List;
+import java.util.Optional;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.UriBuilder;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opendaylight.mdsal.common.api.CommitInfo;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeReadTransaction;
import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
@ExtendWith(MockitoExtension.class)
class RestconfDataPostTest extends AbstractRestconfTest {
+ private static final String BASE_URI = "http://localhost:8181/rests/";
+ private static final String INSERT = "insert";
+ private static final String POINT = "point";
@Mock
private DOMDataTreeReadWriteTransaction tx;
@Mock
+ private DOMDataTreeReadTransaction readTx;
+ @Mock
private AsyncResponse asyncResponse;
@BeforeEach
doReturn(immediateFalseFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
doNothing().when(tx).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID,
ImmutableNodes.newContainerBuilder().withNodeIdentifier(new NodeIdentifier(JUKEBOX_QNAME)).build());
- doReturn(UriBuilder.fromUri("http://localhost:8181/rests/")).when(uriInfo).getBaseUriBuilder();
+ doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox"),
assertResponse(201, ar -> restconf.postDataJSON(stringInputStream("""
final var node = PLAYLIST_IID.node(BAND_ENTRY.name());
doReturn(immediateFalseFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, node);
doNothing().when(tx).put(LogicalDatastoreType.CONFIGURATION, node, BAND_ENTRY);
- doReturn(UriBuilder.fromUri("http://localhost:8181/rests/")).when(uriInfo).getBaseUriBuilder();
+ doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=name%20of%20band"),
assertResponse(201, ar -> restconf.postDataJSON(JUKEBOX_API_PATH, stringInputStream("""
.build();
assertEquals(expectedPath, actualPath);
}
+
+ @Test
+ public void testPostDataWithInsertLast() {
+ // Mocking the query parameters to include 'insert=last'
+ final var queryParams = new MultivaluedHashMap<String, String>();
+ queryParams.put(INSERT, List.of("last"));
+ doReturn(queryParams).when(uriInfo).getQueryParameters();
+
+ doReturn(immediateFalseFluentFuture()).when(tx)
+ .exists(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+
+ doNothing().when(tx).put(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class),
+ any(NormalizedNode.class));
+ doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
+
+ assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=0/song=3"),
+ assertResponse(201, ar -> restconf.postDataJSON(
+ apiPath("example-jukebox:jukebox/playlist=0"), stringInputStream("""
+ {
+ "example-jukebox:song" : [
+ {
+ "index": "3"
+ }
+ ]
+ }"""), uriInfo, ar)).getLocation());
+ verify(tx, times(1)).put(any(), any(), any());
+ }
+
+ @Test
+ public void testPostDataWithInsertFirst() {
+ // Mocking the query parameters to include 'insert=first'
+ final var queryParams = new MultivaluedHashMap<String, String>();
+ queryParams.put(INSERT, List.of("first"));
+ doReturn(queryParams).when(uriInfo).getQueryParameters();
+ doReturn(readTx).when(dataBroker).newReadOnlyTransaction();
+
+ doReturn(immediateFalseFluentFuture()).when(readTx)
+ .exists(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+ // Mocking existed playlist with two songs in DS
+ doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(tx)
+ .read(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+
+ doNothing().when(tx).put(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class),
+ any(NormalizedNode.class));
+ doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
+
+ assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=0/song=3"),
+ assertResponse(201, ar -> restconf.postDataJSON(
+ apiPath("example-jukebox:jukebox/playlist=0"), stringInputStream("""
+ {
+ "example-jukebox:song" : [
+ {
+ "index": "3"
+ }
+ ]
+ }"""), uriInfo, ar)).getLocation());
+ verify(tx, times(3)).put(any(), any(), any());
+ }
+
+ @Test
+ public void testPostDataWithInsertBefore() {
+ // Mocking the query parameters to include 'insert=before' and 'point=example-jukebox:jukebox/playlist=0/song=2'
+ final var queryParams = new MultivaluedHashMap<String, String>();
+ queryParams.put(INSERT, List.of("before"));
+ queryParams.put(POINT, List.of("example-jukebox:jukebox/playlist=0/song=2"));
+ doReturn(queryParams).when(uriInfo).getQueryParameters();
+ doReturn(readTx).when(dataBroker).newReadOnlyTransaction();
+
+ doReturn(immediateFalseFluentFuture()).when(readTx)
+ .exists(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+ // Mocking existed playlist with two songs in DS
+ doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(tx)
+ .read(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+
+ doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
+
+ assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=0/song=3"),
+ assertResponse(201, ar -> restconf.postDataJSON(
+ apiPath("example-jukebox:jukebox/playlist=0"), stringInputStream("""
+ {
+ "example-jukebox:song" : [
+ {
+ "index": "3",
+ "id" = "C"
+ }
+ ]
+ }"""), uriInfo, ar)).getLocation());
+
+ verify(tx, times(3)).put(any(), any(), any());
+ }
+
+ @Test
+ public void testPostDataWithInsertAfter() {
+ // Mocking the query parameters to include 'insert=after' and 'point=example-jukebox:jukebox/playlist=0/song=1'
+ final var queryParams = new MultivaluedHashMap<String, String>();
+ queryParams.put(INSERT, List.of("after"));
+ queryParams.put(POINT, List.of("example-jukebox:jukebox/playlist=0/song=1"));
+ doReturn(queryParams).when(uriInfo).getQueryParameters();
+ doReturn(readTx).when(dataBroker).newReadOnlyTransaction();
+
+ doReturn(immediateFalseFluentFuture()).when(readTx)
+ .exists(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+ doReturn(immediateFluentFuture(Optional.of(PLAYLIST_WITH_SONGS))).when(tx)
+ .read(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class));
+
+ doReturn(UriBuilder.fromUri(BASE_URI)).when(uriInfo).getBaseUriBuilder();
+
+ assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=0/song=3"),
+ assertResponse(201, ar -> restconf.postDataJSON(
+ apiPath("example-jukebox:jukebox/playlist=0"), stringInputStream("""
+ {
+ "example-jukebox:song" : [
+ {
+ "index": "3",
+ "id" = "C"
+ }
+ ]
+ }"""), uriInfo, ar)).getLocation());
+
+ verify(tx, times(3)).put(any(), any(), any());
+ }
}
import org.opendaylight.restconf.server.api.DatabindContext;
import org.opendaylight.yangtools.yang.common.Decimal64;
import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.Uint32;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
.build();
protected static final MapEntryNode SONG1 = ImmutableNodes.newMapEntryBuilder()
.withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME, SONG_INDEX_QNAME,
- "1"))
+ Uint32.ONE))
.withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "A"))
.build();
protected static final MapEntryNode SONG2 = ImmutableNodes.newMapEntryBuilder()
.withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME, SONG_INDEX_QNAME,
- "2"))
+ Uint32.TWO))
.withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "B"))
.build();
protected static final SystemMapNode PLAYLIST_WITH_SONGS = ImmutableNodes.newSystemMapBuilder()
- .withNodeIdentifier(new NodeIdentifier(JUKEBOX_QNAME))
- .withChild(ImmutableNodes.newMapEntryBuilder()
- .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(PLAYLIST_QNAME, NAME_QNAME,
- "0"))
- .withChild(ImmutableNodes.newUserMapBuilder()
- .withNodeIdentifier(new NodeIdentifier(SONG_QNAME))
- .withChild(SONG1)
- .withChild(SONG2)
- .build())
- .build())
- .build();
+ .withNodeIdentifier(new NodeIdentifier(SONG_QNAME))
+ .withChild(SONG1)
+ .withChild(SONG2)
+ .build();
protected static final @NonNull EffectiveModelContext JUKEBOX_SCHEMA =
YangParserTestUtils.parseYangResourceDirectory("/jukebox");
import org.opendaylight.yangtools.yang.common.ErrorSeverity;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
-import org.opendaylight.yangtools.yang.common.Uint32;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
-import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
import org.w3c.dom.DOMException;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
doReturn(spyTx).when(spyStrategy).prepareWriteExecution();
doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(any());
- // For this test we are using
- final var songsList = ImmutableNodes.newUserMapBuilder()
- .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(SONG_QNAME))
- .withChild(ImmutableNodes.newMapEntryBuilder()
- .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME, SONG_INDEX_QNAME,
- Uint32.valueOf(1)))
- .withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "A"))
- .build())
- .withChild(ImmutableNodes.newMapEntryBuilder()
- .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME, SONG_INDEX_QNAME,
- Uint32.valueOf(2)))
- .withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "B"))
- .build())
- .build();
- doReturn(songsList).when(spyTx).readList(any(YangInstanceIdentifier.class));
+ doReturn(PLAYLIST_WITH_SONGS).when(spyTx).readList(any(YangInstanceIdentifier.class));
// Creating query params to insert new item after last existing item in list
final var queryParams = new HashMap<String, String>();
doReturn(spyTx).when(spyStrategy).prepareWriteExecution();
doReturn(immediateFluentFuture(Optional.empty())).when(netconfService).getConfig(any());
- // For this test we are using
- final var songsList = ImmutableNodes.newUserMapBuilder()
- .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(SONG_QNAME))
- .withChild(ImmutableNodes.newMapEntryBuilder()
- .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME,
- SONG_INDEX_QNAME, Uint32.valueOf(1)))
- .withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "A"))
- .build())
- .withChild(ImmutableNodes.newMapEntryBuilder()
- .withNodeIdentifier(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(SONG_QNAME,
- SONG_INDEX_QNAME, Uint32.valueOf(2)))
- .withChild(ImmutableNodes.leafNode(SONG_ID_QNAME, "B"))
- .build())
- .build();
- doReturn(songsList).when(spyTx).readList(any(YangInstanceIdentifier.class));
+ doReturn(PLAYLIST_WITH_SONGS).when(spyTx).readList(any(YangInstanceIdentifier.class));
// Creating query params to insert new item after last existing item in list
final var queryParams = new HashMap<String, String>();
queryParams.put("point", "example-jukebox:jukebox/playlist=0/song=2");
// Inserting new song at 3rd position (aka as last element)
- spyStrategy.dataPOST(ApiPath.parse("example-jukebox:jukebox/playlist=0/song=3"),
+ spyStrategy.dataPOST(ApiPath.parse("example-jukebox:jukebox/playlist=0"),
new JsonDataPostBody(stringInputStream("""
- {
- "id" = "C"
- }""")), queryParams);
+ {
+ "example-jukebox:song" : [
+ {
+ "index": "3",
+ "id" = "C"
+ }
+ ]
+ }""")), queryParams);
// Counting how many times we insert items in list
verify(spyTx, times(3)).replace(any(), any());