/*
* 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.EnumUtils;
import com.jcwhatever.nucleus.utils.PreCon;
import com.jcwhatever.nucleus.utils.coords.SyncLocation;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.EulerAngle;
import org.bukkit.util.Vector;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* Read bytes from a stream. Bytes from the stream need to have been
* generated by {@link BasicByteWriter} in order to be read.
*/
public class BasicByteReader extends InputStream implements IByteReader {
private static final byte[] BOOLEAN_FLAGS = new byte[] { 1, 2, 4, 8, 16, 32, 64 };
private final InputStream _stream;
private final byte[] _buffer = new byte[1024];
private long _bytesRead = 0;
private int _booleanReadCount = 7; // resets to 7
private byte[] _booleanBuffer = new byte[1];
/**
* Constructor.
*
* @param stream The input stream.
*/
public BasicByteReader(InputStream stream) {
PreCon.notNull(stream);
_stream = stream;
}
/**
* Get the number of bytes read so far.
*/
@Override
public long getBytesRead() {
return _bytesRead;
}
@Override
public int read() throws IOException {
resetBooleanBuffer();
_bytesRead++;
return _stream.read();
}
/**
* Skip over a number of bytes without returning them.
*
* @param byteDistance The number of bytes to skip.
*
* @return The number of bytes skipped.
*
* @throws IOException
*/
@Override
public long skip(long byteDistance) throws IOException {
return _stream.skip(byteDistance);
}
/**
* Get a boolean.
*
* <p>Booleans read sequentially are read as bits from the current byte. When the byte
* runs out of bits, the next bit is read from the next byte.</p>
*
* <p>Bytes that store boolean values do not share bits with other data types.</p>
*
* @throws IOException
*/
@Override
public boolean getBoolean() throws IOException {
if (_booleanReadCount == 7) {
_bytesRead += (byte)_stream.read(_booleanBuffer, 0, 1);
_booleanReadCount = 0;
}
boolean result = (_booleanBuffer[0] & BOOLEAN_FLAGS[_booleanReadCount])
== BOOLEAN_FLAGS[_booleanReadCount];
_booleanReadCount++;
return result;
}
/**
* Get the next byte.
*
* @throws IOException
*/
@Override
public byte getByte() throws IOException {
resetBooleanBuffer();
_bytesRead += _stream.read(_buffer, 0, 1);
return _buffer[0];
}
/**
* Get the next byte array.
*
* <p>Gets an array by first reading an integer (4 bytes) which
* indicates the length of the array, then reads the number of bytes indicated.</p>
*
* <p>If the number of bytes indicated is 0, then an empty byte array is returned.</p>
*
* @throws IOException
*/
@Override
public byte[] getBytes() throws IOException {
int size = getInteger();
if (size == -1)
return null;
if (size == 0)
return new byte[0];
byte[] bytes = new byte[size];
_bytesRead += _stream.read(bytes);
return bytes;
}
/**
* Read the next 2 bytes and return them as a short.
*
* @throws IOException
*/
@Override
public short getShort() throws IOException {
resetBooleanBuffer();
_bytesRead += (long)_stream.read(_buffer, 0, 2);
return (short)(((_buffer[0] & 0xFF) << 8)
+ (_buffer[1] & 0xFF));
}
/**
* Read the next 4 bytes and return them as an integer.
*
* @throws IOException
*/
@Override
public int getInteger() throws IOException {
resetBooleanBuffer();
_bytesRead += (long)_stream.read(_buffer, 0, 4);
return ((_buffer[0] & 0xFF) << 24)
+ ((_buffer[1] & 0xFF) << 16)
+ ((_buffer[2] & 0xFF) << 8)
+ (_buffer[3] & 0xFF);
}
/**
* Read the next 8 bytes an return them as a long value.
*
* @throws IOException
*/
@Override
public long getLong() throws IOException {
resetBooleanBuffer();
_bytesRead+=(long)_stream.read(_buffer, 0, 8);
return ((_buffer[0] & 0xFFL) << 56)
+ ((_buffer[1] & 0xFFL) << 48)
+ ((_buffer[2] & 0xFFL) << 40)
+ ((_buffer[3] & 0xFFL) << 32)
+ ((_buffer[4] & 0xFFL) << 24)
+ ((_buffer[5] & 0xFFL) << 16)
+ ((_buffer[6] & 0xFFL) << 8)
+ (_buffer[7] & 0xFFL);
}
/**
* Read the next group of bytes as a {@link BigDecimal}.
*
* <p>The data is deserialized using an {@link ObjectInputStream}.</p>
*
* <p>If the {@link BigDecimal} that was written was null, then
* null is returned.</p>
*
* @throws IOException
*/
@Override
@Nullable
public BigDecimal getBigDecimal() throws IOException {
try {
return deserializeObject(BigDecimal.class);
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new AssertionError();
}
}
/**
* Read the next group of bytes as a {@link BigInteger}.
*
* <p>The data is deserialized using an {@link ObjectInputStream}.</p>
*
* <p>If the {@link BigInteger} that was written was null, then
* null is returned.</p>
*
* @throws IOException
*/
@Override
@Nullable
public BigInteger getBigInteger() throws IOException {
try {
return deserializeObject(BigInteger.class);
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new AssertionError();
}
}
/**
* Get the next group of bytes as a UTF-16 string.
*
* <p>The first 2 bytes (short) read indicate the length of the string in bytes.
* The number of bytes indicated is the number of bytes encoded into the
* returned string.</p>
*
* <p>If the original string written was null, then null is returned.</p>
*
* @throws IOException
*/
@Override
@Nullable
public String getString() throws IOException {
return getString(StandardCharsets.UTF_16);
}
/**
* Get the next group of bytes as a string.
*
* <p>The first 2 bytes (short) read indicate the length of the string in bytes.
* The number of bytes indicated is the number of bytes encoded into the
* returned string.</p>
*
* <p>If the original string written was null, then null is returned.</p>
*
* @param charset The character set encoding to use.
*
* @throws IOException
*/
@Override
@Nullable
public String getString(Charset charset) throws IOException {
PreCon.notNull(charset);
resetBooleanBuffer();
int len = getShort();
if (len == -1)
return null;
if (len == 0)
return "";
if (len >= _buffer.length) {
byte[] buffer = new byte[len];
_bytesRead += (long) _stream.read(buffer, 0, len);
return new String(buffer, 0, len, charset);
}
else {
_bytesRead += (long) _stream.read(_buffer, 0, len);
return new String(_buffer, 0, len, charset);
}
}
/**
* Get the next group of bytes as a UTF-8 string that is expected to be
* no more than 255 bytes in length.
*
* <p>The first byte read indicates the length of the string in bytes.
* The number of bytes indicated is the number of bytes encoded into the
* returned string.</p>
*
* <p>If the original string written was null, then null is returned.</p>
*
* @throws IOException
*/
@Override
@Nullable
public String getSmallString() throws IOException {
resetBooleanBuffer();
int len = getByte();
if (len == -1)
return null;
if (len == 0)
return "";
if (len >= _buffer.length) {
byte[] buffer = new byte[len];
_bytesRead += (long) _stream.read(buffer, 0, len);
return new String(buffer, 0, len, StandardCharsets.UTF_8);
}
else {
_bytesRead += (long) _stream.read(_buffer, 0, len);
return new String(_buffer, 0, len, StandardCharsets.UTF_8);
}
}
/**
* Get the next group of bytes as a float value.
*
* <p>Float values are read as a UTF-8 string byte array preceded with 1 byte
* to indicate the number of bytes in the array.</p>
*
* @throws IOException
*/
@Override
public float getFloat() throws IOException {
resetBooleanBuffer();
String str = getSmallString();
if (str == null || str.isEmpty())
throw new IOException("Failed to read float value.");
if (str.equals("NaN"))
return Float.NaN;
if (str.equals("nInf"))
return Float.NEGATIVE_INFINITY;
if (str.equals("pInf"))
return Float.POSITIVE_INFINITY;
try {
return Float.parseFloat(str);
}
catch (NumberFormatException e) {
throw new IOException("Failed to parse data to float value.");
}
}
/**
* Get the next group of bytes as a double value.
*
* <p>Double values are read as a UTF-8 string byte array preceded with 1 byte
* to indicate the number of bytes in the array.</p>
*
* @throws IOException
*/
@Override
public double getDouble() throws IOException {
resetBooleanBuffer();
String str = getSmallString();
if (str == null || str.isEmpty())
throw new IOException("Failed to read double value.");
if (str.equals("NaN"))
return Double.NaN;
if (str.equals("nInf"))
return Double.NEGATIVE_INFINITY;
if (str.equals("pInf"))
return Double.POSITIVE_INFINITY;
try {
return Double.parseDouble(str);
}
catch (NumberFormatException e) {
throw new IOException("Failed to parse data to double value.");
}
}
/**
* Get the next group of bytes as an enum.
*
* <p>Enum values are stored using a UTF-8 byte array preceded with 1 byte
* to indicate the number of UTF-8 bytes. The UTF-8 byte array is the string
* value of the enum constants name.</p>
*
* @param enumClass The enum class.
*
* @param <T> The enum type.
*
* @throws IOException
*/
@Override
@Nullable
public <T extends Enum<T>> T getEnum(Class<T> enumClass) throws IOException {
PreCon.notNull(enumClass);
resetBooleanBuffer();
String constantName = getSmallString();
if (constantName == null) {
return null;
}
T e = EnumUtils.getEnum(constantName, enumClass);
if (e == null) {
throw new IOException(
"The enum name retrieved is not a valid constant name for " +
"enum type: " + enumClass.getName());
}
return e;
}
/**
* Get the next 16-17 bytes as a UUID.
*
* <p>The UUID is read as a boolean value (See {@link #getBoolean}. If
* the boolean value is "true", the next 16 bytes are read as the UUID. If
* the boolean value is "false", null is returned.</p>
*
* @throws IOException
*/
@Override
@Nullable
public UUID getUUID() throws IOException {
boolean hasValue = getBoolean();
if (hasValue) {
long most = getLong();
long least = getLong();
return new UUID(most, least);
}
else {
return null;
}
}
/**
* Get the next group of bytes as a location.
*
* <p>The location is read as follows:</p>
*
* <ul>
* <li>The world name - UTF-8 byte array preceded with a byte to indicate length.</li>
* <li>The X value - Double (See {@link #getDouble})</li>
* <li>The Y value - Double (See {@link #getDouble})</li>
* <li>The Z value - Double (See {@link #getDouble})</li>
* <li>The Yaw value - Double (See {@link #getDouble})</li>
* <li>The Pitch value - Double (See {@link #getDouble})</li>
* </ul>
*
* @throws IOException
*/
@Override
@Nullable
public SyncLocation getLocation() throws IOException {
return getLocation(new SyncLocation((String)null, 0, 0, 0));
}
/**
* Get the next group of bytes as a location.
*
* <p>The location is read as follows:</p>
*
* <ul>
* <li>The world name - UTF-8 byte array preceded with a byte to indicate length.</li>
* <li>The X value - Double (See {@link #getDouble})</li>
* <li>The Y value - Double (See {@link #getDouble})</li>
* <li>The Z value - Double (See {@link #getDouble})</li>
* <li>The Yaw value - Double (See {@link #getDouble})</li>
* <li>The Pitch value - Double (See {@link #getDouble})</li>
* </ul>
*
* @throws IOException
*/
@Override
@Nullable
public SyncLocation getLocation(SyncLocation output) throws IOException {
PreCon.notNull(output);
output.setWorld(getSmallString());
try {
output.setX(getDouble());
}
catch (Exception e) {
if (e.getMessage().equals("Failed to read double value."))
return null;
throw e;
}
output.setY(getDouble());
output.setZ(getDouble());
output.setYaw(getFloat());
output.setPitch(getFloat());
if (output.getWorldName() != null && Bukkit.isPrimaryThread()) {
output.setWorld(Bukkit.getWorld(output.getWorldName()));
}
return output;
}
/**
* Get the next group of bytes as an EulerAngle.
*
* <p>The angle is read as x, y and z value as doubles.
* (See {@link #getDouble})</p>
*
* @throws IOException
*/
@Override
@Nullable
public EulerAngle getEulerAngle() throws IOException {
return getEulerAngle(new EulerAngle(0, 0, 0));
}
/**
* Get the next group of bytes as an EulerAngle.
*
* <p>The angle is read as x, y and z value as doubles.
* (See {@link #getDouble})</p>
*
* @throws IOException
*/
@Override
@Nullable
public EulerAngle getEulerAngle(EulerAngle output) throws IOException {
PreCon.notNull(output);
try {
output.setX(getDouble());
}
catch (IOException e) {
if (e.getMessage().equals("Failed to read double value."))
return null;
throw e;
}
output.setY(getDouble());
output.setZ(getDouble());
return output;
}
/**
* Get the next group of bytes as a Vector.
*
* <p>The vector is read as x, y, and z value as doubles.</p>
*
* @throws IOException
*/
@Override
@Nullable
public Vector getVector() throws IOException {
return getVector(new Vector(0, 0, 0));
}
/**
* Get the next group of bytes as a Vector.
*
* <p>The vector is read as x, y, and z value as doubles.</p>
*
* @throws IOException
*/
@Override
@Nullable
public Vector getVector(Vector output) throws IOException {
PreCon.notNull(output);
try {
output.setX(getDouble());
}
catch (IOException e) {
if (e.getMessage().equals("Failed to read double value."))
return null;
throw e;
}
output.setY(getDouble());
output.setZ(getDouble());
return output;
}
/**
* Get the next group of bytes as an item stack.
*
* <p>Reads 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 #getBoolean})</li>
* <li>Material - Enum (See {@link #getEnum})</li>
* <li>Durability - Integer (See {@link #getInteger})</li>
* <li>Meta count - Integer (See {@link #getInteger})</li>
* <li>Meta collection</li>
* </ul>
*
* <p>Meta is read as follows:</p>
* <ul>
* <li>Meta Name - UTF-8 String byte array preceded with a byte to indicate length.</li>
* <li>Meta Data - UTF-16 String (See {@link #getString})</li>
* </ul>
*
* @throws IOException
*/
@Override
@Nullable
public ItemStack getItemStack() throws IOException {
boolean isNull = !getBoolean();
if (isNull)
return null;
// read basic data
Material type = getEnum(Material.class);
if (type == null)
throw new IOException("Failed to read ItemStack material.");
short durability = (short)getInteger();
int amount = getInteger();
ItemStack result = new ItemStack(type, amount, durability);
int totalMeta = getInteger();
for (int i=0; i < totalMeta; i++) {
String metaName = getSmallString();
if (metaName == null)
throw new IOException("Failed to read meta name of entry #" + i);
String metaData = getString(StandardCharsets.UTF_16);
if (metaData == null)
throw new IOException("Failed to read meta data of entry #" + i);
IItemMetaHandler handler = Nucleus.getItemMetaHandlers().getHandler(metaName);
if (handler == null)
continue;
ItemMetaValue meta = new ItemMetaValue(metaName, metaData);
handler.apply(result, meta);
}
return result;
}
/**
* Get an {@link IByteSerializable} object.
*
* <p>A boolean is read (See {@link #getBoolean} to indicate if the object
* is null (0 = null) and if not null a new object is instantiate via empty
* constructor and is responsible for deserializing data from the stream
* into itself.</p>
*
* @param objectClass The object class.
*
* @param <T> The object type.
*
* @throws Exception
*/
@Override
@Nullable
public <T extends IByteSerializable> T deserialize(Class<T> objectClass)
throws IOException, InstantiationException {
PreCon.notNull(objectClass);
boolean isNull = !getBoolean();
if (isNull)
return null;
T object;
try {
Constructor<T> constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
object = constructor.newInstance();
object.deserialize(this);
} catch (NoSuchMethodException | IllegalAccessException | ClassNotFoundException |
InvocationTargetException | InstantiationException e) {
e.printStackTrace();
throw new InstantiationException("Failed to instantiate IBinarySerializable: "
+ objectClass.getName());
}
return object;
}
/**
* Deserialize an object from the next set of bytes.
*
* <p>A boolean is read (See {@link #getBoolean} indicating if the object
* is null (0 = null) and if not null the object is deserialized using an
* {@link ObjectInputStream}.</p>
*
* @param objectClass The object class.
*
* @param <T> The object type.
*
* @return The deserialized object or null if the object was written as null.
*
* @throws IOException
* @throws ClassNotFoundException
*/
@Override
@Nullable
public <T extends Serializable> T deserializeObject(Class<T> objectClass)
throws IOException, ClassNotFoundException {
PreCon.notNull(objectClass);
boolean isNull = !getBoolean();
if (isNull)
return null;
resetBooleanBuffer();
ObjectInputStream objectStream = new ObjectInputStream(this);
Object object = objectStream.readObject();
if (!object.getClass().isAssignableFrom(objectClass)) {
throw new ClassNotFoundException("The object returned by the stream is not of the " +
"specified class: " + objectClass.getName());
}
return objectClass.cast(object);
}
/**
* Close the stream.
*
* @throws IOException
*/
@Override
public void close() throws IOException {
_stream.close();
super.close();
}
private void resetBooleanBuffer() {
_booleanReadCount = 7;
_booleanBuffer[0] = 0;
}
}