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