//////////////////////////////////////////////////////////////////////////////// // // ADOBE SYSTEMS INCORPORATED // Copyright 2005-2007 Adobe Systems Incorporated // All Rights Reserved. // // NOTICE: Adobe permits you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// package mx.rpc.soap { import flash.utils.getTimer; import flash.xml.XMLDocument; import flash.xml.XMLNode; import mx.collections.IList; import mx.core.mx_internal; import mx.logging.ILogger; import mx.logging.Log; import mx.rpc.soap.types.ICustomSOAPType; import mx.rpc.wsdl.WSDLMessagePart; import mx.rpc.wsdl.WSDLConstants; import mx.rpc.wsdl.WSDLEncoding; import mx.rpc.wsdl.WSDLOperation; import mx.rpc.xml.ContentProxy; import mx.rpc.xml.DecodingContext; import mx.rpc.xml.SchemaConstants; import mx.rpc.xml.SchemaDatatypes; import mx.rpc.xml.TypeIterator; import mx.rpc.xml.XMLDecoder; import mx.utils.ObjectProxy; import mx.utils.object_proxy; import mx.utils.StringUtil; import mx.utils.XMLUtil; use namespace object_proxy; [ExcludeClass] /** * Decodes the SOAP response for a particular operation * * @private */ public class SOAPDecoder extends XMLDecoder implements ISOAPDecoder { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- public function SOAPDecoder() { super(); log = Log.getLogger("mx.rpc.soap.SOAPDecoder"); } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * Controls whether the decoder supports the legacy literal style encoding * for generic compound type (such as arrays). Older document-literal SOAP * implementations sometimes encoded unbounded element sequences with * generic child item elements instead of repeating the * value element itself. The default is true. */ public var supportGenericCompoundTypes:Boolean = false; //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- public function get forcePartArrays():Boolean { return _forcePartArrays; } public function set forcePartArrays(value:Boolean):void { _forcePartArrays = value; } public function get headerFormat():String { return _headerFormat; } public function set headerFormat(value:String):void { _headerFormat = value; } /** * Determines whether the decoder should ignore whitespace when processing * the XML of a SOAP encoded response. The default is true * and thus whitespace is not preserved. If an XML Schema type definition * specifies a whiteSpace restriction set to * preserve then ignoreWhitespace must first be set to false. * Conversely, if a type whiteSpace restriction is set to * replace or collapse then that setting will * be honored even if ignoreWhitespace is set to false. */ public function get ignoreWhitespace():Boolean { return _ignoreWhitespace; } public function set ignoreWhitespace(value:Boolean):void { _ignoreWhitespace = value; } public function get multiplePartsFormat():String { return _multiplePartsFormat; } public function set multiplePartsFormat(value:String):void { _multiplePartsFormat = value; } public function get resultFormat():String { return _resultFormat; } public function set resultFormat(value:String):void { _resultFormat = value; } public function get schemaConstants():SchemaConstants { return schemaManager.schemaConstants; } public function get soapConstants():SOAPConstants { return wsdlOperation.soapConstants; } public function get wsdlOperation():WSDLOperation { return _wsdlOperation; } public function set wsdlOperation(value:WSDLOperation):void { _wsdlOperation = value; schemaManager = _wsdlOperation.schemaManager; } /** * @private */ protected function get inputEncoding():WSDLEncoding { var encoding:WSDLEncoding; if (_wsdlOperation.inputMessage != null) encoding = _wsdlOperation.inputMessage.encoding; else encoding = new WSDLEncoding(); return encoding; } /** * @private */ protected function get outputEncoding():WSDLEncoding { var encoding:WSDLEncoding; if (_wsdlOperation.outputMessage != null) encoding = _wsdlOperation.outputMessage.encoding; else encoding = new WSDLEncoding(); return encoding; } //-------------------------------------------------------------------------- // // Methods - SOAP Decoding // //-------------------------------------------------------------------------- /** * Decodes a SOAP response into a result and headers. */ public function decodeResponse(response:*):SOAPResult { var soapResult:SOAPResult; var responseString:String; if (response is XML) responseString = XML(response).toXMLString(); else responseString = String(response); var startTime:int = getTimer(); log.info("Decoding SOAP response"); // Reset the decoder state to clear contexts for schema types, etc. reset(); if (responseString != null) { log.debug("Encoded SOAP response {0}", responseString); // Keep track of the previous ignoreWhitespace setting (as it is // unfortunately a static API on the intrinsic XML type) so we // can set it back to its original state once we're finished. var oldIgnoreWhitespace:Boolean = XML.ignoreWhitespace; try { // Work around Flash Player bug 192355 by removing whitespace // between processing instructions and the root tag before // constructing an XML instance of the SOAP response. responseString = responseString.replace(PI_WHITESPACE_PATTERN, "?><"); responseString = StringUtil.trim(responseString); XML.ignoreWhitespace = ignoreWhitespace; var responseXML:XML = new XML(responseString); soapResult = decodeEnvelope(responseXML); } finally { XML.ignoreWhitespace = oldIgnoreWhitespace; } } log.info("Decoded SOAP response into result [{0} millis]", getTimer() - startTime); return soapResult } protected function decodeEnvelope(responseXML:XML):SOAPResult { log.debug("Decoding SOAP response envelope"); var soapResult:SOAPResult = new SOAPResult(); var envNS:Namespace; if (responseXML != null) { envNS = responseXML.namespace(); } if (envNS == null) { throw new Error("SOAP Response cannot be decoded. Raw response: " + responseXML); } else if (envNS.uri != SOAPConstants.SOAP_ENVELOPE_URI) { throw new Error("SOAP Response Version Mismatch"); } else { // Set the namespaces and the Schema uri for the decoder. var schemaConst:SchemaConstants; var nsArray:Array = responseXML.inScopeNamespaces(); for each (var ns:Namespace in nsArray) { schemaManager.namespaces[ns.prefix] = ns; } // SOAP Headers var headerXML:XML = responseXML[soapConstants.headerQName][0]; if (headerXML != null) { soapResult.headers = decodeHeaders(headerXML); } // SOAP Body var bodyXML:XML = responseXML[soapConstants.bodyQName][0]; if (bodyXML == null || bodyXML.hasComplexContent() == false || bodyXML.children().length() <= 0) { soapResult.result = undefined; } else { // Check for SOAP faults for all resultFormats var faultXMLList:XMLList = bodyXML[soapConstants.faultQName]; if (faultXMLList.length() > 0) { soapResult.isFault = true; soapResult.result = decodeFaults(faultXMLList); } else { if (resultFormat == "object") { decodeBody(bodyXML, soapResult); } else if (resultFormat == "e4x") { // Return the children as an XMLList. soapResult.result = bodyXML.children(); } else if (resultFormat == "xml") { // Return the children as an Array of XMLNode // or String for text children var bodyArray:Array = []; var bodyXMLList:XMLList = bodyXML.children(); for each (var bodyChild:XML in bodyXMLList) { var nodeKind:String = bodyChild.nodeKind(); if (nodeKind == "element") { var xmlDoc:XMLDocument = new XMLDocument(bodyChild.toString()); var xmlNode:XMLNode = xmlDoc.firstChild; bodyArray.push(xmlNode); } else if (nodeKind == "text") { bodyArray.push(bodyChild.toString()); } } soapResult.result = bodyArray; } } } } return soapResult; } /** * Decodes the response SOAP Body. The contents may either be the encoded * output parameters, or a collection of SOAP faults. */ protected function decodeBody(bodyXML:XML, soapResult:SOAPResult):void { log.debug("Decoding SOAP response body"); var result:*; document = bodyXML; // Pre-process encoded body. preProcessXML(bodyXML); // Check for operations without an output message if (wsdlOperation.outputMessage == null) { soapResult.result = undefined; return; } // Decode WSDL output message parts var parts:Array = wsdlOperation.outputMessage.parts; // Check for operations with a void return type if (parts == null || parts.length == 0) { soapResult.result = undefined; return; } var outputMessageXML:XML = bodyXML; if (wsdlOperation.style == SOAPConstants.RPC_STYLE) { // Unwrap the output message from the operation; both RPC encoded and literal wrap. outputMessageXML = outputMessageXML.elements()[0]; } else if (outputEncoding.useStyle == SOAPConstants.USE_LITERAL && wsdlOperation.outputMessage.isWrapped == true) { // Unwrap the output message only if this is wrapped literal. outputMessageXML = outputMessageXML.elements()[0]; } // The "literal" use style may define a part using a type or // element, the "encoded" use style always defines a part with a // type. for each (var part:WSDLMessagePart in parts) { var encodedPartValues:XMLList; var encodedPartValue:XML; var decodedPart:*; var partQName:QName; var partType:QName; var partDefinition:XML; // If we have an element, use that to find the part if (part.element != null) { if (outputMessageXML.hasComplexContent()) encodedPartValues = outputMessageXML.elements(part.element); else encodedPartValues = outputMessageXML.text(); partQName = part.element; partType = null; } // Otherwise, find the part by name and decode using the // specified type definition else { partType = part.type; partDefinition = part.definition; if (outputMessageXML.hasComplexContent()) { if (outputEncoding.useStyle == SOAPConstants.USE_ENCODED) { // First, look for the part with an unqualified name partQName = new QName("", part.name.localName); encodedPartValues = outputMessageXML.elements(partQName); if (encodedPartValues.length() == 0) { // HACK: Sometimes the soap:body namespace attribute // is used to qualify parts under the operation // wrapper, so we then look with this in mind... var encodedNamespace:String = outputEncoding.namespaceURI; partQName = new QName(encodedNamespace, part.name.localName); encodedPartValues = outputMessageXML.elements(partQName); // HACK: Sometimes the inputEncoding soap:body // namespace attribute is incorrectly used // for the output message parts too... if (encodedPartValues.length() == 0) { encodedNamespace = inputEncoding.namespaceURI; partQName = new QName(encodedNamespace, part.name.localName); encodedPartValues = outputMessageXML.elements(partQName); } } } else { encodedPartValues = outputMessageXML.elements(part.name); } } else { encodedPartValues = outputMessageXML.text(); } } for each (encodedPartValue in encodedPartValues) { decodedPart = decode(encodedPartValue, partQName, partType, partDefinition); // Handle multiple output parts separately... if (parts.length > 1) { // Map multiple parts to named properties on the result object if (multiplePartsFormat == "object") { // Create the result object, if not created already // (this is the first part we have seen so far) if (result == null) { // The QName of the element that contains the part // values is used to look up a registered AS type // for the result object. For RPC operations the // QName will be the full operation name. For Doc/Lit // wrapped the QName will be the same as the // outputMessage.wrappedQName. For Doc/Lit bare // the QName will be soap:Body. result = createContent(outputMessageXML.name()); result.isSimple = false; } if (result[part.name.localName] == null) { // We need to create an array for this part's values // if there are > 1 encodedPartValues, or we are // forcingPartArrays for parts defined with maxOccurs>1 // regardless of number of values. var partMaxOccurs:uint = getMaxOccurs(partDefinition); if ((partMaxOccurs > 1 && forcePartArrays) || encodedPartValues.length() > 1) { result[part.name.localName] = createIterableValue(part.type); } } // If we have created an iterable container, this part // value needs to be pushed on it if (TypeIterator.isIterable(result[part.name.localName])) TypeIterator.push(result[part.name.localName], decodedPart); // Otherwise just assign the single value to the named property. else result[part.name.localName] = decodedPart; } else if (multiplePartsFormat == "array") { // If multiplePartsFormat == "array", we return each part // as an element in an Array (or some registered collection) if (result == null) { // If this is the first part/value, we create the // array (or typed collection based on the container // element's QName) result = createIterableValue(outputMessageXML.name()); } TypeIterator.push(result, decodedPart); } } else { // Single output part. Create result object if not created. if (result == null) { // The result object with only one part becomes the part // itself. The type of the result object is the type of // the part (if the part specifies a type). var sinlgePartResultType:QName = partType; // If the part specifies an element, the type of the result // object is looked up based on the element QName. if (sinlgePartResultType == null) sinlgePartResultType = part.element; // If neither a type, nor element is specified (rare case // where part becomes anyType), the part.name is used to // look up a strong type for the result object. if (sinlgePartResultType == null) sinlgePartResultType = part.name; var singlePartMaxOccurs:uint = getMaxOccurs(partDefinition); if ((singlePartMaxOccurs > 1 && forcePartArrays) || encodedPartValues.length() > 1) { // If more than one value was returned for a single // output part, or the part is defined with maxOccurs > 1, // we treat it as an array of values. The appropriate // IList class is created based on the singlePartResultType result = createIterableValue(sinlgePartResultType); } else { // decodedPart will already be an instance of the // required strong type (or ObjectProxy or Object). // If the single part has a single value, the result // object itself becomes that value, so we only need // a content proxy here. result = createContent(); } } if (TypeIterator.isIterable(result)) { // Push multiple values to the iterable result TypeIterator.push(result, decodedPart); } else { result = decodedPart; } } } } // If necessary, unwrap result from its proxy wrapper if (result is ContentProxy) result = ContentProxy(result).object_proxy::content; soapResult.result = result; } /** * Decodes a SOAP 1.1. Fault. * * FIXME: We need to add SOAP 1.2 Fault support which is very different * from SOAP 1.1. */ protected function decodeFaults(faultsXMLList:XMLList):Array { log.debug("SOAP: Decoding SOAP response fault"); var faults:Array = []; for each (var faultXML:XML in faultsXMLList) { var code:QName; var string:String; var detail:String; var element:XML = faultXML; var actor:String; var faultProperties:XMLList = faultXML.children(); for each (var child:XML in faultProperties) { if (child.localName() == "faultcode") { code = schemaManager.getQNameForPrefixedName(child.toString(), child); } else if (child.localName() == "faultstring") { string = child.toString(); } else if (child.localName() == "faultactor") { actor = child.toString(); } else if (child.localName() == "detail") { if (child.hasComplexContent()) { detail = child.children().toXMLString(); } else { detail = child.toString(); } } } var fault:SOAPFault = new SOAPFault(code, string, detail, element, actor); faults.push(fault); } return faults; } protected function decodeHeaders(headerXML:XML):Array { log.debug("Decoding SOAP response headers"); var headers:Array = []; var headerXMLList:XMLList = headerXML.elements(); for each (var headerChild:XML in headerXMLList) { if (headerFormat == "object") { var xsiType:QName = getXSIType(headerChild); var definition:XML = null; var headerContent:Object = null; // Check for xsi:type on the header if (xsiType != null) definition = schemaManager.getNamedDefinition(xsiType, constants.complexTypeQName, constants.simpleTypeQName); // We found a definition for the type. if (definition != null) { // Release scope, since we were only checking if definition exists. schemaManager.releaseScope(); headerContent = decode(headerChild, null, xsiType); } else { // We don't have a type definition. Attempt to find an element // definition for the QName of the header. If there is none, // decode() will fall back to anyType. headerContent = decode(headerChild, headerChild.name()); } // Create the SOAPHeader wrapper. var headerObject:SOAPHeader = new SOAPHeader(headerChild.name(), headerContent); // decode mustUnderstand attribute var muValue:String = XMLUtil.getAttributeByQName(headerChild, soapConstants.mustUnderstandQName).toString(); if (muValue == "1") headerObject.mustUnderstand = true; // decode actor attribute var actValue:String = XMLUtil.getAttributeByQName(headerChild, soapConstants.actorQName).toString(); if (actValue != "") headerObject.role = actValue; headers.push(headerObject); } else if (headerFormat == "e4x") { headers.push(headerChild); } else if (headerFormat == "xml") { headers.push(new XMLDocument(headerChild.toString())); } } return headers; } //-------------------------------------------------------------------------- // // Methods - XML Decoding // //-------------------------------------------------------------------------- /** * @private */ override public function decodeComplexType(definition:XML, parent:*, name:QName, value:*, restriction:XML=null, context:DecodingContext=null):void { if (value is XML) { var valXML:XML = value as XML; if (valXML.elements(SOAPConstants.diffgramQName).length() > 0 && valXML.elements(schemaConstants.schemaQName).length() > 0) { // If we have XML with the elements of a .NET DataSet, we // short-circuit to decodeType, which will call the special // decode function for the DataSetType decodeType(SOAPConstants.diffgramQName, parent, valXML.name(), value); return; } } // If the value provided is not XML, or doesn't have the elements // of a .NET DataSet, we just call super. super.decodeComplexType(definition, parent, name, value, restriction, context); } /** * @private */ override public function decodeType(type:QName, parent:*, name:QName, value:*, restriction:XML = null):void { // SOAP encoding specifies a type directly on the value and we // can usually use it unless it is a type that is not built-in // nor has a schema type definition. The value's specific type is // retained as originalType in case we need to fall back to using it. var originalType:QName = type; var xsiType:QName = getXSIType(value); if (xsiType != null) type = xsiType; // HACK: If encoded, translate simple SOAP types to XSD types. if (outputEncoding.useStyle == SOAPConstants.USE_ENCODED) { // FIXME: This should be managed by the schemaManager's unmarshaller if (SOAPConstants.isSOAPEncodedType(type)) { var datatypes:SchemaDatatypes = schemaManager.schemaDatatypes; if (type == soapConstants.soapBase64QName) { type = datatypes.base64BinaryQName; } else { var localName:String = type.localName; if (localName != "Array" && localName != "arrayType") { type = schemaConstants.getQName(localName); } } } } // Look for a custom SOAP type to handle the decoding var customType:ICustomSOAPType = SOAPConstants.getCustomSOAPType(type); if (customType != null) { customType.decode(this, parent, name, value, restriction); setXSIType(parent, type); } else { // We didn't do custom SOAP type decoding, so we need to delegate // but we need to pass a valid type to the base processing routine. var constants:SchemaConstants = schemaManager.schemaConstants; if (isBuiltInType(type)) { super.decodeType(type, parent, name, value, restriction); } else { var definition:XML = schemaManager.getNamedDefinition(type, constants.complexTypeQName, constants.simpleTypeQName, constants.elementTypeQName); if (definition != null) { // We're done with this definition; just needed to see if // we had it so we release the scope. schemaManager.releaseScope(); super.decodeType(type, parent, name, value, restriction); } else { // We don't have a type def for the value's specific // xsi type, so fall back to the default type passed in. super.decodeType(originalType, parent, name, value, restriction); } } } } /** * This override intercepts dencoding a complexType with complexContent based * on a SOAP encoded Array. This awkward approach to Array type definitions * was popular in WSDL 1.1 rpc-encoded operations and is a special case that * needs to be handled, but note it violates the WS-I Basic Profile 1.0. * * @private */ override public function decodeComplexRestriction(restriction:XML, parent:*, name:QName, value:*):void { // Handle as a special case var schemaConstants:SchemaConstants = schemaManager.schemaConstants; var baseName:String = restriction.@base; var baseQName:QName = schemaManager.getQNameForPrefixedName(baseName, restriction); if (baseQName == soapConstants.soapencArrayQName) { var customType:ICustomSOAPType = SOAPConstants.getCustomSOAPType(baseQName); if (customType != null) { customType.decode(this, parent, name, value, restriction); return; } } super.decodeComplexRestriction(restriction, parent, name, value); } override public function reset():void { super.reset(); _referencesResolved = false; _elementsWithId = null; } /** * Overrides XMLDecoder.parseValue to allow us to detect a legacy case * of literal style encoding where by generically encoded compound types * (such as arrays) had entries encoded with multiple child * item elements (instead of matching the correct schema * definition of just repeating the value node). * * @private */ override protected function parseValue(name:*, value:XMLList):* { if (supportGenericCompoundTypes && outputEncoding.useStyle == SOAPConstants.USE_LITERAL && value.length() > 0) { // Look for child elements as direct descendents of the value var itemQName:QName = new QName(value[0].name().uri, "item"); var items:XMLList = value.elements(itemQName); if (items.length() > 0) value = items; } return super.parseValue(name, value); } /** * Overrides XMLDecoder.preProcessXML to allow us to handle multi-ref SOAP * encoding. * @private */ override protected function preProcessXML(root:XML):void { // Only RPC/encoded uses multi-ref encoding. if (outputEncoding.useStyle == SOAPConstants.USE_ENCODED) resolveReferences(root); } /** * Resolves multi-refs in rpc/encoded. Substitutes each reference by its * referent node. */ private function resolveReferences(root:XML, cleanupElementsWithIdCache:Boolean=true):void { if (_referencesResolved) return; var index:uint = 0; if (_elementsWithId == null) _elementsWithId = document..*.(attribute("id").length() > 0); // Note that we must consider all child nodes here, not just elements // as we need the accurate index in terms of child XML nodes to replace // a node with the referent. for each (var child:XML in root.children()) { if (child.nodeKind() == "element") { var element:XML = child; var href:String = getAttributeFromNode("href", element); if (href != null) { var hashPosition:int = href.indexOf("#"); if (hashPosition >= 0) href = href.substring(hashPosition + 1); // Find the first element with a matching id attribute var matches:XMLList = _elementsWithId.(@id == href); var referent:XML; if (matches.length() > 0) referent = matches[0]; else throw new Error("The element referenced by id '" + href + "' was not found."); referent.setName(element.name()); if (referent.hasComplexContent()) resolveReferences(referent, false); root.replace(index, referent); } else if (element.hasComplexContent()) { resolveReferences(element, false); } } index++; } if (cleanupElementsWithIdCache) { _elementsWithId = null; // At this point all references have been resolved. _referencesResolved = true; } } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- private var log:ILogger; private var _elementsWithId:XMLList; private var _forcePartArrays:Boolean; private var _headerFormat:String; private var _ignoreWhitespace:Boolean = true; private var _multiplePartsFormat:String; private var _referencesResolved:Boolean; // Used to prevent repeat resolution passes for a single document. private var _resultFormat:String; private var _wsdlOperation:mx.rpc.wsdl.WSDLOperation; /** * A RegEx pattern to help replace the whitespace between processing * instructions and root tags. */ public static var PI_WHITESPACE_PATTERN:RegExp = new RegExp("[\\?][>]\\s*[<]", "g"); } }