2 * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
10 import static java.util.Objects.requireNonNull;
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;
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;
66 * Implementation of RESTCONF operations on top of a raw NETCONF backend.
68 * @see NetconfDataTreeService
70 public final class NetconfRestconfStrategy extends RestconfStrategy {
71 private final NetconfDataTreeService netconfService;
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);
82 RestconfTransaction prepareWriteExecution() {
83 return new NetconfRestconfTransaction(modelContext(), netconfService);
87 void delete(final SettableRestconfFuture<Empty> future, final ServerRequest request,
88 final YangInstanceIdentifier path) {
89 final var tx = prepareWriteExecution();
91 Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
93 public void onSuccess(final CommitInfo result) {
94 future.set(Empty.value());
98 public void onFailure(final Throwable cause) {
99 future.setFailure(TransactionUtil.decodeException(cause, "DELETE", path, modelContext()));
101 }, MoreExecutors.directExecutor());
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;
112 tmp = fieldsParamToPaths(inference.modelContext(), path.schema(), fields);
113 } catch (RestconfDocumentedException e) {
114 return RestconfFuture.failed(e);
116 fieldPaths = tmp.isEmpty() ? null : tmp;
121 final NormalizedNode node;
122 if (fieldPaths != null) {
123 node = readData(params.content(), path.instance(), params.withDefaults(), fieldPaths);
125 node = readData(params.content(), path.instance(), params.withDefaults());
128 return completeDataGET(request.prettyPrint(), inference, WriterParameters.of(params.depth()), node, null);
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);
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);
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.
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}
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) {
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);
169 yield mergeConfigAndSTateDataIfNeeded(stateDataNode, withDefa == null ? configDataNode
170 : prepareDataByParamWithDef(configDataNode, path, withDefa.mode()));
173 final var read = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path, fields);
174 yield withDefa == null ? read : prepareDataByParamWithDef(read, path, withDefa.mode());
176 case NONCONFIG -> readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path, fields);
181 * Read specific type of data {@link LogicalDatastoreType} via transaction in {@link RestconfStrategy} with
182 * specified subtrees that should only be read.
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}
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);
195 ListenableFuture<Boolean> exists(final YangInstanceIdentifier path) {
196 return Futures.transform(remapException(netconfService.getConfig(path)),
197 optionalNode -> optionalNode != null && optionalNode.isPresent(),
198 MoreExecutors.directExecutor());
201 private static <T> ListenableFuture<T> remapException(final ListenableFuture<T> input) {
202 final var ret = SettableFuture.<T>create();
203 Futures.addCallback(input, new FutureCallback<>() {
205 public void onSuccess(final T result) {
210 public void onFailure(final Throwable cause) {
211 ret.setException(cause instanceof ReadFailedException ? cause
212 : new ReadFailedException("NETCONF operation failed", cause));
214 }, MoreExecutors.directExecutor());
219 * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with
220 * {@link NetconfDataTreeService}.
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}.
229 * Example: field 'a(b/c;d/e)' ('e' is place under choice node 'x') is parsed into following levels:
231 * - './a' +- 'a/b' - 'b/c'
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}
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();
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;
259 // Note: path is guaranteed to have at least one step
260 final var it = selector.path().iterator();
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();
269 // add parsed path element linked to its parent
270 pathElement = addChildPathElement(pathElement, step.identifier().bindTo(namespace));
271 } while (it.hasNext());
273 final var subs = selector.subSelectors();
274 if (!subs.isEmpty()) {
275 processSelectors(parsed, context, namespace, pathElement, subs);
277 parsed.add(pathElement);
282 private static LinkedPathElement addChildPathElement(final LinkedPathElement currentElement,
283 final QName childQName) {
284 final var collectedMixinNodes = new ArrayList<PathArgument>();
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);
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
303 collectedMixinNodes.add(actualContextNode.getPathStep());
304 actualContextNode = childByQName(actualContextNode, childQName);
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);
314 return new LinkedPathElement(currentElement, collectedMixinNodes, actualContextNode);
317 private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) {
318 return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null;
321 private static YangInstanceIdentifier buildPath(final LinkedPathElement lastPathElement) {
322 var pathElement = lastPathElement;
323 final var path = new LinkedList<PathArgument>();
325 path.addFirst(contextPathArgument(pathElement.targetNode));
326 path.addAll(0, pathElement.mixinNodesToTarget);
327 pathElement = pathElement.parentPathElement;
328 } while (pathElement.parentPathElement != null);
330 return YangInstanceIdentifier.of(path);
333 private static @NonNull PathArgument contextPathArgument(final DataSchemaContext context) {
334 final var arg = context.pathStep();
339 final var schema = context.dataSchemaNode();
340 if (schema instanceof ListSchemaNode listSchema && !listSchema.getKeyDefinition().isEmpty()) {
341 return NodeIdentifierWithPredicates.of(listSchema.getQName());
343 if (schema instanceof LeafListSchemaNode leafListSchema) {
344 return new NodeWithValue<>(leafListSchema.getQName(), Empty.value());
346 throw new UnsupportedOperationException("Unsupported schema " + schema);
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);
359 * {@link DataSchemaContext} of data element grouped with identifiers of leading mixin nodes and previous path
361 * - identifiers of mixin nodes on the path to the target node - required for construction of full valid
363 * - {@link LinkedPathElement} of the previous non-mixin node - required to successfully create a chain
364 * of {@link PathArgument}s
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
370 private record LinkedPathElement(
371 @Nullable LinkedPathElement parentPathElement,
372 @NonNull List<PathArgument> mixinNodesToTarget,
373 @NonNull DataSchemaContext targetNode) {
375 requireNonNull(mixinNodesToTarget);
376 requireNonNull(targetNode);