06a68f3dd93e44caa3696567d0b4e469fead5834
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / transactions / MdsalRestconfStrategy.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 import static org.opendaylight.mdsal.common.api.LogicalDatastoreType.CONFIGURATION;
12
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;
22 import java.util.Set;
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.nb.rfc8040.legacy.WriterParameters;
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.HttpGetResource;
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;
61
62 /**
63  * Implementation of RESTCONF operations using {@link DOMTransactionChain} and related concepts.
64  *
65  * @see DOMTransactionChain
66  * @see DOMDataTreeReadWriteTransaction
67  */
68 public final class MdsalRestconfStrategy extends RestconfStrategy {
69     private final @NonNull HttpGetResource yangLibraryVersion;
70     private final @NonNull DOMDataBroker dataBroker;
71
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);
79     }
80
81     @NonNullByDefault
82     public RestconfFuture<FormattableBody> yangLibraryVersionGET(final ServerRequest request) {
83         return yangLibraryVersion.httpGET(request);
84     }
85
86     @Override
87     RestconfTransaction prepareWriteExecution() {
88         return new MdsalRestconfTransaction(modelContext(), dataBroker);
89     }
90
91     @Override
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<>() {
96             @Override
97             public void onSuccess(final Boolean result) {
98                 if (!result) {
99                     cancelTx(new RestconfDocumentedException("Data does not exist", ErrorType.PROTOCOL,
100                         ErrorTag.DATA_MISSING, path));
101                     return;
102                 }
103
104                 tx.delete(CONFIGURATION, path);
105                 tx.commit().addCallback(new FutureCallback<CommitInfo>() {
106                     @Override
107                     public void onSuccess(final CommitInfo result) {
108                         future.set(Empty.value());
109                     }
110
111                     @Override
112                     public void onFailure(final Throwable cause) {
113                         future.setFailure(new RestconfDocumentedException("Transaction to delete " + path + " failed",
114                             cause));
115                     }
116                 }, MoreExecutors.directExecutor());
117             }
118
119             @Override
120             public void onFailure(final Throwable cause) {
121                 cancelTx(new RestconfDocumentedException("Failed to access " + path, cause));
122             }
123
124             private void cancelTx(final RestconfDocumentedException ex) {
125                 tx.cancel();
126                 future.setFailure(ex);
127             }
128         }, MoreExecutors.directExecutor());
129     }
130
131     @Override
132     RestconfFuture<DataGetResult> dataGET(final ServerRequest request, final Data path, final DataGetParams params) {
133         final var inference = path.inference();
134         final var fields = params.fields();
135         return completeDataGET(request.prettyPrint(), inference,
136             fields == null ? WriterParameters.of(params.depth())
137                 : new WriterParameters(params.depth(),
138                     translateFieldsParam(inference.modelContext(), path.schema(), fields)),
139             readData(params.content(), path.instance(), params.withDefaults()), null);
140     }
141
142     @Override
143     ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
144             final YangInstanceIdentifier path) {
145         try (var tx = dataBroker.newReadOnlyTransaction()) {
146             return tx.read(store, path);
147         }
148     }
149
150     @Override
151     ListenableFuture<Boolean> exists(final YangInstanceIdentifier path) {
152         try (var tx = dataBroker.newReadOnlyTransaction()) {
153             return tx.exists(LogicalDatastoreType.CONFIGURATION, path);
154         }
155     }
156
157     /**
158      * Translate a {@link FieldsParam} to a complete list of child nodes organized into levels, suitable for use with
159      * {@link RestconfNormalizedNodeWriter}.
160      *
161      * <p>
162      * Fields parser that stores set of {@link QName}s in each level. Because of this fact, from the output it is only
163      * possible to assume on what depth the selected element is placed. Identifiers of intermediary mixin nodes are also
164      * flatten to the same level as identifiers of data nodes.<br>
165      * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:<br>
166      * <pre>
167      * level 0: ['a', 'd']
168      * level 1: ['b', 'x', 'e']
169      * level 2: ['c']
170      * </pre>
171      *
172      * @param modelContext EffectiveModelContext
173      * @param startNode {@link DataSchemaContext} of the API request path
174      * @param input input value of fields parameter
175      * @return {@link List} of levels; each level contains set of {@link QName}
176      */
177     @VisibleForTesting
178     public static @NonNull List<Set<QName>> translateFieldsParam(final @NonNull EffectiveModelContext modelContext,
179             final DataSchemaContext startNode, final @NonNull FieldsParam input) {
180         final var parsed = new ArrayList<Set<QName>>();
181         processSelectors(parsed, modelContext, startNode.dataSchemaNode().getQName().getModule(), startNode,
182             input.nodeSelectors(), 0);
183         return parsed;
184     }
185
186     private static void processSelectors(final List<Set<QName>> parsed, final EffectiveModelContext context,
187             final QNameModule startNamespace, final DataSchemaContext startNode, final List<NodeSelector> selectors,
188             final int index) {
189         final Set<QName> startLevel;
190         if (parsed.size() <= index) {
191             startLevel = new HashSet<>();
192             parsed.add(startLevel);
193         } else {
194             startLevel = parsed.get(index);
195         }
196         for (var selector : selectors) {
197             var node = startNode;
198             var namespace = startNamespace;
199             var level = startLevel;
200             var levelIndex = index;
201
202             // Note: path is guaranteed to have at least one step
203             final var it = selector.path().iterator();
204             while (true) {
205                 // FIXME: The layout of this loop is rather weird, which is due to how prepareQNameLevel() operates. We
206                 //        need to call it only when we know there is another identifier coming, otherwise we would end
207                 //        up with empty levels sneaking into the mix.
208                 //
209                 //        Dealing with that weirdness requires understanding what the expected end results are and a
210                 //        larger rewrite of the algorithms involved.
211                 final var step = it.next();
212                 final var module = step.module();
213                 if (module != null) {
214                     // FIXME: this is not defensive enough, as we can fail to find the module
215                     namespace = context.findModules(module).iterator().next().getQNameModule();
216                 }
217
218                 // add parsed identifier to results for current level
219                 node = addChildToResult(node, step.identifier().bindTo(namespace), level);
220                 if (!it.hasNext()) {
221                     break;
222                 }
223
224                 // go one level down
225                 level = prepareQNameLevel(parsed, level);
226                 levelIndex++;
227             }
228
229             final var subs = selector.subSelectors();
230             if (!subs.isEmpty()) {
231                 processSelectors(parsed, context, namespace, node, subs, levelIndex + 1);
232             }
233         }
234     }
235
236     /**
237      * Preparation of the identifiers level that is used as storage for parsed identifiers. If the current level exist
238      * at the index that doesn't equal to the last index of already parsed identifiers, a new level of identifiers
239      * is allocated and pushed to input parsed identifiers.
240      *
241      * @param parsedIdentifiers Already parsed list of identifiers grouped to multiple levels.
242      * @param currentLevel Current level of identifiers (set).
243      * @return Existing or new level of identifiers.
244      */
245     private static Set<QName> prepareQNameLevel(final List<Set<QName>> parsedIdentifiers,
246             final Set<QName> currentLevel) {
247         final var existingLevel = parsedIdentifiers.stream()
248                 .filter(qNameSet -> qNameSet.equals(currentLevel))
249                 .findAny();
250         if (existingLevel.isPresent()) {
251             final int index = parsedIdentifiers.indexOf(existingLevel.orElseThrow());
252             if (index == parsedIdentifiers.size() - 1) {
253                 final var nextLevel = new HashSet<QName>();
254                 parsedIdentifiers.add(nextLevel);
255                 return nextLevel;
256             }
257
258             return parsedIdentifiers.get(index + 1);
259         }
260
261         final var nextLevel = new HashSet<QName>();
262         parsedIdentifiers.add(nextLevel);
263         return nextLevel;
264     }
265
266     /**
267      * Add parsed child of current node to result for current level.
268      *
269      * @param currentNode current node
270      * @param childQName parsed identifier of child node
271      * @param level current nodes level
272      * @return {@link DataSchemaContextNode}
273      */
274     private static DataSchemaContext addChildToResult(final DataSchemaContext currentNode, final QName childQName,
275             final Set<QName> level) {
276         // resolve parent node
277         final var parentNode = resolveMixinNode(currentNode, level, currentNode.dataSchemaNode().getQName());
278         if (parentNode == null) {
279             throw new RestconfDocumentedException(
280                     "Not-mixin node missing in " + currentNode.getPathStep().getNodeType().getLocalName(),
281                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
282         }
283
284         // resolve child node
285         final DataSchemaContext childNode = resolveMixinNode(childByQName(parentNode, childQName), level, childQName);
286         if (childNode == null) {
287             throw new RestconfDocumentedException(
288                     "Child " + childQName.getLocalName() + " node missing in "
289                             + currentNode.getPathStep().getNodeType().getLocalName(),
290                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
291         }
292
293         // add final childNode node to level nodes
294         level.add(childNode.dataSchemaNode().getQName());
295         return childNode;
296     }
297
298     private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) {
299         return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null;
300     }
301
302     /**
303      * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
304      * All nodes expect of not mixin node are added to current level nodes.
305      *
306      * @param node          initial mixin or not-mixin node
307      * @param level         current nodes level
308      * @param qualifiedName qname of initial node
309      * @return {@link DataSchemaContextNode}
310      */
311     private static @Nullable DataSchemaContext resolveMixinNode(final @Nullable DataSchemaContext node,
312             final @NonNull Set<QName> level, final @NonNull QName qualifiedName) {
313         DataSchemaContext currentNode = node;
314         while (currentNode instanceof PathMixin currentMixin) {
315             level.add(qualifiedName);
316             currentNode = currentMixin.childByQName(qualifiedName);
317         }
318         return currentNode;
319     }
320 }