/* * 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.beans.PropertyDescriptor; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Vector; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.beanutils.BeanUtilsBean; import org.apache.commons.beanutils.PropertyUtilsBean; import org.apache.mina.core.buffer.IoBuffer; import org.red5.io.amf3.ByteArray; import org.red5.io.object.BaseInput; import org.red5.io.object.DataTypes; import org.red5.io.object.Deserializer; import org.red5.io.object.RecordSet; import org.red5.io.object.RecordSetPage; import org.red5.io.utils.ArrayUtils; import org.red5.io.utils.ObjectMap; import org.red5.io.utils.XMLUtils; import org.red5.server.util.ConversionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; /** * Input for Red5 data types * * @author The Red5 Project (red5@osflash.org) * @author Luke Hubbard, Codegent Ltd (luke@codegent.com) */ @SuppressWarnings("serial") public class Input extends BaseInput implements org.red5.io.object.Input { protected static Logger log = LoggerFactory.getLogger(Input.class); protected static Map<String, String> classAliases = new HashMap<String, String>(3) { { put("DSA", "org.red5.compatibility.flex.messaging.messages.AsyncMessageExt"); put("DSC", "org.red5.compatibility.flex.messaging.messages.CommandMessageExt"); put("DSK", "org.red5.compatibility.flex.messaging.messages.AcknowledgeMessageExt"); } }; protected IoBuffer buf; protected byte currentDataType; /** * Creates Input object from byte buffer * * @param buf Byte buffer */ public Input(IoBuffer buf) { super(); this.buf = buf; } /** * Reads the data type. * * @return byte Data type */ public byte readDataType() { if (buf != null) { // XXX Paul: prevent an NPE here by returning the current data type // when there is a null buffer currentDataType = buf.get(); } else { log.error("Why is buf null?"); } return readDataType(currentDataType); } /** * Reads the data type. * * @param dataType Data type as byte * @return One of AMF class constants with type * @see org.red5.io.amf.AMF */ protected byte readDataType(byte dataType) { byte coreType; switch (currentDataType) { case AMF.TYPE_NULL: case AMF.TYPE_UNDEFINED: coreType = DataTypes.CORE_NULL; break; case AMF.TYPE_NUMBER: coreType = DataTypes.CORE_NUMBER; break; case AMF.TYPE_BOOLEAN: coreType = DataTypes.CORE_BOOLEAN; break; case AMF.TYPE_STRING: case AMF.TYPE_LONG_STRING: coreType = DataTypes.CORE_STRING; break; case AMF.TYPE_CLASS_OBJECT: case AMF.TYPE_OBJECT: coreType = DataTypes.CORE_OBJECT; break; case AMF.TYPE_MIXED_ARRAY: coreType = DataTypes.CORE_MAP; break; case AMF.TYPE_ARRAY: coreType = DataTypes.CORE_ARRAY; break; case AMF.TYPE_DATE: coreType = DataTypes.CORE_DATE; break; case AMF.TYPE_XML: coreType = DataTypes.CORE_XML; break; case AMF.TYPE_REFERENCE: coreType = DataTypes.OPT_REFERENCE; break; case AMF.TYPE_UNSUPPORTED: case AMF.TYPE_MOVIECLIP: case AMF.TYPE_RECORDSET: // These types are not handled by core datatypes // So add the amf mask to them, this way the deserializer // will call back to readCustom, we can then handle or return null coreType = (byte) (currentDataType + DataTypes.CUSTOM_AMF_MASK); break; case AMF.TYPE_END_OF_OBJECT: default: // End of object, and anything else lets just skip coreType = DataTypes.CORE_SKIP; break; } return coreType; } // Basic /** * Reads a null. * * @return Object */ public Object readNull(Type target) { return null; } /** * Reads a boolean. * * @return boolean */ public Boolean readBoolean(Type target) { // TODO: check values return (buf.get() == AMF.VALUE_TRUE) ? Boolean.TRUE : Boolean.FALSE; } /** * Reads a Number. In ActionScript 1 and 2 Number type represents all numbers, * both floats and integers. * * @return Number */ public Number readNumber(Type target) { double num = buf.getDouble(); if (num == Math.round(num)) { if (num < Integer.MAX_VALUE) { return (int) num; } else { return Math.round(num); } } else { return num; } } /** * Reads string from buffer * @return String */ public String getString() { return getString(buf); } /** * Reads a string * * @return String */ public String readString(Type target) { int len = 0; switch (currentDataType) { case AMF.TYPE_LONG_STRING: len = buf.getInt(); break; case AMF.TYPE_STRING: len = buf.getShort() & 0xffff; //buf.getUnsignedShort(); break; default: log.debug("Unknown AMF type: {}", currentDataType); } int limit = buf.limit(); log.debug("Limit: {}", limit); String string = bufferToString(buf.buf(), len); buf.limit(limit); // Reset the limit return string; } /** * Returns a string based on the buffer * * @param buf Byte buffer with data * @return String Decoded string */ public static String getString(IoBuffer buf) { int len = buf.getShort() & 0xffff; //buf.getUnsignedShort(); XXX is appears to be broken in mina at 2.0.4 log.debug("Length: {}", len); int limit = buf.limit(); log.debug("Limit: {}", limit); String string = bufferToString(buf.buf(), len); buf.limit(limit); // Reset the limit return string; } /** * Returns a string based on the buffer * * @param buf Byte buffer with data * @return String Decoded string */ public static String getString(java.nio.ByteBuffer buf) { int len = buf.getShort() & 0xffff; log.debug("Length: {}", len); int limit = buf.limit(); log.debug("Limit: {}", limit); String string = bufferToString(buf, len); buf.limit(limit); // Reset the limit return string; } /** * Converts the bytes into a string. * * @param strBuf * @return contents of the ByteBuffer as a String */ private final static String bufferToString(final java.nio.ByteBuffer strBuf, int len) { //Trac #601 - part of the problem seems to be a null byte buffer String string = null; if (strBuf != null) { int pos = strBuf.position(); log.debug("String buf - position: {} limit: {}", pos, (pos + len)); strBuf.limit(pos + len); string = AMF.CHARSET.decode(strBuf).toString(); log.debug("String: {}", string); } else { log.warn("ByteBuffer was null attempting to read String"); } return string; } /** * Returns a date * * @return Date Decoded string object */ public Date readDate(Type target) { /* * Date: 0x0B T7 T6 .. T0 Z1 Z2 T7 to T0 form a 64 bit Big Endian number * that specifies the number of nanoseconds that have passed since * 1/1/1970 0:00 to the specified time. This format is UTC 1970. Z1 an * Z0 for a 16 bit Big Endian number indicating the indicated time's * timezone in minutes. */ long ms = (long) buf.getDouble(); // The timezone can be ignored as the date always is encoded in UTC @SuppressWarnings("unused") short timeZoneMins = buf.getShort(); Date date = new Date(ms); storeReference(date); return date; } // Array public Object readArray(Deserializer deserializer, Type target) { log.debug("readArray - deserializer: {} target: {}", deserializer, target); Object result = null; int count = buf.getInt(); log.debug("Count: {}", count); List<Object> resultCollection = new ArrayList<Object>(count); storeReference(result); for (int i = 0; i < count; i++) { resultCollection.add(deserializer.deserialize(this, Object.class)); } // To maintain conformance to the Input API, we should convert the output // into an Array if the Type asks us to. Class<?> collection = Collection.class; if (target instanceof Class<?>) { collection = (Class<?>) target; } if (collection.isArray()) { result = ArrayUtils.toArray(collection.getComponentType(), resultCollection); } else { result = resultCollection; } return result; } // Map /** * Read key - value pairs. This is required for the RecordSet * deserializer. */ public Map<String, Object> readKeyValues(Deserializer deserializer) { Map<String, Object> result = new HashMap<String, Object>(); readKeyValues(result, deserializer); return result; } /** * Read key - value pairs into Map object * @param result Map to put resulting pair to * @param deserializer Deserializer used */ protected void readKeyValues(Map<String, Object> result, Deserializer deserializer) { while (hasMoreProperties()) { String name = readPropertyName(); log.debug("property: {}", name); Object property = deserializer.deserialize(this, Object.class); log.debug("val: {}", property); result.put(name, property); if (hasMoreProperties()) { skipPropertySeparator(); } } skipEndObject(); } public Object readMap(Deserializer deserializer, Type target) { // The maximum number used in this mixed array. int maxNumber = buf.getInt(); log.debug("Read start mixed array: {}", maxNumber); Object result; final Map<Object, Object> mixedResult = new LinkedHashMap<Object, Object>(maxNumber); // we must store the reference before we deserialize any items in it to ensure // that reference IDs are correct int reference = storeReference(mixedResult); Boolean normalArray = true; while (hasMoreProperties()) { String key = getString(buf); log.debug("key: {}", key); try { Integer.parseInt(key); } catch (NumberFormatException e) { log.debug("key {} is causing non normal array", key); normalArray = false; } Object item = deserializer.deserialize(this, Object.class); log.debug("item: {}", item); mixedResult.put(key, item); } if (mixedResult.size() <= maxNumber + 1 && normalArray) { // MixedArray actually is a regular array log.debug("mixed array is a regular array"); final List<Object> listResult = new ArrayList<Object>(maxNumber); for (int i = 0; i < maxNumber; i++) { listResult.add(i, mixedResult.get(String.valueOf(i))); } result = listResult; } else { // Convert initial indexes mixedResult.remove("length"); for (int i = 0; i < maxNumber; i++) { final Object value = mixedResult.remove(String.valueOf(i)); mixedResult.put(i, value); } result = mixedResult; } // Replace the original reference with the final result storeReference(reference, result); skipEndObject(); return result; } // Object /** * Creates a new instance of the className parameter and * returns as an Object * * @param className Class name as String * @return Object New object instance (for given class) */ @SuppressWarnings("all") protected Object newInstance(String className) { log.debug("Loading class: {}", className); Object instance = null; Class<?> clazz = null; //fix for Trac #604 if ("".equals(className) || className == null) return instance; try { //check for special DS class aliases if (className.length() == 3) { className = classAliases.get(className); } if (className.startsWith("flex.")) { // Use Red5 compatibility class instead className = "org.red5.compatibility." + className; log.debug("Modified classname: {}", className); } clazz = Thread.currentThread().getContextClassLoader().loadClass(className); instance = clazz.newInstance(); } catch (InstantiationException iex) { try { //check for default ctor clazz.getConstructor(null); log.error("Error loading class: {}", className); } catch (NoSuchMethodException nse) { log.error("Error loading class: {}; this can be resolved by adding a default constructor to your class", className); } log.debug("Exception was: {}", iex); } catch (Exception ex) { log.error("Error loading class: {}", className); log.debug("Exception was: {}", ex); } return instance; } /** * Reads the input as a bean and returns an object * * @param deserializer Deserializer used * @param bean Input as bean * @return Decoded object */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected Object readBean(Deserializer deserializer, Object bean) { log.debug("read bean"); storeReference(bean); Class theClass = bean.getClass(); while (hasMoreProperties()) { String name = readPropertyName(); Type type = getPropertyType(bean, name); log.debug("property: {}", name); Object property = deserializer.deserialize(this, type); log.debug("val: {}", property); //log.debug("val: "+property.getClass().getName()); if (property != null) { try { if (type instanceof Class) { Class t = (Class) type; if (!t.isAssignableFrom(property.getClass())) { property = ConversionUtils.convert(property, t); } } final Field field = theClass.getField(name); field.set(bean, property); } catch (Exception ex2) { try { BeanUtils.setProperty(bean, name, property); } catch (Exception ex) { log.error("Error mapping property: {} ({})", name, property); } } } else { log.debug("Skipping null property: {}", name); } if (hasMoreProperties()) { skipPropertySeparator(); } } skipEndObject(); return bean; } /** * Reads the input as a map and returns a Map * * @param deserializer Deserializer to use * @return Read map */ protected Map<String, Object> readSimpleObject(Deserializer deserializer) { log.debug("read map"); Map<String, Object> result = new ObjectMap<String, Object>(); readKeyValues(result, deserializer); storeReference(result); return result; } /** * Reads start object * * @param deserializer Deserializer to use * @return Read object */ public Object readObject(Deserializer deserializer, Type target) { String className; if (currentDataType == AMF.TYPE_CLASS_OBJECT) { className = getString(buf); } else { className = null; } log.debug("readObject: {}", className); Object result = null; if (className != null) { log.debug("read class object"); Object instance; if (className.equals("RecordSet")) { result = new RecordSet(this); storeReference(result); } else if (className.equals("RecordSetPage")) { result = new RecordSetPage(this); storeReference(result); } else { instance = newInstance(className); if (instance != null) { result = readBean(deserializer, instance); } else { log.debug("Forced to use simple object for class {}", className); result = readSimpleObject(deserializer); } } } else { result = readSimpleObject(deserializer); } return result; } /** * Returns a boolean stating whether there are more properties * * @return boolean <code>true</code> if there are more properties to read, <code>false</code> otherwise */ public boolean hasMoreProperties() { byte pad = 0x00; byte pad0 = buf.get(); byte pad1 = buf.get(); byte type = buf.get(); boolean isEndOfObject = (pad0 == pad && pad1 == pad && type == AMF.TYPE_END_OF_OBJECT); log.debug("End of object: ? {}", isEndOfObject); buf.position(buf.position() - 3); return !isEndOfObject; } /** * Reads property name * * @return String Object property name */ public String readPropertyName() { return getString(buf); } /** * Skips property seperator */ public void skipPropertySeparator() { // SKIP } /** * Skips end object */ public void skipEndObject() { // skip two marker bytes // then end of object byte buf.skip(3); } // Others /** * Reads XML * * @return String XML as string */ public Document readXML(Type target) { final String xmlString = readString(target); Document doc = null; try { doc = XMLUtils.stringToDoc(xmlString); } catch (IOException ioex) { log.error("IOException converting xml to dom", ioex); } storeReference(doc); return doc; } /** * Reads Custom * * @return Object Custom type object */ public Object readCustom(Type target) { // Return null for now return null; } /** * Read ByteArray object. This is not supported by the AMF0 deserializer. * * @return ByteArray object */ public ByteArray readByteArray(Type target) { throw new RuntimeException("ByteArray objects not supported with AMF0"); } /** * Read Vector<int> object. This is not supported by the AMF0 deserializer. * * @return Vector<Integer> object */ public Vector<Integer> readVectorInt() { throw new RuntimeException("Vector objects not supported with AMF0"); } /** * Read Vector<Long> object. This is not supported by the AMF0 deserializer. * * @return Vector<Long> object */ public Vector<Long> readVectorUInt() { throw new RuntimeException("Vector objects not supported with AMF0"); } /** * Read Vector<Number> object. This is not supported by the AMF0 deserializer. * * @return Vector<Double> object */ public Vector<Double> readVectorNumber() { throw new RuntimeException("Vector objects not supported with AMF0"); } /** * Read Vector<Object> object. This is not supported by the AMF0 deserializer. * * @return Vector<Object> object */ public Vector<Object> readVectorObject() { throw new RuntimeException("Vector objects not supported with AMF0"); } /** * Reads Reference * * @return Object Read reference to object */ public Object readReference(Type target) { //return getReference(buf.getUnsignedShort()); return getReference(buf.getShort() & 0xffff); } /** * Resets map * */ public void reset() { this.clearReferences(); } protected Type getPropertyType(Object instance, String propertyName) { try { if (instance != null) { Field field = instance.getClass().getField(propertyName); return field.getGenericType(); } else { // instance is null for anonymous class, use default type } } catch (NoSuchFieldException e1) { try { BeanUtilsBean beanUtilsBean = BeanUtilsBean.getInstance(); PropertyUtilsBean propertyUtils = beanUtilsBean.getPropertyUtils(); PropertyDescriptor propertyDescriptor = propertyUtils.getPropertyDescriptor(instance, propertyName); return propertyDescriptor.getReadMethod().getGenericReturnType(); } catch (Exception e2) { // nothing } } catch (Exception e) { // ignore other exceptions } // return Object class type by default return Object.class; } }