Optimize RestconfStrategy.postData()
[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.List;
18 import java.util.Optional;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.opendaylight.mdsal.common.api.CommitInfo;
22 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
23 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
24 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
25 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
26 import org.opendaylight.restconf.api.query.InsertParam;
27 import org.opendaylight.restconf.api.query.PointParam;
28 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
29 import org.opendaylight.restconf.common.errors.RestconfFuture;
30 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
31 import org.opendaylight.restconf.nb.rfc8040.WriteDataParams;
32 import org.opendaylight.restconf.nb.rfc8040.rests.utils.TransactionUtil;
33 import org.opendaylight.restconf.nb.rfc8040.utils.parser.YangInstanceIdentifierDeserializer;
34 import org.opendaylight.yangtools.yang.common.Empty;
35 import org.opendaylight.yangtools.yang.common.ErrorTag;
36 import org.opendaylight.yangtools.yang.common.ErrorType;
37 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
38 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
39 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
40 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
41 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
42 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
44 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 /**
50  * Baseline execution strategy for various RESTCONF operations.
51  *
52  * @see NetconfRestconfStrategy
53  * @see MdsalRestconfStrategy
54  */
55 // FIXME: it seems the first three operations deal with lifecycle of a transaction, while others invoke various
56 //        operations. This should be handled through proper allocation indirection.
57 public abstract class RestconfStrategy {
58     /**
59      * Result of a {@code PUT} request as defined in
60      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.5">RFC8040 section 4.5</a>. The definition makes it
61      * clear that the logical operation is {@code create-or-replace}.
62      */
63     public enum CreateOrReplaceResult {
64         /**
65          * A new resource has been created.
66          */
67         CREATED,
68         /*
69          * An existing resources has been replaced.
70          */
71         REPLACED;
72     }
73
74     private static final Logger LOG = LoggerFactory.getLogger(RestconfStrategy.class);
75
76     RestconfStrategy() {
77         // Hidden on purpose
78     }
79
80     /**
81      * Look up the appropriate strategy for a particular mount point.
82      *
83      * @param mountPoint Target mount point
84      * @return A strategy, or null if the mount point does not expose a supported interface
85      * @throws NullPointerException if {@code mountPoint} is null
86      */
87     public static Optional<RestconfStrategy> forMountPoint(final DOMMountPoint mountPoint) {
88         final Optional<RestconfStrategy> netconf = mountPoint.getService(NetconfDataTreeService.class)
89             .map(NetconfRestconfStrategy::new);
90         if (netconf.isPresent()) {
91             return netconf;
92         }
93
94         return mountPoint.getService(DOMDataBroker.class).map(MdsalRestconfStrategy::new);
95     }
96
97     /**
98      * Lock the entire datastore.
99      *
100      * @return A {@link RestconfTransaction}. This transaction needs to be either committed or canceled before doing
101      *         anything else.
102      */
103     public abstract RestconfTransaction prepareWriteExecution();
104
105     /**
106      * Read data from the datastore.
107      *
108      * @param store the logical data store which should be modified
109      * @param path the data object path
110      * @return a ListenableFuture containing the result of the read
111      */
112     public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
113         YangInstanceIdentifier path);
114
115     /**
116      * Read data selected using fields from the datastore.
117      *
118      * @param store the logical data store which should be modified
119      * @param path the parent data object path
120      * @param fields paths to selected fields relative to parent path
121      * @return a ListenableFuture containing the result of the read
122      */
123     public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
124             YangInstanceIdentifier path, List<YangInstanceIdentifier> fields);
125
126     /**
127      * Check if data already exists in the configuration datastore.
128      *
129      * @param path the data object path
130      * @return a ListenableFuture containing the result of the check
131      */
132     // FIXME: this method should be hosted in RestconfTransaction
133     // FIXME: this method should only be needed in MdsalRestconfStrategy
134     abstract ListenableFuture<Boolean> exists(YangInstanceIdentifier path);
135
136     /**
137      * Delete data from the configuration datastore. If the data does not exist, this operation will fail, as outlined
138      * in <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.7">RFC8040 section 4.7</a>
139      *
140      * @param path Path to delete
141      * @return A {@link RestconfFuture}
142      * @throws NullPointerException if {@code path} is {@code null}
143      */
144     public final @NonNull RestconfFuture<Empty> delete(final YangInstanceIdentifier path) {
145         final var ret = new SettableRestconfFuture<Empty>();
146         delete(ret, requireNonNull(path));
147         return ret;
148     }
149
150     protected abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
151
152     /**
153      * Merge data into the configuration datastore, as outlined in
154      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040 section 4.6.1</a>.
155      *
156      * @param path Path to merge
157      * @param data Data to merge
158      * @param context Corresponding EffectiveModelContext
159      * @return A {@link RestconfFuture}
160      * @throws NullPointerException if any argument is {@code null}
161      */
162     public final @NonNull RestconfFuture<Empty> merge(final YangInstanceIdentifier path, final NormalizedNode data,
163             final EffectiveModelContext context) {
164         final var ret = new SettableRestconfFuture<Empty>();
165         merge(ret, requireNonNull(path), requireNonNull(data), requireNonNull(context));
166         return ret;
167     }
168
169     private void merge(final @NonNull SettableRestconfFuture<Empty> future,
170             final @NonNull YangInstanceIdentifier path, final @NonNull NormalizedNode data,
171             final @NonNull EffectiveModelContext context) {
172         final var tx = prepareWriteExecution();
173         // FIXME: this method should be further specialized to eliminate this call -- it is only needed for MD-SAL
174         TransactionUtil.ensureParentsByMerge(path, context, tx);
175         tx.merge(path, data);
176         Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
177             @Override
178             public void onSuccess(final CommitInfo result) {
179                 future.set(Empty.value());
180             }
181
182             @Override
183             public void onFailure(final Throwable cause) {
184                 future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path));
185             }
186         }, MoreExecutors.directExecutor());
187     }
188
189     /**
190      * Check mount point and prepare variables for put data to DS.
191      *
192      * @param path    path of data
193      * @param data    data
194      * @param context reference to {@link EffectiveModelContext}
195      * @param params  {@link WriteDataParams}
196      * @return A {@link CreateOrReplaceResult}
197      */
198     public @NonNull CreateOrReplaceResult putData(final YangInstanceIdentifier path, final NormalizedNode data,
199             final EffectiveModelContext context, final WriteDataParams params) {
200         final var exists = TransactionUtil.syncAccess(exists(path), path);
201
202         final var insert = params.insert();
203         final ListenableFuture<? extends CommitInfo> commitFuture;
204         if (insert != null) {
205             final var parentPath = path.coerceParent();
206             checkListAndOrderedType(context, parentPath);
207             commitFuture = insertAndCommitPut(path, data, insert, params.point(), parentPath, context);
208         } else {
209             commitFuture = replaceAndCommit(prepareWriteExecution(), path, data, context);
210         }
211
212         TransactionUtil.syncCommit(commitFuture, "PUT", path);
213         return exists ? CreateOrReplaceResult.REPLACED : CreateOrReplaceResult.CREATED;
214     }
215
216     private ListenableFuture<? extends CommitInfo> insertAndCommitPut(final YangInstanceIdentifier path,
217             final NormalizedNode data, final @NonNull InsertParam insert, final @Nullable PointParam point,
218             final YangInstanceIdentifier parentPath, final EffectiveModelContext context) {
219         final var tx = prepareWriteExecution();
220
221         return switch (insert) {
222             case FIRST -> {
223                 final var readData = tx.readList(parentPath);
224                 if (readData == null || readData.isEmpty()) {
225                     yield replaceAndCommit(tx, path, data, context);
226                 }
227                 tx.remove(parentPath);
228                 tx.replace(path, data, context);
229                 tx.replace(parentPath, readData, context);
230                 yield tx.commit();
231             }
232             case LAST -> replaceAndCommit(tx, path, data, context);
233             case BEFORE -> {
234                 final var readData = tx.readList(parentPath);
235                 if (readData == null || readData.isEmpty()) {
236                     yield replaceAndCommit(tx, path, data, context);
237                 }
238                 insertWithPointPut(tx, path, data, verifyNotNull(point), readData, true, context);
239                 yield tx.commit();
240             }
241             case AFTER -> {
242                 final var readData = tx.readList(parentPath);
243                 if (readData == null || readData.isEmpty()) {
244                     yield replaceAndCommit(tx, path, data, context);
245                 }
246                 insertWithPointPut(tx, path, data, verifyNotNull(point), readData, false, context);
247                 yield tx.commit();
248             }
249         };
250     }
251
252     private static void insertWithPointPut(final RestconfTransaction tx, final YangInstanceIdentifier path,
253             final NormalizedNode data, final @NonNull PointParam point, final NormalizedNodeContainer<?> readList,
254             final boolean before, final EffectiveModelContext context) {
255         tx.remove(path.getParent());
256         final var pointArg = YangInstanceIdentifierDeserializer.create(context, point.value()).path
257             .getLastPathArgument();
258         int lastItemPosition = 0;
259         for (var nodeChild : readList.body()) {
260             if (nodeChild.name().equals(pointArg)) {
261                 break;
262             }
263             lastItemPosition++;
264         }
265         if (!before) {
266             lastItemPosition++;
267         }
268         int lastInsertedPosition = 0;
269         final var emptySubtree = ImmutableNodes.fromInstanceId(context, path.getParent());
270         tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
271         for (var nodeChild : readList.body()) {
272             if (lastInsertedPosition == lastItemPosition) {
273                 tx.replace(path, data, context);
274             }
275             final var childPath = path.coerceParent().node(nodeChild.name());
276             tx.replace(childPath, nodeChild, context);
277             lastInsertedPosition++;
278         }
279     }
280
281     private static ListenableFuture<? extends CommitInfo> replaceAndCommit(final RestconfTransaction tx,
282             final YangInstanceIdentifier path, final NormalizedNode data, final EffectiveModelContext context) {
283         tx.replace(path, data, context);
284         return tx.commit();
285     }
286
287     private static DataSchemaNode checkListAndOrderedType(final EffectiveModelContext ctx,
288             final YangInstanceIdentifier path) {
289         final var dataSchemaNode = DataSchemaContextTree.from(ctx).findChild(path).orElseThrow().dataSchemaNode();
290
291         final String message;
292         if (dataSchemaNode instanceof ListSchemaNode listSchema) {
293             if (listSchema.isUserOrdered()) {
294                 return listSchema;
295             }
296             message = "Insert parameter can be used only with ordered-by user list.";
297         } else if (dataSchemaNode instanceof LeafListSchemaNode leafListSchema) {
298             if (leafListSchema.isUserOrdered()) {
299                 return leafListSchema;
300             }
301             message = "Insert parameter can be used only with ordered-by user leaf-list.";
302         } else {
303             message = "Insert parameter can be used only with list or leaf-list";
304         }
305         throw new RestconfDocumentedException(message, ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
306     }
307
308     /**
309      * Check mount point and prepare variables for post data.
310      *
311      * @param path    path
312      * @param data    data
313      * @param context reference to actual {@link EffectiveModelContext}
314      * @param params  {@link WriteDataParams}
315      */
316     public final void postData(final YangInstanceIdentifier path, final NormalizedNode data,
317             final EffectiveModelContext context, final WriteDataParams params) {
318         final var insert = params.insert();
319         final ListenableFuture<? extends CommitInfo> future;
320         if (insert != null) {
321             final var parentPath = path.coerceParent();
322             checkListAndOrderedType(context, parentPath);
323             future = insertAndCommitPost(path, data, insert, params.point(), parentPath, context);
324         } else {
325             future = createAndCommit(prepareWriteExecution(), path, data, context);
326         }
327         TransactionUtil.syncCommit(future, "POST", path);
328     }
329
330     private ListenableFuture<? extends CommitInfo> insertAndCommitPost(final YangInstanceIdentifier path,
331             final NormalizedNode data, final @NonNull InsertParam insert, final @Nullable PointParam point,
332             final YangInstanceIdentifier parent, final EffectiveModelContext context) {
333         final var grandParent = parent.coerceParent();
334         final var tx = prepareWriteExecution();
335
336         return switch (insert) {
337             case FIRST -> {
338                 final var readData = tx.readList(grandParent);
339                 if (readData == null || readData.isEmpty()) {
340                     tx.replace(path, data, context);
341                 } else {
342                     checkItemDoesNotExists(exists(path), path);
343                     tx.remove(grandParent);
344                     tx.replace(path, data, context);
345                     tx.replace(grandParent, readData, context);
346                 }
347                 yield tx.commit();
348             }
349             case LAST -> createAndCommit(tx, path, data, context);
350             case BEFORE -> {
351                 final var readData = tx.readList(grandParent);
352                 if (readData == null || readData.isEmpty()) {
353                     tx.replace(path, data, context);
354                 } else {
355                     checkItemDoesNotExists(exists(path), path);
356                     insertWithPointPost(tx, path, data, verifyNotNull(point), readData, grandParent, true, context);
357                 }
358                 yield tx.commit();
359             }
360             case AFTER -> {
361                 final var readData = tx.readList(grandParent);
362                 if (readData == null || readData.isEmpty()) {
363                     tx.replace(path, data, context);
364                 } else {
365                     checkItemDoesNotExists(exists(path), path);
366                     insertWithPointPost(tx, path, data, verifyNotNull(point), readData, grandParent, false, context);
367                 }
368                 yield tx.commit();
369             }
370         };
371     }
372
373     private static void insertWithPointPost(final RestconfTransaction tx, final YangInstanceIdentifier path,
374             final NormalizedNode data, final PointParam point, final NormalizedNodeContainer<?> readList,
375             final YangInstanceIdentifier grandParentPath, final boolean before, final EffectiveModelContext context) {
376         tx.remove(grandParentPath);
377         final var pointArg = YangInstanceIdentifierDeserializer.create(context, point.value()).path
378             .getLastPathArgument();
379         int lastItemPosition = 0;
380         for (var nodeChild : readList.body()) {
381             if (nodeChild.name().equals(pointArg)) {
382                 break;
383             }
384             lastItemPosition++;
385         }
386         if (!before) {
387             lastItemPosition++;
388         }
389         int lastInsertedPosition = 0;
390         final var emptySubtree = ImmutableNodes.fromInstanceId(context, grandParentPath);
391         tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
392         for (var nodeChild : readList.body()) {
393             if (lastInsertedPosition == lastItemPosition) {
394                 tx.replace(path, data, context);
395             }
396             final YangInstanceIdentifier childPath = grandParentPath.node(nodeChild.name());
397             tx.replace(childPath, nodeChild, context);
398             lastInsertedPosition++;
399         }
400     }
401
402     private static ListenableFuture<? extends CommitInfo> createAndCommit(final RestconfTransaction tx,
403             final YangInstanceIdentifier path, final NormalizedNode data, final EffectiveModelContext context) {
404         try {
405             tx.create(path, data, context);
406         } catch (RestconfDocumentedException e) {
407             // close transaction if any and pass exception further
408             tx.cancel();
409             throw e;
410         }
411
412         return tx.commit();
413     }
414
415     /**
416      * Check if items do NOT already exists at specified {@code path}.
417      *
418      * @param existsFuture if checked data exists
419      * @param path         Path to be checked
420      * @throws RestconfDocumentedException if data already exists.
421      */
422     static void checkItemDoesNotExists(final ListenableFuture<Boolean> existsFuture,
423             final YangInstanceIdentifier path) {
424         if (TransactionUtil.syncAccess(existsFuture, path)) {
425             LOG.trace("Operation via Restconf was not executed because data at {} already exists", path);
426             throw new RestconfDocumentedException("Data already exists", ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS,
427                 path);
428         }
429     }
430 }