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