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.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;
43 * Baseline execution strategy for various RESTCONF operations.
45 * @see NetconfRestconfStrategy
46 * @see MdsalRestconfStrategy
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 {
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}.
56 public enum CreateOrReplaceResult {
58 * A new resource has been created.
62 * An existing resources has been replaced.
72 * Look up the appropriate strategy for a particular mount point.
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
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()) {
85 return mountPoint.getService(DOMDataBroker.class).map(MdsalRestconfStrategy::new);
89 * Lock the entire datastore.
91 * @return A {@link RestconfTransaction}. This transaction needs to be either committed or canceled before doing
94 public abstract RestconfTransaction prepareWriteExecution();
97 * Read data from the datastore.
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
103 public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
104 YangInstanceIdentifier path);
107 * Read data selected using fields from the datastore.
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
114 public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
115 YangInstanceIdentifier path, List<YangInstanceIdentifier> fields);
118 * Check if data already exists in the datastore.
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
124 public abstract ListenableFuture<Boolean> exists(LogicalDatastoreType store, YangInstanceIdentifier path);
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>
130 * @param path Path to delete
131 * @return A {@link RestconfFuture}
132 * @throws NullPointerException if {@code path} is {@code null}
134 public final @NonNull RestconfFuture<Empty> delete(final YangInstanceIdentifier path) {
135 final var ret = new SettableRestconfFuture<Empty>();
136 delete(ret, requireNonNull(path));
140 protected abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
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>.
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}
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));
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>() {
168 public void onSuccess(final CommitInfo result) {
169 future.set(Empty.value());
173 public void onFailure(final Throwable cause) {
174 future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path));
176 }, MoreExecutors.directExecutor());
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.
183 * @param path path of data
185 * @param context reference to {@link EffectiveModelContext}
186 * @param params {@link WriteDataParams}
187 * @return A {@link CreateOrReplaceResult}
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);
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);
200 commitFuture = replaceAndCommit(prepareWriteExecution(), path, data, context);
203 TransactionUtil.syncCommit(commitFuture, "PUT", path);
204 return exists ? CreateOrReplaceResult.REPLACED : CreateOrReplaceResult.CREATED;
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();
212 return switch (insert) {
214 final var readData = tx.readList(parentPath);
215 if (readData == null || readData.isEmpty()) {
216 yield replaceAndCommit(tx, path, data, context);
218 tx.remove(parentPath);
219 tx.replace(path, data, context);
220 tx.replace(parentPath, readData, context);
223 case LAST -> replaceAndCommit(tx, path, data, context);
225 final var readData = tx.readList(parentPath);
226 if (readData == null || readData.isEmpty()) {
227 yield replaceAndCommit(tx, path, data, context);
229 insertWithPointPut(tx, path, data, verifyNotNull(point), readData, true, context);
233 final var readData = tx.readList(parentPath);
234 if (readData == null || readData.isEmpty()) {
235 yield replaceAndCommit(tx, path, data, context);
237 insertWithPointPut(tx, path, data, verifyNotNull(point), readData, false, context);
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)) {
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);
266 final var childPath = path.coerceParent().node(nodeChild.name());
267 tx.replace(childPath, nodeChild, context);
268 lastInsertedPosition++;
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);