import static java.util.Objects.requireNonNull;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
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 com.google.common.util.concurrent.SettableFuture;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.mdsal.common.api.CommitInfo;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.mdsal.common.api.ReadFailedException;
+import org.opendaylight.mdsal.dom.api.DOMActionService;
+import org.opendaylight.mdsal.dom.api.DOMMountPointService;
import org.opendaylight.mdsal.dom.api.DOMRpcService;
-import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
+import org.opendaylight.mdsal.dom.api.DOMSchemaService.YangTextSourceExtension;
+import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
+import org.opendaylight.restconf.api.query.ContentParam;
+import org.opendaylight.restconf.api.query.FieldsParam;
+import org.opendaylight.restconf.api.query.FieldsParam.NodeSelector;
+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.server.api.DataGetParams;
+import org.opendaylight.restconf.server.api.DataGetResult;
+import org.opendaylight.restconf.server.api.DatabindContext;
+import org.opendaylight.restconf.server.api.DatabindPath.Data;
+import org.opendaylight.restconf.server.api.ServerRequest;
+import org.opendaylight.restconf.server.spi.NormalizedNodeWriterFactory;
import org.opendaylight.yangtools.yang.common.Empty;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+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.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
+import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
/**
* Implementation of RESTCONF operations on top of a raw NETCONF backend.
public final class NetconfRestconfStrategy extends RestconfStrategy {
private final NetconfDataTreeService netconfService;
- public NetconfRestconfStrategy(final EffectiveModelContext modelContext,
- final NetconfDataTreeService netconfService, final @Nullable DOMRpcService rpcService,
- final @Nullable DOMYangTextSourceProvider sourceProvider) {
- super(modelContext, ImmutableMap.of(), rpcService, sourceProvider);
+ public NetconfRestconfStrategy(final DatabindContext databind, final NetconfDataTreeService netconfService,
+ final @Nullable DOMRpcService rpcService, final @Nullable DOMActionService actionService,
+ final @Nullable YangTextSourceExtension sourceProvider,
+ final @Nullable DOMMountPointService mountPointService) {
+ super(databind, ImmutableMap.of(), rpcService, actionService, sourceProvider, mountPointService);
this.netconfService = requireNonNull(netconfService);
}
@Override
- void delete(final SettableRestconfFuture<Empty> future, final YangInstanceIdentifier path) {
+ RestconfTransaction prepareWriteExecution() {
+ return new NetconfRestconfTransaction(modelContext(), netconfService);
+ }
+
+ @Override
+ void delete(final SettableRestconfFuture<Empty> future, final ServerRequest request,
+ final YangInstanceIdentifier path) {
final var tx = prepareWriteExecution();
tx.delete(path);
Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
}
@Override
- RestconfTransaction prepareWriteExecution() {
- return new NetconfRestconfTransaction(modelContext(), netconfService);
+ RestconfFuture<DataGetResult> dataGET(final ServerRequest request, final Data path, final DataGetParams params) {
+ final var fields = params.fields();
+ final List<YangInstanceIdentifier> fieldPaths;
+ if (fields != null) {
+ final List<YangInstanceIdentifier> tmp;
+ try {
+ tmp = fieldsParamToPaths(path.inference().modelContext(), path.schema(), fields);
+ } catch (RestconfDocumentedException e) {
+ return RestconfFuture.failed(e);
+ }
+ fieldPaths = tmp.isEmpty() ? null : tmp;
+ } else {
+ fieldPaths = null;
+ }
+
+ final NormalizedNode node;
+ if (fieldPaths != null) {
+ node = readData(params.content(), path.instance(), params.withDefaults(), fieldPaths);
+ } else {
+ node = readData(params.content(), path.instance(), params.withDefaults());
+ }
+ return completeDataGET(node, path, NormalizedNodeWriterFactory.of(params.depth()), null);
}
@Override
};
}
- @Override
- ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
+ private ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
final YangInstanceIdentifier path, final List<YangInstanceIdentifier> fields) {
return switch (store) {
case CONFIGURATION -> netconfService.getConfig(path, fields);
};
}
+ /**
+ * 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 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);
+ };
+ }
+
+ /**
+ * 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 fields paths to selected subtrees which should be read, relative 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);
+ }
+
@Override
ListenableFuture<Boolean> exists(final YangInstanceIdentifier path) {
return Futures.transform(remapException(netconfService.getConfig(path)),
private static <T> ListenableFuture<T> remapException(final ListenableFuture<T> input) {
final var ret = SettableFuture.<T>create();
- Futures.addCallback(input, new FutureCallback<T>() {
+ Futures.addCallback(input, new FutureCallback<>() {
@Override
public void onSuccess(final T result) {
ret.set(result);
}, MoreExecutors.directExecutor());
return ret;
}
+
+ /**
+ * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with
+ * {@link NetconfDataTreeService}.
+ *
+ * <p>
+ * Fields parser that stores a set of all the leaf {@link LinkedPathElement}s specified in {@link FieldsParam}.
+ * Using {@link LinkedPathElement} it is possible to create a chain of path arguments and build complete paths
+ * since this element contains identifiers of intermediary mixin nodes and also linked to its parent
+ * {@link LinkedPathElement}.
+ *
+ * <p>
+ * Example: field 'a(b/c;d/e)' ('e' is place under choice node 'x') is parsed into following levels:
+ * <pre>
+ * - './a' +- 'a/b' - 'b/c'
+ * |
+ * +- 'a/d' - 'd/x/e'
+ * </pre>
+ *
+ *
+ * @param modelContext EffectiveModelContext
+ * @param startNode Root DataSchemaNode
+ * @param input input value of fields parameter
+ * @return {@link List} of {@link YangInstanceIdentifier} that are relative to the last {@link PathArgument}
+ * of provided {@code identifier}
+ */
+ @VisibleForTesting
+ static @NonNull List<YangInstanceIdentifier> fieldsParamToPaths(final @NonNull EffectiveModelContext modelContext,
+ final @NonNull DataSchemaContext startNode, final @NonNull FieldsParam input) {
+ final var parsed = new HashSet<LinkedPathElement>();
+ processSelectors(parsed, modelContext, startNode.dataSchemaNode().getQName().getModule(),
+ new LinkedPathElement(null, List.of(), startNode), input.nodeSelectors());
+ return parsed.stream().map(NetconfRestconfStrategy::buildPath).toList();
+ }
+
+ private static void processSelectors(final Set<LinkedPathElement> parsed, final EffectiveModelContext context,
+ final QNameModule startNamespace, final LinkedPathElement startPathElement,
+ final List<NodeSelector> selectors) {
+ for (var selector : selectors) {
+ var pathElement = startPathElement;
+ var namespace = startNamespace;
+
+ // Note: path is guaranteed to have at least one step
+ final var it = selector.path().iterator();
+ do {
+ final var step = it.next();
+ final var module = step.module();
+ if (module != null) {
+ // FIXME: this is not defensive enough, as we can fail to find the module
+ namespace = context.findModules(module).iterator().next().getQNameModule();
+ }
+
+ // add parsed path element linked to its parent
+ pathElement = addChildPathElement(pathElement, step.identifier().bindTo(namespace));
+ } while (it.hasNext());
+
+ final var subs = selector.subSelectors();
+ if (!subs.isEmpty()) {
+ processSelectors(parsed, context, namespace, pathElement, subs);
+ } else {
+ parsed.add(pathElement);
+ }
+ }
+ }
+
+ private static LinkedPathElement addChildPathElement(final LinkedPathElement currentElement,
+ final QName childQName) {
+ final var collectedMixinNodes = new ArrayList<PathArgument>();
+
+ DataSchemaContext currentNode = currentElement.targetNode;
+ DataSchemaContext actualContextNode = childByQName(currentNode, childQName);
+ if (actualContextNode == null) {
+ actualContextNode = resolveMixinNode(currentNode, currentNode.getPathStep().getNodeType());
+ actualContextNode = childByQName(actualContextNode, childQName);
+ }
+
+ while (actualContextNode != null && actualContextNode instanceof PathMixin) {
+ final var actualDataSchemaNode = actualContextNode.dataSchemaNode();
+ if (actualDataSchemaNode instanceof ListSchemaNode listSchema && listSchema.getKeyDefinition().isEmpty()) {
+ // we need just a single node identifier from list in the path IFF it is an unkeyed list, otherwise
+ // we need both (which is the default case)
+ actualContextNode = childByQName(actualContextNode, childQName);
+ } else if (actualDataSchemaNode instanceof LeafListSchemaNode) {
+ // NodeWithValue is unusable - stop parsing
+ break;
+ } else {
+ collectedMixinNodes.add(actualContextNode.getPathStep());
+ actualContextNode = childByQName(actualContextNode, childQName);
+ }
+ }
+
+ if (actualContextNode == null) {
+ throw new RestconfDocumentedException("Child " + childQName.getLocalName() + " node missing in "
+ + currentNode.getPathStep().getNodeType().getLocalName(),
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ }
+
+ return new LinkedPathElement(currentElement, collectedMixinNodes, actualContextNode);
+ }
+
+ private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) {
+ return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null;
+ }
+
+ private static YangInstanceIdentifier buildPath(final LinkedPathElement lastPathElement) {
+ var pathElement = lastPathElement;
+ final var path = new LinkedList<PathArgument>();
+ do {
+ path.addFirst(contextPathArgument(pathElement.targetNode));
+ path.addAll(0, pathElement.mixinNodesToTarget);
+ pathElement = pathElement.parentPathElement;
+ } while (pathElement.parentPathElement != null);
+
+ return YangInstanceIdentifier.of(path);
+ }
+
+ private static @NonNull PathArgument contextPathArgument(final DataSchemaContext context) {
+ final var arg = context.pathStep();
+ if (arg != null) {
+ return arg;
+ }
+
+ final var schema = context.dataSchemaNode();
+ if (schema instanceof ListSchemaNode listSchema && !listSchema.getKeyDefinition().isEmpty()) {
+ return NodeIdentifierWithPredicates.of(listSchema.getQName());
+ }
+ if (schema instanceof LeafListSchemaNode leafListSchema) {
+ return new NodeWithValue<>(leafListSchema.getQName(), Empty.value());
+ }
+ throw new UnsupportedOperationException("Unsupported schema " + schema);
+ }
+
+ private static DataSchemaContext resolveMixinNode(final DataSchemaContext node,
+ final @NonNull QName qualifiedName) {
+ DataSchemaContext currentNode = node;
+ while (currentNode != null && currentNode instanceof PathMixin currentMixin) {
+ currentNode = currentMixin.childByQName(qualifiedName);
+ }
+ return currentNode;
+ }
+
+ /**
+ * {@link DataSchemaContext} of data element grouped with identifiers of leading mixin nodes and previous path
+ * element.<br>
+ * - identifiers of mixin nodes on the path to the target node - required for construction of full valid
+ * DOM paths,<br>
+ * - {@link LinkedPathElement} of the previous non-mixin node - required to successfully create a chain
+ * of {@link PathArgument}s
+ *
+ * @param parentPathElement parent path element
+ * @param mixinNodesToTarget identifiers of mixin nodes on the path to the target node
+ * @param targetNode target non-mixin node
+ */
+ private record LinkedPathElement(
+ @Nullable LinkedPathElement parentPathElement,
+ @NonNull List<PathArgument> mixinNodesToTarget,
+ @NonNull DataSchemaContext targetNode) {
+ LinkedPathElement {
+ requireNonNull(mixinNodesToTarget);
+ requireNonNull(targetNode);
+ }
+ }
}