001    /**
002     *   GRANITE DATA SERVICES
003     *   Copyright (C) 2006-2013 GRANITE DATA SERVICES S.A.S.
004     *
005     *   This file is part of the Granite Data Services Platform.
006     *
007     *   Granite Data Services is free software; you can redistribute it and/or
008     *   modify it under the terms of the GNU Lesser General Public
009     *   License as published by the Free Software Foundation; either
010     *   version 2.1 of the License, or (at your option) any later version.
011     *
012     *   Granite Data Services is distributed in the hope that it will be useful,
013     *   but WITHOUT ANY WARRANTY; without even the implied warranty of
014     *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
015     *   General Public License for more details.
016     *
017     *   You should have received a copy of the GNU Lesser General Public
018     *   License along with this library; if not, write to the Free Software
019     *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
020     *   USA, or see <http://www.gnu.org/licenses/>.
021     */
022    /*
023     * www.openamf.org
024     *
025     * Distributable under LGPL license.
026     * See terms of license at gnu.org.
027     */
028    
029    package org.granite.messaging.amf.io;
030    
031    import java.io.ByteArrayOutputStream;
032    import java.io.DataOutputStream;
033    import java.io.IOException;
034    import java.io.ObjectOutput;
035    import java.io.OutputStream;
036    import java.lang.reflect.Method;
037    import java.sql.ResultSet;
038    import java.util.ArrayList;
039    import java.util.Collection;
040    import java.util.Date;
041    import java.util.IdentityHashMap;
042    import java.util.Iterator;
043    import java.util.List;
044    import java.util.Map;
045    import java.util.TimeZone;
046    
047    import org.granite.context.GraniteContext;
048    import org.granite.logging.Logger;
049    import org.granite.messaging.amf.AMF0Body;
050    import org.granite.messaging.amf.AMF0Header;
051    import org.granite.messaging.amf.AMF0Message;
052    import org.granite.messaging.amf.AMF3Object;
053    import org.granite.util.Introspector;
054    import org.granite.util.PropertyDescriptor;
055    import org.w3c.dom.Document;
056    import org.w3c.dom.Element;
057    import org.w3c.dom.NamedNodeMap;
058    import org.w3c.dom.Node;
059    import org.w3c.dom.NodeList;
060    
061    import flex.messaging.io.ASObject;
062    import flex.messaging.io.ASRecordSet;
063    
064    /**
065     * AMF Serializer
066     *
067     * @author Jason Calabrese <jasonc@missionvi.com>
068     * @author Pat Maddox <pergesu@users.sourceforge.net>
069     * @author Sylwester Lachiewicz <lachiewicz@plusnet.pl>
070     * @author Richard Pitt
071     *
072     * @version $Revision: 1.54 $, $Date: 2006/03/25 23:41:41 $
073     */
074    public class AMF0Serializer {
075    
076        private static final Logger log = Logger.getLogger(AMF0Serializer.class);
077    
078        private static final int MILLS_PER_HOUR = 60000;
079    
080        /**
081         * Null message
082         */
083        private static final String NULL_MESSAGE = "null";
084    
085        /**
086         * The output stream
087         */
088        private final DataOutputStream dataOutputStream;
089        private final OutputStream rawOutputStream;
090    
091        private final Map<Object, Integer> storedObjects = new IdentityHashMap<Object, Integer>();
092        private int storedObjectCount = 0;
093    
094        /**
095         * Constructor
096         *
097         * @param outputStream
098         */
099        public AMF0Serializer(OutputStream outputStream) {
100            this.rawOutputStream = outputStream;
101            this.dataOutputStream = outputStream instanceof DataOutputStream
102                    ? ((DataOutputStream)outputStream)
103                    : new DataOutputStream(outputStream);
104        }
105    
106        /**
107         * Writes message
108         *
109         * @param message
110         * @throws IOException
111         */
112        public void serializeMessage(AMF0Message message) throws IOException {
113            //if (log.isInfoEnabled())
114            //    log.info("Serializing Message, for more info turn on debug level");
115    
116            clearStoredObjects();
117            dataOutputStream.writeShort(message.getVersion());
118            // write header
119            dataOutputStream.writeShort(message.getHeaderCount());
120            Iterator<AMF0Header> headers = message.getHeaders().iterator();
121            while (headers.hasNext()) {
122                AMF0Header header = headers.next();
123                writeHeader(header);
124            }
125            // write body
126            dataOutputStream.writeShort(message.getBodyCount());
127            Iterator<AMF0Body> bodies = message.getBodies();
128            while (bodies.hasNext()) {
129                AMF0Body body = bodies.next();
130                writeBody(body);
131            }
132        }
133        /**
134         * Writes message header
135         *
136         * @param header AMF message header
137         * @throws IOException
138         */
139        protected void writeHeader(AMF0Header header) throws IOException {
140            dataOutputStream.writeUTF(header.getKey());
141            dataOutputStream.writeBoolean(header.isRequired());
142            // Always, always there is four bytes of FF, which is -1 of course
143            dataOutputStream.writeInt(-1);
144            writeData(header.getValue());
145        }
146        /**
147         * Writes message body
148         *
149         * @param body AMF message body
150         * @throws IOException
151         */
152        protected void writeBody(AMF0Body body) throws IOException {
153            // write url
154            if (body.getTarget() == null) {
155                dataOutputStream.writeUTF(NULL_MESSAGE);
156            } else {
157                dataOutputStream.writeUTF(body.getTarget());
158            }
159            // write response
160            if (body.getResponse() == null) {
161                dataOutputStream.writeUTF(NULL_MESSAGE);
162            } else {
163                dataOutputStream.writeUTF(body.getResponse());
164            }
165            // Always, always there is four bytes of FF, which is -1 of course
166            dataOutputStream.writeInt(-1);
167            // Write the data to the output stream
168            writeData(body.getValue());
169        }
170    
171        /**
172         * Writes Data
173         *
174         * @param value
175         * @throws IOException
176         */
177        protected void writeData(Object value) throws IOException {
178            if (value == null) {
179                // write null object
180                dataOutputStream.writeByte(AMF0Body.DATA_TYPE_NULL);
181            } else if (value instanceof AMF3Object) {
182                writeAMF3Data((AMF3Object)value);
183            } else if (isPrimitiveArray(value)) {
184                writePrimitiveArray(value);
185            } else if (value instanceof Number) {
186                // write number object
187                dataOutputStream.writeByte(AMF0Body.DATA_TYPE_NUMBER);
188                dataOutputStream.writeDouble(((Number) value).doubleValue());
189            } else if (value instanceof String) {
190               writeString((String)value);
191            } else if (value instanceof Character) {
192                // write String object
193                dataOutputStream.writeByte(AMF0Body.DATA_TYPE_STRING);
194                dataOutputStream.writeUTF(value.toString());
195            } else if (value instanceof Boolean) {
196                // write boolean object
197                dataOutputStream.writeByte(AMF0Body.DATA_TYPE_BOOLEAN);
198                dataOutputStream.writeBoolean(((Boolean) value).booleanValue());
199            } else if (value instanceof Date) {
200                // write Date object
201                dataOutputStream.writeByte(AMF0Body.DATA_TYPE_DATE);
202                dataOutputStream.writeDouble(((Date) value).getTime());
203                int offset = TimeZone.getDefault().getRawOffset();
204                dataOutputStream.writeShort(offset / MILLS_PER_HOUR);
205            } else {
206    
207                if (storedObjects.containsKey(value)) {
208                    writeStoredObject(value);
209                    return;
210                }
211                storeObject(value);
212    
213                if (value instanceof Object[]) {
214                    // write Object Array
215                    writeArray((Object[]) value);
216                } else if (value instanceof Iterator<?>) {
217                    write((Iterator<?>) value);
218                } else if (value instanceof Collection<?>) {
219                    write((Collection<?>) value);
220                } else if (value instanceof Map<?, ?>) {
221                    writeMap((Map<?, ?>) value);
222                } else if (value instanceof ResultSet) {
223                    ASRecordSet asRecordSet = new ASRecordSet();
224                    asRecordSet.populate((ResultSet) value);
225                    writeData(asRecordSet);
226                } else if (value instanceof Document) {
227                    write((Document) value);
228                } else {
229                    /*
230                    MM's gateway requires all objects to be marked with the
231                    Serializable interface in order to be serialized
232                    That should still be followed if possible, but there is
233                    no good reason to enforce it.
234                    */
235                    writeObject(value);
236                }
237            }
238        }
239    
240        /**
241         * Writes Object
242         *
243         * @param object
244         * @throws IOException
245         */
246        protected void writeObject(Object object) throws IOException {
247            if (object == null) {
248                log.debug("Writing object, object param == null");
249                throw new NullPointerException("object cannot be null");
250            }
251            log.debug("Writing object, class = %s", object.getClass());
252    
253            dataOutputStream.writeByte(AMF0Body.DATA_TYPE_OBJECT);
254            try {
255                PropertyDescriptor[] properties = Introspector.getPropertyDescriptors(object.getClass());
256                if (properties == null)
257                    properties = new PropertyDescriptor[0];
258    
259                for (int i = 0; i < properties.length; i++) {
260                    if (!properties[i].getName().equals("class")) {
261                        String propertyName = properties[i].getName();
262                        Method readMethod = properties[i].getReadMethod();
263                        Object propertyValue = null;
264                        if (readMethod == null) {
265                            log.error("unable to find readMethod for : %s writing null!", propertyName);
266                        } else {
267                            log.debug("invoking readMethod: %s", readMethod);
268                            propertyValue = readMethod.invoke(object, new Object[0]);
269                        }
270                        log.debug("%s=%s", propertyName, propertyValue);
271                        dataOutputStream.writeUTF(propertyName);
272                        writeData(propertyValue);
273                    }
274                }
275                dataOutputStream.writeShort(0);
276                dataOutputStream.writeByte(AMF0Body.DATA_TYPE_OBJECT_END);
277            } catch (RuntimeException e) {
278                throw e;
279            } catch (Exception e) {
280                log.error("Write error", e);
281                throw new IOException(e.getMessage());
282            }
283        }
284    
285        /**
286         * Writes Array Object - call <code>writeData</code> foreach element
287         *
288         * @param array
289         * @throws IOException
290         */
291        protected void writeArray(Object[] array) throws IOException {
292            dataOutputStream.writeByte(AMF0Body.DATA_TYPE_ARRAY);
293            dataOutputStream.writeInt(array.length);
294            for (int i = 0; i < array.length; i++) {
295                writeData(array[i]);
296            }
297        }
298    
299        protected void writePrimitiveArray(Object array) throws IOException {
300            writeArray(convertPrimitiveArrayToObjectArray(array));
301        }
302    
303        protected Object[] convertPrimitiveArrayToObjectArray(Object array) {
304            Class<?> componentType = array.getClass().getComponentType();
305    
306            Object[] result = null;
307    
308            if (componentType == null)
309            {
310                throw new NullPointerException("componentType is null");
311            }
312            else if (componentType == Character.TYPE)
313            {
314                char[] carray = (char[]) array;
315                result = new Object[carray.length];
316                for (int i = 0; i < carray.length; i++)
317                {
318                    result[i] = new Character(carray[i]);
319                }
320            }
321            else if (componentType == Byte.TYPE)
322            {
323                byte[] barray = (byte[]) array;
324                result = new Object[barray.length];
325                for (int i = 0; i < barray.length; i++)
326                {
327                    result[i] = new Byte(barray[i]);
328                }
329            }
330            else if (componentType == Short.TYPE)
331            {
332                short[] sarray = (short[]) array;
333                result = new Object[sarray.length];
334                for (int i = 0; i < sarray.length; i++)
335                {
336                    result[i] = new Short(sarray[i]);
337                }
338            }
339            else if (componentType == Integer.TYPE)
340            {
341                int[] iarray = (int[]) array;
342                result = new Object[iarray.length];
343                for (int i = 0; i < iarray.length; i++)
344                {
345                    result[i] = Integer.valueOf(iarray[i]);
346                }
347            }
348            else if (componentType == Long.TYPE)
349            {
350                long[] larray = (long[]) array;
351                result = new Object[larray.length];
352                for (int i = 0; i < larray.length; i++)
353                {
354                    result[i] = new Long(larray[i]);
355                }
356            }
357            else if (componentType == Double.TYPE)
358            {
359                double[] darray = (double[]) array;
360                result = new Object[darray.length];
361                for (int i = 0; i < darray.length; i++)
362                {
363                    result[i] = new Double(darray[i]);
364                }
365            }
366            else if (componentType == Float.TYPE)
367            {
368                float[] farray = (float[]) array;
369                result = new Object[farray.length];
370                for (int i = 0; i < farray.length; i++)
371                {
372                    result[i] = new Float(farray[i]);
373                }
374            }
375            else if (componentType == Boolean.TYPE)
376            {
377                boolean[] barray = (boolean[]) array;
378                result = new Object[barray.length];
379                for (int i = 0; i < barray.length; i++)
380                {
381                    result[i] = new Boolean(barray[i]);
382                }
383            }
384            else {
385                throw new IllegalArgumentException(
386                        "unexpected component type: "
387                        + componentType.getClass().getName());
388            }
389    
390            return result;
391        }
392    
393        /**
394         * Writes Iterator - convert to List and call <code>writeCollection</code>
395         *
396         * @param iterator Iterator
397         * @throws IOException
398         */
399        protected void write(Iterator<?> iterator) throws IOException {
400            List<Object> list = new ArrayList<Object>();
401            while (iterator.hasNext()) {
402                list.add(iterator.next());
403            }
404            write(list);
405        }
406        /**
407         * Writes collection
408         *
409         * @param collection Collection
410         * @throws IOException
411         */
412        protected void write(Collection<?> collection) throws IOException {
413            dataOutputStream.writeByte(AMF0Body.DATA_TYPE_ARRAY);
414            dataOutputStream.writeInt(collection.size());
415            for (Iterator<?> objects = collection.iterator(); objects.hasNext();) {
416                Object object = objects.next();
417                writeData(object);
418            }
419        }
420        /**
421         * Writes Object Map
422         *
423         * @param map
424         * @throws IOException
425         */
426        protected void writeMap(Map<?, ?> map) throws IOException {
427            if (map instanceof ASObject && ((ASObject) map).getType() != null) {
428                log.debug("Writing Custom Class: %s", ((ASObject) map).getType());
429                dataOutputStream.writeByte(AMF0Body.DATA_TYPE_CUSTOM_CLASS);
430                dataOutputStream.writeUTF(((ASObject) map).getType());
431            } else {
432                log.debug("Writing Map");
433                dataOutputStream.writeByte(AMF0Body.DATA_TYPE_MIXED_ARRAY);
434                dataOutputStream.writeInt(0);
435            }
436            for (Iterator<?> entrys = map.entrySet().iterator(); entrys.hasNext();) {
437                Map.Entry<?, ?> entry = (Map.Entry<?, ?>)entrys.next();
438                log.debug("%s: %s", entry.getKey(), entry.getValue());
439                dataOutputStream.writeUTF(entry.getKey().toString());
440                writeData(entry.getValue());
441            }
442            dataOutputStream.writeShort(0);
443            dataOutputStream.writeByte(AMF0Body.DATA_TYPE_OBJECT_END);
444        }
445    
446        /**
447         * Writes XML Document
448         *
449         * @param document
450         * @throws IOException
451         */
452        protected void write(Document document) throws IOException {
453            dataOutputStream.writeByte(AMF0Body.DATA_TYPE_XML);
454            Element docElement = document.getDocumentElement();
455            String xmlData = convertDOMToString(docElement);
456            log.debug("Writing xmlData: \n%s", xmlData);
457            ByteArrayOutputStream baOutputStream = new ByteArrayOutputStream();
458            baOutputStream.write(xmlData.getBytes("UTF-8"));
459            dataOutputStream.writeInt(baOutputStream.size());
460            baOutputStream.writeTo(dataOutputStream);
461        }
462    
463        /**
464         * Most of this code was cribbed from Java's DataOutputStream.writeUTF method
465         * which only supports Strings <= 65535 UTF-encoded characters.
466         */
467        protected int writeString(String str) throws IOException {
468                int strlen = str.length();
469                int utflen = 0;
470                char[] charr = new char[strlen];
471                int c, count = 0;
472            
473                str.getChars(0, strlen, charr, 0);
474            
475                // check the length of the UTF-encoded string
476                for (int i = 0; i < strlen; i++) {
477                    c = charr[i];
478                    if ((c >= 0x0001) && (c <= 0x007F)) {
479                            utflen++;
480                    } else if (c > 0x07FF) {
481                            utflen += 3;
482                    } else {
483                            utflen += 2;
484                    }
485                }
486            
487                /**
488                 * if utf-encoded String is < 64K, use the "String" data type, with a
489                 * two-byte prefix specifying string length; otherwise use the "Long String"
490                 * data type, withBUG#298 a four-byte prefix
491                 */
492                byte[] bytearr;
493                if (utflen <= 65535) {
494                    dataOutputStream.writeByte(AMF0Body.DATA_TYPE_STRING);
495                    bytearr = new byte[utflen+2];
496                } else {
497                    dataOutputStream.writeByte(AMF0Body.DATA_TYPE_LONG_STRING);
498                    bytearr = new byte[utflen+4];
499                    bytearr[count++] = (byte) ((utflen >>> 24) & 0xFF);
500                    bytearr[count++] = (byte) ((utflen >>> 16) & 0xFF);
501                }
502            
503                bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
504                bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);
505                for (int i = 0; i < strlen; i++) {
506                    c = charr[i];
507                    if ((c >= 0x0001) && (c <= 0x007F)) {
508                            bytearr[count++] = (byte) c;
509                    } else if (c > 0x07FF) {
510                            bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
511                            bytearr[count++] = (byte) (0x80 | ((c >>  6) & 0x3F));
512                            bytearr[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
513                    } else {
514                            bytearr[count++] = (byte) (0xC0 | ((c >>  6) & 0x1F));
515                            bytearr[count++] = (byte) (0x80 | ((c >>  0) & 0x3F));
516                    }
517                }
518            
519                dataOutputStream.write(bytearr);
520                return utflen + 2;
521        }
522    
523        private void writeStoredObject(Object obj) throws IOException {
524            log.debug("Writing object reference for %s", obj);
525            dataOutputStream.write(AMF0Body.DATA_TYPE_REFERENCE_OBJECT);
526            dataOutputStream.writeShort((storedObjects.get(obj)).intValue());
527        }
528    
529        private void storeObject(Object obj) {
530            storedObjects.put(obj, Integer.valueOf(storedObjectCount++));
531        }
532    
533        private void clearStoredObjects() {
534            storedObjects.clear();
535            storedObjectCount = 0;
536        }
537    
538        protected boolean isPrimitiveArray(Object obj) {
539            if (obj == null)
540                return false;
541            return obj.getClass().isArray() && obj.getClass().getComponentType().isPrimitive();
542        }
543    
544        private void writeAMF3Data(AMF3Object data) throws IOException {
545            dataOutputStream.writeByte(AMF0Body.DATA_TYPE_AMF3_OBJECT);
546            ObjectOutput amf3 = GraniteContext.getCurrentInstance().getGraniteConfig().newAMF3Serializer(rawOutputStream);
547            amf3.writeObject(data.getValue());
548        }
549    
550        public static String convertDOMToString(Node node) {
551            StringBuffer sb = new StringBuffer();
552            if (node.getNodeType() == Node.TEXT_NODE) {
553                sb.append(node.getNodeValue());
554            } else {
555                String currentTag = node.getNodeName();
556                sb.append('<');
557                sb.append(currentTag);
558                appendAttributes(node, sb);
559                sb.append('>');
560                if (node.getNodeValue() != null) {
561                    sb.append(node.getNodeValue());
562                }
563    
564                appendChildren(node, sb);
565    
566                appendEndTag(sb, currentTag);
567            }
568            return sb.toString();
569        }
570    
571        private static void appendAttributes(Node node, StringBuffer sb) {
572            if (node instanceof Element) {
573                NamedNodeMap nodeMap = node.getAttributes();
574                for (int i = 0; i < nodeMap.getLength(); i++) {
575                    sb.append(' ');
576                    sb.append(nodeMap.item(i).getNodeName());
577                    sb.append('=');
578                    sb.append('"');
579                    sb.append(nodeMap.item(i).getNodeValue());
580                    sb.append('"');
581                }
582            }
583        }
584    
585        private static void appendChildren(Node node, StringBuffer sb) {
586            if (node.hasChildNodes()) {
587                NodeList children = node.getChildNodes();
588                for (int i = 0; i < children.getLength(); i++) {
589                    sb.append(convertDOMToString(children.item(i)));
590                }
591            }
592        }
593    
594        private static void appendEndTag(StringBuffer sb, String currentTag) {
595            sb.append('<');
596            sb.append('/');
597            sb.append(currentTag);
598            sb.append('>');
599        }
600    }