/* * This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT). * * Copyright (c) JCThePants (www.jcwhatever.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.jcwhatever.nucleus.utils.file; import com.jcwhatever.nucleus.Nucleus; import com.jcwhatever.nucleus.managed.items.meta.IItemMetaHandler; import com.jcwhatever.nucleus.managed.items.meta.ItemMetaValue; import com.jcwhatever.nucleus.utils.coords.SyncLocation; import org.bukkit.Location; import org.bukkit.inventory.ItemStack; import org.bukkit.util.EulerAngle; import org.bukkit.util.Vector; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.UUID; import javax.annotation.Nullable; /** * Write bytes to a stream. In order to read the stream * properly, {@link BasicByteReader} needs to be used. */ public class BasicByteWriter extends OutputStream implements IByteWriter { private static final byte[] BOOLEAN_FLAGS = new byte[] { 1, 2, 4, 8, 16, 32, 64 }; private final OutputStream _stream; private final byte[] _buffer = new byte[64]; private long _bytesWritten = 0; private int _booleanCount = 0; private byte[] _booleanBuffer = new byte[1]; /** * Constructor. * * @param outputStream The output stream. */ public BasicByteWriter(OutputStream outputStream) { _stream = outputStream; } /** * Get the number of bytes written. */ @Override public long getBytesWritten() { return _bytesWritten; } /** * Write a boolean value. * * <p>Booleans written sequentially are written as bits into the current byte. When the byte * runs out of bits, the next bit is written to the next byte. Because of this, 7 boolean * values written sequentially use only 1 byte of space in the stream instead of 7 bytes.</p> * * <p>Bytes that store the boolean values do not share their bits with other data types.</p> * * @param booleanValue The boolean value. * * @throws IOException */ @Override public void write(boolean booleanValue) throws IOException { if (_booleanCount == 7) { writeBooleans(); } if (booleanValue) { _booleanBuffer[0] |= BOOLEAN_FLAGS[_booleanCount]; } _booleanCount++; } /** * Write a byte. * * @param byteValue The byte. * * @throws IOException */ @Override public void write(byte byteValue) throws IOException { // write buffered booleans writeBooleans(); _stream.write(byteValue); _bytesWritten++; } /** * Write a byte array. * * <p>Writes an array by first writing an integer (4 bytes) which * indicates the length of the array, then writes the entire byte array.</p> * * <p>If the array is null or empty then the integer 0 (4 bytes) is * written and no array bytes are added.</p> * * @param byteArray The byte array. * * @throws IOException */ @Override public void write(@Nullable byte[] byteArray) throws IOException { // write buffered booleans writeBooleans(); if (byteArray == null) { write(-1); return; } else if (byteArray.length == 0) { write(0); return; } write(byteArray.length); _stream.write(byteArray, 0, byteArray.length); _bytesWritten += byteArray.length; } /** * Write a 16-bit number (2 bytes). * * @param shortValue The short. * * @throws IOException */ @Override public void write(short shortValue) throws IOException { // write buffered booleans writeBooleans(); _buffer[0] = (byte)(shortValue >> 8 & 0xFF); _buffer[1] = (byte)(shortValue & 0xFF); _stream.write(_buffer, 0, 2); _bytesWritten += 2; } /** * Write a 32-bit number (4 bytes). * * @param integerValue The integer. * * @throws IOException */ @Override public void write(int integerValue) throws IOException { // write buffered booleans writeBooleans(); _buffer[0] = (byte)(integerValue >> 24 & 0xFF); _buffer[1] = (byte)(integerValue >> 16 & 0xFF); _buffer[2] = (byte)(integerValue >> 8 & 0xFF); _buffer[3] = (byte)(integerValue & 0xFF); _stream.write(_buffer, 0, 4); _bytesWritten+=4; } /** * Write a 64 bit number (8 bytes). * * @param longValue The long. * * @throws IOException */ @Override public void write(long longValue) throws IOException { // write buffered booleans writeBooleans(); _buffer[0] = (byte)(longValue >> 56 & 0xFF); _buffer[1] = (byte)(longValue >> 48 & 0xFF); _buffer[2] = (byte)(longValue >> 40 & 0xFF); _buffer[3] = (byte)(longValue >> 32 & 0xFF); _buffer[4] = (byte)(longValue >> 24 & 0xFF); _buffer[5] = (byte)(longValue >> 16 & 0xFF); _buffer[6] = (byte)(longValue >> 8 & 0xFF); _buffer[7] = (byte)(longValue & 0xFF); _stream.write(_buffer, 0, 8); _bytesWritten+=8; } /** * Write a floating point number. * * <p>Float values are written as a UTF-8 byte array preceded with 1 byte * to indicate the number of bytes in the array.</p> * * @param floatValue The floating point value. * * @throws IOException */ @Override public void write(float floatValue) throws IOException { if (Float.isNaN(floatValue)) { writeSmallString("NaN"); } else if (Float.isInfinite(floatValue)) { if (floatValue < 0) writeSmallString("nInf"); else writeSmallString("pInf"); } else { writeSmallString(String.valueOf(floatValue)); } } /** * Write a double number. * * <p>Double values are written using a UTF-8 byte array preceded with 1 byte * to indicate the number of bytes in the array.</p> * * @param doubleValue The floating point double. * * @throws IOException */ @Override public void write(double doubleValue) throws IOException { if (Double.isNaN(doubleValue)) { writeSmallString("NaN"); } else if (Double.isInfinite(doubleValue)) { if (doubleValue < 0) writeSmallString("nInf"); else writeSmallString("pInf"); } else { writeSmallString(String.valueOf(doubleValue)); } } /** * Write a {@link BigDecimal} number. * * <p>Serializes number using {@link ObjectOutputStream}. * (See {@link #write(Serializable)}).</p> * * @param decimal The big decimal. * * @throws IOException */ @Override public void write(@Nullable BigDecimal decimal) throws IOException { write((Serializable) decimal); } /** * Write a {@link BigInteger} number. * * <p>Serializes number using {@link ObjectOutputStream}. * (See {@link #write(Serializable)}).</p> * * @param integer The big integer. * * @throws IOException */ @Override public void write(@Nullable BigInteger integer) throws IOException { write((Serializable) integer); } /** * Write a text string using UTF-16 encoding. * * <p>The first 2 bytes (short) written indicate the length * of the string in bytes.</p> * * <p>If the string is null then the short -1 (2 bytes) is written * and no array bytes are written.</p> * * <p>If the string is empty then the short 0 (2 bytes) is written * and no array bytes are written.</p> * * @param text The text to write. Can be null. * * @throws IOException */ @Override public void write(@Nullable String text) throws IOException { write(text, StandardCharsets.UTF_16); } /** * Write a text string. * * <p>The first 2 bytes (short) written indicate the length * of the string in bytes.</p> * * <p>If the string is null then the short -1 (2 bytes) is written * and no array bytes are written.</p> * * <p>If the string is empty then the short 0 (2 bytes) is written * and no array bytes are written.</p> * * @param text The text to write. Can be null. * @param charset The charset encoding to use. * * @throws IOException */ @Override public void write(@Nullable String text, Charset charset) throws IOException { // write buffered booleans writeBooleans(); // handle null text if (text == null) { write((short)-1); return; } // handle empty text else if (text.length() == 0) { write((short)0); return; } byte[] bytes = text.getBytes(charset); // write string byte length write((short)bytes.length); // write string bytes _stream.write(bytes); // record bytes written _bytesWritten+=bytes.length; } /** * Write a text string that is expected to be no more than 255 bytes in length * using UTF-8 encoding. * * <p>The first byte written indicate the length of the string in bytes.</p> * * <p>If the string is null then the byte -1 is written * and no array bytes are written.</p> * * <p>If the string is empty then the byte 0 is written * and no array bytes are written.</p> * * @param text The text to write. Can be null. * * @throws IOException */ @Override public void writeSmallString(@Nullable String text) throws IOException { // write buffered booleans writeBooleans(); // handle null text if (text == null) { write((byte)-1); return; } // handle empty text else if (text.length() == 0) { write((byte)0); return; } byte[] bytes = text.getBytes(StandardCharsets.UTF_8); if (bytes.length > Byte.MAX_VALUE) throw new IllegalArgumentException("text cannot be larger than " + Byte.MAX_VALUE + " in length."); // write string byte length write((byte)bytes.length); // write string bytes _stream.write(bytes); // record bytes written _bytesWritten+=bytes.length; } /** * Write an enum. * * <p>Enum values are written using a UTF-8 byte array preceded with 1 byte * to indicate the number of bytes in the array. The UTF-8 byte array * is the string value of the enum constants name.</p> * * @param enumConstant The enum constant. * * @param <T> The enum type. * * @throws IOException */ @Override public <T extends Enum<T>> void write(@Nullable T enumConstant) throws IOException { if (enumConstant == null) { writeSmallString(null); return; } writeSmallString(enumConstant.name()); } /** * Write a UUID. * * <p>The UUID is written as 16-7 bytes. A boolean is written first to indicate if * the value is null. If the value is not null, the next 8 bytes are the most * significant bits while the last 8 bytes are the least significant bits.</p> * * @param uuid The UUID to write. * * @throws IOException */ @Override public void write(@Nullable UUID uuid) throws IOException { if (uuid == null) { write(false); return; } write(true); write(uuid.getMostSignificantBits()); write(uuid.getLeastSignificantBits()); } /** * Write a {@link Location}. * * <p>The location is written as follows:</p> * * <ul> * <li>The world name - UTF-8 String preceded with a single byte to indicate length.</li> * <li>The X value - Double (See {@link #write(double)})</li> * <li>The Y value - Double (See {@link #write(double)})</li> * <li>The Z value - Double (See {@link #write(double)})</li> * <li>The Yaw value - Double (See {@link #write(double)})</li> * <li>The Pitch value - Double (See {@link #write(double)})</li> * </ul> * * @param location The location. * * @throws IOException */ @Override public void write(@Nullable Location location) throws IOException { if (location == null) { writeSmallString(null); writeSmallString(null); return; } String worldName = null; if (location instanceof SyncLocation) { worldName = ((SyncLocation) location).getWorldName(); } else if (location.getWorld() != null) { worldName = location.getWorld().getName(); } writeSmallString(worldName); write(location.getX()); write(location.getY()); write(location.getZ()); write(location.getYaw()); write(location.getPitch()); } /** * Write an {@link EulerAngle}. * * The angle is written as three doubles representing * x, y and z. (See {@link #write(double)}. * * @param angle The angle. * * @throws IOException */ @Override public void write(@Nullable EulerAngle angle) throws IOException { if (angle == null) { writeSmallString(null); } else { write(angle.getX()); write(angle.getY()); write(angle.getZ()); } } /** * Write a {@link Vector}. * * <p>The vector is written as three doubles representing * x, y and z. (See {@link #write(double)}</p> * * @param vector The vector. * * @throws IOException */ @Override public void write(@Nullable Vector vector) throws IOException { if (vector == null) { writeSmallString(null); } else { write(vector.getX()); write(vector.getY()); write(vector.getZ()); } } /** * Write an {@link ItemStack}. * * <p>Writes the item stack as follows:</p> * <ul> * <li>Boolean (bit or byte depending on the data structure) indicating * if the item stack is null. 0 = null. (See {@link #write(boolean)})</li> * <li>Material - Enum (See {@link #write(Enum)})</li> * <li>Durability - Short (See {@link #write(int)})</li> * <li>Meta count - Integer (See {@link #write(int)})</li> * <li>Meta collection</li> * </ul> * * <p>Meta is written first with an integer to indicate the number of * meta elements followed by the elements, each with the following format:</p> * <ul> * <li>Meta Name - UTF-8 String preceded with a byte to indicate length. * (See {@link #writeSmallString(String)}</li> * <li>Meta Data - UTF-16 String (See {@link #write(String)})</li> * </ul> * * @param itemStack The item stack. * * @throws IOException */ @Override public void write(@Nullable ItemStack itemStack) throws IOException { write(itemStack != null); if (itemStack == null) return; // write basic data write(itemStack.getType()); write((int)itemStack.getDurability()); write(itemStack.getAmount()); Collection<IItemMetaHandler> handlers = Nucleus.getItemMetaHandlers().getHandlers(); List<ItemMetaValue> metaObjects = new ArrayList<>(10); for (IItemMetaHandler handler : handlers) { metaObjects.addAll(handler.getMeta(itemStack)); } write(metaObjects.size()); for (ItemMetaValue metaObject : metaObjects) { writeSmallString(metaObject.getName()); write(metaObject.getRawData(), StandardCharsets.UTF_16); } } /** * Serialize an {@link IByteSerializable} object. * * <p>A boolean is written to indicate if the object is null (0 = null) and if not * null the object serializes itself into the stream.</p> * * @param object The object to serialize. * * @param <T> The object type. * * @throws IOException */ @Override public <T extends IByteSerializable> void write(@Nullable T object) throws IOException { write(object != null); if (object == null) return; object.serialize(this); } /** * Serialize an object. * * <p>A boolean is written indicating if the object is null (0 = null) and if not * null the object is serialized using an {@link ObjectOutputStream}.</p> * * @param object The object to serialize. Can be null. * * @throws IOException */ @Override public <T extends Serializable> void write(@Nullable T object) throws IOException { // write null flag write(object != null); if (object == null) return; // clear boolean buffer writeBooleans(); ObjectOutputStream objectStream = new ObjectOutputStream(this); objectStream.writeObject(object); } @Override public void close() throws IOException { flush(); _stream.close(); } @Override public void flush() throws IOException { // write buffered booleans writeBooleans(); _stream.flush(); } private void writeBooleans() throws IOException { if (_booleanCount > 0) { _stream.write(_booleanBuffer, 0, 1); _booleanBuffer[0] = 0; _bytesWritten++; _booleanCount = 0; } } }