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