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;
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;
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;
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;
// 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();
}
/**
@Override
public void onFailure(final Throwable cause) {
- future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path));
+ future.setFailure(TransactionUtil.decodeException(cause, "MERGE", path, modelContext()));
}
}, MoreExecutors.directExecutor());
}
* @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;
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,
}
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) {
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) {
* @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();
} 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,
* @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();
}
}
- // 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,
}
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) {
* @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) {
* @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) {
};
// 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);
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));
+ }
+
}