import java.util.List;
import java.util.Set;
import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
public final class WriterParameters {
private final String content;
private final Integer depth;
private final List<Set<QName>> fields;
+ private final List<YangInstanceIdentifier> fieldPaths;
private final boolean prettyPrint;
private final boolean tagged;
private final String withDefault;
this.content = builder.content;
this.depth = builder.depth;
this.fields = builder.fields;
+ this.fieldPaths = builder.fieldPaths;
this.prettyPrint = builder.prettyPrint;
this.tagged = builder.tagged;
this.withDefault = builder.withDefault;
return this.fields;
}
+ public List<YangInstanceIdentifier> getFieldPaths() {
+ return this.fieldPaths;
+ }
+
public boolean isPrettyPrint() {
return this.prettyPrint;
}
private String content;
private Integer depth;
private List<Set<QName>> fields;
+ private List<YangInstanceIdentifier> fieldPaths;
private boolean prettyPrint;
private boolean tagged;
private String withDefault;
return this;
}
+ public WriterParametersBuilder setFieldPaths(final List<YangInstanceIdentifier> fieldPaths) {
+ this.fieldPaths = fieldPaths;
+ return this;
+ }
+
public WriterParametersBuilder setPrettyPrint(final boolean prettyPrint) {
this.prettyPrint = prettyPrint;
return this;
final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
final RestconfStrategy strategy = getRestconfStrategy(mountPoint);
- final NormalizedNode<?, ?> node = readData(identifier, parameters.getContent(),
- instanceIdentifier.getInstanceIdentifier(), strategy, parameters.getWithDefault(), schemaContextRef,
- uriInfo);
+ final NormalizedNode<?, ?> node;
+ if (parameters.getFieldPaths() != null && !parameters.getFieldPaths().isEmpty()) {
+ node = ReadDataTransactionUtil.readData(parameters.getContent(), instanceIdentifier.getInstanceIdentifier(),
+ strategy, parameters.getWithDefault(), schemaContextRef, parameters.getFieldPaths());
+ } else {
+ node = readData(identifier, parameters.getContent(), instanceIdentifier.getInstanceIdentifier(), strategy,
+ parameters.getWithDefault(), schemaContextRef, uriInfo);
+ }
if (identifier != null && identifier.contains(STREAM_PATH) && identifier.contains(STREAM_ACCESS_PATH_PART)
&& identifier.contains(STREAM_LOCATION_PATH_PART)) {
final String value = (String) node.getValue();
import static java.util.Objects.requireNonNull;
import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.List;
import java.util.Optional;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.mdsal.dom.api.DOMDataBroker;
}
}
+ @Override
+ public ListenableFuture<Optional<NormalizedNode<?, ?>>> read(final LogicalDatastoreType store,
+ final YangInstanceIdentifier path, final List<YangInstanceIdentifier> fields) {
+ return Futures.immediateFailedFuture(new UnsupportedOperationException(
+ "Reading of selected subtrees is currently not supported in: " + MdsalRestconfStrategy.class));
+ }
+
@Override
public FluentFuture<Boolean> exists(final LogicalDatastoreType store, final YangInstanceIdentifier path) {
try (DOMDataTreeReadTransaction tx = transactionChain.newReadOnlyTransaction()) {
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
+import java.util.List;
import java.util.Optional;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.mdsal.common.api.ReadFailedException;
}
}
+ @Override
+ public ListenableFuture<Optional<NormalizedNode<?, ?>>> read(final LogicalDatastoreType store,
+ final YangInstanceIdentifier path, final List<YangInstanceIdentifier> fields) {
+ switch (store) {
+ case CONFIGURATION:
+ return netconfService.getConfig(path, fields);
+ case OPERATIONAL:
+ return netconfService.get(path, fields);
+ default:
+ LOG.info("Unknown datastore type: {}.", store);
+ throw new IllegalArgumentException(String.format(
+ "%s, Cannot read data %s with fields %s for %s datastore, unknown datastore type",
+ netconfService.getDeviceId(), path, fields, store));
+ }
+ }
+
@Override
public FluentFuture<Boolean> exists(final LogicalDatastoreType store, final YangInstanceIdentifier path) {
return remapException(read(store, path))
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.List;
import java.util.Optional;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.mdsal.dom.api.DOMDataBroker;
public 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
+ */
+ public abstract ListenableFuture<Optional<NormalizedNode<?, ?>>> read(LogicalDatastoreType store,
+ YangInstanceIdentifier path, List<YangInstanceIdentifier> fields);
+
/**
* Check if data already exists in the datastore.
*
*/
package org.opendaylight.restconf.nb.rfc8040.rests.utils;
+import static org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserFieldsParameter.parseFieldsParameter;
+import static org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserFieldsParameter.parseFieldsPaths;
+
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
import org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfDataServiceConstant.ReadData.WithDefaults;
-import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserFieldsParameter;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.common.QNameModule;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
// check and set fields
if (!fields.isEmpty()) {
- builder.setFields(ParserFieldsParameter.parseFieldsParameter(identifier, fields.get(0)));
+ if (identifier.getMountPoint() != null) {
+ builder.setFieldPaths(parseFieldsPaths(identifier, fields.get(0)));
+ } else {
+ builder.setFields(parseFieldsParameter(identifier, fields.get(0)));
+ }
}
// check and set withDefaults parameter
}
}
+ /**
+ * 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 valueOfContent type of data to read (config, state, all)
+ * @param path the parent path to read
+ * @param strategy {@link RestconfStrategy} - object that perform the actual DS operations
+ * @param withDefa value of with-defaults parameter
+ * @param ctx schema context
+ * @param fields paths to selected subtrees which should be read, relative to to the parent path
+ * @return {@link NormalizedNode}
+ */
+ public static @Nullable NormalizedNode<?, ?> readData(final @NonNull String valueOfContent,
+ final @NonNull YangInstanceIdentifier path, final @NonNull RestconfStrategy strategy,
+ final @Nullable String withDefa, @NonNull final EffectiveModelContext ctx,
+ final @NonNull List<YangInstanceIdentifier> fields) {
+ switch (valueOfContent) {
+ case RestconfDataServiceConstant.ReadData.CONFIG:
+ if (withDefa == null) {
+ return readDataViaTransaction(strategy, LogicalDatastoreType.CONFIGURATION, path, true, fields);
+ } else {
+ return prepareDataByParamWithDef(
+ readDataViaTransaction(strategy, LogicalDatastoreType.CONFIGURATION, path, true, fields),
+ path, withDefa, ctx);
+ }
+ case RestconfDataServiceConstant.ReadData.NONCONFIG:
+ return readDataViaTransaction(strategy, LogicalDatastoreType.OPERATIONAL, path, true, fields);
+ case RestconfDataServiceConstant.ReadData.ALL:
+ return readAllData(strategy, path, withDefa, ctx, fields);
+ default:
+ strategy.close();
+ throw new RestconfDocumentedException(new RestconfError(RestconfError.ErrorType.PROTOCOL,
+ RestconfError.ErrorTag.INVALID_VALUE, "Invalid content parameter: " + valueOfContent, null,
+ "The content parameter value must be either config, nonconfig or all (default)"));
+ }
+ }
/**
* Check if URI does not contain value for the same parameter more than once.
static @Nullable NormalizedNode<?, ?> readDataViaTransaction(final @NonNull RestconfStrategy strategy,
final LogicalDatastoreType store, final YangInstanceIdentifier path,
final boolean closeTransactionChain) {
- final NormalizedNodeFactory dataFactory = new NormalizedNodeFactory();
final ListenableFuture<Optional<NormalizedNode<?, ?>>> listenableFuture = strategy.read(store, path);
+ return extractReadData(strategy, path, closeTransactionChain, listenableFuture);
+ }
+
+ /**
+ * Read specific type of data {@link LogicalDatastoreType} via transaction in {@link RestconfStrategy} with
+ * specified subtrees that should only be read.
+ *
+ * @param strategy {@link RestconfStrategy} - object that perform the actual DS operations
+ * @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 static @Nullable NormalizedNode<?, ?> readDataViaTransaction(final @NonNull RestconfStrategy strategy,
+ final @NonNull LogicalDatastoreType store, final @NonNull YangInstanceIdentifier path,
+ final boolean closeTransactionChain, final @NonNull List<YangInstanceIdentifier> fields) {
+ final ListenableFuture<Optional<NormalizedNode<?, ?>>> listenableFuture = strategy.read(store, path, fields);
+ return extractReadData(strategy, path, closeTransactionChain, listenableFuture);
+ }
+
+ private static NormalizedNode<?, ?> extractReadData(final RestconfStrategy strategy,
+ final YangInstanceIdentifier path, final boolean closeTransactionChain,
+ final ListenableFuture<Optional<NormalizedNode<?, ?>>> dataFuture) {
+ final NormalizedNodeFactory dataFactory = new NormalizedNodeFactory();
if (closeTransactionChain) {
//Method close transactionChain if any
- FutureCallbackTx.addCallback(listenableFuture, READ_TYPE_TX, dataFactory, strategy, path);
+ FutureCallbackTx.addCallback(dataFuture, READ_TYPE_TX, dataFactory, strategy, path);
} else {
- FutureCallbackTx.addCallback(listenableFuture, READ_TYPE_TX, dataFactory);
+ FutureCallbackTx.addCallback(dataFuture, READ_TYPE_TX, dataFactory);
}
return dataFactory.build();
}
path, withDefa, ctx);
}
+ return mergeConfigAndSTateDataIfNeeded(stateDataNode, configDataNode);
+ }
+
+ /**
+ * Read config and state data with selected subtrees that should only be read, then map them.
+ * Close {@link DOMTransactionChain} inside of object {@link RestconfStrategy} provided as a parameter.
+ *
+ * @param strategy {@link RestconfStrategy} - object that perform the actual DS operations
+ * @param path parent path to selected fields
+ * @param withDefa with-defaults parameter
+ * @param ctx schema context
+ * @param fields paths to selected subtrees which should be read, relative to to the parent path
+ * @return {@link NormalizedNode}
+ */
+ private static @Nullable NormalizedNode<?, ?> readAllData(final @NonNull RestconfStrategy strategy,
+ final @NonNull YangInstanceIdentifier path, final @Nullable String withDefa,
+ final @NonNull EffectiveModelContext ctx, final @NonNull List<YangInstanceIdentifier> fields) {
+ // PREPARE STATE DATA NODE
+ final NormalizedNode<?, ?> stateDataNode = readDataViaTransaction(
+ strategy, LogicalDatastoreType.OPERATIONAL, path, false, fields);
+
+ // PREPARE CONFIG DATA NODE
+ final NormalizedNode<?, ?> configDataNode;
+ //Here will be closed transactionChain if any
+ if (withDefa == null) {
+ configDataNode = readDataViaTransaction(strategy, LogicalDatastoreType.CONFIGURATION, path, true, fields);
+ } else {
+ configDataNode = prepareDataByParamWithDef(
+ readDataViaTransaction(strategy, LogicalDatastoreType.CONFIGURATION, path, true, fields),
+ path, withDefa, ctx);
+ }
+
+ return mergeConfigAndSTateDataIfNeeded(stateDataNode, configDataNode);
+ }
+
+ private static NormalizedNode<?, ?> mergeConfigAndSTateDataIfNeeded(final NormalizedNode<?, ?> stateDataNode,
+ final NormalizedNode<?, ?> configDataNode) {
// if no data exists
if (stateDataNode == null && configDataNode == null) {
return null;
*/
package org.opendaylight.restconf.nb.rfc8040.utils.parser;
+import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
import org.opendaylight.restconf.common.errors.RestconfError.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.PathArgument;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
-public final class ParserFieldsParameter {
- private ParserFieldsParameter() {
+/**
+ * Utilities used for parsing of fields query parameter content.
+ *
+ * @param <T> type of identifier
+ */
+public abstract class ParserFieldsParameter<T> {
+ private static final ParserFieldsParameter<QName> QNAME_PARSER = new QNameParser();
+ private static final ParserFieldsParameter<LinkedPathElement> PATH_PARSER = new PathParser();
+ private ParserFieldsParameter() {
}
/**
* Parse fields parameter and return complete list of child nodes organized into levels.
+ *
* @param identifier identifier context created from request URI
* @param input input value of fields parameter
- * @return {@link List}
+ * @return {@link List} of levels; each level contains set of {@link QName}
*/
public static @NonNull List<Set<QName>> parseFieldsParameter(final @NonNull InstanceIdentifierContext<?> identifier,
final @NonNull String input) {
- final List<Set<QName>> parsed = new ArrayList<>();
+ return QNAME_PARSER.parseFields(identifier, input);
+ }
+
+ /**
+ * Parse fields parameter and return list of child node paths saved in lists.
+ *
+ * @param identifier identifier context created from request URI
+ * @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}
+ */
+ public static @NonNull List<YangInstanceIdentifier> parseFieldsPaths(
+ final @NonNull InstanceIdentifierContext<?> identifier, final @NonNull String input) {
+ final List<Set<LinkedPathElement>> levels = PATH_PARSER.parseFields(identifier, input);
+ final List<Map<PathArgument, LinkedPathElement>> mappedLevels = mapLevelsContentByIdentifiers(levels);
+ return buildPaths(mappedLevels);
+ }
+
+ private static List<Map<PathArgument, LinkedPathElement>> mapLevelsContentByIdentifiers(
+ final List<Set<LinkedPathElement>> levels) {
+ // this step is used for saving some processing power - we can directly find LinkedPathElement using
+ // representing PathArgument
+ return levels.stream()
+ .map(linkedPathElements -> linkedPathElements.stream()
+ .map(linkedPathElement -> new SimpleEntry<>(linkedPathElement.targetNodeIdentifier,
+ linkedPathElement))
+ .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)))
+ .collect(Collectors.toList());
+ }
+
+ private static List<YangInstanceIdentifier> buildPaths(
+ final List<Map<PathArgument, LinkedPathElement>> mappedLevels) {
+ final List<YangInstanceIdentifier> completePaths = new ArrayList<>();
+ // we must traverse levels from the deepest level to the top level, because each LinkedPathElement is only
+ // linked to previous element
+ for (int levelIndex = mappedLevels.size() - 1; levelIndex >= 0; levelIndex--) {
+ // we go through unprocessed LinkedPathElements that represent leaves
+ for (final LinkedPathElement pathElement : mappedLevels.get(levelIndex).values()) {
+ if (pathElement.processed) {
+ // this element was already processed from the lower level - skip it
+ continue;
+ }
+ pathElement.processed = true;
+
+ // adding deepest path arguments, LinkedList is used for more effective insertion at the 0 index
+ final LinkedList<PathArgument> path = new LinkedList<>(pathElement.mixinNodesToTarget);
+ path.add(pathElement.targetNodeIdentifier);
+
+ PathArgument previousIdentifier = pathElement.previousNodeIdentifier;
+ // adding path arguments from the linked LinkedPathElements recursively
+ for (int buildingLevel = levelIndex - 1; buildingLevel >= 0; buildingLevel--) {
+ final LinkedPathElement previousElement = mappedLevels.get(buildingLevel).get(previousIdentifier);
+ path.addFirst(previousElement.targetNodeIdentifier);
+ path.addAll(0, previousElement.mixinNodesToTarget);
+ previousIdentifier = previousElement.previousNodeIdentifier;
+ previousElement.processed = true;
+ }
+ completePaths.add(YangInstanceIdentifier.create(path));
+ }
+ }
+ return completePaths;
+ }
+
+ /**
+ * Parse fields parameter and return complete list of child nodes organized into levels.
+ *
+ * @param identifier identifier context created from request URI
+ * @param input input value of fields parameter
+ * @return {@link List} of levels; each level contains {@link Set} of identifiers of type {@link T}
+ */
+ private @NonNull List<Set<T>> parseFields(final @NonNull InstanceIdentifierContext<?> identifier,
+ final @NonNull String input) {
+ final List<Set<T>> parsed = new ArrayList<>();
final SchemaContext context = identifier.getSchemaContext();
final QNameModule startQNameModule = identifier.getSchemaNode().getQName().getModule();
final DataSchemaContextNode<?> startNode = DataSchemaContextNode.fromDataSchemaNode(
/**
* Parse input value of fields parameter and create list of sets. Each set represents one level of child nodes.
+ *
* @param input input value of fields parameter
* @param startQNameModule starting qname module
* @param startNode starting node
* @param parsed list of results
* @param context schema context
*/
- private static void parseInput(final @NonNull String input, final @NonNull QNameModule startQNameModule,
- final @NonNull DataSchemaContextNode<?> startNode,
- final @NonNull List<Set<QName>> parsed, final SchemaContext context) {
+ private void parseInput(final @NonNull String input, final @NonNull QNameModule startQNameModule,
+ final @NonNull DataSchemaContextNode<?> startNode,
+ final @NonNull List<Set<T>> parsed, final SchemaContext context) {
int currentPosition = 0;
int startPosition = 0;
DataSchemaContextNode<?> currentNode = startNode;
QNameModule currentQNameModule = startQNameModule;
- Set<QName> currentLevel = new HashSet<>();
+ Set<T> currentLevel = new HashSet<>();
parsed.add(currentLevel);
DataSchemaContextNode<?> parenthesisNode = currentNode;
- Set<QName> parenthesisLevel = currentLevel;
+ Set<T> parenthesisLevel = currentLevel;
QNameModule parenthesisQNameModule = currentQNameModule;
while (currentPosition < input.length()) {
case '/':
// add parsed identifier to results for current level
currentNode = addChildToResult(currentNode, input.substring(startPosition, currentPosition),
- currentQNameModule, currentLevel);
+ currentQNameModule, currentLevel);
// go one level down
currentLevel = prepareQNameLevel(parsed, currentLevel);
}
/**
- * Preparation of the QName level that is used as storage for parsed QNames. If the current level exist at the
- * index that doesn't equal to the last index of already parsed QNames, a new level of QNames is allocated and
- * pushed to input parsed QNames.
+ * Preparation of the identifiers level that is used as storage for parsed identifiers. If the current level exist
+ * at the index that doesn't equal to the last index of already parsed identifiers, a new level of identifiers
+ * is allocated and pushed to input parsed identifiers.
*
- * @param parsedQNames Already parsed list of QNames grouped to multiple levels.
- * @param currentLevel Current level of QNames (set).
- * @return Existing or new level of QNames.
+ * @param parsedIdentifiers Already parsed list of identifiers grouped to multiple levels.
+ * @param currentLevel Current level of identifiers (set).
+ * @return Existing or new level of identifiers.
*/
- private static Set<QName> prepareQNameLevel(final List<Set<QName>> parsedQNames, final Set<QName> currentLevel) {
- final Optional<Set<QName>> existingLevel = parsedQNames.stream()
+ private Set<T> prepareQNameLevel(final List<Set<T>> parsedIdentifiers, final Set<T> currentLevel) {
+ final Optional<Set<T>> existingLevel = parsedIdentifiers.stream()
.filter(qNameSet -> qNameSet.equals(currentLevel))
.findAny();
if (existingLevel.isPresent()) {
- final int index = parsedQNames.indexOf(existingLevel.get());
- if (index == parsedQNames.size() - 1) {
- final Set<QName> nextLevel = new HashSet<>();
- parsedQNames.add(nextLevel);
+ final int index = parsedIdentifiers.indexOf(existingLevel.get());
+ if (index == parsedIdentifiers.size() - 1) {
+ final Set<T> nextLevel = new HashSet<>();
+ parsedIdentifiers.add(nextLevel);
return nextLevel;
}
- return parsedQNames.get(index + 1);
+ return parsedIdentifiers.get(index + 1);
}
- final Set<QName> nextLevel = new HashSet<>();
- parsedQNames.add(nextLevel);
+ final Set<T> nextLevel = new HashSet<>();
+ parsedIdentifiers.add(nextLevel);
return nextLevel;
}
- /**
- * Add parsed child of current node to result for current level.
- * @param currentNode current node
- * @param identifier parsed identifier of child node
- * @param currentQNameModule current namespace and revision in {@link QNameModule}
- * @param level current nodes level
- * @return {@link DataSchemaContextNode}
- */
- private static @NonNull DataSchemaContextNode<?> addChildToResult(
- final @NonNull DataSchemaContextNode<?> currentNode, final @NonNull String identifier,
- final @NonNull QNameModule currentQNameModule, final @NonNull Set<QName> level) {
- final QName childQName = QName.create(currentQNameModule, identifier);
-
- // resolve parent node
- final DataSchemaContextNode<?> parentNode = resolveMixinNode(
- currentNode, level, currentNode.getIdentifier().getNodeType());
- if (parentNode == null) {
- throw new RestconfDocumentedException(
- "Not-mixin node missing in " + currentNode.getIdentifier().getNodeType().getLocalName(),
- ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
- }
-
- // resolve child node
- final DataSchemaContextNode<?> childNode = resolveMixinNode(
- parentNode.getChild(childQName), level, childQName);
- if (childNode == null) {
- throw new RestconfDocumentedException(
- "Child " + identifier + " node missing in "
- + currentNode.getIdentifier().getNodeType().getLocalName(),
- ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
- }
-
- // add final childNode node to level nodes
- level.add(childNode.getIdentifier().getNodeType());
- return childNode;
- }
-
- /**
- * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
- * All nodes expect of not mixin node are added to current level nodes.
- * @param node initial mixin or not-mixin node
- * @param level current nodes level
- * @param qualifiedName qname of initial node
- * @return {@link DataSchemaContextNode}
- */
- private static @Nullable DataSchemaContextNode<?> resolveMixinNode(final @Nullable DataSchemaContextNode<?> node,
- final @NonNull Set<QName> level, final @NonNull QName qualifiedName) {
- DataSchemaContextNode<?> currentNode = node;
- while (currentNode != null && currentNode.isMixin()) {
- level.add(qualifiedName);
- currentNode = currentNode.getChild(qualifiedName);
- }
-
- return currentNode;
- }
-
/**
* Find position of matching parenthesis increased by one, but at most equals to input size.
+ *
* @param input input where to find for closing parenthesis
* @return int position of closing parenthesis increased by one
*/
- private static int findClosingParenthesis(final @Nullable String input) {
+ private static int findClosingParenthesis(final @NonNull String input) {
int position = 0;
int count = 1;
return ++position;
}
-}
+
+ /**
+ * Add parsed child of current node to result for current level.
+ *
+ * @param currentNode current node
+ * @param identifier parsed identifier of child node
+ * @param currentQNameModule current namespace and revision in {@link QNameModule}
+ * @param level current nodes level
+ * @return {@link DataSchemaContextNode}
+ */
+ abstract @NonNull DataSchemaContextNode<?> addChildToResult(@NonNull DataSchemaContextNode<?> currentNode,
+ @NonNull String identifier, @NonNull QNameModule currentQNameModule, @NonNull Set<T> level);
+
+ /**
+ * Fields parser that stores set of {@link QName}s in each level. Because of this fact, from the output
+ * it is is only possible to assume on what depth the selected element is placed. Identifiers of intermediary
+ * mixin nodes are also flatten to the same level as identifiers of data nodes.<br>
+ * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:<br>
+ * <pre>
+ * level 0: ['a', 'd']
+ * level 1: ['b', 'x', 'e']
+ * level 2: ['c']
+ * </pre>
+ */
+ private static final class QNameParser extends ParserFieldsParameter<QName> {
+ @Override
+ DataSchemaContextNode<?> addChildToResult(final DataSchemaContextNode<?> currentNode, final String identifier,
+ final QNameModule currentQNameModule, final Set<QName> level) {
+ final QName childQName = QName.create(currentQNameModule, identifier);
+
+ // resolve parent node
+ final DataSchemaContextNode<?> parentNode = resolveMixinNode(
+ currentNode, level, currentNode.getIdentifier().getNodeType());
+ if (parentNode == null) {
+ throw new RestconfDocumentedException(
+ "Not-mixin node missing in " + currentNode.getIdentifier().getNodeType().getLocalName(),
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ }
+
+ // resolve child node
+ final DataSchemaContextNode<?> childNode = resolveMixinNode(
+ parentNode.getChild(childQName), level, childQName);
+ if (childNode == null) {
+ throw new RestconfDocumentedException(
+ "Child " + identifier + " node missing in "
+ + currentNode.getIdentifier().getNodeType().getLocalName(),
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ }
+
+ // add final childNode node to level nodes
+ level.add(childNode.getIdentifier().getNodeType());
+ return childNode;
+ }
+
+ /**
+ * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
+ * All nodes expect of not mixin node are added to current level nodes.
+ *
+ * @param node initial mixin or not-mixin node
+ * @param level current nodes level
+ * @param qualifiedName qname of initial node
+ * @return {@link DataSchemaContextNode}
+ */
+ private static @Nullable DataSchemaContextNode<?> resolveMixinNode(
+ final @Nullable DataSchemaContextNode<?> node, final @NonNull Set<QName> level,
+ final @NonNull QName qualifiedName) {
+ DataSchemaContextNode<?> currentNode = node;
+ while (currentNode != null && currentNode.isMixin()) {
+ level.add(qualifiedName);
+ currentNode = currentNode.getChild(qualifiedName);
+ }
+
+ return currentNode;
+ }
+ }
+
+ /**
+ * Fields parser that stores set of {@link LinkedPathElement}s in each level. 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 previous element.<br>
+ * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:<br>
+ * <pre>
+ * level 0: ['./a', './d']
+ * level 1: ['a/b', '/d/x/e']
+ * level 2: ['b/c']
+ * </pre>
+ */
+ private static final class PathParser extends ParserFieldsParameter<LinkedPathElement> {
+ @Override
+ DataSchemaContextNode<?> addChildToResult(final DataSchemaContextNode<?> currentNode, final String identifier,
+ final QNameModule currentQNameModule, final Set<LinkedPathElement> level) {
+ final QName childQName = QName.create(currentQNameModule, identifier);
+ final List<PathArgument> collectedMixinNodes = new ArrayList<>();
+
+ DataSchemaContextNode<?> actualContextNode = currentNode.getChild(childQName);
+ while (actualContextNode != null && actualContextNode.isMixin()) {
+ if (actualContextNode.getDataSchemaNode() instanceof ListSchemaNode) {
+ // we need just a single node identifier from list in the path (key is not available)
+ actualContextNode = actualContextNode.getChild(childQName);
+ break;
+ } else if (actualContextNode.getDataSchemaNode() instanceof LeafListSchemaNode) {
+ // NodeWithValue is unusable - stop parsing
+ break;
+ } else {
+ collectedMixinNodes.add(actualContextNode.getIdentifier());
+ actualContextNode = actualContextNode.getChild(childQName);
+ }
+ }
+
+ if (actualContextNode == null) {
+ throw new RestconfDocumentedException("Child " + identifier + " node missing in "
+ + currentNode.getIdentifier().getNodeType().getLocalName(),
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+ }
+ final LinkedPathElement linkedPathElement = new LinkedPathElement(currentNode.getIdentifier(),
+ collectedMixinNodes, actualContextNode.getIdentifier());
+ level.add(linkedPathElement);
+ return actualContextNode;
+ }
+ }
+
+ /**
+ * {@link PathArgument} of data element grouped with identifiers of leading mixin nodes and previous node.<br>
+ * - identifiers of mixin nodes on the path to the target node - required for construction of full valid
+ * DOM paths,<br>
+ * - identifier of the previous non-mixin node - required to successfully create a chain of {@link PathArgument}s
+ */
+ private static final class LinkedPathElement {
+ private final PathArgument previousNodeIdentifier;
+ private final List<PathArgument> mixinNodesToTarget;
+ private final PathArgument targetNodeIdentifier;
+ private boolean processed = false;
+
+ /**
+ * Creation of new {@link LinkedPathElement}.
+ *
+ * @param previousNodeIdentifier identifier of the previous non-mixin node
+ * @param mixinNodesToTarget identifiers of mixin nodes on the path to the target node
+ * @param targetNodeIdentifier identifier of target non-mixin node
+ */
+ private LinkedPathElement(final PathArgument previousNodeIdentifier,
+ final List<PathArgument> mixinNodesToTarget, final PathArgument targetNodeIdentifier) {
+ this.previousNodeIdentifier = previousNodeIdentifier;
+ this.mixinNodesToTarget = mixinNodesToTarget;
+ this.targetNodeIdentifier = targetNodeIdentifier;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ // this is need in order to make 'prepareQNameLevel(..)' working
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ final LinkedPathElement that = (LinkedPathElement) obj;
+ return targetNodeIdentifier.equals(that.targetNodeIdentifier);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(targetNodeIdentifier);
+ }
+ }
+}
\ No newline at end of file
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import com.google.common.collect.Sets;
import java.net.URI;
+import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.common.QNameModule;
import org.opendaylight.yangtools.yang.common.Revision;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
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.test.util.YangParserTestUtils;
private static final QNameModule Q_NAME_MODULE_TEST_SERVICES = QNameModule.create(
URI.create("tests:test-services"),
Revision.of("2019-03-25"));
+ private static final QNameModule Q_NAME_MODULE_AUGMENTED_JUKEBOX = QNameModule.create(
+ URI.create("http://example.com/ns/augmented-jukebox"),
+ Revision.of("2016-05-05"));
// container jukebox
@Mock
// container augmented library
@Mock
private ContainerSchemaNode augmentedContainerLibrary;
- private static final QName AUGMENTED_LIBRARY_Q_NAME = QName.create(
- QNameModule.create(
- URI.create("http://example.com/ns/augmented-jukebox"),
- Revision.of("2016-05-05")),
+ private static final QName AUGMENTED_LIBRARY_Q_NAME = QName.create(Q_NAME_MODULE_AUGMENTED_JUKEBOX,
"augmented-library");
+ // augmentation that contains speed leaf
+ @Mock
+ private AugmentationSchemaNode speedAugmentation;
+
+ // leaf speed
+ @Mock
+ private LeafSchemaNode leafSpeed;
+ private static final QName SPEED_Q_NAME = QName.create(Q_NAME_MODULE_AUGMENTED_JUKEBOX, "speed");
+
// list album
@Mock
private ListSchemaNode listAlbum;
private LeafSchemaNode leafNextService;
private static final QName NEXT_SERVICE_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "next-service");
+ // leaf-list protocols
+ @Mock
+ private LeafListSchemaNode leafListProtocols;
+ private static final QName PROTOCOLS_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "protocols");
+
@Before
public void setUp() throws Exception {
final EffectiveModelContext schemaContextJukebox =
when(leafName.getQName()).thenReturn(NAME_Q_NAME);
when(listAlbum.dataChildByName(NAME_Q_NAME)).thenReturn(leafName);
+
+ when(leafSpeed.getQName()).thenReturn(SPEED_Q_NAME);
+ when(leafSpeed.isAugmenting()).thenReturn(true);
+ when(containerPlayer.dataChildByName(SPEED_Q_NAME)).thenReturn(leafSpeed);
+ when(containerPlayer.getDataChildByName(SPEED_Q_NAME)).thenReturn(leafSpeed);
+ doReturn(Collections.singletonList(leafSpeed)).when(speedAugmentation).getChildNodes();
+ doReturn(Collections.singleton(speedAugmentation)).when(containerPlayer).getAvailableAugmentations();
+ when(speedAugmentation.findDataChildByName(SPEED_Q_NAME)).thenReturn(Optional.of(leafSpeed));
}
private void initTestServicesSchemaNodes(final EffectiveModelContext schemaContext) {
when(listServices.getQName()).thenReturn(SERVICES_Q_NAME);
when(containerTestData.dataChildByName(SERVICES_Q_NAME)).thenReturn(listServices);
+ when(leafListProtocols.getQName()).thenReturn(PROTOCOLS_Q_NAME);
+ when(containerTestData.dataChildByName(PROTOCOLS_Q_NAME)).thenReturn(leafListProtocols);
+
when(leafTypeOfService.getQName()).thenReturn(TYPE_OF_SERVICE_Q_NAME);
when(listServices.dataChildByName(TYPE_OF_SERVICE_Q_NAME)).thenReturn(leafTypeOfService);
}
}
+ @Test
+ public void parseTopLevelContainerToPathTest() {
+ final String input = "library";
+ final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+ identifierJukebox, input);
+
+ assertNotNull(parsedFields);
+ assertEquals(1, parsedFields.size());
+ final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
+ assertEquals(1, pathArguments.size());
+ assertEquals(LIBRARY_Q_NAME, pathArguments.get(0).getNodeType());
+ }
+
+ @Test
+ public void parseTwoTopLevelContainersToPathsTest() {
+ final String input = "library;player";
+ final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+ identifierJukebox, input);
+
+ assertNotNull(parsedFields);
+ assertEquals(2, parsedFields.size());
+
+ final Optional<YangInstanceIdentifier> libraryPath = findPath(parsedFields, LIBRARY_Q_NAME);
+ assertTrue(libraryPath.isPresent());
+ assertEquals(1, libraryPath.get().getPathArguments().size());
+
+ final Optional<YangInstanceIdentifier> playerPath = findPath(parsedFields, PLAYER_Q_NAME);
+ assertTrue(playerPath.isPresent());
+ assertEquals(1, libraryPath.get().getPathArguments().size());
+ }
+
+ @Test
+ public void parseNestedLeafToPathTest() {
+ final String input = "library/album/name";
+ final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+ identifierJukebox, input);
+
+ assertEquals(1, parsedFields.size());
+ final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
+ assertEquals(3, pathArguments.size());
+
+ assertEquals(LIBRARY_Q_NAME, pathArguments.get(0).getNodeType());
+ assertEquals(ALBUM_Q_NAME, pathArguments.get(1).getNodeType());
+ assertEquals(NAME_Q_NAME, pathArguments.get(2).getNodeType());
+ }
+
+ @Test
+ public void parseAugmentedLeafToPathTest() {
+ final String input = "player/augmented-jukebox:speed";
+ final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+ identifierJukebox, input);
+
+ assertEquals(1, parsedFields.size());
+ final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
+
+ assertEquals(3, pathArguments.size());
+ assertEquals(PLAYER_Q_NAME, pathArguments.get(0).getNodeType());
+ assertTrue(pathArguments.get(1) instanceof AugmentationIdentifier);
+ assertEquals(SPEED_Q_NAME, pathArguments.get(2).getNodeType());
+ }
+
+ @Test
+ public void parseMultipleFieldsOnDifferentLevelsToPathsTest() {
+ final String input = "services(type-of-service;instance/instance-name;instance/provider)";
+ final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+ identifierTestServices, input);
+
+ assertEquals(3, parsedFields.size());
+
+ final Optional<YangInstanceIdentifier> tosPath = findPath(parsedFields, TYPE_OF_SERVICE_Q_NAME);
+ assertTrue(tosPath.isPresent());
+ assertEquals(2, tosPath.get().getPathArguments().size());
+
+ final Optional<YangInstanceIdentifier> instanceNamePath = findPath(parsedFields, INSTANCE_NAME_Q_NAME);
+ assertTrue(instanceNamePath.isPresent());
+ assertEquals(3, instanceNamePath.get().getPathArguments().size());
+
+ final Optional<YangInstanceIdentifier> providerPath = findPath(parsedFields, PROVIDER_Q_NAME);
+ assertTrue(providerPath.isPresent());
+ assertEquals(3, providerPath.get().getPathArguments().size());
+ }
+
+ @Test
+ public void parseListFieldUnderListToPathTest() {
+ final String input = "services/instance";
+ final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+ identifierTestServices, input);
+
+ assertEquals(1, parsedFields.size());
+ final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
+ assertEquals(2, pathArguments.size());
+
+ assertEquals(SERVICES_Q_NAME, pathArguments.get(0).getNodeType());
+ assertTrue(pathArguments.get(0) instanceof NodeIdentifier);
+ assertEquals(INSTANCE_Q_NAME, pathArguments.get(1).getNodeType());
+ assertTrue(pathArguments.get(1) instanceof NodeIdentifier);
+ }
+
+ @Test
+ public void parseLeafListFieldToPathTest() {
+ final String input = "protocols";
+ final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+ identifierTestServices, input);
+ assertEquals(1, parsedFields.size());
+ final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
+ assertEquals(1, pathArguments.size());
+ assertTrue(pathArguments.get(0) instanceof NodeIdentifier);
+ assertEquals(PROTOCOLS_Q_NAME, pathArguments.get(0).getNodeType());
+ }
+
+ private static Optional<YangInstanceIdentifier> findPath(final List<YangInstanceIdentifier> paths,
+ final QName lastPathArg) {
+ return paths.stream()
+ .filter(path -> lastPathArg.equals(path.getLastPathArgument().getNodeType()))
+ .findAny();
+ }
}
\ No newline at end of file
container augmented-library {
}
}
+
+ augment "/jbox:jukebox/jbox:player" {
+ leaf speed {
+ type uint8;
+ }
+ }
}
\ No newline at end of file
}
}
}
+
+ leaf-list protocols {
+ type string;
+ }
}
}
\ No newline at end of file