Promote OperationOutput to restconf.server.api
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / transactions / RestconfStrategy.java
index 1b33afda3a2933d769af42febb61a62b8d778ff3..50725a28237bf202d9f9abc0e2962919e5fc5210 100644 (file)
@@ -10,16 +10,20 @@ package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
 import static com.google.common.base.Verify.verifyNotNull;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.CharSource;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Optional;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.eclipse.jdt.annotation.NonNull;
@@ -28,7 +32,11 @@ import org.opendaylight.mdsal.common.api.CommitInfo;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
+import org.opendaylight.mdsal.dom.api.DOMRpcResult;
+import org.opendaylight.mdsal.dom.api.DOMRpcService;
+import org.opendaylight.mdsal.dom.api.DOMSchemaService;
 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
+import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
 import org.opendaylight.restconf.api.query.ContentParam;
 import org.opendaylight.restconf.api.query.WithDefaultsParam;
@@ -40,6 +48,13 @@ import org.opendaylight.restconf.common.patch.PatchContext;
 import org.opendaylight.restconf.common.patch.PatchStatusContext;
 import org.opendaylight.restconf.common.patch.PatchStatusEntity;
 import org.opendaylight.restconf.nb.rfc8040.Insert;
+import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
+import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
+import org.opendaylight.restconf.server.api.DataPutResult;
+import org.opendaylight.restconf.server.api.DatabindContext;
+import org.opendaylight.restconf.server.api.OperationsPostResult;
+import org.opendaylight.restconf.server.spi.OperationInput;
+import org.opendaylight.restconf.server.spi.RpcImplementation;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.with.defaults.rev110601.WithDefaultsMode;
 import org.opendaylight.yangtools.yang.common.Empty;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
@@ -71,13 +86,18 @@ import org.opendaylight.yangtools.yang.data.api.schema.builder.NormalizedNodeCon
 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
-import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
+import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.api.stmt.SubmoduleEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceRepresentation;
+import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
+import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
+import org.opendaylight.yangtools.yang.model.repo.api.YinTextSchemaSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -90,53 +110,53 @@ import org.slf4j.LoggerFactory;
 // FIXME: it seems the first three operations deal with lifecycle of a transaction, while others invoke various
 //        operations. This should be handled through proper allocation indirection.
 public abstract class RestconfStrategy {
-    /**
-     * Result of a {@code PUT} request as defined in
-     * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.5">RFC8040 section 4.5</a>. The definition makes it
-     * clear that the logical operation is {@code create-or-replace}.
-     */
-    public enum CreateOrReplaceResult {
-        /**
-         * A new resource has been created.
-         */
-        CREATED,
-        /*
-         * An existing resources has been replaced.
-         */
-        REPLACED;
-    }
-
     private static final Logger LOG = LoggerFactory.getLogger(RestconfStrategy.class);
 
-    private final @NonNull EffectiveModelContext modelContext;
-
-    RestconfStrategy(final EffectiveModelContext modelContext) {
-        this.modelContext = requireNonNull(modelContext);
+    private final @NonNull DatabindContext databind;
+    private final @NonNull ImmutableMap<QName, RpcImplementation> localRpcs;
+    private final DOMYangTextSourceProvider sourceProvider;
+    private final DOMRpcService rpcService;
+
+    RestconfStrategy(final DatabindContext databind, final ImmutableMap<QName, RpcImplementation> localRpcs,
+            final @Nullable DOMRpcService rpcService, final DOMYangTextSourceProvider sourceProvider) {
+        this.databind = requireNonNull(databind);
+        this.localRpcs = requireNonNull(localRpcs);
+        this.rpcService = rpcService;
+        this.sourceProvider = sourceProvider;
     }
 
     /**
      * Look up the appropriate strategy for a particular mount point.
      *
-     * @param modelContext {@link EffectiveModelContext} of target mount point
+     * @param databind {@link DatabindContext} of target mount point
      * @param mountPoint Target mount point
      * @return A strategy, or null if the mount point does not expose a supported interface
      * @throws NullPointerException if any argument is {@code null}
      */
-    public static @Nullable RestconfStrategy forMountPoint(final EffectiveModelContext modelContext,
+    public static @Nullable RestconfStrategy forMountPoint(final DatabindContext databind,
             final DOMMountPoint mountPoint) {
+        final var rpcService = mountPoint.getService(DOMRpcService.class).orElse(null);
+        final var sourceProvider = mountPoint.getService(DOMSchemaService.class)
+            .flatMap(schema -> Optional.ofNullable(schema.getExtensions().getInstance(DOMYangTextSourceProvider.class)))
+            .orElse(null);
+
         final var netconfService = mountPoint.getService(NetconfDataTreeService.class);
         if (netconfService.isPresent()) {
-            return new NetconfRestconfStrategy(modelContext, netconfService.orElseThrow());
+            return new NetconfRestconfStrategy(databind, netconfService.orElseThrow(), rpcService, sourceProvider);
         }
         final var dataBroker = mountPoint.getService(DOMDataBroker.class);
         if (dataBroker.isPresent()) {
-            return new MdsalRestconfStrategy(modelContext, dataBroker.orElseThrow());
+            return new MdsalRestconfStrategy(databind, dataBroker.orElseThrow(), rpcService, sourceProvider);
         }
         return null;
     }
 
+    public final @NonNull DatabindContext databind() {
+        return databind;
+    }
+
     public final @NonNull EffectiveModelContext modelContext() {
-        return modelContext;
+        return databind.modelContext();
     }
 
     /**
@@ -222,7 +242,7 @@ public abstract class RestconfStrategy {
 
             @Override
             public void onFailure(final Throwable cause) {
-                future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path));
+                future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path, modelContext()));
             }
         }, MoreExecutors.directExecutor());
     }
@@ -233,10 +253,10 @@ public abstract class RestconfStrategy {
      * @param path    path of data
      * @param data    data
      * @param insert  {@link Insert}
-     * @return A {@link CreateOrReplaceResult}
+     * @return A {@link DataPutResult}
      */
-    public final @NonNull CreateOrReplaceResult putData(final YangInstanceIdentifier path, final NormalizedNode data,
-            final @Nullable Insert insert) {
+    public final RestconfFuture<DataPutResult> putData(final YangInstanceIdentifier path,
+            final NormalizedNode data, final @Nullable Insert insert) {
         final var exists = TransactionUtil.syncAccess(exists(path), path);
 
         final ListenableFuture<? extends CommitInfo> commitFuture;
@@ -248,8 +268,21 @@ public abstract class RestconfStrategy {
             commitFuture = replaceAndCommit(prepareWriteExecution(), path, data);
         }
 
-        TransactionUtil.syncCommit(commitFuture, "PUT", path);
-        return exists ? CreateOrReplaceResult.REPLACED : CreateOrReplaceResult.CREATED;
+        final var ret = new SettableRestconfFuture<DataPutResult>();
+
+        Futures.addCallback(commitFuture, new FutureCallback<CommitInfo>() {
+            @Override
+            public void onSuccess(final CommitInfo result) {
+                ret.set(exists ? DataPutResult.REPLACED : DataPutResult.CREATED);
+            }
+
+            @Override
+            public void onFailure(final Throwable cause) {
+                ret.setFailure(TransactionUtil.decodeException(cause, "PUT", path, modelContext()));
+            }
+        }, MoreExecutors.directExecutor());
+
+        return ret;
     }
 
     private ListenableFuture<? extends CommitInfo> insertAndCommitPut(final YangInstanceIdentifier path,
@@ -304,7 +337,7 @@ public abstract class RestconfStrategy {
         }
 
         int lastInsertedPosition = 0;
-        final var emptySubtree = ImmutableNodes.fromInstanceId(modelContext, path.getParent());
+        final var emptySubtree = ImmutableNodes.fromInstanceId(modelContext(), path.getParent());
         tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
         for (var nodeChild : readList.body()) {
             if (lastInsertedPosition == lastItemPosition) {
@@ -324,8 +357,7 @@ public abstract class RestconfStrategy {
 
     private DataSchemaNode checkListAndOrderedType(final YangInstanceIdentifier path) {
         // FIXME: we have this available in InstanceIdentifierContext
-        final var dataSchemaNode = DataSchemaContextTree.from(modelContext).findChild(path).orElseThrow()
-            .dataSchemaNode();
+        final var dataSchemaNode = databind.schemaTree().findChild(path).orElseThrow().dataSchemaNode();
 
         final String message;
         if (dataSchemaNode instanceof ListSchemaNode listSchema) {
@@ -350,9 +382,10 @@ public abstract class RestconfStrategy {
      * @param path    path
      * @param data    data
      * @param insert  {@link Insert}
+     * @return A {@link RestconfFuture}
      */
-    public final void postData(final YangInstanceIdentifier path, final NormalizedNode data,
-            final @Nullable Insert insert) {
+    public final @NonNull RestconfFuture<CreateResource> postData(final YangInstanceIdentifier path,
+            final NormalizedNode data, final @Nullable Insert insert) {
         final ListenableFuture<? extends CommitInfo> future;
         if (insert != null) {
             final var parentPath = path.coerceParent();
@@ -361,7 +394,24 @@ public abstract class RestconfStrategy {
         } else {
             future = createAndCommit(prepareWriteExecution(), path, data);
         }
-        TransactionUtil.syncCommit(future, "POST", path);
+
+        final var ret = new SettableRestconfFuture<CreateResource>();
+        Futures.addCallback(future, new FutureCallback<CommitInfo>() {
+            @Override
+            public void onSuccess(final CommitInfo result) {
+                ret.set(new CreateResource(IdentifierCodec.serialize(
+                    data instanceof MapNode mapData && !mapData.isEmpty()
+                        ? path.node(mapData.body().iterator().next().name()) : path,
+                    databind)));
+            }
+
+            @Override
+            public void onFailure(final Throwable cause) {
+                ret.setFailure(TransactionUtil.decodeException(cause, "POST", path, modelContext()));
+            }
+
+        }, MoreExecutors.directExecutor());
+        return ret;
     }
 
     private ListenableFuture<? extends CommitInfo> insertAndCommitPost(final YangInstanceIdentifier path,
@@ -412,7 +462,7 @@ public abstract class RestconfStrategy {
      * @param patch Patch context to be processed
      * @return {@link PatchStatusContext}
      */
-    public final @NonNull PatchStatusContext patchData(final PatchContext patch) {
+    public final @NonNull RestconfFuture<PatchStatusContext> patchData(final PatchContext patch) {
         final var editCollection = new ArrayList<PatchStatusEntity>();
         final var tx = prepareWriteExecution();
 
@@ -481,21 +531,30 @@ public abstract class RestconfStrategy {
             }
         }
 
-        // if no errors then submit transaction, otherwise cancel
-        final var patchId = patch.patchId();
-        if (noError) {
-            try {
-                TransactionUtil.syncCommit(tx.commit(), "PATCH", null);
-            } catch (RestconfDocumentedException e) {
+        final var ret = new SettableRestconfFuture<PatchStatusContext>();
+        // We have errors
+        if (!noError) {
+            tx.cancel();
+            ret.set(new PatchStatusContext(modelContext(), patch.patchId(), List.copyOf(editCollection), false, null));
+            return ret;
+        }
+
+        Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
+            @Override
+            public void onSuccess(final CommitInfo result) {
+                ret.set(new PatchStatusContext(modelContext(), patch.patchId(), List.copyOf(editCollection), true,
+                    null));
+            }
+
+            @Override
+            public void onFailure(final Throwable cause) {
                 // if errors occurred during transaction commit then patch failed and global errors are reported
-                return new PatchStatusContext(modelContext, patchId, List.copyOf(editCollection), false, e.getErrors());
+                ret.set(new PatchStatusContext(modelContext(), patch.patchId(), List.copyOf(editCollection), false,
+                    TransactionUtil.decodeException(cause, "PATCH", null, modelContext()).getErrors()));
             }
+        }, MoreExecutors.directExecutor());
 
-            return new PatchStatusContext(modelContext, patchId, List.copyOf(editCollection), true, null);
-        } else {
-            tx.cancel();
-            return new PatchStatusContext(modelContext, patchId, List.copyOf(editCollection), false, null);
-        }
+        return ret;
     }
 
     private void insertWithPointPost(final RestconfTransaction tx, final YangInstanceIdentifier path,
@@ -515,7 +574,7 @@ public abstract class RestconfStrategy {
         }
 
         int lastInsertedPosition = 0;
-        final var emptySubtree = ImmutableNodes.fromInstanceId(modelContext, grandParentPath);
+        final var emptySubtree = ImmutableNodes.fromInstanceId(modelContext(), grandParentPath);
         tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
         for (var nodeChild : readList.body()) {
             if (lastInsertedPosition == lastItemPosition) {
@@ -564,6 +623,7 @@ public abstract class RestconfStrategy {
      * @param defaultsMode value of with-defaults parameter
      * @return {@link NormalizedNode}
      */
+    // FIXME: NETCONF-1155: this method should asynchronous
     public @Nullable NormalizedNode readData(final @NonNull ContentParam content,
             final @NonNull YangInstanceIdentifier path, final WithDefaultsParam defaultsMode) {
         return switch (content) {
@@ -595,6 +655,7 @@ public abstract class RestconfStrategy {
      * @param fields   paths to selected subtrees which should be read, relative to to the parent path
      * @return {@link NormalizedNode}
      */
+    // FIXME: NETCONF-1155: this method should asynchronous
     public @Nullable NormalizedNode readData(final @NonNull ContentParam content,
             final @NonNull YangInstanceIdentifier path, final @Nullable WithDefaultsParam withDefa,
             final @NonNull List<YangInstanceIdentifier> fields) {
@@ -647,7 +708,7 @@ public abstract class RestconfStrategy {
         };
 
         // FIXME: we have this readily available in InstanceIdentifierContext
-        final var ctxNode = DataSchemaContextTree.from(modelContext).findChild(path).orElseThrow();
+        final var ctxNode = databind.schemaTree().findChild(path).orElseThrow();
         if (readData instanceof ContainerNode container) {
             final var builder = Builders.containerBuilder().withNodeIdentifier(container.name());
             buildCont(builder, container.body(), ctxNode, trim);
@@ -995,4 +1056,105 @@ public abstract class RestconfStrategy {
         configMap.entrySet().stream().filter(x -> stateMap.containsKey(x.getKey())).forEach(
             y -> builder.addChild((T) prepareData(y.getValue(), stateMap.get(y.getKey()))));
     }
+
+    public @NonNull RestconfFuture<OperationsPostResult> invokeRpc(final URI restconfURI, final QName type,
+            final OperationInput input) {
+        final var local = localRpcs.get(type);
+        if (local != null) {
+            return local.invoke(restconfURI, input);
+        }
+        if (rpcService == null) {
+            LOG.debug("RPC invocation is not available");
+            return RestconfFuture.failed(new RestconfDocumentedException("RPC invocation is not available",
+                ErrorType.PROTOCOL, ErrorTag.OPERATION_NOT_SUPPORTED));
+        }
+
+        final var ret = new SettableRestconfFuture<OperationsPostResult>();
+        Futures.addCallback(rpcService.invokeRpc(requireNonNull(type), input.input()),
+            new FutureCallback<DOMRpcResult>() {
+                @Override
+                public void onSuccess(final DOMRpcResult response) {
+                    final var errors = response.errors();
+                    if (errors.isEmpty()) {
+                        ret.set(input.newOperationOutput(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 SchemaSourceRepresentation> representation) {
+        final var src = requireNonNull(source);
+        if (YangTextSchemaSource.class.isAssignableFrom(representation)) {
+            if (sourceProvider != null) {
+                final var ret = new SettableRestconfFuture<CharSource>();
+                Futures.addCallback(sourceProvider.getSource(src), new FutureCallback<YangTextSchemaSource>() {
+                    @Override
+                    public void onSuccess(final YangTextSchemaSource 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 (YinTextSchemaSource.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));
+    }
+
 }