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 ex
return (Stream) 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);
+ }
}