diff --git a/src/main/java/org/glavo/nbt/NBTPath.java b/src/main/java/org/glavo/nbt/NBTPath.java index a6fdc2e..a2647db 100644 --- a/src/main/java/org/glavo/nbt/NBTPath.java +++ b/src/main/java/org/glavo/nbt/NBTPath.java @@ -16,12 +16,16 @@ package org.glavo.nbt; import org.glavo.nbt.internal.path.NBTPathImpl; +import org.glavo.nbt.internal.path.NBTPathNode; import org.glavo.nbt.internal.snbt.SNBTParser; +import org.glavo.nbt.tag.ParentTag; import org.glavo.nbt.tag.Tag; import org.glavo.nbt.tag.TagType; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; +import java.util.*; + /// An NBT path is a descriptive string used to specify one or more particular elements from an NBT data tree. /// /// @see NBT Path - Minecraft Wiki @@ -34,6 +38,34 @@ static NBTPath of(String path) throws IllegalArgumentException { return new SNBTParser(path, 0, path.length()).nextPath(); } + /// Get the path from the root tag to the given tag. + /// + /// @param expectedRoot the expected root instead of the top of the tree. + /// @return the path or `null` if parent is null. + /// @throws IllegalStateException when the expected root doesn't match the actual root. + @SuppressWarnings({"DataFlowIssue", "unchecked"}) + @Contract(pure = true) + static @Nullable NBTPath of(T tag, @Nullable Tag expectedRoot) throws IllegalArgumentException, IllegalStateException { + ParentTag parentTag = tag.getParentTag(); + if (parentTag == null) return null; + + List paths = new ArrayList<>(); + paths.add(NBTPathImpl.getIndicator(tag)); + while (true) { + if (parentTag == expectedRoot) break; + paths.add(NBTPathImpl.getIndicator(parentTag)); + ParentTag parent = parentTag.getParentTag(); + if (parent == null) break; + parentTag = parent; + } + + if (parentTag != expectedRoot) + throw new IllegalStateException("Unexpected root tag " + parentTag + ", expected " + expectedRoot + "."); + Collections.reverse(paths); + NBTPathNode[] nodes = paths.toArray(NBTPathNode[]::new); + return (NBTPath) new NBTPathImpl<>(nodes, tag.getType()); + } + /// Returns the tag type of this path. @Contract(pure = true) @Nullable TagType getTagType(); @@ -50,4 +82,16 @@ static NBTPath of(String path) throws IllegalArgumentException { @Contract(pure = true) NBTPath withTagType(TagType tagType) throws IllegalStateException; + /// Returns the path string. + /// + /// @param omitDots `true` to omit dots if possible. + @Contract(pure = true) + String toPathString(boolean omitDots); + + /// Returns the path string with dots omitted if possible. + @Contract(pure = true) + default String toPathString() { + return toPathString(true); + } + } diff --git a/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java b/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java index 6a9c432..3ded6e9 100644 --- a/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java +++ b/src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java @@ -22,6 +22,7 @@ import org.glavo.nbt.internal.snbt.SNBTWriter; import org.glavo.nbt.io.SNBTCodec; import org.glavo.nbt.tag.CompoundTag; +import org.glavo.nbt.tag.ParentTag; import org.glavo.nbt.tag.Tag; import org.glavo.nbt.tag.TagType; import org.jetbrains.annotations.Nullable; @@ -59,6 +60,14 @@ public static Stream select(NBTParent parent, NBTPath) tags; } + /// Get the indicator of the given tag, depends on its parent tag. + /// + /// @return the name node if parent is [CompoundTag], the index node if parent is other [ParentTag], or `null` if parent is null. + public static @Nullable NBTPathNode getIndicator(Tag tag) { + ParentTag parentTag = tag.getParentTag(); + return parentTag == null ? null : parentTag instanceof CompoundTag ? new NBTPathNode.NamedSubTag(tag.getName()) : new NBTPathNode.Index(tag.getIndex()); + } + private final NBTPathNode @Unmodifiable [] nodes; private final @Nullable TagType tagType; @@ -107,33 +116,31 @@ public int hashCode() { } @Override - public String toString() { - if (cachedString == null) { - StringBuilder builder = new StringBuilder(); - - if (tagType != null) { - builder.append("<").append(tagType).append("> "); + public String toPathString(boolean omitDots) { + StringBuilder builder = new StringBuilder(); + + SNBTWriter writer = new SNBTWriter<>(SNBTCodec.ofCompact(), builder); + for (int i = 0; i < nodes.length; i++) { + NBTPathNode node = nodes[i]; + try { + node.appendTo(writer); + } catch (IOException e) { + throw new AssertionError(e); } - - var writer = new SNBTWriter<>(SNBTCodec.ofCompact(), builder); - - boolean first = true; - for (NBTPathNode node : nodes) { - if (first) { - first = false; - } else if (node.needDot()) { - writer.getAppendable().append('.'); - } - - try { - node.appendTo(writer); - } catch (IOException e) { - throw new AssertionError(e); - } + if (i + 1 < nodes.length && (!omitDots || nodes[i + 1].needDot())) { + writer.getAppendable().append('.'); } + } + + return builder.toString(); + } - builder.append(']'); - cachedString = builder.toString(); + @Override + public String toString() { + if (cachedString == null) { + String pathString = toPathString(); + if (tagType != null) pathString = "<" + tagType + ">" + " " + pathString; + cachedString = pathString; } return cachedString; diff --git a/src/test/java/org/glavo/nbt/NBTPathTest.java b/src/test/java/org/glavo/nbt/NBTPathTest.java index ce5d648..ea2816e 100644 --- a/src/test/java/org/glavo/nbt/NBTPathTest.java +++ b/src/test/java/org/glavo/nbt/NBTPathTest.java @@ -21,12 +21,8 @@ import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertIterableEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; final class NBTPathTest { @@ -188,4 +184,93 @@ void testInvalidPathSyntax() { assertThrows(IllegalArgumentException.class, () -> NBTPath.of("\"unterminated")); assertThrows(IllegalArgumentException.class, () -> NBTPath.of("[2147483648]")); } + + @Test + void testPathString() { + assertEquals("{}", NBTPath.of("{}").toPathString()); + assertEquals("{Invisible:1B}", NBTPath.of("{Invisible:1b}").toPathString()); + assertEquals("\"A Very Cool Name[]\"", NBTPath.of("\"A Very Cool Name[]\"").toPathString()); + assertEquals("\"A Very Cool Name[]\"{}", NBTPath.of("\"A Very Cool Name[]\"{}").toPathString()); + assertEquals("\"A Very Cool Name[]\"[]", NBTPath.of("\"A Very Cool Name[]\"[]").toPathString()); + assertEquals("\"A Very Cool Name[]\"[{}]", NBTPath.of("\"A Very Cool Name[]\"[{}]").toPathString()); + assertEquals("\"A Very Cool Name[]\"[{Count:25B}]", NBTPath.of("\"A Very Cool Name[]\"[{Count:25b}]").toPathString()); + assertEquals("\"A Very Cool Name[]\"[][][]", NBTPath.of("\"A Very Cool Name[]\"[][][]").toPathString()); + assertEquals("foo.bar", NBTPath.of("foo.bar").toPathString()); + assertEquals("foo.bar[]", NBTPath.of("foo.bar.[]").toPathString()); + assertEquals("foo.bar[{}]", NBTPath.of("foo.bar.[{}]").toPathString()); + assertEquals("foo.bar[0]", NBTPath.of("foo.bar.[0]").toPathString()); + assertEquals("foo.bar[-1]", NBTPath.of("foo.bar.[-1]").toPathString()); + assertEquals("foo.bar.\"0123\"", NBTPath.of("foo.bar.\"0123\"").toPathString()); + } + + @Test + void testPathStringKeepDots() { + assertEquals("{}", NBTPath.of("{}").toPathString(false)); + assertEquals("{Invisible:1B}", NBTPath.of("{Invisible:1b}").toPathString(false)); + assertEquals("\"A Very Cool Name[]\"", NBTPath.of("\"A Very Cool Name[]\"").toPathString(false)); + assertEquals("\"A Very Cool Name[]\"{}", NBTPath.of("\"A Very Cool Name[]\"{}").toPathString(false)); + assertEquals("\"A Very Cool Name[]\".[]", NBTPath.of("\"A Very Cool Name[]\"[]").toPathString(false)); + assertEquals("\"A Very Cool Name[]\".[{}]", NBTPath.of("\"A Very Cool Name[]\"[{}]").toPathString(false)); + assertEquals("\"A Very Cool Name[]\".[{Count:25B}]", NBTPath.of("\"A Very Cool Name[]\"[{Count:25b}]").toPathString(false)); + assertEquals("\"A Very Cool Name[]\".[].[].[]", NBTPath.of("\"A Very Cool Name[]\"[][][]").toPathString(false)); + assertEquals("foo.bar", NBTPath.of("foo.bar").toPathString(false)); + assertEquals("foo.bar.[]", NBTPath.of("foo.bar.[]").toPathString(false)); + assertEquals("foo.bar.[{}]", NBTPath.of("foo.bar.[{}]").toPathString(false)); + assertEquals("foo.bar.[0]", NBTPath.of("foo.bar.[0]").toPathString(false)); + assertEquals("foo.bar.[-1]", NBTPath.of("foo.bar.[-1]").toPathString(false)); + assertEquals("foo.bar.\"0123\"", NBTPath.of("foo.bar.\"0123\"").toPathString(false)); + } + + @Test + void testOfPath() { + CompoundTag root = new CompoundTag().setName("root"); + IntTag tag; + + root.addTag("foo", new CompoundTag() + .addTag("bar", new ListTag() + .addTag(new IntTag(0)) + .addTag(new IntTag(1)) + .addTag(tag = new IntTag(2)) + )); + + NBTPath pathTo2 = NBTPath.of(tag, root); + assertNotNull(pathTo2); + assertEquals(2, root.getFirstInt(pathTo2)); + assertEquals(NBTPath.of("foo.bar[2]").withTagType(TagType.INT), pathTo2); + } + + @Test + void testOfPath2() { + CompoundTag root = new CompoundTag().setName("root"); + CompoundTag expectedRoot; + IntTag tag; + + root.addTag("foo", expectedRoot = new CompoundTag() + .addTag("bar", new CompoundTag() + .addTag("baz", new ListTag() + .addTag(new IntTag(0)) + .addTag(new IntTag(1)) + .addTag(tag = new IntTag(2)) + ))); + + NBTPath pathTo2 = NBTPath.of(tag, expectedRoot); + assertNotNull(pathTo2); + assertEquals(2, expectedRoot.getFirstInt(pathTo2)); + assertEquals(NBTPath.of("bar.baz[2]").withTagType(TagType.INT), pathTo2); + } + + @Test + void testOfPath3() { + CompoundTag root = new CompoundTag().setName("root"); + StringTag tag; + + root.addTag("Very Cool Name", new CompoundTag() + .addTag("bar", new CompoundTag() + .addTag("baz", tag = new StringTag(":D")))); + + NBTPath pathToSmile = NBTPath.of(tag, root); + assertNotNull(pathToSmile); + assertEquals(":D", root.getFirstString(pathToSmile)); + assertEquals(NBTPath.of("\"Very Cool Name\".bar.baz").withTagType(TagType.STRING), pathToSmile); + } }