/**
* GRANITE DATA SERVICES
* Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S.
*
* This file is part of the Granite Data Services Platform.
*
* Granite Data Services 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.
*
* Granite Data Services 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA, or see <http://www.gnu.org/licenses/>.
*/
/*
* www.openamf.org
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.granite.messaging.amf.io;
import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.IOException;
import java.io.InputStream;
import java.io.UTFDataFormatException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.granite.config.AMF3Config;
import org.granite.context.GraniteContext;
import org.granite.logging.Logger;
import org.granite.messaging.amf.AMF0Body;
import org.granite.messaging.amf.AMF0Message;
import org.w3c.dom.Document;
import flex.messaging.io.ASObject;
/**
* AMF Deserializer
*
* @author Jason Calabrese <jasonc@missionvi.com>
* @author Pat Maddox <pergesu@users.sourceforge.net>
* @author Sylwester Lachiewicz <lachiewicz@plusnet.pl>
* @version $Revision: 1.38 $, $Date: 2004/12/09 04:50:07 $
*/
public class AMF0Deserializer {
private static final Logger log = Logger.getLogger(AMF0Deserializer.class);
private List<Object> storedObjects = null;
/**
* The AMF input stream
*/
private final DataInput dataInput;
/**
* Object to store the deserialized data
*/
private final AMF0Message message = new AMF0Message();
/**
* Deserialize message
*
* @param inputStream message input stream
* @throws IOException
*/
public AMF0Deserializer(InputStream inputStream) throws IOException {
AMF3Config config = GraniteContext.getCurrentInstance().getGraniteConfig();
this.dataInput = config.newAMF3Deserializer(inputStream);
// Read the binary header
readHeaders();
log.debug("readHeader");
// Read the binary body
readBodies();
log.debug("readBody");
}
public AMF0Message getAMFMessage() {
return message;
}
/**
* Read message header
*
* @throws IOException
*/
protected void readHeaders() throws IOException {
// version
message.setVersion(dataInput.readUnsignedShort());
// Find total number of header elements
int headerCount = dataInput.readUnsignedShort();
log.debug("headerCount = %d", headerCount);
// Loop over all the header elements
for (int i = 0; i < headerCount; i++) {
// clear storedObjects - references are new for every header
storedObjects = new ArrayList<Object>();
String key = dataInput.readUTF();
// Find the must understand flag
boolean required = dataInput.readBoolean();
// Grab the length of the header element
/*long length =*/ dataInput.readInt();
// Grab the type of the element
byte type = dataInput.readByte();
// Turn the element into real data
Object value = readData(type);
// Save the name/value into the headers array
message.addHeader(key, required, value);
}
}
/**
* Read message body
*
* @throws IOException
*/
protected void readBodies() throws IOException {
// Find the total number of body elements
int bodyCount = dataInput.readUnsignedShort();
log.debug("bodyCount = %d", bodyCount);
// Loop over all the body elements
for (int i = 0; i < bodyCount; i++) {
//clear storedObjects
storedObjects = new ArrayList<Object>();
// The target method
String method = dataInput.readUTF();
// The target that the client understands
String target = dataInput.readUTF();
// Get the length of the body element
/*long length =*/ dataInput.readInt();
// Grab the type of the element
byte type = dataInput.readByte();
log.debug("type = 0x%02X", type);
// Turn the argument elements into real data
Object data = readData(type);
// Add the body element to the body object
message.addBody(method, target, data, type);
}
}
/**
* Reads custom class
*
* @return the read Object
* @throws IOException
*/
protected Object readCustomClass() throws IOException {
// Grab the explicit type - somehow it works
String type = dataInput.readUTF();
log.debug("Reading Custom Class: %s", type);
/*
String mappedJavaClass = OpenAMFConfig.getInstance().getJavaClassName(type);
if (mappedJavaClass != null) {
type = mappedJavaClass;
}
*/
ASObject aso = new ASObject(type);
// The rest of the bytes are an object without the 0x03 header
return readObject(aso);
}
protected ASObject readObject() throws IOException {
ASObject aso = new ASObject();
return readObject(aso);
}
/**
* Reads an object and converts the binary data into an List
*
* @param aso
* @return the read object
* @throws IOException
*/
protected ASObject readObject(ASObject aso) throws IOException {
storeObject(aso);
// Init the array
log.debug("reading object");
// Grab the key
String key = dataInput.readUTF();
for (byte type = dataInput.readByte();
type != 9;
type = dataInput.readByte()) {
// Grab the value
Object value = readData(type);
// Save the name/value pair in the map
if (value == null) {
log.info("Skipping NULL value for : %s", key);
} else {
aso.put(key, value);
log.debug(" adding {key=%s, value=%s, type=%d}", key, value, type);
}
// Get the next name
key = dataInput.readUTF();
}
log.debug("finished reading object");
// Return the map
return aso;
}
/**
* Reads array
*
* @return the read array (as a list).
* @throws IOException
*/
protected List<?> readArray() throws IOException {
// Init the array
List<Object> array = new ArrayList<Object>();
storeObject(array);
log.debug("Reading array");
// Grab the length of the array
long length = dataInput.readInt();
log.debug("array length = %d", length);
// Loop over all the elements in the data
for (long i = 0; i < length; i++) {
// Grab the type for each element
byte type = dataInput.readByte();
// Grab the element
Object data = readData(type);
array.add(data);
}
// Return the data
return array;
}
/**
* Store object in internal array
*
* @param o the object to store
*/
private void storeObject(Object o) {
storedObjects.add(o);
log.debug("storedObjects.size: %d", storedObjects.size());
}
/**
* Reads date
*
* @return the read date
* @throws IOException
*/
protected Date readDate() throws IOException {
long ms = (long) dataInput.readDouble(); // Date in millis from 01/01/1970
// here we have to read in the raw
// timezone offset (which comes in minutes, but incorrectly signed),
// make it millis, and fix the sign.
int timeoffset = dataInput.readShort() * 60000 * -1; // now we have millis
TimeZone serverTimeZone = TimeZone.getDefault();
// now we subtract the current timezone offset and add the one that was passed
// in (which is of the Flash client), which gives us the appropriate ms (i think)
// -alon
Calendar sent = new GregorianCalendar();
sent.setTime( (new Date(ms - serverTimeZone.getRawOffset() + timeoffset)));
TimeZone sentTimeZone = sent.getTimeZone();
// we have to handle daylight savings ms as well
if (sentTimeZone.inDaylightTime(sent.getTime()))
{
//
// Implementation note: we are trying to maintain compatibility
// with J2SE 1.3.1
//
// As such, we can't use java.util.Calendar.getDSTSavings() here
//
sent.setTime(new java.util.Date(sent.getTime().getTime() - 3600000));
}
return sent.getTime();
}
/**
* Reads flushed stored object
*
* @return the stored object
* @throws IOException
*/
protected Object readFlushedSO() throws IOException {
int index = dataInput.readUnsignedShort();
log.debug("Object Index: %d", index);
return storedObjects.get(index);
}
/**
* Reads object
*
* @return always null...
*/
protected Object readASObject() {
return null;
}
/**
* Reads object
*
* @return the AMF3 decoded object
*/
protected Object readAMF3Data() throws IOException {
AMF3Deserializer amf3 = (AMF3Deserializer)dataInput;
amf3.reset();
return amf3.readObject();
}
/**
* Reads object from inputstream with selected type
*
* @param type
* @return the read object
* @throws IOException
*/
protected Object readData(byte type) throws IOException {
log.debug("Reading data of type: %s", AMF0Body.getObjectTypeDescription(type));
switch (type) {
case AMF0Body.DATA_TYPE_NUMBER: // 0
return new Double(dataInput.readDouble());
case AMF0Body.DATA_TYPE_BOOLEAN: // 1
return new Boolean(dataInput.readBoolean());
case AMF0Body.DATA_TYPE_STRING: // 2
return dataInput.readUTF();
case AMF0Body.DATA_TYPE_OBJECT: // 3
return readObject();
case AMF0Body.DATA_TYPE_MOVIE_CLIP: // 4
throw new IOException("Unknown/unsupported object type " + AMF0Body.getObjectTypeDescription(type));
case AMF0Body.DATA_TYPE_NULL: // 5
case AMF0Body.DATA_TYPE_UNDEFINED: //6
return null;
case AMF0Body.DATA_TYPE_REFERENCE_OBJECT: // 7
return readFlushedSO();
case AMF0Body.DATA_TYPE_MIXED_ARRAY: // 8
/*long length =*/ dataInput.readInt();
//don't do anything with the length
return readObject();
case AMF0Body.DATA_TYPE_OBJECT_END: // 9
return null;
case AMF0Body.DATA_TYPE_ARRAY: // 10
return readArray();
case AMF0Body.DATA_TYPE_DATE: // 11
return readDate();
case AMF0Body.DATA_TYPE_LONG_STRING: // 12
return readLongUTF(dataInput);
case AMF0Body.DATA_TYPE_AS_OBJECT: // 13
return readASObject();
case AMF0Body.DATA_TYPE_RECORDSET: // 14
return null;
case AMF0Body.DATA_TYPE_XML: // 15
return convertToDOM(dataInput);
case AMF0Body.DATA_TYPE_CUSTOM_CLASS: // 16
return readCustomClass();
case AMF0Body.DATA_TYPE_AMF3_OBJECT: // 17
return readAMF3Data();
default :
throw new IOException("Unknown/unsupported object type " + AMF0Body.getObjectTypeDescription(type));
}
}
/**
* This is a hacked verison of Java's DataInputStream.readUTF(), which only
* supports Strings <= 65535 UTF-8-encoded characters
*/
private Object readLongUTF(DataInput in) throws IOException {
int utflen = in.readInt();
StringBuffer str = new StringBuffer(utflen);
byte bytearr [] = new byte[utflen];
int c, char2, char3;
int count = 0;
in.readFully(bytearr, 0, utflen);
while (count < utflen) {
c = bytearr[count] & 0xff;
switch (c >> 4) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
/* 0xxxxxxx*/
count++;
str.append((char)c);
break;
case 12: case 13:
/* 110x xxxx 10xx xxxx*/
count += 2;
if (count > utflen)
throw new UTFDataFormatException();
char2 = bytearr[count-1];
if ((char2 & 0xC0) != 0x80)
throw new UTFDataFormatException();
str.append((char)(((c & 0x1F) << 6) | (char2 & 0x3F)));
break;
case 14:
/* 1110 xxxx 10xx xxxx 10xx xxxx */
count += 3;
if (count > utflen)
throw new UTFDataFormatException();
char2 = bytearr[count-2];
char3 = bytearr[count-1];
if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
throw new UTFDataFormatException();
str.append((char)(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0)));
break;
default:
/* 10xx xxxx, 1111 xxxx */
throw new UTFDataFormatException();
}
}
// The number of chars produced may be less than utflen
return new String(str);
}
public static Document convertToDOM(DataInput is) throws IOException {
Document document = null;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
int length = is.readInt();
try {
byte[] buf = new byte[length];
is.readFully(buf, 0, length);
DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.parse(new ByteArrayInputStream(buf));
} catch (Exception e) {
throw new IOException("Error while parsing xml: " + e.getMessage());
}
return document;
}
}