Move DatabindProvider
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / server / mdsal / MdsalRestconfServer.java
1 /*
2  * Copyright (c) 2023 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.server.mdsal;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.VisibleForTesting;
13 import com.google.common.base.Splitter;
14 import com.google.common.collect.ImmutableMap;
15 import com.google.common.collect.ImmutableSet;
16 import com.google.common.collect.ImmutableSetMultimap;
17 import com.google.common.collect.Maps;
18 import com.google.common.util.concurrent.FutureCallback;
19 import com.google.common.util.concurrent.Futures;
20 import com.google.common.util.concurrent.MoreExecutors;
21 import java.io.IOException;
22 import java.lang.invoke.MethodHandles;
23 import java.lang.invoke.VarHandle;
24 import java.net.URI;
25 import java.time.format.DateTimeParseException;
26 import java.util.Comparator;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Locale;
30 import java.util.Map;
31 import java.util.Map.Entry;
32 import java.util.Optional;
33 import java.util.concurrent.CancellationException;
34 import javax.inject.Inject;
35 import javax.inject.Singleton;
36 import org.eclipse.jdt.annotation.NonNull;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
39 import org.opendaylight.mdsal.dom.api.DOMActionException;
40 import org.opendaylight.mdsal.dom.api.DOMActionResult;
41 import org.opendaylight.mdsal.dom.api.DOMActionService;
42 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
43 import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
44 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
45 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
46 import org.opendaylight.mdsal.dom.api.DOMRpcService;
47 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
48 import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
49 import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
50 import org.opendaylight.restconf.api.ApiPath;
51 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
52 import org.opendaylight.restconf.common.errors.RestconfFuture;
53 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
54 import org.opendaylight.restconf.common.patch.PatchContext;
55 import org.opendaylight.restconf.common.patch.PatchStatusContext;
56 import org.opendaylight.restconf.nb.rfc8040.Insert;
57 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
58 import org.opendaylight.restconf.nb.rfc8040.databind.ChildBody;
59 import org.opendaylight.restconf.nb.rfc8040.databind.DataPostBody;
60 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
61 import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
62 import org.opendaylight.restconf.nb.rfc8040.databind.PatchBody;
63 import org.opendaylight.restconf.nb.rfc8040.databind.ResourceBody;
64 import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
65 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
66 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
67 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
68 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
69 import org.opendaylight.restconf.server.api.DataPostResult;
70 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
71 import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
72 import org.opendaylight.restconf.server.api.DataPutResult;
73 import org.opendaylight.restconf.server.api.ModulesGetResult;
74 import org.opendaylight.restconf.server.api.OperationsGetResult;
75 import org.opendaylight.restconf.server.api.RestconfServer;
76 import org.opendaylight.restconf.server.spi.DatabindProvider;
77 import org.opendaylight.restconf.server.spi.OperationInput;
78 import org.opendaylight.restconf.server.spi.OperationOutput;
79 import org.opendaylight.restconf.server.spi.RpcImplementation;
80 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.YangApi;
81 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.restconf.Restconf;
82 import org.opendaylight.yangtools.yang.common.Empty;
83 import org.opendaylight.yangtools.yang.common.ErrorTag;
84 import org.opendaylight.yangtools.yang.common.ErrorType;
85 import org.opendaylight.yangtools.yang.common.QName;
86 import org.opendaylight.yangtools.yang.common.QNameModule;
87 import org.opendaylight.yangtools.yang.common.Revision;
88 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
89 import org.opendaylight.yangtools.yang.common.XMLNamespace;
90 import org.opendaylight.yangtools.yang.common.YangNames;
91 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
92 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
93 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
94 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
95 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
96 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
97 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
98 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
99 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
100 import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation;
101 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
102 import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
103 import org.opendaylight.yangtools.yang.model.repo.api.YinTextSchemaSource;
104 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
105 import org.osgi.service.component.annotations.Activate;
106 import org.osgi.service.component.annotations.Component;
107 import org.osgi.service.component.annotations.Reference;
108 import org.slf4j.Logger;
109 import org.slf4j.LoggerFactory;
110
111 /**
112  * A RESTCONF server implemented on top of MD-SAL.
113  */
114 @Singleton
115 @Component
116 public final class MdsalRestconfServer implements RestconfServer {
117     private static final Logger LOG = LoggerFactory.getLogger(MdsalRestconfServer.class);
118     private static final QName YANG_LIBRARY_VERSION = QName.create(Restconf.QNAME, "yang-library-version").intern();
119     private static final VarHandle LOCAL_STRATEGY;
120
121     static {
122         try {
123             LOCAL_STRATEGY = MethodHandles.lookup()
124                 .findVarHandle(MdsalRestconfServer.class, "localStrategy", RestconfStrategy.class);
125         } catch (NoSuchFieldException | IllegalAccessException e) {
126             throw new ExceptionInInitializerError(e);
127         }
128     }
129
130     // FIXME: Remove this constant. All logic relying on this constant should instead rely on YangInstanceIdentifier
131     //        equivalent coming out of argument parsing. This may require keeping List<YangInstanceIdentifier> as the
132     //        nested path split on yang-ext:mount. This splitting needs to be based on consulting the
133     //        EffectiveModelContext and allowing it only where yang-ext:mount is actually used in models.
134     @Deprecated(forRemoval = true)
135     private static final String MOUNT = "yang-ext:mount";
136     @Deprecated(forRemoval = true)
137     private static final Splitter SLASH_SPLITTER = Splitter.on('/');
138
139     private final @NonNull ImmutableMap<QName, RpcImplementation> localRpcs;
140     private final @NonNull DOMMountPointService mountPointService;
141     private final @NonNull DatabindProvider databindProvider;
142     private final @NonNull DOMDataBroker dataBroker;
143     private final @Nullable DOMRpcService rpcService;
144     private final @Nullable DOMActionService actionService;
145
146     @SuppressWarnings("unused")
147     private volatile RestconfStrategy localStrategy;
148
149     @Inject
150     @Activate
151     public MdsalRestconfServer(@Reference final DatabindProvider databindProvider,
152             @Reference final DOMDataBroker dataBroker, @Reference final DOMRpcService rpcService,
153             @Reference final DOMActionService actionService,
154             @Reference final DOMMountPointService mountPointService,
155             @Reference final List<RpcImplementation> localRpcs) {
156         this.databindProvider = requireNonNull(databindProvider);
157         this.dataBroker = requireNonNull(dataBroker);
158         this.rpcService = requireNonNull(rpcService);
159         this.actionService = requireNonNull(actionService);
160         this.mountPointService = requireNonNull(mountPointService);
161         this.localRpcs = Maps.uniqueIndex(localRpcs, RpcImplementation::qname);
162     }
163
164     public MdsalRestconfServer(final DatabindProvider databindProvider, final DOMDataBroker dataBroker,
165             final DOMRpcService rpcService, final DOMActionService actionService,
166             final DOMMountPointService mountPointService, final RpcImplementation... localRpcs) {
167         this(databindProvider, dataBroker, rpcService, actionService, mountPointService, List.of(localRpcs));
168     }
169
170     @Override
171     public RestconfFuture<Empty> dataDELETE(final ApiPath identifier) {
172         final var reqPath = bindRequestPath(identifier);
173         final var strategy = getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
174         return strategy.delete(reqPath.getInstanceIdentifier());
175     }
176
177     @Override
178     public RestconfFuture<NormalizedNodePayload> dataGET(final ReadDataParams readParams) {
179         return readData(bindRequestRoot(), readParams);
180     }
181
182     @Override
183     public RestconfFuture<NormalizedNodePayload> dataGET(final ApiPath identifier, final ReadDataParams readParams) {
184         return readData(bindRequestPath(identifier), readParams);
185     }
186
187     private @NonNull RestconfFuture<NormalizedNodePayload> readData(final InstanceIdentifierContext reqPath,
188             final ReadDataParams readParams) {
189         final var queryParams = QueryParams.newQueryParameters(readParams, reqPath);
190         final var fieldPaths = queryParams.fieldPaths();
191         final var strategy = getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
192         final NormalizedNode node;
193         if (fieldPaths != null && !fieldPaths.isEmpty()) {
194             node = strategy.readData(readParams.content(), reqPath.getInstanceIdentifier(), readParams.withDefaults(),
195                 fieldPaths);
196         } else {
197             node = strategy.readData(readParams.content(), reqPath.getInstanceIdentifier(), readParams.withDefaults());
198         }
199         if (node == null) {
200             return RestconfFuture.failed(new RestconfDocumentedException(
201                 "Request could not be completed because the relevant data model content does not exist",
202                 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING));
203         }
204
205         return RestconfFuture.of(new NormalizedNodePayload(reqPath.inference(), node, queryParams));
206     }
207
208     @Override
209     public RestconfFuture<Empty> dataPATCH(final ResourceBody body) {
210         return dataPATCH(bindRequestRoot(), body);
211     }
212
213     @Override
214     public RestconfFuture<Empty> dataPATCH(final ApiPath identifier, final ResourceBody body) {
215         return dataPATCH(bindRequestPath(identifier), body);
216     }
217
218     private @NonNull RestconfFuture<Empty> dataPATCH(final InstanceIdentifierContext reqPath, final ResourceBody body) {
219         final var req = bindResourceRequest(reqPath, body);
220         return req.strategy().merge(req.path(), req.data());
221     }
222
223     @Override
224     public RestconfFuture<PatchStatusContext> dataPATCH(final PatchBody body) {
225         return dataPATCH(bindRequestRoot(), body);
226     }
227
228     @Override
229     public RestconfFuture<PatchStatusContext> dataPATCH(final ApiPath identifier, final PatchBody body) {
230         return dataPATCH(bindRequestPath(identifier), body);
231     }
232
233     private @NonNull RestconfFuture<PatchStatusContext> dataPATCH(final InstanceIdentifierContext reqPath,
234             final PatchBody body) {
235         final var modelContext = reqPath.getSchemaContext();
236         final PatchContext patch;
237         try {
238             patch = body.toPatchContext(modelContext, reqPath.getInstanceIdentifier());
239         } catch (IOException e) {
240             LOG.debug("Error parsing YANG Patch input", e);
241             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
242                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
243         }
244         return getRestconfStrategy(modelContext, reqPath.getMountPoint()).patchData(patch);
245     }
246
247     @Override
248     public RestconfFuture<CreateResource> dataPOST(final ChildBody body, final Map<String, String> queryParameters) {
249         return dataCreatePOST(bindRequestRoot(), body, queryParameters);
250     }
251
252     @Override
253     public RestconfFuture<? extends DataPostResult> dataPOST(final ApiPath identifier, final DataPostBody body,
254             final Map<String, String> queryParameters) {
255         final var reqPath = bindRequestPath(identifier);
256         if (reqPath.getSchemaNode() instanceof ActionDefinition) {
257             try (var inputBody = body.toOperationInput()) {
258                 return dataInvokePOST(reqPath, inputBody);
259             }
260         }
261
262         try (var childBody = body.toResource()) {
263             return dataCreatePOST(reqPath, childBody, queryParameters);
264         }
265     }
266
267     private @NonNull RestconfFuture<CreateResource> dataCreatePOST(final InstanceIdentifierContext reqPath,
268             final ChildBody body, final Map<String, String> queryParameters) {
269         final var inference = reqPath.inference();
270         final var modelContext = inference.getEffectiveModelContext();
271
272         final Insert insert;
273         try {
274             insert = Insert.ofQueryParameters(modelContext, queryParameters);
275         } catch (IllegalArgumentException e) {
276             return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
277                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
278         }
279
280         final var parentPath = reqPath.getInstanceIdentifier();
281         final var payload = body.toPayload(parentPath, inference);
282         return getRestconfStrategy(modelContext, reqPath.getMountPoint())
283             .postData(concat(parentPath, payload.prefix()), payload.body(), insert);
284     }
285
286     private static YangInstanceIdentifier concat(final YangInstanceIdentifier parent, final List<PathArgument> args) {
287         var ret = parent;
288         for (var arg : args) {
289             ret = ret.node(arg);
290         }
291         return ret;
292     }
293
294     private RestconfFuture<InvokeOperation> dataInvokePOST(final InstanceIdentifierContext reqPath,
295             final OperationInputBody body) {
296         final var yangIIdContext = reqPath.getInstanceIdentifier();
297         final var inference = reqPath.inference();
298         final ContainerNode input;
299         try {
300             input = body.toContainerNode(inference);
301         } catch (IOException e) {
302             LOG.debug("Error reading input", e);
303             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
304                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
305         }
306
307         final var mountPoint = reqPath.getMountPoint();
308         final var schemaPath = inference.toSchemaInferenceStack().toSchemaNodeIdentifier();
309         final var future = mountPoint != null ? dataInvokePOST(input, schemaPath, yangIIdContext, mountPoint)
310             : dataInvokePOST(input, schemaPath, yangIIdContext, actionService);
311
312         return future.transform(result -> result.getOutput()
313             .flatMap(output -> output.isEmpty() ? Optional.empty()
314                 : Optional.of(new InvokeOperation(new NormalizedNodePayload(reqPath.inference(), output))))
315             .orElse(InvokeOperation.EMPTY));
316     }
317
318     /**
319      * Invoke Action via ActionServiceHandler.
320      *
321      * @param data input data
322      * @param yangIId invocation context
323      * @param schemaPath schema path of data
324      * @param actionService action service to invoke action
325      * @return {@link DOMActionResult}
326      */
327     private static RestconfFuture<DOMActionResult> dataInvokePOST(final ContainerNode data, final Absolute schemaPath,
328             final YangInstanceIdentifier yangIId, final DOMActionService actionService) {
329         final var ret = new SettableRestconfFuture<DOMActionResult>();
330
331         Futures.addCallback(actionService.invokeAction(schemaPath,
332             new DOMDataTreeIdentifier(LogicalDatastoreType.OPERATIONAL, yangIId.getParent()), data),
333             new FutureCallback<DOMActionResult>() {
334                 @Override
335                 public void onSuccess(final DOMActionResult result) {
336                     final var errors = result.getErrors();
337                     LOG.debug("InvokeAction Error Message {}", errors);
338                     if (errors.isEmpty()) {
339                         ret.set(result);
340                     } else {
341                         ret.setFailure(new RestconfDocumentedException("InvokeAction Error Message ", null, errors));
342                     }
343                 }
344
345                 @Override
346                 public void onFailure(final Throwable cause) {
347                     if (cause instanceof DOMActionException) {
348                         ret.set(new SimpleDOMActionResult(List.of(RpcResultBuilder.newError(
349                             ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage()))));
350                     } else if (cause instanceof RestconfDocumentedException e) {
351                         ret.setFailure(e);
352                     } else if (cause instanceof CancellationException) {
353                         ret.setFailure(new RestconfDocumentedException("Action cancelled while executing",
354                             ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, cause));
355                     } else {
356                         ret.setFailure(new RestconfDocumentedException("Invocation failed", cause));
357                     }
358                 }
359             }, MoreExecutors.directExecutor());
360
361         return ret;
362     }
363
364     /**
365      * Invoking Action via mount point.
366      *
367      * @param mountPoint mount point
368      * @param data input data
369      * @param schemaPath schema path of data
370      * @return {@link DOMActionResult}
371      */
372     private static RestconfFuture<DOMActionResult> dataInvokePOST(final ContainerNode data, final Absolute schemaPath,
373             final YangInstanceIdentifier yangIId, final DOMMountPoint mountPoint) {
374         final var actionService = mountPoint.getService(DOMActionService.class);
375         return actionService.isPresent() ? dataInvokePOST(data, schemaPath, yangIId, actionService.orElseThrow())
376             : RestconfFuture.failed(new RestconfDocumentedException("DOMActionService is missing."));
377     }
378
379     @Override
380     public RestconfFuture<DataPutResult> dataPUT(final ResourceBody body, final Map<String, String> query) {
381         return dataPUT(bindRequestRoot(), body, query);
382     }
383
384     @Override
385     public RestconfFuture<DataPutResult> dataPUT(final ApiPath identifier, final ResourceBody body,
386              final Map<String, String> queryParameters) {
387         return dataPUT(bindRequestPath(identifier), body, queryParameters);
388     }
389
390     private @NonNull RestconfFuture<DataPutResult> dataPUT(final InstanceIdentifierContext reqPath,
391             final ResourceBody body, final Map<String, String> queryParameters) {
392         final Insert insert;
393         try {
394             insert = Insert.ofQueryParameters(reqPath.getSchemaContext(), queryParameters);
395         } catch (IllegalArgumentException e) {
396             return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
397                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
398         }
399         final var req = bindResourceRequest(reqPath, body);
400         return req.strategy().putData(req.path(), req.data(), insert);
401     }
402
403     @Override
404     public RestconfFuture<ModulesGetResult> modulesYangGET(final String fileName, final String revision) {
405         return modulesGET(fileName, revision, YangTextSchemaSource.class);
406     }
407
408     @Override
409     public RestconfFuture<ModulesGetResult> modulesYangGET(final ApiPath mountPath, final String fileName,
410             final String revision) {
411         return modulesGET(mountPath, fileName, revision, YangTextSchemaSource.class);
412     }
413
414     @Override
415     public RestconfFuture<ModulesGetResult> modulesYinGET(final String fileName, final String revision) {
416         return modulesGET(fileName, revision, YinTextSchemaSource.class);
417     }
418
419     @Override
420     public RestconfFuture<ModulesGetResult> modulesYinGET(final ApiPath mountPath, final String fileName,
421             final String revision) {
422         return modulesGET(mountPath, fileName, revision, YinTextSchemaSource.class);
423     }
424
425     private @NonNull RestconfFuture<ModulesGetResult> modulesGET(final String fileName, final String revision,
426             final Class<? extends SchemaSourceRepresentation> representation) {
427         return modulesGET(databindProvider.currentContext(), fileName, revision, representation);
428     }
429
430     private @NonNull RestconfFuture<ModulesGetResult> modulesGET(final ApiPath mountPath, final String fileName,
431             final String revision, final Class<? extends SchemaSourceRepresentation> representation) {
432         final var mountOffset = mountPath.indexOf("yang-ext", "mount");
433         if (mountOffset != mountPath.steps().size() - 1) {
434             return RestconfFuture.failed(new RestconfDocumentedException("Mount path has to end with yang-ext:mount"));
435         }
436
437         final var currentContext = databindProvider.currentContext();
438         final InstanceIdentifierContext point;
439         try {
440             point = InstanceIdentifierContext.ofApiPath(mountPath, currentContext.modelContext(), mountPointService);
441         } catch (RestconfDocumentedException e) {
442             return RestconfFuture.failed(e);
443         }
444
445         final var mountPoint = point.getMountPoint();
446         final var modelContext = point.getSchemaContext();
447         final var domProvider = mountPoint.getService(DOMSchemaService.class)
448             .flatMap(svc -> Optional.ofNullable(svc.getExtensions().getInstance(DOMYangTextSourceProvider.class)));
449
450         return modulesGET(DatabindContext.ofModel(modelContext,
451             domProvider.isEmpty() ? null : new DOMSourceResolver(domProvider.orElseThrow())), fileName, revision,
452             representation);
453     }
454
455     private static @NonNull RestconfFuture<ModulesGetResult> modulesGET(final DatabindContext databind,
456             final String moduleName, final String revisionStr,
457             final Class<? extends SchemaSourceRepresentation> representation) {
458         if (moduleName == null) {
459             return RestconfFuture.failed(new RestconfDocumentedException("Module name must be supplied",
460                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
461         }
462         if (moduleName.isEmpty() || !YangNames.IDENTIFIER_START.matches(moduleName.charAt(0))) {
463             return RestconfFuture.failed(new RestconfDocumentedException(
464                 "Identifier must start with character from set 'a-zA-Z_", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
465         }
466         if (moduleName.toUpperCase(Locale.ROOT).startsWith("XML")) {
467             return RestconfFuture.failed(new RestconfDocumentedException(
468                 "Identifier must NOT start with XML ignore case", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
469         }
470         if (YangNames.NOT_IDENTIFIER_PART.matchesAnyOf(moduleName.substring(1))) {
471             return RestconfFuture.failed(new RestconfDocumentedException(
472                 "Supplied name has not expected identifier format", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
473         }
474
475         // YANG Revision-compliant string is required
476         final Revision revision;
477         try {
478             revision = Revision.ofNullable(revisionStr).orElse(null);
479         } catch (final DateTimeParseException e) {
480             return RestconfFuture.failed(new RestconfDocumentedException(
481                 "Supplied revision is not in expected date format YYYY-mm-dd",
482                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
483         }
484
485         return databind.resolveSource(new SourceIdentifier(moduleName, revision), representation)
486             .transform(ModulesGetResult::new);
487     }
488
489     @Override
490     public RestconfFuture<OperationsGetResult> operationsGET() {
491         return operationsGET(databindProvider.currentContext().modelContext());
492     }
493
494     @Override
495     public RestconfFuture<OperationsGetResult> operationsGET(final ApiPath operation) {
496         // get current module RPCs/actions by RPC/action name
497         final var inference = bindRequestPath(operation).inference();
498         if (inference.isEmpty()) {
499             return operationsGET(inference.getEffectiveModelContext());
500         }
501
502         final var stmt = inference.toSchemaInferenceStack().currentStatement();
503         if (stmt instanceof RpcEffectiveStatement rpc) {
504             return RestconfFuture.of(
505                 new OperationsGetResult.Leaf(inference.getEffectiveModelContext(), rpc.argument()));
506         }
507         return RestconfFuture.failed(new RestconfDocumentedException("RPC not found",
508             ErrorType.PROTOCOL, ErrorTag.DATA_MISSING));
509     }
510
511     private static @NonNull RestconfFuture<OperationsGetResult> operationsGET(
512             final EffectiveModelContext modelContext) {
513         final var modules = modelContext.getModuleStatements();
514         if (modules.isEmpty()) {
515             // No modules, or defensive return empty content
516             return RestconfFuture.of(new OperationsGetResult.Container(modelContext, ImmutableSetMultimap.of()));
517         }
518
519         // RPC QNames by their XMLNamespace/Revision. This should be a Table, but Revision can be null, which wrecks us.
520         final var table = new HashMap<XMLNamespace, Map<Revision, ImmutableSet<QName>>>();
521         for (var entry : modules.entrySet()) {
522             final var module = entry.getValue();
523             final var rpcNames = module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
524                 .map(RpcEffectiveStatement::argument)
525                 .collect(ImmutableSet.toImmutableSet());
526             if (!rpcNames.isEmpty()) {
527                 final var namespace = entry.getKey();
528                 table.computeIfAbsent(namespace.getNamespace(), ignored -> new HashMap<>())
529                     .put(namespace.getRevision().orElse(null), rpcNames);
530             }
531         }
532
533         // Now pick the latest revision for each namespace
534         final var rpcs = ImmutableSetMultimap.<QNameModule, QName>builder();
535         for (var entry : table.entrySet()) {
536             entry.getValue().entrySet().stream()
537                 .sorted(Comparator.comparing(Entry::getKey, (first, second) -> Revision.compare(second, first)))
538                 .findFirst()
539                 .ifPresent(row -> rpcs.putAll(QNameModule.create(entry.getKey(), row.getKey()), row.getValue()));
540         }
541         return RestconfFuture.of(new OperationsGetResult.Container(modelContext, rpcs.build()));
542     }
543
544     @Override
545     public RestconfFuture<OperationOutput> operationsPOST(final URI restconfURI, final ApiPath apiPath,
546             final OperationInputBody body) {
547         final var currentContext = databindProvider.currentContext();
548         final var reqPath = bindRequestPath(currentContext, apiPath);
549         final var inference = reqPath.inference();
550         final ContainerNode input;
551         try {
552             input = body.toContainerNode(inference);
553         } catch (IOException e) {
554             LOG.debug("Error reading input", e);
555             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
556                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
557         }
558
559         return getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint())
560             .invokeRpc(restconfURI, reqPath.getSchemaNode().getQName(),
561                 new OperationInput(currentContext, inference, input));
562     }
563
564     @Override
565     public RestconfFuture<NormalizedNodePayload> yangLibraryVersionGET() {
566         final var stack = SchemaInferenceStack.of(databindProvider.currentContext().modelContext());
567         try {
568             stack.enterYangData(YangApi.NAME);
569             stack.enterDataTree(Restconf.QNAME);
570             stack.enterDataTree(YANG_LIBRARY_VERSION);
571         } catch (IllegalArgumentException e) {
572             return RestconfFuture.failed(new RestconfDocumentedException("RESTCONF is not available"));
573         }
574         return RestconfFuture.of(new NormalizedNodePayload(stack.toInference(),
575             ImmutableNodes.leafNode(YANG_LIBRARY_VERSION, stack.getEffectiveModelContext()
576                 .findModuleStatements("ietf-yang-library").iterator().next().localQNameModule().getRevision()
577                 .map(Revision::toString).orElse(""))));
578     }
579
580     private @NonNull InstanceIdentifierContext bindRequestPath(final @NonNull ApiPath identifier) {
581         return bindRequestPath(databindProvider.currentContext(), identifier);
582     }
583
584     private @NonNull InstanceIdentifierContext bindRequestPath(final @NonNull DatabindContext databind,
585             final @NonNull ApiPath identifier) {
586         // FIXME: DatabindContext looks like it should be internal
587         return InstanceIdentifierContext.ofApiPath(identifier, databind.modelContext(), mountPointService);
588     }
589
590     private @NonNull InstanceIdentifierContext bindRequestRoot() {
591         return InstanceIdentifierContext.ofLocalRoot(databindProvider.currentContext().modelContext());
592     }
593
594     private @NonNull ResourceRequest bindResourceRequest(final InstanceIdentifierContext reqPath,
595             final ResourceBody body) {
596         final var inference = reqPath.inference();
597         final var path = reqPath.getInstanceIdentifier();
598         final var data = body.toNormalizedNode(path, inference, reqPath.getSchemaNode());
599
600         return new ResourceRequest(
601             getRestconfStrategy(inference.getEffectiveModelContext(), reqPath.getMountPoint()), path, data);
602     }
603
604     @VisibleForTesting
605     @NonNull RestconfStrategy getRestconfStrategy(final EffectiveModelContext modelContext,
606             final @Nullable DOMMountPoint mountPoint) {
607         if (mountPoint == null) {
608             return localStrategy(modelContext);
609         }
610
611         final var ret = RestconfStrategy.forMountPoint(modelContext, mountPoint);
612         if (ret == null) {
613             final var mountId = mountPoint.getIdentifier();
614             LOG.warn("Mount point {} does not expose a suitable access interface", mountId);
615             throw new RestconfDocumentedException("Could not find a supported access interface in mount point",
616                 ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, mountId);
617         }
618         return ret;
619     }
620
621     private @NonNull RestconfStrategy localStrategy(final EffectiveModelContext modelContext) {
622         final var local = (RestconfStrategy) LOCAL_STRATEGY.getAcquire(this);
623         if (local != null && modelContext.equals(local.modelContext())) {
624             return local;
625         }
626
627         final var created = new MdsalRestconfStrategy(modelContext, dataBroker, rpcService, localRpcs);
628         LOCAL_STRATEGY.setRelease(this, created);
629         return created;
630     }
631 }