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