+ final var ret = new SettableRestconfFuture<OperationsPostResult>();
+ Futures.addCallback(rpcService.invokeRpc(type, data), new FutureCallback<DOMRpcResult>() {
+ @Override
+ public void onSuccess(final DOMRpcResult response) {
+ final var errors = response.errors();
+ if (errors.isEmpty()) {
+ ret.set(new OperationsPostResult(databind, postPath.operation(), response.value()));
+ } else {
+ LOG.debug("RPC invocation reported {}", response.errors());
+ ret.setFailure(new RestconfDocumentedException("RPC implementation reported errors", null,
+ response.errors()));
+ }
+ }
+
+ @Override
+ public void onFailure(final Throwable cause) {
+ LOG.debug("RPC invocation failed, cause");
+ if (cause instanceof RestconfDocumentedException ex) {
+ ret.setFailure(ex);
+ } else {
+ // TODO: YangNetconfErrorAware if we ever get into a broader invocation scope
+ ret.setFailure(new RestconfDocumentedException(cause,
+ new RestconfError(ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage())));
+ }
+ }
+ }, MoreExecutors.directExecutor());
+ return ret;
+ }
+
+ public @NonNull RestconfFuture<CharSource> resolveSource(final SourceIdentifier source,
+ final Class<? extends SourceRepresentation> representation) {
+ final var src = requireNonNull(source);
+ if (YangTextSource.class.isAssignableFrom(representation)) {
+ if (sourceProvider != null) {
+ final var ret = new SettableRestconfFuture<CharSource>();
+ Futures.addCallback(sourceProvider.getYangTexttSource(src), new FutureCallback<>() {
+ @Override
+ public void onSuccess(final YangTextSource result) {
+ ret.set(result);
+ }
+
+ @Override
+ public void onFailure(final Throwable cause) {
+ ret.setFailure(cause instanceof RestconfDocumentedException e ? e
+ : new RestconfDocumentedException(cause.getMessage(), ErrorType.RPC,
+ ErrorTag.OPERATION_FAILED, cause));
+ }
+ }, MoreExecutors.directExecutor());
+ return ret;
+ }
+ return exportSource(modelContext(), src, YangCharSource::new, YangCharSource::new);
+ }
+ if (YinTextSource.class.isAssignableFrom(representation)) {
+ return exportSource(modelContext(), src, YinCharSource.OfModule::new, YinCharSource.OfSubmodule::new);
+ }
+ return RestconfFuture.failed(new RestconfDocumentedException(
+ "Unsupported source representation " + representation.getName()));
+ }
+
+ private static @NonNull RestconfFuture<CharSource> exportSource(final EffectiveModelContext modelContext,
+ final SourceIdentifier source, final Function<ModuleEffectiveStatement, CharSource> moduleCtor,
+ final BiFunction<ModuleEffectiveStatement, SubmoduleEffectiveStatement, CharSource> submoduleCtor) {
+ // If the source identifies a module, things are easy
+ final var name = source.name().getLocalName();
+ final var optRevision = Optional.ofNullable(source.revision());
+ final var optModule = modelContext.findModule(name, optRevision);
+ if (optModule.isPresent()) {
+ return RestconfFuture.of(moduleCtor.apply(optModule.orElseThrow().asEffectiveStatement()));
+ }
+
+ // The source could be a submodule, which we need to hunt down
+ for (var module : modelContext.getModules()) {
+ for (var submodule : module.getSubmodules()) {
+ if (name.equals(submodule.getName()) && optRevision.equals(submodule.getRevision())) {
+ return RestconfFuture.of(submoduleCtor.apply(module.asEffectiveStatement(),
+ submodule.asEffectiveStatement()));
+ }
+ }
+ }
+
+ final var sb = new StringBuilder().append("Source ").append(source.name().getLocalName());
+ optRevision.ifPresent(rev -> sb.append('@').append(rev));
+ sb.append(" not found");
+ return RestconfFuture.failed(new RestconfDocumentedException(sb.toString(),
+ ErrorType.APPLICATION, ErrorTag.DATA_MISSING));
+ }
+
+ public final @NonNull RestconfFuture<? extends DataPostResult> dataPOST(final ApiPath apiPath,
+ final DataPostBody body, final Map<String, String> queryParameters) {
+ if (apiPath.steps().isEmpty()) {
+ return dataCreatePOST(body.toResource(), queryParameters);
+ }
+ final InstanceReference path;
+ try {
+ path = pathNormalizer.normalizeDataOrActionPath(apiPath);
+ } catch (RestconfDocumentedException e) {
+ return RestconfFuture.failed(e);
+ }
+ if (path instanceof DataPath dataPath) {
+ try (var resourceBody = body.toResource()) {
+ return dataCreatePOST(new DataPostPath(databind, dataPath.inference(), dataPath.instance()),
+ resourceBody, queryParameters);
+ }
+ }
+ if (path instanceof OperationPath.Action actionPath) {
+ try (var inputBody = body.toOperationInput()) {
+ return dataInvokePOST(actionPath, inputBody);
+ }
+ }
+ // Note: this should never happen
+ // FIXME: we should be able to eliminate this path with Java 21+ pattern matching
+ return RestconfFuture.failed(new RestconfDocumentedException("Unhandled path " + path));
+ }
+
+ public @NonNull RestconfFuture<CreateResource> dataCreatePOST(final ChildBody body,
+ final Map<String, String> queryParameters) {
+ return dataCreatePOST(new DataPostPath(databind,
+ SchemaInferenceStack.of(databind.modelContext()).toInference(), YangInstanceIdentifier.of()), body,
+ queryParameters);
+ }
+
+ private @NonNull RestconfFuture<CreateResource> dataCreatePOST(final DataPostPath path, final ChildBody body,
+ final Map<String, String> queryParameters) {
+ final Insert insert;
+ try {
+ insert = Insert.ofQueryParameters(path.databind(), queryParameters);
+ } catch (IllegalArgumentException e) {
+ return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
+ }
+
+ final var payload = body.toPayload(path);
+ return postData(concat(path.instance(), payload.prefix()), payload.body(), insert);
+ }
+
+ private static YangInstanceIdentifier concat(final YangInstanceIdentifier parent, final List<PathArgument> args) {
+ var ret = parent;
+ for (var arg : args) {
+ ret = ret.node(arg);
+ }
+ return ret;
+ }
+
+ private @NonNull RestconfFuture<InvokeOperation> dataInvokePOST(final OperationPath.Action path,
+ final OperationInputBody body) {
+ final var inference = path.inference();
+ final ContainerNode input;
+ try {
+ input = body.toContainerNode(new OperationsPostPath(databind, inference));
+ } catch (IOException e) {
+ LOG.debug("Error reading input", e);
+ return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
+ ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
+ }
+
+ if (actionService == null) {
+ return RestconfFuture.failed(new RestconfDocumentedException("DOMActionService is missing."));
+ }
+
+ final var future = dataInvokePOST(actionService, path, input);
+ return future.transform(result -> result.getOutput()
+ .flatMap(output -> output.isEmpty() ? Optional.empty()
+ : Optional.of(new InvokeOperation(new NormalizedNodePayload(inference, output))))
+ .orElse(InvokeOperation.EMPTY));
+ }
+
+ /**
+ * Invoke Action via ActionServiceHandler.
+ *
+ * @param input input data
+ * @param yangIId invocation context
+ * @param schemaPath schema path of data
+ * @param actionService action service to invoke action
+ * @return {@link DOMActionResult}
+ */
+ private static RestconfFuture<DOMActionResult> dataInvokePOST(final DOMActionService actionService,
+ final OperationPath.Action path, final @NonNull ContainerNode input) {
+ final var ret = new SettableRestconfFuture<DOMActionResult>();
+
+ Futures.addCallback(actionService.invokeAction(
+ path.inference().toSchemaInferenceStack().toSchemaNodeIdentifier(),
+ new DOMDataTreeIdentifier(LogicalDatastoreType.OPERATIONAL, path.instance()), input),
+ new FutureCallback<DOMActionResult>() {