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.ByteArrayOutputStream;
011 import java.io.DataOutputStream;
012 import java.io.IOException;
013 import java.io.ObjectOutput;
014 import java.io.OutputStream;
015 import java.lang.reflect.Method;
016 import java.sql.ResultSet;
017 import java.util.ArrayList;
018 import java.util.Collection;
019 import java.util.Date;
020 import java.util.IdentityHashMap;
021 import java.util.Iterator;
022 import java.util.List;
023 import java.util.Map;
024 import java.util.TimeZone;
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.AMF0Header;
030 import org.granite.messaging.amf.AMF0Message;
031 import org.granite.messaging.amf.AMF3Object;
032 import org.granite.util.Introspector;
033 import org.granite.util.PropertyDescriptor;
034 import org.w3c.dom.Document;
035 import org.w3c.dom.Element;
036 import org.w3c.dom.NamedNodeMap;
037 import org.w3c.dom.Node;
038 import org.w3c.dom.NodeList;
039
040 import flex.messaging.io.ASObject;
041 import 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 */
053 public 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 }