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;
11 import static org.opendaylight.mdsal.common.api.LogicalDatastoreType.CONFIGURATION;
13 import com.google.common.annotations.VisibleForTesting;
14 import com.google.common.collect.ImmutableMap;
15 import com.google.common.util.concurrent.FutureCallback;
16 import com.google.common.util.concurrent.ListenableFuture;
17 import com.google.common.util.concurrent.MoreExecutors;
18 import java.util.ArrayList;
19 import java.util.HashSet;
20 import java.util.List;
21 import java.util.Optional;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.opendaylight.mdsal.common.api.CommitInfo;
27 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
28 import org.opendaylight.mdsal.dom.api.DOMActionService;
29 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
30 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
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.restconf.api.FormattableBody;
36 import org.opendaylight.restconf.api.query.FieldsParam;
37 import org.opendaylight.restconf.api.query.FieldsParam.NodeSelector;
38 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
39 import org.opendaylight.restconf.common.errors.RestconfFuture;
40 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
41 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.RestconfNormalizedNodeWriter;
42 import org.opendaylight.restconf.server.api.DataGetParams;
43 import org.opendaylight.restconf.server.api.DataGetResult;
44 import org.opendaylight.restconf.server.api.DatabindContext;
45 import org.opendaylight.restconf.server.api.DatabindPath.Data;
46 import org.opendaylight.restconf.server.api.ServerRequest;
47 import org.opendaylight.restconf.server.spi.HttpGetResource;
48 import org.opendaylight.restconf.server.spi.NormalizedNodeWriterFactory;
49 import org.opendaylight.restconf.server.spi.RpcImplementation;
50 import org.opendaylight.restconf.server.spi.YangLibraryVersionResource;
51 import org.opendaylight.yangtools.yang.common.Empty;
52 import org.opendaylight.yangtools.yang.common.ErrorTag;
53 import org.opendaylight.yangtools.yang.common.ErrorType;
54 import org.opendaylight.yangtools.yang.common.QName;
55 import org.opendaylight.yangtools.yang.common.QNameModule;
56 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
57 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
58 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
59 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
60 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
63 * Implementation of RESTCONF operations using {@link DOMTransactionChain} and related concepts.
65 * @see DOMTransactionChain
66 * @see DOMDataTreeReadWriteTransaction
68 public final class MdsalRestconfStrategy extends RestconfStrategy {
69 private final @NonNull HttpGetResource yangLibraryVersion;
70 private final @NonNull DOMDataBroker dataBroker;
72 public MdsalRestconfStrategy(final DatabindContext databind, final DOMDataBroker dataBroker,
73 final ImmutableMap<QName, RpcImplementation> localRpcs, final @Nullable DOMRpcService rpcService,
74 final @Nullable DOMActionService actionService, final @Nullable YangTextSourceExtension sourceProvider,
75 final @Nullable DOMMountPointService mountPointService) {
76 super(databind, localRpcs, rpcService, actionService, sourceProvider, mountPointService);
77 this.dataBroker = requireNonNull(dataBroker);
78 yangLibraryVersion = YangLibraryVersionResource.of(databind);
82 public RestconfFuture<FormattableBody> yangLibraryVersionGET(final ServerRequest request) {
83 return yangLibraryVersion.httpGET(request);
87 RestconfTransaction prepareWriteExecution() {
88 return new MdsalRestconfTransaction(modelContext(), dataBroker);
92 void delete(final SettableRestconfFuture<Empty> future, final ServerRequest request,
93 final YangInstanceIdentifier path) {
94 final var tx = dataBroker.newReadWriteTransaction();
95 tx.exists(CONFIGURATION, path).addCallback(new FutureCallback<>() {
97 public void onSuccess(final Boolean result) {
99 cancelTx(new RestconfDocumentedException("Data does not exist", ErrorType.PROTOCOL,
100 ErrorTag.DATA_MISSING, path));
104 tx.delete(CONFIGURATION, path);
105 tx.commit().addCallback(new FutureCallback<CommitInfo>() {
107 public void onSuccess(final CommitInfo result) {
108 future.set(Empty.value());
112 public void onFailure(final Throwable cause) {
113 future.setFailure(new RestconfDocumentedException("Transaction to delete " + path + " failed",
116 }, MoreExecutors.directExecutor());
120 public void onFailure(final Throwable cause) {
121 cancelTx(new RestconfDocumentedException("Failed to access " + path, cause));
124 private void cancelTx(final RestconfDocumentedException ex) {
126 future.setFailure(ex);
128 }, MoreExecutors.directExecutor());
132 RestconfFuture<DataGetResult> dataGET(final ServerRequest request, final Data path, final DataGetParams params) {
133 final var depth = params.depth();
134 final var fields = params.fields();
135 final var writerFactory = fields == null ? NormalizedNodeWriterFactory.of(depth)
136 : new MdsalNormalizedNodeWriterFactory(
137 translateFieldsParam(path.inference().modelContext(), path.schema(), fields), depth);
139 return completeDataGET(readData(params.content(), path.instance(), params.withDefaults()), path, writerFactory,
144 ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
145 final YangInstanceIdentifier path) {
146 try (var tx = dataBroker.newReadOnlyTransaction()) {
147 return tx.read(store, path);
152 ListenableFuture<Boolean> exists(final YangInstanceIdentifier path) {
153 try (var tx = dataBroker.newReadOnlyTransaction()) {
154 return tx.exists(LogicalDatastoreType.CONFIGURATION, path);
159 * Translate a {@link FieldsParam} to a complete list of child nodes organized into levels, suitable for use with
160 * {@link RestconfNormalizedNodeWriter}.
163 * Fields parser that stores set of {@link QName}s in each level. Because of this fact, from the output it is only
164 * possible to assume on what depth the selected element is placed. Identifiers of intermediary mixin nodes are also
165 * flatten to the same level as identifiers of data nodes.<br>
166 * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:<br>
168 * level 0: ['a', 'd']
169 * level 1: ['b', 'x', 'e']
173 * @param modelContext EffectiveModelContext
174 * @param startNode {@link DataSchemaContext} of the API request path
175 * @param input input value of fields parameter
176 * @return {@link List} of levels; each level contains set of {@link QName}
179 public static @NonNull List<Set<QName>> translateFieldsParam(final @NonNull EffectiveModelContext modelContext,
180 final DataSchemaContext startNode, final @NonNull FieldsParam input) {
181 final var parsed = new ArrayList<Set<QName>>();
182 processSelectors(parsed, modelContext, startNode.dataSchemaNode().getQName().getModule(), startNode,
183 input.nodeSelectors(), 0);
187 private static void processSelectors(final List<Set<QName>> parsed, final EffectiveModelContext context,
188 final QNameModule startNamespace, final DataSchemaContext startNode, final List<NodeSelector> selectors,
190 final Set<QName> startLevel;
191 if (parsed.size() <= index) {
192 startLevel = new HashSet<>();
193 parsed.add(startLevel);
195 startLevel = parsed.get(index);
197 for (var selector : selectors) {
198 var node = startNode;
199 var namespace = startNamespace;
200 var level = startLevel;
201 var levelIndex = index;
203 // Note: path is guaranteed to have at least one step
204 final var it = selector.path().iterator();
206 // FIXME: The layout of this loop is rather weird, which is due to how prepareQNameLevel() operates. We
207 // need to call it only when we know there is another identifier coming, otherwise we would end
208 // up with empty levels sneaking into the mix.
210 // Dealing with that weirdness requires understanding what the expected end results are and a
211 // larger rewrite of the algorithms involved.
212 final var step = it.next();
213 final var module = step.module();
214 if (module != null) {
215 // FIXME: this is not defensive enough, as we can fail to find the module
216 namespace = context.findModules(module).iterator().next().getQNameModule();
219 // add parsed identifier to results for current level
220 node = addChildToResult(node, step.identifier().bindTo(namespace), level);
226 level = prepareQNameLevel(parsed, level);
230 final var subs = selector.subSelectors();
231 if (!subs.isEmpty()) {
232 processSelectors(parsed, context, namespace, node, subs, levelIndex + 1);
238 * Preparation of the identifiers level that is used as storage for parsed identifiers. If the current level exist
239 * at the index that doesn't equal to the last index of already parsed identifiers, a new level of identifiers
240 * is allocated and pushed to input parsed identifiers.
242 * @param parsedIdentifiers Already parsed list of identifiers grouped to multiple levels.
243 * @param currentLevel Current level of identifiers (set).
244 * @return Existing or new level of identifiers.
246 private static Set<QName> prepareQNameLevel(final List<Set<QName>> parsedIdentifiers,
247 final Set<QName> currentLevel) {
248 final var existingLevel = parsedIdentifiers.stream()
249 .filter(qNameSet -> qNameSet.equals(currentLevel))
251 if (existingLevel.isPresent()) {
252 final int index = parsedIdentifiers.indexOf(existingLevel.orElseThrow());
253 if (index == parsedIdentifiers.size() - 1) {
254 final var nextLevel = new HashSet<QName>();
255 parsedIdentifiers.add(nextLevel);
259 return parsedIdentifiers.get(index + 1);
262 final var nextLevel = new HashSet<QName>();
263 parsedIdentifiers.add(nextLevel);
268 * Add parsed child of current node to result for current level.
270 * @param currentNode current node
271 * @param childQName parsed identifier of child node
272 * @param level current nodes level
273 * @return {@link DataSchemaContextNode}
275 private static DataSchemaContext addChildToResult(final DataSchemaContext currentNode, final QName childQName,
276 final Set<QName> level) {
277 // resolve parent node
278 final var parentNode = resolveMixinNode(currentNode, level, currentNode.dataSchemaNode().getQName());
279 if (parentNode == null) {
280 throw new RestconfDocumentedException(
281 "Not-mixin node missing in " + currentNode.getPathStep().getNodeType().getLocalName(),
282 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
285 // resolve child node
286 final DataSchemaContext childNode = resolveMixinNode(childByQName(parentNode, childQName), level, childQName);
287 if (childNode == null) {
288 throw new RestconfDocumentedException(
289 "Child " + childQName.getLocalName() + " node missing in "
290 + currentNode.getPathStep().getNodeType().getLocalName(),
291 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
294 // add final childNode node to level nodes
295 level.add(childNode.dataSchemaNode().getQName());
299 private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) {
300 return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null;
304 * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
305 * All nodes expect of not mixin node are added to current level nodes.
307 * @param node initial mixin or not-mixin node
308 * @param level current nodes level
309 * @param qualifiedName qname of initial node
310 * @return {@link DataSchemaContextNode}
312 private static @Nullable DataSchemaContext resolveMixinNode(final @Nullable DataSchemaContext node,
313 final @NonNull Set<QName> level, final @NonNull QName qualifiedName) {
314 DataSchemaContext currentNode = node;
315 while (currentNode instanceof PathMixin currentMixin) {
316 level.add(qualifiedName);
317 currentNode = currentMixin.childByQName(qualifiedName);