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