// @tag enterprise/** * @class Ext.data.amf.XmlEncoder * This class serializes data in the Action Message Format XML (AMFX) format. * It can write simple and complex objects, to be used in conjunction with an * AMFX-compliant server. * To create an encoded XMl, first construct an Encoder: * * var encoder = Ext.create('Ext.data.amf.XmlEncoder'); * * Then use the writer methods to out data to the : * * encoder.writeObject(1); * encoder.writeObject({a: "b"}); * * And access the data through the #bytes property: * encoder.body; * * You can also reset the class to start a new body: * * encoder.clear(); * * Current limitations: * AMF3 format (format:3) * - Each object is written out explicitly, not using the reference tables * supported by the AMFX format. This means the function does NOT support * circular reference objects. * - Objects that aren't Arrays, Dates, Strings, Document (XML) or primitive * values will be written out as anonymous objects with dynamic data. * - If the object has a $flexType field, that field will be used in signifying * the object-type as an attribute, instead of being passed as 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. * * For more information on working with AMF data please refer to the * [AMF Guide](../guides/backend_connectors/amf.html). */ Ext.define('Ext.data.amf.XmlEncoder', { alias: 'data.amf.xmlencoder', /** * @property {String} body - The output string */ body: "", statics: { /** * Utility function to generate a flex-friendly UID * @param {Number} id used in the first 8 chars of the id. If not provided, a random number will be used. * @return {String} a string-encoded opaque UID */ generateFlexUID: function(id) { var uid = "", i, j, t; if (id === undefined) { id = Ext.Number.randomInt(0, 0xffffffff); } // The format of a UID is XXXXXXXX-XXXX-XXXX-XXXX-YYYYYYYYXXXX // where each X is a random hex digit and each Y is a hex digit from the least significant part of a time stamp. t = (id + 0x100000000).toString(16).toUpperCase(); // padded uid = t.substr(t.length - 8, 8); // last 8 chars for (j = 0; j < 3; j++) { // 3 -XXXX segments uid += "-"; for (i = 0; i < 4; i++) { uid += Ext.Number.randomInt(0, 15).toString(16).toUpperCase(); } } uid += "-"; // add timestamp t = new Number(new Date()).valueOf().toString(16).toUpperCase(); // get the String representation of milliseconds in hex format j = 0; if (t.length < 8) { // pad with "0" if needed for (i = 0; i < t.length - 8; i++) { j++; uid += "0"; } } // actual timestamp: uid += t.substr(-(8-j)); // last few chars // and last 4 random digits for (i = 0; i < 4; i++) { uid += Ext.Number.randomInt(0, 15).toString(16).toUpperCase(); } return uid; } }, /** * Creates new encoder. * @param {Object} config Configuration options */ constructor: function(config) { this.initConfig(config); this.clear(); }, /** * Clears the accumulated data, starting with an empty string */ clear: function() { this.body = ""; }, /** * Returns the encoding for undefined (which is the same as the encoding for null) */ encodeUndefined: function() { return this.encodeNull(); }, /** * Writes the undefined value to the string */ writeUndefined: function() { this.write(this.encodeUndefined()); }, /** * Returns the encoding for null */ encodeNull: function() { return "<null />"; }, /** * Writes the null value to the string */ writeNull: function() { this.write(this.encodeNull()); }, /** * Returns an encoded boolean * @param {Boolean} val a boolean value */ encodeBoolean: function(val) { var str; if (val) { str = "<true />"; } else { str = "<false />"; } return str; }, /** * Writes a boolean value to the string * @param {Boolean} val a boolean value */ writeBoolean: function(val) { this.write(this.encodeBoolean(val)); }, /** * Returns an encoded string * @param {String} str the string to encode */ encodeString: function(str) { var ret; if (str === "") { ret = "<string />"; } else { ret ="<string>"+str+"</string>"; } return ret; }, /** * Writes a string tag with the string content. * @param {String} str the string to encode */ writeString: function(str) { this.write(this.encodeString(str)); }, /** * Returns an encoded int * @param {Number} num the integer to encode */ encodeInt: function(num) { return "<int>" + num.toString() + "</int>"; }, /** * Writes a int tag with the content. * @param {Number} num the integer to encode */ writeInt: function(num) { this.write(this.encodeInt(num)); }, /** * Returns an encoded double * @param {Number} num the double to encode */ encodeDouble: function(num) { return "<double>" + num.toString() + "</double>"; }, /** * Writes a double tag with the content. * @param {Number} num the double to encode */ writeDouble: function(num) { this.write(this.encodeDouble(num)); }, /** * Returns an encoded number. Decides wheter to use int or double encoding. * @param {Number} num the number to encode */ encodeNumber: function(num) { var maxInt = 0x1fffffff, minSignedInt = -0xfffffff; //<debug> if (typeof(num) !== "number" && !(num instanceof Number)) { Ext.log.warn("Encoder: writeNumber argument is not numeric. Can't coerce."); } // </debug> // switch to the primitive value for handling: if (num instanceof Number) { num = num.valueOf(); } // Determine if this is an integer or a float. if (num % 1 === 0 && num >= minSignedInt && num <= maxInt) { // The number has no decimal point and is within bounds. Let's encode it. return this.encodeInt(num); } else { return this.encodeDouble(num); } }, /** * Writes a number, deciding if to use int or double as the tag * @param {Number} num the number to encode */ writeNumber: function(num) { this.write(this.encodeNumber(num)); }, /** * Encode a date * @param {Date} date the date to encode */ encodeDate: function(date) { return "<date>" + (new Number(date)).toString() + "</date>"; }, /** * Write a date to the string * @param {Date} date the date to encode */ writeDate: function(date) { this.write(this.encodeDate(date)); }, /** * @private * Encodes one ECMA array element * @param {String} key the name of the element * @param {Object} value the value of the element * @return {String} the encoded key-value pair */ encodeEcmaElement: function(key, value) { var str = '<item name="' + key.toString() + '">' + this.encodeObject(value) + '</item>'; return str; }, /** * Encodes an array, marking it as an ECMA array if it has associative (non-ordinal) indices * @param {Array} array the array to encode */ encodeArray: function(array) { var ordinals=[], firstNonOrdinal, ecmaElements=[], length = array.length, // length is of ordinal section only i, str; for (i in array) { if (Ext.isNumeric(i) && (i % 1 == 0)) { //this is an integer. Add to ordinals array ordinals[i] = this.encodeObject(array[i]); } else { ecmaElements.push(this.encodeEcmaElement(i, array[i])); } } firstNonOrdinal=ordinals.length; // now, check if we have consecutive numbers in the ordinals array for (i = 0; i < ordinals.length; i++) { if (ordinals[i] === undefined) { // we have a gap in the array. Mark it - the rest of the items become ECMA elements firstNonOrdinal = i; break; } } if (firstNonOrdinal < ordinals.length) { // transfer some of the elements to the ecma array for (i = firstNonOrdinals; i < ordinals.length; i++) { if (ordinals[i] !== undefined) { ecmaElements.push(this.encodeEcmaElement(i, ordinals[i])); } } ordinals = ordinals.slice(0, firstNonOrdinal); } // finally start constructing the string str = '<array length="' + ordinals.length + '"'; if (ecmaElements.length > 0) { str += ' ecma="true"'; } str += '>'; // first add the oridnals in consecutive order: for (i = 0; i < ordinals.length; i++) { // iterate by counting since we need to guarantee the order str += ordinals[i]; } // Now add ECMA items for (i in ecmaElements) { str += ecmaElements[i]; } // And close the array: str += '</array>'; return str; }, /** * Writes an array to the string, marking it as an ECMA array if it has associative (non-ordinal) indices * @param {Array} array the array to encode */ writeArray: function(array) { this.write(this.encodeArray(array)); }, /** * Encodes an xml document into a CDATA section * @param {XMLElement/HTMLElement} xml an XML document or element (Document type in some browsers) */ encodeXml: function(xml) { var str = this.convertXmlToString(xml); return "<xml><![CDATA[" + str + "]]></xml>"; }, /** * Write an XML document to the string * @param {XMLElement/HTMLElement} xml an XML document or element (Document type in some browsers) */ writeXml: function(xml) { this.write(this.encodeXml(xml)); }, /** * Encodes a generic object into AMFX format. If a <tt>$flexType</tt> member is defined, list that as the object type. * @param {Object} obj the object to encode * @return {String} the encoded text */ encodeGenericObject: function(obj) { var traits = [], values = [], flexType = null, i, str; for (i in obj) { if (i == "$flexType") { flexType = obj[i]; } else { traits.push(this.encodeString(new String(i))); values.push(this.encodeObject(obj[i])); } } if (flexType) { str = '<object type="' +flexType + '">'; } else { str="<object>"; } if (traits.length > 0) { str += "<traits>"; str += traits.join(""); str += "</traits>"; } else { str += "<traits />"; } str += values.join(""); str += "</object>"; return str; }, /** * Writes a generic object to the string. If a <tt>$flexType</tt> member is defined, list that as the object type. * @param {Object} obj the object to encode */ writeGenericObject: function(obj) { this.write(this.encodeGenericObject(obj)); }, /** * Encodes a byte arrat in AMFX format * @param {Array} array the byte array to encode */ encodeByteArray: function(array) { var str, i, h; if (array.length > 0) { str = "<bytearray>"; for (i = 0; i < array.length; i++) { //<debug> if (!Ext.isNumber(array[i])) { Ext.raise("Byte array contains a non-number: " + array[i] + " in index: " + i); } if (array[i] < 0 || array[i] > 255) { Ext.raise("Byte array value out of bounds: " + array[i]); } //</debug> h = array[i].toString(16).toUpperCase(); if (array[i] < 0x10) { h = "0" + h; } str += h; } str += "</bytearray>"; } else { str = "<bytearray />"; } return str; }, /** * Writes an AMFX byte array to the string. This is for convenience only and is not called automatically by writeObject. * @param {Array} array the byte array to encode */ writeByteArray: function(array) { this.write(this.encodeByteArray(array)); }, /** * encode the appropriate data item. Supported types: * - undefined * - null * - boolean * - integer * - double * - UTF-8 string * - XML Document (identified by being instaneof Document. Can be generated with: new DOMParser()).parseFromString(xml, "text/xml"); * - Date * - Array * - Generic object * @param {Object} item A primitive or object to write to the stream * @return {String} the encoded object in AMFX format */ encodeObject: function(item) { var t = typeof(item); //Ext.log("Writing " + item + " of type " + t); if (t === "undefined") { return this.encodeUndefined(); } else if (item === null) { // can't check type since typeof(null) returns "object" return this.encodeNull(); } else if (Ext.isBoolean(item)) { return this.encodeBoolean(item); } else if (Ext.isString(item)) { return this.encodeString(item); } else if (t === "number" || item instanceof Number) { // Can't use Ext.isNumeric since it accepts strings as well return this.encodeNumber(item); } else if (t === "object") { // Figure out which object this is if (item instanceof Date) { return this.encodeDate(item); } else if (Ext.isArray(item)) { return this.encodeArray(item); } else if (this.isXmlDocument(item)) { return this.encodeXml(item); } else { // Treat this as a generic object with name/value pairs of data. return this.encodeGenericObject(item); } } else { //<debug> Ext.log.warn("AMFX Encoder: Unknown item type " + t + " can't be written to stream: " + item); //</debug> } return null; // if we reached here, return null }, /** * Writes the appropriate data item to the string. Supported types: * - undefined * - null * - boolean * - integer * - double * - UTF-8 string * - XML Document (identified by being instaneof Document. Can be generated with: new DOMParser()).parseFromString(xml, "text/xml"); * - Date * - Array * - Generic object * @param {Object} item A primitive or object to write to the stream */ writeObject: function(item) { this.write(this.encodeObject(item)); }, /** * Encodes an AMFX remoting message with the AMFX envelope. * @param {Ext.data.amf.RemotingMessage} message the message to pass on to serialize. */ encodeAmfxRemotingPacket: function(message) { var msg, str; str = '<amfx ver="3" xmlns="http://www.macromedia.com/2005/amfx"><body>'; str += message.encodeMessage(); str += '</body></amfx>'; return str; }, /** * Writes an AMFX remoting message with the AMFX envelope to the string. * @param {Ext.data.amf.RemotingMessage} message the message to pass on to serialize. */ writeAmfxRemotingPacket: function(message) { this.write(this.encodeAmfxRemotingPacket(message)); }, /** * Converts an XML Document object to a string. * @param {Object} xml XML document to convert (typically Document object) * @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; }, /** * Appends a string to the body of the message * @param {String} str the string to append * @private */ write: function(str) { this.body += str; }});