001/*
002  GRANITE DATA SERVICES
003  Copyright (C) 2011 GRANITE DATA SERVICES S.A.S.
004
005  This file is part of Granite Data Services.
006
007  Granite Data Services is free software; you can redistribute it and/or modify
008  it under the terms of the GNU Library General Public License as published by
009  the Free Software Foundation; either version 2 of the License, or (at your
010  option) any later version.
011
012  Granite Data Services is distributed in the hope that it will be useful, but
013  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015  for more details.
016
017  You should have received a copy of the GNU Library General Public License
018  along with this library; if not, see <http://www.gnu.org/licenses/>.
019*/
020
021package org.granite.messaging.amf.io;
022
023import java.io.DataOutputStream;
024import java.io.Externalizable;
025import java.io.IOException;
026import java.io.ObjectOutput;
027import java.io.OutputStream;
028import java.lang.reflect.Array;
029import java.math.BigDecimal;
030import java.math.BigInteger;
031import java.util.Calendar;
032import java.util.Collection;
033import java.util.Date;
034import java.util.HashMap;
035import java.util.IdentityHashMap;
036import java.util.Map;
037
038import org.granite.config.flex.Channel;
039import org.granite.context.GraniteContext;
040import org.granite.logging.Logger;
041import org.granite.messaging.amf.AMF3Constants;
042import org.granite.messaging.amf.io.convert.Converters;
043import org.granite.messaging.amf.io.util.ClassGetter;
044import org.granite.messaging.amf.io.util.DefaultJavaClassDescriptor;
045import org.granite.messaging.amf.io.util.IndexedJavaClassDescriptor;
046import org.granite.messaging.amf.io.util.JavaClassDescriptor;
047import org.granite.messaging.amf.io.util.externalizer.Externalizer;
048import org.granite.util.TypeUtil;
049import org.granite.util.XMLUtil;
050import org.granite.util.XMLUtilFactory;
051import org.w3c.dom.Document;
052
053import flex.messaging.io.ArrayCollection;
054
055/**
056 * @author Franck WOLFF
057 */
058public class AMF3Serializer extends DataOutputStream implements ObjectOutput, AMF3Constants {
059
060    ///////////////////////////////////////////////////////////////////////////
061    // Fields.
062
063    protected static final Logger log = Logger.getLogger(AMF3Serializer.class);
064    protected static final Logger logMore = Logger.getLogger(AMF3Serializer.class.getName() + "_MORE");
065
066    protected final Map<String, Integer> storedStrings = new HashMap<String, Integer>();
067    protected final Map<Object, Integer> storedObjects = new IdentityHashMap<Object, Integer>();
068    protected final Map<String, IndexedJavaClassDescriptor> storedClassDescriptors
069        = new HashMap<String, IndexedJavaClassDescriptor>();
070
071    protected final GraniteContext context = GraniteContext.getCurrentInstance();
072    protected final Converters converters = context.getGraniteConfig().getConverters();
073
074    protected final boolean externalizeLong
075                = (context.getGraniteConfig().getExternalizer(Long.class.getName()) != null);
076    protected final boolean externalizeBigInteger
077                = (context.getGraniteConfig().getExternalizer(BigInteger.class.getName()) != null);
078    protected final boolean externalizeBigDecimal
079        = (context.getGraniteConfig().getExternalizer(BigDecimal.class.getName()) != null);
080
081    protected final XMLUtil xmlUtil = XMLUtilFactory.getXMLUtil();
082    
083    protected final boolean debug = log.isDebugEnabled();
084    protected final boolean debugMore = logMore.isDebugEnabled();
085
086    protected Channel channel = null;
087
088    ///////////////////////////////////////////////////////////////////////////
089    // Constructor.
090
091    public AMF3Serializer(OutputStream out) {
092        super(out);
093
094        if (debugMore) logMore.debug("new AMF3Serializer(out=%s)", out);
095    }
096
097    ///////////////////////////////////////////////////////////////////////////
098    // ObjectOutput implementation.
099
100    public void writeObject(Object o) throws IOException {
101        if (debugMore) logMore.debug("writeObject(o=%s)", o);
102
103        try {
104                if (o == null)
105                    write(AMF3_NULL);
106                else if (!(o instanceof Externalizable)) {
107        
108                    if (converters.hasReverters())
109                        o = converters.revert(o);
110        
111                        if (o == null)
112                            write(AMF3_NULL);
113                        else if (o instanceof String || o instanceof Character)
114                        writeAMF3String(o.toString());
115                    else if (o instanceof Boolean)
116                        write(((Boolean)o).booleanValue() ? AMF3_BOOLEAN_TRUE : AMF3_BOOLEAN_FALSE);
117                    else if (o instanceof Number) {
118                        if (o instanceof Integer || o instanceof Short || o instanceof Byte)
119                            writeAMF3Integer(((Number)o).intValue());
120                        else if (externalizeLong && o instanceof Long)
121                                writeAMF3Object(o);
122                        else if (externalizeBigInteger && o instanceof BigInteger)
123                                writeAMF3Object(o);
124                        else if (externalizeBigDecimal && o instanceof BigDecimal)
125                                writeAMF3Object(o);
126                        else
127                            writeAMF3Number(((Number)o).doubleValue());
128                    }
129                    else if (o instanceof Date)
130                        writeAMF3Date((Date)o);
131                    else if (o instanceof Calendar)
132                        writeAMF3Date(((Calendar)o).getTime());
133                    else if (o instanceof Document)
134                        writeAMF3Xml((Document)o);
135                    else if (o instanceof Collection<?>)
136                        writeAMF3Collection((Collection<?>)o);
137                    else if (o.getClass().isArray()) {
138                        if (o.getClass().getComponentType() == Byte.TYPE)
139                            writeAMF3ByteArray((byte[])o);
140                        else
141                            writeAMF3Array(o);
142                    }
143                    else
144                        writeAMF3Object(o);
145                } else
146                    writeAMF3Object(o);
147        }
148        catch (IOException e) {
149                throw e;
150        }
151        catch (Exception e) {
152                throw new AMF3SerializationException(e);
153        }
154    }
155
156    ///////////////////////////////////////////////////////////////////////////
157    // AMF3 serialization.
158
159    protected void writeAMF3Integer(int i) throws IOException {
160        if (debugMore) logMore.debug("writeAMF3Integer(i=%d)", i);
161
162        if (i < AMF3_INTEGER_MIN || i > AMF3_INTEGER_MAX) {
163            if (debugMore) logMore.debug("writeAMF3Integer() - %d is out of AMF3 int range, writing it as a Number", i);
164            writeAMF3Number(i);
165        } else {
166            write(AMF3_INTEGER);
167            writeAMF3IntegerData(i);
168        }
169    }
170
171    protected void writeAMF3IntegerData(int i) throws IOException {
172        if (debugMore) logMore.debug("writeAMF3IntegerData(i=%d)", i);
173
174        if (i < AMF3_INTEGER_MIN || i > AMF3_INTEGER_MAX)
175            throw new IllegalArgumentException("Integer out of range: " + i);
176
177        if (i < 0 || i >= 0x200000) {
178            write(((i >> 22) & 0x7F) | 0x80);
179            write(((i >> 15) & 0x7F) | 0x80);
180            write(((i >> 8) & 0x7F) | 0x80);
181            write(i & 0xFF);
182        } else {
183            if (i >= 0x4000)
184                write(((i >> 14) & 0x7F) | 0x80);
185            if (i >= 0x80)
186                write(((i >> 7) & 0x7F) | 0x80);
187            write(i & 0x7F);
188        }
189    }
190
191    protected void writeAMF3Number(double d) throws IOException {
192        if (debugMore) logMore.debug("writeAMF3Number(d=%f)", d);
193
194        write(AMF3_NUMBER);
195        writeDouble(d);
196    }
197
198    protected void writeAMF3String(String s) throws IOException {
199        if (debugMore) logMore.debug("writeAMF3String(s=%s)", s);
200
201        write(AMF3_STRING);
202        writeAMF3StringData(s);
203    }
204
205    protected void writeAMF3StringData(String s) throws IOException {
206        if (debugMore) logMore.debug("writeAMF3StringData(s=%s)", s);
207
208        if (s.length() == 0) {
209            write(0x01);
210            return;
211        }
212
213        int index = indexOfStoredStrings(s);
214
215        if (index >= 0)
216            writeAMF3IntegerData(index << 1);
217        else {
218            addToStoredStrings(s);
219
220            final int sLength = s.length();
221
222            // Compute and write modified UTF-8 string length.
223            int uLength = 0;
224            for (int i = 0; i < sLength; i++) {
225                int c = s.charAt(i);
226                if ((c >= 0x0001) && (c <= 0x007F))
227                    uLength++;
228                else if (c > 0x07FF)
229                    uLength += 3;
230                else
231                    uLength += 2;
232            }
233            writeAMF3IntegerData((uLength << 1) | 0x01);
234
235            // Write modified UTF-8 bytes.
236            for (int i = 0; i < sLength; i++) {
237                int c = s.charAt(i);
238                if ((c >= 0x0001) && (c <= 0x007F)) {
239                    write(c);
240                } else if (c > 0x07FF) {
241                    write(0xE0 | ((c >> 12) & 0x0F));
242                    write(0x80 | ((c >>  6) & 0x3F));
243                    write(0x80 | ((c >>  0) & 0x3F));
244                } else {
245                    write(0xC0 | ((c >>  6) & 0x1F));
246                    write(0x80 | ((c >>  0) & 0x3F));
247                }
248            }
249        }
250    }
251
252    protected void writeAMF3Xml(Document doc) throws IOException {
253        if (debugMore) logMore.debug("writeAMF3Xml(doc=%s)", doc);
254
255        byte xmlType = AMF3_XMLSTRING;
256        Channel channel = getChannel();
257        if (channel != null && channel.isLegacyXmlSerialization())
258            xmlType = AMF3_XML;
259        write(xmlType);
260
261        int index = indexOfStoredObjects(doc);
262        if (index >= 0)
263            writeAMF3IntegerData(index << 1);
264        else {
265            addToStoredObjects(doc);
266
267            byte[] bytes = xmlUtil.toString(doc).getBytes("UTF-8");
268            writeAMF3IntegerData((bytes.length << 1) | 0x01);
269            write(bytes);
270        }
271    }
272
273    protected void writeAMF3Date(Date date) throws IOException {
274        if (debugMore) logMore.debug("writeAMF3Date(date=%s)", date);
275
276        write(AMF3_DATE);
277
278        int index = indexOfStoredObjects(date);
279        if (index >= 0)
280            writeAMF3IntegerData(index << 1);
281        else {
282            addToStoredObjects(date);
283            writeAMF3IntegerData(0x01);
284            writeDouble(date.getTime());
285        }
286    }
287
288    protected void writeAMF3Array(Object array) throws IOException {
289        if (debugMore) logMore.debug("writeAMF3Array(array=%s)", array);
290
291        write(AMF3_ARRAY);
292
293        int index = indexOfStoredObjects(array);
294        if (index >= 0)
295            writeAMF3IntegerData(index << 1);
296        else {
297            addToStoredObjects(array);
298
299            int length = Array.getLength(array);
300            writeAMF3IntegerData(length << 1 | 0x01);
301            write(0x01);
302            for (int i = 0; i < length; i++)
303                writeObject(Array.get(array, i));
304        }
305    }
306
307    protected void writeAMF3ByteArray(byte[] bytes) throws IOException {
308        if (debugMore) logMore.debug("writeAMF3ByteArray(bytes=%s)", bytes);
309
310        write(AMF3_BYTEARRAY);
311
312        int index = indexOfStoredObjects(bytes);
313        if (index >= 0)
314            writeAMF3IntegerData(index << 1);
315        else {
316            addToStoredObjects(bytes);
317
318            writeAMF3IntegerData(bytes.length << 1 | 0x01);
319            //write(bytes);
320
321            for (int i = 0; i < bytes.length; i++)
322                out.write(bytes[i]);
323        }
324    }
325
326    protected void writeAMF3Collection(Collection<?> c) throws IOException {
327        if (debugMore) logMore.debug("writeAMF3Collection(c=%s)", c);
328
329        Channel channel = getChannel();
330        if (channel != null && channel.isLegacyCollectionSerialization())
331            writeAMF3Array(c.toArray());
332        else {
333            ArrayCollection ac = (c instanceof ArrayCollection ? (ArrayCollection)c : new ArrayCollection(c));
334            writeAMF3Object(ac);
335        }
336    }
337
338    protected void writeAMF3Object(Object o) throws IOException {
339        if (debug) log.debug("writeAMF3Object(o=%s)...", o);
340
341        write(AMF3_OBJECT);
342
343        int index = indexOfStoredObjects(o);
344        if (index >= 0)
345            writeAMF3IntegerData(index << 1);
346        else {
347            addToStoredObjects(o);
348
349            ClassGetter classGetter = context.getGraniteConfig().getClassGetter();
350            if (debug) log.debug("writeAMF3Object() - classGetter=%s", classGetter);
351
352            Class<?> oClass = classGetter.getClass(o);
353            if (debug) log.debug("writeAMF3Object() - oClass=%s", oClass);
354
355            JavaClassDescriptor desc = null;
356
357            // write class description.
358            IndexedJavaClassDescriptor iDesc = getFromStoredClassDescriptors(oClass);
359            if (iDesc != null) {
360                desc = iDesc.getDescriptor();
361                writeAMF3IntegerData(iDesc.getIndex() << 2 | 0x01);
362            }
363            else {
364                iDesc = addToStoredClassDescriptors(oClass);
365                desc = iDesc.getDescriptor();
366
367                writeAMF3IntegerData((desc.getPropertiesCount() << 4) | (desc.getEncoding() << 2) | 0x03);
368                writeAMF3StringData(desc.getName());
369
370                for (int i = 0; i < desc.getPropertiesCount(); i++)
371                    writeAMF3StringData(desc.getPropertyName(i));
372            }
373            if (debug) log.debug("writeAMF3Object() - desc=%s", desc);
374
375            // write object content.
376            if (desc.isExternalizable()) {
377                Externalizer externalizer = desc.getExternalizer();
378
379                if (externalizer != null) {
380                    if (debug) log.debug("writeAMF3Object() - using externalizer=%s", externalizer);
381                    try {
382                        externalizer.writeExternal(o, this);
383                    } catch (IOException e) {
384                        throw e;
385                    } catch (Exception e) {
386                        throw new RuntimeException("Could not externalize object: " + o, e);
387                    }
388                }
389                else {
390                    if (debug) log.debug("writeAMF3Object() - legacy Externalizable=%s", o);
391                    ((Externalizable)o).writeExternal(this);
392                }
393            }
394            else {
395                if (debug) log.debug("writeAMF3Object() - writing defined properties...");
396                for (int i = 0; i < desc.getPropertiesCount(); i++) {
397                    Object obj = desc.getPropertyValue(i, o);
398                    if (debug) log.debug("writeAMF3Object() - writing defined property: %s=%s", desc.getPropertyName(i), obj);
399                    writeObject(obj);
400                }
401
402                if (desc.isDynamic()) {
403                    if (debug) log.debug("writeAMF3Object() - writing dynamic properties...");
404                    Map<?, ?> oMap = (Map<?, ?>)o;
405                    for (Map.Entry<?, ?> entry : oMap.entrySet()) {
406                        Object key = entry.getKey();
407                        if (key != null) {
408                            String propertyName = key.toString();
409                            if (propertyName.length() > 0) {
410                                if (debug) log.debug(
411                                    "writeAMF3Object() - writing dynamic property: %s=%s",
412                                    propertyName, entry.getValue()
413                                );
414                                writeAMF3StringData(propertyName);
415                                writeObject(entry.getValue());
416                            }
417                        }
418                    }
419                    writeAMF3StringData("");
420                }
421            }
422        }
423
424        if (debug) log.debug("writeAMF3Object(o=%s) - Done", o);
425    }
426
427    ///////////////////////////////////////////////////////////////////////////
428    // Cached objects methods.
429
430    protected void addToStoredStrings(String s) {
431        if (!storedStrings.containsKey(s)) {
432            Integer index = Integer.valueOf(storedStrings.size());
433            if (debug) log.debug("addToStoredStrings(s=%s) at index=%d", s, index);
434            storedStrings.put(s, index);
435        }
436    }
437
438    protected int indexOfStoredStrings(String s) {
439        Integer index = storedStrings.get(s);
440        if (debug) log.debug("indexOfStoredStrings(s=%s) -> %d", s, (index != null ? index : -1));
441        return (index != null ? index : -1);
442    }
443
444    protected void addToStoredObjects(Object o) {
445        if (o != null && !storedObjects.containsKey(o)) {
446            Integer index = Integer.valueOf(storedObjects.size());
447            if (debug) log.debug("addToStoredObjects(o=%s) at index=%d", o, index);
448            storedObjects.put(o, index);
449        }
450    }
451
452    protected int indexOfStoredObjects(Object o) {
453        Integer index = storedObjects.get(o);
454        if (debug) log.debug("indexOfStoredObjects(o=%s) -> %d", o, (index != null ? index : -1));
455        return (index != null ? index : -1);
456    }
457
458    protected IndexedJavaClassDescriptor addToStoredClassDescriptors(Class<?> clazz) {
459        final String name = JavaClassDescriptor.getClassName(clazz);
460
461        if (debug) log.debug("addToStoredClassDescriptors(clazz=%s)", clazz);
462
463        if (storedClassDescriptors.containsKey(name))
464            throw new RuntimeException(
465                "Descriptor of \"" + name + "\" is already stored at index: " +
466                getFromStoredClassDescriptors(clazz).getIndex()
467            );
468
469        // find custom class descriptor and instantiate if any
470        JavaClassDescriptor desc = null;
471
472        Class<? extends JavaClassDescriptor> descriptorType
473            = context.getGraniteConfig().getJavaDescriptor(clazz.getName());
474        if (descriptorType != null) {
475            Class<?>[] argsDef = new Class[]{Class.class};
476            Object[] argsVal = new Object[]{clazz};
477            try {
478                desc = TypeUtil.newInstance(descriptorType, argsDef, argsVal);
479            } catch (Exception e) {
480                throw new RuntimeException("Could not instantiate Java descriptor: " + descriptorType);
481            }
482        }
483
484        if (desc == null)
485            desc = new DefaultJavaClassDescriptor(clazz);
486
487        IndexedJavaClassDescriptor iDesc = new IndexedJavaClassDescriptor(storedClassDescriptors.size(), desc);
488
489        if (debug) log.debug("addToStoredClassDescriptors() - putting: name=%s, iDesc=%s", name, iDesc);
490
491        storedClassDescriptors.put(name, iDesc);
492
493        return iDesc;
494    }
495
496    protected IndexedJavaClassDescriptor getFromStoredClassDescriptors(Class<?> clazz) {
497        if (debug) log.debug("getFromStoredClassDescriptors(clazz=%s)", clazz);
498
499        String name = JavaClassDescriptor.getClassName(clazz);
500        IndexedJavaClassDescriptor iDesc = storedClassDescriptors.get(name);
501
502        if (debug) log.debug("getFromStoredClassDescriptors() -> %s", iDesc);
503
504        return iDesc;
505    }
506
507    ///////////////////////////////////////////////////////////////////////////
508    // Utilities.
509
510    protected Channel getChannel() {
511        if (channel == null) {
512            String channelId = context.getAMFContext().getChannelId();
513            if (channelId != null)
514                channel = context.getServicesConfig().findChannelById(channelId);
515            if (channel == null)
516                log.debug("Could not get channel for channel id: %s", channelId);
517        }
518        return channel;
519    }
520}