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