/*
* RED5 Open Source Flash Server - http://code.google.com/p/red5/
*
* Copyright 2006-2012 by respective authors (see below). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.red5.io.amf;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.Vector;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import org.apache.commons.beanutils.BeanMap;
import org.apache.mina.core.buffer.IoBuffer;
import org.red5.annotations.Anonymous;
import org.red5.io.amf3.ByteArray;
import org.red5.io.object.BaseOutput;
import org.red5.io.object.ICustomSerializable;
import org.red5.io.object.RecordSet;
import org.red5.io.object.Serializer;
import org.red5.io.utils.XMLUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
/**
*
* @author The Red5 Project (red5@osflash.org)
* @author Luke Hubbard, Codegent Ltd (luke@codegent.com)
* @author Paul Gregoire (mondain@gmail.com)
* @author Harald Radi (harald.radi@nme.at)
*/
public class Output extends BaseOutput implements org.red5.io.object.Output {
protected static Logger log = LoggerFactory.getLogger(Output.class);
private static Cache stringCache;
private static Cache serializeCache;
private static Cache fieldCache;
private static Cache getterCache;
private static CacheManager cacheManager;
private static CacheManager getCacheManager() {
if (cacheManager == null) {
if (System.getProperty("red5.root") != null) {
// we're running Red5 as a server.
try {
cacheManager = new CacheManager(System.getProperty("red5.root") + File.separator + "conf" + File.separator + "ehcache.xml");
} catch (CacheException e) {
cacheManager = constructDefault();
}
} else {
// not a server, maybe running tests?
cacheManager = constructDefault();
}
}
return cacheManager;
}
private static CacheManager constructDefault() {
CacheManager manager = CacheManager.getInstance();
manager.addCacheIfAbsent("org.red5.io.amf.Output.stringCache");
manager.addCacheIfAbsent("org.red5.io.amf.Output.getterCache");
manager.addCacheIfAbsent("org.red5.io.amf.Output.fieldCache");
manager.addCacheIfAbsent("org.red5.io.amf.Output.serializeCache");
return manager;
}
/**
* Output buffer
*/
protected IoBuffer buf;
/**
* Creates output with given byte buffer
* @param buf Byte buffer
*/
public Output(IoBuffer buf) {
super();
this.buf = buf;
}
/** {@inheritDoc} */
public boolean isCustom(Object custom) {
return false;
}
protected boolean checkWriteReference(Object obj) {
if (hasReference(obj)) {
writeReference(obj);
return true;
} else
return false;
}
/** {@inheritDoc} */
public void writeArray(Collection<?> array, Serializer serializer) {
if (checkWriteReference(array)) {
return;
}
storeReference(array);
buf.put(AMF.TYPE_ARRAY);
buf.putInt(array.size());
for (Object item : array) {
serializer.serialize(this, item);
}
}
/** {@inheritDoc} */
public void writeArray(Object[] array, Serializer serializer) {
log.debug("writeArray - array: {} serializer: {}", array, serializer);
if (array != null) {
if (checkWriteReference(array)) {
return;
}
storeReference(array);
buf.put(AMF.TYPE_ARRAY);
buf.putInt(array.length);
for (Object item : array) {
serializer.serialize(this, item);
}
} else {
writeNull();
}
}
/** {@inheritDoc} */
public void writeArray(Object array, Serializer serializer) {
if (array != null) {
if (checkWriteReference(array)) {
return;
}
storeReference(array);
buf.put(AMF.TYPE_ARRAY);
buf.putInt(Array.getLength(array));
for (int i = 0; i < Array.getLength(array); i++) {
serializer.serialize(this, Array.get(array, i));
}
} else {
writeNull();
}
}
/** {@inheritDoc} */
public void writeMap(Map<Object, Object> map, Serializer serializer) {
if (checkWriteReference(map)) {
return;
}
storeReference(map);
buf.put(AMF.TYPE_MIXED_ARRAY);
int maxInt = -1;
for (int i = 0; i < map.size(); i++) {
try {
if (!map.containsKey(i))
break;
} catch (ClassCastException err) {
// Map has non-number keys.
break;
}
maxInt = i;
}
buf.putInt(maxInt + 1);
// TODO: Need to support an incoming key named length
for (Map.Entry<Object, Object> entry : map.entrySet()) {
final String key = entry.getKey().toString();
if ("length".equals(key)) {
continue;
}
putString(key);
serializer.serialize(this, entry.getValue());
}
if (maxInt >= 0) {
putString("length");
serializer.serialize(this, maxInt + 1);
}
buf.put((byte) 0x00);
buf.put((byte) 0x00);
buf.put(AMF.TYPE_END_OF_OBJECT);
}
/** {@inheritDoc} */
public void writeMap(Collection<?> array, Serializer serializer) {
if (checkWriteReference(array)) {
return;
}
storeReference(array);
buf.put(AMF.TYPE_MIXED_ARRAY);
buf.putInt(array.size() + 1);
int idx = 0;
for (Object item : array) {
if (item != null) {
putString(String.valueOf(idx++));
serializer.serialize(this, item);
} else {
idx++;
}
}
putString("length");
serializer.serialize(this, array.size() + 1);
buf.put((byte) 0x00);
buf.put((byte) 0x00);
buf.put(AMF.TYPE_END_OF_OBJECT);
}
/** {@inheritDoc} */
public void writeRecordSet(RecordSet recordset, Serializer serializer) {
if (checkWriteReference(recordset)) {
return;
}
storeReference(recordset);
// Write out start of object marker
buf.put(AMF.TYPE_CLASS_OBJECT);
putString("RecordSet");
// Serialize
Map<String, Object> info = recordset.serialize();
// Write out serverInfo key
putString("serverInfo");
// Serialize
serializer.serialize(this, info);
// Write out end of object marker
buf.put((byte) 0x00);
buf.put((byte) 0x00);
buf.put(AMF.TYPE_END_OF_OBJECT);
}
/** {@inheritDoc} */
public boolean supportsDataType(byte type) {
return false;
}
/** {@inheritDoc} */
public void writeBoolean(Boolean bol) {
buf.put(AMF.TYPE_BOOLEAN);
buf.put(bol ? AMF.VALUE_TRUE : AMF.VALUE_FALSE);
}
/** {@inheritDoc} */
public void writeCustom(Object custom) {
}
/** {@inheritDoc} */
public void writeDate(Date date) {
buf.put(AMF.TYPE_DATE);
buf.putDouble(date.getTime());
buf.putShort((short) (TimeZone.getDefault().getRawOffset() / 60 / 1000));
}
/** {@inheritDoc} */
public void writeNull() {
// System.err.println("Write null");
buf.put(AMF.TYPE_NULL);
}
/** {@inheritDoc} */
public void writeNumber(Number num) {
buf.put(AMF.TYPE_NUMBER);
buf.putDouble(num.doubleValue());
}
/** {@inheritDoc} */
public void writeReference(Object obj) {
log.debug("Write reference");
buf.put(AMF.TYPE_REFERENCE);
buf.putShort(getReferenceId(obj));
}
/** {@inheritDoc} */
@SuppressWarnings({ "rawtypes" })
public void writeObject(Object object, Serializer serializer) {
if (checkWriteReference(object)) {
return;
}
storeReference(object);
// Create new map out of bean properties
BeanMap beanMap = new BeanMap(object);
// Set of bean attributes
Set set = beanMap.keySet();
if ((set.size() == 0) || (set.size() == 1 && beanMap.containsKey("class"))) {
// BeanMap is empty or can only access "class" attribute, skip it
writeArbitraryObject(object, serializer);
return;
}
// Write out either start of object marker for class name or "empty" start of object marker
Class<?> objectClass = object.getClass();
if (!objectClass.isAnnotationPresent(Anonymous.class)) {
buf.put(AMF.TYPE_CLASS_OBJECT);
putString(buf, serializer.getClassName(objectClass));
} else {
buf.put(AMF.TYPE_OBJECT);
}
if (object instanceof ICustomSerializable) {
((ICustomSerializable) object).serialize(this, serializer);
buf.put((byte) 0x00);
buf.put((byte) 0x00);
buf.put(AMF.TYPE_END_OF_OBJECT);
return;
}
// Iterate thru entries and write out property names with separators
for (Object key : set) {
String fieldName = key.toString();
log.debug("Field name: {} class: {}", fieldName, objectClass);
Field field = getField(objectClass, fieldName);
Method getter = getGetter(objectClass, beanMap, fieldName);
// Check if the Field corresponding to the getter/setter pair is transient
if (!serializeField(serializer, objectClass, fieldName, field, getter)) {
continue;
}
putString(buf, fieldName);
serializer.serialize(this, field, getter, object, beanMap.get(key));
}
// Write out end of object mark
buf.put((byte) 0x00);
buf.put((byte) 0x00);
buf.put(AMF.TYPE_END_OF_OBJECT);
}
@SuppressWarnings("unchecked")
protected boolean serializeField(Serializer serializer, Class<?> objectClass, String keyName, Field field, Method getter) {
// to prevent, NullPointerExceptions, get the element first and check if it's null.
Element element = getSerializeCache().get(objectClass);
Map<String, Boolean> serializeMap = (element == null ? null : (Map<String, Boolean>) element.getObjectValue());
if (serializeMap == null) {
serializeMap = new HashMap<String, Boolean>();
getSerializeCache().put(new Element(objectClass, serializeMap));
}
Boolean serialize;
if (getSerializeCache().isKeyInCache(keyName)) {
serialize = serializeMap.get(keyName);
} else {
serialize = serializer.serializeField(keyName, field, getter);
serializeMap.put(keyName, serialize);
}
return serialize;
}
@SuppressWarnings("unchecked")
protected Field getField(Class<?> objectClass, String keyName) {
//again, to prevent null pointers, check if the element exists first.
Element element = getFieldCache().get(objectClass);
Map<String, Field> fieldMap = (element == null ? null : (Map<String, Field>) element.getObjectValue());
if (fieldMap == null) {
fieldMap = new HashMap<String, Field>();
getFieldCache().put(new Element(objectClass, fieldMap));
}
Field field = null;
if (fieldMap.containsKey(keyName)) {
field = fieldMap.get(keyName);
} else {
for (Class<?> clazz = objectClass; !clazz.equals(Object.class); clazz = clazz.getSuperclass()) {
Field[] fields = clazz.getDeclaredFields();
if (fields.length > 0) {
for (Field fld : fields) {
if (fld.getName().equals(keyName)) {
field = fld;
break;
}
}
}
}
fieldMap.put(keyName, field);
}
return field;
}
@SuppressWarnings("unchecked")
protected Method getGetter(Class<?> objectClass, BeanMap beanMap, String keyName) {
//check element to prevent null pointer
Element element = getGetterCache().get(objectClass);
Map<String, Method> getterMap = (element == null ? null : (Map<String, Method>) element.getObjectValue());
if (getterMap == null) {
getterMap = new HashMap<String, Method>();
getGetterCache().put(new Element(objectClass, getterMap));
}
Method getter;
if (getterMap.containsKey(keyName)) {
getter = getterMap.get(keyName);
} else {
getter = beanMap.getReadMethod(keyName);
getterMap.put(keyName, getter);
}
return getter;
}
/** {@inheritDoc} */
public void writeObject(Map<Object, Object> map, Serializer serializer) {
if (checkWriteReference(map)) {
return;
}
storeReference(map);
buf.put(AMF.TYPE_OBJECT);
boolean isBeanMap = (map instanceof BeanMap);
for (Map.Entry<Object, Object> entry : map.entrySet()) {
if (isBeanMap && "class".equals(entry.getKey())) {
continue;
}
putString(entry.getKey().toString());
serializer.serialize(this, entry.getValue());
}
buf.put((byte) 0x00);
buf.put((byte) 0x00);
buf.put(AMF.TYPE_END_OF_OBJECT);
}
/**
* Writes an arbitrary object to the output.
*
* @param serializer Output writer
* @param object Object to write
*/
protected void writeArbitraryObject(Object object, Serializer serializer) {
log.debug("writeObject");
// If we need to serialize class information...
Class<?> objectClass = object.getClass();
if (!objectClass.isAnnotationPresent(Anonymous.class)) {
// Write out start object marker for class name
buf.put(AMF.TYPE_CLASS_OBJECT);
putString(buf, serializer.getClassName(objectClass));
} else {
// Write out start object marker without class name
buf.put(AMF.TYPE_OBJECT);
}
// Iterate thru fields of an object to build "name-value" map from it
for (Field field : objectClass.getFields()) {
String fieldName = field.getName();
log.debug("Field: {} class: {}", field, objectClass);
// Check if the Field corresponding to the getter/setter pair is transient
if (!serializeField(serializer, objectClass, fieldName, field, null)) {
continue;
}
Object value;
try {
// Get field value
value = field.get(object);
} catch (IllegalAccessException err) {
// Swallow on private and protected properties access exception
continue;
}
// Write out prop name
putString(buf, fieldName);
// Write out
serializer.serialize(this, field, null, object, value);
}
// Write out end of object marker
buf.put((byte) 0x00);
buf.put((byte) 0x00);
buf.put(AMF.TYPE_END_OF_OBJECT);
}
/** {@inheritDoc} */
public void writeString(String string) {
final byte[] encoded = encodeString(string);
final int len = encoded.length;
if (len < AMF.LONG_STRING_LENGTH) {
buf.put(AMF.TYPE_STRING);
buf.putShort((short) len);
} else {
buf.put(AMF.TYPE_LONG_STRING);
buf.putInt(len);
}
buf.put(encoded);
}
/** {@inheritDoc} */
public void writeByteArray(ByteArray array) {
throw new RuntimeException("ByteArray objects not supported with AMF0");
}
/** {@inheritDoc} */
public void writeVectorInt(Vector<Integer> vector) {
throw new RuntimeException("Vector objects not supported with AMF0");
}
/** {@inheritDoc} */
public void writeVectorUInt(Vector<Long> vector) {
throw new RuntimeException("Vector objects not supported with AMF0");
}
/** {@inheritDoc} */
public void writeVectorNumber(Vector<Double> vector) {
throw new RuntimeException("Vector objects not supported with AMF0");
}
/** {@inheritDoc} */
public void writeVectorObject(Vector<Object> vector) {
throw new RuntimeException("Vector objects not supported with AMF0");
}
/**
* Encode string.
*
* @param string
* @return encoded string
*/
protected static byte[] encodeString(String string) {
Element element = getStringCache().get(string);
byte[] encoded = (element == null ? null : (byte[]) element.getObjectValue());
if (encoded == null) {
ByteBuffer buf = AMF.CHARSET.encode(string);
encoded = new byte[buf.limit()];
buf.get(encoded);
getStringCache().put(new Element(string, encoded));
}
return encoded;
}
/**
* Write out string
* @param buf Byte buffer to write to
* @param string String to write
*/
public static void putString(IoBuffer buf, String string) {
final byte[] encoded = encodeString(string);
buf.putShort((short) encoded.length);
buf.put(encoded);
}
/** {@inheritDoc} */
public void putString(String string) {
putString(buf, string);
}
/** {@inheritDoc} */
public void writeXML(Document xml) {
buf.put(AMF.TYPE_XML);
putString(XMLUtils.docToString(xml));
}
/**
* Convenience method to allow XML text to be used, instead
* of requiring an XML Document.
*
* @param xml xml to write
*/
public void writeXML(String xml) {
buf.put(AMF.TYPE_XML);
putString(xml);
}
/**
* Return buffer of this Output object
* @return Byte buffer of this Output object
*/
public IoBuffer buf() {
return this.buf;
}
public void reset() {
clearReferences();
}
protected static Cache getStringCache() {
if (stringCache == null) {
stringCache = getCacheManager().getCache("org.red5.io.amf.Output.stringCache");
}
return stringCache;
}
protected static Cache getSerializeCache() {
if (serializeCache == null) {
serializeCache = getCacheManager().getCache("org.red5.io.amf.Output.serializeCache");
}
return serializeCache;
}
protected static Cache getFieldCache() {
if (fieldCache == null) {
fieldCache = getCacheManager().getCache("org.red5.io.amf.Output.fieldCache");
}
return fieldCache;
}
protected static Cache getGetterCache() {
if (getterCache == null) {
getterCache = getCacheManager().getCache("org.red5.io.amf.Output.getterCache");
}
return getterCache;
}
public static void destroyCache() {
if (cacheManager != null) {
cacheManager.shutdown();
fieldCache = null;
getterCache = null;
serializeCache = null;
stringCache = null;
}
}
}