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
029 package org.granite.messaging.amf.io;
030
031 import java.io.ByteArrayOutputStream;
032 import java.io.DataOutputStream;
033 import java.io.IOException;
034 import java.io.ObjectOutput;
035 import java.io.OutputStream;
036 import java.lang.reflect.Method;
037 import java.sql.ResultSet;
038 import java.util.ArrayList;
039 import java.util.Collection;
040 import java.util.Date;
041 import java.util.IdentityHashMap;
042 import java.util.Iterator;
043 import java.util.List;
044 import java.util.Map;
045 import java.util.TimeZone;
046
047 import org.granite.context.GraniteContext;
048 import org.granite.logging.Logger;
049 import org.granite.messaging.amf.AMF0Body;
050 import org.granite.messaging.amf.AMF0Header;
051 import org.granite.messaging.amf.AMF0Message;
052 import org.granite.messaging.amf.AMF3Object;
053 import org.granite.util.Introspector;
054 import org.granite.util.PropertyDescriptor;
055 import org.w3c.dom.Document;
056 import org.w3c.dom.Element;
057 import org.w3c.dom.NamedNodeMap;
058 import org.w3c.dom.Node;
059 import org.w3c.dom.NodeList;
060
061 import flex.messaging.io.ASObject;
062 import 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 */
074 public 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 }