Improve NormalizedNode formatting capabilities 59/95459/30
authorKonstantin.Nosach <Kostiantyn.Nosach@pantheon.tech>
Fri, 12 Feb 2021 12:31:57 +0000 (14:31 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Sun, 25 Apr 2021 07:46:54 +0000 (09:46 +0200)
Add NormalizedNode.prettyTree(), which acts as a conduit to a
human-readable String representing the tree. The tree is hidden behind a
toString()/Supplier<String>, so it is usable with logging.

Since this is a generally-useful concept, which can span different kinds
of structures, capture it as PrettyTree/PrettyTreeAware as well.

JIRA: YANGTOOLS-1203
Change-Id: I6f2c98bc3048a086063846699ec73e0f2ac02b33
Signed-off-by: Kostiantyn Nosach <kostiantyn.nosach@pantheon.tech>
Signed-off-by: Ivan Hrasko <ivan.hrasko@pantheon.tech>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
14 files changed:
common/concepts/src/main/java/org/opendaylight/yangtools/concepts/PrettyTree.java [new file with mode: 0644]
common/concepts/src/main/java/org/opendaylight/yangtools/concepts/PrettyTreeAware.java [new file with mode: 0644]
common/concepts/src/main/java/org/opendaylight/yangtools/concepts/PrettyTreeIndent.java [new file with mode: 0644]
data/yang-data-spi/src/main/java/module-info.java
data/yang-data-spi/src/main/java/org/opendaylight/yangtools/yang/data/spi/node/AbstractNormalizedNode.java
data/yang-data-spi/src/main/java/org/opendaylight/yangtools/yang/data/spi/node/NormalizedNodePrettyTree.java [new file with mode: 0644]
yang/rfc8528-data-util/pom.xml
yang/rfc8528-data-util/src/main/java/module-info.java
yang/rfc8528-data-util/src/main/java/org/opendaylight/yangtools/rfc8528/data/util/ImmutableMountPointNode.java
yang/yang-data-api/src/main/java/org/opendaylight/yangtools/yang/data/api/schema/NormalizedNode.java
yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AbstractPrettyTreeTest.java [new file with mode: 0644]
yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/NormalizedNodePrettyTreeTest.java [new file with mode: 0644]
yang/yang-data-impl/src/test/resources/pretty-print/another.yang [new file with mode: 0644]
yang/yang-data-impl/src/test/resources/pretty-print/test.yang [new file with mode: 0644]

diff --git a/common/concepts/src/main/java/org/opendaylight/yangtools/concepts/PrettyTree.java b/common/concepts/src/main/java/org/opendaylight/yangtools/concepts/PrettyTree.java
new file mode 100644 (file)
index 0000000..f9dbe59
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.concepts;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.annotations.Beta;
+import java.util.function.Supplier;
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * A capture of a tree-like construct, which can be formatted into a pretty-printed tree. The string can be acquired
+ * via {@link #get()}.
+ *
+ * <p>
+ * This concept is purposefully designed as an abstract class which defers its {@link #toString()} to {@link #get()}, as
+ * it allows convenient and light-weight use with logging:
+ *
+ * <pre>
+ *   <code>
+ *     PrettyTreeAware treeLike;
+ *     LOG.debug("Tree is {}", treeLike.prettyTree());
+ *   </code>
+ * </pre>
+ */
+@Beta
+public abstract class PrettyTree implements Supplier<String> {
+    @Override
+    public @NonNull String get() {
+        final StringBuilder sb = new StringBuilder();
+        appendTo(sb, 0);
+        return sb.toString();
+    }
+
+    @Override
+    public final @NonNull String toString() {
+        return get();
+    }
+
+    /**
+     * Format this object into specified {@link StringBuilder} starting at specified initial depth.
+     *
+     * @param sb Target {@link StringBuilder}
+     * @param depth Initial nesting depth
+     * @throws NullPointerException if {@code sb} is null
+     * @throws IllegalArgumentException if {@code depth} is negative
+     */
+    public abstract void appendTo(StringBuilder sb, int depth);
+
+    /**
+     * Append a number of spaces equivalent to specified tree nesting depth into the specified {@link StringBuilder}.
+     *
+     * @param sb Target {@link StringBuilder}
+     * @param depth Nesting depth
+     * @throws NullPointerException if {@code sb} is null
+     * @throws IllegalArgumentException if {@code depth} is negative
+     */
+    protected static final void appendIndent(final StringBuilder sb, final int depth) {
+        checkArgument(depth >= 0, "Invalid depth %s", depth);
+        PrettyTreeIndent.indent(sb, depth);
+    }
+}
diff --git a/common/concepts/src/main/java/org/opendaylight/yangtools/concepts/PrettyTreeAware.java b/common/concepts/src/main/java/org/opendaylight/yangtools/concepts/PrettyTreeAware.java
new file mode 100644 (file)
index 0000000..7c99453
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.concepts;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utility interface to bridge structures which can be formatted via {@link PrettyTree}.
+ */
+@NonNullByDefault
+public interface PrettyTreeAware {
+    /**
+     * Return a {@link PrettyTree} view of this object.
+     *
+     * @return A {@link PrettyTree}.
+     */
+    PrettyTree prettyTree();
+}
diff --git a/common/concepts/src/main/java/org/opendaylight/yangtools/concepts/PrettyTreeIndent.java b/common/concepts/src/main/java/org/opendaylight/yangtools/concepts/PrettyTreeIndent.java
new file mode 100644 (file)
index 0000000..2cfaa62
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.concepts;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Indentation handling for {@link PrettyTree}. This class is split out to defer initialization of the string table --
+ * it might never be used after all.
+ *
+ * <p>
+ * We want to be formatting strings quickly and a lot of that can very easily be dominated by dealing with indents.
+ * To deal with that we pre-compute a few indentation strings and then append them directly using a specialized
+ * method. We allow tuning the default indentation at runtime, but choose a fixed string table size.
+ */
+final class PrettyTreeIndent {
+    private static final Logger LOG = LoggerFactory.getLogger(PrettyTreeIndent.class);
+    private static final int DEFAULT_INDENT = 4;
+    private static final int INDENT_STRINGS_SIZE = 16;
+    private static final String[] INDENT_STRINGS;
+
+    static {
+        int indent = Integer.getInteger("org.opendaylight.yangtools.concepts.pretty-tree-indent", DEFAULT_INDENT);
+        if (indent < 1) {
+            LOG.warn("Invalid pretty-tree-indent {}, using {} instead", indent, DEFAULT_INDENT);
+            indent = DEFAULT_INDENT;
+        } else if (indent != DEFAULT_INDENT) {
+            LOG.info("Using pretty-tree-indent {}", indent);
+        }
+
+        final String one = " ".repeat(indent);
+        final String[] strings = new String[INDENT_STRINGS_SIZE];
+        for (int i = 0; i < INDENT_STRINGS_SIZE; i++) {
+            strings[i] = one.repeat(i).intern();
+        }
+        INDENT_STRINGS = strings;
+    }
+
+    private PrettyTreeIndent() {
+        // Hidden on purpose
+    }
+
+    static void indent(final StringBuilder sb, final int depth) {
+        int remaining = depth;
+        while (remaining >= INDENT_STRINGS_SIZE) {
+            sb.append(INDENT_STRINGS[INDENT_STRINGS_SIZE - 1]);
+            remaining -= INDENT_STRINGS_SIZE;
+        }
+        sb.append(INDENT_STRINGS[remaining]);
+    }
+}
index 35a9c20ecb1d28a48d071a1e94084bd438c68998..189a2267ef2bb2b1201680ef7181b502a070f995 100644 (file)
@@ -13,6 +13,7 @@ module org.opendaylight.yangtools.yang.data.spi {
     requires transitive org.opendaylight.yangtools.yang.data.api;
     requires transitive org.opendaylight.yangtools.concepts;
     requires org.opendaylight.yangtools.util;
+    requires org.opendaylight.yangtools.yang.common;
     requires org.slf4j;
 
     // Annotations
index 571573142d8167bbfb09ce180343239925400471..0796b868b3acc6c6944a2374d1ecc8d2879bf4fe 100644 (file)
@@ -13,6 +13,7 @@ import com.google.common.base.MoreObjects.ToStringHelper;
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.concepts.AbstractIdentifiable;
 import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.concepts.PrettyTree;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 
@@ -29,6 +30,11 @@ public abstract class AbstractNormalizedNode<I extends PathArgument, T extends N
         super(identifier);
     }
 
+    @Override
+    public final PrettyTree prettyTree() {
+        return new NormalizedNodePrettyTree(this);
+    }
+
     @Override
     public final boolean equals(final Object obj) {
         if (this == obj) {
diff --git a/data/yang-data-spi/src/main/java/org/opendaylight/yangtools/yang/data/spi/node/NormalizedNodePrettyTree.java b/data/yang-data-spi/src/main/java/org/opendaylight/yangtools/yang/data/spi/node/NormalizedNodePrettyTree.java
new file mode 100644 (file)
index 0000000..4bf3a1a
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2021 PANTHEON.tech s.r.o. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.spi.node;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import java.util.Base64;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Optional;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.concepts.PrettyTree;
+import org.opendaylight.yangtools.concepts.PrettyTreeAware;
+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.schema.AugmentationNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ForeignDataNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
+import org.opendaylight.yangtools.yang.data.api.schema.ValueNode;
+
+@Beta
+public final class NormalizedNodePrettyTree extends PrettyTree implements Immutable {
+    private final @NonNull NormalizedNode node;
+
+    public NormalizedNodePrettyTree(final @NonNull NormalizedNode node) {
+        this.node = requireNonNull(node);
+    }
+
+    @Override
+    public void appendTo(final StringBuilder sb, final int depth) {
+        appendNode(sb, depth, null, node);
+    }
+
+    private static void appendNode(final StringBuilder sb, final int depth, final QNameModule parentNamespace,
+            final NormalizedNode node) {
+        final String simpleName = node.contract().getSimpleName();
+        appendIndent(sb, depth);
+        sb.append(simpleName.toLowerCase(Locale.ROOT).charAt(0)).append(simpleName, 1, simpleName.length()).append(' ');
+
+        final QNameModule currentNamespace;
+        if (node instanceof AugmentationNode) {
+            // Add identifier, but augmentations are special enough
+            currentNamespace = ((AugmentationNode) node).getIdentifier().getPossibleChildNames().iterator().next()
+                .getModule();
+            if (appendNamespace(sb, parentNamespace, currentNamespace)) {
+                sb.append(' ');
+            }
+        } else {
+            final QName qname = node.getIdentifier().getNodeType();
+            currentNamespace = qname.getModule();
+            appendNamespace(sb, parentNamespace, currentNamespace);
+            sb.append(qname.getLocalName()).append(' ');
+        }
+
+        if (node instanceof NormalizedNodeContainer) {
+            final NormalizedNodeContainer<?> container = (NormalizedNodeContainer<?>) node;
+            sb.append("= {");
+
+            final Iterator<? extends NormalizedNode> it = container.body().iterator();
+            if (it.hasNext()) {
+                final int childIndent = depth + 1;
+                do {
+                    sb.append('\n');
+                    appendNode(sb, childIndent, currentNamespace, it.next());
+                } while (it.hasNext());
+
+                sb.append('\n');
+                appendIndent(sb, depth);
+            }
+            sb.append('}');
+        } else if (node instanceof ValueNode) {
+            sb.append("= ");
+            final Object value = node.body();
+            if (value instanceof byte[]) {
+                sb.append("(byte[])").append(Base64.getEncoder().encodeToString((byte[]) value));
+            } else if (value instanceof String) {
+                appendString(sb, (String) value);
+            } else {
+                sb.append(value);
+            }
+        } else if (node instanceof ForeignDataNode) {
+            final ForeignDataNode<?> data = (ForeignDataNode<?>) node;
+            sb.append("= (").append(data.bodyObjectModel().getName()).append(')');
+
+            final Object body = data.body();
+            if (body instanceof PrettyTreeAware) {
+                sb.append(" {\n");
+                ((PrettyTreeAware) body).prettyTree().appendTo(sb, depth + 1);
+                appendIndent(sb, depth);
+                sb.append('}');
+            }
+        } else {
+            throw new IllegalStateException("Unhandled node " + node);
+        }
+    }
+
+    private static boolean appendNamespace(final StringBuilder sb, final QNameModule parent,
+            final QNameModule current) {
+        if (!current.equals(parent)) {
+            sb.append('(').append(current.getNamespace());
+            final Optional<Revision> rev = current.getRevision();
+            if (rev.isPresent()) {
+                sb.append('@').append(rev.orElseThrow());
+            }
+            sb.append(')');
+            return true;
+        }
+        return false;
+    }
+
+    private static void appendString(final StringBuilder sb, final String str) {
+        // TODO: do some escaping: '\r' '\n' '"' '\\' to make things even more zazzy
+        sb.append('"').append(str).append('"');
+    }
+}
index 4a0a5d193fd8431cdf33f9d7b82083171ddb8e97..331b8a2b87196994d6196b90c5f5a4cf426e8fff 100644 (file)
             <groupId>org.opendaylight.yangtools</groupId>
             <artifactId>yang-data-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.opendaylight.yangtools</groupId>
+            <artifactId>yang-data-spi</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.opendaylight.yangtools</groupId>
             <artifactId>yang-model-api</artifactId>
index 38d6d6c405b82ad858bcf17b0d1a847c469f61be..77de84111bd1143bacb7a59ac8421960fde211e9 100644 (file)
@@ -20,6 +20,7 @@ module org.opendaylight.yangtools.rfc8528.data.util {
 
     requires org.opendaylight.yangtools.rfc8528.model.api;
     requires org.opendaylight.yangtools.yang.common;
+    requires org.opendaylight.yangtools.yang.data.spi;
     requires org.slf4j;
 
     // Annotations
index fbdf318e92ad1bcc09ed2a5e08e6f9c1b13caac6..3b937644d37e865e4df3c2b4a86cf773f409363a 100644 (file)
@@ -15,12 +15,14 @@ import java.util.Collection;
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.concepts.AbstractIdentifiable;
 import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.concepts.PrettyTree;
 import org.opendaylight.yangtools.rfc8528.data.api.MountPointContext;
 import org.opendaylight.yangtools.rfc8528.data.api.MountPointIdentifier;
 import org.opendaylight.yangtools.rfc8528.data.api.MountPointNode;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.spi.node.NormalizedNodePrettyTree;
 
 @Beta
 public final class ImmutableMountPointNode extends AbstractIdentifiable<PathArgument, MountPointIdentifier>
@@ -55,6 +57,11 @@ public final class ImmutableMountPointNode extends AbstractIdentifiable<PathArgu
         return delegate.childByArg(child);
     }
 
+    @Override
+    public PrettyTree prettyTree() {
+        return new NormalizedNodePrettyTree(this);
+    }
+
     @Override
     protected ToStringHelper addToStringAttributes(final ToStringHelper helper) {
         return super.addToStringAttributes(helper).add("delegate", delegate);
index 957c7b135e46e2a9a4660eba437321af9c297276..348d4dbb0c0b20442ec8e357314ae29f6e7a04d9 100644 (file)
@@ -9,6 +9,7 @@ package org.opendaylight.yangtools.yang.data.api.schema;
 
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.concepts.Identifiable;
+import org.opendaylight.yangtools.concepts.PrettyTreeAware;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 
 /**
@@ -44,7 +45,7 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgum
  *               boundary -- like RFC8528. Hence we cannot really have a reasonably-structured concept of unverified
  *               data. Nevertheless, this interface should be named 'NormalizedData'.
  */
-public interface NormalizedNode extends Identifiable<PathArgument> {
+public interface NormalizedNode extends Identifiable<PathArgument>, PrettyTreeAware {
     @Override
     // We override here, so that NormalizedNode.getIdentifier() has fewer implementations
     PathArgument getIdentifier();
diff --git a/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AbstractPrettyTreeTest.java b/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AbstractPrettyTreeTest.java
new file mode 100644 (file)
index 0000000..0e72399
--- /dev/null
@@ -0,0 +1,227 @@
+/*
+ * Copyright (c) 2021 PANTHEON.tech s.r.o. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.impl.schema.tree;
+
+import static org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes.leafNode;
+import static org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes.mapEntry;
+import static org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes.mapEntryBuilder;
+import static org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes.mapNodeBuilder;
+
+import java.util.List;
+import java.util.Set;
+import org.opendaylight.yangtools.yang.common.QName;
+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.NodeWithValue;
+import org.opendaylight.yangtools.yang.data.api.schema.AnydataNode;
+import org.opendaylight.yangtools.yang.data.api.schema.AugmentationNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode;
+import org.opendaylight.yangtools.yang.data.api.schema.UserLeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.UserMapNode;
+import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.data.impl.schema.builder.impl.ImmutableContainerNodeBuilder;
+
+public abstract class AbstractPrettyTreeTest {
+    protected static final QName ROOT_QNAME = QName.create(
+            "urn:opendaylight:controller:sal:dom:store:test", "2014-03-13", "root");
+    protected static final QName ANOTHER_QNAME = QName.create(
+            "urn:opendaylight:controller:sal:dom:store:another", "another");
+
+    protected static final QName LIST_A_QNAME = QName.create(ROOT_QNAME, "list-a");
+    protected static final QName LEAF_A_QNAME = QName.create(ROOT_QNAME, "leaf-a");
+    protected static final QName LIST_B_QNAME = QName.create(ROOT_QNAME, "list-b");
+    protected static final QName LEAF_B_QNAME = QName.create(ROOT_QNAME, "leaf-b");
+
+    protected static final QName CHOICE_QNAME = QName.create(ROOT_QNAME, "choice");
+    protected static final QName AUGMENT_QNAME = QName.create(ROOT_QNAME, "augment");
+
+    protected static final QName LIST_ANOTHER_NAMESPACE_QNAME = QName.create(ANOTHER_QNAME,
+            "list-from-another-namespace");
+    protected static final QName LEAF_ANOTHER_NAMESPACE_QNAME = QName.create(ANOTHER_QNAME,
+            "leaf-from-another-namespace");
+
+    protected static final QName LEAF_QNAME = QName.create(ROOT_QNAME, "leaf");
+    protected static final QName LEAF_SET_QNAME = QName.create(ROOT_QNAME, "leaf-set");
+
+    protected static final QName USER_LEAF_SET_QNAME = QName.create(ROOT_QNAME, "user-leaf-set");
+    protected static final QName USER_MAP_QNAME = QName.create(ROOT_QNAME, "user-map");
+    protected static final QName USER_MAP_ENTRY_QNAME = QName.create(ROOT_QNAME, "user-map-entry");
+
+    protected static final QName UNKEYED_LIST_QNAME = QName.create(ROOT_QNAME,
+            "unkeyed-list");
+    protected static final QName UNKEYED_LIST_ENTRY_QNAME = QName.create(ROOT_QNAME,
+            "unkeyed-list-entry");
+    protected static final QName UNKEYED_LIST_LEAF_QNAME = QName.create(ROOT_QNAME,
+            "unkeyed-list-leaf");
+
+    protected static final QName ANY_DATA_QNAME = QName.create(ROOT_QNAME, "any-data");
+
+    /**
+     * Return a test node.
+     *
+     * <pre>
+     * root
+     *     list-a
+     *          leaf-a "foo"
+     *     list-a
+     *          leaf-a "bar"
+     *          list-b
+     *                  leaf-b "one"
+     *          list-b
+     *                  leaf-b "two"
+     *     choice
+     *          augment
+     *                  augmented-leaf "Augmented leaf value"
+     *     another
+     *          list-from-another-namespace
+     *               leaf-from-another-namespace "Leaf from another namespace value"
+     *     leaf "Leaf value"
+     *     leaf-set "Leaf set value"
+     *     user-leaf-set "User leaf set value"
+     *     user-map
+     *          user-map-entry "User map entry value"
+     *     unkeyed-list
+     *          unkeyed-list-entry
+     *               unkeyed-list-leaf "Unkeyed list leaf value"
+     *     any-data "Any data value"
+     *
+     * </pre>
+     *
+     * @return A test node
+     */
+    protected static NormalizedNode createContainerNode() {
+        return ImmutableContainerNodeBuilder.create()
+                .withNodeIdentifier(new NodeIdentifier(ROOT_QNAME))
+                .withChild(createMapNode())
+                .withChild(createChoiceNode())
+                .withChild(createContainerFromAnotherNamespace())
+                .withChild(createLeafNode())
+                .withChild(createLeafSetNode())
+                .withChild(createUserLeafSetNode())
+                .withChild(createUserMapNode())
+                .withChild(createUnkeyedListNode())
+                .withChild(createAnyDataNode())
+                .build();
+    }
+
+    protected static MapNode createMapNode() {
+        return mapNodeBuilder(LIST_A_QNAME)
+                .withChild(mapEntry(LIST_A_QNAME, LEAF_A_QNAME, "foo"))
+                .withChild(createMapEntryNode()).build();
+    }
+
+    protected static MapEntryNode createMapEntryNode() {
+        return mapEntryBuilder(LIST_A_QNAME, LEAF_A_QNAME, "bar")
+                .withChild(mapNodeBuilder(LIST_B_QNAME)
+                        .withChild(mapEntry(LIST_B_QNAME, LEAF_B_QNAME, "one"))
+                        .withChild(mapEntry(LIST_B_QNAME, LEAF_B_QNAME, "two"))
+                        .build()).build();
+    }
+
+    protected static ChoiceNode createChoiceNode() {
+        return Builders.choiceBuilder()
+                .withNodeIdentifier(NodeIdentifier.create(CHOICE_QNAME))
+                .withChild(createAugmentationNode())
+                .build();
+    }
+
+    protected static AugmentationNode createAugmentationNode() {
+        return Builders.augmentationBuilder()
+                .withNodeIdentifier(AugmentationIdentifier
+                        .create(Set.of(AUGMENT_QNAME)))
+                .withChild(createAugmentedLeafNode())
+                .build();
+    }
+
+    protected static LeafNode<String> createAugmentedLeafNode() {
+        return leafNode(AUGMENT_QNAME, "Augmented leaf value");
+    }
+
+    protected static ContainerNode createContainerFromAnotherNamespace() {
+        return ImmutableContainerNodeBuilder.create()
+                .withNodeIdentifier(new NodeIdentifier(ANOTHER_QNAME))
+                .withChild(mapNodeBuilder(LIST_ANOTHER_NAMESPACE_QNAME)
+                        .withChild(mapEntry(LIST_ANOTHER_NAMESPACE_QNAME,
+                                LEAF_ANOTHER_NAMESPACE_QNAME,
+                                "Leaf from another namespace value"))
+                        .build())
+                .build();
+    }
+
+    protected static LeafNode<String> createLeafNode() {
+        return Builders.<String>leafBuilder()
+                .withNodeIdentifier(NodeIdentifier.create(LEAF_QNAME))
+                .withValue("Leaf value")
+                .build();
+    }
+
+    protected static LeafSetNode<String> createLeafSetNode() {
+        final String value = "Leaf set value";
+        final LeafSetEntryNode<String> leafSetValue = Builders.<String>leafSetEntryBuilder()
+                .withNodeIdentifier(new NodeWithValue<>(LEAF_SET_QNAME, value))
+                .withValue(value)
+                .build();
+        return Builders.<String>leafSetBuilder()
+                .withNodeIdentifier(NodeIdentifier.create(LEAF_SET_QNAME))
+                .withValue(List.of(leafSetValue))
+                .build();
+    }
+
+    protected static UserLeafSetNode<String> createUserLeafSetNode() {
+        final String value = "User leaf set value";
+        final LeafSetEntryNode<String> leafSetValue = Builders.<String>leafSetEntryBuilder()
+                .withNodeIdentifier(new NodeWithValue<>(USER_LEAF_SET_QNAME, value))
+                .withValue(value)
+                .build();
+        return Builders.<String>orderedLeafSetBuilder()
+                .withNodeIdentifier(NodeIdentifier.create(USER_LEAF_SET_QNAME))
+                .withValue(List.of(leafSetValue))
+                .build();
+    }
+
+    protected static UserMapNode createUserMapNode() {
+        return Builders.orderedMapBuilder()
+                .withNodeIdentifier(NodeIdentifier.create(USER_MAP_QNAME))
+                .withValue(List.of(createUserMapEntryNode()))
+                .build();
+    }
+
+    protected static MapEntryNode createUserMapEntryNode() {
+        return mapEntry(USER_MAP_QNAME, USER_MAP_ENTRY_QNAME, "User map entry value");
+    }
+
+    protected static UnkeyedListNode createUnkeyedListNode() {
+        return Builders.unkeyedListBuilder()
+                .withNodeIdentifier(NodeIdentifier.create(UNKEYED_LIST_QNAME))
+                .withChild(createUnkeyedListEntryNode())
+                .build();
+    }
+
+    protected static UnkeyedListEntryNode createUnkeyedListEntryNode() {
+        return Builders.unkeyedListEntryBuilder()
+                .withNodeIdentifier(NodeIdentifier.create(UNKEYED_LIST_ENTRY_QNAME))
+                .withChild(leafNode(UNKEYED_LIST_LEAF_QNAME, "Unkeyed list leaf value"))
+                .build();
+    }
+
+    protected static AnydataNode<String> createAnyDataNode() {
+        return Builders.anydataBuilder(String.class)
+                .withNodeIdentifier(NodeIdentifier.create(ANY_DATA_QNAME))
+                .withValue("Any data value")
+                .build();
+    }
+}
diff --git a/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/NormalizedNodePrettyTreeTest.java b/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/NormalizedNodePrettyTreeTest.java
new file mode 100644 (file)
index 0000000..4223ce1
--- /dev/null
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2021 PANTHEON.tech s.r.o. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.impl.schema.tree;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class NormalizedNodePrettyTreeTest extends AbstractPrettyTreeTest {
+    @Test
+    public void testMapNodePrettyTree() {
+        assertEquals(String.join("\n",
+            "systemMapNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)list-a = {",
+            "    mapEntryNode list-a = {",
+            "        leafNode leaf-a = \"bar\"",
+            "        systemMapNode list-b = {",
+            "            mapEntryNode list-b = {",
+            "                leafNode leaf-b = \"two\"",
+            "            }",
+            "            mapEntryNode list-b = {",
+            "                leafNode leaf-b = \"one\"",
+            "            }",
+            "        }",
+            "    }",
+            "    mapEntryNode list-a = {",
+            "        leafNode leaf-a = \"foo\"",
+            "    }",
+            "}"), createMapNode().prettyTree().get());
+    }
+
+    @Test
+    public void testMapEntryPrettyTree() {
+        assertEquals(String.join("\n",
+            "mapEntryNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)list-a = {",
+            "    leafNode leaf-a = \"bar\"",
+            "    systemMapNode list-b = {",
+            "        mapEntryNode list-b = {",
+            "            leafNode leaf-b = \"two\"",
+            "        }",
+            "        mapEntryNode list-b = {",
+            "            leafNode leaf-b = \"one\"",
+            "        }",
+            "    }",
+            "}"), createMapEntryNode().prettyTree().get());
+    }
+
+    @Test
+    public void testChoicePrettyTree() {
+        assertEquals(String.join("\n",
+            "choiceNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)choice = {",
+            "    augmentationNode = {",
+            "        leafNode augment = \"Augmented leaf value\"",
+            "    }",
+            "}"), createChoiceNode().prettyTree().get());
+    }
+
+    @Test
+    public void testAugmentationPrettyTree() {
+        assertEquals(String.join("\n",
+            "augmentationNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13) = {",
+            "    leafNode augment = \"Augmented leaf value\"",
+            "}"), createAugmentationNode().prettyTree().get());
+    }
+
+    @Test
+    public void testLeafPrettyTree() {
+        assertEquals("leafNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)leaf = \"Leaf value\"",
+            createLeafNode().prettyTree().get());
+    }
+
+    @Test
+    public void testLeafSetPrettyTree() {
+        assertEquals(String.join("\n",
+            "systemLeafSetNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)leaf-set = {",
+            "    leafSetEntryNode leaf-set = \"Leaf set value\"",
+            "}"), createLeafSetNode().prettyTree().get());
+    }
+
+    @Test
+    public void testUserLeafSetPrettyTree() {
+        assertEquals(String.join("\n",
+            "userLeafSetNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)user-leaf-set = {",
+            "    leafSetEntryNode user-leaf-set = \"User leaf set value\"",
+            "}"), createUserLeafSetNode().prettyTree().get());
+    }
+
+    @Test
+    public void testUserMapPrettyTree() {
+        assertEquals(String.join("\n",
+            "userMapNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)user-map = {",
+            "    mapEntryNode user-map = {",
+            "        leafNode user-map-entry = \"User map entry value\"",
+            "    }",
+            "}"), createUserMapNode().prettyTree().get());
+    }
+
+    @Test
+    public void testUserMapEntryPrettyTree() {
+        assertEquals(String.join("\n",
+            "mapEntryNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)user-map = {",
+            "    leafNode user-map-entry = \"User map entry value\"",
+            "}"), createUserMapEntryNode().prettyTree().get());
+    }
+
+    @Test
+    public void testUnkeyedListPrettyTree() {
+        assertEquals(String.join("\n",
+            "unkeyedListNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)unkeyed-list = {",
+            "    unkeyedListEntryNode unkeyed-list-entry = {",
+            "        leafNode unkeyed-list-leaf = \"Unkeyed list leaf value\"",
+            "    }",
+            "}"), createUnkeyedListNode().prettyTree().get());
+    }
+
+    @Test
+    public void testUnkeyedListEntryPrettyTree() {
+        assertEquals(String.join("\n",
+            "unkeyedListEntryNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)unkeyed-list-entry = {",
+            "    leafNode unkeyed-list-leaf = \"Unkeyed list leaf value\"",
+            "}"), createUnkeyedListEntryNode().prettyTree().get());
+    }
+
+    @Test
+    public void testAnyDataPrettyTree() {
+        assertEquals(String.join("\n",
+            "anydataNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)any-data = (java.lang.String)"),
+            createAnyDataNode().prettyTree().get());
+    }
+
+    @Test
+    public void testContainerPrettyTree() {
+        assertEquals(String.join("\n",
+            "containerNode (urn:opendaylight:controller:sal:dom:store:test@2014-03-13)root = {",
+            "    userMapNode user-map = {",
+            "        mapEntryNode user-map = {",
+            "            leafNode user-map-entry = \"User map entry value\"",
+            "        }",
+            "    }",
+            "    userLeafSetNode user-leaf-set = {",
+            "        leafSetEntryNode user-leaf-set = \"User leaf set value\"",
+            "    }",
+            "    systemMapNode list-a = {",
+            "        mapEntryNode list-a = {",
+            "            leafNode leaf-a = \"bar\"",
+            "            systemMapNode list-b = {",
+            "                mapEntryNode list-b = {",
+            "                    leafNode leaf-b = \"two\"",
+            "                }",
+            "                mapEntryNode list-b = {",
+            "                    leafNode leaf-b = \"one\"",
+            "                }",
+            "            }",
+            "        }",
+            "        mapEntryNode list-a = {",
+            "            leafNode leaf-a = \"foo\"",
+            "        }",
+            "    }",
+            "    containerNode (urn:opendaylight:controller:sal:dom:store:another)another = {",
+            "        systemMapNode list-from-another-namespace = {",
+            "            mapEntryNode list-from-another-namespace = {",
+            "                leafNode leaf-from-another-namespace = \"Leaf from another namespace value\"",
+            "            }",
+            "        }",
+            "    }",
+            "    choiceNode choice = {",
+            "        augmentationNode = {",
+            "            leafNode augment = \"Augmented leaf value\"",
+            "        }",
+            "    }",
+            "    anydataNode any-data = (java.lang.String)",
+            "    unkeyedListNode unkeyed-list = {",
+            "        unkeyedListEntryNode unkeyed-list-entry = {",
+            "            leafNode unkeyed-list-leaf = \"Unkeyed list leaf value\"",
+            "        }",
+            "    }",
+            "    leafNode leaf = \"Leaf value\"",
+            "    systemLeafSetNode leaf-set = {",
+            "        leafSetEntryNode leaf-set = \"Leaf set value\"",
+            "    }",
+            "}"), createContainerNode().prettyTree().get());
+    }
+}
diff --git a/yang/yang-data-impl/src/test/resources/pretty-print/another.yang b/yang/yang-data-impl/src/test/resources/pretty-print/another.yang
new file mode 100644 (file)
index 0000000..08219ae
--- /dev/null
@@ -0,0 +1,18 @@
+module another {
+    yang-version 1.1;
+    namespace "urn:opendaylight:controller:sal:dom:store:another";
+    prefix another;
+
+    import test { prefix test; }
+
+    augment "/test:root" {
+        container another {
+            list list-from-another-namespace {
+                key "leaf-from-another-namespace";
+                leaf leaf-from-another-namespace {
+                    type string;
+                }
+            }
+        }
+    }
+}
diff --git a/yang/yang-data-impl/src/test/resources/pretty-print/test.yang b/yang/yang-data-impl/src/test/resources/pretty-print/test.yang
new file mode 100644 (file)
index 0000000..1c4eda9
--- /dev/null
@@ -0,0 +1,60 @@
+module test {
+    yang-version 1.1;
+    namespace "urn:opendaylight:controller:sal:dom:store:test";
+    prefix test;
+
+    revision 2014-03-13;
+
+    container root {
+        list list-a {
+            key "leaf-a";
+            leaf leaf-a {
+                type string;
+            }
+            list list-b {
+                key "leaf-b";
+                leaf leaf-b {
+                    type string;
+                }
+            }
+        }
+
+        choice choice {
+        }
+
+        leaf leaf {
+            type string;
+        }
+
+        leaf-list leaf-set {
+            type string;
+        }
+
+        leaf-list user-leaf-set {
+            ordered-by user;
+            type string;
+        }
+
+        list user-map {
+            ordered-by user;
+            key "user-map-entry";
+            leaf user-map-entry {
+                type string;
+            }
+        }
+
+        list unkeyed-list {
+            leaf unkeyed-list-leaf {
+                type string;
+            }
+        }
+
+        anydata any-data {}
+    }
+
+    augment "/test:root/test:choice" {
+        leaf augment {
+            type string;
+        }
+    }
+}