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 java.util.Objects.requireNonNull;
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;
41 * Baseline execution strategy for various RESTCONF operations.
43 * @see NetconfRestconfStrategy
44 * @see MdsalRestconfStrategy
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 {
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}.
54 public enum CreateOrReplaceResult {
56 * A new resource has been created.
60 * An existing resources has been replaced.
70 * Look up the appropriate strategy for a particular mount point.
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
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()) {
83 return mountPoint.getService(DOMDataBroker.class).map(MdsalRestconfStrategy::new);
87 * Lock the entire datastore.
89 * @return A {@link RestconfTransaction}. This transaction needs to be either committed or canceled before doing
92 public abstract RestconfTransaction prepareWriteExecution();
95 * Read data from the datastore.
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
101 public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
102 YangInstanceIdentifier path);
105 * Read data selected using fields from the datastore.
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
112 public abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store,
113 YangInstanceIdentifier path, List<YangInstanceIdentifier> fields);
116 * Check if data already exists in the datastore.
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
122 public abstract ListenableFuture<Boolean> exists(LogicalDatastoreType store, YangInstanceIdentifier path);
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>
128 * @param path Path to delete
129 * @return A {@link RestconfFuture}
130 * @throws NullPointerException if {@code path} is {@code null}
132 public final @NonNull RestconfFuture<Empty> delete(final YangInstanceIdentifier path) {
133 final var ret = new SettableRestconfFuture<Empty>();
134 delete(ret, requireNonNull(path));
138 protected abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
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>.
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}
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));
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>() {
166 public void onSuccess(final CommitInfo result) {
167 future.set(Empty.value());
171 public void onFailure(final Throwable cause) {
172 future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path));
174 }, MoreExecutors.directExecutor());
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.
181 * @param path path of data
183 * @param schemaContext reference to {@link EffectiveModelContext}
184 * @param params {@link WriteDataParams}
185 * @return A {@link CreateOrReplaceResult}
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;
197 * @param path path of data
198 * @param schemaContext {@link SchemaContext}
200 * @param params {@link WriteDataParams}
201 * @return A {@link ListenableFuture}
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);
211 final var parentPath = path.coerceParent();
212 PostDataTransactionUtil.checkListAndOrderedType(schemaContext, parentPath);
214 return switch (insert) {
216 final var readData = transaction.readList(parentPath);
217 if (readData == null || readData.isEmpty()) {
218 yield makePut(path, schemaContext, transaction, data);
220 transaction.remove(parentPath);
221 transaction.replace(path, data, schemaContext);
222 transaction.replace(parentPath, readData, schemaContext);
223 yield transaction.commit();
225 case LAST -> makePut(path, schemaContext, transaction, data);
227 final var readData = transaction.readList(parentPath);
228 if (readData == null || readData.isEmpty()) {
229 yield makePut(path, schemaContext, transaction, data);
231 insertWithPointPut(transaction, path, data, schemaContext, params.getPoint(), readData, true);
232 yield transaction.commit();
235 final var readData = transaction.readList(parentPath);
236 if (readData == null || readData.isEmpty()) {
237 yield makePut(path, schemaContext, transaction, data);
239 insertWithPointPut(transaction, path, data, schemaContext, params.getPoint(), readData, false);
240 yield transaction.commit();
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)) {
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);
268 final var childPath = path.coerceParent().node(nodeChild.name());
269 transaction.replace(childPath, nodeChild, schemaContext);
270 lastInsertedPosition++;
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();