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.ByteArrayInputStream;
032    import java.io.DataInputStream;
033    import java.io.IOException;
034    import java.io.InputStream;
035    import java.io.ObjectInput;
036    import java.io.UTFDataFormatException;
037    import java.util.ArrayList;
038    import java.util.Calendar;
039    import java.util.Date;
040    import java.util.GregorianCalendar;
041    import java.util.List;
042    import java.util.TimeZone;
043    
044    import javax.xml.parsers.DocumentBuilder;
045    import javax.xml.parsers.DocumentBuilderFactory;
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.AMF0Message;
051    import org.w3c.dom.Document;
052    
053    import flex.messaging.io.ASObject;
054    
055    /**
056     * AMF Deserializer
057     *
058     * @author Jason Calabrese <jasonc@missionvi.com>
059     * @author Pat Maddox <pergesu@users.sourceforge.net>
060     * @author Sylwester Lachiewicz <lachiewicz@plusnet.pl>
061     * @version $Revision: 1.38 $, $Date: 2004/12/09 04:50:07 $
062     */
063    public class AMF0Deserializer {
064    
065        private static final Logger log = Logger.getLogger(AMF0Deserializer.class);
066    
067        private List<Object> storedObjects = null;
068    
069        /**
070         * The AMF input stream
071         */
072        private final InputStream rawInputStream;
073        private final DataInputStream dataInputStream;
074    
075        /**
076         * Object to store the deserialized data
077         */
078        private final AMF0Message message = new AMF0Message();
079    
080    
081        /**
082         * Deserialize message
083         *
084         * @param inputStream message input stream
085         * @throws IOException
086         */
087        public AMF0Deserializer(InputStream inputStream) throws IOException {
088            //log.info("Deserializing Message, for more info turn on debug level");
089    
090            // Save the input stream for this object
091            this.rawInputStream = inputStream;
092            this.dataInputStream = inputStream instanceof DataInputStream
093                    ? ((DataInputStream)inputStream)
094                    : new DataInputStream(inputStream);
095    
096            // Read the binary header
097            readHeaders();
098            log.debug("readHeader");
099            // Read the binary body
100            readBodies();
101            log.debug("readBody");
102        }
103    
104        public AMF0Message getAMFMessage() {
105            return message;
106        }
107    
108        /**
109         * Read message header
110         *
111         * @throws IOException
112         */
113        protected void readHeaders() throws IOException {
114            // version
115            message.setVersion(dataInputStream.readUnsignedShort());
116            // Find total number of header elements
117            int headerCount = dataInputStream.readUnsignedShort();
118            log.debug("headerCount = %d", headerCount);
119    
120            // Loop over all the header elements
121            for (int i = 0; i < headerCount; i++) {
122                // clear storedObjects - references are new for every header
123                storedObjects = new ArrayList<Object>();
124                String key = dataInputStream.readUTF();
125                // Find the must understand flag
126                boolean required = dataInputStream.readBoolean();
127                // Grab the length of the header element
128                /*long length =*/ dataInputStream.readInt();
129                // Grab the type of the element
130                byte type = dataInputStream.readByte();
131                // Turn the element into real data
132                Object value = readData(type);
133                // Save the name/value into the headers array
134                message.addHeader(key, required, value);
135            }
136        }
137    
138        /**
139         * Read message body
140         *
141         * @throws IOException
142         */
143        protected void readBodies() throws IOException {
144            // Find the total number of body elements
145            int bodyCount = dataInputStream.readUnsignedShort();
146            log.debug("bodyCount = %d", bodyCount);
147    
148            // Loop over all the body elements
149            for (int i = 0; i < bodyCount; i++) {
150                //clear storedObjects
151                storedObjects = new ArrayList<Object>();
152                // The target method
153                String method = dataInputStream.readUTF();
154                // The target that the client understands
155                String target = dataInputStream.readUTF();
156                // Get the length of the body element
157                /*long length =*/ dataInputStream.readInt();
158                // Grab the type of the element
159                byte type = dataInputStream.readByte();
160                log.debug("type = 0x%02X", type);
161                // Turn the argument elements into real data
162                Object data = readData(type);
163                // Add the body element to the body object
164                message.addBody(method, target, data, type);
165            }
166        }
167    
168        /**
169         * Reads custom class
170         *
171         * @return the read Object
172         * @throws IOException
173         */
174        protected Object readCustomClass() throws IOException {
175            // Grab the explicit type - somehow it works
176            String type = dataInputStream.readUTF();
177            log.debug("Reading Custom Class: %s", type);
178            /*
179            String mappedJavaClass = OpenAMFConfig.getInstance().getJavaClassName(type);
180            if (mappedJavaClass != null) {
181                type = mappedJavaClass;
182            }
183            */
184            ASObject aso = new ASObject(type);
185            // The rest of the bytes are an object without the 0x03 header
186            return readObject(aso);
187        }
188    
189        protected ASObject readObject() throws IOException {
190            ASObject aso = new ASObject();
191            return readObject(aso);
192        }
193    
194        /**
195         * Reads an object and converts the binary data into an List
196         *
197         * @param aso
198         * @return the read object
199         * @throws IOException
200         */
201        protected ASObject readObject(ASObject aso) throws IOException {
202            storeObject(aso);
203            // Init the array
204            log.debug("reading object");
205            // Grab the key
206            String key = dataInputStream.readUTF();
207            for (byte type = dataInputStream.readByte();
208                 type != 9;
209                 type = dataInputStream.readByte()) {
210                // Grab the value
211                Object value = readData(type);
212                // Save the name/value pair in the map
213                if (value == null) {
214                    log.info("Skipping NULL value for : %s", key);
215                } else {
216                    aso.put(key, value);
217                    log.debug(" adding {key=%s, value=%s, type=%d}", key, value, type);
218                }
219                // Get the next name
220                key = dataInputStream.readUTF();
221            }
222            log.debug("finished reading object");
223            // Return the map
224            return aso;
225        }
226    
227        /**
228         * Reads array
229         *
230         * @return the read array (as a list).
231         * @throws IOException
232         */
233        protected List<?> readArray() throws IOException {
234            // Init the array
235            List<Object> array = new ArrayList<Object>();
236            storeObject(array);
237            log.debug("Reading array");
238            // Grab the length of the array
239            long length = dataInputStream.readInt();
240            log.debug("array length = %d", length);
241            // Loop over all the elements in the data
242            for (long i = 0; i < length; i++) {
243                // Grab the type for each element
244                byte type = dataInputStream.readByte();
245                // Grab the element
246                Object data = readData(type);
247                array.add(data);
248            }
249            // Return the data
250            return array;
251        }
252    
253        /**
254         * Store object in  internal array
255         *
256         * @param o the object to store
257         */
258        private void storeObject(Object o) {
259            storedObjects.add(o);
260            log.debug("storedObjects.size: %d", storedObjects.size());
261        }
262    
263        /**
264         * Reads date
265         *
266         * @return the read date
267         * @throws IOException
268         */
269        protected Date readDate() throws IOException {
270            long ms = (long) dataInputStream.readDouble(); // Date in millis from 01/01/1970
271    
272          // here we have to read in the raw
273          // timezone offset (which comes in minutes, but incorrectly signed),
274          // make it millis, and fix the sign.
275          int timeoffset = dataInputStream.readShort() * 60000 * -1; // now we have millis
276    
277          TimeZone serverTimeZone = TimeZone.getDefault();
278    
279          // now we subtract the current timezone offset and add the one that was passed
280          // in (which is of the Flash client), which gives us the appropriate ms (i think)
281          // -alon
282          Calendar sent = new GregorianCalendar();
283          sent.setTime( (new Date(ms - serverTimeZone.getRawOffset() + timeoffset)));
284    
285          TimeZone sentTimeZone = sent.getTimeZone();
286    
287          // we have to handle daylight savings ms as well
288          if (sentTimeZone.inDaylightTime(sent.getTime()))
289          {
290              //
291              // Implementation note: we are trying to maintain compatibility
292              // with J2SE 1.3.1
293              //
294              // As such, we can't use java.util.Calendar.getDSTSavings() here
295              //
296            sent.setTime(new java.util.Date(sent.getTime().getTime() - 3600000));
297          }
298    
299          return sent.getTime();
300        }
301    
302        /**
303         * Reads flushed stored object
304         *
305         * @return the stored object
306         * @throws IOException
307         */
308        protected Object readFlushedSO() throws IOException {
309            int index = dataInputStream.readUnsignedShort();
310            log.debug("Object Index: %d", index);
311            return storedObjects.get(index);
312        }
313    
314        /**
315         * Reads object
316         *
317         * @return always null...
318         */
319        protected Object readASObject() {
320            return null;
321        }
322    
323        /**
324         * Reads object
325         *
326         * @return the AMF3 decoded object
327         */
328        protected Object readAMF3Data() throws IOException {
329            ObjectInput amf3 = GraniteContext.getCurrentInstance().getGraniteConfig().newAMF3Deserializer(rawInputStream);
330            try {
331                return amf3.readObject();
332            } catch (ClassNotFoundException e) {
333                throw new RuntimeException(e);
334            }
335        }
336    
337        /**
338         * Reads object from inputstream with selected type
339         *
340         * @param type
341         * @return the read object
342         * @throws IOException
343         */
344        protected Object readData(byte type) throws IOException {
345            log.debug("Reading data of type: %s", AMF0Body.getObjectTypeDescription(type));
346            switch (type) {
347                case AMF0Body.DATA_TYPE_NUMBER: // 0
348                    return new Double(dataInputStream.readDouble());
349                case AMF0Body.DATA_TYPE_BOOLEAN: // 1
350                    return new Boolean(dataInputStream.readBoolean());
351                case AMF0Body.DATA_TYPE_STRING: // 2
352                    return dataInputStream.readUTF();
353                case AMF0Body.DATA_TYPE_OBJECT: // 3
354                    return readObject();
355                case AMF0Body.DATA_TYPE_MOVIE_CLIP: // 4
356                    throw new IOException("Unknown/unsupported object type " + AMF0Body.getObjectTypeDescription(type));
357                case AMF0Body.DATA_TYPE_NULL: // 5
358                case AMF0Body.DATA_TYPE_UNDEFINED: //6
359                    return null;
360                case AMF0Body.DATA_TYPE_REFERENCE_OBJECT: // 7
361                    return readFlushedSO();
362                case AMF0Body.DATA_TYPE_MIXED_ARRAY: // 8
363                    /*long length =*/ dataInputStream.readInt();
364                    //don't do anything with the length
365                    return readObject();
366                case AMF0Body.DATA_TYPE_OBJECT_END: // 9
367                    return null;
368                case AMF0Body.DATA_TYPE_ARRAY: // 10
369                    return readArray();
370                case AMF0Body.DATA_TYPE_DATE: // 11
371                    return readDate();
372                case AMF0Body.DATA_TYPE_LONG_STRING: // 12
373                    return readLongUTF(dataInputStream);
374                case AMF0Body.DATA_TYPE_AS_OBJECT: // 13
375                    return readASObject();
376                case AMF0Body.DATA_TYPE_RECORDSET: // 14
377                    return null;
378                case AMF0Body.DATA_TYPE_XML: // 15
379                    return convertToDOM(dataInputStream);
380                case AMF0Body.DATA_TYPE_CUSTOM_CLASS: // 16
381                    return readCustomClass();
382                case AMF0Body.DATA_TYPE_AMF3_OBJECT: // 17
383                    return readAMF3Data();
384                default :
385                    throw new IOException("Unknown/unsupported object type " + AMF0Body.getObjectTypeDescription(type));
386            }
387        }
388    
389        /**
390         * This is a hacked verison of Java's DataInputStream.readUTF(), which only
391         * supports Strings <= 65535 UTF-8-encoded characters
392         */
393        private Object readLongUTF(DataInputStream in) throws IOException {
394            int utflen = in.readInt();
395            StringBuffer str = new StringBuffer(utflen);
396            byte bytearr [] = new byte[utflen];
397            int c, char2, char3;
398            int count = 0;
399    
400            in.readFully(bytearr, 0, utflen);
401    
402            while (count < utflen) {
403                c = bytearr[count] & 0xff;
404                switch (c >> 4) {
405                    case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
406                        /* 0xxxxxxx*/
407                        count++;
408                        str.append((char)c);
409                        break;
410                    case 12: case 13:
411                        /* 110x xxxx   10xx xxxx*/
412                        count += 2;
413                        if (count > utflen)
414                            throw new UTFDataFormatException();
415                        char2 = bytearr[count-1];
416                        if ((char2 & 0xC0) != 0x80)
417                            throw new UTFDataFormatException();
418                        str.append((char)(((c & 0x1F) << 6) | (char2 & 0x3F)));
419                        break;
420                    case 14:
421                        /* 1110 xxxx  10xx xxxx  10xx xxxx */
422                        count += 3;
423                        if (count > utflen)
424                            throw new UTFDataFormatException();
425                        char2 = bytearr[count-2];
426                        char3 = bytearr[count-1];
427                        if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
428                            throw new UTFDataFormatException();
429                        str.append((char)(((c     & 0x0F) << 12) |
430                                          ((char2 & 0x3F) << 6)  |
431                                          ((char3 & 0x3F) << 0)));
432                        break;
433                    default:
434                        /* 10xx xxxx,  1111 xxxx */
435                        throw new UTFDataFormatException();
436                }
437            }
438    
439            // The number of chars produced may be less than utflen
440            return new String(str);
441        }
442    
443        public static Document convertToDOM(DataInputStream is) throws IOException {
444            Document document = null;
445            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
446            int length = is.readInt();
447            try {
448                byte[] buf = new byte[length];
449                is.readFully(buf, 0, length);
450                DocumentBuilder builder = factory.newDocumentBuilder();
451                document = builder.parse(new ByteArrayInputStream(buf));
452            } catch (Exception e) {
453                throw new IOException("Error while parsing xml: " + e.getMessage());
454            }
455            return document;
456        }
457    }