001/* 002 * www.openamf.org 003 * 004 * Distributable under LGPL license. 005 * See terms of license at gnu.org. 006 */ 007 008package org.granite.messaging.amf.io; 009 010import java.io.ByteArrayInputStream; 011import java.io.DataInputStream; 012import java.io.IOException; 013import java.io.InputStream; 014import java.io.ObjectInput; 015import java.io.UTFDataFormatException; 016import java.util.ArrayList; 017import java.util.Calendar; 018import java.util.Date; 019import java.util.GregorianCalendar; 020import java.util.List; 021import java.util.TimeZone; 022 023import javax.xml.parsers.DocumentBuilder; 024import javax.xml.parsers.DocumentBuilderFactory; 025 026import org.granite.context.GraniteContext; 027import org.granite.logging.Logger; 028import org.granite.messaging.amf.AMF0Body; 029import org.granite.messaging.amf.AMF0Message; 030import org.w3c.dom.Document; 031 032import 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 */ 042public 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}