/*
* Flazr <http://flazr.com> Copyright (C) 2009 Peter Thomas.
*
* This file is part of Flazr.
*
* Flazr is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Flazr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Flazr. If not, see <http://www.gnu.org/licenses/>.
*/
package com.flazr.amf;
import com.flazr.util.ValueToEnum;
import java.util.Arrays;
import static com.flazr.amf.Amf0Value.Type.*;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import org.jboss.netty.buffer.ChannelBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Amf0Value {
private static final Logger logger = LoggerFactory.getLogger(Amf0Value.class);
private Amf0Value() {}
public static enum Type implements ValueToEnum.IntValue {
NUMBER(0x00),
BOOLEAN(0x01),
STRING(0x02),
OBJECT(0x03),
NULL(0x05),
UNDEFINED(0x06),
REFERENCE(0x07),
MAP(0x08), // ecma-array
OBJECT_END(0x09),
ARRAY(0x0A), // strict-array
DATE(0x0B),
LONG_STRING(0x0C),
UNSUPPORTED(0x0D),
RECORDSET(0x0E),
XML_DOCUMENT(0x0F),
TYPED_OBJECT(0x10);
private final int value;
private Type(int value) {
this.value = value;
}
@Override
public int intValue() {
return value;
}
private static final ValueToEnum<Type> converter = new ValueToEnum<Type>(Type.values());
public static Type valueToEnum(final int value) {
return converter.valueToEnum(value);
}
private static Type getType(final Object value) {
if (value == null) {
return NULL;
} else if (value instanceof String) {
return STRING;
} else if (value instanceof Number) {
return NUMBER;
} else if (value instanceof Boolean) {
return BOOLEAN;
} else if (value instanceof Amf0Object) {
Amf0Object obj = (Amf0Object) value;
if (obj.containsKey("classname"))
return TYPED_OBJECT;
else
return OBJECT;
} else if (value instanceof Map) {
return MAP;
} else if (value instanceof Object[]) {
return ARRAY;
} else if(value instanceof Date) {
return DATE;
} else {
throw new RuntimeException("unexpected type: " + value.getClass());
}
}
}
private static final byte BOOLEAN_TRUE = 0x01;
private static final byte BOOLEAN_FALSE = 0x00;
private static final byte[] OBJECT_END_MARKER = new byte[]{0x00, 0x00, 0x09};
public static void encode(final ChannelBuffer out, final Object value) {
final Type type = Type.getType(value);
if(logger.isDebugEnabled()) {
logger.debug(">> " + toString(type, value));
}
out.writeByte((byte) type.value);
switch (type) {
case NUMBER:
if(value instanceof Double) {
out.writeLong(Double.doubleToLongBits((Double) value));
} else { // this coverts int also
out.writeLong(Double.doubleToLongBits(Double.valueOf(value.toString())));
}
return;
case BOOLEAN:
out.writeByte((Boolean) value ? BOOLEAN_TRUE : BOOLEAN_FALSE);
return;
case STRING:
encodeString(out, (String) value);
return;
case NULL:
return;
case MAP:
out.writeInt(0);
// no break; remaining processing same as OBJECT
case OBJECT:
encodeObject(out, value);
return;
case ARRAY:
final Object[] array = (Object[]) value;
out.writeInt(array.length);
for(Object o : array) {
encode(out, o);
}
return;
case DATE:
final long time = ((Date) value).getTime();
out.writeLong(Double.doubleToLongBits(time));
out.writeShort((short) 0);
return;
case TYPED_OBJECT:
final Map<String, Object> map = (Map) value;
encodeString(out, (String) map.remove("classname"));
encodeObject(out, value);
return;
default:
// ignoring other types client doesn't require for now
throw new RuntimeException("unexpected type: " + type);
}
}
private static String decodeString(final ChannelBuffer in) {
final short size = in.readShort();
final byte[] bytes = new byte[size];
in.readBytes(bytes);
return new String(bytes); // TODO UTF-8 ?
}
private static void encodeString(final ChannelBuffer out, final String value) {
final byte[] bytes = value.getBytes(); // TODO UTF-8 ?
out.writeShort((short) bytes.length);
out.writeBytes(bytes);
}
private static void encodeObject(final ChannelBuffer out, final Object value) {
final Map<String, Object> map = (Map) value;
for (final Map.Entry<String, Object> entry : map.entrySet()) {
encodeString(out, entry.getKey());
encode(out, entry.getValue());
}
out.writeBytes(OBJECT_END_MARKER);
}
public static void encode(final ChannelBuffer out, final Object... values) {
for (final Object value : values) {
encode(out, value);
}
}
public static Object decode(final ChannelBuffer in) {
final Type type = Type.valueToEnum(in.readByte());
final Object value = decode(in, type);
if(logger.isDebugEnabled()) {
logger.debug("<< " + toString(type, value));
}
return value;
}
private static Object decode(final ChannelBuffer in, final Type type) {
switch (type) {
case NUMBER: return Double.longBitsToDouble(in.readLong());
case BOOLEAN: return in.readByte() == BOOLEAN_TRUE;
case STRING: return decodeString(in);
case ARRAY:
final int arraySize = in.readInt();
final Object[] array = new Object[arraySize];
for (int i = 0; i < arraySize; i++) {
array[i] = decode(in);
}
return array;
case MAP:
case OBJECT:
final int count;
final Map<String, Object> map;
if(type == MAP) {
count = in.readInt(); // should always be 0
map = new LinkedHashMap<String, Object>();
if(count > 0 && logger.isDebugEnabled()) {
logger.debug("non-zero size for MAP type: {}", count);
}
} else {
count = 0;
map = new Amf0Object();
}
int i = 0;
final byte[] endMarker = new byte[3];
while (in.readable()) {
in.getBytes(in.readerIndex(), endMarker);
if (Arrays.equals(endMarker, OBJECT_END_MARKER)) {
in.skipBytes(3);
if(logger.isDebugEnabled()) {
logger.debug("end MAP / OBJECT, found object end marker [000009]");
}
break;
}
if(count > 0 && i++ == count) {
if(logger.isDebugEnabled()) {
logger.debug("stopping map decode after reaching count: {}", count);
}
break;
}
map.put(decodeString(in), decode(in));
}
return map;
case DATE:
final long dateValue = in.readLong();
in.readShort(); // consume the timezone
return new Date((long) Double.longBitsToDouble(dateValue));
case LONG_STRING:
final int stringSize = in.readInt();
final byte[] bytes = new byte[stringSize];
in.readBytes(bytes);
return new String(bytes); // TODO UTF-8 ?
case NULL:
case UNDEFINED:
case UNSUPPORTED:
return null;
case TYPED_OBJECT:
String classname = decodeString(in);
Amf0Object object = (Amf0Object) decode(in, OBJECT);
object.put("classname", classname);
return object;
default:
throw new RuntimeException("unexpected type: " + type);
}
}
private static String toString(final Type type, final Object value) {
StringBuilder sb = new StringBuilder();
sb.append('[').append(type).append(" ");
if(type == Type.ARRAY) {
sb.append(Arrays.toString((Object[]) value));
} else {
sb.append(value);
}
sb.append(']');
return sb.toString();
}
}