Eliminate PutDataTransactionUtil
[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 java.util.Objects.requireNonNull;
11
12 import com.google.common.util.concurrent.FutureCallback;
13 import com.google.common.util.concurrent.Futures;
14 import com.google.common.util.concurrent.ListenableFuture;
15 import com.google.common.util.concurrent.MoreExecutors;
16 import java.util.List;
17 import java.util.Optional;
18 import org.eclipse.jdt.annotation.NonNull;
19 import org.opendaylight.mdsal.common.api.CommitInfo;
20 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
21 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
22 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
23 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
24 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
25 import org.opendaylight.restconf.api.query.PointParam;
26 import org.opendaylight.restconf.common.errors.RestconfFuture;
27 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
28 import org.opendaylight.restconf.nb.rfc8040.WriteDataParams;
29 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PostDataTransactionUtil;
30 import org.opendaylight.restconf.nb.rfc8040.rests.utils.TransactionUtil;
31 import org.opendaylight.restconf.nb.rfc8040.utils.parser.YangInstanceIdentifierDeserializer;
32 import org.opendaylight.yangtools.yang.common.Empty;
33 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
34 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
35 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
36 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
37 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
38 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
39
40 /**
41  * Baseline execution strategy for various RESTCONF operations.
42  *
43  * @see NetconfRestconfStrategy
44  * @see MdsalRestconfStrategy
45  */
46 // FIXME: it seems the first three operations deal with lifecycle of a transaction, while others invoke various
47 //        operations. This should be handled through proper allocation indirection.
48 public abstract class RestconfStrategy {
49     /**
50      * Result of a {@code PUT} request as defined in
51      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.5">RFC8040 section 4.5</a>. The definition makes it
52      * clear that the logical operation is {@code create-or-replace}.
53      */
54     public enum CreateOrReplaceResult {
55         /**
56          * A new resource has been created.
57          */
58         CREATED,
59         /*
60          * An existing resources has been replaced.
61          */
62         REPLACED;
63     }
64
65     RestconfStrategy() {
66         // Hidden on purpose
67     }
68
69     /**
70      * Look up the appropriate strategy for a particular mount point.
71      *
72      * @param mountPoint Target mount point
73      * @return A strategy, or null if the mount point does not expose a supported interface
74      * @throws NullPointerException if {@code mountPoint} is null
75      */
76     public static Optional<RestconfStrategy> forMountPoint(final DOMMountPoint mountPoint) {
77         final Optional<RestconfStrategy> netconf = mountPoint.getService(NetconfDataTreeService.class)
78             .map(NetconfRestconfStrategy::new);
79         if (netconf.isPresent()) {
80             return netconf;
81         }
82
83         return mountPoint.getService(DOMDataBroker.class).map(MdsalRestconfStrategy::new);
84     }
85
86     /**
87      * Lock the entire datastore.
88      *
89      * @return A {@link RestconfTransaction}. This transaction needs to be either committed or canceled before doing
90      *         anything else.
91      */
92     public abstract RestconfTransaction prepareWriteExecution();
93
94     /**
95      * Read data from the datastore.
96      *
97      * @param store the logical data store which should be modified
98      * @param path the data object path
99      * @return a ListenableFuture containing the result of the read
100      */
101     public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
102         YangInstanceIdentifier path);
103
104     /**
105      * Read data selected using fields from the datastore.
106      *
107      * @param store the logical data store which should be modified
108      * @param path the parent data object path
109      * @param fields paths to selected fields relative to parent path
110      * @return a ListenableFuture containing the result of the read
111      */
112     public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
113             YangInstanceIdentifier path, List<YangInstanceIdentifier> fields);
114
115     /**
116      * Check if data already exists in the datastore.
117      *
118      * @param store the logical data store which should be modified
119      * @param path the data object path
120      * @return a FluentFuture containing the result of the check
121      */
122     public abstract ListenableFuture<Boolean> exists(LogicalDatastoreType store, YangInstanceIdentifier path);
123
124     /**
125      * Delete data from the configuration datastore. If the data does not exist, this operation will fail, as outlined
126      * in <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.7">RFC8040 section 4.7</a>
127      *
128      * @param path Path to delete
129      * @return A {@link RestconfFuture}
130      * @throws NullPointerException if {@code path} is {@code null}
131      */
132     public final @NonNull RestconfFuture<Empty> delete(final YangInstanceIdentifier path) {
133         final var ret = new SettableRestconfFuture<Empty>();
134         delete(ret, requireNonNull(path));
135         return ret;
136     }
137
138     protected abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
139
140     /**
141      * Merge data into the configuration datastore, as outlined in
142      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040 section 4.6.1</a>.
143      *
144      * @param path Path to merge
145      * @param data Data to merge
146      * @param context Corresponding EffectiveModelContext
147      * @return A {@link RestconfFuture}
148      * @throws NullPointerException if any argument is {@code null}
149      */
150     public final @NonNull RestconfFuture<Empty> merge(final YangInstanceIdentifier path, final NormalizedNode data,
151             final EffectiveModelContext context) {
152         final var ret = new SettableRestconfFuture<Empty>();
153         merge(ret, requireNonNull(path), requireNonNull(data), requireNonNull(context));
154         return ret;
155     }
156
157     private void merge(final @NonNull SettableRestconfFuture<Empty> future,
158             final @NonNull YangInstanceIdentifier path, final @NonNull NormalizedNode data,
159             final @NonNull EffectiveModelContext context) {
160         final var tx = prepareWriteExecution();
161         // FIXME: this method should be further specialized to eliminate this call -- it is only needed for MD-SAL
162         TransactionUtil.ensureParentsByMerge(path, context, tx);
163         tx.merge(path, data);
164         Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
165             @Override
166             public void onSuccess(final CommitInfo result) {
167                 future.set(Empty.value());
168             }
169
170             @Override
171             public void onFailure(final Throwable cause) {
172                 future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path));
173             }
174         }, MoreExecutors.directExecutor());
175     }
176
177     /**
178      * Check mount point and prepare variables for put data to DS. Close {@link DOMTransactionChain} if any
179      * inside of object {@link RestconfStrategy} provided as a parameter if any.
180      *
181      * @param path          path of data
182      * @param data          data
183      * @param schemaContext reference to {@link EffectiveModelContext}
184      * @param params        {@link WriteDataParams}
185      * @return A {@link CreateOrReplaceResult}
186      */
187     public @NonNull CreateOrReplaceResult putData(final YangInstanceIdentifier path, final NormalizedNode data,
188             final EffectiveModelContext schemaContext, final WriteDataParams params) {
189         final var exists = TransactionUtil.syncAccess(exists(LogicalDatastoreType.CONFIGURATION, path), path);
190         TransactionUtil.syncCommit(submitData(path, schemaContext, data, params), "PUT", path);
191         return exists ? CreateOrReplaceResult.REPLACED : CreateOrReplaceResult.CREATED;
192     }
193
194     /**
195      * Put data to DS.
196      *
197      * @param path          path of data
198      * @param schemaContext {@link SchemaContext}
199      * @param data          data
200      * @param params        {@link WriteDataParams}
201      * @return A {@link ListenableFuture}
202      */
203     private ListenableFuture<? extends CommitInfo> submitData(final YangInstanceIdentifier path,
204             final EffectiveModelContext schemaContext, final NormalizedNode data, final WriteDataParams params) {
205         final var transaction = prepareWriteExecution();
206         final var insert = params.insert();
207         if (insert == null) {
208             return makePut(path, schemaContext, transaction, data);
209         }
210
211         final var parentPath = path.coerceParent();
212         PostDataTransactionUtil.checkListAndOrderedType(schemaContext, parentPath);
213
214         return switch (insert) {
215             case FIRST -> {
216                 final var readData = transaction.readList(parentPath);
217                 if (readData == null || readData.isEmpty()) {
218                     yield makePut(path, schemaContext, transaction, data);
219                 }
220                 transaction.remove(parentPath);
221                 transaction.replace(path, data, schemaContext);
222                 transaction.replace(parentPath, readData, schemaContext);
223                 yield transaction.commit();
224             }
225             case LAST -> makePut(path, schemaContext, transaction, data);
226             case BEFORE -> {
227                 final var readData = transaction.readList(parentPath);
228                 if (readData == null || readData.isEmpty()) {
229                     yield makePut(path, schemaContext, transaction, data);
230                 }
231                 insertWithPointPut(transaction, path, data, schemaContext, params.getPoint(), readData, true);
232                 yield transaction.commit();
233             }
234             case AFTER -> {
235                 final var readData = transaction.readList(parentPath);
236                 if (readData == null || readData.isEmpty()) {
237                     yield makePut(path, schemaContext, transaction, data);
238                 }
239                 insertWithPointPut(transaction, path, data, schemaContext, params.getPoint(), readData, false);
240                 yield transaction.commit();
241             }
242         };
243     }
244
245     private static void insertWithPointPut(final RestconfTransaction transaction, final YangInstanceIdentifier path,
246             final NormalizedNode data, final EffectiveModelContext schemaContext, final PointParam point,
247             final NormalizedNodeContainer<?> readList, final boolean before) {
248         transaction.remove(path.getParent());
249         final var pointArg = YangInstanceIdentifierDeserializer.create(schemaContext, point.value()).path
250             .getLastPathArgument();
251         int lastItemPosition = 0;
252         for (var nodeChild : readList.body()) {
253             if (nodeChild.name().equals(pointArg)) {
254                 break;
255             }
256             lastItemPosition++;
257         }
258         if (!before) {
259             lastItemPosition++;
260         }
261         int lastInsertedPosition = 0;
262         final var emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path.getParent());
263         transaction.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
264         for (var nodeChild : readList.body()) {
265             if (lastInsertedPosition == lastItemPosition) {
266                 transaction.replace(path, data, schemaContext);
267             }
268             final var childPath = path.coerceParent().node(nodeChild.name());
269             transaction.replace(childPath, nodeChild, schemaContext);
270             lastInsertedPosition++;
271         }
272     }
273
274     private static ListenableFuture<? extends CommitInfo> makePut(final YangInstanceIdentifier path,
275             final EffectiveModelContext schemaContext, final RestconfTransaction transaction,
276             final NormalizedNode data) {
277         transaction.replace(path, data, schemaContext);
278         return transaction.commit();
279     }
280 }