Bind RestconfStrategy to EffectiveModelContext
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / transactions / RestconfStrategy.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
9
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
12
13 import com.google.common.util.concurrent.FutureCallback;
14 import com.google.common.util.concurrent.Futures;
15 import com.google.common.util.concurrent.ListenableFuture;
16 import com.google.common.util.concurrent.MoreExecutors;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.NoSuchElementException;
22 import java.util.Optional;
23 import java.util.function.Function;
24 import java.util.stream.Collectors;
25 import org.eclipse.jdt.annotation.NonNull;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.opendaylight.mdsal.common.api.CommitInfo;
28 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
29 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
30 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
31 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
32 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
33 import org.opendaylight.restconf.api.query.ContentParam;
34 import org.opendaylight.restconf.api.query.PointParam;
35 import org.opendaylight.restconf.api.query.WithDefaultsParam;
36 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
37 import org.opendaylight.restconf.common.errors.RestconfError;
38 import org.opendaylight.restconf.common.errors.RestconfFuture;
39 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
40 import org.opendaylight.restconf.common.patch.PatchContext;
41 import org.opendaylight.restconf.common.patch.PatchStatusContext;
42 import org.opendaylight.restconf.common.patch.PatchStatusEntity;
43 import org.opendaylight.restconf.nb.rfc8040.Insert;
44 import org.opendaylight.restconf.nb.rfc8040.utils.parser.YangInstanceIdentifierDeserializer;
45 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.with.defaults.rev110601.WithDefaultsMode;
46 import org.opendaylight.yangtools.yang.common.Empty;
47 import org.opendaylight.yangtools.yang.common.ErrorTag;
48 import org.opendaylight.yangtools.yang.common.ErrorType;
49 import org.opendaylight.yangtools.yang.common.QName;
50 import org.opendaylight.yangtools.yang.common.QNameModule;
51 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
52 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
53 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
54 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
55 import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
56 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
57 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
58 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
59 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
60 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
61 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
62 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
63 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
64 import org.opendaylight.yangtools.yang.data.api.schema.SystemLeafSetNode;
65 import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode;
66 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
67 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode;
68 import org.opendaylight.yangtools.yang.data.api.schema.UserLeafSetNode;
69 import org.opendaylight.yangtools.yang.data.api.schema.UserMapNode;
70 import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder;
71 import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
72 import org.opendaylight.yangtools.yang.data.api.schema.builder.NormalizedNodeContainerBuilder;
73 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
74 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
75 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
76 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
77 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
78 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
79 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
80 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
81 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
82 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
85
86 /**
87  * Baseline execution strategy for various RESTCONF operations.
88  *
89  * @see NetconfRestconfStrategy
90  * @see MdsalRestconfStrategy
91  */
92 // FIXME: it seems the first three operations deal with lifecycle of a transaction, while others invoke various
93 //        operations. This should be handled through proper allocation indirection.
94 public abstract class RestconfStrategy {
95     /**
96      * Result of a {@code PUT} request as defined in
97      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.5">RFC8040 section 4.5</a>. The definition makes it
98      * clear that the logical operation is {@code create-or-replace}.
99      */
100     public enum CreateOrReplaceResult {
101         /**
102          * A new resource has been created.
103          */
104         CREATED,
105         /*
106          * An existing resources has been replaced.
107          */
108         REPLACED;
109     }
110
111     private static final Logger LOG = LoggerFactory.getLogger(RestconfStrategy.class);
112
113     private final @NonNull EffectiveModelContext modelContext;
114
115     RestconfStrategy(final EffectiveModelContext modelContext) {
116         this.modelContext = requireNonNull(modelContext);
117     }
118
119     /**
120      * Look up the appropriate strategy for a particular mount point.
121      *
122      * @param modelContext {@link EffectiveModelContext} of target mount point
123      * @param mountPoint Target mount point
124      * @return A strategy, or null if the mount point does not expose a supported interface
125      * @throws NullPointerException if any argument is {@code null}
126      */
127     public static @Nullable RestconfStrategy forMountPoint(final EffectiveModelContext modelContext,
128             final DOMMountPoint mountPoint) {
129         final var netconfService = mountPoint.getService(NetconfDataTreeService.class);
130         if (netconfService.isPresent()) {
131             return new NetconfRestconfStrategy(modelContext, netconfService.orElseThrow());
132         }
133         final var dataBroker = mountPoint.getService(DOMDataBroker.class);
134         if (dataBroker.isPresent()) {
135             return new MdsalRestconfStrategy(modelContext, dataBroker.orElseThrow());
136         }
137         return null;
138     }
139
140     public final @NonNull EffectiveModelContext modelContext() {
141         return modelContext;
142     }
143
144     /**
145      * Lock the entire datastore.
146      *
147      * @return A {@link RestconfTransaction}. This transaction needs to be either committed or canceled before doing
148      *         anything else.
149      */
150     abstract RestconfTransaction prepareWriteExecution();
151
152     /**
153      * Read data from the datastore.
154      *
155      * @param store the logical data store which should be modified
156      * @param path the data object path
157      * @return a ListenableFuture containing the result of the read
158      */
159     abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store, YangInstanceIdentifier path);
160
161     /**
162      * Read data selected using fields from the datastore.
163      *
164      * @param store the logical data store which should be modified
165      * @param path the parent data object path
166      * @param fields paths to selected fields relative to parent path
167      * @return a ListenableFuture containing the result of the read
168      */
169     abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store, YangInstanceIdentifier path,
170         List<YangInstanceIdentifier> fields);
171
172     /**
173      * Check if data already exists in the configuration datastore.
174      *
175      * @param path the data object path
176      * @return a ListenableFuture containing the result of the check
177      */
178     // FIXME: this method should be hosted in RestconfTransaction
179     // FIXME: this method should only be needed in MdsalRestconfStrategy
180     abstract ListenableFuture<Boolean> exists(YangInstanceIdentifier path);
181
182     /**
183      * Delete data from the configuration datastore. If the data does not exist, this operation will fail, as outlined
184      * in <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.7">RFC8040 section 4.7</a>
185      *
186      * @param path Path to delete
187      * @return A {@link RestconfFuture}
188      * @throws NullPointerException if {@code path} is {@code null}
189      */
190     public final @NonNull RestconfFuture<Empty> delete(final YangInstanceIdentifier path) {
191         final var ret = new SettableRestconfFuture<Empty>();
192         delete(ret, requireNonNull(path));
193         return ret;
194     }
195
196     abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
197
198     /**
199      * Merge data into the configuration datastore, as outlined in
200      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040 section 4.6.1</a>.
201      *
202      * @param path Path to merge
203      * @param data Data to merge
204      * @return A {@link RestconfFuture}
205      * @throws NullPointerException if any argument is {@code null}
206      */
207     public final @NonNull RestconfFuture<Empty> merge(final YangInstanceIdentifier path, final NormalizedNode data) {
208         final var ret = new SettableRestconfFuture<Empty>();
209         merge(ret, requireNonNull(path), requireNonNull(data));
210         return ret;
211     }
212
213     private void merge(final @NonNull SettableRestconfFuture<Empty> future, final @NonNull YangInstanceIdentifier path,
214             final @NonNull NormalizedNode data) {
215         final var tx = prepareWriteExecution();
216         // FIXME: this method should be further specialized to eliminate this call -- it is only needed for MD-SAL
217         tx.ensureParentsByMerge(path);
218         tx.merge(path, data);
219         Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
220             @Override
221             public void onSuccess(final CommitInfo result) {
222                 future.set(Empty.value());
223             }
224
225             @Override
226             public void onFailure(final Throwable cause) {
227                 future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path));
228             }
229         }, MoreExecutors.directExecutor());
230     }
231
232     /**
233      * Check mount point and prepare variables for put data to DS.
234      *
235      * @param path    path of data
236      * @param data    data
237      * @param insert  {@link Insert}
238      * @return A {@link CreateOrReplaceResult}
239      */
240     public final @NonNull CreateOrReplaceResult putData(final YangInstanceIdentifier path, final NormalizedNode data,
241             final @Nullable Insert insert) {
242         final var exists = TransactionUtil.syncAccess(exists(path), path);
243
244         final ListenableFuture<? extends CommitInfo> commitFuture;
245         if (insert != null) {
246             final var parentPath = path.coerceParent();
247             checkListAndOrderedType(parentPath);
248             commitFuture = insertAndCommitPut(path, data, insert, parentPath);
249         } else {
250             commitFuture = replaceAndCommit(prepareWriteExecution(), path, data);
251         }
252
253         TransactionUtil.syncCommit(commitFuture, "PUT", path);
254         return exists ? CreateOrReplaceResult.REPLACED : CreateOrReplaceResult.CREATED;
255     }
256
257     private ListenableFuture<? extends CommitInfo> insertAndCommitPut(final YangInstanceIdentifier path,
258             final NormalizedNode data, final @NonNull Insert insert, final YangInstanceIdentifier parentPath) {
259         final var tx = prepareWriteExecution();
260
261         return switch (insert.insert()) {
262             case FIRST -> {
263                 final var readData = tx.readList(parentPath);
264                 if (readData == null || readData.isEmpty()) {
265                     yield replaceAndCommit(tx, path, data);
266                 }
267                 tx.remove(parentPath);
268                 tx.replace(path, data);
269                 tx.replace(parentPath, readData);
270                 yield tx.commit();
271             }
272             case LAST -> replaceAndCommit(tx, path, data);
273             case BEFORE -> {
274                 final var readData = tx.readList(parentPath);
275                 if (readData == null || readData.isEmpty()) {
276                     yield replaceAndCommit(tx, path, data);
277                 }
278                 insertWithPointPut(tx, path, data, verifyNotNull(insert.point()), readData, true);
279                 yield tx.commit();
280             }
281             case AFTER -> {
282                 final var readData = tx.readList(parentPath);
283                 if (readData == null || readData.isEmpty()) {
284                     yield replaceAndCommit(tx, path, data);
285                 }
286                 insertWithPointPut(tx, path, data, verifyNotNull(insert.point()), readData, false);
287                 yield tx.commit();
288             }
289         };
290     }
291
292     private void insertWithPointPut(final RestconfTransaction tx, final YangInstanceIdentifier path,
293             final NormalizedNode data, final @NonNull PointParam point, final NormalizedNodeContainer<?> readList,
294             final boolean before) {
295         tx.remove(path.getParent());
296         // FIXME: this should have happened sooner
297         final var pointArg = YangInstanceIdentifierDeserializer.create(modelContext, point.value()).path
298             .getLastPathArgument();
299         int lastItemPosition = 0;
300         for (var nodeChild : readList.body()) {
301             if (nodeChild.name().equals(pointArg)) {
302                 break;
303             }
304             lastItemPosition++;
305         }
306         if (!before) {
307             lastItemPosition++;
308         }
309         int lastInsertedPosition = 0;
310         final var emptySubtree = ImmutableNodes.fromInstanceId(modelContext, path.getParent());
311         tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
312         for (var nodeChild : readList.body()) {
313             if (lastInsertedPosition == lastItemPosition) {
314                 tx.replace(path, data);
315             }
316             final var childPath = path.coerceParent().node(nodeChild.name());
317             tx.replace(childPath, nodeChild);
318             lastInsertedPosition++;
319         }
320     }
321
322     private static ListenableFuture<? extends CommitInfo> replaceAndCommit(final RestconfTransaction tx,
323             final YangInstanceIdentifier path, final NormalizedNode data) {
324         tx.replace(path, data);
325         return tx.commit();
326     }
327
328     private DataSchemaNode checkListAndOrderedType(final YangInstanceIdentifier path) {
329         // FIXME: we have this available in InstanceIdentifierContext
330         final var dataSchemaNode = DataSchemaContextTree.from(modelContext).findChild(path).orElseThrow()
331             .dataSchemaNode();
332
333         final String message;
334         if (dataSchemaNode instanceof ListSchemaNode listSchema) {
335             if (listSchema.isUserOrdered()) {
336                 return listSchema;
337             }
338             message = "Insert parameter can be used only with ordered-by user list.";
339         } else if (dataSchemaNode instanceof LeafListSchemaNode leafListSchema) {
340             if (leafListSchema.isUserOrdered()) {
341                 return leafListSchema;
342             }
343             message = "Insert parameter can be used only with ordered-by user leaf-list.";
344         } else {
345             message = "Insert parameter can be used only with list or leaf-list";
346         }
347         throw new RestconfDocumentedException(message, ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
348     }
349
350     /**
351      * Check mount point and prepare variables for post data.
352      *
353      * @param path    path
354      * @param data    data
355      * @param insert  {@link Insert}
356      */
357     public final void postData(final YangInstanceIdentifier path, final NormalizedNode data,
358             final @Nullable Insert insert) {
359         final ListenableFuture<? extends CommitInfo> future;
360         if (insert != null) {
361             final var parentPath = path.coerceParent();
362             checkListAndOrderedType(parentPath);
363             future = insertAndCommitPost(path, data, insert, parentPath);
364         } else {
365             future = createAndCommit(prepareWriteExecution(), path, data);
366         }
367         TransactionUtil.syncCommit(future, "POST", path);
368     }
369
370     private ListenableFuture<? extends CommitInfo> insertAndCommitPost(final YangInstanceIdentifier path,
371             final NormalizedNode data, final @NonNull Insert insert, final YangInstanceIdentifier parent) {
372         final var grandParent = parent.coerceParent();
373         final var tx = prepareWriteExecution();
374
375         return switch (insert.insert()) {
376             case FIRST -> {
377                 final var readData = tx.readList(grandParent);
378                 if (readData == null || readData.isEmpty()) {
379                     tx.replace(path, data);
380                 } else {
381                     checkItemDoesNotExists(exists(path), path);
382                     tx.remove(grandParent);
383                     tx.replace(path, data);
384                     tx.replace(grandParent, readData);
385                 }
386                 yield tx.commit();
387             }
388             case LAST -> createAndCommit(tx, path, data);
389             case BEFORE -> {
390                 final var readData = tx.readList(grandParent);
391                 if (readData == null || readData.isEmpty()) {
392                     tx.replace(path, data);
393                 } else {
394                     checkItemDoesNotExists(exists(path), path);
395                     insertWithPointPost(tx, path, data, verifyNotNull(insert.point()), readData, grandParent, true);
396                 }
397                 yield tx.commit();
398             }
399             case AFTER -> {
400                 final var readData = tx.readList(grandParent);
401                 if (readData == null || readData.isEmpty()) {
402                     tx.replace(path, data);
403                 } else {
404                     checkItemDoesNotExists(exists(path), path);
405                     insertWithPointPost(tx, path, data, verifyNotNull(insert.point()), readData, grandParent, false);
406                 }
407                 yield tx.commit();
408             }
409         };
410     }
411
412     /**
413      * Process edit operations of one {@link PatchContext}.
414      *
415      * @param patch Patch context to be processed
416      * @return {@link PatchStatusContext}
417      */
418     public final @NonNull PatchStatusContext patchData(final PatchContext patch) {
419         final var editCollection = new ArrayList<PatchStatusEntity>();
420         final var tx = prepareWriteExecution();
421
422         boolean noError = true;
423         for (var patchEntity : patch.entities()) {
424             if (noError) {
425                 final var targetNode = patchEntity.getTargetNode();
426                 final var editId = patchEntity.getEditId();
427
428                 switch (patchEntity.getOperation()) {
429                     case Create:
430                         try {
431                             tx.create(targetNode, patchEntity.getNode());
432                             editCollection.add(new PatchStatusEntity(editId, true, null));
433                         } catch (RestconfDocumentedException e) {
434                             editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
435                             noError = false;
436                         }
437                         break;
438                     case Delete:
439                         try {
440                             tx.delete(targetNode);
441                             editCollection.add(new PatchStatusEntity(editId, true, null));
442                         } catch (RestconfDocumentedException e) {
443                             editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
444                             noError = false;
445                         }
446                         break;
447                     case Merge:
448                         try {
449                             tx.ensureParentsByMerge(targetNode);
450                             tx.merge(targetNode, patchEntity.getNode());
451                             editCollection.add(new PatchStatusEntity(editId, true, null));
452                         } catch (RestconfDocumentedException e) {
453                             editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
454                             noError = false;
455                         }
456                         break;
457                     case Replace:
458                         try {
459                             tx.replace(targetNode, patchEntity.getNode());
460                             editCollection.add(new PatchStatusEntity(editId, true, null));
461                         } catch (RestconfDocumentedException e) {
462                             editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
463                             noError = false;
464                         }
465                         break;
466                     case Remove:
467                         try {
468                             tx.remove(targetNode);
469                             editCollection.add(new PatchStatusEntity(editId, true, null));
470                         } catch (RestconfDocumentedException e) {
471                             editCollection.add(new PatchStatusEntity(editId, false, e.getErrors()));
472                             noError = false;
473                         }
474                         break;
475                     default:
476                         editCollection.add(new PatchStatusEntity(editId, false, List.of(
477                             new RestconfError(ErrorType.PROTOCOL, ErrorTag.OPERATION_NOT_SUPPORTED,
478                                 "Not supported Yang Patch operation"))));
479                         noError = false;
480                         break;
481                 }
482             } else {
483                 break;
484             }
485         }
486
487         // if no errors then submit transaction, otherwise cancel
488         final var patchId = patch.patchId();
489         if (noError) {
490             try {
491                 TransactionUtil.syncCommit(tx.commit(), "PATCH", null);
492             } catch (RestconfDocumentedException e) {
493                 // if errors occurred during transaction commit then patch failed and global errors are reported
494                 return new PatchStatusContext(modelContext, patchId, List.copyOf(editCollection), false, e.getErrors());
495             }
496
497             return new PatchStatusContext(modelContext, patchId, List.copyOf(editCollection), true, null);
498         } else {
499             tx.cancel();
500             return new PatchStatusContext(modelContext, patchId, List.copyOf(editCollection), false, null);
501         }
502     }
503
504     private void insertWithPointPost(final RestconfTransaction tx, final YangInstanceIdentifier path,
505             final NormalizedNode data, final PointParam point, final NormalizedNodeContainer<?> readList,
506             final YangInstanceIdentifier grandParentPath, final boolean before) {
507         tx.remove(grandParentPath);
508         // FIXME: this should have happened sooner
509         final var pointArg = YangInstanceIdentifierDeserializer.create(modelContext, point.value()).path
510             .getLastPathArgument();
511         int lastItemPosition = 0;
512         for (var nodeChild : readList.body()) {
513             if (nodeChild.name().equals(pointArg)) {
514                 break;
515             }
516             lastItemPosition++;
517         }
518         if (!before) {
519             lastItemPosition++;
520         }
521         int lastInsertedPosition = 0;
522         final var emptySubtree = ImmutableNodes.fromInstanceId(modelContext, grandParentPath);
523         tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
524         for (var nodeChild : readList.body()) {
525             if (lastInsertedPosition == lastItemPosition) {
526                 tx.replace(path, data);
527             }
528             final YangInstanceIdentifier childPath = grandParentPath.node(nodeChild.name());
529             tx.replace(childPath, nodeChild);
530             lastInsertedPosition++;
531         }
532     }
533
534     private static ListenableFuture<? extends CommitInfo> createAndCommit(final RestconfTransaction tx,
535             final YangInstanceIdentifier path, final NormalizedNode data) {
536         try {
537             tx.create(path, data);
538         } catch (RestconfDocumentedException e) {
539             // close transaction if any and pass exception further
540             tx.cancel();
541             throw e;
542         }
543
544         return tx.commit();
545     }
546
547     /**
548      * Check if items do NOT already exists at specified {@code path}.
549      *
550      * @param existsFuture if checked data exists
551      * @param path         Path to be checked
552      * @throws RestconfDocumentedException if data already exists.
553      */
554     static void checkItemDoesNotExists(final ListenableFuture<Boolean> existsFuture,
555             final YangInstanceIdentifier path) {
556         if (TransactionUtil.syncAccess(existsFuture, path)) {
557             LOG.trace("Operation via Restconf was not executed because data at {} already exists", path);
558             throw new RestconfDocumentedException("Data already exists", ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS,
559                 path);
560         }
561     }
562
563     /**
564      * Read specific type of data from data store via transaction. Close {@link DOMTransactionChain} if any
565      * inside of object {@link RestconfStrategy} provided as a parameter.
566      *
567      * @param content      type of data to read (config, state, all)
568      * @param path         the path to read
569      * @param defaultsMode value of with-defaults parameter
570      * @return {@link NormalizedNode}
571      */
572     public @Nullable NormalizedNode readData(final @NonNull ContentParam content,
573             final @NonNull YangInstanceIdentifier path, final WithDefaultsParam defaultsMode) {
574         return switch (content) {
575             case ALL -> {
576                 // PREPARE STATE DATA NODE
577                 final var stateDataNode = readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path);
578                 // PREPARE CONFIG DATA NODE
579                 final var configDataNode = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path);
580
581                 yield mergeConfigAndSTateDataIfNeeded(stateDataNode, defaultsMode == null ? configDataNode
582                     : prepareDataByParamWithDef(configDataNode, path, defaultsMode.mode()));
583             }
584             case CONFIG -> {
585                 final var read = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path);
586                 yield defaultsMode == null ? read
587                     : prepareDataByParamWithDef(read, path, defaultsMode.mode());
588             }
589             case NONCONFIG -> readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path);
590         };
591     }
592
593     /**
594      * Read specific type of data from data store via transaction with specified subtrees that should only be read.
595      * Close {@link DOMTransactionChain} inside of object {@link RestconfStrategy} provided as a parameter.
596      *
597      * @param content  type of data to read (config, state, all)
598      * @param path     the parent path to read
599      * @param withDefa value of with-defaults parameter
600      * @param fields   paths to selected subtrees which should be read, relative to to the parent path
601      * @return {@link NormalizedNode}
602      */
603     public @Nullable NormalizedNode readData(final @NonNull ContentParam content,
604             final @NonNull YangInstanceIdentifier path, final @Nullable WithDefaultsParam withDefa,
605             final @NonNull List<YangInstanceIdentifier> fields) {
606         return switch (content) {
607             case ALL -> {
608                 // PREPARE STATE DATA NODE
609                 final var stateDataNode = readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path, fields);
610                 // PREPARE CONFIG DATA NODE
611                 final var configDataNode = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path, fields);
612
613                 yield mergeConfigAndSTateDataIfNeeded(stateDataNode, withDefa == null ? configDataNode
614                     : prepareDataByParamWithDef(configDataNode, path, withDefa.mode()));
615             }
616             case CONFIG -> {
617                 final var read = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path, fields);
618                 yield withDefa == null ? read : prepareDataByParamWithDef(read, path, withDefa.mode());
619             }
620             case NONCONFIG -> readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path, fields);
621         };
622     }
623
624     private @Nullable NormalizedNode readDataViaTransaction(final LogicalDatastoreType store,
625             final YangInstanceIdentifier path) {
626         return TransactionUtil.syncAccess(read(store, path), path).orElse(null);
627     }
628
629     /**
630      * Read specific type of data {@link LogicalDatastoreType} via transaction in {@link RestconfStrategy} with
631      * specified subtrees that should only be read.
632      *
633      * @param store                 datastore type
634      * @param path                  parent path to selected fields
635      * @param closeTransactionChain if it is set to {@code true}, after transaction it will close transactionChain
636      *                              in {@link RestconfStrategy} if any
637      * @param fields                paths to selected subtrees which should be read, relative to to the parent path
638      * @return {@link NormalizedNode}
639      */
640     private @Nullable NormalizedNode readDataViaTransaction(final @NonNull LogicalDatastoreType store,
641             final @NonNull YangInstanceIdentifier path, final @NonNull List<YangInstanceIdentifier> fields) {
642         return TransactionUtil.syncAccess(read(store, path, fields), path).orElse(null);
643     }
644
645     private NormalizedNode prepareDataByParamWithDef(final NormalizedNode readData, final YangInstanceIdentifier path,
646             final WithDefaultsMode defaultsMode) {
647         final boolean trim = switch (defaultsMode) {
648             case Trim -> true;
649             case Explicit -> false;
650             case ReportAll, ReportAllTagged -> throw new RestconfDocumentedException(
651                 "Unsupported with-defaults value " + defaultsMode.getName());
652         };
653
654         // FIXME: we have this readily available in InstanceIdentifierContext
655         final var ctxNode = DataSchemaContextTree.from(modelContext).findChild(path).orElseThrow();
656         if (readData instanceof ContainerNode container) {
657             final var builder = Builders.containerBuilder().withNodeIdentifier(container.name());
658             buildCont(builder, container.body(), ctxNode, trim);
659             return builder.build();
660         } else if (readData instanceof MapEntryNode mapEntry) {
661             if (!(ctxNode.dataSchemaNode() instanceof ListSchemaNode listSchema)) {
662                 throw new IllegalStateException("Input " + mapEntry + " does not match " + ctxNode);
663             }
664
665             final var builder = Builders.mapEntryBuilder().withNodeIdentifier(mapEntry.name());
666             buildMapEntryBuilder(builder, mapEntry.body(), ctxNode, trim, listSchema.getKeyDefinition());
667             return builder.build();
668         } else {
669             throw new IllegalStateException("Unhandled data contract " + readData.contract());
670         }
671     }
672
673     private static void buildMapEntryBuilder(
674             final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> builder,
675             final Collection<@NonNull DataContainerChild> children, final DataSchemaContext ctxNode,
676             final boolean trim, final List<QName> keys) {
677         for (var child : children) {
678             final var childCtx = getChildContext(ctxNode, child);
679
680             if (child instanceof ContainerNode container) {
681                 appendContainer(builder, container, childCtx, trim);
682             } else if (child instanceof MapNode map) {
683                 appendMap(builder, map, childCtx, trim);
684             } else if (child instanceof LeafNode<?> leaf) {
685                 appendLeaf(builder, leaf, childCtx, trim, keys);
686             } else {
687                 // FIXME: we should never hit this, throw an ISE if this ever happens
688                 LOG.debug("Ignoring unhandled child contract {}", child.contract());
689             }
690         }
691     }
692
693     private static void appendContainer(final DataContainerNodeBuilder<?, ?> builder, final ContainerNode container,
694             final DataSchemaContext ctxNode, final boolean trim) {
695         final var childBuilder = Builders.containerBuilder().withNodeIdentifier(container.name());
696         buildCont(childBuilder, container.body(), ctxNode, trim);
697         builder.withChild(childBuilder.build());
698     }
699
700     private static void appendLeaf(final DataContainerNodeBuilder<?, ?> builder, final LeafNode<?> leaf,
701             final DataSchemaContext ctxNode, final boolean trim, final List<QName> keys) {
702         if (!(ctxNode.dataSchemaNode() instanceof LeafSchemaNode leafSchema)) {
703             throw new IllegalStateException("Input " + leaf + " does not match " + ctxNode);
704         }
705
706         // FIXME: Document now this works with the likes of YangInstanceIdentifier. I bet it does not.
707         final var defaultVal = leafSchema.getType().getDefaultValue().orElse(null);
708
709         // This is a combined check for when we need to emit the leaf.
710         if (
711             // We always have to emit key leaf values
712             keys.contains(leafSchema.getQName())
713             // trim == WithDefaultsParam.TRIM and the source is assumed to store explicit values:
714             //
715             //            When data is retrieved with a <with-defaults> parameter equal to
716             //            'trim', data nodes MUST NOT be reported if they contain the schema
717             //            default value.  Non-configuration data nodes containing the schema
718             //            default value MUST NOT be reported.
719             //
720             || trim && (defaultVal == null || !defaultVal.equals(leaf.body()))
721             // !trim == WithDefaultsParam.EXPLICIT and the source is assume to store explicit values... but I fail to
722             // grasp what we are doing here... emit only if it matches default ???!!!
723             // FIXME: The WithDefaultsParam.EXPLICIT says:
724             //
725             //            Data nodes set to the YANG default by the client are reported.
726             //
727             //        and RFC8040 (https://www.rfc-editor.org/rfc/rfc8040#page-60) says:
728             //
729             //            If the "with-defaults" parameter is set to "explicit", then the
730             //            server MUST adhere to the default-reporting behavior defined in
731             //            Section 3.3 of [RFC6243].
732             //
733             //        and then RFC6243 (https://www.rfc-editor.org/rfc/rfc6243#section-3.3) says:
734             //
735             //            When data is retrieved with a <with-defaults> parameter equal to
736             //            'explicit', a data node that was set by a client to its schema
737             //            default value MUST be reported.  A conceptual data node that would be
738             //            set by the server to the schema default value MUST NOT be reported.
739             //            Non-configuration data nodes containing the schema default value MUST
740             //            be reported.
741             //
742             // (rovarga): The source reports explicitly-defined leaves and does *not* create defaults by itself.
743             //            This seems to disregard the 'trim = true' case semantics (see above).
744             //            Combining the above, though, these checks are missing the 'non-config' check, which would
745             //            distinguish, but barring that this check is superfluous and results in the wrong semantics.
746             //            Without that input, this really should be  covered by the previous case.
747                 || !trim && defaultVal != null && defaultVal.equals(leaf.body())) {
748             builder.withChild(leaf);
749         }
750     }
751
752     private static void appendMap(final DataContainerNodeBuilder<?, ?> builder, final MapNode map,
753             final DataSchemaContext childCtx, final boolean trim) {
754         if (!(childCtx.dataSchemaNode() instanceof ListSchemaNode listSchema)) {
755             throw new IllegalStateException("Input " + map + " does not match " + childCtx);
756         }
757
758         final var childBuilder = switch (map.ordering()) {
759             case SYSTEM -> Builders.mapBuilder();
760             case USER -> Builders.orderedMapBuilder();
761         };
762         buildList(childBuilder.withNodeIdentifier(map.name()), map.body(), childCtx, trim,
763             listSchema.getKeyDefinition());
764         builder.withChild(childBuilder.build());
765     }
766
767     private static void buildList(final CollectionNodeBuilder<MapEntryNode, ? extends MapNode> builder,
768             final Collection<@NonNull MapEntryNode> entries, final DataSchemaContext ctxNode, final boolean trim,
769             final List<@NonNull QName> keys) {
770         for (var entry : entries) {
771             final var childCtx = getChildContext(ctxNode, entry);
772             final var mapEntryBuilder = Builders.mapEntryBuilder().withNodeIdentifier(entry.name());
773             buildMapEntryBuilder(mapEntryBuilder, entry.body(), childCtx, trim, keys);
774             builder.withChild(mapEntryBuilder.build());
775         }
776     }
777
778     private static void buildCont(final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> builder,
779             final Collection<DataContainerChild> children, final DataSchemaContext ctxNode, final boolean trim) {
780         for (var child : children) {
781             final var childCtx = getChildContext(ctxNode, child);
782             if (child instanceof ContainerNode container) {
783                 appendContainer(builder, container, childCtx, trim);
784             } else if (child instanceof MapNode map) {
785                 appendMap(builder, map, childCtx, trim);
786             } else if (child instanceof LeafNode<?> leaf) {
787                 appendLeaf(builder, leaf, childCtx, trim, List.of());
788             }
789         }
790     }
791
792     private static @NonNull DataSchemaContext getChildContext(final DataSchemaContext ctxNode,
793             final NormalizedNode child) {
794         final var childId = child.name();
795         final var childCtx = ctxNode instanceof DataSchemaContext.Composite composite ? composite.childByArg(childId)
796             : null;
797         if (childCtx == null) {
798             throw new NoSuchElementException("Cannot resolve child " + childId + " in " + ctxNode);
799         }
800         return childCtx;
801     }
802
803     private static NormalizedNode mergeConfigAndSTateDataIfNeeded(final NormalizedNode stateDataNode,
804                                                                   final NormalizedNode configDataNode) {
805         // if no data exists
806         if (stateDataNode == null && configDataNode == null) {
807             return null;
808         }
809
810         // return config data
811         if (stateDataNode == null) {
812             return configDataNode;
813         }
814
815         // return state data
816         if (configDataNode == null) {
817             return stateDataNode;
818         }
819
820         // merge data from config and state
821         return mergeStateAndConfigData(stateDataNode, configDataNode);
822     }
823
824     /**
825      * Merge state and config data into a single NormalizedNode.
826      *
827      * @param stateDataNode  data node of state data
828      * @param configDataNode data node of config data
829      * @return {@link NormalizedNode}
830      */
831     private static @NonNull NormalizedNode mergeStateAndConfigData(
832             final @NonNull NormalizedNode stateDataNode, final @NonNull NormalizedNode configDataNode) {
833         validateNodeMerge(stateDataNode, configDataNode);
834         // FIXME: this check is bogus, as it confuses yang.data.api (NormalizedNode) with yang.model.api (RpcDefinition)
835         if (configDataNode instanceof RpcDefinition) {
836             return prepareRpcData(configDataNode, stateDataNode);
837         } else {
838             return prepareData(configDataNode, stateDataNode);
839         }
840     }
841
842     /**
843      * Validates whether the two NormalizedNodes can be merged.
844      *
845      * @param stateDataNode  data node of state data
846      * @param configDataNode data node of config data
847      */
848     private static void validateNodeMerge(final @NonNull NormalizedNode stateDataNode,
849                                           final @NonNull NormalizedNode configDataNode) {
850         final QNameModule moduleOfStateData = stateDataNode.name().getNodeType().getModule();
851         final QNameModule moduleOfConfigData = configDataNode.name().getNodeType().getModule();
852         if (!moduleOfStateData.equals(moduleOfConfigData)) {
853             throw new RestconfDocumentedException("Unable to merge data from different modules.");
854         }
855     }
856
857     /**
858      * Prepare and map data for rpc.
859      *
860      * @param configDataNode data node of config data
861      * @param stateDataNode  data node of state data
862      * @return {@link NormalizedNode}
863      */
864     private static @NonNull NormalizedNode prepareRpcData(final @NonNull NormalizedNode configDataNode,
865                                                           final @NonNull NormalizedNode stateDataNode) {
866         final var mapEntryBuilder = Builders.mapEntryBuilder()
867             .withNodeIdentifier((NodeIdentifierWithPredicates) configDataNode.name());
868
869         // MAP CONFIG DATA
870         mapRpcDataNode(configDataNode, mapEntryBuilder);
871         // MAP STATE DATA
872         mapRpcDataNode(stateDataNode, mapEntryBuilder);
873
874         return Builders.mapBuilder()
875             .withNodeIdentifier(NodeIdentifier.create(configDataNode.name().getNodeType()))
876             .addChild(mapEntryBuilder.build())
877             .build();
878     }
879
880     /**
881      * Map node to map entry builder.
882      *
883      * @param dataNode        data node
884      * @param mapEntryBuilder builder for mapping data
885      */
886     private static void mapRpcDataNode(final @NonNull NormalizedNode dataNode,
887             final @NonNull DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> mapEntryBuilder) {
888         ((ContainerNode) dataNode).body().forEach(mapEntryBuilder::addChild);
889     }
890
891     /**
892      * Prepare and map all data from DS.
893      *
894      * @param configDataNode data node of config data
895      * @param stateDataNode  data node of state data
896      * @return {@link NormalizedNode}
897      */
898     @SuppressWarnings("unchecked")
899     private static @NonNull NormalizedNode prepareData(final @NonNull NormalizedNode configDataNode,
900                                                        final @NonNull NormalizedNode stateDataNode) {
901         if (configDataNode instanceof UserMapNode configMap) {
902             final var builder = Builders.orderedMapBuilder().withNodeIdentifier(configMap.name());
903             mapValueToBuilder(configMap.body(), ((UserMapNode) stateDataNode).body(), builder);
904             return builder.build();
905         } else if (configDataNode instanceof SystemMapNode configMap) {
906             final var builder = Builders.mapBuilder().withNodeIdentifier(configMap.name());
907             mapValueToBuilder(configMap.body(), ((SystemMapNode) stateDataNode).body(), builder);
908             return builder.build();
909         } else if (configDataNode instanceof MapEntryNode configEntry) {
910             final var builder = Builders.mapEntryBuilder().withNodeIdentifier(configEntry.name());
911             mapValueToBuilder(configEntry.body(), ((MapEntryNode) stateDataNode).body(), builder);
912             return builder.build();
913         } else if (configDataNode instanceof ContainerNode configContaienr) {
914             final var builder = Builders.containerBuilder().withNodeIdentifier(configContaienr.name());
915             mapValueToBuilder(configContaienr.body(), ((ContainerNode) stateDataNode).body(), builder);
916             return builder.build();
917         } else if (configDataNode instanceof ChoiceNode configChoice) {
918             final var builder = Builders.choiceBuilder().withNodeIdentifier(configChoice.name());
919             mapValueToBuilder(configChoice.body(), ((ChoiceNode) stateDataNode).body(), builder);
920             return builder.build();
921         } else if (configDataNode instanceof LeafNode configLeaf) {
922             // config trumps oper
923             return configLeaf;
924         } else if (configDataNode instanceof UserLeafSetNode) {
925             final var configLeafSet = (UserLeafSetNode<Object>) configDataNode;
926             final var builder = Builders.<Object>orderedLeafSetBuilder().withNodeIdentifier(configLeafSet.name());
927             mapValueToBuilder(configLeafSet.body(), ((UserLeafSetNode<Object>) stateDataNode).body(), builder);
928             return builder.build();
929         } else if (configDataNode instanceof SystemLeafSetNode) {
930             final var configLeafSet = (SystemLeafSetNode<Object>) configDataNode;
931             final var builder = Builders.<Object>leafSetBuilder().withNodeIdentifier(configLeafSet.name());
932             mapValueToBuilder(configLeafSet.body(), ((SystemLeafSetNode<Object>) stateDataNode).body(), builder);
933             return builder.build();
934         } else if (configDataNode instanceof LeafSetEntryNode<?> configEntry) {
935             // config trumps oper
936             return configEntry;
937         } else if (configDataNode instanceof UnkeyedListNode configList) {
938             final var builder = Builders.unkeyedListBuilder().withNodeIdentifier(configList.name());
939             mapValueToBuilder(configList.body(), ((UnkeyedListNode) stateDataNode).body(), builder);
940             return builder.build();
941         } else if (configDataNode instanceof UnkeyedListEntryNode configEntry) {
942             final var builder = Builders.unkeyedListEntryBuilder().withNodeIdentifier(configEntry.name());
943             mapValueToBuilder(configEntry.body(), ((UnkeyedListEntryNode) stateDataNode).body(), builder);
944             return builder.build();
945         } else {
946             throw new RestconfDocumentedException("Unexpected node type: " + configDataNode.getClass().getName());
947         }
948     }
949
950     /**
951      * Map value from container node to builder.
952      *
953      * @param configData collection of config data nodes
954      * @param stateData  collection of state data nodes
955      * @param builder    builder
956      */
957     private static <T extends NormalizedNode> void mapValueToBuilder(
958             final @NonNull Collection<T> configData, final @NonNull Collection<T> stateData,
959             final @NonNull NormalizedNodeContainerBuilder<?, PathArgument, T, ?> builder) {
960         final var configMap = configData.stream().collect(Collectors.toMap(NormalizedNode::name, Function.identity()));
961         final var stateMap = stateData.stream().collect(Collectors.toMap(NormalizedNode::name, Function.identity()));
962
963         // merge config and state data of children with different identifiers
964         mapDataToBuilder(configMap, stateMap, builder);
965
966         // merge config and state data of children with the same identifiers
967         mergeDataToBuilder(configMap, stateMap, builder);
968     }
969
970     /**
971      * Map data with different identifiers to builder. Data with different identifiers can be just added
972      * as childs to parent node.
973      *
974      * @param configMap map of config data nodes
975      * @param stateMap  map of state data nodes
976      * @param builder   - builder
977      */
978     private static <T extends NormalizedNode> void mapDataToBuilder(
979             final @NonNull Map<PathArgument, T> configMap, final @NonNull Map<PathArgument, T> stateMap,
980             final @NonNull NormalizedNodeContainerBuilder<?, PathArgument, T, ?> builder) {
981         configMap.entrySet().stream().filter(x -> !stateMap.containsKey(x.getKey())).forEach(
982             y -> builder.addChild(y.getValue()));
983         stateMap.entrySet().stream().filter(x -> !configMap.containsKey(x.getKey())).forEach(
984             y -> builder.addChild(y.getValue()));
985     }
986
987     /**
988      * Map data with the same identifiers to builder. Data with the same identifiers cannot be just added but we need to
989      * go one level down with {@code prepareData} method.
990      *
991      * @param configMap immutable config data
992      * @param stateMap  immutable state data
993      * @param builder   - builder
994      */
995     @SuppressWarnings("unchecked")
996     private static <T extends NormalizedNode> void mergeDataToBuilder(
997             final @NonNull Map<PathArgument, T> configMap, final @NonNull Map<PathArgument, T> stateMap,
998             final @NonNull NormalizedNodeContainerBuilder<?, PathArgument, T, ?> builder) {
999         // it is enough to process only config data because operational contains the same data
1000         configMap.entrySet().stream().filter(x -> stateMap.containsKey(x.getKey())).forEach(
1001             y -> builder.addChild((T) prepareData(y.getValue(), stateMap.get(y.getKey()))));
1002     }
1003 }