package robombs.clientserver;
import java.io.*;
import java.util.*;
import java.util.zip.*;
/**
* DataContainers are the basic transfer containers for data in this little client/server-world. Each transfer from or to
* a server or a client will most likely contain at least one DataContainer dropped into an array of DataContainers which itself will be wrapped into an array of bytes.<br>
* A transfer can contain multiple DataContainers of different types. What's stored into these containers is up to the implementation.
* <br>One can easily extend the DataContainer to wrap complex data in a convenient way.
*/
public class DataContainer implements Cloneable {
public final static byte TYPE_INT = 0;
public final static byte TYPE_FLOAT = 1;
public final static byte TYPE_STRING = 2;
public final static byte TYPE_BYTE = 3;
private final static Byte I_TYPE_INT = Byte.valueOf(TYPE_INT);
private final static Byte I_TYPE_FLOAT = Byte.valueOf(TYPE_FLOAT);
private final static Byte I_TYPE_STRING = Byte.valueOf(TYPE_STRING);
private final static Byte I_TYPE_BYTE = Byte.valueOf(TYPE_BYTE);
protected List<Object> objs = new ArrayList<Object>(200);
protected List<Byte> types = new ArrayList<Byte>(200);
protected int pos = 0;
protected ClientInfo ci = null;
protected int msgType = MessageTypes.OBJ_TRANSFER;
private byte[] bytes = null;
private int inLen = 0;
private boolean zip = false;
private boolean extracted = false;
private static Map<Object, byte[]> cache=Collections.synchronizedMap(new ByteArrayCache<Object, byte[]>());
private static volatile long hits=0;
private static volatile long misses=0;
/**
* Creates a new, empty DataContainer.
*/
public DataContainer() {
}
/**
* Creates a new DataContainer and populates it with the data wrapped in the given byte arrays.
* @param bytes the data for this container
*/
public DataContainer(byte[] bytes) {
this(bytes, false);
}
/**
* Creates a new DataContainer and fills it with the date from the given container.
* @param dc
*/
public DataContainer(DataContainer dc) {
objs=dc.objs;
types=dc.types;
pos=dc.pos;
ci=dc.ci;
msgType=dc.msgType;
bytes=dc.bytes;
inLen=dc.inLen;
zip=dc.zip;
extracted=dc.extracted;
}
/**
* Creates a new DataContainer and populates it with the data wrapped in the given byte arrays.
* @param bytes the data for this container
* @param zip is the data in this array zipped?
*/
public DataContainer(byte[] bytes, boolean zip) {
inLen = bytes.length;
if (inLen > 1) {
this.zip = zip;
this.bytes = bytes;
msgType = ((bytes[2] & 0xff) << 8) + (bytes[3] & 0xff);
} else {
NetLogger.log("Invalid package size: " + inLen);
}
}
/**
* Returns the length of the byte array from which this container has been created. If it hasn't, it's 0.
* @return int the length or 0
*/
public int getLength() {
return inLen;
}
/**
* Returns the number of entries in this container
* @return int the number of entries
*/
public int getBufferLength() {
extract();
return objs.size();
}
public int getCurrentSize() {
return objs.size();
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
/**
* Returns the raw data (after unzipping) if this DataContainer has been created from a byte array. If it hasn't, this method
* returns null. This method is useful for extending DataContainer so that the extended instance can be created from an instance of
* DataContainer simply by feeding the raw data back into super(...);
* @return byte[] the raw data. Another DataContainer can be created from this one.
*/
public byte[] getRawData() {
extract();
return bytes;
}
/**
* Sets the ClientInfo, the info about the client from which this container came. This information isn't transfered, it's
* set by the server itself when creating the containers from the stream.
* @param c the client info
*/
public void setClientInfo(ClientInfo c) {
ci = c;
}
/**
* Returns the ClientInfo, the info about the client from which this container came. This information isn't transfered, it's
* set by the server itself when creating the containers from the stream.
* @return ClientInfo the client info
*/
public ClientInfo getClientInfo() {
return ci;
}
/**
* Gets the message type of this container as defined by MessageTypes.
* @return int the type
*/
public int getMessageType() {
return msgType;
}
/**
* Sets the message type of this container as defined by MessageTypes.
* @param type the message type
*/
public void setMessageType(int type) {
msgType = type;
}
/**
* Adds a new float to the container.
* @param flt a float
*/
public void add(float flt) {
objs.add(Float.valueOf(flt));
types.add(I_TYPE_FLOAT);
}
/**
* Adds a new int to the container.
* @param in a int
*/
public void add(int in) {
objs.add(Integer.valueOf(in));
types.add(I_TYPE_INT);
}
/**
* Adds a new byte to the container.
* @param byt a byte
*/
public void add(byte byt) {
objs.add(Byte.valueOf(byt));
types.add(I_TYPE_BYTE);
}
/**
* Adds a new String to the container.
* @param str a String
*/
public void add(String str) {
objs.add(str);
types.add(I_TYPE_STRING);
}
/**
* Clears the container. It's empty afterwards.
*/
public void reset() {
pos = 0;
}
/**
* Checks, if the container contains more data to read.
* @return boolean does it?
*/
public boolean hasData() {
extract();
return objs.size() > pos;
}
/**
* Skips an entry in the container.
* @return DataContainer the same container as return value for convenience reasons.
*/
public DataContainer skip() {
extract();
if (hasData()) {
pos++;
} else {
throw new RuntimeException("No such element!");
}
return this;
}
/**
* Returns the type of the next entry in the container.
* @return byte the type
*/
public byte getType() {
extract();
return ((Byte) types.get(pos)).byteValue();
}
/**
* Reads the next float from the container. If it's not a float, this will cause an ClassCastException.
* @return float the next float
*/
public float getNextFloat() {
extract();
return ((Float) objs.get(pos++)).floatValue();
}
/**
* Reads the next int from the container. If it's not an int, this will cause an ClassCastException.
* @return int the next int
*/
public int getNextInt() {
extract();
return ((Integer) objs.get(pos++)).intValue();
}
/**
* Reads the next byte from the container. If it's not a byte, this will cause an ClassCastException.
* @return byte the next byte
*/
public byte getNextByte() {
extract();
return ((Byte) objs.get(pos++)).byteValue();
}
/**
* Reads the next string from the container. If it's not a string, this will cause an ClassCastException.
* @return String the next string
*/
public String getNextString() {
extract();
return (String) objs.get(pos++);
}
/**
* Wraps the container's data into a byte array.
* @param zip boolean
* @return byte[]
*/
byte[] toByteArray(boolean zip) {
ByteArrayOutputStream bos = new ByteArrayOutputStream(500);
try {
for (int i = 0; i < objs.size(); i++) {
// Type schreiben...
byte type = ((Byte) types.get(i)).byteValue();
bos.write(new byte[] {type});
if (type == TYPE_STRING) {
// String
String txt=(String) objs.get(i);
byte[] content=(byte[]) cache.get(txt);
int ii = txt.length();
int hi = (byte) (ii >> 8);
int low = (byte) (ii - (hi << 8));
bos.write(new byte[] {(byte) hi, (byte) low}); // Length
if (content==null) {
content=((String) objs.get(i)).getBytes("UTF-8");
cache.put(txt, content);
misses++;
} else {
hits++;
}
bos.write(content);
}
if (type == TYPE_INT) {
Integer in=(Integer) objs.get(i);
byte[] content=(byte[]) cache.get(in);
if (content==null) {
int ii = in.intValue();
content=new byte[] {(byte) (ii >> 24), (byte) ((ii >> 16) & 0xFF), (byte) ((ii >> 8) & 0xFF), (byte) (ii & 0xFF)};
cache.put(in, content);
misses++;
} else {
hits++;
}
bos.write(content);
}
if (type == TYPE_FLOAT) {
Float flo=(Float) objs.get(i);
byte[] content=(byte[]) cache.get(flo);
if (content==null) {
int ii = Float.floatToIntBits(flo.floatValue());
content=new byte[] {(byte) (ii >> 24), (byte) ((ii >> 16) & 0xFF), (byte) ((ii >> 8) & 0xFF), (byte) (ii & 0xFF)};
cache.put(flo, content);
misses++;
} else {
hits++;
}
bos.write(content);
}
if (type == TYPE_BYTE) {
bos.write(((Byte) objs.get(i)).byteValue());
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
byte[] bs = bos.toByteArray();
byte[] bts = new byte[bs.length + 4];
System.arraycopy(bs, 0, bts, 4, bs.length);
int ii = msgType;
bts[2] = (byte) (ii >> 8);
bts[3] = (byte) (ii & 0xff);
if (zip) {
bts = zip(bts);
}
ii = bts.length;
bts[0] = (byte) (ii >> 8);
bts[1] = (byte) (ii & 0xff);
/*
if (misses!=0) {
System.out.println(hits*100/(misses+hits)+"/"+cache.size());
}
*/
return bts;
}
/**
* Sets zip mode for this container
* @param zip zipped?
*/
void setZip(boolean zip) {
this.zip = zip;
}
/**
* Gets the zip mode of this container
* @return boolean zipped?
*/
boolean getZip() {
return zip;
}
/**
* Reads the wrapped data from an byte array and fills the DataContainer with it.
*/
private void extract() {
if (!extracted) {
int len = ((bytes[0] & 0xff) << 8) + (bytes[1] & 0xff);
if (zip) {
bytes = unzip(bytes, len);
len = bytes.length;
bytes[0] = (byte) (len >> 8);
bytes[1] = (byte) (len - ((len >> 8) << 8));
}
if (bytes.length > 3) {
try {
for (int i = 4; i < len; ) {
byte type = bytes[i];
switch (type) {
case (TYPE_INT): {
types.add(I_TYPE_INT);
objs.add(Integer.valueOf(((bytes[i + 1] & 0xFF) << 24) + ((bytes[i + 2] & 0xFF) << 16) + ((bytes[i + 3] & 0xFF) << 8) + (bytes[i + 4] & 0xFF)));
i += 5;
break;
}
case (TYPE_BYTE): {
types.add(I_TYPE_BYTE);
objs.add(Byte.valueOf(bytes[i + 1]));
i += 2;
break;
}
case (TYPE_FLOAT): {
types.add(I_TYPE_FLOAT);
float fl = Float.intBitsToFloat(((bytes[i + 1] & 0xFF) << 24) + ((bytes[i + 2] & 0xFF) << 16) + ((bytes[i + 3] & 0xFF) << 8) + (bytes[i + 4] & 0xFF));
objs.add(Float.valueOf(fl));
i += 5;
break;
}
case (TYPE_STRING): {
types.add(I_TYPE_STRING);
int leny = (bytes[i + 1] << 8) + bytes[i + 2];
objs.add(new String(bytes, i + 3, leny, "UTF-8"));
i += 3 + leny;
break;
}
default: {
throw new RuntimeException("Unable to detect data type (" + type + ")!");
}
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
pos = 0;
extracted = true;
}
}
/**
* Zips the data in the array and returns a zipped version.
* @param data byte[]
* @return byte[]
*/
private byte[] zip(byte[] data) {
ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
bos.write(data, 0, 4);
try {
GZIPOutputStream zos = new GZIPOutputStream(bos);
zos.write(data, 4, data.length - 4);
zos.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
return bos.toByteArray();
}
/**
* Unzip the data in an array
* @param data byte[]
* @param len int
* @return byte[]
*/
private byte[] unzip(byte[] data, int len) {
ByteArrayInputStream bis = new ByteArrayInputStream(data, 4, len - 4);
ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length<<1);
bos.write(data, 0, 4);
try {
GZIPInputStream zis = new GZIPInputStream(bis);
byte[] buffer = new byte[len];
int cnt = 0;
do {
cnt = zis.read(buffer);
if (cnt != -1) {
bos.write(buffer, 0, cnt);
}
} while (cnt >= 0);
zis.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
return bos.toByteArray();
}
private static class ByteArrayCache<K, V> extends LinkedHashMap<Object, byte[]> {
protected final static long serialVersionUID=1;
private final static int SIZE=200;
public ByteArrayCache() {
super(SIZE, 0.75f, true);
}
protected boolean removeEldestEntry(Map.Entry<Object, byte[]> eldest) {
return size() > SIZE;
}
}
}