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.ByteArrayOutputStream; 011import java.io.DataOutputStream; 012import java.io.IOException; 013import java.io.ObjectOutput; 014import java.io.OutputStream; 015import java.lang.reflect.Method; 016import java.sql.ResultSet; 017import java.util.ArrayList; 018import java.util.Collection; 019import java.util.Date; 020import java.util.IdentityHashMap; 021import java.util.Iterator; 022import java.util.List; 023import java.util.Map; 024import java.util.TimeZone; 025 026import org.granite.context.GraniteContext; 027import org.granite.logging.Logger; 028import org.granite.messaging.amf.AMF0Body; 029import org.granite.messaging.amf.AMF0Header; 030import org.granite.messaging.amf.AMF0Message; 031import org.granite.messaging.amf.AMF3Object; 032import org.granite.util.Introspector; 033import org.granite.util.PropertyDescriptor; 034import org.w3c.dom.Document; 035import org.w3c.dom.Element; 036import org.w3c.dom.NamedNodeMap; 037import org.w3c.dom.Node; 038import org.w3c.dom.NodeList; 039 040import flex.messaging.io.ASObject; 041import flex.messaging.io.ASRecordSet; 042 043/** 044 * AMF Serializer 045 * 046 * @author Jason Calabrese <jasonc@missionvi.com> 047 * @author Pat Maddox <pergesu@users.sourceforge.net> 048 * @author Sylwester Lachiewicz <lachiewicz@plusnet.pl> 049 * @author Richard Pitt 050 * 051 * @version $Revision: 1.54 $, $Date: 2006/03/25 23:41:41 $ 052 */ 053public class AMF0Serializer { 054 055 private static final Logger log = Logger.getLogger(AMF0Serializer.class); 056 057 private static final int MILLS_PER_HOUR = 60000; 058 059 /** 060 * Null message 061 */ 062 private static final String NULL_MESSAGE = "null"; 063 064 /** 065 * The output stream 066 */ 067 private final DataOutputStream dataOutputStream; 068 private final OutputStream rawOutputStream; 069 070 private final Map<Object, Integer> storedObjects = new IdentityHashMap<Object, Integer>(); 071 private int storedObjectCount = 0; 072 073 /** 074 * Constructor 075 * 076 * @param outputStream 077 */ 078 public AMF0Serializer(OutputStream outputStream) { 079 this.rawOutputStream = outputStream; 080 this.dataOutputStream = outputStream instanceof DataOutputStream 081 ? ((DataOutputStream)outputStream) 082 : new DataOutputStream(outputStream); 083 } 084 085 /** 086 * Writes message 087 * 088 * @param message 089 * @throws IOException 090 */ 091 public void serializeMessage(AMF0Message message) throws IOException { 092 //if (log.isInfoEnabled()) 093 // log.info("Serializing Message, for more info turn on debug level"); 094 095 clearStoredObjects(); 096 dataOutputStream.writeShort(message.getVersion()); 097 // write header 098 dataOutputStream.writeShort(message.getHeaderCount()); 099 Iterator<AMF0Header> headers = message.getHeaders().iterator(); 100 while (headers.hasNext()) { 101 AMF0Header header = headers.next(); 102 writeHeader(header); 103 } 104 // write body 105 dataOutputStream.writeShort(message.getBodyCount()); 106 Iterator<AMF0Body> bodies = message.getBodies(); 107 while (bodies.hasNext()) { 108 AMF0Body body = bodies.next(); 109 writeBody(body); 110 } 111 } 112 /** 113 * Writes message header 114 * 115 * @param header AMF message header 116 * @throws IOException 117 */ 118 protected void writeHeader(AMF0Header header) throws IOException { 119 dataOutputStream.writeUTF(header.getKey()); 120 dataOutputStream.writeBoolean(header.isRequired()); 121 // Always, always there is four bytes of FF, which is -1 of course 122 dataOutputStream.writeInt(-1); 123 writeData(header.getValue()); 124 } 125 /** 126 * Writes message body 127 * 128 * @param body AMF message body 129 * @throws IOException 130 */ 131 protected void writeBody(AMF0Body body) throws IOException { 132 // write url 133 if (body.getTarget() == null) { 134 dataOutputStream.writeUTF(NULL_MESSAGE); 135 } else { 136 dataOutputStream.writeUTF(body.getTarget()); 137 } 138 // write response 139 if (body.getResponse() == null) { 140 dataOutputStream.writeUTF(NULL_MESSAGE); 141 } else { 142 dataOutputStream.writeUTF(body.getResponse()); 143 } 144 // Always, always there is four bytes of FF, which is -1 of course 145 dataOutputStream.writeInt(-1); 146 // Write the data to the output stream 147 writeData(body.getValue()); 148 } 149 150 /** 151 * Writes Data 152 * 153 * @param value 154 * @throws IOException 155 */ 156 protected void writeData(Object value) throws IOException { 157 if (value == null) { 158 // write null object 159 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_NULL); 160 } else if (value instanceof AMF3Object) { 161 writeAMF3Data((AMF3Object)value); 162 } else if (isPrimitiveArray(value)) { 163 writePrimitiveArray(value); 164 } else if (value instanceof Number) { 165 // write number object 166 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_NUMBER); 167 dataOutputStream.writeDouble(((Number) value).doubleValue()); 168 } else if (value instanceof String) { 169 writeString((String)value); 170 } else if (value instanceof Character) { 171 // write String object 172 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_STRING); 173 dataOutputStream.writeUTF(value.toString()); 174 } else if (value instanceof Boolean) { 175 // write boolean object 176 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_BOOLEAN); 177 dataOutputStream.writeBoolean(((Boolean) value).booleanValue()); 178 } else if (value instanceof Date) { 179 // write Date object 180 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_DATE); 181 dataOutputStream.writeDouble(((Date) value).getTime()); 182 int offset = TimeZone.getDefault().getRawOffset(); 183 dataOutputStream.writeShort(offset / MILLS_PER_HOUR); 184 } else { 185 186 if (storedObjects.containsKey(value)) { 187 writeStoredObject(value); 188 return; 189 } 190 storeObject(value); 191 192 if (value instanceof Object[]) { 193 // write Object Array 194 writeArray((Object[]) value); 195 } else if (value instanceof Iterator<?>) { 196 write((Iterator<?>) value); 197 } else if (value instanceof Collection<?>) { 198 write((Collection<?>) value); 199 } else if (value instanceof Map<?, ?>) { 200 writeMap((Map<?, ?>) value); 201 } else if (value instanceof ResultSet) { 202 ASRecordSet asRecordSet = new ASRecordSet(); 203 asRecordSet.populate((ResultSet) value); 204 writeData(asRecordSet); 205 } else if (value instanceof Document) { 206 write((Document) value); 207 } else { 208 /* 209 MM's gateway requires all objects to be marked with the 210 Serializable interface in order to be serialized 211 That should still be followed if possible, but there is 212 no good reason to enforce it. 213 */ 214 writeObject(value); 215 } 216 } 217 } 218 219 /** 220 * Writes Object 221 * 222 * @param object 223 * @throws IOException 224 */ 225 protected void writeObject(Object object) throws IOException { 226 if (object == null) { 227 log.debug("Writing object, object param == null"); 228 throw new NullPointerException("object cannot be null"); 229 } 230 log.debug("Writing object, class = %s", object.getClass()); 231 232 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_OBJECT); 233 try { 234 PropertyDescriptor[] properties = Introspector.getPropertyDescriptors(object.getClass()); 235 if (properties == null) 236 properties = new PropertyDescriptor[0]; 237 238 for (int i = 0; i < properties.length; i++) { 239 if (!properties[i].getName().equals("class")) { 240 String propertyName = properties[i].getName(); 241 Method readMethod = properties[i].getReadMethod(); 242 Object propertyValue = null; 243 if (readMethod == null) { 244 log.error("unable to find readMethod for : %s writing null!", propertyName); 245 } else { 246 log.debug("invoking readMethod: %s", readMethod); 247 propertyValue = readMethod.invoke(object, new Object[0]); 248 } 249 log.debug("%s=%s", propertyName, propertyValue); 250 dataOutputStream.writeUTF(propertyName); 251 writeData(propertyValue); 252 } 253 } 254 dataOutputStream.writeShort(0); 255 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_OBJECT_END); 256 } catch (RuntimeException e) { 257 throw e; 258 } catch (Exception e) { 259 log.error("Write error", e); 260 throw new IOException(e.getMessage()); 261 } 262 } 263 264 /** 265 * Writes Array Object - call <code>writeData</code> foreach element 266 * 267 * @param array 268 * @throws IOException 269 */ 270 protected void writeArray(Object[] array) throws IOException { 271 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_ARRAY); 272 dataOutputStream.writeInt(array.length); 273 for (int i = 0; i < array.length; i++) { 274 writeData(array[i]); 275 } 276 } 277 278 protected void writePrimitiveArray(Object array) throws IOException { 279 writeArray(convertPrimitiveArrayToObjectArray(array)); 280 } 281 282 protected Object[] convertPrimitiveArrayToObjectArray(Object array) { 283 Class<?> componentType = array.getClass().getComponentType(); 284 285 Object[] result = null; 286 287 if (componentType == null) 288 { 289 throw new NullPointerException("componentType is null"); 290 } 291 else if (componentType == Character.TYPE) 292 { 293 char[] carray = (char[]) array; 294 result = new Object[carray.length]; 295 for (int i = 0; i < carray.length; i++) 296 { 297 result[i] = new Character(carray[i]); 298 } 299 } 300 else if (componentType == Byte.TYPE) 301 { 302 byte[] barray = (byte[]) array; 303 result = new Object[barray.length]; 304 for (int i = 0; i < barray.length; i++) 305 { 306 result[i] = new Byte(barray[i]); 307 } 308 } 309 else if (componentType == Short.TYPE) 310 { 311 short[] sarray = (short[]) array; 312 result = new Object[sarray.length]; 313 for (int i = 0; i < sarray.length; i++) 314 { 315 result[i] = new Short(sarray[i]); 316 } 317 } 318 else if (componentType == Integer.TYPE) 319 { 320 int[] iarray = (int[]) array; 321 result = new Object[iarray.length]; 322 for (int i = 0; i < iarray.length; i++) 323 { 324 result[i] = Integer.valueOf(iarray[i]); 325 } 326 } 327 else if (componentType == Long.TYPE) 328 { 329 long[] larray = (long[]) array; 330 result = new Object[larray.length]; 331 for (int i = 0; i < larray.length; i++) 332 { 333 result[i] = new Long(larray[i]); 334 } 335 } 336 else if (componentType == Double.TYPE) 337 { 338 double[] darray = (double[]) array; 339 result = new Object[darray.length]; 340 for (int i = 0; i < darray.length; i++) 341 { 342 result[i] = new Double(darray[i]); 343 } 344 } 345 else if (componentType == Float.TYPE) 346 { 347 float[] farray = (float[]) array; 348 result = new Object[farray.length]; 349 for (int i = 0; i < farray.length; i++) 350 { 351 result[i] = new Float(farray[i]); 352 } 353 } 354 else if (componentType == Boolean.TYPE) 355 { 356 boolean[] barray = (boolean[]) array; 357 result = new Object[barray.length]; 358 for (int i = 0; i < barray.length; i++) 359 { 360 result[i] = new Boolean(barray[i]); 361 } 362 } 363 else { 364 throw new IllegalArgumentException( 365 "unexpected component type: " 366 + componentType.getClass().getName()); 367 } 368 369 return result; 370 } 371 372 /** 373 * Writes Iterator - convert to List and call <code>writeCollection</code> 374 * 375 * @param iterator Iterator 376 * @throws IOException 377 */ 378 protected void write(Iterator<?> iterator) throws IOException { 379 List<Object> list = new ArrayList<Object>(); 380 while (iterator.hasNext()) { 381 list.add(iterator.next()); 382 } 383 write(list); 384 } 385 /** 386 * Writes collection 387 * 388 * @param collection Collection 389 * @throws IOException 390 */ 391 protected void write(Collection<?> collection) throws IOException { 392 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_ARRAY); 393 dataOutputStream.writeInt(collection.size()); 394 for (Iterator<?> objects = collection.iterator(); objects.hasNext();) { 395 Object object = objects.next(); 396 writeData(object); 397 } 398 } 399 /** 400 * Writes Object Map 401 * 402 * @param map 403 * @throws IOException 404 */ 405 protected void writeMap(Map<?, ?> map) throws IOException { 406 if (map instanceof ASObject && ((ASObject) map).getType() != null) { 407 log.debug("Writing Custom Class: %s", ((ASObject) map).getType()); 408 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_CUSTOM_CLASS); 409 dataOutputStream.writeUTF(((ASObject) map).getType()); 410 } else { 411 log.debug("Writing Map"); 412 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_MIXED_ARRAY); 413 dataOutputStream.writeInt(0); 414 } 415 for (Iterator<?> entrys = map.entrySet().iterator(); entrys.hasNext();) { 416 Map.Entry<?, ?> entry = (Map.Entry<?, ?>)entrys.next(); 417 log.debug("%s: %s", entry.getKey(), entry.getValue()); 418 dataOutputStream.writeUTF(entry.getKey().toString()); 419 writeData(entry.getValue()); 420 } 421 dataOutputStream.writeShort(0); 422 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_OBJECT_END); 423 } 424 425 /** 426 * Writes XML Document 427 * 428 * @param document 429 * @throws IOException 430 */ 431 protected void write(Document document) throws IOException { 432 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_XML); 433 Element docElement = document.getDocumentElement(); 434 String xmlData = convertDOMToString(docElement); 435 log.debug("Writing xmlData: \n%s", xmlData); 436 ByteArrayOutputStream baOutputStream = new ByteArrayOutputStream(); 437 baOutputStream.write(xmlData.getBytes("UTF-8")); 438 dataOutputStream.writeInt(baOutputStream.size()); 439 baOutputStream.writeTo(dataOutputStream); 440 } 441 442 /** 443 * Most of this code was cribbed from Java's DataOutputStream.writeUTF method 444 * which only supports Strings <= 65535 UTF-encoded characters. 445 */ 446 protected int writeString(String str) throws IOException { 447 int strlen = str.length(); 448 int utflen = 0; 449 char[] charr = new char[strlen]; 450 int c, count = 0; 451 452 str.getChars(0, strlen, charr, 0); 453 454 // check the length of the UTF-encoded string 455 for (int i = 0; i < strlen; i++) { 456 c = charr[i]; 457 if ((c >= 0x0001) && (c <= 0x007F)) { 458 utflen++; 459 } else if (c > 0x07FF) { 460 utflen += 3; 461 } else { 462 utflen += 2; 463 } 464 } 465 466 /** 467 * if utf-encoded String is < 64K, use the "String" data type, with a 468 * two-byte prefix specifying string length; otherwise use the "Long String" 469 * data type, withBUG#298 a four-byte prefix 470 */ 471 byte[] bytearr; 472 if (utflen <= 65535) { 473 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_STRING); 474 bytearr = new byte[utflen+2]; 475 } else { 476 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_LONG_STRING); 477 bytearr = new byte[utflen+4]; 478 bytearr[count++] = (byte) ((utflen >>> 24) & 0xFF); 479 bytearr[count++] = (byte) ((utflen >>> 16) & 0xFF); 480 } 481 482 bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF); 483 bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF); 484 for (int i = 0; i < strlen; i++) { 485 c = charr[i]; 486 if ((c >= 0x0001) && (c <= 0x007F)) { 487 bytearr[count++] = (byte) c; 488 } else if (c > 0x07FF) { 489 bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F)); 490 bytearr[count++] = (byte) (0x80 | ((c >> 6) & 0x3F)); 491 bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F)); 492 } else { 493 bytearr[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F)); 494 bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F)); 495 } 496 } 497 498 dataOutputStream.write(bytearr); 499 return utflen + 2; 500 } 501 502 private void writeStoredObject(Object obj) throws IOException { 503 log.debug("Writing object reference for %s", obj); 504 dataOutputStream.write(AMF0Body.DATA_TYPE_REFERENCE_OBJECT); 505 dataOutputStream.writeShort((storedObjects.get(obj)).intValue()); 506 } 507 508 private void storeObject(Object obj) { 509 storedObjects.put(obj, Integer.valueOf(storedObjectCount++)); 510 } 511 512 private void clearStoredObjects() { 513 storedObjects.clear(); 514 storedObjectCount = 0; 515 } 516 517 protected boolean isPrimitiveArray(Object obj) { 518 if (obj == null) 519 return false; 520 return obj.getClass().isArray() && obj.getClass().getComponentType().isPrimitive(); 521 } 522 523 private void writeAMF3Data(AMF3Object data) throws IOException { 524 dataOutputStream.writeByte(AMF0Body.DATA_TYPE_AMF3_OBJECT); 525 ObjectOutput amf3 = GraniteContext.getCurrentInstance().getGraniteConfig().newAMF3Serializer(rawOutputStream); 526 amf3.writeObject(data.getValue()); 527 } 528 529 public static String convertDOMToString(Node node) { 530 StringBuffer sb = new StringBuffer(); 531 if (node.getNodeType() == Node.TEXT_NODE) { 532 sb.append(node.getNodeValue()); 533 } else { 534 String currentTag = node.getNodeName(); 535 sb.append('<'); 536 sb.append(currentTag); 537 appendAttributes(node, sb); 538 sb.append('>'); 539 if (node.getNodeValue() != null) { 540 sb.append(node.getNodeValue()); 541 } 542 543 appendChildren(node, sb); 544 545 appendEndTag(sb, currentTag); 546 } 547 return sb.toString(); 548 } 549 550 private static void appendAttributes(Node node, StringBuffer sb) { 551 if (node instanceof Element) { 552 NamedNodeMap nodeMap = node.getAttributes(); 553 for (int i = 0; i < nodeMap.getLength(); i++) { 554 sb.append(' '); 555 sb.append(nodeMap.item(i).getNodeName()); 556 sb.append('='); 557 sb.append('"'); 558 sb.append(nodeMap.item(i).getNodeValue()); 559 sb.append('"'); 560 } 561 } 562 } 563 564 private static void appendChildren(Node node, StringBuffer sb) { 565 if (node.hasChildNodes()) { 566 NodeList children = node.getChildNodes(); 567 for (int i = 0; i < children.getLength(); i++) { 568 sb.append(convertDOMToString(children.item(i))); 569 } 570 } 571 } 572 573 private static void appendEndTag(StringBuffer sb, String currentTag) { 574 sb.append('<'); 575 sb.append('/'); 576 sb.append(currentTag); 577 sb.append('>'); 578 } 579}