hangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegram
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1120 lines
39 KiB
1120 lines
39 KiB
9 years ago
|
// @tag enterprise
|
||
|
/**
|
||
|
* @class Ext.data.amf.Encoder
|
||
|
* This class serializes data in the Action Message Format (AMF) format.
|
||
|
* It can write simple and complex objects, to be used in conjunction with an
|
||
|
* AMF-compliant server.
|
||
|
* To encode a byte array, first construct an Encoder, optionally setting the format:
|
||
|
*
|
||
|
* var encoder = Ext.create('Ext.data.amf.Encoder', {
|
||
|
* format: 3
|
||
|
* });
|
||
|
*
|
||
|
* Then use the writer methods to out data to the :
|
||
|
*
|
||
|
* encoder.writeObject(1);
|
||
|
*
|
||
|
* And access the data through the #bytes property:
|
||
|
* encoder.bytes;
|
||
|
*
|
||
|
* You can also reset the class to start a new byte array:
|
||
|
*
|
||
|
* encoder.clear();
|
||
|
*
|
||
|
* Current limitations:
|
||
|
* AMF3 format (format:3)
|
||
|
* - writeObject will write out XML object, not legacy XMLDocument objects. A
|
||
|
* writeXmlDocument method is provided for explicitly writing XMLDocument
|
||
|
* objects.
|
||
|
* - Each object is written out explicitly, not using the reference tables
|
||
|
* supported by the AMF format. This means the function does NOT support
|
||
|
* circular reference objects.
|
||
|
* - Array objects: only the numbered indices and data will be written out.
|
||
|
* Associative values will be ignored.
|
||
|
* - Objects that aren't Arrays, Dates, Strings, Document (XML) or primitive
|
||
|
* values will be written out as anonymous objects with dynamic data.
|
||
|
* - There's no JavaScript equivalent to the ByteArray type in ActionScript,
|
||
|
* hence data will never be searialized as ByteArrays by the writeObject
|
||
|
* function. A writeByteArray method is provided for writing out ByteArray objects.
|
||
|
*
|
||
|
* AMF0 format (format:0)
|
||
|
* - Each object is written out explicitly, not using the reference tables
|
||
|
* supported by the AMF format. This means the function does NOT support
|
||
|
* circular reference objects.
|
||
|
* - Array objects: the function always writes an associative array (following
|
||
|
* the behavior of flex).
|
||
|
* - Objects that aren't Arrays, Dates, Strings, Document (XML) or primitive
|
||
|
* values will be written out as anonymous objects.
|
||
|
*
|
||
|
* For more information on working with AMF data please refer to the
|
||
|
* [AMF Guide](#/guide/amf).
|
||
|
*/
|
||
|
Ext.define('Ext.data.amf.Encoder', {
|
||
|
|
||
|
alias: 'data.amf.Encoder',
|
||
|
|
||
|
config: {
|
||
|
format: 3
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* @property {Array} bytes
|
||
|
* @readonly
|
||
|
* The constructed byte array.
|
||
|
*/
|
||
|
bytes: [],
|
||
|
|
||
|
/**
|
||
|
* Creates new Encoder.
|
||
|
* @param {Object} config Configuration options
|
||
|
*/
|
||
|
constructor: function(config) {
|
||
|
this.initConfig(config);
|
||
|
this.clear();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Reset all class states and starts a new empty array for encoding data.
|
||
|
* The method generates a new array for encoding, so it's safe to keep a
|
||
|
* reference to the old one.
|
||
|
*/
|
||
|
clear: function() {
|
||
|
this.bytes = [];
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Sets the functions that will correctly serialize for the relevant
|
||
|
* protocol version.
|
||
|
* @param {Number} protocol_version the protocol version to support
|
||
|
*/
|
||
|
applyFormat: function(protocol_version) {
|
||
|
var funcs = {
|
||
|
0: {
|
||
|
writeUndefined: this.write0Undefined,
|
||
|
writeNull: this.write0Null,
|
||
|
writeBoolean: this.write0Boolean,
|
||
|
writeNumber: this.write0Number,
|
||
|
writeString: this.write0String,
|
||
|
writeXml: this.write0Xml,
|
||
|
writeDate: this.write0Date,
|
||
|
writeArray: this.write0Array,
|
||
|
writeGenericObject: this.write0GenericObject
|
||
|
},
|
||
|
|
||
|
3: {
|
||
|
writeUndefined: this.write3Undefined,
|
||
|
writeNull: this.write3Null,
|
||
|
writeBoolean: this.write3Boolean,
|
||
|
writeNumber: this.write3Number,
|
||
|
writeString: this.write3String,
|
||
|
writeXml: this.write3Xml,
|
||
|
writeDate: this.write3Date,
|
||
|
writeArray: this.write3Array,
|
||
|
writeGenericObject: this.write3GenericObject
|
||
|
}
|
||
|
|
||
|
}[protocol_version];
|
||
|
if (funcs) {
|
||
|
Ext.apply(this, funcs);
|
||
|
return protocol_version;
|
||
|
} else {
|
||
|
//<debug>
|
||
|
Ext.Error.raise("Unsupported AMF format: " + protocol_version + ". Only '3' (AMF3) is supported at this point.");
|
||
|
//</debug>
|
||
|
return; // return nothing
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Write the appropriate data items to the byte array. Supported types:
|
||
|
* - undefined
|
||
|
* - null
|
||
|
* - boolean
|
||
|
* - integer (if AMF3 - limited by 29-bit int, otherwise passed as double)
|
||
|
* - double
|
||
|
* - UTF-8 string
|
||
|
* - XML Document (identified by being instaneof Document. Can be generated with: new DOMParser()).parseFromString(xml, "text/xml");
|
||
|
* @param {Object} item A primitive or object to write to the stream
|
||
|
*/
|
||
|
writeObject: function(item) {
|
||
|
var t = typeof(item);
|
||
|
//Ext.log("Writing " + item + " of type " + t);
|
||
|
if (t === "undefined") {
|
||
|
this.writeUndefined();
|
||
|
} else if (item === null) { // can't check type since typeof(null) returns "object"
|
||
|
this.writeNull();
|
||
|
} else if (Ext.isBoolean(item)) {
|
||
|
this.writeBoolean(item);
|
||
|
} else if (Ext.isString(item)) {
|
||
|
this.writeString(item);
|
||
|
} else if (t === "number" || item instanceof Number) { // Can't use Ext.isNumeric since it accepts strings as well
|
||
|
this.writeNumber(item);
|
||
|
} else if (t === "object") {
|
||
|
// Figure out which object this is
|
||
|
if (item instanceof Date) {
|
||
|
this.writeDate(item);
|
||
|
} else if (Ext.isArray(item)) { // this won't catch associative arrays deserialized by the Packet class!
|
||
|
this.writeArray(item);
|
||
|
} else if (this.isXmlDocument(item)) {
|
||
|
this.writeXml(item);
|
||
|
} else {
|
||
|
// Treat this as a generic object with name/value pairs of data.
|
||
|
this.writeGenericObject(item);
|
||
|
}
|
||
|
} else {
|
||
|
//<debug>
|
||
|
Ext.log.warn("AMF Encoder: Unknown item type " + t + " can't be written to stream: " + item);
|
||
|
//</debug>
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes the AMF3 undefined value to the byte array.
|
||
|
* @private
|
||
|
*/
|
||
|
write3Undefined: function() {
|
||
|
this.writeByte(0x00); // AMF3 undefined
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes the AMF0 undefined value to the byte array.
|
||
|
* @private
|
||
|
*/
|
||
|
write0Undefined: function() {
|
||
|
this.writeByte(0x06); // AMF0 undefined
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes the AMF3 null value to the byte array.
|
||
|
* @private
|
||
|
*/
|
||
|
write3Null: function() {
|
||
|
this.writeByte(0x01); // AMF3 null
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes the AMF0 null value to the byte array.
|
||
|
* @private
|
||
|
*/
|
||
|
write0Null: function() {
|
||
|
this.writeByte(0x05); // AMF0 null
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes the appropriate AMF3 boolean value to the byte array.
|
||
|
* @param {boolean} item The value to write
|
||
|
* @private
|
||
|
*/
|
||
|
write3Boolean: function(item) {
|
||
|
//<debug>
|
||
|
if (typeof(item) !== "boolean") {
|
||
|
Ext.log.warn("Encoder: writeBoolean argument is not a boolean. Coercing.");
|
||
|
}
|
||
|
// </debug>
|
||
|
if (item) {
|
||
|
this.writeByte(0x03); // AMF3 true
|
||
|
} else {
|
||
|
this.writeByte(0x02); // AMF3 false
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes the appropriate AMF0 boolean value to the byte array.
|
||
|
* @param {boolean} item The value to write
|
||
|
* @private
|
||
|
*/
|
||
|
write0Boolean: function(item) {
|
||
|
//<debug>
|
||
|
if (typeof(item) !== "boolean") {
|
||
|
Ext.log.warn("Encoder: writeBoolean argument is not a boolean. Coercing.");
|
||
|
}
|
||
|
// </debug>
|
||
|
this.writeByte(0x01); // AMF0 boolean marker
|
||
|
if (item) {
|
||
|
this.writeByte(0x01); // AMF0 true
|
||
|
} else {
|
||
|
this.writeByte(0x00); // AMF0 false
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Encodes a U29 int, returning a byte array with the encoded number.
|
||
|
* @param item - unsigned int value
|
||
|
* @private
|
||
|
*/
|
||
|
encode29Int: function(item) {
|
||
|
var data = [], // prepare the bytes, then send them to the array
|
||
|
num = item,
|
||
|
nibble,
|
||
|
i;
|
||
|
if (num == 0) {
|
||
|
return [0]; // no other data
|
||
|
}
|
||
|
// we have a special case if the number is 4-nibbles in U29 encoding
|
||
|
if (num > 0x001fffff) {
|
||
|
// last nibble is an 8-bit value
|
||
|
nibble = num & 0xff;
|
||
|
data.unshift(nibble);
|
||
|
num = num >> 8;
|
||
|
}
|
||
|
// get all the 7-bit parts ready
|
||
|
while (num > 0) {
|
||
|
nibble = num & 0x7f; // 7 bits
|
||
|
data.unshift(nibble);
|
||
|
num = num >> 7;
|
||
|
}
|
||
|
// now we need to mark each MSb of a 7-bit byte with a 1, except the absolute last one which has a 0.
|
||
|
// If there's an 8-bit byte, the 7-bit byte before it is marked with 1 as well.
|
||
|
for (i = 0; i < data.length - 1; i++) {
|
||
|
data[i] = data[i] | 0x80;
|
||
|
}
|
||
|
return data;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes a numberic value to the byte array in AMF3 format
|
||
|
* @param item A native numeric value, Number instance or one of Infinity, -Infinity or NaN
|
||
|
* @private
|
||
|
*/
|
||
|
write3Number: function(item) {
|
||
|
var data;
|
||
|
var maxInt = 0x1fffffff,
|
||
|
minSignedInt = -0xfffffff;
|
||
|
//<debug>
|
||
|
if (typeof(item) !== "number" && !(item instanceof Number)) {
|
||
|
Ext.log.warn("Encoder: writeNumber argument is not numeric. Can't coerce.");
|
||
|
}
|
||
|
// </debug>
|
||
|
|
||
|
// switch to the primitive value for handling:
|
||
|
if (item instanceof Number) {
|
||
|
item = item.valueOf();
|
||
|
}
|
||
|
// First we need to determine if this is an integer or a float.
|
||
|
// AMF3 allows integers between -2^28 < item < 2^29, else they need to be passed as doubles.
|
||
|
if (item % 1 === 0 && item >= minSignedInt && item <= maxInt) {
|
||
|
// The number has no decimal point and is within bounds. Let's encode it.
|
||
|
item = item & maxInt; // get an unsigned value to work with - we only care about 29 bits.
|
||
|
data = this.encode29Int(item);
|
||
|
// And , mark it as an integer
|
||
|
data.unshift(0x04); // AMF3 integer marker
|
||
|
// write it!
|
||
|
this.writeBytes(data);
|
||
|
|
||
|
} else {
|
||
|
data = this.encodeDouble(item);
|
||
|
data.unshift(0x05); // AMF3 double marker
|
||
|
this.writeBytes(data);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes a numberic value to the byte array in AMF0 format
|
||
|
* @param item A native numeric value, Number instance or one of Infinity, -Infinity or NaN
|
||
|
* @private
|
||
|
*/
|
||
|
write0Number: function(item) {
|
||
|
var data;
|
||
|
//<debug>
|
||
|
if (typeof(item) !== "number" && !(item instanceof Number)) {
|
||
|
Ext.log.warn("Encoder: writeNumber argument is not numeric. Can't coerce.");
|
||
|
}
|
||
|
// </debug>
|
||
|
|
||
|
// switch to the primitive value for handling:
|
||
|
if (item instanceof Number) {
|
||
|
item = item.valueOf();
|
||
|
}
|
||
|
//In AMF0 numbers are always serialized as double-float values.
|
||
|
data = this.encodeDouble(item);
|
||
|
data.unshift(0x00); // AMF0 double marker
|
||
|
this.writeBytes(data);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Convert a UTF 16 char to a UTF 8 char
|
||
|
* @param {Number} c char 16-bit code to convert
|
||
|
* @return {Array} byte array with the UTF 8 values
|
||
|
*/
|
||
|
encodeUtf8Char: function(c) {
|
||
|
var data = [],
|
||
|
val, b, i,
|
||
|
marker;
|
||
|
//<debug>
|
||
|
if (c > 0x10FFFF) {
|
||
|
//<debug>
|
||
|
Ext.Error.raise("UTF 8 char out of bounds");
|
||
|
//</debug>
|
||
|
}
|
||
|
//</debug>
|
||
|
if (c <= 0x7F) {
|
||
|
// One byte UTF8
|
||
|
data.push(c);
|
||
|
} else {
|
||
|
// Multi-byte UTF8. Figure out how many bytes:
|
||
|
if (c <= 0x7ff) {
|
||
|
b = 2;
|
||
|
} else if (c <= 0xffff) {
|
||
|
b = 3;
|
||
|
} else {
|
||
|
b = 4;
|
||
|
}
|
||
|
// encode LSBs of value
|
||
|
marker = 0x80; // MSB marker
|
||
|
for (i = 1; i < b; i++) {
|
||
|
val = (c & 0x3F) | 0x80; // lowest 6 bits of number, plus a flag to mark the byte
|
||
|
data.unshift(val);
|
||
|
c = c >> 6; // drop 6 LSbs
|
||
|
marker = (marker >> 1) | 0x80; // add one more bit for every byte
|
||
|
}
|
||
|
// the final byte is now ready, but we need to mark it to show how many other bytes follow
|
||
|
val = c | marker;
|
||
|
data.unshift(val);
|
||
|
}
|
||
|
return data;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Accepts a string and returns a byte array encoded in UTF-8
|
||
|
* @param {String} str String to encode
|
||
|
* @return {Array} byte array with string encoded in UTF-8 format
|
||
|
* @private
|
||
|
*/
|
||
|
encodeUtf8String: function(str) {
|
||
|
var i,
|
||
|
utf8Data = [];
|
||
|
for (i = 0; i < str.length; i++) {
|
||
|
var data = this.encodeUtf8Char(str.charCodeAt(i));
|
||
|
Ext.Array.push(utf8Data, data);
|
||
|
}
|
||
|
return utf8Data;
|
||
|
|
||
|
// quicker conversion, doesn't work in IE:
|
||
|
// utf8String = unescape(encodeURIComponent(str)),
|
||
|
// utf8Data = [];
|
||
|
|
||
|
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Encode the length of a UTF-8 string in AMF3 format.
|
||
|
* @param {Array} utf8Data byte array with the encoded data
|
||
|
* @return {Array} byte array encoding of length
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
encode3Utf8StringLen: function(utf8Data) {
|
||
|
var len = utf8Data.length,
|
||
|
data = [];
|
||
|
if (len <= 0xFFFFFFF) {
|
||
|
// String is under max allowed length in AMF3
|
||
|
// AMF3 strings use the LSb to mark whether it's a string reference or a string value. For now we only pass values:
|
||
|
len = len << 1;
|
||
|
len = len | 1; // mark as value
|
||
|
// push length value to the array
|
||
|
data =this.encode29Int(len);
|
||
|
} else {
|
||
|
//<debug>
|
||
|
Ext.Error.raise("UTF8 encoded string too long to serialize to AMF: " + len);
|
||
|
//</debug>
|
||
|
}
|
||
|
return data;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Write an AMF3 UTF-8 string to the byte array
|
||
|
* @param {String} item The string to write
|
||
|
* @private
|
||
|
*/
|
||
|
write3String: function(item) {
|
||
|
//<debug>
|
||
|
if (!Ext.isString(item)) {
|
||
|
Ext.log.warn("Encoder: writString argument is not a string.");
|
||
|
}
|
||
|
// </debug>
|
||
|
if (item == "") { // special case for the empty string
|
||
|
this.writeByte(0x06); // AMF3 string marker
|
||
|
this.writeByte(0x01); // zero length string
|
||
|
} else {
|
||
|
// first encode string to UTF-8.
|
||
|
var utf8Data = this.encodeUtf8String(item);
|
||
|
var lenData = this.encode3Utf8StringLen(utf8Data);
|
||
|
// only write encoding once we got here, i.e. length of string is legal
|
||
|
this.writeByte(0x06); // AMF3 string marker, only if we can actually write a string
|
||
|
this.writeBytes(lenData);
|
||
|
this.writeBytes(utf8Data);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Encode 16- or 32-bit integers into big-endian (network order) bytes
|
||
|
* @param {Number} value the number to encode.
|
||
|
* @param {Number} byte_count 2 or 4 byte encoding
|
||
|
* @return {Array} byte array with encoded number
|
||
|
*/
|
||
|
encodeXInt: function(value, byte_count) {
|
||
|
var data = [],
|
||
|
i;
|
||
|
for (i = 0; i < byte_count; i++) {
|
||
|
data.unshift(value & 0xff);
|
||
|
value = value >> 8;
|
||
|
}
|
||
|
return data;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Write an AMF0 UTF-8 string to the byte array
|
||
|
* @param {String} item The string to write
|
||
|
* @private
|
||
|
*/
|
||
|
write0String: function(item) {
|
||
|
//<debug>
|
||
|
if (!Ext.isString(item)) {
|
||
|
Ext.log.warn("Encoder: writString argument is not a string.");
|
||
|
}
|
||
|
// </debug>
|
||
|
if (item == "") { // special case for the empty string
|
||
|
this.writeByte(0x02); // AMF0 short string marker
|
||
|
this.writeBytes([0x00, 0x00]); // zero length string
|
||
|
} else {
|
||
|
// first encode string to UTF-8.
|
||
|
var utf8Data = this.encodeUtf8String(item);
|
||
|
var encoding;
|
||
|
var lenData;
|
||
|
if (utf8Data.length <= 0xffff) {
|
||
|
// short string
|
||
|
encoding = 0x02; // short string
|
||
|
lenData = this.encodeXInt(utf8Data.length, 2);
|
||
|
} else {
|
||
|
// long string
|
||
|
encoding = 0x0C; // long string
|
||
|
lenData = this.encodeXInt(utf8Data.length, 4);
|
||
|
}
|
||
|
this.writeByte(encoding); // Approperiate AMF0 string marker
|
||
|
this.writeBytes(lenData);
|
||
|
this.writeBytes(utf8Data);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes an XML document in AMF3 format.
|
||
|
* @param {Object} xml XML document (type Document typically)
|
||
|
* @param {number} amfType Either 0x07 or 0x0B - the AMF3 object type to use
|
||
|
* @private
|
||
|
*/
|
||
|
write3XmlWithType: function(xml, amfType) {
|
||
|
//<debug>
|
||
|
// We accept XML Documents, or strings
|
||
|
if (amfType !== 0x07 && amfType !== 0x0B) {
|
||
|
Ext.Error.raise("write XML with unknown AMF3 code: " + amfType);
|
||
|
}
|
||
|
if (!this.isXmlDocument(xml)) {
|
||
|
Ext.log.warn("Encoder: write3XmlWithType argument is not an xml document.");
|
||
|
}
|
||
|
// </debug>
|
||
|
var xmlStr = this.convertXmlToString(xml);
|
||
|
if (xmlStr == "") { // special case for the empty string
|
||
|
this.writeByte(amfType); // AMF3 XML marker
|
||
|
this.writeByte(0x01); // zero length string
|
||
|
} else {
|
||
|
// first encode string to UTF-8.
|
||
|
var utf8Data = this.encodeUtf8String(xmlStr);
|
||
|
var lenData = this.encode3Utf8StringLen(utf8Data);
|
||
|
// only write encoding once we got here, i.e. length of string is legal
|
||
|
this.writeByte(amfType); // AMF3 XML marker, only if we can actually write the string
|
||
|
this.writeBytes(lenData);
|
||
|
this.writeBytes(utf8Data);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes an Legacy XMLDocument (ActionScript Legacy XML object) in AMF3
|
||
|
* format. Must be called explicitly.
|
||
|
* The writeObject method will call writeXml and not writeXmlDocument.
|
||
|
* @param {Object} xml XML document (type Document typically) to write
|
||
|
*/
|
||
|
write3XmlDocument: function(xml) {
|
||
|
this.write3XmlWithType(xml, 0x07);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes an XML object (ActionScript 3 new XML object) in AMF3 format.
|
||
|
* @param {Object} xml XML document (type Document typically) to write
|
||
|
* @private
|
||
|
*/
|
||
|
write3Xml: function(xml) {
|
||
|
this.write3XmlWithType(xml, 0x0B);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes an XMLDocument in AMF0 format.
|
||
|
* @param {Object} xml XML document (type Document typically) to write
|
||
|
* @private
|
||
|
*/
|
||
|
write0Xml: function(xml) {
|
||
|
//<debug>
|
||
|
// We accept XML Documents, or strings
|
||
|
if (!this.isXmlDocument(xml)) {
|
||
|
Ext.log.warn("Encoder: write0Xml argument is not an xml document.");
|
||
|
}
|
||
|
// </debug>
|
||
|
var xmlStr = this.convertXmlToString(xml);
|
||
|
this.writeByte(0x0F); // AMF0 XML marker
|
||
|
|
||
|
// Always encoded as a long string
|
||
|
var utf8Data = this.encodeUtf8String(xmlStr);
|
||
|
var lenData = this.encodeXInt(utf8Data.length, 4);
|
||
|
this.writeBytes(lenData);
|
||
|
this.writeBytes(utf8Data);
|
||
|
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes a date in AMF3 format.
|
||
|
* @param {Date} date the date object
|
||
|
* @private
|
||
|
*/
|
||
|
write3Date: function(date) {
|
||
|
|
||
|
//<debug>
|
||
|
if (!(date instanceof Date)) {
|
||
|
Ext.Error.raise("Serializing a non-date object as date: " + date);
|
||
|
}
|
||
|
//</debug>
|
||
|
// For now, we don't use object references to just encode the date.
|
||
|
this.writeByte(0x08); // AMF3 date marker
|
||
|
this.writeBytes(this.encode29Int(0x1)); // mark this as a date value - we don't support references yet
|
||
|
this.writeBytes(this.encodeDouble(new Number(date)));
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes a date in AMF0 format.
|
||
|
* @param {Date} date the date object
|
||
|
* @private
|
||
|
*/
|
||
|
write0Date: function(date) {
|
||
|
|
||
|
//<debug>
|
||
|
if (!(date instanceof Date)) {
|
||
|
Ext.Error.raise("Serializing a non-date object as date: " + date);
|
||
|
}
|
||
|
//</debug>
|
||
|
// For now, we don't use object references to just encode the date.
|
||
|
this.writeByte(0x0B); // AMF0 date marker
|
||
|
this.writeBytes(this.encodeDouble(new Number(date)));
|
||
|
this.writeBytes([0x00, 0x00]); // placeholder for timezone, standard says to keep 0, flash actually writes data here
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes an array in AMF3 format. Only the ordered part of the array use handled.
|
||
|
* Unordered parts are ignored (e.g. a["hello"] will not be encoded).
|
||
|
* @param {Array} arr the array to serialize.
|
||
|
* @private
|
||
|
*/
|
||
|
write3Array: function(arr) {
|
||
|
|
||
|
//<debug>
|
||
|
if (!Ext.isArray(arr)) {
|
||
|
Ext.Error.raise("Serializing a non-array object as array: " + arr);
|
||
|
}
|
||
|
if (arr.length > 0xFFFFFFF) {
|
||
|
Ext.Error.raise("Array size too long to encode in AMF3: " + arr.length);
|
||
|
}
|
||
|
//</debug>
|
||
|
// For now, we don't use object references to just encode the array.
|
||
|
this.writeByte(0x09); // AMF3 array marker
|
||
|
|
||
|
// encode ordered part of array's length
|
||
|
var len = arr.length;
|
||
|
len = len << 1; // right-most bit marks this as size
|
||
|
len = len | 0x1; // mark it a size
|
||
|
this.writeBytes(this.encode29Int(len));
|
||
|
|
||
|
// The associative part of the array is ignored, so mark it as empty
|
||
|
this.writeByte(0x01); // equivalent to an empty UTF-8 string
|
||
|
|
||
|
// now iterate over the array, writing each element
|
||
|
Ext.each(arr, function(x) {this.writeObject(x);}, this);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes a key-value pair in AMF0 format.
|
||
|
* @param {String} key the name of the property
|
||
|
* @param {Object} value to write in AMF0 format
|
||
|
*/
|
||
|
write0ObjectProperty: function(key, value) {
|
||
|
if (!(key instanceof String) && (typeof(key) !== "string")) {
|
||
|
// coerce to a string
|
||
|
key = key + "";
|
||
|
}
|
||
|
// first encode the key to a short UTF-8.
|
||
|
var utf8Data = this.encodeUtf8String(key);
|
||
|
var lenData;
|
||
|
lenData = this.encodeXInt(utf8Data.length, 2);
|
||
|
this.writeBytes(lenData);
|
||
|
this.writeBytes(utf8Data);
|
||
|
// and now write out the object
|
||
|
this.writeObject(value);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes an associative array in AMF0 format.
|
||
|
* @param {Array} arr the array to serialize.
|
||
|
* @private
|
||
|
*/
|
||
|
write0Array: function(arr) {
|
||
|
var key;
|
||
|
//<debug>
|
||
|
if (!Ext.isArray(arr)) {
|
||
|
Ext.Error.raise("Serializing a non-array object as array: " + arr);
|
||
|
}
|
||
|
//</debug>
|
||
|
|
||
|
/* This writes a strict array, but it seems Flex always writes out associative arrays, so mimic behavior
|
||
|
|
||
|
// For now, we don't use object references to just encode the array.
|
||
|
this.writeByte(0x0A); // AMF0 strict array marker
|
||
|
|
||
|
// encode ordered part of array's length
|
||
|
var len = arr.length;
|
||
|
this.writeBytes(this.encodeXInt(len, 4));
|
||
|
|
||
|
// now iterate over the array, writing each element
|
||
|
Ext.each(arr, function(x) {this.writeObject(x);}, this);
|
||
|
*/
|
||
|
// Use ECMA (associative) array type
|
||
|
this.writeByte(0x08); // AMF0 ECMA-array marker
|
||
|
// we need to know the length of the array before we write the serialized data
|
||
|
// to the array. Better to traverse it twice than to copy all the byte data afterwards
|
||
|
var total = 0;
|
||
|
for (key in arr) {
|
||
|
total++;
|
||
|
}
|
||
|
// Now write out the length of the array
|
||
|
this.writeBytes(this.encodeXInt(total, 4));
|
||
|
// then write out the data
|
||
|
for (key in arr) {
|
||
|
Ext.Array.push(this.write0ObjectProperty(key, arr[key]));
|
||
|
}
|
||
|
// And finally the object end marker
|
||
|
this.writeBytes([0x00, 0x00, 0x09]);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes a strict-array in AMF0 format. Unordered parts are ignored (e.g.
|
||
|
* a["hello"] will not be encoded). This function is included for
|
||
|
* completeness and will never be called by writeObject.
|
||
|
* @param {Array} arr the array to serialize.
|
||
|
*/
|
||
|
write0StrictArray: function(arr) {
|
||
|
|
||
|
//<debug>
|
||
|
if (!Ext.isArray(arr)) {
|
||
|
Ext.Error.raise("Serializing a non-array object as array: " + arr);
|
||
|
}
|
||
|
//</debug>
|
||
|
|
||
|
// For now, we don't use object references to just encode the array.
|
||
|
this.writeByte(0x0A); // AMF0 strict array marker
|
||
|
|
||
|
// encode ordered part of array's length
|
||
|
var len = arr.length;
|
||
|
this.writeBytes(this.encodeXInt(len, 4));
|
||
|
|
||
|
// now iterate over the array, writing each element
|
||
|
Ext.each(arr, function(x) {this.writeObject(x);}, this);
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Write a byte array in AMF3 format. This function is never called directly
|
||
|
* by writeObject since there's no way to distinguish a regular array from a
|
||
|
* byte array.
|
||
|
* @param {Array} arr the object to serialize.
|
||
|
*/
|
||
|
write3ByteArray: function(arr) {
|
||
|
|
||
|
//<debug>
|
||
|
if (!Ext.isArray(arr)) {
|
||
|
Ext.Error.raise("Serializing a non-array object as array: " + arr);
|
||
|
}
|
||
|
if (arr.length > 0xFFFFFFF) {
|
||
|
Ext.Error.raise("Array size too long to encode in AMF3: " + arr.length);
|
||
|
}
|
||
|
//</debug>
|
||
|
this.writeByte(0x0c); // Byte array marker
|
||
|
|
||
|
// for now no support for references, so just dump the length and data
|
||
|
|
||
|
// encode array's length
|
||
|
var len = arr.length;
|
||
|
len = len << 1; // right-most bit marks this as size
|
||
|
len = len | 0x1; // mark it a size
|
||
|
this.writeBytes(this.encode29Int(len));
|
||
|
// and finally, dump the byte data
|
||
|
this.writeBytes(arr);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Write an object to the byte array in AMF3 format.
|
||
|
* Since we don't have the class information form Flex, the object
|
||
|
* is written as an anonymous object.
|
||
|
* @param {Array} obj the object to serialize.
|
||
|
* @private
|
||
|
*/
|
||
|
write3GenericObject: function(obj) {
|
||
|
var name;
|
||
|
|
||
|
//<debug>
|
||
|
if (!Ext.isObject(obj)) {
|
||
|
Ext.Error.raise("Serializing a non-object object: " + obj);
|
||
|
}
|
||
|
//</debug>
|
||
|
// For now, we don't use object references so just encode the object.
|
||
|
this.writeByte(0x0A); // AMF3 object marker
|
||
|
// The following 29-int is marked as follows (LSb to MSb) to signify a
|
||
|
// "U29O-traits":
|
||
|
// 1 - LSb marks an object value (1) or an object reference (0) which
|
||
|
// is not yet supported.
|
||
|
// 1 - trait values (1) or trait references (0) which are not supported
|
||
|
// yet.
|
||
|
// 0 - AMF3 format (0) or externalizable, i.e. object handles own
|
||
|
// serialization (1) which is not supported.
|
||
|
// 1 - dynamic (1) or not dynamic (0) object which is not relevant since
|
||
|
// we pass all data as dynamic fields.
|
||
|
// The reset of the bits signify how many sealed traits the object has.
|
||
|
// we pass 0 since all data is passed as dynamic fields
|
||
|
var oType = 0x0b; // binary 1011
|
||
|
this.writeByte(oType);
|
||
|
// Next we pass the class name, which is the empty string for anonymous
|
||
|
// objects
|
||
|
this.writeByte(0x01);
|
||
|
// Next are the sealed traits (of which we have none)
|
||
|
// And now the name / value pairs of dynamic fields
|
||
|
for (name in obj) {
|
||
|
// Ensure that name is actually a string
|
||
|
var newName = new String(name).valueOf();
|
||
|
if (newName == "") {
|
||
|
//<debug>
|
||
|
Ext.Error.raise("Can't encode non-string field name: " + name);
|
||
|
//</debug>
|
||
|
}
|
||
|
var nameData = (this.encodeUtf8String(name));
|
||
|
this.writeBytes(this.encode3Utf8StringLen(name));
|
||
|
this.writeBytes(nameData);
|
||
|
this.writeObject(obj[name]);
|
||
|
}
|
||
|
// And mark the end of the dynamic field section with the empty string
|
||
|
this.writeByte(0x01);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Write an object to the byte array in AMF0 format.
|
||
|
* Since we don't have the class information form Flex, the object
|
||
|
* is written as an anonymous object.
|
||
|
* @param {Array} obj the object to serialize.
|
||
|
* @private
|
||
|
*/
|
||
|
write0GenericObject: function(obj) {
|
||
|
var typed, amfType, key;
|
||
|
//<debug>
|
||
|
if (!Ext.isObject(obj)) {
|
||
|
Ext.Error.raise("Serializing a non-object object: " + obj);
|
||
|
}
|
||
|
//</debug>
|
||
|
// For now, we don't use object references so just encode the object.
|
||
|
// if the object is typed, the ID changes and we need to send the type ahead of the data
|
||
|
typed = !!obj.$flexType;
|
||
|
amfType = typed ? 0x10 : 0x03; // typed vs. untyped object
|
||
|
this.writeByte(amfType); // AMF0 object marker
|
||
|
// if typed, send object type
|
||
|
if (typed) {
|
||
|
this.write0ShortUtf8String(obj.$flexType);
|
||
|
}
|
||
|
// then write out the data. There's no counter, but other than that it's the same as an ECMA array
|
||
|
for (key in obj) {
|
||
|
if (key != "$flexType") {
|
||
|
Ext.Array.push(this.write0ObjectProperty(key, obj[key]));
|
||
|
}
|
||
|
}
|
||
|
// And finally the object end marker
|
||
|
this.writeBytes([0x00, 0x00, 0x09]);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes a byte to the byte array
|
||
|
* @param {number} b Byte to write to the array
|
||
|
* @private
|
||
|
*/
|
||
|
writeByte: function(b) {
|
||
|
|
||
|
//<debug>
|
||
|
if (b < 0 || b > 255) {
|
||
|
Ex.Error.raise('ERROR: Value being written outside byte range: ' + b);
|
||
|
}
|
||
|
//</debug>
|
||
|
Ext.Array.push(this.bytes, b);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes a byte array to the byte array
|
||
|
* @param {number} b Byte array to append to the array
|
||
|
* @private
|
||
|
*/
|
||
|
writeBytes: function(b) {
|
||
|
var i;
|
||
|
//<debug>
|
||
|
if (!Ext.isArray(b)) {
|
||
|
Ext.Error.raise("Decoder: writeBytes parameter is not an array: " + b);
|
||
|
}
|
||
|
for (i = 0; i < b.length; i++) {
|
||
|
if (b[i] < 0 || b[i] > 255 || !Ext.isNumber(b[i])) {
|
||
|
Ext.Error.raise("ERROR: Value " + i + " being written outside byte range: " + b[i]);
|
||
|
}
|
||
|
}
|
||
|
//</debug>
|
||
|
Ext.Array.push(this.bytes, b);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Converts an XML Document object to a string.
|
||
|
* @param {Object} xml XML document (type Document typically) to convert
|
||
|
* @return {String} A string representing the document
|
||
|
* @private
|
||
|
*/
|
||
|
convertXmlToString: function(xml) {
|
||
|
var str;
|
||
|
if (window.XMLSerializer) {
|
||
|
// this is not IE, so:
|
||
|
str = new window.XMLSerializer().serializeToString(xml);
|
||
|
} else {
|
||
|
//no XMLSerializer, might be an old version of IE
|
||
|
str = xml.xml;
|
||
|
}
|
||
|
return str;
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Tries to determine if an object is an XML document
|
||
|
* @param {Object} item to identify
|
||
|
* @return {Boolean} true if it's an XML document, false otherwise
|
||
|
*/
|
||
|
isXmlDocument: function(item) {
|
||
|
// We can't test if Document is defined since IE just throws an exception. Instead rely on the DOMParser object
|
||
|
if (window.DOMParser) {
|
||
|
if (Ext.isDefined(item.doctype)) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
// Otherwise, check if it has an XML field
|
||
|
if (Ext.isString(item.xml)) {
|
||
|
// and we can get the xml
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
/*
|
||
|
* The encodeDouble function is derived from code from the typedarray.js library by Linden Research, Inc.
|
||
|
*
|
||
|
|
||
|
Copyright (c) 2010, Linden Research, Inc.
|
||
|
|
||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
of this software and associated documentation files (the "Software"), to deal
|
||
|
in the Software without restriction, including without limitation the rights
|
||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
copies of the Software, and to permit persons to whom the Software is
|
||
|
furnished to do so, subject to the following conditions:
|
||
|
|
||
|
The above copyright notice and this permission notice shall be included in
|
||
|
all copies or substantial portions of the Software.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
|
THE SOFTWARE.
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Encodes an IEEE-754 double-precision number.
|
||
|
* @param {Number} num the number to encode
|
||
|
* @return {Array} byte array containing the encoded number
|
||
|
* @private
|
||
|
*/
|
||
|
encodeDouble: function(v) {
|
||
|
var ebits = 11, fbits = 52; // double
|
||
|
var bias = (1 << (ebits - 1)) - 1,
|
||
|
s, e, f, ln,
|
||
|
i, bits, str, data = [];
|
||
|
|
||
|
// Precalculated values
|
||
|
var K_INFINITY=[127,240,0,0,0,0,0,0],
|
||
|
K_NINFINITY=[255,240,0,0,0,0,0,0],
|
||
|
K_NAN=[255,248,0,0,0,0,0,0];
|
||
|
|
||
|
|
||
|
// Compute sign, exponent, fraction
|
||
|
if (isNaN(v)) {
|
||
|
data = K_NAN;
|
||
|
} else if (v === Infinity) {
|
||
|
data = K_INFINITY;
|
||
|
} else if (v == -Infinity) {
|
||
|
data = K_NINFINITY;
|
||
|
} else {
|
||
|
// not a special case, so encode
|
||
|
if (v === 0) {
|
||
|
e = 0; f = 0; s = (1 / v === -Infinity) ? 1 : 0;
|
||
|
}
|
||
|
else {
|
||
|
s = v < 0;
|
||
|
v = Math.abs(v);
|
||
|
|
||
|
if (v >= Math.pow(2, 1 - bias)) {
|
||
|
// Normalized
|
||
|
ln = Math.min(Math.floor(Math.log(v) / Math.LN2), bias);
|
||
|
e = ln + bias;
|
||
|
f = Math.round(v * Math.pow(2, fbits - ln) - Math.pow(2, fbits));
|
||
|
}
|
||
|
else {
|
||
|
// Denormalized
|
||
|
e = 0;
|
||
|
f = Math.round(v / Math.pow(2, 1 - bias - fbits));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Pack sign, exponent, fraction
|
||
|
bits = [];
|
||
|
for (i = fbits; i; i -= 1) { bits.push(f % 2 ? 1 : 0); f = Math.floor(f / 2); }
|
||
|
for (i = ebits; i; i -= 1) { bits.push(e % 2 ? 1 : 0); e = Math.floor(e / 2); }
|
||
|
bits.push(s ? 1 : 0);
|
||
|
bits.reverse();
|
||
|
str = bits.join('');
|
||
|
|
||
|
// Bits to bytes
|
||
|
data = [];
|
||
|
while (str.length) {
|
||
|
data.push(parseInt(str.substring(0, 8), 2));
|
||
|
str = str.substring(8);
|
||
|
}
|
||
|
}
|
||
|
return data;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes a short UTF8 string preceded with a 16-bit length.
|
||
|
* @param {String} str the string to write
|
||
|
*/
|
||
|
write0ShortUtf8String: function(str) {
|
||
|
var utf8Data = this.encodeUtf8String(str),
|
||
|
lenData;
|
||
|
lenData = this.encodeXInt(utf8Data.length, 2);
|
||
|
this.writeBytes(lenData);
|
||
|
this.writeBytes(utf8Data);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes an AMF packet to the byte array
|
||
|
* @param {Array} headers the headers to serialize. Each item in the array
|
||
|
* should be an object with three fields:
|
||
|
* name, mustUnderstand, value
|
||
|
* @param {Array} messages the messages to serialize. Each item in the array
|
||
|
* should be an object with three fields:
|
||
|
* targetUri, responseUri, body
|
||
|
*/
|
||
|
writeAmfPacket: function(headers, messages) {
|
||
|
var i;
|
||
|
//<debug>
|
||
|
if (this.config.format != 0) {
|
||
|
Ext.Error.raise ("Trying to write a packet on an AMF3 Encoder. Only AMF0 is supported!");
|
||
|
}
|
||
|
if (!Ext.isArray(headers)) {
|
||
|
Ext.Error.raise("headers is not an array: " + headers);
|
||
|
}
|
||
|
if (!Ext.isArray(messages)) {
|
||
|
Ext.Error.raise("messages is not an array: " + messages);
|
||
|
}
|
||
|
//</debug>
|
||
|
// Write Packet marker
|
||
|
this.writeBytes([0x00, 0x00]); // AMF 0 version for this packet.
|
||
|
// Write header count
|
||
|
this.writeBytes(this.encodeXInt(headers.length, 2));
|
||
|
// And actual headers
|
||
|
for (i in headers) {
|
||
|
this.writeAmfHeader(headers[i].name, headers[i].mustUnderstand, headers[i].value);
|
||
|
}
|
||
|
// Write message count
|
||
|
this.writeBytes(this.encodeXInt(messages.length, 2));
|
||
|
// And actual messages
|
||
|
for (i in messages) {
|
||
|
this.writeAmfMessage(messages[i].targetUri, messages[i].responseUri, messages[i].body);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Write an AMF header to the byte array. AMF headers are always encoded in AMF0.
|
||
|
* @param {String} headerName the header name
|
||
|
* @param {Boolean} mustUnderstand true if the receiver must understand this header or else reject it, false otherwise
|
||
|
* @param {Object} value the value to serialize. Must be an object that can be serialized by AMF
|
||
|
* @private
|
||
|
*/
|
||
|
writeAmfHeader: function(headerName, mustUnderstand, value) {
|
||
|
//<debug>
|
||
|
if (this.config.format != 0) {
|
||
|
Ext.Error.raise ("Trying to write a header on an AMF3 Encoder. Only AMF0 is supported!");
|
||
|
}
|
||
|
if (!Ext.isString(headerName)) {
|
||
|
Ext.Error.raise("targetURI is not a string: " + targetUri);
|
||
|
}
|
||
|
if ((typeof(mustUnderstand) !== "boolean") && !Ext.isBoolean(mustUnderstand)) {
|
||
|
Ext.Error.raise("mustUnderstand is not a boolean value: " + mustUnderstand);
|
||
|
}
|
||
|
//</debug>
|
||
|
// write header name
|
||
|
this.write0ShortUtf8String(headerName);
|
||
|
// write must understand byte
|
||
|
var mu = mustUnderstand ? 0x01 : 0x00;
|
||
|
this.writeByte(mu);
|
||
|
// next write the header length of -1 (undetermined) to the stream
|
||
|
this.writeBytes(this.encodeXInt(-1, 4));
|
||
|
// write value
|
||
|
this.writeObject(value);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Writes an AMF message to the byte array. AMF messages are always encoded in AMF0.
|
||
|
* @param {String} targetUri the class / method to call
|
||
|
* @param {String} responseUri the response should call here
|
||
|
* @param {Array} body the parameters to pass to the called method, wrapped in an array
|
||
|
* @private
|
||
|
*/
|
||
|
writeAmfMessage: function(targetUri, responseUri, body) {
|
||
|
//<debug>
|
||
|
if (this.config.format != 0) {
|
||
|
Ext.Error.raise ("Trying to write a message on an AMF3 Encoder. Only AMF0 is supported!");
|
||
|
}
|
||
|
if (!Ext.isString(targetUri)) {
|
||
|
Ext.Error.raise("targetURI is not a string: " + targetUri);
|
||
|
}
|
||
|
if (!Ext.isString(responseUri)) {
|
||
|
Ext.Error.raise("targetURI is not a string: " + responseUri);
|
||
|
}
|
||
|
if (!Ext.isArray(body)) {
|
||
|
Ext.Error.raise("body is not an array: " + typeof(body));
|
||
|
}
|
||
|
//</debug>
|
||
|
// write target URI
|
||
|
this.write0ShortUtf8String(targetUri);
|
||
|
// write response URI
|
||
|
this.write0ShortUtf8String(responseUri);
|
||
|
// next write the message length of -1 (undetermined) to the stream
|
||
|
this.writeBytes(this.encodeXInt(-1, 4));
|
||
|
// write the paramters
|
||
|
this.write0StrictArray(body);
|
||
|
}
|
||
|
|
||
|
});
|