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    }