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