2 * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
13 import com.google.common.util.concurrent.FutureCallback;
14 import com.google.common.util.concurrent.Futures;
15 import com.google.common.util.concurrent.ListenableFuture;
16 import com.google.common.util.concurrent.MoreExecutors;
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.Optional;
20 import org.eclipse.jdt.annotation.NonNull;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.opendaylight.mdsal.common.api.CommitInfo;
23 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
24 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
25 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
26 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
27 import org.opendaylight.restconf.api.query.PointParam;
28 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
29 import org.opendaylight.restconf.common.errors.RestconfError;
30 import org.opendaylight.restconf.common.errors.RestconfFuture;
31 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
32 import org.opendaylight.restconf.common.patch.PatchContext;
33 import org.opendaylight.restconf.common.patch.PatchStatusContext;
34 import org.opendaylight.restconf.common.patch.PatchStatusEntity;
35 import org.opendaylight.restconf.nb.rfc8040.Insert;
36 import org.opendaylight.restconf.nb.rfc8040.rests.utils.TransactionUtil;
37 import org.opendaylight.restconf.nb.rfc8040.utils.parser.YangInstanceIdentifierDeserializer;
38 import org.opendaylight.yangtools.yang.common.Empty;
39 import org.opendaylight.yangtools.yang.common.ErrorTag;
40 import org.opendaylight.yangtools.yang.common.ErrorType;
41 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
42 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
43 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
44 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
45 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
46 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
47 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
48 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * Baseline execution strategy for various RESTCONF operations.
56 * @see NetconfRestconfStrategy
57 * @see MdsalRestconfStrategy
59 // FIXME: it seems the first three operations deal with lifecycle of a transaction, while others invoke various
60 // operations. This should be handled through proper allocation indirection.
61 public abstract class RestconfStrategy {
63 * Result of a {@code PUT} request as defined in
64 * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.5">RFC8040 section 4.5</a>. The definition makes it
65 * clear that the logical operation is {@code create-or-replace}.
67 public enum CreateOrReplaceResult {
69 * A new resource has been created.
73 * An existing resources has been replaced.
78 private static final Logger LOG = LoggerFactory.getLogger(RestconfStrategy.class);
85 * Look up the appropriate strategy for a particular mount point.
87 * @param mountPoint Target mount point
88 * @return A strategy, or null if the mount point does not expose a supported interface
89 * @throws NullPointerException if {@code mountPoint} is null
91 public static Optional<RestconfStrategy> forMountPoint(final DOMMountPoint mountPoint) {
92 final Optional<RestconfStrategy> netconf = mountPoint.getService(NetconfDataTreeService.class)
93 .map(NetconfRestconfStrategy::new);
94 if (netconf.isPresent()) {
98 return mountPoint.getService(DOMDataBroker.class).map(MdsalRestconfStrategy::new);
102 * Lock the entire datastore.
104 * @return A {@link RestconfTransaction}. This transaction needs to be either committed or canceled before doing
107 public abstract RestconfTransaction prepareWriteExecution();
110 * Read data from the datastore.
112 * @param store the logical data store which should be modified
113 * @param path the data object path
114 * @return a ListenableFuture containing the result of the read
116 public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
117 YangInstanceIdentifier path);
120 * Read data selected using fields from the datastore.
122 * @param store the logical data store which should be modified
123 * @param path the parent data object path
124 * @param fields paths to selected fields relative to parent path
125 * @return a ListenableFuture containing the result of the read
127 public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
128 YangInstanceIdentifier path, List<YangInstanceIdentifier> fields);
131 * Check if data already exists in the configuration datastore.
133 * @param path the data object path
134 * @return a ListenableFuture containing the result of the check
136 // FIXME: this method should be hosted in RestconfTransaction
137 // FIXME: this method should only be needed in MdsalRestconfStrategy
138 abstract ListenableFuture<Boolean> exists(YangInstanceIdentifier path);
141 * Delete data from the configuration datastore. If the data does not exist, this operation will fail, as outlined
142 * in <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.7">RFC8040 section 4.7</a>
144 * @param path Path to delete
145 * @return A {@link RestconfFuture}
146 * @throws NullPointerException if {@code path} is {@code null}
148 public final @NonNull RestconfFuture<Empty> delete(final YangInstanceIdentifier path) {
149 final var ret = new SettableRestconfFuture<Empty>();
150 delete(ret, requireNonNull(path));
154 protected abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
157 * Merge data into the configuration datastore, as outlined in
158 * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040 section 4.6.1</a>.
160 * @param path Path to merge
161 * @param data Data to merge
162 * @param context Corresponding EffectiveModelContext
163 * @return A {@link RestconfFuture}
164 * @throws NullPointerException if any argument is {@code null}
166 public final @NonNull RestconfFuture<Empty> merge(final YangInstanceIdentifier path, final NormalizedNode data,
167 final EffectiveModelContext context) {
168 final var ret = new SettableRestconfFuture<Empty>();
169 merge(ret, requireNonNull(path), requireNonNull(data), requireNonNull(context));
173 private void merge(final @NonNull SettableRestconfFuture<Empty> future,
174 final @NonNull YangInstanceIdentifier path, final @NonNull NormalizedNode data,
175 final @NonNull EffectiveModelContext context) {
176 final var tx = prepareWriteExecution();
177 // FIXME: this method should be further specialized to eliminate this call -- it is only needed for MD-SAL
178 TransactionUtil.ensureParentsByMerge(path, context, tx);
179 tx.merge(path, data);
180 Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
182 public void onSuccess(final CommitInfo result) {
183 future.set(Empty.value());
187 public void onFailure(final Throwable cause) {
188 future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path));
190 }, MoreExecutors.directExecutor());
194 * Check mount point and prepare variables for put data to DS.
196 * @param path path of data
198 * @param context reference to {@link EffectiveModelContext}
199 * @param insert {@link Insert}
200 * @return A {@link CreateOrReplaceResult}
202 public @NonNull CreateOrReplaceResult putData(final YangInstanceIdentifier path, final NormalizedNode data,
203 final EffectiveModelContext context, final @Nullable Insert insert) {
204 final var exists = TransactionUtil.syncAccess(exists(path), path);
206 final ListenableFuture<? extends CommitInfo> commitFuture;
207 if (insert != null) {
208 final var parentPath = path.coerceParent();
209 checkListAndOrderedType(context, parentPath);
210 commitFuture = insertAndCommitPut(path, data, insert, parentPath, context);
212 commitFuture = replaceAndCommit(prepareWriteExecution(), path, data, context);
215 TransactionUtil.syncCommit(commitFuture, "PUT", path);
216 return exists ? CreateOrReplaceResult.REPLACED : CreateOrReplaceResult.CREATED;
219 private ListenableFuture<? extends CommitInfo> insertAndCommitPut(final YangInstanceIdentifier path,
220 final NormalizedNode data, final @NonNull Insert insert, final YangInstanceIdentifier parentPath,
221 final EffectiveModelContext context) {
222 final var tx = prepareWriteExecution();
224 return switch (insert.insert()) {
226 final var readData = tx.readList(parentPath);
227 if (readData == null || readData.isEmpty()) {
228 yield replaceAndCommit(tx, path, data, context);
230 tx.remove(parentPath);
231 tx.replace(path, data, context);
232 tx.replace(parentPath, readData, context);
235 case LAST -> replaceAndCommit(tx, path, data, context);
237 final var readData = tx.readList(parentPath);
238 if (readData == null || readData.isEmpty()) {
239 yield replaceAndCommit(tx, path, data, context);
241 insertWithPointPut(tx, path, data, verifyNotNull(insert.point()), readData, true, context);
245 final var readData = tx.readList(parentPath);
246 if (readData == null || readData.isEmpty()) {
247 yield replaceAndCommit(tx, path, data, context);
249 insertWithPointPut(tx, path, data, verifyNotNull(insert.point()), readData, false, context);
255 private static void insertWithPointPut(final RestconfTransaction tx, final YangInstanceIdentifier path,
256 final NormalizedNode data, final @NonNull PointParam point, final NormalizedNodeContainer<?> readList,
257 final boolean before, final EffectiveModelContext context) {
258 tx.remove(path.getParent());
259 final var pointArg = YangInstanceIdentifierDeserializer.create(context, point.value()).path
260 .getLastPathArgument();
261 int lastItemPosition = 0;
262 for (var nodeChild : readList.body()) {
263 if (nodeChild.name().equals(pointArg)) {
271 int lastInsertedPosition = 0;
272 final var emptySubtree = ImmutableNodes.fromInstanceId(context, path.getParent());
273 tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
274 for (var nodeChild : readList.body()) {
275 if (lastInsertedPosition == lastItemPosition) {
276 tx.replace(path, data, context);
278 final var childPath = path.coerceParent().node(nodeChild.name());
279 tx.replace(childPath, nodeChild, context);
280 lastInsertedPosition++;
284 private static ListenableFuture<? extends CommitInfo> replaceAndCommit(final RestconfTransaction tx,
285 final YangInstanceIdentifier path, final NormalizedNode data, final EffectiveModelContext context) {
286 tx.replace(path, data, context);
290 private static DataSchemaNode checkListAndOrderedType(final EffectiveModelContext ctx,
291 final YangInstanceIdentifier path) {
292 final var dataSchemaNode = DataSchemaContextTree.from(ctx).findChild(path).orElseThrow().dataSchemaNode();
294 final String message;
295 if (dataSchemaNode instanceof ListSchemaNode listSchema) {
296 if (listSchema.isUserOrdered()) {
299 message = "Insert parameter can be used only with ordered-by user list.";
300 } else if (dataSchemaNode instanceof LeafListSchemaNode leafListSchema) {
301 if (leafListSchema.isUserOrdered()) {
302 return leafListSchema;
304 message = "Insert parameter can be used only with ordered-by user leaf-list.";
306 message = "Insert parameter can be used only with list or leaf-list";
308 throw new RestconfDocumentedException(message, ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
312 * Check mount point and prepare variables for post data.
316 * @param context reference to actual {@link EffectiveModelContext}
317 * @param insert {@link Insert}
319 public final void postData(final YangInstanceIdentifier path, final NormalizedNode data,
320 final EffectiveModelContext context, final @Nullable Insert insert) {
321 final ListenableFuture<? extends CommitInfo> future;
322 if (insert != null) {
323 final var parentPath = path.coerceParent();
324 checkListAndOrderedType(context, parentPath);
325 future = insertAndCommitPost(path, data, insert, parentPath, context);
327 future = createAndCommit(prepareWriteExecution(), path, data, context);
329 TransactionUtil.syncCommit(future, "POST", path);
332 private ListenableFuture<? extends CommitInfo> insertAndCommitPost(final YangInstanceIdentifier path,
333 final NormalizedNode data, final @NonNull Insert insert, final YangInstanceIdentifier parent,
334 final EffectiveModelContext context) {
335 final var grandParent = parent.coerceParent();
336 final var tx = prepareWriteExecution();
338 return switch (insert.insert()) {
340 final var readData = tx.readList(grandParent);
341 if (readData == null || readData.isEmpty()) {
342 tx.replace(path, data, context);
344 checkItemDoesNotExists(exists(path), path);
345 tx.remove(grandParent);
346 tx.replace(path, data, context);
347 tx.replace(grandParent, readData, context);
351 case LAST -> createAndCommit(tx, path, data, context);
353 final var readData = tx.readList(grandParent);
354 if (readData == null || readData.isEmpty()) {
355 tx.replace(path, data, context);
357 checkItemDoesNotExists(exists(path), path);
358 insertWithPointPost(tx, path, data, verifyNotNull(insert.point()), readData, grandParent, true,
364 final var readData = tx.readList(grandParent);
365 if (readData == null || readData.isEmpty()) {
366 tx.replace(path, data, context);
368 checkItemDoesNotExists(exists(path), path);
369 insertWithPointPost(tx, path, data, verifyNotNull(insert.point()), readData, grandParent, false,
378 * Process edit operations of one {@link PatchContext}.
380 * @param patch Patch context to be processed
381 * @param context Global schema context
382 * @return {@link PatchStatusContext}
384 public final @NonNull PatchStatusContext patchData(final PatchContext patch, final EffectiveModelContext context) {
385 final var editCollection = new ArrayList<PatchStatusEntity>();
386 final var tx = prepareWriteExecution();
388 boolean noError = true;
389 for (var patchEntity : patch.getData()) {
391 final var targetNode = patchEntity.getTargetNode();
392 final var editId = patchEntity.getEditId();
394 switch (patchEntity.getOperation()) {
397 tx.create(targetNode, patchEntity.getNode(), context);
398 editCollection.add(new PatchStatusEntity(editId, true, null));
399 } catch (RestconfDocumentedException e) {
400 editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
406 tx.delete(targetNode);
407 editCollection.add(new PatchStatusEntity(editId, true, null));
408 } catch (RestconfDocumentedException e) {
409 editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
415 TransactionUtil.ensureParentsByMerge(targetNode, context, tx);
416 tx.merge(targetNode, patchEntity.getNode());
417 editCollection.add(new PatchStatusEntity(editId, true, null));
418 } catch (RestconfDocumentedException e) {
419 editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
425 tx.replace(targetNode, patchEntity.getNode(), context);
426 editCollection.add(new PatchStatusEntity(editId, true, null));
427 } catch (RestconfDocumentedException e) {
428 editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
434 tx.remove(targetNode);
435 editCollection.add(new PatchStatusEntity(editId, true, null));
436 } catch (RestconfDocumentedException e) {
437 editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
442 editCollection.add(new PatchStatusEntity(editId, false, List.of(
443 new RestconfError(ErrorType.PROTOCOL, ErrorTag.OPERATION_NOT_SUPPORTED,
444 "Not supported Yang Patch operation"))));
453 // if no errors then submit transaction, otherwise cancel
456 TransactionUtil.syncCommit(tx.commit(), "PATCH", null);
457 } catch (RestconfDocumentedException e) {
458 // if errors occurred during transaction commit then patch failed and global errors are reported
459 return new PatchStatusContext(patch.getPatchId(), List.copyOf(editCollection), false, e.getErrors());
462 return new PatchStatusContext(patch.getPatchId(), List.copyOf(editCollection), true, null);
465 return new PatchStatusContext(patch.getPatchId(), List.copyOf(editCollection), false, null);
469 private static void insertWithPointPost(final RestconfTransaction tx, final YangInstanceIdentifier path,
470 final NormalizedNode data, final PointParam point, final NormalizedNodeContainer<?> readList,
471 final YangInstanceIdentifier grandParentPath, final boolean before, final EffectiveModelContext context) {
472 tx.remove(grandParentPath);
473 final var pointArg = YangInstanceIdentifierDeserializer.create(context, point.value()).path
474 .getLastPathArgument();
475 int lastItemPosition = 0;
476 for (var nodeChild : readList.body()) {
477 if (nodeChild.name().equals(pointArg)) {
485 int lastInsertedPosition = 0;
486 final var emptySubtree = ImmutableNodes.fromInstanceId(context, grandParentPath);
487 tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
488 for (var nodeChild : readList.body()) {
489 if (lastInsertedPosition == lastItemPosition) {
490 tx.replace(path, data, context);
492 final YangInstanceIdentifier childPath = grandParentPath.node(nodeChild.name());
493 tx.replace(childPath, nodeChild, context);
494 lastInsertedPosition++;
498 private static ListenableFuture<? extends CommitInfo> createAndCommit(final RestconfTransaction tx,
499 final YangInstanceIdentifier path, final NormalizedNode data, final EffectiveModelContext context) {
501 tx.create(path, data, context);
502 } catch (RestconfDocumentedException e) {
503 // close transaction if any and pass exception further
512 * Check if items do NOT already exists at specified {@code path}.
514 * @param existsFuture if checked data exists
515 * @param path Path to be checked
516 * @throws RestconfDocumentedException if data already exists.
518 static void checkItemDoesNotExists(final ListenableFuture<Boolean> existsFuture,
519 final YangInstanceIdentifier path) {
520 if (TransactionUtil.syncAccess(existsFuture, path)) {
521 LOG.trace("Operation via Restconf was not executed because data at {} already exists", path);
522 throw new RestconfDocumentedException("Data already exists", ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS,