001 /*
002 * www.openamf.org
003 *
004 * Distributable under LGPL license.
005 * See terms of license at gnu.org.
006 */
007
008 package org.granite.messaging.amf.io;
009
010 import java.io.ByteArrayInputStream;
011 import java.io.DataInputStream;
012 import java.io.IOException;
013 import java.io.InputStream;
014 import java.io.ObjectInput;
015 import java.io.UTFDataFormatException;
016 import java.util.ArrayList;
017 import java.util.Calendar;
018 import java.util.Date;
019 import java.util.GregorianCalendar;
020 import java.util.List;
021 import java.util.TimeZone;
022
023 import javax.xml.parsers.DocumentBuilder;
024 import javax.xml.parsers.DocumentBuilderFactory;
025
026 import org.granite.context.GraniteContext;
027 import org.granite.logging.Logger;
028 import org.granite.messaging.amf.AMF0Body;
029 import org.granite.messaging.amf.AMF0Message;
030 import org.w3c.dom.Document;
031
032 import flex.messaging.io.ASObject;
033
034 /**
035 * AMF Deserializer
036 *
037 * @author Jason Calabrese <jasonc@missionvi.com>
038 * @author Pat Maddox <pergesu@users.sourceforge.net>
039 * @author Sylwester Lachiewicz <lachiewicz@plusnet.pl>
040 * @version $Revision: 1.38 $, $Date: 2004/12/09 04:50:07 $
041 */
042 public class AMF0Deserializer {
043
044 private static final Logger log = Logger.getLogger(AMF0Deserializer.class);
045
046 private List<Object> storedObjects = null;
047
048 /**
049 * The AMF input stream
050 */
051 private final InputStream rawInputStream;
052 private final DataInputStream dataInputStream;
053
054 /**
055 * Object to store the deserialized data
056 */
057 private final AMF0Message message = new AMF0Message();
058
059
060 /**
061 * Deserialize message
062 *
063 * @param inputStream message input stream
064 * @throws IOException
065 */
066 public AMF0Deserializer(InputStream inputStream) throws IOException {
067 //log.info("Deserializing Message, for more info turn on debug level");
068
069 // Save the input stream for this object
070 this.rawInputStream = inputStream;
071 this.dataInputStream = inputStream instanceof DataInputStream
072 ? ((DataInputStream)inputStream)
073 : new DataInputStream(inputStream);
074
075 // Read the binary header
076 readHeaders();
077 log.debug("readHeader");
078 // Read the binary body
079 readBodies();
080 log.debug("readBody");
081 }
082
083 public AMF0Message getAMFMessage() {
084 return message;
085 }
086
087 /**
088 * Read message header
089 *
090 * @throws IOException
091 */
092 protected void readHeaders() throws IOException {
093 // version
094 message.setVersion(dataInputStream.readUnsignedShort());
095 // Find total number of header elements
096 int headerCount = dataInputStream.readUnsignedShort();
097 log.debug("headerCount = %d", headerCount);
098
099 // Loop over all the header elements
100 for (int i = 0; i < headerCount; i++) {
101 // clear storedObjects - references are new for every header
102 storedObjects = new ArrayList<Object>();
103 String key = dataInputStream.readUTF();
104 // Find the must understand flag
105 boolean required = dataInputStream.readBoolean();
106 // Grab the length of the header element
107 /*long length =*/ dataInputStream.readInt();
108 // Grab the type of the element
109 byte type = dataInputStream.readByte();
110 // Turn the element into real data
111 Object value = readData(type);
112 // Save the name/value into the headers array
113 message.addHeader(key, required, value);
114 }
115 }
116
117 /**
118 * Read message body
119 *
120 * @throws IOException
121 */
122 protected void readBodies() throws IOException {
123 // Find the total number of body elements
124 int bodyCount = dataInputStream.readUnsignedShort();
125 log.debug("bodyCount = %d", bodyCount);
126
127 // Loop over all the body elements
128 for (int i = 0; i < bodyCount; i++) {
129 //clear storedObjects
130 storedObjects = new ArrayList<Object>();
131 // The target method
132 String method = dataInputStream.readUTF();
133 // The target that the client understands
134 String target = dataInputStream.readUTF();
135 // Get the length of the body element
136 /*long length =*/ dataInputStream.readInt();
137 // Grab the type of the element
138 byte type = dataInputStream.readByte();
139 log.debug("type = 0x%02X", type);
140 // Turn the argument elements into real data
141 Object data = readData(type);
142 // Add the body element to the body object
143 message.addBody(method, target, data, type);
144 }
145 }
146
147 /**
148 * Reads custom class
149 *
150 * @return the read Object
151 * @throws IOException
152 */
153 protected Object readCustomClass() throws IOException {
154 // Grab the explicit type - somehow it works
155 String type = dataInputStream.readUTF();
156 log.debug("Reading Custom Class: %s", type);
157 /*
158 String mappedJavaClass = OpenAMFConfig.getInstance().getJavaClassName(type);
159 if (mappedJavaClass != null) {
160 type = mappedJavaClass;
161 }
162 */
163 ASObject aso = new ASObject(type);
164 // The rest of the bytes are an object without the 0x03 header
165 return readObject(aso);
166 }
167
168 protected ASObject readObject() throws IOException {
169 ASObject aso = new ASObject();
170 return readObject(aso);
171 }
172
173 /**
174 * Reads an object and converts the binary data into an List
175 *
176 * @param aso
177 * @return the read object
178 * @throws IOException
179 */
180 protected ASObject readObject(ASObject aso) throws IOException {
181 storeObject(aso);
182 // Init the array
183 log.debug("reading object");
184 // Grab the key
185 String key = dataInputStream.readUTF();
186 for (byte type = dataInputStream.readByte();
187 type != 9;
188 type = dataInputStream.readByte()) {
189 // Grab the value
190 Object value = readData(type);
191 // Save the name/value pair in the map
192 if (value == null) {
193 log.info("Skipping NULL value for : %s", key);
194 } else {
195 aso.put(key, value);
196 log.debug(" adding {key=%s, value=%s, type=%d}", key, value, type);
197 }
198 // Get the next name
199 key = dataInputStream.readUTF();
200 }
201 log.debug("finished reading object");
202 // Return the map
203 return aso;
204 }
205
206 /**
207 * Reads array
208 *
209 * @return the read array (as a list).
210 * @throws IOException
211 */
212 protected List<?> readArray() throws IOException {
213 // Init the array
214 List<Object> array = new ArrayList<Object>();
215 storeObject(array);
216 log.debug("Reading array");
217 // Grab the length of the array
218 long length = dataInputStream.readInt();
219 log.debug("array length = %d", length);
220 // Loop over all the elements in the data
221 for (long i = 0; i < length; i++) {
222 // Grab the type for each element
223 byte type = dataInputStream.readByte();
224 // Grab the element
225 Object data = readData(type);
226 array.add(data);
227 }
228 // Return the data
229 return array;
230 }
231
232 /**
233 * Store object in internal array
234 *
235 * @param o the object to store
236 */
237 private void storeObject(Object o) {
238 storedObjects.add(o);
239 log.debug("storedObjects.size: %d", storedObjects.size());
240 }
241
242 /**
243 * Reads date
244 *
245 * @return the read date
246 * @throws IOException
247 */
248 protected Date readDate() throws IOException {
249 long ms = (long) dataInputStream.readDouble(); // Date in millis from 01/01/1970
250
251 // here we have to read in the raw
252 // timezone offset (which comes in minutes, but incorrectly signed),
253 // make it millis, and fix the sign.
254 int timeoffset = dataInputStream.readShort() * 60000 * -1; // now we have millis
255
256 TimeZone serverTimeZone = TimeZone.getDefault();
257
258 // now we subtract the current timezone offset and add the one that was passed
259 // in (which is of the Flash client), which gives us the appropriate ms (i think)
260 // -alon
261 Calendar sent = new GregorianCalendar();
262 sent.setTime( (new Date(ms - serverTimeZone.getRawOffset() + timeoffset)));
263
264 TimeZone sentTimeZone = sent.getTimeZone();
265
266 // we have to handle daylight savings ms as well
267 if (sentTimeZone.inDaylightTime(sent.getTime()))
268 {
269 //
270 // Implementation note: we are trying to maintain compatibility
271 // with J2SE 1.3.1
272 //
273 // As such, we can't use java.util.Calendar.getDSTSavings() here
274 //
275 sent.setTime(new java.util.Date(sent.getTime().getTime() - 3600000));
276 }
277
278 return sent.getTime();
279 }
280
281 /**
282 * Reads flushed stored object
283 *
284 * @return the stored object
285 * @throws IOException
286 */
287 protected Object readFlushedSO() throws IOException {
288 int index = dataInputStream.readUnsignedShort();
289 log.debug("Object Index: %d", index);
290 return storedObjects.get(index);
291 }
292
293 /**
294 * Reads object
295 *
296 * @return always null...
297 */
298 protected Object readASObject() {
299 return null;
300 }
301
302 /**
303 * Reads object
304 *
305 * @return the AMF3 decoded object
306 */
307 protected Object readAMF3Data() throws IOException {
308 ObjectInput amf3 = GraniteContext.getCurrentInstance().getGraniteConfig().newAMF3Deserializer(rawInputStream);
309 try {
310 return amf3.readObject();
311 } catch (ClassNotFoundException e) {
312 throw new RuntimeException(e);
313 }
314 }
315
316 /**
317 * Reads object from inputstream with selected type
318 *
319 * @param type
320 * @return the read object
321 * @throws IOException
322 */
323 protected Object readData(byte type) throws IOException {
324 log.debug("Reading data of type: %s", AMF0Body.getObjectTypeDescription(type));
325 switch (type) {
326 case AMF0Body.DATA_TYPE_NUMBER: // 0
327 return new Double(dataInputStream.readDouble());
328 case AMF0Body.DATA_TYPE_BOOLEAN: // 1
329 return new Boolean(dataInputStream.readBoolean());
330 case AMF0Body.DATA_TYPE_STRING: // 2
331 return dataInputStream.readUTF();
332 case AMF0Body.DATA_TYPE_OBJECT: // 3
333 return readObject();
334 case AMF0Body.DATA_TYPE_MOVIE_CLIP: // 4
335 throw new IOException("Unknown/unsupported object type " + AMF0Body.getObjectTypeDescription(type));
336 case AMF0Body.DATA_TYPE_NULL: // 5
337 case AMF0Body.DATA_TYPE_UNDEFINED: //6
338 return null;
339 case AMF0Body.DATA_TYPE_REFERENCE_OBJECT: // 7
340 return readFlushedSO();
341 case AMF0Body.DATA_TYPE_MIXED_ARRAY: // 8
342 /*long length =*/ dataInputStream.readInt();
343 //don't do anything with the length
344 return readObject();
345 case AMF0Body.DATA_TYPE_OBJECT_END: // 9
346 return null;
347 case AMF0Body.DATA_TYPE_ARRAY: // 10
348 return readArray();
349 case AMF0Body.DATA_TYPE_DATE: // 11
350 return readDate();
351 case AMF0Body.DATA_TYPE_LONG_STRING: // 12
352 return readLongUTF(dataInputStream);
353 case AMF0Body.DATA_TYPE_AS_OBJECT: // 13
354 return readASObject();
355 case AMF0Body.DATA_TYPE_RECORDSET: // 14
356 return null;
357 case AMF0Body.DATA_TYPE_XML: // 15
358 return convertToDOM(dataInputStream);
359 case AMF0Body.DATA_TYPE_CUSTOM_CLASS: // 16
360 return readCustomClass();
361 case AMF0Body.DATA_TYPE_AMF3_OBJECT: // 17
362 return readAMF3Data();
363 default :
364 throw new IOException("Unknown/unsupported object type " + AMF0Body.getObjectTypeDescription(type));
365 }
366 }
367
368 /**
369 * This is a hacked verison of Java's DataInputStream.readUTF(), which only
370 * supports Strings <= 65535 UTF-8-encoded characters
371 */
372 private Object readLongUTF(DataInputStream in) throws IOException {
373 int utflen = in.readInt();
374 StringBuffer str = new StringBuffer(utflen);
375 byte bytearr [] = new byte[utflen];
376 int c, char2, char3;
377 int count = 0;
378
379 in.readFully(bytearr, 0, utflen);
380
381 while (count < utflen) {
382 c = bytearr[count] & 0xff;
383 switch (c >> 4) {
384 case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
385 /* 0xxxxxxx*/
386 count++;
387 str.append((char)c);
388 break;
389 case 12: case 13:
390 /* 110x xxxx 10xx xxxx*/
391 count += 2;
392 if (count > utflen)
393 throw new UTFDataFormatException();
394 char2 = bytearr[count-1];
395 if ((char2 & 0xC0) != 0x80)
396 throw new UTFDataFormatException();
397 str.append((char)(((c & 0x1F) << 6) | (char2 & 0x3F)));
398 break;
399 case 14:
400 /* 1110 xxxx 10xx xxxx 10xx xxxx */
401 count += 3;
402 if (count > utflen)
403 throw new UTFDataFormatException();
404 char2 = bytearr[count-2];
405 char3 = bytearr[count-1];
406 if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
407 throw new UTFDataFormatException();
408 str.append((char)(((c & 0x0F) << 12) |
409 ((char2 & 0x3F) << 6) |
410 ((char3 & 0x3F) << 0)));
411 break;
412 default:
413 /* 10xx xxxx, 1111 xxxx */
414 throw new UTFDataFormatException();
415 }
416 }
417
418 // The number of chars produced may be less than utflen
419 return new String(str);
420 }
421
422 public static Document convertToDOM(DataInputStream is) throws IOException {
423 Document document = null;
424 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
425 int length = is.readInt();
426 try {
427 byte[] buf = new byte[length];
428 is.readFully(buf, 0, length);
429 DocumentBuilder builder = factory.newDocumentBuilder();
430 document = builder.parse(new ByteArrayInputStream(buf));
431 } catch (Exception e) {
432 throw new IOException("Error while parsing xml: " + e.getMessage());
433 }
434 return document;
435 }
436 }