From dbc28c0035a84dd04abe677de4d7bbf6d1e6a828 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Wed, 9 Jan 2013 04:50:04 +0100 Subject: [PATCH] Added the ability to serialize and deserialize NBT to many formats. --- .../protocol/wrappers/nbt/NbtBase.java | 7 + .../protocol/wrappers/nbt/NbtCompound.java | 12 +- .../protocol/wrappers/nbt/NbtFactory.java | 54 +--- .../protocol/wrappers/nbt/NbtList.java | 18 +- .../protocol/wrappers/nbt/NbtType.java | 27 +- .../protocol/wrappers/nbt/NbtVisitor.java | 43 +++ .../wrappers/nbt/WrappedCompound.java | 30 +- .../protocol/wrappers/nbt/WrappedElement.java | 9 +- .../protocol/wrappers/nbt/WrappedList.java | 103 ++++-- .../wrappers/nbt/io/NbtBinarySerializer.java | 88 +++++ .../nbt/io/NbtConfigurationSerializer.java | 301 ++++++++++++++++++ .../wrappers/nbt/io/NbtTextSerializer.java | 119 +++++++ .../wrappers/nbt/NbtCompoundTest.java | 5 + .../protocol/wrappers/nbt/NbtFactoryTest.java | 3 +- .../io/NbtConfigurationSerializerTest.java | 37 +++ 15 files changed, 772 insertions(+), 84 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtVisitor.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/io/NbtBinarySerializer.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/io/NbtConfigurationSerializer.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/io/NbtTextSerializer.java create mode 100644 ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/io/NbtConfigurationSerializerTest.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java index 985f6e22..4fda61b8 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java @@ -29,6 +29,13 @@ import com.comphenix.protocol.wrappers.nbt.NbtType; * @param - type of the value that is stored. */ public interface NbtBase { + /** + * Accepts a NBT visitor. + * @param visitor - the hierarchical NBT visitor. + * @return TRUE if the parent should continue processing children at the current level, FALSE otherwise. + */ + public abstract boolean accept(NbtVisitor visitor); + /** * Retrieve the type of this NBT element. * @return The type of this NBT element. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java index ee066ffe..ba27a6cb 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java @@ -10,7 +10,7 @@ import java.util.Set; *

* Use {@link NbtFactory} to load or create an instance. *

- * The {@link NbtBase#getValue()} method returns a {@link java.util.Map} that will correctly return the content + * The {@link NbtBase#getValue()} method returns a {@link java.util.Map} that will return the full content * of this NBT compound, but may throw an {@link UnsupportedOperationException} for any of the write operations. * * @author Kristian @@ -73,6 +73,14 @@ public interface NbtCompound extends NbtBase>>, Iterable< * @return This current compound, for chaining. */ public abstract NbtCompound put(String key, String value); + + /** + * Inserts an entry after cloning it and renaming it to "key". + * @param key - the name of the entry. + * @param entry - the entry to insert. + * @return This current compound, for chaining. + */ + public abstract NbtCompound put(String key, NbtBase entry); /** * Retrieve the byte value of an entry identified by a given key. @@ -286,7 +294,7 @@ public interface NbtCompound extends NbtBase>>, Iterable< * @param list - the list value. * @return This current compound, for chaining. */ - public abstract NbtCompound put(WrappedList list); + public abstract NbtCompound put(NbtList list); /** * Associate a new NBT list with the given key. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java index 95885165..4c932ade 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtFactory.java @@ -17,8 +17,6 @@ package com.comphenix.protocol.wrappers.nbt; -import java.io.DataInput; -import java.io.DataOutput; import java.lang.reflect.Method; import java.util.Collection; import java.util.List; @@ -40,11 +38,7 @@ import com.comphenix.protocol.wrappers.BukkitConverters; public class NbtFactory { // Used to create the underlying tag private static Method methodCreateTag; - - // Used to read and write NBT - private static Method methodWrite; - private static Method methodLoad; - + // Item stack trickery private static StructureModifier itemStackModifier; @@ -102,7 +96,7 @@ public class NbtFactory { } else if (base.getType() == NbtType.TAG_LIST) { // As above - WrappedList copy = WrappedList.fromName(base.getName()); + NbtList copy = WrappedList.fromName(base.getName()); copy.setValue((List>) base.getValue()); return (NbtWrapper) copy; @@ -166,49 +160,7 @@ public class NbtFactory { else return partial; } - - /** - * Write the content of a wrapped NBT tag to a stream. - * @param value - the NBT tag to write. - * @param destination - the destination stream. - */ - public static void toStream(NbtWrapper value, DataOutput destination) { - if (methodWrite == null) { - Class base = MinecraftReflection.getNBTBaseClass(); - - // Use the base class - methodWrite = FuzzyReflection.fromClass(base). - getMethodByParameters("writeNBT", base, DataOutput.class); - } - try { - methodWrite.invoke(null, fromBase(value).getHandle(), destination); - } catch (Exception e) { - throw new FieldAccessException("Unable to write NBT " + value, e); - } - } - - /** - * Load an NBT tag from a stream. - * @param source - the input stream. - * @return An NBT tag. - */ - public static NbtWrapper fromStream(DataInput source) { - if (methodLoad == null) { - Class base = MinecraftReflection.getNBTBaseClass(); - - // Use the base class - methodLoad = FuzzyReflection.fromClass(base). - getMethodByParameters("load", base, new Class[] { DataInput.class }); - } - - try { - return fromNMS(methodLoad.invoke(null, source)); - } catch (Exception e) { - throw new FieldAccessException("Unable to read NBT from " + source, e); - } - } - /** * Constructs a NBT tag of type string. * @param name - name of the tag. @@ -276,7 +228,7 @@ public class NbtFactory { * @return The constructed NBT tag. */ public static NbtBase of(String name, double value) { - return ofWrapper(NbtType.TAG_DOUBlE, name, value); + return ofWrapper(NbtType.TAG_DOUBLE, name, value); } /** diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtList.java index 1aa823a2..85ef13de 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtList.java @@ -23,11 +23,27 @@ public interface NbtList extends NbtBase>>, Iterable< public static String EMPTY_NAME = ""; /** - * Get the type of each element. + * Get the type of each element. + *

+ * This will be {@link NbtType#TAG_END TAG_END} if the NBT list has just been created. * @return Element type. */ public abstract NbtType getElementType(); + /** + * Set the type of each element. + * @param type - type of each element. + */ + public abstract void setElementType(NbtType type); + + /** + * Add a value to a typed list by attempting to convert it to the nearest value. + *

+ * Note that the list must be typed by setting {@link #setElementType(NbtType)} before calling this function. + * @param value - the value to add. + */ + public abstract void addClosest(Object value); + /** * Add a NBT list or NBT compound to the list. * @param element diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtType.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtType.java index 16b1787a..2ef6e59d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtType.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtType.java @@ -62,7 +62,7 @@ public enum NbtType { /** * A signed 8 byte floating point type. */ - TAG_DOUBlE(6, double.class), + TAG_DOUBLE(6, double.class), /** * An array of bytes. @@ -113,13 +113,25 @@ public enum NbtType { classLookup.put(Primitives.wrap(type.getValueType()), type); } } + + // Additional lookup + classLookup.put(NbtList.class, TAG_LIST); + classLookup.put(NbtCompound.class, TAG_COMPOUND); } private NbtType(int rawID, Class valueType) { this.rawID = rawID; this.valueType = valueType; } - + + /** + * Determine if the given NBT can store multiple children NBT tags. + * @return TRUE if this is a composite NBT tag, FALSE otherwise. + */ + public boolean isComposite() { + return this == TAG_COMPOUND || this == TAG_LIST; + } + /** * Retrieves the raw unique integer that identifies the type of the parent NBT element. * @return Integer that uniquely identifying the type. @@ -157,9 +169,16 @@ public enum NbtType { NbtType result = classLookup.get(clazz); // Try to lookup this value - if (result != null) + if (result != null) { return result; - else + } else { + // Look for interfaces + for (Class implemented : clazz.getInterfaces()) { + if (classLookup.containsKey(implemented)) + return classLookup.get(implemented); + } + throw new IllegalArgumentException("No NBT tag can represent a " + clazz); + } } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtVisitor.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtVisitor.java new file mode 100644 index 00000000..8a9a3ec9 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtVisitor.java @@ -0,0 +1,43 @@ +package com.comphenix.protocol.wrappers.nbt; + +/** + * A visitor that can enumerate a NBT tree structure. + * + * @author Kristian + */ +public interface NbtVisitor { + /** + * Visit a leaf node, which is a NBT tag with a primitive or String value. + * @param node - the visited leaf node. + * @return TRUE to continue visiting children at this level, FALSE otherwise. + */ + public boolean visit(NbtBase node); + + /** + * Begin visiting a list node that contains multiple child nodes of the same type. + * @param list - the NBT tag to process. + * @return TRUE to visit the child nodes of this list, FALSE otherwise. + */ + public boolean visitEnter(NbtList list); + + /** + * Begin visiting a compound node that contains multiple child nodes of different types. + * @param compound - the NBT tag to process. + * @return TRUE to visit the child nodes of this compound, FALSE otherwise. + */ + public boolean visitEnter(NbtCompound compound); + + /** + * Stop visiting a list node. + * @param list - the list we're done visiting. + * @return TRUE for the parent to visit any subsequent sibling nodes, FALSE otherwise. + */ + public boolean visitLeave(NbtList list); + + /** + * Stop visiting a compound node. + * @param compound - the compound we're done visting. + * @return TRUE for the parent to visit any subsequent sibling nodes, FALSE otherwise + */ + public boolean visitLeave(NbtCompound compound); +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedCompound.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedCompound.java index 899fae1e..410e093d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedCompound.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedCompound.java @@ -23,6 +23,8 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; +import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer; + /** * A concrete implementation of an NbtCompound that wraps an underlying NMS Compound. * @@ -66,6 +68,19 @@ class WrappedCompound implements NbtWrapper>>, Iterable>(handle); } + + @Override + public boolean accept(NbtVisitor visitor) { + // Enter this node? + if (visitor.visitEnter(this)) { + for (NbtBase node : this) { + if (!node.accept(visitor)) + break; + } + } + + return visitor.visitLeave(this); + } @Override public Object getHandle() { @@ -420,7 +435,7 @@ class WrappedCompound implements NbtWrapper>>, Iterable>>, Iterable NbtCompound put(WrappedList list) { + public NbtCompound put(NbtList list) { getValue().put(list.getName(), list); return this; } + @Override + public NbtCompound put(String key, NbtBase entry) { + // Don't modify the original NBT + NbtBase clone = entry.deepClone(); + + clone.setName(key); + return put(clone); + } + /** * Associate a new NBT list with the given key. * @param key - the key and name of the new NBT list. @@ -561,7 +585,7 @@ class WrappedCompound implements NbtWrapper>>, Iterable implements NbtWrapper { return modifier; } + @Override + public boolean accept(NbtVisitor visitor) { + return visitor.visit(this); + } + /** * Retrieve the underlying NBT tag object. * @return The underlying Minecraft tag object. @@ -173,7 +179,8 @@ class WrappedElement implements NbtWrapper { @Override public void write(DataOutput destination) { - NbtFactory.toStream(this, destination); + // No need to cache this object + NbtBinarySerializer.DEFAULT.serialize(this, destination); } @Override diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedList.java index 24fa7a05..7e281b04 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedList.java @@ -24,6 +24,7 @@ import java.util.List; import javax.annotation.Nullable; +import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; @@ -41,13 +42,17 @@ class WrappedList implements NbtWrapper>>, Iterable> savedList; + // Element type + private NbtType elementType = NbtType.TAG_END; + /** * Construct a new empty NBT list. * @param name - name of this list. * @return The new empty NBT list. */ - public static WrappedList fromName(String name) { - return (WrappedList) NbtFactory.>>ofWrapper(NbtType.TAG_LIST, name); + @SuppressWarnings("unchecked") + public static NbtList fromName(String name) { + return (NbtList) NbtFactory.>>ofWrapper(NbtType.TAG_LIST, name); } /** @@ -56,13 +61,18 @@ class WrappedList implements NbtWrapper>>, Iterable WrappedList fromArray(String name, T... elements) { - WrappedList result = fromName(name); + @SuppressWarnings({"unchecked", "rawtypes"}) + public static NbtList fromArray(String name, T... elements) { + NbtList result = fromName(name); for (T element : elements) { if (element == null) throw new IllegalArgumentException("An NBT list cannot contain a null element!"); - result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element)); + + if (element instanceof NbtBase) + result.add((NbtBase) element); + else + result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element)); } return result; } @@ -73,21 +83,44 @@ class WrappedList implements NbtWrapper>>, Iterable WrappedList fromList(String name, Collection elements) { - WrappedList result = fromName(name); + @SuppressWarnings({"unchecked", "rawtypes"}) + public static NbtList fromList(String name, Collection elements) { + NbtList result = fromName(name); for (T element : elements) { if (element == null) throw new IllegalArgumentException("An NBT list cannot contain a null element!"); - result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element)); + + if (element instanceof NbtBase) + result.add((NbtBase) element); + else + result.add(NbtFactory.ofWrapper(element.getClass(), EMPTY_NAME, element)); } return result; } + /** + * Construct a list from an NMS instance. + * @param handle - NMS instance. + */ public WrappedList(Object handle) { this.container = new WrappedElement>(handle); + this.elementType = container.getSubType(); } + @Override + public boolean accept(NbtVisitor visitor) { + // Enter this node? + if (visitor.visitEnter(this)) { + for (NbtBase node : getValue()) { + if (!node.accept(visitor)) + break; + } + } + + return visitor.visitLeave(this); + } + @Override public Object getHandle() { return container.getHandle(); @@ -98,15 +131,17 @@ class WrappedList implements NbtWrapper>>, Iterable implements NbtWrapper>>, Iterable 0) { + if (getElementType() != NbtType.TAG_END) { if (!element.getType().equals(getElementType())) { throw new IllegalArgumentException( "Cannot add " + element + " of " + element.getType() + " to a list of type " + getElementType()); @@ -153,18 +188,12 @@ class WrappedList implements NbtWrapper>>, Iterable> c) { - boolean empty = size() == 0; boolean result = false; for (NbtBase element : c) { add(element); result = true; } - - // See if we now added our first object(s) - if (empty && result) { - container.setSubType(get(0).getType()); - } return result; } @@ -197,6 +226,38 @@ class WrappedList implements NbtWrapper>>, Iterable) value); + + } else { + // Just add it + add((NbtBase) NbtFactory.ofWrapper(getElementType(), EMPTY_NAME, value)); + } + } + @Override public void add(NbtBase element) { getValue().add(element); @@ -293,7 +354,7 @@ class WrappedList implements NbtWrapper>>, Iterable void serialize(NbtBase value, DataOutput destination) { + if (methodWrite == null) { + Class base = MinecraftReflection.getNBTBaseClass(); + + // Use the base class + methodWrite = FuzzyReflection.fromClass(base). + getMethodByParameters("writeNBT", base, DataOutput.class); + } + + try { + methodWrite.invoke(null, NbtFactory.fromBase(value).getHandle(), destination); + } catch (Exception e) { + throw new FieldAccessException("Unable to write NBT " + value, e); + } + } + + /** + * Load an NBT tag from a stream. + * @param source - the input stream. + * @return An NBT tag. + */ + public NbtWrapper deserialize(DataInput source) { + if (methodLoad == null) { + Class base = MinecraftReflection.getNBTBaseClass(); + + // Use the base class + methodLoad = FuzzyReflection.fromClass(base). + getMethodByParameters("load", base, new Class[] { DataInput.class }); + } + + try { + return NbtFactory.fromNMS(methodLoad.invoke(null, source)); + } catch (Exception e) { + throw new FieldAccessException("Unable to read NBT from " + source, e); + } + } + + /** + * Load an NBT compound from a stream. + * @param source - the input stream. + * @return An NBT compound. + */ + @SuppressWarnings("rawtypes") + public NbtCompound deserializeCompound(DataInput source) { + // I always seem to override generics ... + return (NbtCompound) (NbtBase) deserialize(source); + } + + /** + * Load an NBT list from a stream. + * @param source - the input stream. + * @return An NBT list. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public NbtList deserializeList(DataInput source) { + return (NbtList) (NbtBase) deserialize(source); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/io/NbtConfigurationSerializer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/io/NbtConfigurationSerializer.java new file mode 100644 index 00000000..acdf3271 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/io/NbtConfigurationSerializer.java @@ -0,0 +1,301 @@ +package com.comphenix.protocol.wrappers.nbt.io; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import com.comphenix.protocol.wrappers.nbt.NbtBase; +import com.comphenix.protocol.wrappers.nbt.NbtCompound; +import com.comphenix.protocol.wrappers.nbt.NbtFactory; +import com.comphenix.protocol.wrappers.nbt.NbtList; +import com.comphenix.protocol.wrappers.nbt.NbtType; +import com.comphenix.protocol.wrappers.nbt.NbtVisitor; +import com.comphenix.protocol.wrappers.nbt.NbtWrapper; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.primitives.Ints; + +/** + * Serialize and deserialize NBT information from a configuration section. + *

+ * Note that data types may be internally preserved by modifying the serialized name. This may + * be visible to the end-user. + * + * @author Kristian + */ +public class NbtConfigurationSerializer { + /** + * The default delimiter that is used to store the data type in YAML. + */ + public static final String TYPE_DELIMITER = "$"; + + /** + * A standard YAML serializer. + */ + public static final NbtConfigurationSerializer DEFAULT = new NbtConfigurationSerializer(); + + private String dataTypeDelimiter; + + /** + * Construct a serializer using {@link #TYPE_DELIMITER} as the default delimiter. + */ + public NbtConfigurationSerializer() { + this.dataTypeDelimiter = TYPE_DELIMITER; + } + + /** + * Construct a serializer using the given value as a delimiter. + * @param dataTypeDelimiter - the local data type delimiter. + */ + public NbtConfigurationSerializer(String dataTypeDelimiter) { + this.dataTypeDelimiter = dataTypeDelimiter; + } + + /** + * Retrieve the current data type delimiter. + * @return The current data type delimiter. + */ + public String getDataTypeDelimiter() { + return dataTypeDelimiter; + } + + /** + * Write the content of a NBT tag to a configuration section. + * @param value - the NBT tag to write. + * @param destination - the destination section. + */ + public void serialize(NbtBase value, final ConfigurationSection destination) { + value.accept(new NbtVisitor() { + private ConfigurationSection current = destination; + + // The current list we're working on + private List currentList; + + // Store the index of a configuration section that works like a list + private Map workingIndex = Maps.newHashMap(); + + @Override + public boolean visitEnter(NbtCompound compound) { + current = current.createSection(compound.getName()); + return true; + } + + @Override + public boolean visitEnter(NbtList list) { + Integer listIndex = getNextIndex(); + String name = getEncodedName(list, listIndex); + + if (list.getElementType().isComposite()) { + // Use a configuration section to store this list + current = current.createSection(name); + workingIndex.put(current, 0); + } else { + currentList = Lists.newArrayList(); + current.set(name, currentList); + } + return true; + } + + @Override + public boolean visitLeave(NbtCompound compound) { + current = current.getParent(); + return true; + } + + @Override + public boolean visitLeave(NbtList list) { + // Write the list to the configuration section + if (currentList != null) { + // Save and reset the temporary list + currentList = null; + } else { + // Go up a level + workingIndex.remove(current); + current = current.getParent(); + } + return true; + } + + @Override + public boolean visit(NbtBase node) { + // Are we working on a list? + if (currentList == null) { + Integer listIndex = getNextIndex(); + String name = getEncodedName(node, listIndex); + + // Save member + current.set(name, node.getValue()); + + } else { + currentList.add(node.getValue()); + } + return true; + } + + private Integer getNextIndex() { + Integer listIndex = workingIndex.get(current); + + if (listIndex != null) + return workingIndex.put(current, listIndex + 1); + else + return null; + } + + // We need to store the data type somehow + private String getEncodedName(NbtBase node, Integer index) { + if (index != null) + return index + dataTypeDelimiter + node.getType().getRawID(); + else + return node.getName() + dataTypeDelimiter + node.getType().getRawID(); + } + + private String getEncodedName(NbtList node, Integer index) { + if (index != null) + return index + dataTypeDelimiter + node.getElementType().getRawID(); + else + return node.getName() + dataTypeDelimiter + node.getElementType().getRawID(); + } + }); + } + + /** + * Read a NBT tag from a root configuration. + * @param root - configuration that contains the NBT tag. + * @param nodeName - name of the NBT tag. + * @return The read NBT tag. + */ + @SuppressWarnings("unchecked") + public NbtWrapper deserialize(ConfigurationSection root, String nodeName) { + return (NbtWrapper) readNode(root, nodeName); + } + + /** + * Read a NBT compound from a root configuration. + * @param root - configuration that contains the NBT compound. + * @param nodeName - name of the NBT compound. + * @return The read NBT compound. + */ + public NbtCompound deserializeCompound(YamlConfiguration root, String nodeName) { + return (NbtCompound) readNode(root, nodeName); + } + + /** + * Read a NBT compound from a root configuration. + * @param root - configuration that contains the NBT compound. + * @param nodeName - name of the NBT compound. + * @return The read NBT compound. + */ + @SuppressWarnings("unchecked") + public NbtList deserializeList(YamlConfiguration root, String nodeName) { + return (NbtList) readNode(root, nodeName); + } + + @SuppressWarnings("unchecked") + private NbtWrapper readNode(ConfigurationSection parent, String name) { + String[] decoded = getDecodedName(name); + Object node = parent.get(name); + NbtType type = NbtType.TAG_END; + + // It's possible that the caller isn't aware of the encoded name itself + if (node == null) { + for (String key : parent.getKeys(false)) { + decoded = getDecodedName(key); + + // Great + if (decoded[0].equals(name)) { + node = parent.get(decoded[0]); + break; + } + } + + // Inform the caller of the problem + if (node == null) { + throw new IllegalArgumentException("Unable to find node " + name + " in " + parent); + } + } + + // Attempt to decode a NBT type + if (decoded.length > 1) { + type = NbtType.getTypeFromID(Integer.parseInt(decoded[1])); + } + + // Is this a compound? + if (node instanceof ConfigurationSection) { + // Is this a list of a map? + if (type != NbtType.TAG_END) { + NbtList list = NbtFactory.ofList(decoded[0]); + ConfigurationSection section = (ConfigurationSection) node; + List sorted = sortSet(section.getKeys(false)); + + // Read everything in order + for (String key : sorted) { + NbtBase base = (NbtBase) readNode(section, key.toString()); + base.setName(NbtList.EMPTY_NAME); + list.getValue().add(base); + } + return (NbtWrapper) list; + + } else { + NbtCompound compound = NbtFactory.ofCompound(decoded[0]); + ConfigurationSection section = (ConfigurationSection) node; + + // As above + for (String key : section.getKeys(false)) + compound.put(readNode(section, key)); + return (NbtWrapper) compound; + } + + } else { + // We need to know + if (type == NbtType.TAG_END) { + throw new IllegalArgumentException("Cannot find encoded type of " + decoded[0] + " in " + name); + } + + if (node instanceof List) { + NbtList list = NbtFactory.ofList(decoded[0]); + list.setElementType(type); + + for (Object value : (List) node) { + list.addClosest(value); + } + + // Add the list + return (NbtWrapper) list; + + } else { + // Normal node + return NbtFactory.ofWrapper(type, decoded[0], node); + } + } + } + + private List sortSet(Set unsorted) { + // Convert to integers + List sorted = new ArrayList(unsorted); + + Collections.sort(sorted, new Comparator() { + @Override + public int compare(String o1, String o2) { + // Parse the name + int index1 = Integer.parseInt(getDecodedName(o1)[0]); + int index2 = Integer.parseInt(getDecodedName(o2)[0]); + return Ints.compare(index1, index2); + } + }); + return sorted; + } + + private String[] getDecodedName(String nodeName) { + int delimiter = nodeName.lastIndexOf('$'); + + if (delimiter > 0) + return new String[] { nodeName.substring(0, delimiter), nodeName.substring(delimiter + 1) }; + else + return new String[] { nodeName }; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/io/NbtTextSerializer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/io/NbtTextSerializer.java new file mode 100644 index 00000000..b64ddb55 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/io/NbtTextSerializer.java @@ -0,0 +1,119 @@ +package com.comphenix.protocol.wrappers.nbt.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; + +import com.comphenix.protocol.wrappers.nbt.NbtBase; +import com.comphenix.protocol.wrappers.nbt.NbtCompound; +import com.comphenix.protocol.wrappers.nbt.NbtList; +import com.comphenix.protocol.wrappers.nbt.NbtWrapper; + +/** + * Serializes NBT to a base N (default 32) encoded string and back. + * + * @author Kristian + */ +public class NbtTextSerializer { + /** + * The default radix to use while converting to text. + */ + public static final int STANDARD_BASE = 32; + + /** + * A default instance of this serializer. + */ + public static final NbtTextSerializer DEFAULT = new NbtTextSerializer(); + + private NbtBinarySerializer binarySerializer; + private int baseRadix; + + public NbtTextSerializer() { + this(new NbtBinarySerializer(), STANDARD_BASE); + } + + /** + * Construct a serializer with a custom binary serializer and base radix. + * @param binary - binary serializer. + * @param baseRadix - base radix in the range 2 - 32. + */ + public NbtTextSerializer(NbtBinarySerializer binary, int baseRadix) { + this.binarySerializer = binary; + this.baseRadix = baseRadix; + } + + /** + * Retrieve the binary serializer that is used. + * @return The binary serializer. + */ + public NbtBinarySerializer getBinarySerializer() { + return binarySerializer; + } + + /** + * Retrieve the base radix. + * @return The base radix. + */ + public int getBaseRadix() { + return baseRadix; + } + + /** + * Serialize a NBT tag to a String. + * @param value - the NBT tag to serialize. + * @return The NBT tag in base N form. + */ + public String serialize(NbtBase value) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + DataOutputStream dataOutput = new DataOutputStream(outputStream); + + binarySerializer.serialize(value, dataOutput); + + // Serialize that array + return new BigInteger(1, outputStream.toByteArray()).toString(baseRadix); + } + + /** + * Deserialize a NBT tag from a base N encoded string. + * @param input - the base N string. + * @return The NBT tag contained in the string. + * @throws IOException If we are unable to parse the input. + */ + public NbtWrapper deserialize(String input) throws IOException { + try { + BigInteger baseN = new BigInteger(input, baseRadix); + ByteArrayInputStream inputStream = new ByteArrayInputStream(baseN.toByteArray()); + + return binarySerializer.deserialize(new DataInputStream(inputStream)); + + } catch (NumberFormatException e) { + throw new IOException("Input is not valid base " + baseRadix + ".", e); + } + } + + /** + * Deserialize a NBT compound from a base N encoded string. + * @param input - the base N string. + * @return The NBT tag contained in the string. + * @throws IOException If we are unable to parse the input. + */ + @SuppressWarnings("rawtypes") + public NbtCompound deserializeCompound(String input) throws IOException { + // I always seem to override generics ... + return (NbtCompound) (NbtBase) deserialize(input); + } + + /** + * Deserialize a NBT list from a base N encoded string. + * @param input - the base N string. + * @return The NBT tag contained in the string. + * @throws IOException If we are unable to parse the input. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public NbtList deserializeList(String input) throws IOException { + return (NbtList) (NbtBase) deserialize(input); + } +} diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtCompoundTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtCompoundTest.java index 3b807eb3..bfeea39d 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtCompoundTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtCompoundTest.java @@ -90,6 +90,11 @@ public class NbtCompoundTest { @Override public NbtBase deepClone() { return new NbtCustomTag(name, value); + } + + @Override + public boolean accept(NbtVisitor visitor) { + return visitor.visit(this); } } } diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtFactoryTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtFactoryTest.java index f31d301b..c2504df0 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtFactoryTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/NbtFactoryTest.java @@ -30,6 +30,7 @@ import org.junit.BeforeClass; import org.junit.Test; import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer; public class NbtFactoryTest { @BeforeClass @@ -54,7 +55,7 @@ public class NbtFactoryTest { ByteArrayInputStream source = new ByteArrayInputStream(buffer.toByteArray()); DataInput input = new DataInputStream(source); - NbtCompound cloned = (NbtCompound) NbtFactory.fromStream(input); + NbtCompound cloned = NbtBinarySerializer.DEFAULT.deserializeCompound(input); assertEquals(compound.getString("name"), cloned.getString("name")); assertEquals(compound.getInteger("age"), cloned.getInteger("age")); diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/io/NbtConfigurationSerializerTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/io/NbtConfigurationSerializerTest.java new file mode 100644 index 00000000..3b3888a3 --- /dev/null +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/nbt/io/NbtConfigurationSerializerTest.java @@ -0,0 +1,37 @@ +package com.comphenix.protocol.wrappers.nbt.io; + +import static org.junit.Assert.*; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.nbt.NbtCompound; +import com.comphenix.protocol.wrappers.nbt.NbtFactory; + +public class NbtConfigurationSerializerTest { + @BeforeClass + public static void initializeBukkit() { + // Initialize reflection + MinecraftReflection.setMinecraftPackage("net.minecraft.server.v1_4_6", "org.bukkit.craftbukkit.v1_4_6"); + } + + @SuppressWarnings("unchecked") + @Test + public void testSerialization() { + NbtCompound compound = NbtFactory.ofCompound("hello"); + compound.put("age", (short) 30); + compound.put("name", "test"); + compound.put(NbtFactory.ofList("telephone", "12345678", "81549300")); + + compound.put(NbtFactory.ofList("lists", NbtFactory.ofList("", "a", "a", "b", "c"))); + + YamlConfiguration yaml = new YamlConfiguration(); + NbtConfigurationSerializer.DEFAULT.serialize(compound, yaml); + + NbtCompound result = NbtConfigurationSerializer.DEFAULT.deserializeCompound(yaml, "hello"); + + assertEquals(compound, result); + } +}