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