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.ByteArrayInputStream;
011import java.io.DataInputStream;
012import java.io.IOException;
013import java.io.InputStream;
014import java.io.ObjectInput;
015import java.io.UTFDataFormatException;
016import java.util.ArrayList;
017import java.util.Calendar;
018import java.util.Date;
019import java.util.GregorianCalendar;
020import java.util.List;
021import java.util.TimeZone;
022
023import javax.xml.parsers.DocumentBuilder;
024import javax.xml.parsers.DocumentBuilderFactory;
025
026import org.granite.context.GraniteContext;
027import org.granite.logging.Logger;
028import org.granite.messaging.amf.AMF0Body;
029import org.granite.messaging.amf.AMF0Message;
030import org.w3c.dom.Document;
031
032import 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 */
042public 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}