+
+ /**
+ * 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