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    }