Eliminate NormalizedNodePayload
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / transactions / NetconfRestconfStrategy.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 java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.VisibleForTesting;
13 import com.google.common.collect.ImmutableMap;
14 import com.google.common.util.concurrent.FutureCallback;
15 import com.google.common.util.concurrent.Futures;
16 import com.google.common.util.concurrent.ListenableFuture;
17 import com.google.common.util.concurrent.MoreExecutors;
18 import com.google.common.util.concurrent.SettableFuture;
19 import java.util.ArrayList;
20 import java.util.HashSet;
21 import java.util.LinkedList;
22 import java.util.List;
23 import java.util.Optional;
24 import java.util.Set;
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.common.api.ReadFailedException;
30 import org.opendaylight.mdsal.dom.api.DOMActionService;
31 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
32 import org.opendaylight.mdsal.dom.api.DOMRpcService;
33 import org.opendaylight.mdsal.dom.api.DOMSchemaService.YangTextSourceExtension;
34 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
35 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
36 import org.opendaylight.restconf.api.query.ContentParam;
37 import org.opendaylight.restconf.api.query.FieldsParam;
38 import org.opendaylight.restconf.api.query.FieldsParam.NodeSelector;
39 import org.opendaylight.restconf.api.query.WithDefaultsParam;
40 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
41 import org.opendaylight.restconf.common.errors.RestconfFuture;
42 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
43 import org.opendaylight.restconf.server.api.DataGetParams;
44 import org.opendaylight.restconf.server.api.DataGetResult;
45 import org.opendaylight.restconf.server.api.DatabindContext;
46 import org.opendaylight.restconf.server.api.DatabindPath.Data;
47 import org.opendaylight.restconf.server.api.ServerRequest;
48 import org.opendaylight.restconf.server.spi.NormalizedNodeWriterFactory;
49 import org.opendaylight.yangtools.yang.common.Empty;
50 import org.opendaylight.yangtools.yang.common.ErrorTag;
51 import org.opendaylight.yangtools.yang.common.ErrorType;
52 import org.opendaylight.yangtools.yang.common.QName;
53 import org.opendaylight.yangtools.yang.common.QNameModule;
54 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
55 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
56 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
57 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
58 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
59 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
60 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
61 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
62 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
63 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
64
65 /**
66  * Implementation of RESTCONF operations on top of a raw NETCONF backend.
67  *
68  * @see NetconfDataTreeService
69  */
70 public final class NetconfRestconfStrategy extends RestconfStrategy {
71     private final NetconfDataTreeService netconfService;
72
73     public NetconfRestconfStrategy(final DatabindContext databind, final NetconfDataTreeService netconfService,
74             final @Nullable DOMRpcService rpcService, final @Nullable DOMActionService actionService,
75             final @Nullable YangTextSourceExtension sourceProvider,
76             final @Nullable DOMMountPointService mountPointService) {
77         super(databind, ImmutableMap.of(), rpcService, actionService, sourceProvider, mountPointService);
78         this.netconfService = requireNonNull(netconfService);
79     }
80
81     @Override
82     RestconfTransaction prepareWriteExecution() {
83         return new NetconfRestconfTransaction(modelContext(), netconfService);
84     }
85
86     @Override
87     void delete(final SettableRestconfFuture<Empty> future, final ServerRequest request,
88             final YangInstanceIdentifier path) {
89         final var tx = prepareWriteExecution();
90         tx.delete(path);
91         Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
92             @Override
93             public void onSuccess(final CommitInfo result) {
94                 future.set(Empty.value());
95             }
96
97             @Override
98             public void onFailure(final Throwable cause) {
99                 future.setFailure(TransactionUtil.decodeException(cause, "DELETE", path, modelContext()));
100             }
101         }, MoreExecutors.directExecutor());
102     }
103
104     @Override
105     RestconfFuture<DataGetResult> dataGET(final ServerRequest request, final Data path, final DataGetParams params) {
106         final var fields = params.fields();
107         final List<YangInstanceIdentifier> fieldPaths;
108         if (fields != null) {
109             final List<YangInstanceIdentifier> tmp;
110             try {
111                 tmp = fieldsParamToPaths(path.inference().modelContext(), path.schema(), fields);
112             } catch (RestconfDocumentedException e) {
113                 return RestconfFuture.failed(e);
114             }
115             fieldPaths = tmp.isEmpty() ? null : tmp;
116         } else {
117             fieldPaths = null;
118         }
119
120         final NormalizedNode node;
121         if (fieldPaths != null) {
122             node = readData(params.content(), path.instance(), params.withDefaults(), fieldPaths);
123         } else {
124             node = readData(params.content(), path.instance(), params.withDefaults());
125         }
126         return completeDataGET(node, path, NormalizedNodeWriterFactory.of(params.depth()), null);
127     }
128
129     @Override
130     ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
131             final YangInstanceIdentifier path) {
132         return switch (store) {
133             case CONFIGURATION -> netconfService.getConfig(path);
134             case OPERATIONAL -> netconfService.get(path);
135         };
136     }
137
138     private ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
139             final YangInstanceIdentifier path, final List<YangInstanceIdentifier> fields) {
140         return switch (store) {
141             case CONFIGURATION -> netconfService.getConfig(path, fields);
142             case OPERATIONAL -> netconfService.get(path, fields);
143         };
144     }
145
146     /**
147      * Read specific type of data from data store via transaction with specified subtrees that should only be read.
148      * Close {@link DOMTransactionChain} inside of object {@link RestconfStrategy} provided as a parameter.
149      *
150      * @param content  type of data to read (config, state, all)
151      * @param path     the parent path to read
152      * @param withDefa value of with-defaults parameter
153      * @param fields   paths to selected subtrees which should be read, relative to the parent path
154      * @return {@link NormalizedNode}
155      */
156     // FIXME: NETCONF-1155: this method should asynchronous
157     public @Nullable NormalizedNode readData(final @NonNull ContentParam content,
158             final @NonNull YangInstanceIdentifier path, final @Nullable WithDefaultsParam withDefa,
159             final @NonNull List<YangInstanceIdentifier> fields) {
160         return switch (content) {
161             case ALL -> {
162                 // PREPARE STATE DATA NODE
163                 final var stateDataNode = readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path, fields);
164                 // PREPARE CONFIG DATA NODE
165                 final var configDataNode = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path, fields);
166
167                 yield mergeConfigAndSTateDataIfNeeded(stateDataNode, withDefa == null ? configDataNode
168                     : prepareDataByParamWithDef(configDataNode, path, withDefa.mode()));
169             }
170             case CONFIG -> {
171                 final var read = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path, fields);
172                 yield withDefa == null ? read : prepareDataByParamWithDef(read, path, withDefa.mode());
173             }
174             case NONCONFIG -> readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path, fields);
175         };
176     }
177
178     /**
179      * Read specific type of data {@link LogicalDatastoreType} via transaction in {@link RestconfStrategy} with
180      * specified subtrees that should only be read.
181      *
182      * @param store                 datastore type
183      * @param path                  parent path to selected fields
184      * @param fields                paths to selected subtrees which should be read, relative to the parent path
185      * @return {@link NormalizedNode}
186      */
187     private @Nullable NormalizedNode readDataViaTransaction(final @NonNull LogicalDatastoreType store,
188             final @NonNull YangInstanceIdentifier path, final @NonNull List<YangInstanceIdentifier> fields) {
189         return TransactionUtil.syncAccess(read(store, path, fields), path).orElse(null);
190     }
191
192     @Override
193     ListenableFuture<Boolean> exists(final YangInstanceIdentifier path) {
194         return Futures.transform(remapException(netconfService.getConfig(path)),
195             optionalNode -> optionalNode != null && optionalNode.isPresent(),
196             MoreExecutors.directExecutor());
197     }
198
199     private static <T> ListenableFuture<T> remapException(final ListenableFuture<T> input) {
200         final var ret = SettableFuture.<T>create();
201         Futures.addCallback(input, new FutureCallback<>() {
202             @Override
203             public void onSuccess(final T result) {
204                 ret.set(result);
205             }
206
207             @Override
208             public void onFailure(final Throwable cause) {
209                 ret.setException(cause instanceof ReadFailedException ? cause
210                     : new ReadFailedException("NETCONF operation failed", cause));
211             }
212         }, MoreExecutors.directExecutor());
213         return ret;
214     }
215
216     /**
217      * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with
218      * {@link NetconfDataTreeService}.
219      *
220      * <p>
221      * Fields parser that stores a set of all the leaf {@link LinkedPathElement}s specified in {@link FieldsParam}.
222      * Using {@link LinkedPathElement} it is possible to create a chain of path arguments and build complete paths
223      * since this element contains identifiers of intermediary mixin nodes and also linked to its parent
224      * {@link LinkedPathElement}.
225      *
226      * <p>
227      * Example: field 'a(b/c;d/e)' ('e' is place under choice node 'x') is parsed into following levels:
228      * <pre>
229      *   - './a' +- 'a/b' - 'b/c'
230      *           |
231      *           +- 'a/d' - 'd/x/e'
232      * </pre>
233      *
234      *
235      * @param modelContext EffectiveModelContext
236      * @param startNode Root DataSchemaNode
237      * @param input input value of fields parameter
238      * @return {@link List} of {@link YangInstanceIdentifier} that are relative to the last {@link PathArgument}
239      *         of provided {@code identifier}
240      */
241     @VisibleForTesting
242     static @NonNull List<YangInstanceIdentifier> fieldsParamToPaths(final @NonNull EffectiveModelContext modelContext,
243             final @NonNull DataSchemaContext startNode, final @NonNull FieldsParam input) {
244         final var parsed = new HashSet<LinkedPathElement>();
245         processSelectors(parsed, modelContext, startNode.dataSchemaNode().getQName().getModule(),
246             new LinkedPathElement(null, List.of(), startNode), input.nodeSelectors());
247         return parsed.stream().map(NetconfRestconfStrategy::buildPath).toList();
248     }
249
250     private static void processSelectors(final Set<LinkedPathElement> parsed, final EffectiveModelContext context,
251             final QNameModule startNamespace, final LinkedPathElement startPathElement,
252             final List<NodeSelector> selectors) {
253         for (var selector : selectors) {
254             var pathElement = startPathElement;
255             var namespace = startNamespace;
256
257             // Note: path is guaranteed to have at least one step
258             final var it = selector.path().iterator();
259             do {
260                 final var step = it.next();
261                 final var module = step.module();
262                 if (module != null) {
263                     // FIXME: this is not defensive enough, as we can fail to find the module
264                     namespace = context.findModules(module).iterator().next().getQNameModule();
265                 }
266
267                 // add parsed path element linked to its parent
268                 pathElement = addChildPathElement(pathElement, step.identifier().bindTo(namespace));
269             } while (it.hasNext());
270
271             final var subs = selector.subSelectors();
272             if (!subs.isEmpty()) {
273                 processSelectors(parsed, context, namespace, pathElement, subs);
274             } else {
275                 parsed.add(pathElement);
276             }
277         }
278     }
279
280     private static LinkedPathElement addChildPathElement(final LinkedPathElement currentElement,
281             final QName childQName) {
282         final var collectedMixinNodes = new ArrayList<PathArgument>();
283
284         DataSchemaContext currentNode = currentElement.targetNode;
285         DataSchemaContext actualContextNode = childByQName(currentNode, childQName);
286         if (actualContextNode == null) {
287             actualContextNode = resolveMixinNode(currentNode, currentNode.getPathStep().getNodeType());
288             actualContextNode = childByQName(actualContextNode, childQName);
289         }
290
291         while (actualContextNode != null && actualContextNode instanceof PathMixin) {
292             final var actualDataSchemaNode = actualContextNode.dataSchemaNode();
293             if (actualDataSchemaNode instanceof ListSchemaNode listSchema && listSchema.getKeyDefinition().isEmpty()) {
294                 // we need just a single node identifier from list in the path IFF it is an unkeyed list, otherwise
295                 // we need both (which is the default case)
296                 actualContextNode = childByQName(actualContextNode, childQName);
297             } else if (actualDataSchemaNode instanceof LeafListSchemaNode) {
298                 // NodeWithValue is unusable - stop parsing
299                 break;
300             } else {
301                 collectedMixinNodes.add(actualContextNode.getPathStep());
302                 actualContextNode = childByQName(actualContextNode, childQName);
303             }
304         }
305
306         if (actualContextNode == null) {
307             throw new RestconfDocumentedException("Child " + childQName.getLocalName() + " node missing in "
308                 + currentNode.getPathStep().getNodeType().getLocalName(),
309                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
310         }
311
312         return new LinkedPathElement(currentElement, collectedMixinNodes, actualContextNode);
313     }
314
315     private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) {
316         return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null;
317     }
318
319     private static YangInstanceIdentifier buildPath(final LinkedPathElement lastPathElement) {
320         var pathElement = lastPathElement;
321         final var path = new LinkedList<PathArgument>();
322         do {
323             path.addFirst(contextPathArgument(pathElement.targetNode));
324             path.addAll(0, pathElement.mixinNodesToTarget);
325             pathElement = pathElement.parentPathElement;
326         } while (pathElement.parentPathElement != null);
327
328         return YangInstanceIdentifier.of(path);
329     }
330
331     private static @NonNull PathArgument contextPathArgument(final DataSchemaContext context) {
332         final var arg = context.pathStep();
333         if (arg != null) {
334             return arg;
335         }
336
337         final var schema = context.dataSchemaNode();
338         if (schema instanceof ListSchemaNode listSchema && !listSchema.getKeyDefinition().isEmpty()) {
339             return NodeIdentifierWithPredicates.of(listSchema.getQName());
340         }
341         if (schema instanceof LeafListSchemaNode leafListSchema) {
342             return new NodeWithValue<>(leafListSchema.getQName(), Empty.value());
343         }
344         throw new UnsupportedOperationException("Unsupported schema " + schema);
345     }
346
347     private static DataSchemaContext resolveMixinNode(final DataSchemaContext node,
348             final @NonNull QName qualifiedName) {
349         DataSchemaContext currentNode = node;
350         while (currentNode != null && currentNode instanceof PathMixin currentMixin) {
351             currentNode = currentMixin.childByQName(qualifiedName);
352         }
353         return currentNode;
354     }
355
356     /**
357      * {@link DataSchemaContext} of data element grouped with identifiers of leading mixin nodes and previous path
358      * element.<br>
359      *  - identifiers of mixin nodes on the path to the target node - required for construction of full valid
360      *    DOM paths,<br>
361      *  - {@link LinkedPathElement} of the previous non-mixin node - required to successfully create a chain
362      *    of {@link PathArgument}s
363      *
364      * @param parentPathElement     parent path element
365      * @param mixinNodesToTarget    identifiers of mixin nodes on the path to the target node
366      * @param targetNode            target non-mixin node
367      */
368     private record LinkedPathElement(
369             @Nullable LinkedPathElement parentPathElement,
370             @NonNull List<PathArgument> mixinNodesToTarget,
371             @NonNull DataSchemaContext targetNode) {
372         LinkedPathElement {
373             requireNonNull(mixinNodesToTarget);
374             requireNonNull(targetNode);
375         }
376     }
377 }