Eliminate PatchDataTransactionUtil
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / transactions / RestconfStrategy.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
9
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
12
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;
52
53 /**
54  * Baseline execution strategy for various RESTCONF operations.
55  *
56  * @see NetconfRestconfStrategy
57  * @see MdsalRestconfStrategy
58  */
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 {
62     /**
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}.
66      */
67     public enum CreateOrReplaceResult {
68         /**
69          * A new resource has been created.
70          */
71         CREATED,
72         /*
73          * An existing resources has been replaced.
74          */
75         REPLACED;
76     }
77
78     private static final Logger LOG = LoggerFactory.getLogger(RestconfStrategy.class);
79
80     RestconfStrategy() {
81         // Hidden on purpose
82     }
83
84     /**
85      * Look up the appropriate strategy for a particular mount point.
86      *
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
90      */
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()) {
95             return netconf;
96         }
97
98         return mountPoint.getService(DOMDataBroker.class).map(MdsalRestconfStrategy::new);
99     }
100
101     /**
102      * Lock the entire datastore.
103      *
104      * @return A {@link RestconfTransaction}. This transaction needs to be either committed or canceled before doing
105      *         anything else.
106      */
107     public abstract RestconfTransaction prepareWriteExecution();
108
109     /**
110      * Read data from the datastore.
111      *
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
115      */
116     public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
117         YangInstanceIdentifier path);
118
119     /**
120      * Read data selected using fields from the datastore.
121      *
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
126      */
127     public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
128             YangInstanceIdentifier path, List<YangInstanceIdentifier> fields);
129
130     /**
131      * Check if data already exists in the configuration datastore.
132      *
133      * @param path the data object path
134      * @return a ListenableFuture containing the result of the check
135      */
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);
139
140     /**
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>
143      *
144      * @param path Path to delete
145      * @return A {@link RestconfFuture}
146      * @throws NullPointerException if {@code path} is {@code null}
147      */
148     public final @NonNull RestconfFuture<Empty> delete(final YangInstanceIdentifier path) {
149         final var ret = new SettableRestconfFuture<Empty>();
150         delete(ret, requireNonNull(path));
151         return ret;
152     }
153
154     protected abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
155
156     /**
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>.
159      *
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}
165      */
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));
170         return ret;
171     }
172
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>() {
181             @Override
182             public void onSuccess(final CommitInfo result) {
183                 future.set(Empty.value());
184             }
185
186             @Override
187             public void onFailure(final Throwable cause) {
188                 future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path));
189             }
190         }, MoreExecutors.directExecutor());
191     }
192
193     /**
194      * Check mount point and prepare variables for put data to DS.
195      *
196      * @param path    path of data
197      * @param data    data
198      * @param context reference to {@link EffectiveModelContext}
199      * @param insert  {@link Insert}
200      * @return A {@link CreateOrReplaceResult}
201      */
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);
205
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);
211         } else {
212             commitFuture = replaceAndCommit(prepareWriteExecution(), path, data, context);
213         }
214
215         TransactionUtil.syncCommit(commitFuture, "PUT", path);
216         return exists ? CreateOrReplaceResult.REPLACED : CreateOrReplaceResult.CREATED;
217     }
218
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();
223
224         return switch (insert.insert()) {
225             case FIRST -> {
226                 final var readData = tx.readList(parentPath);
227                 if (readData == null || readData.isEmpty()) {
228                     yield replaceAndCommit(tx, path, data, context);
229                 }
230                 tx.remove(parentPath);
231                 tx.replace(path, data, context);
232                 tx.replace(parentPath, readData, context);
233                 yield tx.commit();
234             }
235             case LAST -> replaceAndCommit(tx, path, data, context);
236             case BEFORE -> {
237                 final var readData = tx.readList(parentPath);
238                 if (readData == null || readData.isEmpty()) {
239                     yield replaceAndCommit(tx, path, data, context);
240                 }
241                 insertWithPointPut(tx, path, data, verifyNotNull(insert.point()), readData, true, context);
242                 yield tx.commit();
243             }
244             case AFTER -> {
245                 final var readData = tx.readList(parentPath);
246                 if (readData == null || readData.isEmpty()) {
247                     yield replaceAndCommit(tx, path, data, context);
248                 }
249                 insertWithPointPut(tx, path, data, verifyNotNull(insert.point()), readData, false, context);
250                 yield tx.commit();
251             }
252         };
253     }
254
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)) {
264                 break;
265             }
266             lastItemPosition++;
267         }
268         if (!before) {
269             lastItemPosition++;
270         }
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);
277             }
278             final var childPath = path.coerceParent().node(nodeChild.name());
279             tx.replace(childPath, nodeChild, context);
280             lastInsertedPosition++;
281         }
282     }
283
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);
287         return tx.commit();
288     }
289
290     private static DataSchemaNode checkListAndOrderedType(final EffectiveModelContext ctx,
291             final YangInstanceIdentifier path) {
292         final var dataSchemaNode = DataSchemaContextTree.from(ctx).findChild(path).orElseThrow().dataSchemaNode();
293
294         final String message;
295         if (dataSchemaNode instanceof ListSchemaNode listSchema) {
296             if (listSchema.isUserOrdered()) {
297                 return listSchema;
298             }
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;
303             }
304             message = "Insert parameter can be used only with ordered-by user leaf-list.";
305         } else {
306             message = "Insert parameter can be used only with list or leaf-list";
307         }
308         throw new RestconfDocumentedException(message, ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
309     }
310
311     /**
312      * Check mount point and prepare variables for post data.
313      *
314      * @param path    path
315      * @param data    data
316      * @param context reference to actual {@link EffectiveModelContext}
317      * @param insert  {@link Insert}
318      */
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);
326         } else {
327             future = createAndCommit(prepareWriteExecution(), path, data, context);
328         }
329         TransactionUtil.syncCommit(future, "POST", path);
330     }
331
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();
337
338         return switch (insert.insert()) {
339             case FIRST -> {
340                 final var readData = tx.readList(grandParent);
341                 if (readData == null || readData.isEmpty()) {
342                     tx.replace(path, data, context);
343                 } else {
344                     checkItemDoesNotExists(exists(path), path);
345                     tx.remove(grandParent);
346                     tx.replace(path, data, context);
347                     tx.replace(grandParent, readData, context);
348                 }
349                 yield tx.commit();
350             }
351             case LAST -> createAndCommit(tx, path, data, context);
352             case BEFORE -> {
353                 final var readData = tx.readList(grandParent);
354                 if (readData == null || readData.isEmpty()) {
355                     tx.replace(path, data, context);
356                 } else {
357                     checkItemDoesNotExists(exists(path), path);
358                     insertWithPointPost(tx, path, data, verifyNotNull(insert.point()), readData, grandParent, true,
359                         context);
360                 }
361                 yield tx.commit();
362             }
363             case AFTER -> {
364                 final var readData = tx.readList(grandParent);
365                 if (readData == null || readData.isEmpty()) {
366                     tx.replace(path, data, context);
367                 } else {
368                     checkItemDoesNotExists(exists(path), path);
369                     insertWithPointPost(tx, path, data, verifyNotNull(insert.point()), readData, grandParent, false,
370                         context);
371                 }
372                 yield tx.commit();
373             }
374         };
375     }
376
377     /**
378      * Process edit operations of one {@link PatchContext}.
379      *
380      * @param patch    Patch context to be processed
381      * @param context  Global schema context
382      * @return {@link PatchStatusContext}
383      */
384     public final @NonNull PatchStatusContext patchData(final PatchContext patch, final EffectiveModelContext context) {
385         final var editCollection = new ArrayList<PatchStatusEntity>();
386         final var tx = prepareWriteExecution();
387
388         boolean noError = true;
389         for (var patchEntity : patch.getData()) {
390             if (noError) {
391                 final var targetNode = patchEntity.getTargetNode();
392                 final var editId = patchEntity.getEditId();
393
394                 switch (patchEntity.getOperation()) {
395                     case Create:
396                         try {
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()));
401                             noError = false;
402                         }
403                         break;
404                     case Delete:
405                         try {
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()));
410                             noError = false;
411                         }
412                         break;
413                     case Merge:
414                         try {
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()));
420                             noError = false;
421                         }
422                         break;
423                     case Replace:
424                         try {
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()));
429                             noError = false;
430                         }
431                         break;
432                     case Remove:
433                         try {
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()));
438                             noError = false;
439                         }
440                         break;
441                     default:
442                         editCollection.add(new PatchStatusEntity(editId, false, List.of(
443                             new RestconfError(ErrorType.PROTOCOL, ErrorTag.OPERATION_NOT_SUPPORTED,
444                                 "Not supported Yang Patch operation"))));
445                         noError = false;
446                         break;
447                 }
448             } else {
449                 break;
450             }
451         }
452
453         // if no errors then submit transaction, otherwise cancel
454         if (noError) {
455             try {
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());
460             }
461
462             return new PatchStatusContext(patch.getPatchId(), List.copyOf(editCollection), true, null);
463         } else {
464             tx.cancel();
465             return new PatchStatusContext(patch.getPatchId(), List.copyOf(editCollection), false, null);
466         }
467     }
468
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)) {
478                 break;
479             }
480             lastItemPosition++;
481         }
482         if (!before) {
483             lastItemPosition++;
484         }
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);
491             }
492             final YangInstanceIdentifier childPath = grandParentPath.node(nodeChild.name());
493             tx.replace(childPath, nodeChild, context);
494             lastInsertedPosition++;
495         }
496     }
497
498     private static ListenableFuture<? extends CommitInfo> createAndCommit(final RestconfTransaction tx,
499             final YangInstanceIdentifier path, final NormalizedNode data, final EffectiveModelContext context) {
500         try {
501             tx.create(path, data, context);
502         } catch (RestconfDocumentedException e) {
503             // close transaction if any and pass exception further
504             tx.cancel();
505             throw e;
506         }
507
508         return tx.commit();
509     }
510
511     /**
512      * Check if items do NOT already exists at specified {@code path}.
513      *
514      * @param existsFuture if checked data exists
515      * @param path         Path to be checked
516      * @throws RestconfDocumentedException if data already exists.
517      */
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,
523                 path);
524         }
525     }
526 }