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.ByteArrayInputStream;
032import java.io.DataInputStream;
033import java.io.IOException;
034import java.io.InputStream;
035import java.io.ObjectInput;
036import java.io.UTFDataFormatException;
037import java.util.ArrayList;
038import java.util.Calendar;
039import java.util.Date;
040import java.util.GregorianCalendar;
041import java.util.List;
042import java.util.TimeZone;
043
044import javax.xml.parsers.DocumentBuilder;
045import javax.xml.parsers.DocumentBuilderFactory;
046
047import org.granite.context.GraniteContext;
048import org.granite.logging.Logger;
049import org.granite.messaging.amf.AMF0Body;
050import org.granite.messaging.amf.AMF0Message;
051import org.w3c.dom.Document;
052
053import 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 */
063public 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}