//////////////////////////////////////////////////////////////////////////////// // // 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.events.Event; import flash.xml.XMLNode; import mx.core.mx_internal; import mx.logging.Log; import mx.logging.ILogger; import mx.messaging.ChannelSet; import mx.messaging.events.MessageEvent; import mx.messaging.messages.IMessage; import mx.messaging.messages.SOAPMessage; import mx.resources.IResourceManager; import mx.resources.ResourceManager; import mx.rpc.AbstractOperation; import mx.rpc.AbstractService; import mx.rpc.AsyncToken; import mx.rpc.Fault; import mx.rpc.events.FaultEvent; import mx.rpc.events.HeaderEvent; import mx.rpc.events.ResultEvent; import mx.rpc.soap.AbstractWebService; import mx.rpc.wsdl.WSDLOperation; import mx.rpc.xml.SchemaConstants; import mx.utils.ObjectProxy; import mx.utils.XMLUtil; use namespace mx_internal; /** * Dispatched when an Operation invocation returns with SOAP headers in the * response. A HeaderEvent is dispatched for each SOAP header. * @eventType mx.rpc.events.HeaderEvent.HEADER */ [Event(name="header", type="mx.rpc.events.HeaderEvent")] [ResourceBundle("rpc")] /** * An Operation used specifically by WebServices. An Operation is an individual * method on a service. An Operation can be called either by invoking the * function of the same name on the service or by accessing the Operation as a * property on the service and calling the send() method. */ public class Operation extends AbstractOperation { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Creates a new Operation. This is usually done directly by the MXML * compiler or automatically by the WebService when an unknown operation * has been accessed. It is not recommended that a developer use this * constructor directly. * * @param webService The web service upon which this Operation is invoked. * * @param name The name of this Operation. */ public function Operation(webService:AbstractService = null, name:String = null) { super(webService, name); _resultFormat = "object"; _headerFormat = "e4x"; _headers = []; log = Log.getLogger("mx.rpc.soap.Operation"); if (webService) { this.webService = AbstractWebService(webService); log.info("Creating SOAP Operation for {0}", name); } // No explicit timeout value by default. The user can set this, and // thus engage a timer for this # of milliseconds on each call. If the // timer fires before the call returns, a fault will be generated. timeout = -1; } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * @private */ private var resourceManager:IResourceManager = ResourceManager.getInstance(); //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // decoder //---------------------------------- /** * The ISOAPDecoder implementation used by this Operation to decode a SOAP * encoded response into ActionScript. * * @private */ public function get decoder():ISOAPDecoder { if (_decoder == null) { _decoder = new SOAPDecoder(); _decoder.makeObjectsBindable = makeObjectsBindable; _decoder.ignoreWhitespace = ignoreWhitespace; } if (_decoder.wsdlOperation == null) _decoder.wsdlOperation = wsdlOperation; return _decoder; } /** * @private */ public function set decoder(value:ISOAPDecoder):void { _decoder = value; } //---------------------------------- // encoder //---------------------------------- /** * The ISOAPEncoder implementation used by this Operation to encode * ActionScript input arguments as a SOAP encoded request. * * @private */ public function get encoder():ISOAPEncoder { if (_encoder == null) { _encoder = new SOAPEncoder(); _encoder.ignoreWhitespace = ignoreWhitespace; } if (_encoder.wsdlOperation == null) _encoder.wsdlOperation = wsdlOperation; // Tell the encoder to use the xmlSpecialCharsFilter function specified // on the Operation (or Service). If null, encoder will default to its // own implementation. _encoder.xmlSpecialCharsFilter = xmlSpecialCharsFilter; return _encoder; } /** * @private */ public function set encoder(value:ISOAPEncoder):void { _encoder = value; } //---------------------------------- // endpointURI //---------------------------------- /** * The location of the WebService for this Operation. Normally, the WSDL * specifies the location of the services, but you can set this property to * override that location for the individual Operation. */ public function get endpointURI():String { return _endpointURI ? _endpointURI : webService.endpointURI; } public function set endpointURI(uri:String):void { _endpointURI = uri; } //---------------------------------- // forcePartArrays //---------------------------------- [Inspectable(defaultValue="false", category="General")] /** * Determines whether or not a single or empty return value for an output * message part that is defined as an array should be returned as an array * containing one (or zero, respectively) elements. This is applicable for * document/literal "wrapped" web services, where one or more of the elements * that represent individual message parts in the "wrapper" sequence could * have the maxOccurs attribute set with a value greater than 1. This is a * hint that the corresponding part should be treated as an array even if * the response contains zero or one values for that part. Setting * forcePartArrays to true will always create an array for parts defined in * this manner, regardless of the number of values returned. Leaving * forcePartArrays as false will only create arrays if two or more elements * are returned. */ public function get forcePartArrays():Boolean { return _forcePartArrays; } public function set forcePartArrays(value:Boolean):void { _forcePartArrays = value; } //---------------------------------- // headerFormat //---------------------------------- [Inspectable(enumeration="object,e4x,xml,E4X,XML", defaultValue="e4x", category="General")] /** * Determines how the SOAP encoded headers are decoded. A value of * object specifies that each header XML node will be decoded * into a SOAPHeader object, and its content property will be * an object structure as specified in the WSDL document. A value of * xml specifies that the XML will be left as XMLNodes. A * value of e4x specifies that the XML will be accessible * using ECMAScript for XML (E4X) expressions. */ public function get headerFormat():String { if (_headerFormat == null) _headerFormat = "e4x"; return _headerFormat; } public function set headerFormat(hf:String):void { if (hf != null) hf = hf.toLowerCase(); _headerFormat = hf; } //---------------------------------- // headers //---------------------------------- /** * Accessor to an Array of SOAPHeaders that are to be sent on * each invocation of the operation. */ public function get headers():Array { return _headers; } //---------------------------------- // httpHeaders //---------------------------------- private var _httpHeaders:Object; [Inspectable(defaultValue="undefined", category="General")] /** * Custom HTTP headers to be sent to the SOAP endpoint. If multiple * headers need to be sent with the same name the value should be specified * as an Array. */ public function get httpHeaders():Object { if (_httpHeaders != null) return _httpHeaders; return AbstractWebService(service).httpHeaders; } public function set httpHeaders(value:Object):void { _httpHeaders = value; } //---------------------------------- // ignoreWhitespace //---------------------------------- [Inspectable(defaultValue="true", category="General")] /** * Determines whether whitespace is ignored when processing XML for a SOAP * encoded request or 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; if (_decoder != null) _decoder.ignoreWhitespace = _ignoreWhitespace; if (_encoder != null) _encoder.ignoreWhitespace = _ignoreWhitespace; } //---------------------------------- // makeObjectsBindable //---------------------------------- [Inspectable(defaultValue="true", category="General")] /** * When this value is true, anonymous objects returned are forced to * bindable objects. */ override public function get makeObjectsBindable():Boolean { if (_makeObjectsBindableSet) { return _makeObjectsBindable; } return AbstractWebService(service).makeObjectsBindable; } override public function set makeObjectsBindable(value:Boolean):void { _makeObjectsBindable = value; _makeObjectsBindableSet = true; if (_decoder != null) _decoder.makeObjectsBindable = value; } //---------------------------------- // multiplePartsFormat //---------------------------------- [Inspectable(enumeration="object,array", defaultValue="object", category="General")] /** * Determines the type of the default result object for calls to web services * that define multiple parts in the output message. A value of "object" * specifies that the lastResult object will be an Object with named properties * corresponding to the individual output parts. A value of "array" would * make the lastResult an array, where part values are pushed in the order * they occur in the body of the SOAP message. The default value for document- * literal operations is "object". The default for rpc operations is "array". * The multiplePartsFormat property is applicable only when * resultFormat is "object" and ignored otherwise. */ public function get multiplePartsFormat():String { if (_multiplePartsFormat == null) { // To keep Flex 2.0.1 HF 2+ behavior, we need to determine the // default value based on the style of the operation. if (_wsdlOperation != null && _wsdlOperation.style == "rpc") _multiplePartsFormat = "array"; else _multiplePartsFormat = "object"; } return _multiplePartsFormat; } public function set multiplePartsFormat(value:String):void { if (value != null) value = value.toLowerCase(); _multiplePartsFormat = value; } //---------------------------------- // request //---------------------------------- /** * The request of the Operation is an object structure or an XML structure. * If you specify XML, the XML is sent as is. If you pass an object, it is * encoded into a SOAP XML structure. */ public function get request():Object { return this.arguments; } public function set request(r:Object):void { this.arguments = r; } //---------------------------------- // resultFormat //---------------------------------- [Inspectable(enumeration="object,e4x,xml,E4X,XML", defaultValue="object", category="General")] /** * Determines how the Operation result is decoded. A value of * object specifies that the XML will be decoded into an * object structure as specified in the WSDL document. A value of * xml specifies that the XML will be left as XMLNodes. A * value of e4x specifies that the XML will be accessible * using ECMAScript for XML (E4X) expressions. */ public function get resultFormat():String { if (_resultFormat == null) _resultFormat = "object"; return _resultFormat; } public function set resultFormat(rf:String):void { if (rf != null) rf = rf.toLowerCase(); _resultFormat = rf; } //---------------------------------- // resultHeaders //---------------------------------- [Bindable("resultForBinding")] /** * The headers that were returned as part of the last execution of this * operation. They match up with the lastResult property and * are the same as the collection of headers that are dispatched * individually as HeaderEvents. */ public function get resultHeaders():Array { return _responseHeaders; } //---------------------------------- // xmlSpecialCharsFilter //---------------------------------- private var _xmlSpecialCharsFilter:Function; public function get xmlSpecialCharsFilter():Function { if (_xmlSpecialCharsFilter != null) return _xmlSpecialCharsFilter; return AbstractWebService(service).xmlSpecialCharsFilter; } public function set xmlSpecialCharsFilter(func:Function):void { _xmlSpecialCharsFilter = func; } //-------------------------------------------------------------------------- // // Internal Properties // //-------------------------------------------------------------------------- /** * @private */ mx_internal var handleAxisSession:Boolean; /** * @private */ mx_internal function get wsdlOperation():WSDLOperation { return _wsdlOperation; } /** * @private */ mx_internal function set wsdlOperation(value:WSDLOperation):void { _wsdlOperation = value; } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * Adds a header that is applied only to this Operation. The header can be * provided in a pre-encoded form as an XML instance, or as a SOAPHeader * instance which leaves the encoding up to the internal SOAP encoder. * @param header The SOAP header to add to this Operation. */ public function addHeader(header:Object):void { _headers.push(header); } /** * Adds a header that is applied only to this Operation. * @param qnameLocal the localname for the header QName * @param qnameNamespace the namespace for header QName * @param headerName Name of the header. * @param headerValue Value of the header. */ public function addSimpleHeader(qnameLocal:String, qnameNamespace:String, headerName:String, headerValue:String):void { var obj:Object = {}; obj[headerName] = headerValue; addHeader(new SOAPHeader(new QName(qnameNamespace, qnameLocal), obj)); } /** * @inheritDoc */ override public function cancel(id:String = null):AsyncToken { if (hasPendingInvocations()) { if (null == id) { // remove the last pending call return pendingInvocations.pop().token; } // check if id is one of the pending calls for (var i:int = pendingInvocations.length-1; i >= 0; i--) { if (pendingInvocations[i].token.message != null && pendingInvocations[i].token.message.messageId == id) { var pc:OperationPendingCall = pendingInvocations.splice(i,1)[0]; return pc.token; } } } //if the call is not pending, use super return super.cancel(id); } /** * Clears the headers for this individual Operation. */ public function clearHeaders():void { _headers.length = 0; } /** * Returns a header if a match is found based on QName localName and URI. * @param qname QName of the SOAPHeader. * @param headerName Name of a header in the SOAPHeader content (Optional) * @return Returns the SOAPHeader. */ public function getHeader(qname:QName, headerName:String = null):SOAPHeader { var length:uint = _headers.length; for (var i:uint = 0; i < length; i++) { var header:SOAPHeader = SOAPHeader(_headers[i]); if (XMLUtil.qnamesEqual(header.qname, qname)) { if (headerName) { if (header.content && header.content[headerName]) { return header; } } else { return header; } } } return null; } /** * Removes the header with the given QName from all operations. * @param qname QName of the SOAPHeader. * @param headerName Name of a header in the SOAPHeader content (Optional) */ public function removeHeader(qname:QName, headerName:String = null):void { var length:uint = _headers.length; for (var i:uint = 0; i < length; i++) { var header:SOAPHeader = SOAPHeader(_headers[i]); if (XMLUtil.qnamesEqual(header.qname, qname)) { if (headerName) { if (header.content && header.content[headerName]) { _headers.splice(i, 1); return; // Got it } } else { _headers.splice(i, 1); return; // Got it } } } } /** * @private */ override public function send(...args:Array):AsyncToken { var argsToPass:Object = null; if (args && args.length > 0) { if ((args.length == 1) && (args[0] is XMLNode || args[0] is XML)) { // special case: handle xml node as single argument and drop // into literal mode. argsToPass = args[0]; } else { argsToPass = args; } } //Syntactically pre-registered else if (this.arguments) { argsToPass = this.arguments; } var combinedHeaders:Array = []; if (_headers) { combinedHeaders = combinedHeaders.concat(_headers); } if (webService.headers) { combinedHeaders = combinedHeaders.concat(webService.headers); } //create an empty message and a token to hold it var message:SOAPMessage = new SOAPMessage(); var token:AsyncToken = new AsyncToken(message); var pc:OperationPendingCall = new OperationPendingCall(argsToPass, combinedHeaders, token); if (webService.ready) { invokePendingCall(pc); } else // if (!webService.wsdlFault) { log.debug("Queueing SOAP operation {0}", name); if (!pendingInvocations) { pendingInvocations = []; } pendingInvocations.push(pc); } // FIXME: Handle WSDL error case /* else { var errMsg:String = "Cannot invoke method " + name + " as WSDL did not load successfully"; dispatchRpcEvent(createFaultEvent("Client.InvalidWSDL", errMsg)); } */ return pc.token; } //-------------------------------------------------------------------------- // // Internal Methods // //-------------------------------------------------------------------------- /** * @private */ mx_internal function hasPendingInvocations():Boolean { return pendingInvocations != null && pendingInvocations.length > 0; } /** * @private */ mx_internal function invokeAllPending():void { if (hasPendingInvocations()) { for (var i:int = 0; i < pendingInvocations.length; ++i) { invokePendingCall(pendingInvocations[i]); } pendingInvocations = null; } } /** * We now SOAP encode the pending call and send the request. * * @private */ mx_internal function invokePendingCall(pc:OperationPendingCall):void { log.debug("Invoking SOAP operation {0}", name); startTime = new Date(); var message:SOAPMessage = SOAPMessage(pc.token.message); var soap:XML; if (wsdlOperation == null) { log.debug("No operation found {0}", name); dispatchRpcEvent(createFaultEvent("Client.NoSuchMethod", "Couldn't find method '" + name + "' in service.")); return; } try { soap = encoder.encodeRequest(pc.args, pc.headers); } catch(fault:Fault) { dispatchRpcEvent(FaultEvent.createEvent(fault)); return; } catch(error:Error) { var errorMsg:String = error.message ? error.message : ""; var fault2:Fault = new Fault("EncodingError", errorMsg); var faultEvent:FaultEvent = FaultEvent.createEvent(fault2); dispatchRpcEvent(faultEvent); return; } message.httpHeaders = httpHeaders; if (message.getSOAPAction() == null) message.setSOAPAction(wsdlOperation.soapAction); message.body = soap.toXMLString(); message.url = endpointURI; invoke(message, pc.token); } /** * We decode the SOAP encoded response and update the result and response * headers (if any). * * @private */ override mx_internal function processResult(message:IMessage, token:AsyncToken):Boolean { var body:Object = message.body; var dispatchResultEvent:Boolean = true; try { var stringResult:String = String(body); decoder.resultFormat = resultFormat; decoder.headerFormat = headerFormat; decoder.multiplePartsFormat = multiplePartsFormat; decoder.forcePartArrays = forcePartArrays; var soapResult:SOAPResult = decoder.decodeResponse(stringResult); // Reset result _result = null; // Expose headers for the bindable property "resultHeaders" as // well as RPC fault and result events... _responseHeaders = soapResult.headers; // Process headers for both result and fault dispatchResultEvent = processHeaders(_responseHeaders, token, message); // Handle faults if (soapResult.isFault) { var faults:Array = soapResult.result as Array; for each (var soapFault:Fault in faults) { dispatchRpcEvent(FaultEvent.createEvent(soapFault, token, message)); } dispatchResultEvent = false; } if (dispatchResultEvent) _result = soapResult.result; } catch(fault:Fault) { dispatchRpcEvent(FaultEvent.createEvent(fault, token, message)); return false; } catch(error:Error) { var errorMsg:String = error.message != null ? error.message : ""; var fault2:Fault = new Fault("DecodingError", errorMsg); var faultEvent:FaultEvent = FaultEvent.createEvent(fault2, token, message); dispatchRpcEvent(faultEvent); return false; } return dispatchResultEvent; } /** * Checks SOAP response headers and enforces any mustUnderstand attributes * by checking that a listener exists for the "header" event. If we're * honoring Axis sessions then we also look out for for the sessionID * header and add it to the Operation's corresponding service for * subsequent invocations. Finally, the header is dispatched as a * HeaderEvent. If no problems are encountered the method simply returns * true. * * @private */ protected function processHeaders(responseHeaders:Array, token:AsyncToken, message:IMessage):Boolean { if (responseHeaders != null) { for (var i:uint = 0; i < responseHeaders.length; i++) { var header:Object = responseHeaders[i]; var mustUnderstand:Boolean; var headerQName:QName; var headerContent:Object; if (header is XML) { var headerXML:XML = header as XML; mustUnderstand = headerXML.@mustUnderstand == "1" ? true : false; headerQName = headerXML.name(); if (headerXML.hasComplexContent()) headerContent = headerXML.elements(); else headerContent = headerXML.text(); } else if (header is SOAPHeader) { mustUnderstand = header.mustUnderstand; headerQName = header.qname; headerContent = header.content; } // If header was either XML or SOAPHeader, headerQName would // be set by now. if (headerQName != null) { if (mustUnderstand == true) { if (!hasEventListener(HeaderEvent.HEADER) && !service.hasEventListener(HeaderEvent.HEADER)) { var msg:String = resourceManager.getString( "rpc", "noListenerForHeader", [ headerQName ]); var fault:Fault = new Fault("Client.MustUnderstand", msg); var faultEvent:FaultEvent = FaultEvent.createEvent(fault, token, message); dispatchRpcEvent(faultEvent); return false; } } if (handleAxisSession) { // pass the session id on to any request made on this proxy if (headerQName != null && headerQName.localName == "sessionID" && headerQName.uri == "http://xml.apache.org/axis/session") { var newHeader:SOAPHeader = new SOAPHeader(headerQName, headerContent); webService.addHeader(newHeader); } } var headerEvent:HeaderEvent = HeaderEvent.createEvent(header, token, message); dispatchRpcEvent(headerEvent); } } } return true; } /** * @private */ override mx_internal function setService(value:AbstractService):void { super.setService(value); webService = AbstractWebService(value); } /** * @private */ protected function createFaultEvent(faultCode:String = null, faultString:String = null, faultDetail:String = null):FaultEvent { var fault:Fault = new Fault(faultCode, faultString, faultDetail); var faultEvent:FaultEvent = FaultEvent.createEvent(fault); return faultEvent; } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- // Backing variables for public getter/setters private var _endpointURI:String; private var _forcePartArrays:Boolean = false; // Flex 2.0.1 HF2+ behavior is false private var _headerFormat:String; private var _headers:Array; private var _resultFormat:String; private var _makeObjectsBindableSet:Boolean; private var _multiplePartsFormat:String; // Internal properties //any vars here with underscores minimally have a getter or setter private var _decoder:ISOAPDecoder; private var _encoder:ISOAPEncoder; private var _ignoreWhitespace:Boolean = true; private var log:ILogger; private var pendingInvocations:Array; private var startTime:Date; private var timeout:int; private var webService:AbstractWebService; /** * @private */ protected var _wsdlOperation:mx.rpc.wsdl.WSDLOperation; } } import mx.rpc.AsyncToken; /** * @private */ class OperationPendingCall { public var args:*; public var headers:Array; public var token:AsyncToken; public function OperationPendingCall(args:*, headers:Array, token:AsyncToken) { super(); this.args = args; this.headers = headers; this.token = token; } }