package org.red5.io.amf3;
/*
* RED5 Open Source Flash Server - http://www.osflash.org/red5
*
* Copyright (c) 2006-2009 by respective authors (see below). All rights reserved.
*
* This library 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 2.1 of the License, or (at your option) any later
* version.
*
* This library 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 this library; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.mina.common.ByteBuffer;
import org.red5.io.amf.AMF;
import org.red5.io.object.DataTypes;
import org.red5.io.object.Deserializer;
import org.red5.io.utils.ObjectMap;
import org.red5.io.utils.XMLUtils;
import org.red5.io.utils.ArrayUtils;
import org.red5.server.service.ConversionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
/**
* Input for Red5 data (AMF3) types
*
* @author The Red5 Project (red5@osflash.org)
* @author Luke Hubbard, Codegent Ltd (luke@codegent.com)
* @author Joachim Bauch (jojo@struktur.de)
*/
public class Input extends org.red5.io.amf.Input implements org.red5.io.object.Input {
private static ConvertUtilsBean convertUtilsBean = BeanUtilsBean.getInstance().getConvertUtils();
/**
* Holds informations about already deserialized classes.
*/
protected class ClassReference {
/** Name of the deserialized class. */
protected String className;
/** Type of the class. */
protected int type;
/** Names of the attributes of the class. */
protected List<String> attributeNames;
/** Create new informations about a class.
* @param className class name
* @param type type
* @param attributeNames attributes
*/
public ClassReference(String className, int type, List<String> attributeNames) {
this.className = className;
this.type = type;
this.attributeNames = attributeNames;
}
}
/**
* Dummy class that is stored as reference for objects currently
* being deserialized that reference themselves.
*/
protected class PendingObject {
class PendingProperty {
Object obj;
Class<?> klass;
String name;
PendingProperty(Object obj, Class<?> klass, String name) {
this.obj = obj;
this.klass = klass;
this.name = name;
}
}
private List<PendingProperty> properties;
public void addPendingProperty(Object obj, Class<?> klass, String name) {
if (properties == null) {
properties = new ArrayList<PendingProperty>();
}
properties.add(new PendingProperty(obj, klass, name));
}
public void resolveProperties(Object result) {
if (properties == null)
// No pending properties
return;
for (PendingProperty prop: properties) {
try {
try {
prop.klass.getField(prop.name).set(prop.obj, result);
} catch (Exception e) {
BeanUtils.setProperty(prop.obj, prop.name, result);
}
} catch (Exception e) {
log.error("Error mapping property: {} ({})", prop.name, result);
}
}
properties.clear();
}
}
/**
* Class used to collect AMF3 references.
* In AMF3 references should be collected through the whole "body" (across several Input objects).
*/
public static class RefStorage {
private List<ClassReference> classReferences = new ArrayList<ClassReference>();
private List<String> stringReferences = new ArrayList<String>();
private Map<Integer, Object> refMap = new HashMap<Integer, Object>();
}
/**
* Logger
*/
protected static Logger log = LoggerFactory.getLogger(Input.class);
/**
* Set to a value above <tt>0</tt> to enforce AMF3 decoding mode.
*/
private int amf3_mode;
/**
* List of string values found in the input stream.
*/
private List<String> stringReferences;
/**
* Informations about already deserialized classes.
*/
private List<ClassReference> classReferences;
/**
* Creates Input object for AMF3 from byte buffer
*
* @param buf Byte buffer
*/
public Input(ByteBuffer buf) {
super(buf);
amf3_mode = 0;
stringReferences = new ArrayList<String>();
classReferences = new ArrayList<ClassReference>();
}
/**
* Creates Input object for AMF3 from byte buffer and initializes references
* from passed RefStorage
* @param buf buffer
* @param refStorage ref storage
*/
public Input(ByteBuffer buf, RefStorage refStorage) {
super(buf);
this.stringReferences = refStorage.stringReferences;
this.classReferences = refStorage.classReferences;
this.refMap = refStorage.refMap;
amf3_mode = 0;
}
/**
* Force using AMF3 everywhere
*/
public void enforceAMF3() {
amf3_mode++;
}
/**
* Provide access to raw data.
*
* @return ByteBuffer
*/
protected ByteBuffer getBuffer() {
return buf;
}
/**
* Reads the data type
*
* @return byte Data type
*/
@Override
public byte readDataType() {
if (buf == null) {
log.error("Why is buf null?");
}
currentDataType = buf.get();
log.debug("Current data type: {}", currentDataType);
byte coreType;
if (currentDataType == AMF.TYPE_AMF3_OBJECT) {
currentDataType = buf.get();
} else if (amf3_mode == 0) {
// AMF0 object
return readDataType(currentDataType);
}
log.debug("Current data type (after amf checks): {}", currentDataType);
switch (currentDataType) {
case AMF3.TYPE_UNDEFINED:
case AMF3.TYPE_NULL:
coreType = DataTypes.CORE_NULL;
break;
case AMF3.TYPE_INTEGER:
case AMF3.TYPE_NUMBER:
coreType = DataTypes.CORE_NUMBER;
break;
case AMF3.TYPE_BOOLEAN_TRUE:
case AMF3.TYPE_BOOLEAN_FALSE:
coreType = DataTypes.CORE_BOOLEAN;
break;
case AMF3.TYPE_STRING:
coreType = DataTypes.CORE_STRING;
break;
// TODO check XML_SPECIAL
case AMF3.TYPE_XML:
case AMF3.TYPE_XML_DOCUMENT:
coreType = DataTypes.CORE_XML;
break;
case AMF3.TYPE_OBJECT:
coreType = DataTypes.CORE_OBJECT;
break;
case AMF3.TYPE_ARRAY:
// should we map this to list or array?
coreType = DataTypes.CORE_ARRAY;
break;
case AMF3.TYPE_DATE:
coreType = DataTypes.CORE_DATE;
break;
case AMF3.TYPE_BYTEARRAY:
coreType = DataTypes.CORE_BYTEARRAY;
break;
default:
log.info("Unknown datatype: {}", currentDataType);
// End of object, and anything else lets just skip
coreType = DataTypes.CORE_SKIP;
break;
}
log.debug("Core type: {}", coreType);
return coreType;
}
// Basic
/**
* Reads a null (value)
*
* @return Object null
*/
@Override
public Object readNull(Type target) {
return null;
}
/**
* Reads a boolean
*
* @return boolean Boolean value
*/
@Override
public Boolean readBoolean(Type target) {
return (currentDataType == AMF3.TYPE_BOOLEAN_TRUE) ? Boolean.TRUE
: Boolean.FALSE;
}
/**
* Reads a Number
*
* @return Number Number
*/
@SuppressWarnings("unchecked")
@Override
public Number readNumber(Type target) {
Number v;
if (currentDataType == AMF3.TYPE_NUMBER) {
v = buf.getDouble();
} else {
// we are decoding an int
v = readAMF3Integer();
}
if (target instanceof Class && Number.class.isAssignableFrom((Class<?>) target)) {
Class cls = (Class) target;
if (!cls.isAssignableFrom(v.getClass())) v = (Number) convertUtilsBean.convert(v.toString(), cls);
}
return v;
}
/**
* Reads a string
*
* @return String String
*/
@Override
public String readString(Type target) {
int len = readAMF3Integer();
log.debug("readString - length: {}", len);
if (len == 1) {
// Empty string
return "";
}
if ((len & 1) == 0) {
//if the refs are empty an IndexOutOfBoundsEx will be thrown
if (stringReferences.isEmpty()) {
log.debug("String reference list is empty");
}
// Reference
return stringReferences.get(len >> 1);
}
len >>= 1;
log.debug("readString - new length: {}", len);
int limit = buf.limit();
log.debug("readString - limit: {}", limit);
final java.nio.ByteBuffer strBuf = buf.buf();
strBuf.limit(strBuf.position() + len);
final String string = AMF3.CHARSET.decode(strBuf).toString();
log.debug("String: {}", string);
buf.limit(limit); // Reset the limit
stringReferences.add(string);
return string;
}
public String getString() {
return readString(String.class);
}
/**
* Returns a date
*
* @return Date Date object
*/
@Override
public Date readDate(Type target) {
int ref = readAMF3Integer();
if ((ref & 1) == 0) {
// Reference to previously found date
return (Date) getReference(ref >> 1);
}
long ms = (long) buf.getDouble();
Date date = new Date(ms);
storeReference(date);
return date;
}
// Array
/**
* Returns an array
*
* @return int Length of array
*/
@SuppressWarnings("unchecked")
public Object readArray(Deserializer deserializer, Type target) {
int count = readAMF3Integer();
if ((count & 1) == 0) {
// Reference
return getReference(count >> 1);
}
count = (count >> 1);
String key = readString(String.class);
amf3_mode += 1;
Object result;
if (key.equals("")) {
Class<?> nested = Object.class;
Class<?> collection = Collection.class;
Collection resultCollection;
if (target instanceof ParameterizedType) {
ParameterizedType t = (ParameterizedType) target;
Type[] actualTypeArguments = t.getActualTypeArguments();
if (actualTypeArguments.length == 1) {
nested = (Class<?>) actualTypeArguments[0];
}
target = t.getRawType();
}
if (target instanceof Class) {
collection = (Class) target;
}
if (collection.isArray()) {
nested = ArrayUtils.getGenericType(collection.getComponentType());
resultCollection = new ArrayList(count);
} else if (SortedSet.class.isAssignableFrom(collection)) {
resultCollection = new TreeSet();
} else if (Set.class.isAssignableFrom(collection)) {
resultCollection = new HashSet(count);
} else {
resultCollection = new ArrayList(count);
}
for (int i=0; i<count; i++) {
final Object value = deserializer.deserialize(this, nested);
resultCollection.add(value);
}
if (collection.isArray()) {
result = ArrayUtils.toArray(collection.getComponentType(), resultCollection);
} else {
result = resultCollection;
}
storeReference(result);
} else {
Class<?> k = Object.class;
Class<?> v = Object.class;
Class<?> collection = Collection.class;
if (target instanceof ParameterizedType) {
ParameterizedType t = (ParameterizedType) target;
Type[] actualTypeArguments = t.getActualTypeArguments();
if (actualTypeArguments.length == 2) {
k = (Class<?>) actualTypeArguments[0];
v = (Class<?>) actualTypeArguments[1];
}
target = t.getRawType();
}
if (target instanceof Class) {
collection = (Class) target;
}
if (SortedMap.class.isAssignableFrom(collection)) {
collection = TreeMap.class;
} else {
collection = HashMap.class;
}
Map resultMap;
try {
resultMap= (Map) collection.newInstance();
} catch (Exception e) {
resultMap = new HashMap(count);
}
// associative array
storeReference(resultMap);
while (!key.equals("")) {
final Object value = deserializer.deserialize(this, v);
resultMap.put(key, value);
key = readString(k);
}
for (int i=0; i<count; i++) {
final Object value = deserializer.deserialize(this, v);
resultMap.put(i, value);
}
result = resultMap;
}
amf3_mode -= 1;
return result;
}
public Object readMap(Deserializer deserializer, Type target) {
throw new RuntimeException("AMF3 doesn't support maps.");
}
// Object
@SuppressWarnings("unchecked")
public Object readObject(Deserializer deserializer, Type target) {
int type = readAMF3Integer();
if ((type & 1) == 0) {
// Reference
return getReference(type >> 1);
}
type >>= 1;
List<String> attributes = null;
String className;
Object result = null;
boolean inlineClass = (type & 1) == 1;
if (!inlineClass) {
ClassReference info = classReferences.get(type >> 1);
className = info.className;
attributes = info.attributeNames;
type = info.type;
if (attributes != null) {
type |= attributes.size() << 2;
}
} else {
type >>= 1;
className = readString(String.class);
}
amf3_mode += 1;
Object instance = newInstance(className);
Map<String, Object> properties = null;
PendingObject pending = new PendingObject();
int tempRefId = storeReference(pending);
switch (type & 0x03) {
case AMF3.TYPE_OBJECT_PROPERTY:
// Load object properties into map
int count = type >> 2;
properties = new ObjectMap<String, Object>();
if (attributes == null) {
attributes = new ArrayList<String>(count);
for (int i=0; i<count; i++) {
attributes.add(readString(String.class));
}
classReferences.add(new ClassReference(className, AMF3.TYPE_OBJECT_PROPERTY, attributes));
}
for (int i=0; i<count; i++) {
String name = attributes.get(i);
properties.put(name, deserializer.deserialize(this, getPropertyType(instance, name)));
}
break;
case AMF3.TYPE_OBJECT_EXTERNALIZABLE:
// Use custom class to deserialize the object
if ("".equals(className)) {
throw new RuntimeException("Classname is required to load an Externalizable object");
}
log.debug("Externalizable class: {}", className);
result = newInstance(className);
if (result == null) {
throw new RuntimeException(String.format("Could not instantiate class: %s", className));
}
if (!(result instanceof IExternalizable)) {
throw new RuntimeException(String.format("Class must implement the IExternalizable interface: %s", className));
}
classReferences.add(new ClassReference(className, AMF3.TYPE_OBJECT_EXTERNALIZABLE, null));
storeReference(tempRefId, result);
((IExternalizable) result).readExternal(new DataInput(this, deserializer));
break;
case AMF3.TYPE_OBJECT_VALUE:
// First, we should read typed (non-dynamic) properties ("sealed traits" according to AMF3 specification).
// Property names are stored in the beginning, then values are stored.
count = type >> 2;
properties = new ObjectMap<String, Object>();
if (attributes == null) {
attributes = new ArrayList<String>(count);
for (int i = 0; i < count; i++) {
attributes.add(readString(String.class));
}
classReferences.add(new ClassReference(className, AMF3.TYPE_OBJECT_VALUE, attributes));
}
for (int i = 0; i < count; i++) {
String key = attributes.get(i);
properties.put(key, deserializer.deserialize(this, getPropertyType(instance, key)));
}
// Now we should read dynamic properties which are stored as name-value pairs.
// Dynamic properties are NOT remembered in 'classReferences'.
String key = readString(String.class);
while (!"".equals(key)) {
Object value = deserializer.deserialize(this, getPropertyType(instance, key));
properties.put(key, value);
key = readString(String.class);
}
break;
default:
case AMF3.TYPE_OBJECT_PROXY:
if ("".equals(className)) {
throw new RuntimeException("Classname is required to load an Externalizable object");
}
log.debug("Externalizable class: {}", className);
result = newInstance(className);
if (result == null) {
throw new RuntimeException(String.format("Could not instantiate class: %s", className));
}
if (!(result instanceof IExternalizable)) {
throw new RuntimeException(String.format("Class must implement the IExternalizable interface: %s", className));
}
classReferences.add(new ClassReference(className, AMF3.TYPE_OBJECT_PROXY, null));
storeReference(tempRefId, result);
((IExternalizable) result).readExternal(new DataInput(this, deserializer));
}
amf3_mode -= 1;
if (result == null) {
// Create result object based on classname
if ("".equals(className)) {
// "anonymous" object, load as Map
// Resolve circular references
for (Map.Entry<String, Object> entry: properties.entrySet()) {
if (entry.getValue() == pending) {
entry.setValue(properties);
}
}
storeReference(tempRefId, properties);
result = properties;
} else if ("RecordSet".equals(className)) {
// TODO: how are RecordSet objects encoded?
throw new RuntimeException("Objects of type RecordSet not supported yet.");
} else if ("RecordSetPage".equals(className)) {
// TODO: how are RecordSetPage objects encoded?
throw new RuntimeException("Objects of type RecordSetPage not supported yet.");
} else {
// Apply properties to object
result = newInstance(className);
if (result != null) {
storeReference(tempRefId, result);
Class resultClass = result.getClass();
pending.resolveProperties(result);
for (Map.Entry<String, Object> entry: properties.entrySet()) {
// Resolve circular references
final String key = entry.getKey();
Object value = entry.getValue();
if (value == pending) {
value = result;
}
if (value instanceof PendingObject) {
// Defer setting of value until real object is created
((PendingObject) value).addPendingProperty(result, resultClass, key);
continue;
}
try {
if (value != null) {
try {
final Field field = resultClass.getField(key);
final Class fieldType = field.getType();
if (!fieldType.isAssignableFrom(value.getClass())) {
value = ConversionUtils.convert(value, fieldType);
}
field.set(result, value);
} catch (Exception e) {
BeanUtils.setProperty(result, key, value);
}
} else {
log.debug("Skipping null property: {}", key);
}
} catch (Exception e) {
log.error("Error mapping property: {} ({})", key, value);
}
}
} // else fall through
}
}
return result;
}
public ByteArray readByteArray(Type target) {
int type = readAMF3Integer();
if ((type & 1) == 0) {
// Reference
return (ByteArray) getReference(type >> 1);
}
type >>= 1;
ByteArray result = new ByteArray(buf, type);
storeReference(result);
return result;
}
// Others
/**
* Reads Custom
*
* @return Object Custom type object
*/
@Override
public Object readCustom(Type target) {
// Return null for now
return null;
}
/** {@inheritDoc} */
public Object readReference(Type target) {
throw new RuntimeException("AMF3 doesn't support direct references.");
}
/**
* Resets map
*/
@Override
public void reset() {
super.reset();
stringReferences.clear();
}
/**
* Parser of AMF3 "compressed" integer data type
*
* @return a converted integer value
* @see <a href="http://osflash.org/amf3/parsing_integers">parsing AMF3
* integers (external)</a>
*/
private int readAMF3Integer() {
int n = 0;
int b = buf.get();
int result = 0;
while ((b & 0x80) != 0 && n < 3) {
result <<= 7;
result |= (b & 0x7f);
b = buf.get();
n++;
}
if (n < 3) {
result <<= 7;
result |= b;
} else {
/* Use all 8 bits from the 4th byte */
result <<= 8;
result |= b & 0x0ff;
/* Check if the integer should be negative */
if ((result & 0x10000000) != 0) {
/* and extend the sign bit */
result |= 0xe0000000;
}
}
return result;
}
/** {@inheritDoc} */
protected Object newInstance(String className) {
log.debug("newInstance {}", className);
if (className.startsWith("flex.")) {
// Use Red5 compatibility class instead
className = "org.red5.compatibility." + className;
}
return super.newInstance(className);
}
/** {@inheritDoc} */
public Document readXML(Type target) {
int len = readAMF3Integer();
if (len == 1)
// Empty string, should not happen
return null;
if ((len & 1) == 0) {
// Reference
return (Document) getReference(len >> 1);
}
len >>= 1;
int limit = buf.limit();
final java.nio.ByteBuffer strBuf = buf.buf();
strBuf.limit(strBuf.position() + len);
final String xmlString = AMF3.CHARSET.decode(strBuf).toString();
buf.limit(limit); // Reset the limit
Document doc = null;
try {
doc = XMLUtils.stringToDoc(xmlString);
} catch (IOException ioex) {
log.error("IOException converting xml to dom", ioex);
}
storeReference(doc);
return doc;
}
}