import static com.google.common.base.Verify.verifyNotNull;
import static java.util.Objects.requireNonNull;
+import static org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes.fromInstanceId;
+import com.google.common.annotations.VisibleForTesting;
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.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
+import java.util.concurrent.CancellationException;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.mdsal.common.api.CommitInfo;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.mdsal.dom.api.DOMActionException;
+import org.opendaylight.mdsal.dom.api.DOMActionResult;
+import org.opendaylight.mdsal.dom.api.DOMActionService;
import org.opendaylight.mdsal.dom.api.DOMDataBroker;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
import org.opendaylight.mdsal.dom.api.DOMMountPoint;
+import org.opendaylight.mdsal.dom.api.DOMMountPointService;
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.DOMSchemaService.YangTextSourceExtension;
import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
-import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
+import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
+import org.opendaylight.restconf.api.ApiPath;
+import org.opendaylight.restconf.api.FormattableBody;
import org.opendaylight.restconf.api.query.ContentParam;
import org.opendaylight.restconf.api.query.WithDefaultsParam;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.common.errors.RestconfFuture;
import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
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.nb.rfc8040.legacy.ErrorTags;
+import org.opendaylight.restconf.server.api.ChildBody;
+import org.opendaylight.restconf.server.api.ConfigurationMetadata;
+import org.opendaylight.restconf.server.api.CreateResourceResult;
+import org.opendaylight.restconf.server.api.DataGetParams;
+import org.opendaylight.restconf.server.api.DataGetResult;
+import org.opendaylight.restconf.server.api.DataPatchResult;
+import org.opendaylight.restconf.server.api.DataPostBody;
+import org.opendaylight.restconf.server.api.DataPostResult;
import org.opendaylight.restconf.server.api.DataPutResult;
+import org.opendaylight.restconf.server.api.DataYangPatchResult;
import org.opendaylight.restconf.server.api.DatabindContext;
+import org.opendaylight.restconf.server.api.DatabindPath;
+import org.opendaylight.restconf.server.api.DatabindPath.Action;
+import org.opendaylight.restconf.server.api.DatabindPath.Data;
+import org.opendaylight.restconf.server.api.DatabindPath.InstanceReference;
+import org.opendaylight.restconf.server.api.DatabindPath.OperationPath;
+import org.opendaylight.restconf.server.api.DatabindPath.Rpc;
+import org.opendaylight.restconf.server.api.InvokeResult;
+import org.opendaylight.restconf.server.api.OperationInputBody;
+import org.opendaylight.restconf.server.api.PatchBody;
+import org.opendaylight.restconf.server.api.PatchStatusContext;
+import org.opendaylight.restconf.server.api.PatchStatusEntity;
+import org.opendaylight.restconf.server.api.ResourceBody;
+import org.opendaylight.restconf.server.api.ServerRequest;
+import org.opendaylight.restconf.server.spi.ApiPathCanonizer;
+import org.opendaylight.restconf.server.spi.ApiPathNormalizer;
+import org.opendaylight.restconf.server.spi.DefaultResourceContext;
+import org.opendaylight.restconf.server.spi.HttpGetResource;
+import org.opendaylight.restconf.server.spi.NormalizedFormattableBody;
+import org.opendaylight.restconf.server.spi.NormalizedNodeWriterFactory;
import org.opendaylight.restconf.server.spi.OperationInput;
-import org.opendaylight.restconf.server.spi.OperationOutput;
+import org.opendaylight.restconf.server.spi.OperationOutputBody;
+import org.opendaylight.restconf.server.spi.OperationsResource;
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.ErrorType;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder;
import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
import org.opendaylight.yangtools.yang.data.api.schema.builder.NormalizedNodeContainerBuilder;
-import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
-import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
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.source.SourceIdentifier;
+import org.opendaylight.yangtools.yang.model.api.source.SourceRepresentation;
+import org.opendaylight.yangtools.yang.model.api.source.YangTextSource;
+import org.opendaylight.yangtools.yang.model.api.source.YinTextSource;
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;
// 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 {
+ @NonNullByDefault
+ public record StrategyAndPath(RestconfStrategy strategy, Data path) {
+ public StrategyAndPath {
+ requireNonNull(strategy);
+ requireNonNull(path);
+ }
+ }
+
+ /**
+ * Result of a partial {@link ApiPath} lookup for the purposes of supporting {@code yang-ext:mount}-delimited mount
+ * points with possible nesting.
+ *
+ * @param strategy the strategy to use
+ * @param tail the {@link ApiPath} tail to use with the strategy
+ */
+ @NonNullByDefault
+ public record StrategyAndTail(RestconfStrategy strategy, ApiPath tail) {
+ public StrategyAndTail {
+ requireNonNull(strategy);
+ requireNonNull(tail);
+ }
+ }
+
private static final Logger LOG = LoggerFactory.getLogger(RestconfStrategy.class);
+ private static final @NonNull DataPutResult PUT_CREATED = new DataPutResult(true);
+ private static final @NonNull DataPutResult PUT_REPLACED = new DataPutResult(false);
+ private static final @NonNull DataPatchResult PATCH_EMPTY = new DataPatchResult();
- private final @NonNull DatabindContext databind;
private final @NonNull ImmutableMap<QName, RpcImplementation> localRpcs;
- private final DOMYangTextSourceProvider sourceProvider;
+ private final @NonNull ApiPathNormalizer pathNormalizer;
+ private final @NonNull DatabindContext databind;
+ private final YangTextSourceExtension sourceProvider;
+ private final DOMMountPointService mountPointService;
+ private final DOMActionService actionService;
private final DOMRpcService rpcService;
+ private final HttpGetResource operations;
RestconfStrategy(final DatabindContext databind, final ImmutableMap<QName, RpcImplementation> localRpcs,
- final @Nullable DOMRpcService rpcService, final DOMYangTextSourceProvider sourceProvider) {
+ final @Nullable DOMRpcService rpcService, final @Nullable DOMActionService actionService,
+ final @Nullable YangTextSourceExtension sourceProvider,
+ final @Nullable DOMMountPointService mountPointService) {
this.databind = requireNonNull(databind);
this.localRpcs = requireNonNull(localRpcs);
this.rpcService = rpcService;
+ this.actionService = actionService;
this.sourceProvider = sourceProvider;
+ this.mountPointService = mountPointService;
+ pathNormalizer = new ApiPathNormalizer(databind);
+ operations = new OperationsResource(pathNormalizer);
+ }
+
+ public final @NonNull StrategyAndPath resolveStrategyPath(final ApiPath path) {
+ final var andTail = resolveStrategy(path);
+ final var strategy = andTail.strategy();
+ return new StrategyAndPath(strategy, strategy.pathNormalizer.normalizeDataPath(andTail.tail()));
}
/**
- * Look up the appropriate strategy for a particular mount point.
+ * Resolve any and all {@code yang-ext:mount} to the target {@link StrategyAndTail}.
*
- * @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}
+ * @param path {@link ApiPath} to resolve
+ * @return A strategy and the remaining path
+ * @throws NullPointerException if {@code path} is {@code null}
*/
- public static @Nullable RestconfStrategy forMountPoint(final DatabindContext databind,
- final DOMMountPoint mountPoint) {
+ public final @NonNull StrategyAndTail resolveStrategy(final ApiPath path) {
+ var mount = path.indexOf("yang-ext", "mount");
+ if (mount == -1) {
+ return new StrategyAndTail(this, path);
+ }
+ if (mountPointService == null) {
+ throw new RestconfDocumentedException("Mount point service is not available",
+ ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED);
+ }
+ final var mountPath = path.subPath(0, mount);
+ final var dataPath = pathNormalizer.normalizeDataPath(path.subPath(0, mount));
+ final var mountPoint = mountPointService.getMountPoint(dataPath.instance())
+ .orElseThrow(() -> new RestconfDocumentedException("Mount point '" + mountPath + "' does not exist",
+ ErrorType.PROTOCOL, ErrorTags.RESOURCE_DENIED_TRANSPORT));
+
+ return createStrategy(mountPath, mountPoint).resolveStrategy(path.subPath(mount + 1));
+ }
+
+ private static @NonNull RestconfStrategy createStrategy(final ApiPath mountPath, final DOMMountPoint mountPoint) {
+ final var mountSchemaService = mountPoint.getService(DOMSchemaService.class)
+ .orElseThrow(() -> new RestconfDocumentedException(
+ "Mount point '" + mountPath + "' does not expose DOMSchemaService",
+ ErrorType.PROTOCOL, ErrorTags.RESOURCE_DENIED_TRANSPORT));
+ final var mountModelContext = mountSchemaService.getGlobalContext();
+ if (mountModelContext == null) {
+ throw new RestconfDocumentedException("Mount point '" + mountPath + "' does not have any models",
+ ErrorType.PROTOCOL, ErrorTags.RESOURCE_DENIED_TRANSPORT);
+ }
+ final var mountDatabind = DatabindContext.ofModel(mountModelContext);
+ final var mountPointService = mountPoint.getService(DOMMountPointService.class).orElse(null);
final var rpcService = mountPoint.getService(DOMRpcService.class).orElse(null);
+ final var actionService = mountPoint.getService(DOMActionService.class).orElse(null);
final var sourceProvider = mountPoint.getService(DOMSchemaService.class)
- .flatMap(schema -> Optional.ofNullable(schema.getExtensions().getInstance(DOMYangTextSourceProvider.class)))
+ .flatMap(schema -> Optional.ofNullable(schema.extension(YangTextSourceExtension.class)))
.orElse(null);
final var netconfService = mountPoint.getService(NetconfDataTreeService.class);
if (netconfService.isPresent()) {
- return new NetconfRestconfStrategy(databind, netconfService.orElseThrow(), rpcService, sourceProvider);
+ return new NetconfRestconfStrategy(mountDatabind, netconfService.orElseThrow(), rpcService, actionService,
+ sourceProvider, mountPointService);
}
final var dataBroker = mountPoint.getService(DOMDataBroker.class);
if (dataBroker.isPresent()) {
- return new MdsalRestconfStrategy(databind, dataBroker.orElseThrow(), rpcService, sourceProvider);
+ return new MdsalRestconfStrategy(mountDatabind, dataBroker.orElseThrow(), ImmutableMap.of(), rpcService,
+ actionService, sourceProvider, mountPointService);
}
- return null;
+ LOG.warn("Mount point {} does not expose a suitable access interface", mountPath);
+ throw new RestconfDocumentedException("Could not find a supported access interface in mount point",
+ ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, mountPoint.getIdentifier());
}
public final @NonNull DatabindContext databind() {
*/
abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store, YangInstanceIdentifier path);
- /**
- * Read data selected using fields from the datastore.
- *
- * @param store the logical data store which should be modified
- * @param path the parent data object path
- * @param fields paths to selected fields relative to parent path
- * @return a ListenableFuture containing the result of the read
- */
- abstract ListenableFuture<Optional<NormalizedNode>> read(LogicalDatastoreType store, YangInstanceIdentifier path,
- List<YangInstanceIdentifier> fields);
-
/**
* Check if data already exists in the configuration datastore.
*
// FIXME: this method should only be needed in MdsalRestconfStrategy
abstract ListenableFuture<Boolean> exists(YangInstanceIdentifier path);
- /**
- * Delete data from the configuration datastore. If the data does not exist, this operation will fail, as outlined
- * in <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.7">RFC8040 section 4.7</a>
- *
- * @param path Path to delete
- * @return A {@link RestconfFuture}
- * @throws NullPointerException if {@code path} is {@code null}
- */
- public final @NonNull RestconfFuture<Empty> delete(final YangInstanceIdentifier path) {
- final var ret = new SettableRestconfFuture<Empty>();
- delete(ret, requireNonNull(path));
- return ret;
- }
-
- abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
-
- /**
- * Merge data into the configuration datastore, as outlined in
- * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040 section 4.6.1</a>.
- *
- * @param path Path to merge
- * @param data Data to merge
- * @return A {@link RestconfFuture}
- * @throws NullPointerException if any argument is {@code null}
- */
- public final @NonNull RestconfFuture<Empty> merge(final YangInstanceIdentifier path, final NormalizedNode data) {
- final var ret = new SettableRestconfFuture<Empty>();
+ @VisibleForTesting
+ final @NonNull RestconfFuture<DataPatchResult> merge(final YangInstanceIdentifier path, final NormalizedNode data) {
+ final var ret = new SettableRestconfFuture<DataPatchResult>();
merge(ret, requireNonNull(path), requireNonNull(data));
return ret;
}
- private void merge(final @NonNull SettableRestconfFuture<Empty> future, final @NonNull YangInstanceIdentifier path,
- final @NonNull NormalizedNode data) {
+ private void merge(final @NonNull SettableRestconfFuture<DataPatchResult> future,
+ final @NonNull YangInstanceIdentifier path, final @NonNull NormalizedNode data) {
final var tx = prepareWriteExecution();
// FIXME: this method should be further specialized to eliminate this call -- it is only needed for MD-SAL
tx.ensureParentsByMerge(path);
Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
@Override
public void onSuccess(final CommitInfo result) {
- future.set(Empty.value());
+ // TODO: extract details once CommitInfo can communicate them
+ future.set(PATCH_EMPTY);
}
@Override
}, MoreExecutors.directExecutor());
}
+ public @NonNull RestconfFuture<DataPutResult> dataPUT(final ServerRequest request, final ApiPath apiPath,
+ final ResourceBody body) {
+ final Data path;
+ try {
+ path = pathNormalizer.normalizeDataPath(apiPath);
+ } catch (RestconfDocumentedException e) {
+ return RestconfFuture.failed(e);
+ }
+
+ final Insert insert;
+ try {
+ insert = Insert.of(databind, request.queryParameters());
+ } catch (IllegalArgumentException e) {
+ return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
+ }
+ final NormalizedNode data;
+ try {
+ data = body.toNormalizedNode(path);
+ } catch (RestconfDocumentedException e) {
+ return RestconfFuture.failed(e);
+ }
+ return putData(path.instance(), data, insert);
+ }
+
/**
* Check mount point and prepare variables for put data to DS.
*
* @param insert {@link Insert}
* @return A {@link DataPutResult}
*/
- public final RestconfFuture<DataPutResult> putData(final YangInstanceIdentifier path,
+ public final @NonNull RestconfFuture<DataPutResult> putData(final YangInstanceIdentifier path,
final NormalizedNode data, final @Nullable Insert insert) {
final var exists = TransactionUtil.syncAccess(exists(path), path);
Futures.addCallback(commitFuture, new FutureCallback<CommitInfo>() {
@Override
public void onSuccess(final CommitInfo result) {
- ret.set(exists ? DataPutResult.REPLACED : DataPutResult.CREATED);
+ ret.set(exists ? PUT_REPLACED : PUT_CREATED);
}
@Override
}
int lastInsertedPosition = 0;
- final var emptySubtree = ImmutableNodes.fromInstanceId(modelContext(), path.getParent());
+ final var emptySubtree = fromInstanceId(modelContext(), path.getParent());
tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
for (var nodeChild : readList.body()) {
if (lastInsertedPosition == lastItemPosition) {
tx.replace(childPath, nodeChild);
lastInsertedPosition++;
}
+
+ // In case we are inserting after last element
+ if (!before) {
+ if (lastInsertedPosition == lastItemPosition) {
+ tx.replace(path, data);
+ }
+ }
}
private static ListenableFuture<? extends CommitInfo> replaceAndCommit(final RestconfTransaction tx,
* @param insert {@link Insert}
* @return A {@link RestconfFuture}
*/
- public final @NonNull RestconfFuture<CreateResource> postData(final YangInstanceIdentifier path,
+ public final @NonNull RestconfFuture<CreateResourceResult> postData(final YangInstanceIdentifier path,
final NormalizedNode data, final @Nullable Insert insert) {
final ListenableFuture<? extends CommitInfo> future;
if (insert != null) {
- final var parentPath = path.coerceParent();
- checkListAndOrderedType(parentPath);
- future = insertAndCommitPost(path, data, insert, parentPath);
+ checkListAndOrderedType(path);
+ future = insertAndCommitPost(path, data, insert);
} else {
future = createAndCommit(prepareWriteExecution(), path, data);
}
- final var ret = new SettableRestconfFuture<CreateResource>();
+ final var ret = new SettableRestconfFuture<CreateResourceResult>();
Futures.addCallback(future, new FutureCallback<CommitInfo>() {
@Override
public void onSuccess(final CommitInfo result) {
- ret.set(new CreateResource(IdentifierCodec.serialize(
+ ret.set(new CreateResourceResult(new ApiPathCanonizer(databind).dataToApiPath(
data instanceof MapNode mapData && !mapData.isEmpty()
- ? path.node(mapData.body().iterator().next().name()) : path,
- databind)));
+ ? path.node(mapData.body().iterator().next().name()) : path)));
}
@Override
}
private ListenableFuture<? extends CommitInfo> insertAndCommitPost(final YangInstanceIdentifier path,
- final NormalizedNode data, final @NonNull Insert insert, final YangInstanceIdentifier parent) {
- final var grandParent = parent.coerceParent();
+ final NormalizedNode data, final @NonNull Insert insert) {
final var tx = prepareWriteExecution();
return switch (insert.insert()) {
case FIRST -> {
- final var readData = tx.readList(grandParent);
+ final var readData = tx.readList(path);
if (readData == null || readData.isEmpty()) {
tx.replace(path, data);
} else {
- checkItemDoesNotExists(exists(path), path);
- tx.remove(grandParent);
+ checkListDataDoesNotExist(path, data);
+ tx.remove(path);
tx.replace(path, data);
- tx.replace(grandParent, readData);
+ tx.replace(path, readData);
}
yield tx.commit();
}
case LAST -> createAndCommit(tx, path, data);
case BEFORE -> {
- final var readData = tx.readList(grandParent);
+ final var readData = tx.readList(path);
if (readData == null || readData.isEmpty()) {
tx.replace(path, data);
} else {
- checkItemDoesNotExists(exists(path), path);
- insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, grandParent, true);
+ checkListDataDoesNotExist(path, data);
+ insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, true);
}
yield tx.commit();
}
case AFTER -> {
- final var readData = tx.readList(grandParent);
+ final var readData = tx.readList(path);
if (readData == null || readData.isEmpty()) {
tx.replace(path, data);
} else {
- checkItemDoesNotExists(exists(path), path);
- insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, grandParent, false);
+ checkListDataDoesNotExist(path, data);
+ insertWithPointPost(tx, path, data, verifyNotNull(insert.pointArg()), readData, false);
}
yield tx.commit();
}
};
}
+ /**
+ * Merge data into the configuration datastore, as outlined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040 section 4.6.1</a>.
+ *
+ * @param apiPath Path to merge
+ * @param body Data to merge
+ * @return A {@link RestconfFuture}
+ * @throws NullPointerException if any argument is {@code null}
+ */
+ public final @NonNull RestconfFuture<DataPatchResult> dataPATCH(final ApiPath apiPath, final ResourceBody body) {
+ final Data path;
+ try {
+ path = pathNormalizer.normalizeDataPath(apiPath);
+ } catch (RestconfDocumentedException e) {
+ return RestconfFuture.failed(e);
+ }
+
+ final NormalizedNode data;
+ try {
+ data = body.toNormalizedNode(path);
+ } catch (RestconfDocumentedException e) {
+ return RestconfFuture.failed(e);
+ }
+
+ return merge(path.instance(), data);
+ }
+
+ public final @NonNull RestconfFuture<DataYangPatchResult> dataPATCH(final ApiPath apiPath, final PatchBody body) {
+ final Data path;
+ try {
+ path = pathNormalizer.normalizeDataPath(apiPath);
+ } catch (RestconfDocumentedException e) {
+ return RestconfFuture.failed(e);
+ }
+
+ final PatchContext patch;
+ try {
+ patch = body.toPatchContext(new DefaultResourceContext(path));
+ } catch (IOException e) {
+ LOG.debug("Error parsing YANG Patch input", e);
+ return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
+ ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
+ }
+ return patchData(patch);
+ }
+
/**
* Process edit operations of one {@link PatchContext}.
*
* @param patch Patch context to be processed
* @return {@link PatchStatusContext}
*/
- public final @NonNull RestconfFuture<PatchStatusContext> patchData(final PatchContext patch) {
+ @VisibleForTesting
+ public final @NonNull RestconfFuture<DataYangPatchResult> patchData(final PatchContext patch) {
final var editCollection = new ArrayList<PatchStatusEntity>();
final var tx = prepareWriteExecution();
}
}
- final var ret = new SettableRestconfFuture<PatchStatusContext>();
+ final var ret = new SettableRestconfFuture<DataYangPatchResult>();
// We have errors
if (!noError) {
tx.cancel();
- ret.set(new PatchStatusContext(modelContext(), patch.patchId(), List.copyOf(editCollection), false, null));
+ ret.set(new DataYangPatchResult(
+ new PatchStatusContext(databind(), 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));
+ ret.set(new DataYangPatchResult(
+ new PatchStatusContext(databind(), 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
- ret.set(new PatchStatusContext(modelContext(), patch.patchId(), List.copyOf(editCollection), false,
- TransactionUtil.decodeException(cause, "PATCH", null, modelContext()).getErrors()));
+ ret.set(new DataYangPatchResult(
+ new PatchStatusContext(databind(), patch.patchId(), List.copyOf(editCollection), false,
+ TransactionUtil.decodeException(cause, "PATCH", null, modelContext()).getErrors())));
}
}, MoreExecutors.directExecutor());
return ret;
}
- private void insertWithPointPost(final RestconfTransaction tx, final YangInstanceIdentifier path,
+ private static void insertWithPointPost(final RestconfTransaction tx, final YangInstanceIdentifier path,
final NormalizedNode data, final PathArgument pointArg, final NormalizedNodeContainer<?> readList,
- final YangInstanceIdentifier grandParentPath, final boolean before) {
- tx.remove(grandParentPath);
+ final boolean before) {
+ tx.remove(path);
int lastItemPosition = 0;
for (var nodeChild : readList.body()) {
}
int lastInsertedPosition = 0;
- final var emptySubtree = ImmutableNodes.fromInstanceId(modelContext(), grandParentPath);
- tx.merge(YangInstanceIdentifier.of(emptySubtree.name()), emptySubtree);
for (var nodeChild : readList.body()) {
if (lastInsertedPosition == lastItemPosition) {
tx.replace(path, data);
}
- tx.replace(grandParentPath.node(nodeChild.name()), nodeChild);
+ tx.replace(path.node(nodeChild.name()), nodeChild);
lastInsertedPosition++;
}
+
+ // In case we are inserting after last element
+ if (!before) {
+ if (lastInsertedPosition == lastItemPosition) {
+ tx.replace(path, data);
+ }
+ }
}
private static ListenableFuture<? extends CommitInfo> createAndCommit(final RestconfTransaction tx,
return tx.commit();
}
+ /**
+ * Check if child items do NOT already exists in List at specified {@code path}.
+ *
+ * @param data Data to be checked
+ * @param path Path to be checked
+ * @throws RestconfDocumentedException if data already exists.
+ */
+ private void checkListDataDoesNotExist(final YangInstanceIdentifier path, final NormalizedNode data) {
+ if (data instanceof NormalizedNodeContainer<?> dataNode) {
+ for (final var node : dataNode.body()) {
+ checkItemDoesNotExists(exists(path.node(node.name())), path.node(node.name()));
+ }
+ } else {
+ throw new RestconfDocumentedException("Unexpected node type: " + data.getClass().getName());
+ }
+ }
+
/**
* Check if items do NOT already exists at specified {@code path}.
*
}
}
+ /**
+ * Delete data from the configuration datastore. If the data does not exist, this operation will fail, as outlined
+ * in <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.7">RFC8040 section 4.7</a>
+ *
+ * @param apiPath Path to delete
+ * @return A {@link RestconfFuture}
+ * @throws NullPointerException if {@code apiPath} is {@code null}
+ */
+ @NonNullByDefault
+ @SuppressWarnings("checkstyle:abbreviationAsWordInName")
+ public final RestconfFuture<Empty> dataDELETE(final ServerRequest request, final ApiPath apiPath) {
+ final Data path;
+ try {
+ path = pathNormalizer.normalizeDataPath(apiPath);
+ } catch (RestconfDocumentedException e) {
+ return RestconfFuture.failed(e);
+ }
+
+ // FIXME: reject empty YangInstanceIdentifier, as datastores may not be deleted
+ final var ret = new SettableRestconfFuture<Empty>();
+ delete(ret, request, path.instance());
+ return ret;
+ }
+
+ @NonNullByDefault
+ abstract void delete(SettableRestconfFuture<Empty> future, ServerRequest request, YangInstanceIdentifier path);
+
+ public final @NonNull RestconfFuture<DataGetResult> dataGET(final ServerRequest request, final ApiPath apiPath) {
+ final Data path;
+ try {
+ path = pathNormalizer.normalizeDataPath(apiPath);
+ } catch (RestconfDocumentedException e) {
+ return RestconfFuture.failed(e);
+ }
+
+ final DataGetParams getParams;
+ try {
+ getParams = DataGetParams.of(request.queryParameters());
+ } catch (IllegalArgumentException e) {
+ return RestconfFuture.failed(new RestconfDocumentedException(e,
+ new RestconfError(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, "Invalid GET /data parameters", null,
+ e.getMessage())));
+ }
+ return dataGET(request, path, getParams);
+ }
+
+ abstract @NonNull RestconfFuture<DataGetResult> dataGET(ServerRequest request, Data path, DataGetParams params);
+
+ @NonNullByDefault
+ static final RestconfFuture<DataGetResult> completeDataGET(final @Nullable NormalizedNode node, final Data path,
+ final NormalizedNodeWriterFactory writerFactory, final @Nullable ConfigurationMetadata metadata) {
+ // Non-existing data
+ if (node == null) {
+ return RestconfFuture.failed(new RestconfDocumentedException(
+ "Request could not be completed because the relevant data model content does not exist",
+ ErrorType.PROTOCOL, ErrorTag.DATA_MISSING));
+ }
+
+ final var body = NormalizedFormattableBody.of(path, node, writerFactory);
+ return RestconfFuture.of(metadata == null ? new DataGetResult(body)
+ : new DataGetResult(body, metadata.entityTag(), metadata.lastModified()));
+ }
+
/**
* Read specific type of data from data store via transaction. Close {@link DOMTransactionChain} if any
* inside of object {@link RestconfStrategy} provided as a parameter.
* @return {@link NormalizedNode}
*/
// FIXME: NETCONF-1155: this method should asynchronous
- public @Nullable NormalizedNode readData(final @NonNull ContentParam content,
+ @VisibleForTesting
+ final @Nullable NormalizedNode readData(final @NonNull ContentParam content,
final @NonNull YangInstanceIdentifier path, final WithDefaultsParam defaultsMode) {
return switch (content) {
case ALL -> {
};
}
- /**
- * Read specific type of data from data store via transaction with specified subtrees that should only be read.
- * Close {@link DOMTransactionChain} inside of object {@link RestconfStrategy} provided as a parameter.
- *
- * @param content type of data to read (config, state, all)
- * @param path the parent path to read
- * @param withDefa value of with-defaults parameter
- * @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) {
- return switch (content) {
- case ALL -> {
- // PREPARE STATE DATA NODE
- final var stateDataNode = readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path, fields);
- // PREPARE CONFIG DATA NODE
- final var configDataNode = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path, fields);
-
- yield mergeConfigAndSTateDataIfNeeded(stateDataNode, withDefa == null ? configDataNode
- : prepareDataByParamWithDef(configDataNode, path, withDefa.mode()));
- }
- case CONFIG -> {
- final var read = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path, fields);
- yield withDefa == null ? read : prepareDataByParamWithDef(read, path, withDefa.mode());
- }
- case NONCONFIG -> readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path, fields);
- };
- }
-
private @Nullable NormalizedNode readDataViaTransaction(final LogicalDatastoreType store,
final YangInstanceIdentifier path) {
return TransactionUtil.syncAccess(read(store, path), path).orElse(null);
}
- /**
- * Read specific type of data {@link LogicalDatastoreType} via transaction in {@link RestconfStrategy} with
- * specified subtrees that should only be read.
- *
- * @param store datastore type
- * @param path parent path to selected fields
- * @param closeTransactionChain if it is set to {@code true}, after transaction it will close transactionChain
- * in {@link RestconfStrategy} if any
- * @param fields paths to selected subtrees which should be read, relative to to the parent path
- * @return {@link NormalizedNode}
- */
- private @Nullable NormalizedNode readDataViaTransaction(final @NonNull LogicalDatastoreType store,
- final @NonNull YangInstanceIdentifier path, final @NonNull List<YangInstanceIdentifier> fields) {
- return TransactionUtil.syncAccess(read(store, path, fields), path).orElse(null);
- }
-
- private NormalizedNode prepareDataByParamWithDef(final NormalizedNode readData, final YangInstanceIdentifier path,
+ final NormalizedNode prepareDataByParamWithDef(final NormalizedNode readData, final YangInstanceIdentifier path,
final WithDefaultsMode defaultsMode) {
final boolean trim = switch (defaultsMode) {
case Trim -> true;
// FIXME: we have this readily available in InstanceIdentifierContext
final var ctxNode = databind.schemaTree().findChild(path).orElseThrow();
if (readData instanceof ContainerNode container) {
- final var builder = Builders.containerBuilder().withNodeIdentifier(container.name());
+ final var builder = ImmutableNodes.newContainerBuilder().withNodeIdentifier(container.name());
buildCont(builder, container.body(), ctxNode, trim);
return builder.build();
} else if (readData instanceof MapEntryNode mapEntry) {
throw new IllegalStateException("Input " + mapEntry + " does not match " + ctxNode);
}
- final var builder = Builders.mapEntryBuilder().withNodeIdentifier(mapEntry.name());
+ final var builder = ImmutableNodes.newMapEntryBuilder().withNodeIdentifier(mapEntry.name());
buildMapEntryBuilder(builder, mapEntry.body(), ctxNode, trim, listSchema.getKeyDefinition());
return builder.build();
} else {
private static void appendContainer(final DataContainerNodeBuilder<?, ?> builder, final ContainerNode container,
final DataSchemaContext ctxNode, final boolean trim) {
- final var childBuilder = Builders.containerBuilder().withNodeIdentifier(container.name());
+ final var childBuilder = ImmutableNodes.newContainerBuilder().withNodeIdentifier(container.name());
buildCont(childBuilder, container.body(), ctxNode, trim);
builder.withChild(childBuilder.build());
}
}
final var childBuilder = switch (map.ordering()) {
- case SYSTEM -> Builders.mapBuilder();
- case USER -> Builders.orderedMapBuilder();
+ case SYSTEM -> ImmutableNodes.newSystemMapBuilder();
+ case USER -> ImmutableNodes.newUserMapBuilder();
};
buildList(childBuilder.withNodeIdentifier(map.name()), map.body(), childCtx, trim,
listSchema.getKeyDefinition());
final List<@NonNull QName> keys) {
for (var entry : entries) {
final var childCtx = getChildContext(ctxNode, entry);
- final var mapEntryBuilder = Builders.mapEntryBuilder().withNodeIdentifier(entry.name());
+ final var mapEntryBuilder = ImmutableNodes.newMapEntryBuilder().withNodeIdentifier(entry.name());
buildMapEntryBuilder(mapEntryBuilder, entry.body(), childCtx, trim, keys);
builder.withChild(mapEntryBuilder.build());
}
return childCtx;
}
- private static NormalizedNode mergeConfigAndSTateDataIfNeeded(final NormalizedNode stateDataNode,
- final NormalizedNode configDataNode) {
- // if no data exists
- if (stateDataNode == null && configDataNode == null) {
- return null;
- }
-
- // return config data
+ static final NormalizedNode mergeConfigAndSTateDataIfNeeded(final NormalizedNode stateDataNode,
+ final NormalizedNode configDataNode) {
if (stateDataNode == null) {
+ // No state, return config
return configDataNode;
}
-
- // return state data
if (configDataNode == null) {
+ // No config, return state
return stateDataNode;
}
-
- // merge data from config and state
+ // merge config and state
return mergeStateAndConfigData(stateDataNode, configDataNode);
}
*/
private static @NonNull NormalizedNode prepareRpcData(final @NonNull NormalizedNode configDataNode,
final @NonNull NormalizedNode stateDataNode) {
- final var mapEntryBuilder = Builders.mapEntryBuilder()
+ final var mapEntryBuilder = ImmutableNodes.newMapEntryBuilder()
.withNodeIdentifier((NodeIdentifierWithPredicates) configDataNode.name());
// MAP CONFIG DATA
// MAP STATE DATA
mapRpcDataNode(stateDataNode, mapEntryBuilder);
- return Builders.mapBuilder()
+ return ImmutableNodes.newSystemMapBuilder()
.withNodeIdentifier(NodeIdentifier.create(configDataNode.name().getNodeType()))
.addChild(mapEntryBuilder.build())
.build();
private static @NonNull NormalizedNode prepareData(final @NonNull NormalizedNode configDataNode,
final @NonNull NormalizedNode stateDataNode) {
if (configDataNode instanceof UserMapNode configMap) {
- final var builder = Builders.orderedMapBuilder().withNodeIdentifier(configMap.name());
+ final var builder = ImmutableNodes.newUserMapBuilder().withNodeIdentifier(configMap.name());
mapValueToBuilder(configMap.body(), ((UserMapNode) stateDataNode).body(), builder);
return builder.build();
} else if (configDataNode instanceof SystemMapNode configMap) {
- final var builder = Builders.mapBuilder().withNodeIdentifier(configMap.name());
+ final var builder = ImmutableNodes.newSystemMapBuilder().withNodeIdentifier(configMap.name());
mapValueToBuilder(configMap.body(), ((SystemMapNode) stateDataNode).body(), builder);
return builder.build();
} else if (configDataNode instanceof MapEntryNode configEntry) {
- final var builder = Builders.mapEntryBuilder().withNodeIdentifier(configEntry.name());
+ final var builder = ImmutableNodes.newMapEntryBuilder().withNodeIdentifier(configEntry.name());
mapValueToBuilder(configEntry.body(), ((MapEntryNode) stateDataNode).body(), builder);
return builder.build();
} else if (configDataNode instanceof ContainerNode configContaienr) {
- final var builder = Builders.containerBuilder().withNodeIdentifier(configContaienr.name());
+ final var builder = ImmutableNodes.newContainerBuilder().withNodeIdentifier(configContaienr.name());
mapValueToBuilder(configContaienr.body(), ((ContainerNode) stateDataNode).body(), builder);
return builder.build();
} else if (configDataNode instanceof ChoiceNode configChoice) {
- final var builder = Builders.choiceBuilder().withNodeIdentifier(configChoice.name());
+ final var builder = ImmutableNodes.newChoiceBuilder().withNodeIdentifier(configChoice.name());
mapValueToBuilder(configChoice.body(), ((ChoiceNode) stateDataNode).body(), builder);
return builder.build();
} else if (configDataNode instanceof LeafNode configLeaf) {
return configLeaf;
} else if (configDataNode instanceof UserLeafSetNode) {
final var configLeafSet = (UserLeafSetNode<Object>) configDataNode;
- final var builder = Builders.<Object>orderedLeafSetBuilder().withNodeIdentifier(configLeafSet.name());
+ final var builder = ImmutableNodes.<Object>newUserLeafSetBuilder().withNodeIdentifier(configLeafSet.name());
mapValueToBuilder(configLeafSet.body(), ((UserLeafSetNode<Object>) stateDataNode).body(), builder);
return builder.build();
} else if (configDataNode instanceof SystemLeafSetNode) {
final var configLeafSet = (SystemLeafSetNode<Object>) configDataNode;
- final var builder = Builders.<Object>leafSetBuilder().withNodeIdentifier(configLeafSet.name());
+ final var builder = ImmutableNodes.<Object>newSystemLeafSetBuilder()
+ .withNodeIdentifier(configLeafSet.name());
mapValueToBuilder(configLeafSet.body(), ((SystemLeafSetNode<Object>) stateDataNode).body(), builder);
return builder.build();
} else if (configDataNode instanceof LeafSetEntryNode<?> configEntry) {
// config trumps oper
return configEntry;
} else if (configDataNode instanceof UnkeyedListNode configList) {
- final var builder = Builders.unkeyedListBuilder().withNodeIdentifier(configList.name());
+ final var builder = ImmutableNodes.newUnkeyedListBuilder().withNodeIdentifier(configList.name());
mapValueToBuilder(configList.body(), ((UnkeyedListNode) stateDataNode).body(), builder);
return builder.build();
} else if (configDataNode instanceof UnkeyedListEntryNode configEntry) {
- final var builder = Builders.unkeyedListEntryBuilder().withNodeIdentifier(configEntry.name());
+ final var builder = ImmutableNodes.newUnkeyedListEntryBuilder().withNodeIdentifier(configEntry.name());
mapValueToBuilder(configEntry.body(), ((UnkeyedListEntryNode) stateDataNode).body(), builder);
return builder.build();
} else {
y -> builder.addChild((T) prepareData(y.getValue(), stateMap.get(y.getKey()))));
}
- public @NonNull RestconfFuture<OperationOutput> invokeRpc(final URI restconfURI, final QName type,
- final OperationInput input) {
+ @NonNullByDefault
+ public RestconfFuture<FormattableBody> operationsGET(final ServerRequest request) {
+ return operations.httpGET(request);
+ }
+
+ @NonNullByDefault
+ public RestconfFuture<FormattableBody> operationsGET(final ServerRequest request, final ApiPath apiPath) {
+ return operations.httpGET(request, apiPath);
+ }
+
+ public @NonNull RestconfFuture<InvokeResult> operationsPOST(final ServerRequest request, final URI restconfURI,
+ final ApiPath apiPath, final OperationInputBody body) {
+ final Rpc path;
+ try {
+ path = pathNormalizer.normalizeRpcPath(apiPath);
+ } catch (RestconfDocumentedException e) {
+ return RestconfFuture.failed(e);
+ }
+
+ final ContainerNode data;
+ try {
+ data = body.toContainerNode(path);
+ } 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));
+ }
+
+ final var type = path.rpc().argument();
final var local = localRpcs.get(type);
if (local != null) {
- return local.invoke(restconfURI, input);
+ return local.invoke(restconfURI, new OperationInput(path, data))
+ .transform(output -> outputToInvokeResult(path, output));
}
if (rpcService == null) {
LOG.debug("RPC invocation is not available");
ErrorType.PROTOCOL, ErrorTag.OPERATION_NOT_SUPPORTED));
}
- final var ret = new SettableRestconfFuture<OperationOutput>();
- 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()));
- }
+ final var ret = new SettableRestconfFuture<InvokeResult>();
+ 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(outputToInvokeResult(path, 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())));
- }
+ @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());
+ }
+ }, MoreExecutors.directExecutor());
return ret;
}
+ private static @NonNull InvokeResult outputToInvokeResult(final @NonNull OperationPath path,
+ final @Nullable ContainerNode value) {
+ return value == null || value.isEmpty() ? InvokeResult.EMPTY
+ : new InvokeResult(new OperationOutputBody(path, value));
+ }
+
public @NonNull RestconfFuture<CharSource> resolveSource(final SourceIdentifier source,
- final Class<? extends SchemaSourceRepresentation> representation) {
+ final Class<? extends SourceRepresentation> representation) {
final var src = requireNonNull(source);
- if (YangTextSchemaSource.class.isAssignableFrom(representation)) {
+ if (YangTextSource.class.isAssignableFrom(representation)) {
if (sourceProvider != null) {
final var ret = new SettableRestconfFuture<CharSource>();
- Futures.addCallback(sourceProvider.getSource(src), new FutureCallback<YangTextSchemaSource>() {
+ Futures.addCallback(sourceProvider.getYangTexttSource(src), new FutureCallback<>() {
@Override
- public void onSuccess(final YangTextSchemaSource result) {
+ public void onSuccess(final YangTextSource result) {
ret.set(result);
}
}
return exportSource(modelContext(), src, YangCharSource::new, YangCharSource::new);
}
- if (YinTextSchemaSource.class.isAssignableFrom(representation)) {
+ if (YinTextSource.class.isAssignableFrom(representation)) {
return exportSource(modelContext(), src, YinCharSource.OfModule::new, YinCharSource.OfSubmodule::new);
}
return RestconfFuture.failed(new RestconfDocumentedException(
ErrorType.APPLICATION, ErrorTag.DATA_MISSING));
}
+ public final @NonNull RestconfFuture<? extends DataPostResult> dataPOST(final ServerRequest request,
+ final ApiPath apiPath, final DataPostBody body) {
+ if (apiPath.isEmpty()) {
+ return dataCreatePOST(request, body.toResource());
+ }
+ final InstanceReference path;
+ try {
+ path = pathNormalizer.normalizeDataOrActionPath(apiPath);
+ } catch (RestconfDocumentedException e) {
+ return RestconfFuture.failed(e);
+ }
+ if (path instanceof Data dataPath) {
+ try (var resourceBody = body.toResource()) {
+ return dataCreatePOST(request, dataPath, resourceBody);
+ }
+ }
+ if (path instanceof 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<CreateResourceResult> dataCreatePOST(final ServerRequest request,
+ final ChildBody body) {
+ return dataCreatePOST(request, new DatabindPath.Data(databind), body);
+ }
+
+ private @NonNull RestconfFuture<CreateResourceResult> dataCreatePOST(final ServerRequest request,
+ final DatabindPath.Data path, final ChildBody body) {
+ final Insert insert;
+ try {
+ insert = Insert.of(path.databind(), request.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<InvokeResult> dataInvokePOST(final @NonNull Action path,
+ final @NonNull OperationInputBody body) {
+ final ContainerNode input;
+ try {
+ input = body.toContainerNode(path);
+ } 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."));
+ }
+
+ return dataInvokePOST(actionService, path, input)
+ .transform(result -> outputToInvokeResult(path, result.getOutput().orElse(null)));
+ }
+
+ /**
+ * 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 Action path, final @NonNull ContainerNode input) {
+ final var ret = new SettableRestconfFuture<DOMActionResult>();
+
+ Futures.addCallback(actionService.invokeAction(
+ path.inference().toSchemaInferenceStack().toSchemaNodeIdentifier(),
+ DOMDataTreeIdentifier.of(LogicalDatastoreType.OPERATIONAL, path.instance()), input),
+ new FutureCallback<DOMActionResult>() {
+ @Override
+ public void onSuccess(final DOMActionResult result) {
+ final var errors = result.getErrors();
+ LOG.debug("InvokeAction Error Message {}", errors);
+ if (errors.isEmpty()) {
+ ret.set(result);
+ } else {
+ ret.setFailure(new RestconfDocumentedException("InvokeAction Error Message ", null, errors));
+ }
+ }
+
+ @Override
+ public void onFailure(final Throwable cause) {
+ if (cause instanceof DOMActionException) {
+ ret.set(new SimpleDOMActionResult(List.of(RpcResultBuilder.newError(
+ ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage()))));
+ } else if (cause instanceof RestconfDocumentedException e) {
+ ret.setFailure(e);
+ } else if (cause instanceof CancellationException) {
+ ret.setFailure(new RestconfDocumentedException("Action cancelled while executing",
+ ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, cause));
+ } else {
+ ret.setFailure(new RestconfDocumentedException("Invocation failed", cause));
+ }
+ }
+ }, MoreExecutors.directExecutor());
+
+ return ret;
+ }
}