// @tag enterprise/** * @class Ext.data.amf.XmlDecoder * This class parses an XML-based AMFX message and returns the deserialized * objects. You should not need to use this class directly. It's mostly used by * the AMFX Direct implementation. * To decode a message, first construct a Decoder: * * decoder = Ext.create('Ext.data.amf.XmlDecoder'); * * Then ask it to read in the message : * * resp = decoder.readAmfxMessage(str); * * For more information on working with AMF data please refer to the * [AMF Guide](../guides/backend_connectors/amf.html). */Ext.define('Ext.data.amf.XmlDecoder', { alias: 'data.amf.xmldecoder', statics: { /** * Parses an xml string and returns an xml document * @private * @param {String} xml */ readXml: function(xml) { var doc; if (window.DOMParser) { doc = (new DOMParser()).parseFromString(xml, "text/xml"); } else { doc = new ActiveXObject("Microsoft.XMLDOM"); doc.loadXML(xml); } return doc; }, /** * parses a node containing a byte array in hexadecimal format, returning the reconstructed array. * @param {HTMLElement/XMLElement} node the node * @return {Array} a byte array */ readByteArray: function(node) { var bytes = [], c, i, str; str = node.firstChild.nodeValue; for (i = 0; i < str.length; i = i + 2) { c = str.substr(i, 2); bytes.push(parseInt(c, 16)); } return bytes; }, /** * Deserializes an AMF3 binary object from a byte array * @param {Array} bytes the byte array containing one AMF3-encoded value * @return {Object} the decoded value */ readAMF3Value: function(bytes) { var packet; packet = Ext.create('Ext.data.amf.Packet'); return packet.decodeValue(bytes); }, /** * Accepts Flex-style UID and decodes the number in the first four bytes (8 hex digits) of data. * @param {String} messageId the message ID * @return {Number} the transaction ID */ decodeTidFromFlexUID: function(messageId) { var str; str = messageId.substr(0,8); return parseInt(str, 16); } }, /** * Creates new encoder. * @param {Object} config Configuration options */ constructor: function(config) { this.initConfig(config); this.clear(); }, /** * Clears the accumulated data and reference tables */ clear: function() { // reset reference counters this.objectReferences=[]; this.traitsReferences=[]; this.stringReferences=[]; }, /** * Reads and returns a decoded AMFX packet. * @param {String} xml the xml of the message * @return {Object} the response object containing the message */ readAmfxMessage: function(xml) { var doc, amfx, body, i, resp={}; this.clear(); // reset counters doc = Ext.data.amf.XmlDecoder.readXml(xml); amfx = doc.getElementsByTagName('amfx')[0]; //<debug> if (!amfx) { Ext.warn.log("No AMFX tag in message"); } if (amfx.getAttribute('ver') != "3") { Ext.raise("Unsupported AMFX version: " + amfx.getAttribute('ver')); } //</debug> body = amfx.getElementsByTagName('body')[0]; resp.targetURI = body.getAttribute('targetURI'); resp.responseURI = body.getAttribute('responseURI'); // most likely empty string for (i = 0; i < body.childNodes.length; i++) { if (body.childNodes.item(i).nodeType != 1) { // only process element nodes, ignore white space and text nodes continue; } resp.message = this.readValue(body.childNodes.item(i)); break; // no need to keep iterating } return resp; }, /** * Parses an HTML element returning the appropriate JavaScript value from the AMFX data. * @param {HTMLElement} node The node to parse * @return {Object} a JavaScript object or value */ readValue: function(node) { var val; if (typeof node.normalize === 'function') { node.normalize(); } // 2DO: handle references! if (node.tagName == "null") { return null; } else if (node.tagName == "true") { return true; } else if (node.tagName == "false") { return false; } else if (node.tagName == "string") { return this.readString(node); } else if (node.tagName == "int") { return parseInt(node.firstChild.nodeValue); } else if (node.tagName == "double") { return parseFloat(node.firstChild.nodeValue); } else if (node.tagName == "date") { val = new Date(parseFloat(node.firstChild.nodeValue)); // record in object reference table this.objectReferences.push(val); return val; } else if (node.tagName == "dictionary") { return this.readDictionary(node); } else if (node.tagName == "array") { return this.readArray(node); } else if (node.tagName == "ref") { return this.readObjectRef(node); } else if (node.tagName == "object") { return this.readObject(node); } else if (node.tagName == "xml") { // the CDATA content of the node is a parseable XML document. parse it. return Ext.data.amf.XmlDecoder.readXml(node.firstChild.nodeValue); } else if (node.tagName == "bytearray") { // a byte array is usually an AMF stream. Parse it to a byte array, then pass through the AMF decoder to get the objects inside return Ext.data.amf.XmlDecoder.readAMF3Value(Ext.data.amf.XmlDecoder.readByteArray(node)); } //<debug> Ext.raise("Unknown tag type: " + node.tagName); //</debug> return null; }, /** * Reads a string or string reference and return the value * @param {HTMLElement/XMLElement} node the node containing a string object * @return {String} the parsed string */ readString: function(node) { var val; if (node.getAttributeNode('id')) { return this.stringReferences[parseInt(node.getAttribute('id'))]; } val = (node.firstChild ? node.firstChild.nodeValue : "") || ""; this.stringReferences.push(val); return val; }, /** * Parses and returns an ordered list of trait names * @param {HTMLElement/XMLElement} node the traits node from the XML doc * @return {Array} an array of ordered trait names or null if it's an externalizable object */ readTraits: function(node) { var traits = [], i, rawtraits; if (node === null) { return null; } if (node.getAttribute('externalizable') == "true") { // no traits since it's an externalizable or a null object. return null; } if (node.getAttributeNode('id')) { // return traits reference return this.traitsReferences[parseInt(node.getAttributeNode('id').value)]; } /* // empty anonymous objects still seem to get their empty traits in the reference table if (!node.hasChildNodes()) { var className = node.parentNode.getElementsByTagName('type'); if (className.length == 0) { return traits; // special case of an anonymous object with no traits. Does not get reference counted } } */ rawtraits = node.childNodes; for (i = 0; i < rawtraits.length; i++) { if (rawtraits.item(i).nodeType != 1) { // only process element nodes, ignore white space and text nodes continue; } // this will be a string, but let the readValue function handle it nonetheless traits.push(this.readValue(rawtraits.item(i))); } // register traits in ref table: this.traitsReferences.push(traits); return traits; }, /** * Parses and return an object / array / dictionary / date from reference * @param {HTMLElement/XMLElement} node the ref node * @return {Object} the previously instantiated object referred to by the ref node */ readObjectRef: function(node) { var id; id = parseInt(node.getAttribute('id')); return this.objectReferences[id]; }, /** * Parses and returns an AMFX object. * @param {HTMLElement/XMLElement} node the `<object>` node to parse * @return {Object} the deserialized object */ readObject: function(node) { var obj, traits = [], traitsNode, i, j, n, key, val, klass = null, className; className = node.getAttribute('type'); if (className) { klass = Ext.ClassManager.getByAlias('amfx.' + className); // check if special case for class } obj = klass ? new klass() : (className ? {$className: className} : {}); // if there is no klass, mark the classname for easier parsing of returned results // check if we need special handling for this class if ((!klass) && this.converters[className]) { obj = this.converters[className](this,node); return obj; // we're done } traitsNode = node.getElementsByTagName('traits')[0]; traits = this.readTraits(traitsNode); //<debug> if (traits === null) { Ext.raise("No support for externalizable object: " + className); } //</debug> // Register object if ref table, in case there's a cyclical reference coming this.objectReferences.push(obj); // Now we expect an item for each trait name we have. We assume it's an ordered list. We'll skip the first (traits) tag j = 0; for (i = 0; i < node.childNodes.length; i++) { n = node.childNodes.item(i); if (n.nodeType != 1) { // Ignore text nodes and non-element nodes continue; } if (n.tagName == "traits") { // ignore the traits node. We've already covered it. continue; } key = traits[j]; val = this.readValue(n); j = j + 1; obj[key] = val; //<debug> if (j > traits.length) { Ext.raise("Too many items for object, not enough traits: " + className); } //</debug> } return obj; }, /** * Parses and returns an AMFX array. * @param {HTMLElement/XMLElement} node the array node * @return {Array} the deserialized array */ readArray: function(node) { var arr=[], n,i,j,l,name, val, len, childnodes, cn; // register array in object references table before we parse, in case of circular references this.objectReferences.push(arr); len = parseInt(node.getAttributeNode('length').value); i = 0; // the length only accounts for the ordinal values. For the rest, we'll read them as ECMA key-value pairs for (l = 0; l < node.childNodes.length; l++) { n = node.childNodes.item(l); if (n.nodeType != 1) { // Ignore text nodes and non-element nodes continue; } if (n.tagName == "item") { // parse item node name = n.getAttributeNode('name').value; childnodes = n.childNodes; for (j = 0; j < childnodes.length; j++) { cn = childnodes.item(j); if (cn.nodeType != 1) { // Ignore text nodes and non-element nodes continue; } val = this.readValue(cn); break; // out of loop. We've found our value } arr[name] = val; } else { // ordinal node arr[i] = this.readValue(n); i++; //<debug> if (i > len) { Ext.raise("Array has more items than declared length: " + i + " > " + len); } //</debug> } } //<debug> if (i < len) { Ext.raise("Array has less items than declared length: " + i + " < " + len); } //</debug> return arr; }, /** * Parses and returns an AMFX dictionary. * @param {HTMLElement/XMLElement} node the `<dictionary>` node * @return {Object} a javascript object with the dictionary value-pair elements */ readDictionary: function(node) { // For now, handle regular objects var dict = {}, key, val, i, j, n, len; len = parseInt(node.getAttribute('length')); // Register dictionary in the ref table, in case there's a cyclical reference coming this.objectReferences.push(dict); // now find pairs of keys and values key = null; val = null; j = 0; for (i = 0; i < node.childNodes.length; i++) { n = node.childNodes.item(i); if (n.nodeType != 1) { // Ignore text nodes and non-element nodes continue; } if (!key) { key = this.readValue(n); continue; // next element is the value } val = this.readValue(n); j = j + 1; dict[key] = val; key = null; val = null; } //<debug> if (j != len) { Ext.raise("Incorrect number of dictionary values: " + j + " != " + len); } //</debug> return dict; }, /** * Converts externalizable flex objects with a source array to a regular array. * @private */ convertObjectWithSourceField: function(node) { var i, n, val; for (i = 0; i < node.childNodes.length; i++) { n = node.childNodes.item(i); if (n.tagName == "bytearray") { val = this.readValue(n); this.objectReferences.push(val); return val; } } return null; // we shouldn't reach here, but just in case }, /** * Converters used in converting specific typed Flex classes to JavaScript usable form. * @private */ converters: { 'flex.messaging.io.ArrayCollection': function(decoder,node) { return decoder.convertObjectWithSourceField(node); }, 'mx.collections.ArrayList': function(decoder,node) { return decoder.convertObjectWithSourceField(node); }, 'mx.collections.ArrayCollection': function(decoder,node) { return decoder.convertObjectWithSourceField(node); } }});