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.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;
50 * Baseline execution strategy for various RESTCONF operations.
52 * @see NetconfRestconfStrategy
53 * @see MdsalRestconfStrategy
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 {
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}.
63 public enum CreateOrReplaceResult {
65 * A new resource has been created.
69 * An existing resources has been replaced.
74 private static final Logger LOG = LoggerFactory.getLogger(RestconfStrategy.class);
81 * Look up the appropriate strategy for a particular mount point.
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
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()) {
94 return mountPoint.getService(DOMDataBroker.class).map(MdsalRestconfStrategy::new);
98 * Lock the entire datastore.
100 * @return A {@link RestconfTransaction}. This transaction needs to be either committed or canceled before doing
103 public abstract RestconfTransaction prepareWriteExecution();
106 * Read data from the datastore.
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
112 public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
113 YangInstanceIdentifier path);
116 * Read data selected using fields from the datastore.
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
123 public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
124 YangInstanceIdentifier path, List<YangInstanceIdentifier> fields);
127 * Check if data already exists in the configuration datastore.
129 * @param path the data object path
130 * @return a ListenableFuture containing the result of the check
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);
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>
140 * @param path Path to delete
141 * @return A {@link RestconfFuture}
142 * @throws NullPointerException if {@code path} is {@code null}
144 public final @NonNull RestconfFuture<Empty> delete(final YangInstanceIdentifier path) {
145 final var ret = new SettableRestconfFuture<Empty>();
146 delete(ret, requireNonNull(path));
150 protected abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
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>.
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}
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));
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>() {
178 public void onSuccess(final CommitInfo result) {
179 future.set(Empty.value());
183 public void onFailure(final Throwable cause) {
184 future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path));
186 }, MoreExecutors.directExecutor());
190 * Check mount point and prepare variables for put data to DS.
192 * @param path path of data
194 * @param context reference to {@link EffectiveModelContext}
195 * @param params {@link WriteDataParams}
196 * @return A {@link CreateOrReplaceResult}
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);
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);
209 commitFuture = replaceAndCommit(prepareWriteExecution(), path, data, context);
212 TransactionUtil.syncCommit(commitFuture, "PUT", path);
213 return exists ? CreateOrReplaceResult.REPLACED : CreateOrReplaceResult.CREATED;
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();
221 return switch (insert) {
223 final var readData = tx.readList(parentPath);
224 if (readData == null || readData.isEmpty()) {
225 yield replaceAndCommit(tx, path, data, context);
227 tx.remove(parentPath);
228 tx.replace(path, data, context);
229 tx.replace(parentPath, readData, context);
232 case LAST -> replaceAndCommit(tx, path, data, context);
234 final var readData = tx.readList(parentPath);
235 if (readData == null || readData.isEmpty()) {
236 yield replaceAndCommit(tx, path, data, context);
238 insertWithPointPut(tx, path, data, verifyNotNull(point), readData, true, context);
242 final var readData = tx.readList(parentPath);
243 if (readData == null || readData.isEmpty()) {
244 yield replaceAndCommit(tx, path, data, context);
246 insertWithPointPut(tx, path, data, verifyNotNull(point), readData, false, context);
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)) {
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);
275 final var childPath = path.coerceParent().node(nodeChild.name());
276 tx.replace(childPath, nodeChild, context);
277 lastInsertedPosition++;
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);
287 private static DataSchemaNode checkListAndOrderedType(final EffectiveModelContext ctx,
288 final YangInstanceIdentifier path) {
289 final var dataSchemaNode = DataSchemaContextTree.from(ctx).findChild(path).orElseThrow().dataSchemaNode();
291 final String message;
292 if (dataSchemaNode instanceof ListSchemaNode listSchema) {
293 if (listSchema.isUserOrdered()) {
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;
301 message = "Insert parameter can be used only with ordered-by user leaf-list.";
303 message = "Insert parameter can be used only with list or leaf-list";
305 throw new RestconfDocumentedException(message, ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
309 * Check mount point and prepare variables for post data.
313 * @param context reference to actual {@link EffectiveModelContext}
314 * @param params {@link WriteDataParams}
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);
325 future = createAndCommit(prepareWriteExecution(), path, data, context);
327 TransactionUtil.syncCommit(future, "POST", path);
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();
336 return switch (insert) {
338 final var readData = tx.readList(grandParent);
339 if (readData == null || readData.isEmpty()) {
340 tx.replace(path, data, context);
342 checkItemDoesNotExists(exists(path), path);
343 tx.remove(grandParent);
344 tx.replace(path, data, context);
345 tx.replace(grandParent, readData, context);
349 case LAST -> createAndCommit(tx, path, data, context);
351 final var readData = tx.readList(grandParent);
352 if (readData == null || readData.isEmpty()) {
353 tx.replace(path, data, context);
355 checkItemDoesNotExists(exists(path), path);
356 insertWithPointPost(tx, path, data, verifyNotNull(point), readData, grandParent, true, context);
361 final var readData = tx.readList(grandParent);
362 if (readData == null || readData.isEmpty()) {
363 tx.replace(path, data, context);
365 checkItemDoesNotExists(exists(path), path);
366 insertWithPointPost(tx, path, data, verifyNotNull(point), readData, grandParent, false, context);
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)) {
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);
396 final YangInstanceIdentifier childPath = grandParentPath.node(nodeChild.name());
397 tx.replace(childPath, nodeChild, context);
398 lastInsertedPosition++;
402 private static ListenableFuture<? extends CommitInfo> createAndCommit(final RestconfTransaction tx,
403 final YangInstanceIdentifier path, final NormalizedNode data, final EffectiveModelContext context) {
405 tx.create(path, data, context);
406 } catch (RestconfDocumentedException e) {
407 // close transaction if any and pass exception further
416 * Check if items do NOT already exists at specified {@code path}.
418 * @param existsFuture if checked data exists
419 * @param path Path to be checked
420 * @throws RestconfDocumentedException if data already exists.
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,