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