/* * 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; } } }