Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/main/java/org/glavo/nbt/NBTPath.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="https://minecraft.wiki/w/NBT_path">NBT Path - Minecraft Wiki</a>
Expand All @@ -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 <T extends Tag> NBTPath<T> of(T tag, @Nullable Tag expectedRoot) throws IllegalArgumentException, IllegalStateException {
ParentTag<?> parentTag = tag.getParentTag();
if (parentTag == null) return null;

List<NBTPathNode> 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<T>) new NBTPathImpl<>(nodes, tag.getType());
}

/// Returns the tag type of this path.
@Contract(pure = true)
@Nullable TagType<T> getTagType();
Expand All @@ -50,4 +82,16 @@ static NBTPath<?> of(String path) throws IllegalArgumentException {
@Contract(pure = true)
<T2 extends Tag> NBTPath<T2> withTagType(TagType<T2> 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);
}

}
55 changes: 31 additions & 24 deletions src/main/java/org/glavo/nbt/internal/path/NBTPathImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,6 +60,14 @@ public static <T extends Tag> Stream<T> select(NBTParent<?> parent, NBTPath<? ex
return (Stream<T>) 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<T> tagType;

Expand Down Expand Up @@ -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<StringBuilder> 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;
Expand Down
97 changes: 91 additions & 6 deletions src/test/java/org/glavo/nbt/NBTPathTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<IntTag>()
.addTag(new IntTag(0))
.addTag(new IntTag(1))
.addTag(tag = new IntTag(2))
));

NBTPath<IntTag> 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<IntTag>()
.addTag(new IntTag(0))
.addTag(new IntTag(1))
.addTag(tag = new IntTag(2))
)));

NBTPath<IntTag> 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<StringTag> pathToSmile = NBTPath.of(tag, root);
assertNotNull(pathToSmile);
assertEquals(":D", root.getFirstString(pathToSmile));
assertEquals(NBTPath.of("\"Very Cool Name\".bar.baz").withTagType(TagType.STRING), pathToSmile);
}
}
Loading