Refactor pretty printing
[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 import static org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes.fromInstanceId;
13
14 import com.google.common.annotations.VisibleForTesting;
15 import com.google.common.collect.ImmutableMap;
16 import com.google.common.collect.ImmutableSet;
17 import com.google.common.collect.ImmutableSetMultimap;
18 import com.google.common.io.CharSource;
19 import com.google.common.util.concurrent.FutureCallback;
20 import com.google.common.util.concurrent.Futures;
21 import com.google.common.util.concurrent.ListenableFuture;
22 import com.google.common.util.concurrent.MoreExecutors;
23 import java.io.IOException;
24 import java.net.URI;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.Comparator;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Map.Entry;
32 import java.util.NoSuchElementException;
33 import java.util.Optional;
34 import java.util.concurrent.CancellationException;
35 import java.util.function.BiFunction;
36 import java.util.function.Function;
37 import java.util.stream.Collectors;
38 import org.eclipse.jdt.annotation.NonNull;
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.opendaylight.mdsal.common.api.CommitInfo;
42 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
43 import org.opendaylight.mdsal.dom.api.DOMActionException;
44 import org.opendaylight.mdsal.dom.api.DOMActionResult;
45 import org.opendaylight.mdsal.dom.api.DOMActionService;
46 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
47 import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
48 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
49 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
50 import org.opendaylight.mdsal.dom.api.DOMRpcResult;
51 import org.opendaylight.mdsal.dom.api.DOMRpcService;
52 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
53 import org.opendaylight.mdsal.dom.api.DOMSchemaService.YangTextSourceExtension;
54 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
55 import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
56 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
57 import org.opendaylight.restconf.api.ApiPath;
58 import org.opendaylight.restconf.api.query.ContentParam;
59 import org.opendaylight.restconf.api.query.WithDefaultsParam;
60 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
61 import org.opendaylight.restconf.common.errors.RestconfError;
62 import org.opendaylight.restconf.common.errors.RestconfFuture;
63 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
64 import org.opendaylight.restconf.common.patch.PatchContext;
65 import org.opendaylight.restconf.common.patch.PatchStatusContext;
66 import org.opendaylight.restconf.common.patch.PatchStatusEntity;
67 import org.opendaylight.restconf.nb.rfc8040.Insert;
68 import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
69 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
70 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
71 import org.opendaylight.restconf.server.api.ChildBody;
72 import org.opendaylight.restconf.server.api.ConfigurationMetadata;
73 import org.opendaylight.restconf.server.api.DataGetParams;
74 import org.opendaylight.restconf.server.api.DataGetResult;
75 import org.opendaylight.restconf.server.api.DataPatchResult;
76 import org.opendaylight.restconf.server.api.DataPostBody;
77 import org.opendaylight.restconf.server.api.DataPostResult;
78 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
79 import org.opendaylight.restconf.server.api.DataPutResult;
80 import org.opendaylight.restconf.server.api.DataYangPatchResult;
81 import org.opendaylight.restconf.server.api.DatabindContext;
82 import org.opendaylight.restconf.server.api.DatabindPath;
83 import org.opendaylight.restconf.server.api.DatabindPath.Action;
84 import org.opendaylight.restconf.server.api.DatabindPath.Data;
85 import org.opendaylight.restconf.server.api.DatabindPath.InstanceReference;
86 import org.opendaylight.restconf.server.api.DatabindPath.OperationPath;
87 import org.opendaylight.restconf.server.api.DatabindPath.Rpc;
88 import org.opendaylight.restconf.server.api.InvokeParams;
89 import org.opendaylight.restconf.server.api.InvokeResult;
90 import org.opendaylight.restconf.server.api.OperationInputBody;
91 import org.opendaylight.restconf.server.api.OperationOutputBody;
92 import org.opendaylight.restconf.server.api.OperationsGetResult;
93 import org.opendaylight.restconf.server.api.PatchBody;
94 import org.opendaylight.restconf.server.api.ResourceBody;
95 import org.opendaylight.restconf.server.spi.ApiPathCanonizer;
96 import org.opendaylight.restconf.server.spi.ApiPathNormalizer;
97 import org.opendaylight.restconf.server.spi.DefaultResourceContext;
98 import org.opendaylight.restconf.server.spi.OperationInput;
99 import org.opendaylight.restconf.server.spi.RpcImplementation;
100 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.with.defaults.rev110601.WithDefaultsMode;
101 import org.opendaylight.yangtools.yang.common.Empty;
102 import org.opendaylight.yangtools.yang.common.ErrorTag;
103 import org.opendaylight.yangtools.yang.common.ErrorType;
104 import org.opendaylight.yangtools.yang.common.QName;
105 import org.opendaylight.yangtools.yang.common.QNameModule;
106 import org.opendaylight.yangtools.yang.common.Revision;
107 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
108 import org.opendaylight.yangtools.yang.common.XMLNamespace;
109 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
110 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
111 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
112 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
113 import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
114 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
115 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
116 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
117 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
118 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
119 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
120 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
121 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
122 import org.opendaylight.yangtools.yang.data.api.schema.SystemLeafSetNode;
123 import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode;
124 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
125 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode;
126 import org.opendaylight.yangtools.yang.data.api.schema.UserLeafSetNode;
127 import org.opendaylight.yangtools.yang.data.api.schema.UserMapNode;
128 import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder;
129 import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
130 import org.opendaylight.yangtools.yang.data.api.schema.builder.NormalizedNodeContainerBuilder;
131 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
132 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
133 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
134 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
135 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
136 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
137 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
138 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
139 import org.opendaylight.yangtools.yang.model.api.source.SourceIdentifier;
140 import org.opendaylight.yangtools.yang.model.api.source.SourceRepresentation;
141 import org.opendaylight.yangtools.yang.model.api.source.YangTextSource;
142 import org.opendaylight.yangtools.yang.model.api.source.YinTextSource;
143 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
144 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
145 import org.opendaylight.yangtools.yang.model.api.stmt.SubmoduleEffectiveStatement;
146 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
147 import org.slf4j.Logger;
148 import org.slf4j.LoggerFactory;
149
150 /**
151  * Baseline execution strategy for various RESTCONF operations.
152  *
153  * @see NetconfRestconfStrategy
154  * @see MdsalRestconfStrategy
155  */
156 // FIXME: it seems the first three operations deal with lifecycle of a transaction, while others invoke various
157 //        operations. This should be handled through proper allocation indirection.
158 public abstract class RestconfStrategy {
159     @NonNullByDefault
160     public record StrategyAndPath(RestconfStrategy strategy, Data path) {
161         public StrategyAndPath {
162             requireNonNull(strategy);
163             requireNonNull(path);
164         }
165     }
166
167     /**
168      * Result of a partial {@link ApiPath} lookup for the purposes of supporting {@code yang-ext:mount}-delimited mount
169      * points with possible nesting.
170      *
171      * @param strategy the strategy to use
172      * @param tail the {@link ApiPath} tail to use with the strategy
173      */
174     @NonNullByDefault
175     public record StrategyAndTail(RestconfStrategy strategy, ApiPath tail) {
176         public StrategyAndTail {
177             requireNonNull(strategy);
178             requireNonNull(tail);
179         }
180     }
181
182     private static final Logger LOG = LoggerFactory.getLogger(RestconfStrategy.class);
183     private static final @NonNull DataPutResult PUT_CREATED = new DataPutResult(true);
184     private static final @NonNull DataPutResult PUT_REPLACED = new DataPutResult(false);
185     private static final @NonNull DataPatchResult PATCH_EMPTY = new DataPatchResult();
186
187     private final @NonNull ImmutableMap<QName, RpcImplementation> localRpcs;
188     private final @NonNull ApiPathNormalizer pathNormalizer;
189     private final @NonNull DatabindContext databind;
190     private final YangTextSourceExtension sourceProvider;
191     private final DOMMountPointService mountPointService;
192     private final DOMActionService actionService;
193     private final DOMRpcService rpcService;
194
195     RestconfStrategy(final DatabindContext databind, final ImmutableMap<QName, RpcImplementation> localRpcs,
196             final @Nullable DOMRpcService rpcService, final @Nullable DOMActionService actionService,
197             final YangTextSourceExtension sourceProvider, final @Nullable DOMMountPointService mountPointService) {
198         this.databind = requireNonNull(databind);
199         this.localRpcs = requireNonNull(localRpcs);
200         this.rpcService = rpcService;
201         this.actionService = actionService;
202         this.sourceProvider = sourceProvider;
203         this.mountPointService = mountPointService;
204         pathNormalizer = new ApiPathNormalizer(databind);
205     }
206
207     public final @NonNull StrategyAndPath resolveStrategyPath(final ApiPath path) {
208         final var andTail = resolveStrategy(path);
209         final var strategy = andTail.strategy();
210         return new StrategyAndPath(strategy, strategy.pathNormalizer.normalizeDataPath(andTail.tail()));
211     }
212
213     /**
214      * Resolve any and all {@code yang-ext:mount} to the target {@link StrategyAndTail}.
215      *
216      * @param path {@link ApiPath} to resolve
217      * @return A strategy and the remaining path
218      * @throws NullPointerException if {@code path} is {@code null}
219      */
220     public final @NonNull StrategyAndTail resolveStrategy(final ApiPath path) {
221         var mount = path.indexOf("yang-ext", "mount");
222         if (mount == -1) {
223             return new StrategyAndTail(this, path);
224         }
225         if (mountPointService == null) {
226             throw new RestconfDocumentedException("Mount point service is not available",
227                 ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED);
228         }
229         final var mountPath = path.subPath(0, mount);
230         final var dataPath = pathNormalizer.normalizeDataPath(path.subPath(0, mount));
231         final var mountPoint = mountPointService.getMountPoint(dataPath.instance())
232             .orElseThrow(() -> new RestconfDocumentedException("Mount point '" + mountPath + "' does not exist",
233                 ErrorType.PROTOCOL, ErrorTags.RESOURCE_DENIED_TRANSPORT));
234
235         return createStrategy(mountPath, mountPoint).resolveStrategy(path.subPath(mount + 1));
236     }
237
238     private static @NonNull RestconfStrategy createStrategy(final ApiPath mountPath, final DOMMountPoint mountPoint) {
239         final var mountSchemaService = mountPoint.getService(DOMSchemaService.class)
240             .orElseThrow(() -> new RestconfDocumentedException(
241                 "Mount point '" + mountPath + "' does not expose DOMSchemaService",
242                 ErrorType.PROTOCOL, ErrorTags.RESOURCE_DENIED_TRANSPORT));
243         final var mountModelContext = mountSchemaService.getGlobalContext();
244         if (mountModelContext == null) {
245             throw new RestconfDocumentedException("Mount point '" + mountPath + "' does not have any models",
246                 ErrorType.PROTOCOL, ErrorTags.RESOURCE_DENIED_TRANSPORT);
247         }
248         final var mountDatabind = DatabindContext.ofModel(mountModelContext);
249         final var mountPointService = mountPoint.getService(DOMMountPointService.class).orElse(null);
250         final var rpcService = mountPoint.getService(DOMRpcService.class).orElse(null);
251         final var actionService = mountPoint.getService(DOMActionService.class).orElse(null);
252         final var sourceProvider = mountPoint.getService(DOMSchemaService.class)
253             .flatMap(schema -> Optional.ofNullable(schema.extension(YangTextSourceExtension.class)))
254             .orElse(null);
255
256         final var netconfService = mountPoint.getService(NetconfDataTreeService.class);
257         if (netconfService.isPresent()) {
258             return new NetconfRestconfStrategy(mountDatabind, netconfService.orElseThrow(), rpcService, actionService,
259                 sourceProvider, mountPointService);
260         }
261         final var dataBroker = mountPoint.getService(DOMDataBroker.class);
262         if (dataBroker.isPresent()) {
263             return new MdsalRestconfStrategy(mountDatabind, dataBroker.orElseThrow(), rpcService, actionService,
264                 sourceProvider, mountPointService);
265         }
266         LOG.warn("Mount point {} does not expose a suitable access interface", mountPath);
267         throw new RestconfDocumentedException("Could not find a supported access interface in mount point",
268             ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, mountPoint.getIdentifier());
269     }
270
271     public final @NonNull DatabindContext databind() {
272         return databind;
273     }
274
275     public final @NonNull EffectiveModelContext modelContext() {
276         return databind.modelContext();
277     }
278
279     /**
280      * Lock the entire datastore.
281      *
282      * @return A {@link RestconfTransaction}. This transaction needs to be either committed or canceled before doing
283      *         anything else.
284      */
285     abstract RestconfTransaction prepareWriteExecution();
286
287     /**
288      * Read data from the datastore.
289      *
290      * @param store the logical data store which should be modified
291      * @param path the data object path
292      * @return a ListenableFuture containing the result of the read
293      */
294     abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store, YangInstanceIdentifier path);
295
296     /**
297      * Check if data already exists in the configuration datastore.
298      *
299      * @param path the data object path
300      * @return a ListenableFuture containing the result of the check
301      */
302     // FIXME: this method should be hosted in RestconfTransaction
303     // FIXME: this method should only be needed in MdsalRestconfStrategy
304     abstract ListenableFuture<Boolean> exists(YangInstanceIdentifier path);
305
306     @VisibleForTesting
307     final @NonNull RestconfFuture<DataPatchResult> merge(final YangInstanceIdentifier path, final NormalizedNode data) {
308         final var ret = new SettableRestconfFuture<DataPatchResult>();
309         merge(ret, requireNonNull(path), requireNonNull(data));
310         return ret;
311     }
312
313     private void merge(final @NonNull SettableRestconfFuture<DataPatchResult> future,
314             final @NonNull YangInstanceIdentifier path, final @NonNull NormalizedNode data) {
315         final var tx = prepareWriteExecution();
316         // FIXME: this method should be further specialized to eliminate this call -- it is only needed for MD-SAL
317         tx.ensureParentsByMerge(path);
318         tx.merge(path, data);
319         Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
320             @Override
321             public void onSuccess(final CommitInfo result) {
322                 // TODO: extract details once CommitInfo can communicate them
323                 future.set(PATCH_EMPTY);
324             }
325
326             @Override
327             public void onFailure(final Throwable cause) {
328                 future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path, modelContext()));
329             }
330         }, MoreExecutors.directExecutor());
331     }
332
333     public @NonNull RestconfFuture<DataPutResult> dataPUT(final ApiPath apiPath, final ResourceBody body,
334             final Map<String, String> queryParameters) {
335         final Data path;
336         try {
337             path = pathNormalizer.normalizeDataPath(apiPath);
338         } catch (RestconfDocumentedException e) {
339             return RestconfFuture.failed(e);
340         }
341
342         final Insert insert;
343         try {
344             insert = Insert.ofQueryParameters(databind, queryParameters);
345         } catch (IllegalArgumentException e) {
346             return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
347                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
348         }
349         final NormalizedNode data;
350         try {
351             data = body.toNormalizedNode(path);
352         } catch (RestconfDocumentedException e) {
353             return RestconfFuture.failed(e);
354         }
355         return putData(path.instance(), data, insert);
356     }
357
358     /**
359      * Check mount point and prepare variables for put data to DS.
360      *
361      * @param path    path of data
362      * @param data    data
363      * @param insert  {@link Insert}
364      * @return A {@link DataPutResult}
365      */
366     public final @NonNull RestconfFuture<DataPutResult> putData(final YangInstanceIdentifier path,
367             final NormalizedNode data, final @Nullable Insert insert) {
368         final var exists = TransactionUtil.syncAccess(exists(path), path);
369
370         final ListenableFuture<? extends CommitInfo> commitFuture;
371         if (insert != null) {
372             final var parentPath = path.coerceParent();
373             checkListAndOrderedType(parentPath);
374             commitFuture = insertAndCommitPut(path, data, insert, parentPath);
375         } else {
376             commitFuture = replaceAndCommit(prepareWriteExecution(), path, data);
377         }
378
379         final var ret = new SettableRestconfFuture<DataPutResult>();
380
381         Futures.addCallback(commitFuture, new FutureCallback<CommitInfo>() {
382             @Override
383             public void onSuccess(final CommitInfo result) {
384                 ret.set(exists ? PUT_REPLACED : PUT_CREATED);
385             }
386
387             @Override
388             public void onFailure(final Throwable cause) {
389                 ret.setFailure(TransactionUtil.decodeException(cause, "PUT", path, modelContext()));
390             }
391         }, MoreExecutors.directExecutor());
392
393         return ret;
394     }
395
396     private ListenableFuture<? extends CommitInfo> insertAndCommitPut(final YangInstanceIdentifier path,
397             final NormalizedNode data, final @NonNull Insert insert, final YangInstanceIdentifier parentPath) {
398         final var tx = prepareWriteExecution();
399
400         return switch (insert.insert()) {
401             case FIRST -> {
402                 final var readData = tx.readList(parentPath);
403                 if (readData == null || readData.isEmpty()) {
404                     yield replaceAndCommit(tx, path, data);
405                 }
406                 tx.remove(parentPath);
407                 tx.replace(path, data);
408                 tx.replace(parentPath, readData);
409                 yield tx.commit();
410             }
411             case LAST -> replaceAndCommit(tx, path, data);
412             case BEFORE -> {
413                 final var readData = tx.readList(parentPath);
414                 if (readData == null || readData.isEmpty()) {
415                     yield replaceAndCommit(tx, path, data);
416                 }
417                 insertWithPointPut(tx, path, data, verifyNotNull(insert.pointArg()), readData, true);
418                 yield tx.commit();
419             }
420             case AFTER -> {
421                 final var readData = tx.readList(parentPath);
422                 if (readData == null || readData.isEmpty()) {
423                     yield replaceAndCommit(tx, path, data);
424                 }
425                 insertWithPointPut(tx, path, data, verifyNotNull(insert.pointArg()), readData, false);
426                 yield tx.commit();
427             }
428         };
429     }
430
431     private void insertWithPointPut(final RestconfTransaction tx, final YangInstanceIdentifier path,
432             final NormalizedNode data, final @NonNull PathArgument pointArg, final NormalizedNodeContainer<?> readList,
433             final boolean before) {
434         tx.remove(path.getParent());
435
436         int lastItemPosition = 0;
437         for (var nodeChild : readList.body()) {
438             if (nodeChild.name().equals(pointArg)) {
439                 break;
440             }
441             lastItemPosition++;
442         }
443         if (!before) {
444             lastItemPosition++;
445         }
446
447         int lastInsertedPosition = 0;
448         final var emptySubtree = fromInstanceId(modelContext(), path.getParent());
449         tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
450         for (var nodeChild : readList.body()) {
451             if (lastInsertedPosition == lastItemPosition) {
452                 tx.replace(path, data);
453             }
454             final var childPath = path.coerceParent().node(nodeChild.name());
455             tx.replace(childPath, nodeChild);
456             lastInsertedPosition++;
457         }
458
459         // In case we are inserting after last element
460         if (!before) {
461             if (lastInsertedPosition == lastItemPosition) {
462                 tx.replace(path, data);
463             }
464         }
465     }
466
467     private static ListenableFuture<? extends CommitInfo> replaceAndCommit(final RestconfTransaction tx,
468             final YangInstanceIdentifier path, final NormalizedNode data) {
469         tx.replace(path, data);
470         return tx.commit();
471     }
472
473     private DataSchemaNode checkListAndOrderedType(final YangInstanceIdentifier path) {
474         // FIXME: we have this available in InstanceIdentifierContext
475         final var dataSchemaNode = databind.schemaTree().findChild(path).orElseThrow().dataSchemaNode();
476
477         final String message;
478         if (dataSchemaNode instanceof ListSchemaNode listSchema) {
479             if (listSchema.isUserOrdered()) {
480                 return listSchema;
481             }
482             message = "Insert parameter can be used only with ordered-by user list.";
483         } else if (dataSchemaNode instanceof LeafListSchemaNode leafListSchema) {
484             if (leafListSchema.isUserOrdered()) {
485                 return leafListSchema;
486             }
487             message = "Insert parameter can be used only with ordered-by user leaf-list.";
488         } else {
489             message = "Insert parameter can be used only with list or leaf-list";
490         }
491         throw new RestconfDocumentedException(message, ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
492     }
493
494     /**
495      * Check mount point and prepare variables for post data.
496      *
497      * @param path    path
498      * @param data    data
499      * @param insert  {@link Insert}
500      * @return A {@link RestconfFuture}
501      */
502     public final @NonNull RestconfFuture<CreateResource> postData(final YangInstanceIdentifier path,
503             final NormalizedNode data, final @Nullable Insert insert) {
504         final ListenableFuture<? extends CommitInfo> future;
505         if (insert != null) {
506             checkListAndOrderedType(path);
507             future = insertAndCommitPost(path, data, insert);
508         } else {
509             future = createAndCommit(prepareWriteExecution(), path, data);
510         }
511
512         final var ret = new SettableRestconfFuture<CreateResource>();
513         Futures.addCallback(future, new FutureCallback<CommitInfo>() {
514             @Override
515             public void onSuccess(final CommitInfo result) {
516                 ret.set(new CreateResource(new ApiPathCanonizer(databind).dataToApiPath(
517                     data instanceof MapNode mapData && !mapData.isEmpty()
518                         ? path.node(mapData.body().iterator().next().name()) : path).toString()));
519             }
520
521             @Override
522             public void onFailure(final Throwable cause) {
523                 ret.setFailure(TransactionUtil.decodeException(cause, "POST", path, modelContext()));
524             }
525
526         }, MoreExecutors.directExecutor());
527         return ret;
528     }
529
530     private ListenableFuture<? extends CommitInfo> insertAndCommitPost(final YangInstanceIdentifier path,
531             final NormalizedNode data, final @NonNull Insert insert) {
532         final var tx = prepareWriteExecution();
533
534         return switch (insert.insert()) {
535             case FIRST -> {
536                 final var readData = tx.readList(path);
537                 if (readData == null || readData.isEmpty()) {
538                     tx.replace(path, data);
539                 } else {
540                     checkListDataDoesNotExist(path, data);
541                     tx.remove(path);
542                     tx.replace(path, data);
543                     tx.replace(path, readData);
544                 }
545                 yield tx.commit();
546             }
547             case LAST -> createAndCommit(tx, path, data);
548             case BEFORE -> {
549                 final var readData = tx.readList(path);
550                 if (readData == null || readData.isEmpty()) {
551                     tx.replace(path, data);
552                 } else {
553                     checkListDataDoesNotExist(path, data);
554                     insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, true);
555                 }
556                 yield tx.commit();
557             }
558             case AFTER -> {
559                 final var readData = tx.readList(path);
560                 if (readData == null || readData.isEmpty()) {
561                     tx.replace(path, data);
562                 } else {
563                     checkListDataDoesNotExist(path, data);
564                     insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, false);
565                 }
566                 yield tx.commit();
567             }
568         };
569     }
570
571     /**
572      * Merge data into the configuration datastore, as outlined in
573      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040 section 4.6.1</a>.
574      *
575      * @param apiPath Path to merge
576      * @param body Data to merge
577      * @return A {@link RestconfFuture}
578      * @throws NullPointerException if any argument is {@code null}
579      */
580     public final @NonNull RestconfFuture<DataPatchResult> dataPATCH(final ApiPath apiPath, final ResourceBody body) {
581         final Data path;
582         try {
583             path = pathNormalizer.normalizeDataPath(apiPath);
584         } catch (RestconfDocumentedException e) {
585             return RestconfFuture.failed(e);
586         }
587
588         final NormalizedNode data;
589         try {
590             data = body.toNormalizedNode(path);
591         } catch (RestconfDocumentedException e) {
592             return RestconfFuture.failed(e);
593         }
594
595         return merge(path.instance(), data);
596     }
597
598     public final @NonNull RestconfFuture<DataYangPatchResult> dataPATCH(final ApiPath apiPath, final PatchBody body) {
599         final Data path;
600         try {
601             path = pathNormalizer.normalizeDataPath(apiPath);
602         } catch (RestconfDocumentedException e) {
603             return RestconfFuture.failed(e);
604         }
605
606         final PatchContext patch;
607         try {
608             patch = body.toPatchContext(new DefaultResourceContext(path));
609         } catch (IOException e) {
610             LOG.debug("Error parsing YANG Patch input", e);
611             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
612                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
613         }
614         return patchData(patch);
615     }
616
617     /**
618      * Process edit operations of one {@link PatchContext}.
619      *
620      * @param patch Patch context to be processed
621      * @return {@link PatchStatusContext}
622      */
623     public final @NonNull RestconfFuture<DataYangPatchResult> patchData(final PatchContext patch) {
624         final var editCollection = new ArrayList<PatchStatusEntity>();
625         final var tx = prepareWriteExecution();
626
627         boolean noError = true;
628         for (var patchEntity : patch.entities()) {
629             if (noError) {
630                 final var targetNode = patchEntity.getTargetNode();
631                 final var editId = patchEntity.getEditId();
632
633                 switch (patchEntity.getOperation()) {
634                     case Create:
635                         try {
636                             tx.create(targetNode, patchEntity.getNode());
637                             editCollection.add(new PatchStatusEntity(editId, true, null));
638                         } catch (RestconfDocumentedException e) {
639                             editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
640                             noError = false;
641                         }
642                         break;
643                     case Delete:
644                         try {
645                             tx.delete(targetNode);
646                             editCollection.add(new PatchStatusEntity(editId, true, null));
647                         } catch (RestconfDocumentedException e) {
648                             editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
649                             noError = false;
650                         }
651                         break;
652                     case Merge:
653                         try {
654                             tx.ensureParentsByMerge(targetNode);
655                             tx.merge(targetNode, patchEntity.getNode());
656                             editCollection.add(new PatchStatusEntity(editId, true, null));
657                         } catch (RestconfDocumentedException e) {
658                             editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
659                             noError = false;
660                         }
661                         break;
662                     case Replace:
663                         try {
664                             tx.replace(targetNode, patchEntity.getNode());
665                             editCollection.add(new PatchStatusEntity(editId, true, null));
666                         } catch (RestconfDocumentedException e) {
667                             editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
668                             noError = false;
669                         }
670                         break;
671                     case Remove:
672                         try {
673                             tx.remove(targetNode);
674                             editCollection.add(new PatchStatusEntity(editId, true, null));
675                         } catch (RestconfDocumentedException e) {
676                             editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
677                             noError = false;
678                         }
679                         break;
680                     default:
681                         editCollection.add(new PatchStatusEntity(editId, false, List.of(
682                             new RestconfError(ErrorType.PROTOCOL, ErrorTag.OPERATION_NOT_SUPPORTED,
683                                 "Not supported Yang Patch operation"))));
684                         noError = false;
685                         break;
686                 }
687             } else {
688                 break;
689             }
690         }
691
692         final var ret = new SettableRestconfFuture<DataYangPatchResult>();
693         // We have errors
694         if (!noError) {
695             tx.cancel();
696             ret.set(new DataYangPatchResult(
697                 new PatchStatusContext(modelContext(), patch.patchId(), List.copyOf(editCollection), false, null)));
698             return ret;
699         }
700
701         Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
702             @Override
703             public void onSuccess(final CommitInfo result) {
704                 ret.set(new DataYangPatchResult(
705                     new PatchStatusContext(modelContext(), patch.patchId(), List.copyOf(editCollection), true, null)));
706             }
707
708             @Override
709             public void onFailure(final Throwable cause) {
710                 // if errors occurred during transaction commit then patch failed and global errors are reported
711                 ret.set(new DataYangPatchResult(
712                     new PatchStatusContext(modelContext(), patch.patchId(), List.copyOf(editCollection), false,
713                         TransactionUtil.decodeException(cause, "PATCH", null, modelContext()).getErrors())));
714             }
715         }, MoreExecutors.directExecutor());
716
717         return ret;
718     }
719
720     private void insertWithPointPost(final RestconfTransaction tx, final YangInstanceIdentifier path,
721             final NormalizedNode data, final PathArgument pointArg, final NormalizedNodeContainer<?> readList,
722             final boolean before) {
723         tx.remove(path);
724
725         int lastItemPosition = 0;
726         for (var nodeChild : readList.body()) {
727             if (nodeChild.name().equals(pointArg)) {
728                 break;
729             }
730             lastItemPosition++;
731         }
732         if (!before) {
733             lastItemPosition++;
734         }
735
736         int lastInsertedPosition = 0;
737         for (var nodeChild : readList.body()) {
738             if (lastInsertedPosition == lastItemPosition) {
739                 tx.replace(path, data);
740             }
741             tx.replace(path.node(nodeChild.name()), nodeChild);
742             lastInsertedPosition++;
743         }
744
745         // In case we are inserting after last element
746         if (!before) {
747             if (lastInsertedPosition == lastItemPosition) {
748                 tx.replace(path, data);
749             }
750         }
751     }
752
753     private static ListenableFuture<? extends CommitInfo> createAndCommit(final RestconfTransaction tx,
754             final YangInstanceIdentifier path, final NormalizedNode data) {
755         try {
756             tx.create(path, data);
757         } catch (RestconfDocumentedException e) {
758             // close transaction if any and pass exception further
759             tx.cancel();
760             throw e;
761         }
762
763         return tx.commit();
764     }
765
766     /**
767      * Check if child items do NOT already exists in List at specified {@code path}.
768      *
769      * @param data Data to be checked
770      * @param path Path to be checked
771      * @throws RestconfDocumentedException if data already exists.
772      */
773     private void checkListDataDoesNotExist(final YangInstanceIdentifier path, final NormalizedNode data) {
774         if (data instanceof NormalizedNodeContainer<?> dataNode) {
775             for (final var node : dataNode.body()) {
776                 checkItemDoesNotExists(exists(path.node(node.name())), path.node(node.name()));
777             }
778         } else {
779             throw new RestconfDocumentedException("Unexpected node type: " + data.getClass().getName());
780         }
781     }
782
783     /**
784      * Check if items do NOT already exists at specified {@code path}.
785      *
786      * @param existsFuture if checked data exists
787      * @param path         Path to be checked
788      * @throws RestconfDocumentedException if data already exists.
789      */
790     static void checkItemDoesNotExists(final ListenableFuture<Boolean> existsFuture,
791             final YangInstanceIdentifier path) {
792         if (TransactionUtil.syncAccess(existsFuture, path)) {
793             LOG.trace("Operation via Restconf was not executed because data at {} already exists", path);
794             throw new RestconfDocumentedException("Data already exists", ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS,
795                 path);
796         }
797     }
798
799     /**
800      * Delete data from the configuration datastore. If the data does not exist, this operation will fail, as outlined
801      * in <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.7">RFC8040 section 4.7</a>
802      *
803      * @param apiPath Path to delete
804      * @return A {@link RestconfFuture}
805      * @throws NullPointerException if {@code apiPath} is {@code null}
806      */
807     @SuppressWarnings("checkstyle:abbreviationAsWordInName")
808     public final @NonNull RestconfFuture<Empty> dataDELETE(final ApiPath apiPath) {
809         final Data path;
810         try {
811             path = pathNormalizer.normalizeDataPath(apiPath);
812         } catch (RestconfDocumentedException e) {
813             return RestconfFuture.failed(e);
814         }
815
816         // FIXME: reject empty YangInstanceIdentifier, as datastores may not be deleted
817         final var ret = new SettableRestconfFuture<Empty>();
818         delete(ret, path.instance());
819         return ret;
820     }
821
822     abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
823
824     public final @NonNull RestconfFuture<DataGetResult> dataGET(final ApiPath apiPath,
825             final DataGetParams params) {
826         final Data path;
827         try {
828             path = pathNormalizer.normalizeDataPath(apiPath);
829         } catch (RestconfDocumentedException e) {
830             return RestconfFuture.failed(e);
831         }
832         return dataGET(path, params);
833     }
834
835     abstract @NonNull RestconfFuture<DataGetResult> dataGET(Data path, DataGetParams params);
836
837     static final @NonNull RestconfFuture<DataGetResult> completeDataGET(final Inference inference,
838             final QueryParameters queryParams, final @Nullable NormalizedNode node,
839             final @Nullable ConfigurationMetadata metadata) {
840         if (node == null) {
841             return RestconfFuture.failed(new RestconfDocumentedException(
842                 "Request could not be completed because the relevant data model content does not exist",
843                 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING));
844         }
845
846         final var payload = new NormalizedNodePayload(inference, node, queryParams);
847         return RestconfFuture.of(metadata == null ? new DataGetResult(payload)
848             : new DataGetResult(payload, metadata.entityTag(), metadata.lastModified()));
849     }
850
851     /**
852      * Read specific type of data from data store via transaction. Close {@link DOMTransactionChain} if any
853      * inside of object {@link RestconfStrategy} provided as a parameter.
854      *
855      * @param content      type of data to read (config, state, all)
856      * @param path         the path to read
857      * @param defaultsMode value of with-defaults parameter
858      * @return {@link NormalizedNode}
859      */
860     // FIXME: NETCONF-1155: this method should asynchronous
861     @VisibleForTesting
862     final @Nullable NormalizedNode readData(final @NonNull ContentParam content,
863             final @NonNull YangInstanceIdentifier path, final WithDefaultsParam defaultsMode) {
864         return switch (content) {
865             case ALL -> {
866                 // PREPARE STATE DATA NODE
867                 final var stateDataNode = readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path);
868                 // PREPARE CONFIG DATA NODE
869                 final var configDataNode = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path);
870
871                 yield mergeConfigAndSTateDataIfNeeded(stateDataNode, defaultsMode == null ? configDataNode
872                     : prepareDataByParamWithDef(configDataNode, path, defaultsMode.mode()));
873             }
874             case CONFIG -> {
875                 final var read = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path);
876                 yield defaultsMode == null ? read
877                     : prepareDataByParamWithDef(read, path, defaultsMode.mode());
878             }
879             case NONCONFIG -> readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path);
880         };
881     }
882
883     private @Nullable NormalizedNode readDataViaTransaction(final LogicalDatastoreType store,
884             final YangInstanceIdentifier path) {
885         return TransactionUtil.syncAccess(read(store, path), path).orElse(null);
886     }
887
888     final NormalizedNode prepareDataByParamWithDef(final NormalizedNode readData, final YangInstanceIdentifier path,
889             final WithDefaultsMode defaultsMode) {
890         final boolean trim = switch (defaultsMode) {
891             case Trim -> true;
892             case Explicit -> false;
893             case ReportAll, ReportAllTagged -> throw new RestconfDocumentedException(
894                 "Unsupported with-defaults value " + defaultsMode.getName());
895         };
896
897         // FIXME: we have this readily available in InstanceIdentifierContext
898         final var ctxNode = databind.schemaTree().findChild(path).orElseThrow();
899         if (readData instanceof ContainerNode container) {
900             final var builder = ImmutableNodes.newContainerBuilder().withNodeIdentifier(container.name());
901             buildCont(builder, container.body(), ctxNode, trim);
902             return builder.build();
903         } else if (readData instanceof MapEntryNode mapEntry) {
904             if (!(ctxNode.dataSchemaNode() instanceof ListSchemaNode listSchema)) {
905                 throw new IllegalStateException("Input " + mapEntry + " does not match " + ctxNode);
906             }
907
908             final var builder = ImmutableNodes.newMapEntryBuilder().withNodeIdentifier(mapEntry.name());
909             buildMapEntryBuilder(builder, mapEntry.body(), ctxNode, trim, listSchema.getKeyDefinition());
910             return builder.build();
911         } else {
912             throw new IllegalStateException("Unhandled data contract " + readData.contract());
913         }
914     }
915
916     private static void buildMapEntryBuilder(
917             final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> builder,
918             final Collection<@NonNull DataContainerChild> children, final DataSchemaContext ctxNode,
919             final boolean trim, final List<QName> keys) {
920         for (var child : children) {
921             final var childCtx = getChildContext(ctxNode, child);
922
923             if (child instanceof ContainerNode container) {
924                 appendContainer(builder, container, childCtx, trim);
925             } else if (child instanceof MapNode map) {
926                 appendMap(builder, map, childCtx, trim);
927             } else if (child instanceof LeafNode<?> leaf) {
928                 appendLeaf(builder, leaf, childCtx, trim, keys);
929             } else {
930                 // FIXME: we should never hit this, throw an ISE if this ever happens
931                 LOG.debug("Ignoring unhandled child contract {}", child.contract());
932             }
933         }
934     }
935
936     private static void appendContainer(final DataContainerNodeBuilder<?, ?> builder, final ContainerNode container,
937             final DataSchemaContext ctxNode, final boolean trim) {
938         final var childBuilder = ImmutableNodes.newContainerBuilder().withNodeIdentifier(container.name());
939         buildCont(childBuilder, container.body(), ctxNode, trim);
940         builder.withChild(childBuilder.build());
941     }
942
943     private static void appendLeaf(final DataContainerNodeBuilder<?, ?> builder, final LeafNode<?> leaf,
944             final DataSchemaContext ctxNode, final boolean trim, final List<QName> keys) {
945         if (!(ctxNode.dataSchemaNode() instanceof LeafSchemaNode leafSchema)) {
946             throw new IllegalStateException("Input " + leaf + " does not match " + ctxNode);
947         }
948
949         // FIXME: Document now this works with the likes of YangInstanceIdentifier. I bet it does not.
950         final var defaultVal = leafSchema.getType().getDefaultValue().orElse(null);
951
952         // This is a combined check for when we need to emit the leaf.
953         if (
954             // We always have to emit key leaf values
955             keys.contains(leafSchema.getQName())
956             // trim == WithDefaultsParam.TRIM and the source is assumed to store explicit values:
957             //
958             //            When data is retrieved with a <with-defaults> parameter equal to
959             //            'trim', data nodes MUST NOT be reported if they contain the schema
960             //            default value.  Non-configuration data nodes containing the schema
961             //            default value MUST NOT be reported.
962             //
963             || trim && (defaultVal == null || !defaultVal.equals(leaf.body()))
964             // !trim == WithDefaultsParam.EXPLICIT and the source is assume to store explicit values... but I fail to
965             // grasp what we are doing here... emit only if it matches default ???!!!
966             // FIXME: The WithDefaultsParam.EXPLICIT says:
967             //
968             //            Data nodes set to the YANG default by the client are reported.
969             //
970             //        and RFC8040 (https://www.rfc-editor.org/rfc/rfc8040#page-60) says:
971             //
972             //            If the "with-defaults" parameter is set to "explicit", then the
973             //            server MUST adhere to the default-reporting behavior defined in
974             //            Section 3.3 of [RFC6243].
975             //
976             //        and then RFC6243 (https://www.rfc-editor.org/rfc/rfc6243#section-3.3) says:
977             //
978             //            When data is retrieved with a <with-defaults> parameter equal to
979             //            'explicit', a data node that was set by a client to its schema
980             //            default value MUST be reported.  A conceptual data node that would be
981             //            set by the server to the schema default value MUST NOT be reported.
982             //            Non-configuration data nodes containing the schema default value MUST
983             //            be reported.
984             //
985             // (rovarga): The source reports explicitly-defined leaves and does *not* create defaults by itself.
986             //            This seems to disregard the 'trim = true' case semantics (see above).
987             //            Combining the above, though, these checks are missing the 'non-config' check, which would
988             //            distinguish, but barring that this check is superfluous and results in the wrong semantics.
989             //            Without that input, this really should be  covered by the previous case.
990                 || !trim && defaultVal != null && defaultVal.equals(leaf.body())) {
991             builder.withChild(leaf);
992         }
993     }
994
995     private static void appendMap(final DataContainerNodeBuilder<?, ?> builder, final MapNode map,
996             final DataSchemaContext childCtx, final boolean trim) {
997         if (!(childCtx.dataSchemaNode() instanceof ListSchemaNode listSchema)) {
998             throw new IllegalStateException("Input " + map + " does not match " + childCtx);
999         }
1000
1001         final var childBuilder = switch (map.ordering()) {
1002             case SYSTEM -> ImmutableNodes.newSystemMapBuilder();
1003             case USER -> ImmutableNodes.newUserMapBuilder();
1004         };
1005         buildList(childBuilder.withNodeIdentifier(map.name()), map.body(), childCtx, trim,
1006             listSchema.getKeyDefinition());
1007         builder.withChild(childBuilder.build());
1008     }
1009
1010     private static void buildList(final CollectionNodeBuilder<MapEntryNode, ? extends MapNode> builder,
1011             final Collection<@NonNull MapEntryNode> entries, final DataSchemaContext ctxNode, final boolean trim,
1012             final List<@NonNull QName> keys) {
1013         for (var entry : entries) {
1014             final var childCtx = getChildContext(ctxNode, entry);
1015             final var mapEntryBuilder = ImmutableNodes.newMapEntryBuilder().withNodeIdentifier(entry.name());
1016             buildMapEntryBuilder(mapEntryBuilder, entry.body(), childCtx, trim, keys);
1017             builder.withChild(mapEntryBuilder.build());
1018         }
1019     }
1020
1021     private static void buildCont(final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> builder,
1022             final Collection<DataContainerChild> children, final DataSchemaContext ctxNode, final boolean trim) {
1023         for (var child : children) {
1024             final var childCtx = getChildContext(ctxNode, child);
1025             if (child instanceof ContainerNode container) {
1026                 appendContainer(builder, container, childCtx, trim);
1027             } else if (child instanceof MapNode map) {
1028                 appendMap(builder, map, childCtx, trim);
1029             } else if (child instanceof LeafNode<?> leaf) {
1030                 appendLeaf(builder, leaf, childCtx, trim, List.of());
1031             }
1032         }
1033     }
1034
1035     private static @NonNull DataSchemaContext getChildContext(final DataSchemaContext ctxNode,
1036             final NormalizedNode child) {
1037         final var childId = child.name();
1038         final var childCtx = ctxNode instanceof DataSchemaContext.Composite composite ? composite.childByArg(childId)
1039             : null;
1040         if (childCtx == null) {
1041             throw new NoSuchElementException("Cannot resolve child " + childId + " in " + ctxNode);
1042         }
1043         return childCtx;
1044     }
1045
1046     static final NormalizedNode mergeConfigAndSTateDataIfNeeded(final NormalizedNode stateDataNode,
1047             final NormalizedNode configDataNode) {
1048         if (stateDataNode == null) {
1049             // No state, return config
1050             return configDataNode;
1051         }
1052         if (configDataNode == null) {
1053             // No config, return state
1054             return stateDataNode;
1055         }
1056         // merge config and state
1057         return mergeStateAndConfigData(stateDataNode, configDataNode);
1058     }
1059
1060     /**
1061      * Merge state and config data into a single NormalizedNode.
1062      *
1063      * @param stateDataNode  data node of state data
1064      * @param configDataNode data node of config data
1065      * @return {@link NormalizedNode}
1066      */
1067     private static @NonNull NormalizedNode mergeStateAndConfigData(
1068             final @NonNull NormalizedNode stateDataNode, final @NonNull NormalizedNode configDataNode) {
1069         validateNodeMerge(stateDataNode, configDataNode);
1070         // FIXME: this check is bogus, as it confuses yang.data.api (NormalizedNode) with yang.model.api (RpcDefinition)
1071         if (configDataNode instanceof RpcDefinition) {
1072             return prepareRpcData(configDataNode, stateDataNode);
1073         } else {
1074             return prepareData(configDataNode, stateDataNode);
1075         }
1076     }
1077
1078     /**
1079      * Validates whether the two NormalizedNodes can be merged.
1080      *
1081      * @param stateDataNode  data node of state data
1082      * @param configDataNode data node of config data
1083      */
1084     private static void validateNodeMerge(final @NonNull NormalizedNode stateDataNode,
1085                                           final @NonNull NormalizedNode configDataNode) {
1086         final QNameModule moduleOfStateData = stateDataNode.name().getNodeType().getModule();
1087         final QNameModule moduleOfConfigData = configDataNode.name().getNodeType().getModule();
1088         if (!moduleOfStateData.equals(moduleOfConfigData)) {
1089             throw new RestconfDocumentedException("Unable to merge data from different modules.");
1090         }
1091     }
1092
1093     /**
1094      * Prepare and map data for rpc.
1095      *
1096      * @param configDataNode data node of config data
1097      * @param stateDataNode  data node of state data
1098      * @return {@link NormalizedNode}
1099      */
1100     private static @NonNull NormalizedNode prepareRpcData(final @NonNull NormalizedNode configDataNode,
1101                                                           final @NonNull NormalizedNode stateDataNode) {
1102         final var mapEntryBuilder = ImmutableNodes.newMapEntryBuilder()
1103             .withNodeIdentifier((NodeIdentifierWithPredicates) configDataNode.name());
1104
1105         // MAP CONFIG DATA
1106         mapRpcDataNode(configDataNode, mapEntryBuilder);
1107         // MAP STATE DATA
1108         mapRpcDataNode(stateDataNode, mapEntryBuilder);
1109
1110         return ImmutableNodes.newSystemMapBuilder()
1111             .withNodeIdentifier(NodeIdentifier.create(configDataNode.name().getNodeType()))
1112             .addChild(mapEntryBuilder.build())
1113             .build();
1114     }
1115
1116     /**
1117      * Map node to map entry builder.
1118      *
1119      * @param dataNode        data node
1120      * @param mapEntryBuilder builder for mapping data
1121      */
1122     private static void mapRpcDataNode(final @NonNull NormalizedNode dataNode,
1123             final @NonNull DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> mapEntryBuilder) {
1124         ((ContainerNode) dataNode).body().forEach(mapEntryBuilder::addChild);
1125     }
1126
1127     /**
1128      * Prepare and map all data from DS.
1129      *
1130      * @param configDataNode data node of config data
1131      * @param stateDataNode  data node of state data
1132      * @return {@link NormalizedNode}
1133      */
1134     @SuppressWarnings("unchecked")
1135     private static @NonNull NormalizedNode prepareData(final @NonNull NormalizedNode configDataNode,
1136                                                        final @NonNull NormalizedNode stateDataNode) {
1137         if (configDataNode instanceof UserMapNode configMap) {
1138             final var builder = ImmutableNodes.newUserMapBuilder().withNodeIdentifier(configMap.name());
1139             mapValueToBuilder(configMap.body(), ((UserMapNode) stateDataNode).body(), builder);
1140             return builder.build();
1141         } else if (configDataNode instanceof SystemMapNode configMap) {
1142             final var builder = ImmutableNodes.newSystemMapBuilder().withNodeIdentifier(configMap.name());
1143             mapValueToBuilder(configMap.body(), ((SystemMapNode) stateDataNode).body(), builder);
1144             return builder.build();
1145         } else if (configDataNode instanceof MapEntryNode configEntry) {
1146             final var builder = ImmutableNodes.newMapEntryBuilder().withNodeIdentifier(configEntry.name());
1147             mapValueToBuilder(configEntry.body(), ((MapEntryNode) stateDataNode).body(), builder);
1148             return builder.build();
1149         } else if (configDataNode instanceof ContainerNode configContaienr) {
1150             final var builder = ImmutableNodes.newContainerBuilder().withNodeIdentifier(configContaienr.name());
1151             mapValueToBuilder(configContaienr.body(), ((ContainerNode) stateDataNode).body(), builder);
1152             return builder.build();
1153         } else if (configDataNode instanceof ChoiceNode configChoice) {
1154             final var builder = ImmutableNodes.newChoiceBuilder().withNodeIdentifier(configChoice.name());
1155             mapValueToBuilder(configChoice.body(), ((ChoiceNode) stateDataNode).body(), builder);
1156             return builder.build();
1157         } else if (configDataNode instanceof LeafNode configLeaf) {
1158             // config trumps oper
1159             return configLeaf;
1160         } else if (configDataNode instanceof UserLeafSetNode) {
1161             final var configLeafSet = (UserLeafSetNode<Object>) configDataNode;
1162             final var builder = ImmutableNodes.<Object>newUserLeafSetBuilder().withNodeIdentifier(configLeafSet.name());
1163             mapValueToBuilder(configLeafSet.body(), ((UserLeafSetNode<Object>) stateDataNode).body(), builder);
1164             return builder.build();
1165         } else if (configDataNode instanceof SystemLeafSetNode) {
1166             final var configLeafSet = (SystemLeafSetNode<Object>) configDataNode;
1167             final var builder = ImmutableNodes.<Object>newSystemLeafSetBuilder()
1168                 .withNodeIdentifier(configLeafSet.name());
1169             mapValueToBuilder(configLeafSet.body(), ((SystemLeafSetNode<Object>) stateDataNode).body(), builder);
1170             return builder.build();
1171         } else if (configDataNode instanceof LeafSetEntryNode<?> configEntry) {
1172             // config trumps oper
1173             return configEntry;
1174         } else if (configDataNode instanceof UnkeyedListNode configList) {
1175             final var builder = ImmutableNodes.newUnkeyedListBuilder().withNodeIdentifier(configList.name());
1176             mapValueToBuilder(configList.body(), ((UnkeyedListNode) stateDataNode).body(), builder);
1177             return builder.build();
1178         } else if (configDataNode instanceof UnkeyedListEntryNode configEntry) {
1179             final var builder = ImmutableNodes.newUnkeyedListEntryBuilder().withNodeIdentifier(configEntry.name());
1180             mapValueToBuilder(configEntry.body(), ((UnkeyedListEntryNode) stateDataNode).body(), builder);
1181             return builder.build();
1182         } else {
1183             throw new RestconfDocumentedException("Unexpected node type: " + configDataNode.getClass().getName());
1184         }
1185     }
1186
1187     /**
1188      * Map value from container node to builder.
1189      *
1190      * @param configData collection of config data nodes
1191      * @param stateData  collection of state data nodes
1192      * @param builder    builder
1193      */
1194     private static <T extends NormalizedNode> void mapValueToBuilder(
1195             final @NonNull Collection<T> configData, final @NonNull Collection<T> stateData,
1196             final @NonNull NormalizedNodeContainerBuilder<?, PathArgument, T, ?> builder) {
1197         final var configMap = configData.stream().collect(Collectors.toMap(NormalizedNode::name, Function.identity()));
1198         final var stateMap = stateData.stream().collect(Collectors.toMap(NormalizedNode::name, Function.identity()));
1199
1200         // merge config and state data of children with different identifiers
1201         mapDataToBuilder(configMap, stateMap, builder);
1202
1203         // merge config and state data of children with the same identifiers
1204         mergeDataToBuilder(configMap, stateMap, builder);
1205     }
1206
1207     /**
1208      * Map data with different identifiers to builder. Data with different identifiers can be just added
1209      * as childs to parent node.
1210      *
1211      * @param configMap map of config data nodes
1212      * @param stateMap  map of state data nodes
1213      * @param builder   - builder
1214      */
1215     private static <T extends NormalizedNode> void mapDataToBuilder(
1216             final @NonNull Map<PathArgument, T> configMap, final @NonNull Map<PathArgument, T> stateMap,
1217             final @NonNull NormalizedNodeContainerBuilder<?, PathArgument, T, ?> builder) {
1218         configMap.entrySet().stream().filter(x -> !stateMap.containsKey(x.getKey())).forEach(
1219             y -> builder.addChild(y.getValue()));
1220         stateMap.entrySet().stream().filter(x -> !configMap.containsKey(x.getKey())).forEach(
1221             y -> builder.addChild(y.getValue()));
1222     }
1223
1224     /**
1225      * Map data with the same identifiers to builder. Data with the same identifiers cannot be just added but we need to
1226      * go one level down with {@code prepareData} method.
1227      *
1228      * @param configMap immutable config data
1229      * @param stateMap  immutable state data
1230      * @param builder   - builder
1231      */
1232     @SuppressWarnings("unchecked")
1233     private static <T extends NormalizedNode> void mergeDataToBuilder(
1234             final @NonNull Map<PathArgument, T> configMap, final @NonNull Map<PathArgument, T> stateMap,
1235             final @NonNull NormalizedNodeContainerBuilder<?, PathArgument, T, ?> builder) {
1236         // it is enough to process only config data because operational contains the same data
1237         configMap.entrySet().stream().filter(x -> stateMap.containsKey(x.getKey())).forEach(
1238             y -> builder.addChild((T) prepareData(y.getValue(), stateMap.get(y.getKey()))));
1239     }
1240
1241     public @NonNull RestconfFuture<OperationsGetResult> operationsGET() {
1242         final var modelContext = modelContext();
1243         final var modules = modelContext.getModuleStatements();
1244         if (modules.isEmpty()) {
1245             // No modules, or defensive return empty content
1246             return RestconfFuture.of(new OperationsGetResult.Container(modelContext, ImmutableSetMultimap.of()));
1247         }
1248
1249         // RPC QNames by their XMLNamespace/Revision. This should be a Table, but Revision can be null, which wrecks us.
1250         final var table = new HashMap<XMLNamespace, Map<Revision, ImmutableSet<QName>>>();
1251         for (var entry : modules.entrySet()) {
1252             final var module = entry.getValue();
1253             final var rpcNames = module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
1254                 .map(RpcEffectiveStatement::argument)
1255                 .collect(ImmutableSet.toImmutableSet());
1256             if (!rpcNames.isEmpty()) {
1257                 final var namespace = entry.getKey();
1258                 table.computeIfAbsent(namespace.namespace(), ignored -> new HashMap<>())
1259                     .put(namespace.revision(), rpcNames);
1260             }
1261         }
1262
1263         // Now pick the latest revision for each namespace
1264         final var rpcs = ImmutableSetMultimap.<QNameModule, QName>builder();
1265         for (var entry : table.entrySet()) {
1266             entry.getValue().entrySet().stream()
1267             .sorted(Comparator.comparing(Entry::getKey, (first, second) -> Revision.compare(second, first)))
1268             .findFirst()
1269             .ifPresent(row -> rpcs.putAll(QNameModule.of(entry.getKey(), row.getKey()), row.getValue()));
1270         }
1271         return RestconfFuture.of(new OperationsGetResult.Container(modelContext, rpcs.build()));
1272     }
1273
1274     public @NonNull RestconfFuture<OperationsGetResult> operationsGET(final ApiPath apiPath) {
1275         if (apiPath.steps().isEmpty()) {
1276             return operationsGET();
1277         }
1278
1279         final Rpc rpc;
1280         try {
1281             rpc = pathNormalizer.normalizeRpcPath(apiPath);
1282         } catch (RestconfDocumentedException e) {
1283             return RestconfFuture.failed(e);
1284         }
1285
1286         return RestconfFuture.of(new OperationsGetResult.Leaf(rpc.inference().modelContext(), rpc.rpc().argument()));
1287     }
1288
1289     public @NonNull RestconfFuture<InvokeResult> operationsPOST(final URI restconfURI, final ApiPath apiPath,
1290             final Map<String, String> queryParameters, final OperationInputBody body) {
1291         final Rpc path;
1292         try {
1293             path = pathNormalizer.normalizeRpcPath(apiPath);
1294         } catch (RestconfDocumentedException e) {
1295             return RestconfFuture.failed(e);
1296         }
1297
1298         final InvokeParams params;
1299         try {
1300             params = InvokeParams.ofQueryParameters(queryParameters);
1301         } catch (IllegalArgumentException e) {
1302             return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
1303                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
1304         }
1305
1306         final ContainerNode data;
1307         try {
1308             data = body.toContainerNode(path);
1309         } catch (IOException e) {
1310             LOG.debug("Error reading input", e);
1311             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
1312                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
1313         }
1314
1315         final var type = path.rpc().argument();
1316         final var local = localRpcs.get(type);
1317         if (local != null) {
1318             return local.invoke(restconfURI, new OperationInput(path, data))
1319                 .transform(output -> outputToInvokeResult(path, params, output));
1320         }
1321         if (rpcService == null) {
1322             LOG.debug("RPC invocation is not available");
1323             return RestconfFuture.failed(new RestconfDocumentedException("RPC invocation is not available",
1324                 ErrorType.PROTOCOL, ErrorTag.OPERATION_NOT_SUPPORTED));
1325         }
1326
1327         final var ret = new SettableRestconfFuture<InvokeResult>();
1328         Futures.addCallback(rpcService.invokeRpc(type, data), new FutureCallback<DOMRpcResult>() {
1329             @Override
1330             public void onSuccess(final DOMRpcResult response) {
1331                 final var errors = response.errors();
1332                 if (errors.isEmpty()) {
1333                     ret.set(outputToInvokeResult(path, params, response.value()));
1334                 } else {
1335                     LOG.debug("RPC invocation reported {}", response.errors());
1336                     ret.setFailure(new RestconfDocumentedException("RPC implementation reported errors", null,
1337                         response.errors()));
1338                 }
1339             }
1340
1341             @Override
1342             public void onFailure(final Throwable cause) {
1343                 LOG.debug("RPC invocation failed, cause");
1344                 if (cause instanceof RestconfDocumentedException ex) {
1345                     ret.setFailure(ex);
1346                 } else {
1347                     // TODO: YangNetconfErrorAware if we ever get into a broader invocation scope
1348                     ret.setFailure(new RestconfDocumentedException(cause,
1349                         new RestconfError(ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage())));
1350                 }
1351             }
1352         }, MoreExecutors.directExecutor());
1353         return ret;
1354     }
1355
1356     private static @NonNull InvokeResult outputToInvokeResult(final @NonNull OperationPath path,
1357             final @NonNull InvokeParams params, final @Nullable ContainerNode value) {
1358         return value == null || value.isEmpty() ? InvokeResult.EMPTY
1359             : new InvokeResult(new OperationOutputBody(params, path, value));
1360     }
1361
1362     public @NonNull RestconfFuture<CharSource> resolveSource(final SourceIdentifier source,
1363             final Class<? extends SourceRepresentation> representation) {
1364         final var src = requireNonNull(source);
1365         if (YangTextSource.class.isAssignableFrom(representation)) {
1366             if (sourceProvider != null) {
1367                 final var ret = new SettableRestconfFuture<CharSource>();
1368                 Futures.addCallback(sourceProvider.getYangTexttSource(src), new FutureCallback<>() {
1369                     @Override
1370                     public void onSuccess(final YangTextSource result) {
1371                         ret.set(result);
1372                     }
1373
1374                     @Override
1375                     public void onFailure(final Throwable cause) {
1376                         ret.setFailure(cause instanceof RestconfDocumentedException e ? e
1377                             : new RestconfDocumentedException(cause.getMessage(), ErrorType.RPC,
1378                                 ErrorTag.OPERATION_FAILED, cause));
1379                     }
1380                 }, MoreExecutors.directExecutor());
1381                 return ret;
1382             }
1383             return exportSource(modelContext(), src, YangCharSource::new, YangCharSource::new);
1384         }
1385         if (YinTextSource.class.isAssignableFrom(representation)) {
1386             return exportSource(modelContext(), src, YinCharSource.OfModule::new, YinCharSource.OfSubmodule::new);
1387         }
1388         return RestconfFuture.failed(new RestconfDocumentedException(
1389             "Unsupported source representation " + representation.getName()));
1390     }
1391
1392     private static @NonNull RestconfFuture<CharSource> exportSource(final EffectiveModelContext modelContext,
1393             final SourceIdentifier source, final Function<ModuleEffectiveStatement, CharSource> moduleCtor,
1394             final BiFunction<ModuleEffectiveStatement, SubmoduleEffectiveStatement, CharSource> submoduleCtor) {
1395         // If the source identifies a module, things are easy
1396         final var name = source.name().getLocalName();
1397         final var optRevision = Optional.ofNullable(source.revision());
1398         final var optModule = modelContext.findModule(name, optRevision);
1399         if (optModule.isPresent()) {
1400             return RestconfFuture.of(moduleCtor.apply(optModule.orElseThrow().asEffectiveStatement()));
1401         }
1402
1403         // The source could be a submodule, which we need to hunt down
1404         for (var module : modelContext.getModules()) {
1405             for (var submodule : module.getSubmodules()) {
1406                 if (name.equals(submodule.getName()) && optRevision.equals(submodule.getRevision())) {
1407                     return RestconfFuture.of(submoduleCtor.apply(module.asEffectiveStatement(),
1408                         submodule.asEffectiveStatement()));
1409                 }
1410             }
1411         }
1412
1413         final var sb = new StringBuilder().append("Source ").append(source.name().getLocalName());
1414         optRevision.ifPresent(rev -> sb.append('@').append(rev));
1415         sb.append(" not found");
1416         return RestconfFuture.failed(new RestconfDocumentedException(sb.toString(),
1417             ErrorType.APPLICATION, ErrorTag.DATA_MISSING));
1418     }
1419
1420     public final @NonNull RestconfFuture<? extends DataPostResult> dataPOST(final ApiPath apiPath,
1421             final DataPostBody body, final Map<String, String> queryParameters) {
1422         if (apiPath.steps().isEmpty()) {
1423             return dataCreatePOST(body.toResource(), queryParameters);
1424         }
1425         final InstanceReference path;
1426         try {
1427             path = pathNormalizer.normalizeDataOrActionPath(apiPath);
1428         } catch (RestconfDocumentedException e) {
1429             return RestconfFuture.failed(e);
1430         }
1431         if (path instanceof Data dataPath) {
1432             try (var resourceBody = body.toResource()) {
1433                 return dataCreatePOST(dataPath, resourceBody, queryParameters);
1434             }
1435         }
1436         if (path instanceof Action actionPath) {
1437             try (var inputBody = body.toOperationInput()) {
1438                 return dataInvokePOST(actionPath, inputBody, queryParameters);
1439             }
1440         }
1441         // Note: this should never happen
1442         // FIXME: we should be able to eliminate this path with Java 21+ pattern matching
1443         return RestconfFuture.failed(new RestconfDocumentedException("Unhandled path " + path));
1444     }
1445
1446     public @NonNull RestconfFuture<CreateResource> dataCreatePOST(final ChildBody body,
1447             final Map<String, String> queryParameters) {
1448         return dataCreatePOST(new DatabindPath.Data(databind), body, queryParameters);
1449     }
1450
1451     private @NonNull RestconfFuture<CreateResource> dataCreatePOST(final DatabindPath.Data path, final ChildBody body,
1452             final Map<String, String> queryParameters) {
1453         final Insert insert;
1454         try {
1455             insert = Insert.ofQueryParameters(path.databind(), queryParameters);
1456         } catch (IllegalArgumentException e) {
1457             return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
1458                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
1459         }
1460
1461         final var payload = body.toPayload(path);
1462         return postData(concat(path.instance(), payload.prefix()), payload.body(), insert);
1463     }
1464
1465     private static YangInstanceIdentifier concat(final YangInstanceIdentifier parent, final List<PathArgument> args) {
1466         var ret = parent;
1467         for (var arg : args) {
1468             ret = ret.node(arg);
1469         }
1470         return ret;
1471     }
1472
1473     private @NonNull RestconfFuture<InvokeResult> dataInvokePOST(final @NonNull Action path,
1474             final @NonNull OperationInputBody body, final Map<String, String> queryParameters) {
1475         final InvokeParams params;
1476         try {
1477             params = InvokeParams.ofQueryParameters(queryParameters);
1478         } catch (IllegalArgumentException e) {
1479             return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
1480                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
1481         }
1482
1483         final ContainerNode input;
1484         try {
1485             input = body.toContainerNode(path);
1486         } catch (IOException e) {
1487             LOG.debug("Error reading input", e);
1488             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
1489                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
1490         }
1491
1492         if (actionService == null) {
1493             return RestconfFuture.failed(new RestconfDocumentedException("DOMActionService is missing."));
1494         }
1495
1496         return dataInvokePOST(actionService, path, input)
1497             .transform(result -> outputToInvokeResult(path, params, result.getOutput().orElse(null)));
1498     }
1499
1500     /**
1501      * Invoke Action via ActionServiceHandler.
1502      *
1503      * @param input input data
1504      * @param yangIId invocation context
1505      * @param schemaPath schema path of data
1506      * @param actionService action service to invoke action
1507      * @return {@link DOMActionResult}
1508      */
1509     private static RestconfFuture<DOMActionResult> dataInvokePOST(final DOMActionService actionService,
1510             final Action path, final @NonNull ContainerNode input) {
1511         final var ret = new SettableRestconfFuture<DOMActionResult>();
1512
1513         Futures.addCallback(actionService.invokeAction(
1514             path.inference().toSchemaInferenceStack().toSchemaNodeIdentifier(),
1515             DOMDataTreeIdentifier.of(LogicalDatastoreType.OPERATIONAL, path.instance()), input),
1516             new FutureCallback<DOMActionResult>() {
1517                 @Override
1518                 public void onSuccess(final DOMActionResult result) {
1519                     final var errors = result.getErrors();
1520                     LOG.debug("InvokeAction Error Message {}", errors);
1521                     if (errors.isEmpty()) {
1522                         ret.set(result);
1523                     } else {
1524                         ret.setFailure(new RestconfDocumentedException("InvokeAction Error Message ", null, errors));
1525                     }
1526                 }
1527
1528                 @Override
1529                 public void onFailure(final Throwable cause) {
1530                     if (cause instanceof DOMActionException) {
1531                         ret.set(new SimpleDOMActionResult(List.of(RpcResultBuilder.newError(
1532                             ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage()))));
1533                     } else if (cause instanceof RestconfDocumentedException e) {
1534                         ret.setFailure(e);
1535                     } else if (cause instanceof CancellationException) {
1536                         ret.setFailure(new RestconfDocumentedException("Action cancelled while executing",
1537                             ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, cause));
1538                     } else {
1539                         ret.setFailure(new RestconfDocumentedException("Invocation failed", cause));
1540                     }
1541                 }
1542             }, MoreExecutors.directExecutor());
1543
1544         return ret;
1545     }
1546 }