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