package net.glowstone.util.nbt;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
/**
* This class writes NBT, or Named Binary Tag, {@link Tag} objects to an
* underlying {@link OutputStream}.
* <p/>
* The NBT format was created by Markus Persson, and the specification may
* be found at <a href="http://www.minecraft.net/docs/NBT.txt">
* http://www.minecraft.net/docs/NBT.txt</a>.
*/
public final class NBTOutputStream implements Closeable {
/**
* The output stream.
*/
private final DataOutputStream os;
/**
* Creates a new NBTOutputStream, which will write data to the
* specified underlying output stream. This assumes the output stream
* should be compressed with GZIP.
* @param os The output stream.
* @throws IOException if an I/O error occurs.
*/
public NBTOutputStream(OutputStream os) throws IOException {
this(os, true);
}
/**
* Creates a new NBTOutputStream, which will write data to the
* specified underlying output stream. A flag indicates if the output
* should be compressed with GZIP or not.
* @param os The output stream.
* @param compressed A flag that indicates if the output should be compressed.
* @throws IOException if an I/O error occurs.
*/
@SuppressWarnings("resource")
public NBTOutputStream(OutputStream os, boolean compressed) throws IOException {
this.os = new DataOutputStream(compressed ? new GZIPOutputStream(os) : os);
}
/**
* Write a tag with a blank name (the root tag) to the stream.
* @param tag The tag to write.
* @throws IOException if an I/O error occurs.
*/
public void writeTag(Tag tag) throws IOException {
writeTag("", tag);
}
/**
* Write a tag with a name.
* @param name The name to give the written tag.
* @param tag The tag to write.
* @throws IOException if an I/O error occurs.
*/
private void writeTag(String name, Tag tag) throws IOException {
TagType type = tag.getType();
byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
if (type == TagType.END) {
throw new IOException("Named TAG_End not permitted.");
}
os.writeByte(type.getId());
os.writeShort(nameBytes.length);
os.write(nameBytes);
writeTagPayload(tag);
}
/**
* Writes tag payload.
* @param tag The tag.
* @throws IOException if an I/O error occurs.
*/
@SuppressWarnings("unchecked")
private void writeTagPayload(Tag tag) throws IOException {
TagType type = tag.getType();
byte[] bytes;
switch (type) {
case BYTE:
os.writeByte((byte) tag.getValue());
break;
case SHORT:
os.writeShort((short) tag.getValue());
break;
case INT:
os.writeInt((int) tag.getValue());
break;
case LONG:
os.writeLong((long) tag.getValue());
break;
case FLOAT:
os.writeFloat((float) tag.getValue());
break;
case DOUBLE:
os.writeDouble((double) tag.getValue());
break;
case BYTE_ARRAY:
bytes = (byte[]) tag.getValue();
os.writeInt(bytes.length);
os.write(bytes);
break;
case STRING:
bytes = ((StringTag) tag).getValue().getBytes(StandardCharsets.UTF_8);
os.writeShort(bytes.length);
os.write(bytes);
break;
case LIST:
ListTag<Tag> listTag = (ListTag<Tag>) tag;
List<Tag> tags = listTag.getValue();
os.writeByte(listTag.getChildType().getId());
os.writeInt(tags.size());
for (Tag child : tags) {
writeTagPayload(child);
}
break;
case COMPOUND:
Map<String, Tag> map = ((CompoundTag) tag).getValue();
for (Map.Entry<String, Tag> entry : map.entrySet()) {
writeTag(entry.getKey(), entry.getValue());
}
os.writeByte((byte) 0); // end tag
break;
case INT_ARRAY:
int[] ints = (int[]) tag.getValue();
os.writeInt(ints.length);
for (int value : ints) {
os.writeInt(value);
}
break;
default:
throw new IOException("Invalid tag type: " + type + ".");
}
}
@Override
public void close() throws IOException {
os.close();
}
}