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