001/*
002 * www.openamf.org
003 *
004 * Distributable under LGPL license.
005 * See terms of license at gnu.org.
006 */
007
008package org.granite.messaging.amf.io;
009
010import java.io.ByteArrayOutputStream;
011import java.io.DataOutputStream;
012import java.io.IOException;
013import java.io.ObjectOutput;
014import java.io.OutputStream;
015import java.lang.reflect.Method;
016import java.sql.ResultSet;
017import java.util.ArrayList;
018import java.util.Collection;
019import java.util.Date;
020import java.util.IdentityHashMap;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Map;
024import java.util.TimeZone;
025
026import org.granite.context.GraniteContext;
027import org.granite.logging.Logger;
028import org.granite.messaging.amf.AMF0Body;
029import org.granite.messaging.amf.AMF0Header;
030import org.granite.messaging.amf.AMF0Message;
031import org.granite.messaging.amf.AMF3Object;
032import org.granite.util.Introspector;
033import org.granite.util.PropertyDescriptor;
034import org.w3c.dom.Document;
035import org.w3c.dom.Element;
036import org.w3c.dom.NamedNodeMap;
037import org.w3c.dom.Node;
038import org.w3c.dom.NodeList;
039
040import flex.messaging.io.ASObject;
041import 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 */
053public 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}