Use DatabindContext in InstanceIdentifierContext
[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.DatabindContext;
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.legacy.InstanceIdentifierContext;
67 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
68 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
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.NetconfFieldsTranslator;
72 import org.opendaylight.restconf.nb.rfc8040.utils.parser.WriterFieldsTranslator;
73 import org.opendaylight.restconf.server.api.DataPostResult;
74 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
75 import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
76 import org.opendaylight.restconf.server.api.DataPutResult;
77 import org.opendaylight.restconf.server.api.ModulesGetResult;
78 import org.opendaylight.restconf.server.api.OperationsGetResult;
79 import org.opendaylight.restconf.server.api.RestconfServer;
80 import org.opendaylight.restconf.server.spi.DatabindProvider;
81 import org.opendaylight.restconf.server.spi.OperationInput;
82 import org.opendaylight.restconf.server.spi.OperationOutput;
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 currentContext() {
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.getSchemaContext();
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 var inference = reqPath.inference();
335
336         final Insert insert;
337         try {
338             insert = Insert.ofQueryParameters(inference.getEffectiveModelContext(), queryParameters);
339         } catch (IllegalArgumentException e) {
340             return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
341                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
342         }
343
344         final var parentPath = reqPath.getInstanceIdentifier();
345         final var payload = body.toPayload(parentPath, inference);
346         return getRestconfStrategy(reqPath.databind(), reqPath.getMountPoint())
347             .postData(concat(parentPath, payload.prefix()), payload.body(), insert);
348     }
349
350     private static YangInstanceIdentifier concat(final YangInstanceIdentifier parent, final List<PathArgument> args) {
351         var ret = parent;
352         for (var arg : args) {
353             ret = ret.node(arg);
354         }
355         return ret;
356     }
357
358     private RestconfFuture<InvokeOperation> dataInvokePOST(final InstanceIdentifierContext reqPath,
359             final OperationInputBody body) {
360         final var yangIIdContext = reqPath.getInstanceIdentifier();
361         final var inference = reqPath.inference();
362         final ContainerNode input;
363         try {
364             input = body.toContainerNode(inference);
365         } catch (IOException e) {
366             LOG.debug("Error reading input", e);
367             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
368                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
369         }
370
371         final var mountPoint = reqPath.getMountPoint();
372         final var schemaPath = inference.toSchemaInferenceStack().toSchemaNodeIdentifier();
373         final var future = mountPoint != null ? dataInvokePOST(input, schemaPath, yangIIdContext, mountPoint)
374             : dataInvokePOST(input, schemaPath, yangIIdContext, actionService);
375
376         return future.transform(result -> result.getOutput()
377             .flatMap(output -> output.isEmpty() ? Optional.empty()
378                 : Optional.of(new InvokeOperation(new NormalizedNodePayload(reqPath.inference(), output))))
379             .orElse(InvokeOperation.EMPTY));
380     }
381
382     /**
383      * Invoke Action via ActionServiceHandler.
384      *
385      * @param data input data
386      * @param yangIId invocation context
387      * @param schemaPath schema path of data
388      * @param actionService action service to invoke action
389      * @return {@link DOMActionResult}
390      */
391     private static RestconfFuture<DOMActionResult> dataInvokePOST(final ContainerNode data, final Absolute schemaPath,
392             final YangInstanceIdentifier yangIId, final DOMActionService actionService) {
393         final var ret = new SettableRestconfFuture<DOMActionResult>();
394
395         Futures.addCallback(actionService.invokeAction(schemaPath,
396             new DOMDataTreeIdentifier(LogicalDatastoreType.OPERATIONAL, yangIId.getParent()), data),
397             new FutureCallback<DOMActionResult>() {
398                 @Override
399                 public void onSuccess(final DOMActionResult result) {
400                     final var errors = result.getErrors();
401                     LOG.debug("InvokeAction Error Message {}", errors);
402                     if (errors.isEmpty()) {
403                         ret.set(result);
404                     } else {
405                         ret.setFailure(new RestconfDocumentedException("InvokeAction Error Message ", null, errors));
406                     }
407                 }
408
409                 @Override
410                 public void onFailure(final Throwable cause) {
411                     if (cause instanceof DOMActionException) {
412                         ret.set(new SimpleDOMActionResult(List.of(RpcResultBuilder.newError(
413                             ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage()))));
414                     } else if (cause instanceof RestconfDocumentedException e) {
415                         ret.setFailure(e);
416                     } else if (cause instanceof CancellationException) {
417                         ret.setFailure(new RestconfDocumentedException("Action cancelled while executing",
418                             ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, cause));
419                     } else {
420                         ret.setFailure(new RestconfDocumentedException("Invocation failed", cause));
421                     }
422                 }
423             }, MoreExecutors.directExecutor());
424
425         return ret;
426     }
427
428     /**
429      * Invoking Action via mount point.
430      *
431      * @param mountPoint mount point
432      * @param data input data
433      * @param schemaPath schema path of data
434      * @return {@link DOMActionResult}
435      */
436     private static RestconfFuture<DOMActionResult> dataInvokePOST(final ContainerNode data, final Absolute schemaPath,
437             final YangInstanceIdentifier yangIId, final DOMMountPoint mountPoint) {
438         final var actionService = mountPoint.getService(DOMActionService.class);
439         return actionService.isPresent() ? dataInvokePOST(data, schemaPath, yangIId, actionService.orElseThrow())
440             : RestconfFuture.failed(new RestconfDocumentedException("DOMActionService is missing."));
441     }
442
443     @Override
444     public RestconfFuture<DataPutResult> dataPUT(final ResourceBody body, final Map<String, String> query) {
445         return dataPUT(bindRequestRoot(), body, query);
446     }
447
448     @Override
449     public RestconfFuture<DataPutResult> dataPUT(final ApiPath identifier, final ResourceBody body,
450              final Map<String, String> queryParameters) {
451         return dataPUT(bindRequestPath(identifier), body, queryParameters);
452     }
453
454     private @NonNull RestconfFuture<DataPutResult> dataPUT(final InstanceIdentifierContext reqPath,
455             final ResourceBody body, final Map<String, String> queryParameters) {
456         final Insert insert;
457         try {
458             insert = Insert.ofQueryParameters(reqPath.getSchemaContext(), queryParameters);
459         } catch (IllegalArgumentException e) {
460             return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
461                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
462         }
463         final var req = bindResourceRequest(reqPath, body);
464         return req.strategy().putData(req.path(), req.data(), insert);
465     }
466
467     @Override
468     public RestconfFuture<ModulesGetResult> modulesYangGET(final String fileName, final String revision) {
469         return modulesGET(fileName, revision, YangTextSchemaSource.class);
470     }
471
472     @Override
473     public RestconfFuture<ModulesGetResult> modulesYangGET(final ApiPath mountPath, final String fileName,
474             final String revision) {
475         return modulesGET(mountPath, fileName, revision, YangTextSchemaSource.class);
476     }
477
478     @Override
479     public RestconfFuture<ModulesGetResult> modulesYinGET(final String fileName, final String revision) {
480         return modulesGET(fileName, revision, YinTextSchemaSource.class);
481     }
482
483     @Override
484     public RestconfFuture<ModulesGetResult> modulesYinGET(final ApiPath mountPath, final String fileName,
485             final String revision) {
486         return modulesGET(mountPath, fileName, revision, YinTextSchemaSource.class);
487     }
488
489     private @NonNull RestconfFuture<ModulesGetResult> modulesGET(final String fileName, final String revision,
490             final Class<? extends SchemaSourceRepresentation> representation) {
491         return modulesGET(localStrategy(), fileName, revision, representation);
492     }
493
494     private @NonNull RestconfFuture<ModulesGetResult> modulesGET(final ApiPath mountPath, final String fileName,
495             final String revision, final Class<? extends SchemaSourceRepresentation> representation) {
496         final var mountOffset = mountPath.indexOf("yang-ext", "mount");
497         if (mountOffset != mountPath.steps().size() - 1) {
498             return RestconfFuture.failed(new RestconfDocumentedException("Mount path has to end with yang-ext:mount"));
499         }
500
501         final InstanceIdentifierContext point;
502         try {
503             point = InstanceIdentifierContext.ofApiPath(mountPath, localStrategy().databind(), mountPointService);
504         } catch (RestconfDocumentedException e) {
505             return RestconfFuture.failed(e);
506         }
507
508         final RestconfStrategy strategy;
509         try {
510             strategy = forMountPoint(point.databind(), point.getMountPoint());
511         } catch (RestconfDocumentedException e) {
512             return RestconfFuture.failed(e);
513         }
514         return modulesGET(strategy, fileName, revision, representation);
515     }
516
517     private static @NonNull RestconfFuture<ModulesGetResult> modulesGET(final RestconfStrategy strategy,
518             final String moduleName, final String revisionStr,
519             final Class<? extends SchemaSourceRepresentation> representation) {
520         if (moduleName == null) {
521             return RestconfFuture.failed(new RestconfDocumentedException("Module name must be supplied",
522                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
523         }
524         if (moduleName.isEmpty() || !YangNames.IDENTIFIER_START.matches(moduleName.charAt(0))) {
525             return RestconfFuture.failed(new RestconfDocumentedException(
526                 "Identifier must start with character from set 'a-zA-Z_", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
527         }
528         if (moduleName.toUpperCase(Locale.ROOT).startsWith("XML")) {
529             return RestconfFuture.failed(new RestconfDocumentedException(
530                 "Identifier must NOT start with XML ignore case", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
531         }
532         if (YangNames.NOT_IDENTIFIER_PART.matchesAnyOf(moduleName.substring(1))) {
533             return RestconfFuture.failed(new RestconfDocumentedException(
534                 "Supplied name has not expected identifier format", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
535         }
536
537         // YANG Revision-compliant string is required
538         final Revision revision;
539         try {
540             revision = Revision.ofNullable(revisionStr).orElse(null);
541         } catch (final DateTimeParseException e) {
542             return RestconfFuture.failed(new RestconfDocumentedException(
543                 "Supplied revision is not in expected date format YYYY-mm-dd",
544                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
545         }
546
547         return strategy.resolveSource(new SourceIdentifier(moduleName, revision), representation)
548             .transform(ModulesGetResult::new);
549     }
550
551     @Override
552     public RestconfFuture<OperationsGetResult> operationsGET() {
553         return operationsGET(localStrategy().modelContext());
554     }
555
556     @Override
557     public RestconfFuture<OperationsGetResult> operationsGET(final ApiPath operation) {
558         // get current module RPCs/actions by RPC/action name
559         final var inference = bindRequestPath(operation).inference();
560         if (inference.isEmpty()) {
561             return operationsGET(inference.getEffectiveModelContext());
562         }
563
564         final var stmt = inference.toSchemaInferenceStack().currentStatement();
565         if (stmt instanceof RpcEffectiveStatement rpc) {
566             return RestconfFuture.of(
567                 new OperationsGetResult.Leaf(inference.getEffectiveModelContext(), rpc.argument()));
568         }
569         return RestconfFuture.failed(new RestconfDocumentedException("RPC not found",
570             ErrorType.PROTOCOL, ErrorTag.DATA_MISSING));
571     }
572
573     private static @NonNull RestconfFuture<OperationsGetResult> operationsGET(
574             final EffectiveModelContext modelContext) {
575         final var modules = modelContext.getModuleStatements();
576         if (modules.isEmpty()) {
577             // No modules, or defensive return empty content
578             return RestconfFuture.of(new OperationsGetResult.Container(modelContext, ImmutableSetMultimap.of()));
579         }
580
581         // RPC QNames by their XMLNamespace/Revision. This should be a Table, but Revision can be null, which wrecks us.
582         final var table = new HashMap<XMLNamespace, Map<Revision, ImmutableSet<QName>>>();
583         for (var entry : modules.entrySet()) {
584             final var module = entry.getValue();
585             final var rpcNames = module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
586                 .map(RpcEffectiveStatement::argument)
587                 .collect(ImmutableSet.toImmutableSet());
588             if (!rpcNames.isEmpty()) {
589                 final var namespace = entry.getKey();
590                 table.computeIfAbsent(namespace.getNamespace(), ignored -> new HashMap<>())
591                     .put(namespace.getRevision().orElse(null), rpcNames);
592             }
593         }
594
595         // Now pick the latest revision for each namespace
596         final var rpcs = ImmutableSetMultimap.<QNameModule, QName>builder();
597         for (var entry : table.entrySet()) {
598             entry.getValue().entrySet().stream()
599                 .sorted(Comparator.comparing(Entry::getKey, (first, second) -> Revision.compare(second, first)))
600                 .findFirst()
601                 .ifPresent(row -> rpcs.putAll(QNameModule.create(entry.getKey(), row.getKey()), row.getValue()));
602         }
603         return RestconfFuture.of(new OperationsGetResult.Container(modelContext, rpcs.build()));
604     }
605
606     @Override
607     public RestconfFuture<OperationOutput> operationsPOST(final URI restconfURI, final ApiPath apiPath,
608             final OperationInputBody body) {
609         final var reqPath = bindRequestPath(localStrategy(), apiPath);
610         final var inference = reqPath.inference();
611         final ContainerNode input;
612         try {
613             input = body.toContainerNode(inference);
614         } catch (IOException e) {
615             LOG.debug("Error reading input", e);
616             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
617                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
618         }
619
620         final var strategy = getRestconfStrategy(reqPath.databind(), reqPath.getMountPoint());
621         return strategy.invokeRpc(restconfURI, reqPath.getSchemaNode().getQName(),
622                 new OperationInput(strategy.databind(), inference, input));
623     }
624
625     @Override
626     public RestconfFuture<NormalizedNodePayload> yangLibraryVersionGET() {
627         final var stack = SchemaInferenceStack.of(localStrategy().modelContext());
628         try {
629             stack.enterYangData(YangApi.NAME);
630             stack.enterDataTree(Restconf.QNAME);
631             stack.enterDataTree(YANG_LIBRARY_VERSION);
632         } catch (IllegalArgumentException e) {
633             return RestconfFuture.failed(new RestconfDocumentedException("RESTCONF is not available"));
634         }
635         return RestconfFuture.of(new NormalizedNodePayload(stack.toInference(),
636             ImmutableNodes.leafNode(YANG_LIBRARY_VERSION, stack.getEffectiveModelContext()
637                 .findModuleStatements("ietf-yang-library").iterator().next().localQNameModule().getRevision()
638                 .map(Revision::toString).orElse(""))));
639     }
640
641     private @NonNull InstanceIdentifierContext bindRequestPath(final @NonNull ApiPath identifier) {
642         return bindRequestPath(localStrategy(), identifier);
643     }
644
645     private @NonNull InstanceIdentifierContext bindRequestPath(final @NonNull MdsalRestconfStrategy strategy,
646             final @NonNull ApiPath identifier) {
647         // FIXME: DatabindContext looks like it should be internal
648         return InstanceIdentifierContext.ofApiPath(identifier, strategy.databind(), mountPointService);
649     }
650
651     private @NonNull InstanceIdentifierContext bindRequestRoot() {
652         return InstanceIdentifierContext.ofLocalRoot(localStrategy().databind());
653     }
654
655     private @NonNull ResourceRequest bindResourceRequest(final InstanceIdentifierContext reqPath,
656             final ResourceBody body) {
657         final var path = reqPath.getInstanceIdentifier();
658         final var data = body.toNormalizedNode(path, reqPath.inference(), reqPath.getSchemaNode());
659
660         return new ResourceRequest(getRestconfStrategy(reqPath.databind(), reqPath.getMountPoint()), path, data);
661     }
662
663     @VisibleForTesting
664     @NonNull RestconfStrategy getRestconfStrategy(final DatabindContext databind,
665             final @Nullable DOMMountPoint mountPoint) {
666         if (mountPoint == null) {
667             return localStrategy(databind);
668         }
669         return forMountPoint(databind, mountPoint);
670     }
671
672     private static @NonNull RestconfStrategy forMountPoint(final DatabindContext databind,
673             final DOMMountPoint mountPoint) {
674         final var ret = RestconfStrategy.forMountPoint(databind, mountPoint);
675         if (ret == null) {
676             final var mountId = mountPoint.getIdentifier();
677             LOG.warn("Mount point {} does not expose a suitable access interface", mountId);
678             throw new RestconfDocumentedException("Could not find a supported access interface in mount point",
679                 ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, mountId);
680         }
681         return ret;
682     }
683 }