////////////////////////////////////////////////////////////////////////////////
//
//  Licensed to the Apache Software Foundation (ASF) under one or more
//  contributor license agreements.  See the NOTICE file distributed with
//  this work for additional information regarding copyright ownership.
//  The ASF licenses this file to You under the Apache License, Version 2.0
//  (the "License"); you may not use this file except in compliance with
//  the License.  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

package mx.messaging.channels
{

import mx.events.ErrorEvent;
import org.apache.royale.events.Event;
import mx.events.IOErrorEvent;
import mx.events.SecurityErrorEvent;
import mx.net.URLLoader;
import org.apache.royale.net.URLRequest;

import mx.core.mx_internal;
import mx.messaging.RoyaleClient;
import mx.messaging.MessageAgent;
import mx.messaging.MessageResponder;
import mx.messaging.channels.amfx.AMFXDecoder;
import mx.messaging.channels.amfx.AMFXEncoder;
import mx.messaging.channels.amfx.AMFXHeader;
import mx.messaging.channels.amfx.AMFXResult;
import mx.messaging.config.ConfigMap;
import mx.messaging.config.LoaderConfig;
import mx.messaging.config.ServerConfig;
import mx.messaging.errors.MessageSerializationError;
import mx.messaging.events.ChannelFaultEvent;
import mx.messaging.messages.AbstractMessage;
import mx.messaging.messages.AcknowledgeMessage;
import mx.messaging.messages.AsyncMessage;
import mx.messaging.messages.CommandMessage;
import mx.messaging.messages.ErrorMessage;
import mx.messaging.messages.HTTPRequestMessage;
import mx.messaging.messages.IMessage;
import mx.messaging.messages.MessagePerformanceInfo;
import mx.messaging.messages.MessagePerformanceUtils;
import mx.netmon.NetworkMonitor;
import mx.utils.ObjectUtil;
import mx.utils.StringUtil;

use namespace mx_internal;

/**
 *  The HTTPChannel class provides the HTTP support for messaging.
 *  You can configure this Channel to poll the server at an interval
 *  to approximate server push.
 *  You can also use this Channel with polling disabled to send RPC messages
 *  to remote destinations to invoke their methods.
 *
 *  <p>
 *  The HTTPChannel relies on network services native to Flash Player and AIR,
 *  and exposed to ActionScript by the URLLoader class.
 *  This channel uses URLLoader exclusively, and creates a new URLLoader
 *  per request.
 *  </p>
 *
 *  <p>
 *  Channels are created within the framework using the
 *  <code>ServerConfig.getChannel()</code> method. Channels can be constructed
 *  directly and assigned to a ChannelSet if desired.
 *  </p>
 *
 *  <p>
 *  Channels represent a physical connection to a remote endpoint.
 *  Channels are shared across destinations by default.
 *  This means that a client targetting different destinations may use
 *  the same Channel to communicate with these destinations.
 *  </p>
 *
 *  <p>
 *  When used in polling mode, this Channel polls the server for new messages
 *  based on the <code>polling-interval-seconds</code> property in the configuration file,
 *  and this can be changed by setting the <code>pollingInterval</code> property.
 *  The default value is 3 seconds.
 *  To enable polling, the channel must be connected and the <code>polling-enabled</code>
 *  property in the configuration file must be set to <code>true</code>, or the
 *  <code>pollingEnabled</code> property of the Channel must be set to <code>true</code>.
 *  </p>
 *  
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion BlazeDS 4
 *  @productversion LCDS 3 
 */
public class HTTPChannel extends PollingChannel
{
    //--------------------------------------------------------------------------
    //
    // Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  Constructor.
     *
     *  @param id The id of this Channel.
     *  @param uri The uri for this Channel.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3 
     */
    public function HTTPChannel(id:String = null, uri:String = null)
    {
        super(id, uri);

        _encoder = new AMFXEncoder();
        _appendToURL = "";
        _messageQueue = [];
    }

    //--------------------------------------------------------------------------
    //
    // Variables
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private var _appendToURL:String;

    /**
     *  @private
     *  The loader used to ping the server in internalConnect. We need to hang onto a reference
     *  in order to time out a connect attempt.
     */
    private var _connectLoader:ChannelRequestLoader;

    /**
     *  @private
     */
    private var _encoder:AMFXEncoder;

    /**
     *  @private
     *  Records the request that needs to be completed before other
     *  requests can be sent.
     */
    private var _pendingRequest:ChannelRequestLoader = null;

    /**
     *  @private
     *  This queue contains the messages from send requests that
     *  occurred while an authentication attempt is underway.
     */
    private var _messageQueue:Array;

    //--------------------------------------------------------------------------
    //
    // Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  polling
    //----------------------------------

    /**
     *  Reports whether the channel is actively polling.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3 
     */
    public function get polling():Boolean
    {
        return pollOutstanding;
    }

    //----------------------------------
    //  piggybackingEnabled
    //----------------------------------

    /**
     *  Indicates whether this channel will piggyback poll requests along
     *  with regular outbound messages when an outstanding poll is not in
     *  progress. This allows the server to piggyback data for the client
     *  along with its response to client's message.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3 
     */
    public function get piggybackingEnabled():Boolean
    {
        return internalPiggybackingEnabled;
    }

    /**
     *  @private
     */
    public function set piggybackingEnabled(value:Boolean):void
    {
        internalPiggybackingEnabled = value;
    }

    //----------------------------------
    //  pollingEnabled
    //----------------------------------

    /**
     *  Indicates whether this channel is enabled to poll.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3 
     */
    public function get pollingEnabled():Boolean
    {
        return internalPollingEnabled;
    }

    /**
     *  @private
     */
    public function set pollingEnabled(value:Boolean):void
    {
        internalPollingEnabled = value;
    }

    //----------------------------------
    //  pollingInterval
    //----------------------------------

    /**
     *  Provides access to the polling interval for this Channel.
     *  The value is in milliseconds.
     *  This value determines how often this Channel requests messages from
     *  the server, to approximate server push.
     *
     *  @throws ArgumentError If the pollingInterval is assigned a value of 0 or
     *                        less.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3 
     */
    public function get pollingInterval():Number
    {
        return internalPollingInterval;
    }

    /**
     *  @private
     */
    public function set pollingInterval(value:Number):void
    {
        internalPollingInterval = value;
    }

    //----------------------------------
    //  protocol
    //----------------------------------

    /**
     *  Returns the protocol for this channel (http).
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3 
     */
    override public function get protocol():String
    {
        return "http";
    }

    //--------------------------------------------------------------------------
    //
    // Internal Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  appendToURL
    //----------------------------------

    /**
     * @private
     */
    mx_internal function get appendToURL():String
    {
        return _appendToURL;
    }

    /**
     *  @private
     */
    mx_internal function set appendToURL(value:String):void
    {
        if (value && endpoint)
        {
            _appendToURL = value;
       }
    }

    //--------------------------------------------------------------------------
    //
    // Overridden Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  Processes polling related configuration settings.
     */
    override public function applySettings(settings:XML):void
    {
        super.applySettings(settings);
        applyPollingSettings(settings);
    }

    //--------------------------------------------------------------------------
    //
    // Overridden Protected Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override protected function connectTimeoutHandler(event:Event):void
    {
        _connectLoader.close();
        super.connectTimeoutHandler(event);
    }

    /**
     *  @private
     */
    override protected function getDefaultMessageResponder(agent:MessageAgent, msg:IMessage):MessageResponder
    {
        return new HTTPMessageResponder(agent, msg, this);
    }

    /**
     *  @private
     *  Attempts to connect to the remote destination with the current endpoint
     *  specified for this channel.
     *  This will determine if a connection can be established.
     */
    override protected function internalConnect():void
    {
        // Ping the server to make sure that it is reachable.
        var msg:CommandMessage = new CommandMessage();
        if (credentials != null)
        {
            msg.operation = CommandMessage.LOGIN_OPERATION;
            msg.body = credentials;
        }
        else
        {
            msg.operation = CommandMessage.CLIENT_PING_OPERATION;
        }

        // Report the messaging version for this Channel.
        // msg.headers[CommandMessage.MESSAGING_VERSION] = messagingVersion;

        // Indicate if requesting the dynamic configuration from the server.
        if (ServerConfig.needsConfig(this))
            msg.headers[CommandMessage.NEEDS_CONFIG_HEADER] = true;

        // Add the FlexClient id header.
        setRoyaleClientIdOnMessage(msg);

        var urlRequest:URLRequest = createURLRequest(msg);
        _connectLoader = new ChannelRequestLoader();
        _connectLoader.setErrorCallbacks(pingErrorHandler);
        _connectLoader.completeCallback = pingCompleteHandler;
        _connectLoader.load(urlRequest);
    }

    /**
     *  @private
     *  Disconnects from the remote destination.
     */
    override protected function internalDisconnect(rejected:Boolean = false):void
    {
        // Attempt to notify the server of the disconnect.
        if (!rejected && !shouldBeConnected)
        {
            var msg:CommandMessage = new CommandMessage();
            msg.operation = CommandMessage.DISCONNECT_OPERATION;
            internalSend(new MessageResponder(null, msg, null));
        }
        // Shutdown locally.
        setConnected(false);
        super.internalDisconnect(rejected);
        disconnectSuccess(rejected); // make sure to notify everyone that we have disconnected.
    }

    /**
     *  @private
     */
    override protected function internalSend(msgResp:MessageResponder):void
    {
        if (_pendingRequest != null)
        {
            _messageQueue.push(msgResp);
        }
        else
        {
            // Set the global FlexClient Id.
            setRoyaleClientIdOnMessage(msgResp.message);

            try
            {
                // If MPI is enabled initialize MPI object and stamp it with client send time
                if (mpiEnabled)
                {
                    var mpii:MessagePerformanceInfo = new MessagePerformanceInfo();
                    if (recordMessageTimes)
                        mpii.sendTime = new Date().getTime();
                    msgResp.message.headers[MessagePerformanceUtils.MPI_HEADER_IN] = mpii;
                }

                // Finally, if "Small Messages" are enabled, send this form instead of
                // the normal message where possible.
                /*
                if (useSmallMessages && msgResp.message is ISmallMessage)
                {
                    var smallMessage:IMessage = ISmallMessage(msgResp.message).getSmallMessage();
                    if (smallMessage != null)
                        msgResp.message = smallMessage;
                }
                */

                var urlLoader:ChannelRequestLoader;
                var urlRequest:URLRequest = createURLRequest(msgResp.message);
                if (msgResp is HTTPMessageResponder)
                {
                    var httpMsgResp:HTTPMessageResponder =
                        HTTPMessageResponder(msgResp);
                    urlLoader = httpMsgResp.urlLoader;
                    urlLoader.completeCallback = httpMsgResp.completeHandler;
                    urlLoader.errorCallback = httpMsgResp.errorHandler;
                    urlLoader.ioErrorCallback = httpMsgResp.ioErrorHandler;
                    urlLoader.securityErrorCallback =
                        httpMsgResp.securityErrorHandler;
                }
                else
                {
                    var responderWrapper:HTTPWrapperResponder =
                        new HTTPWrapperResponder(msgResp);
                    urlLoader = new ChannelRequestLoader();
                    urlLoader.completeCallback =
                        responderWrapper.completeHandler;
                    urlLoader.setErrorCallbacks(responderWrapper.errorHandler);
                }
                urlLoader.requestProcessedCallback = requestProcessedHandler;

                // Do not consider poll requests as pending requests to allow
                // clients to send messages while waiting for poll response.
                if (!(msgResp.message is CommandMessage && CommandMessage(msgResp.message).operation == CommandMessage.POLL_OPERATION))
                    _pendingRequest = urlLoader;

                urlLoader.load(urlRequest);
            }
            catch(e:MessageSerializationError)
            {
                msgResp.agent.fault(e.fault, msgResp.message);
            }
        }
    }

    //--------------------------------------------------------------------------
    //
    // Internal Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  Utility function to handle a connection related ErrorMessage.
     *
     *  @param msg The ErrorMessage returned during a connect attempt.
     */
    mx_internal function connectionError(msg:ErrorMessage):void
    {
        var faultEvent:ChannelFaultEvent = ChannelFaultEvent.createEvent(this, false,
                "Channel.Connect.Failed", "error", msg.faultDetail + " url: '" + endpoint +
                    (_appendToURL != null ? _appendToURL : "") + "'");
        faultEvent.rootCause = msg;
        connectFailed(faultEvent);
    }

    /**
     *  @private
     *  This method will serialize the specified message into a new instance of
     *  a URLRequest and return it.
     *
     *  @param   message Message to serialize
     *  @return  URLRequest
     */
    mx_internal function createURLRequest(message:IMessage):URLRequest
    {
        var result:URLRequest = new URLRequest();
        if (_appendToURL)
            result.url = endpoint + _appendToURL;
        else
            result.url = endpoint;
        
        // Propagate our requestTimeout for those platforms
        // supporting the idleTimeout property on URLRequest.
        if ("idleTimeout" in result && requestTimeout > 0)
            result["idleTimeout"] = requestTimeout * 1000;
                
        monitorRpcMessage(message, result); 
        
        result.contentType = HTTPRequestMessage.CONTENT_TYPE_XML;

        var packet:XML = _encoder.encode(message, null);
        result.data = packet.toString();
        result.method = "POST";
        
        return result;
    }
	
	/**
	 * change the result url to redirect request to Network Monitor
	 */
	
	private function monitorRpcMessage(message:IMessage, result:URLRequest):void
	{
		if (NetworkMonitor.isMonitoring())
		{		
			var redirectedUrl:String = NetworkMonitor.adjustNetConnectionURL(LoaderConfig.url, result.url);
			if(redirectedUrl != null){				
				result.url = redirectedUrl;
			}		
		}
	}
	
    //--------------------------------------------------------------------------
    //
    // Protected Methods
    //
    //--------------------------------------------------------------------------

    /**
     * @private
     */
    protected function internalPingComplete(msg:AsyncMessage):void
    {
        if (msg != null)
        {
            ServerConfig.updateServerConfigData(msg.body as ConfigMap, endpoint);

            // Set the server assigned FlexClient Id.
            if (RoyaleClient.getInstance().id == null && msg.headers[AbstractMessage.FLEX_CLIENT_ID_HEADER] != null)
                RoyaleClient.getInstance().id = msg.headers[AbstractMessage.FLEX_CLIENT_ID_HEADER];
        }

        // Process the features advertised by the server endpoint.
        /*
        if (msg.headers[CommandMessage.MESSAGING_VERSION] != null)
        {
            var serverVersion:Number = msg.headers[CommandMessage.MESSAGING_VERSION] as Number;
            handleServerMessagingVersion(serverVersion);
        }
        */

        connectSuccess();
        if (credentials != null && !(msg is ErrorMessage))
            setAuthenticated(true);
    }

    //--------------------------------------------------------------------------
    //
    // Private Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  Special handler for AMFX packet level header "AppendToGatewayUrl".
     *  When we receive this header we assume the server detected that a session
     *  was created but it believed the client could not accept its session
     *  cookie, so we need to decorate the channel endpoint with the session id.
     *
     *  We do not modify the underlying endpoint property, however, as this
     *  session is transient and should not apply if the channel is disconnected
     *  and re-connected at some point in the future.
     */
    private function AppendToGatewayUrl(value:String):void
    {
        if (value != null)
            appendToURL = value;
    }

    private function decodePacket(event:Event):AMFXResult
    {
        var raw:String = String(URLLoader(event.target).data);
        var xmlData:XML = new XML(raw);
        var _decoder:AMFXDecoder = new AMFXDecoder();
        var packet:AMFXResult = _decoder.decode(xmlData);
        return packet;
    }

    /**
     *  @private
     *  Attempts to replicate the packet-level header functionality that AMFChannel
     *  uses for response headers such as AppendToGatewayUrl for session id tracking.
     */
    private function processHeaders(packet:AMFXResult):void
    {
        if (packet.headers != null)
        {
            try
            {
                for (var i:uint = 0; i < packet.headers.length; i++)
                {
                    var header:AMFXHeader = packet.headers[i];
                    if (header != null && header.name == APPEND_TO_URL_HEADER)
                    {
                        AppendToGatewayUrl(String(header.content));
                    }
                }
            }
            catch(e:Error)
            {
            }
        }
    }

    /**
     *  @private
     *  This method indicates that we successfully connected to the endpoint.
     *  Called as a result of the ping operation performed in the
     *  internalConnect() method.
     */
    private function pingCompleteHandler(event:Event):void
    {
        var packet:AMFXResult = decodePacket(event);
        processHeaders(packet);
        var msg:AsyncMessage = packet.result as AsyncMessage;
        if (msg != null && (msg is ErrorMessage) &&
            ErrorMessage(msg).faultCode == "Client.Authentication")
        {
            internalPingComplete(msg);
            var faultEvent:ChannelFaultEvent = ChannelFaultEvent.createEvent(this, false, "Channel.Authentication.Error", "warn");
            faultEvent.rootCause = ErrorMessage(msg);
            dispatchEvent(faultEvent);
        }
        else
        {
            internalPingComplete(msg);
        }
    }

    /**
     *  @private
     *  This method dispatches the appropriate error to any message agents, and
     *  is called as a result of the ping operation performed in the
     *  internalConnect() method.
     */
    private function pingErrorHandler(event:Event):void
    {
        _log.debug("'{0}' fault handler called. {1}", id, event.toString());
        var faultEvent:ChannelFaultEvent = ChannelFaultEvent.createEvent(this, false,
                                                    "Channel.Ping.Failed",
                                                    "error",
                                                    " url: '" + endpoint +
                                                    (_appendToURL == null ? "" : _appendToURL + "'") + "'");
        faultEvent.rootCause = event;
        connectFailed(faultEvent);
    }

    /**
     *  @private
     *  Chains sends for pending messages.
     */
    private function requestProcessedHandler
        (loader:ChannelRequestLoader, event:Event):void
    {
        if (_pendingRequest == loader)
        {
            _pendingRequest = null;
        }
        // TODO: we should do these in a batch for more efficiency and
        // better session maintenance
        while ((_messageQueue.length > 0) && (_pendingRequest == null))
        {
            internalSend(MessageResponder(_messageQueue.shift()));
        }
    }

    //--------------------------------------------------------------------------
    //
    // Static Constants
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private static const APPEND_TO_URL_HEADER:String = "AppendToGatewayUrl";
}
}

//------------------------------------------------------------------------------
//
// Private Classes
//
//------------------------------------------------------------------------------

import mx.events.ErrorEvent;
import org.apache.royale.events.Event;
import mx.events.IOErrorEvent;
import mx.events.SecurityErrorEvent;
import mx.net.URLLoader;
import org.apache.royale.net.URLRequest;
import mx.core.mx_internal;
import mx.messaging.MessageAgent;
import mx.messaging.MessageResponder;
import mx.messaging.channels.HTTPChannel;
import mx.messaging.channels.amfx.AMFXDecoder;
import mx.messaging.channels.amfx.AMFXHeader;
import mx.messaging.channels.amfx.AMFXResult;
import mx.messaging.messages.AcknowledgeMessage;
import mx.messaging.messages.AsyncMessage;
import mx.messaging.messages.CommandMessage;
import mx.messaging.messages.ErrorMessage;
import mx.messaging.messages.HTTPRequestMessage;
import mx.messaging.messages.IMessage;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
import mx.utils.StringUtil;

use namespace mx_internal;

[ResourceBundle("messaging")]

/**
 *  @private
 *  This responder wraps another MessageResponder with HTTP functionality.
 */
class HTTPWrapperResponder
{
    //--------------------------------------------------------------------------
    //
    // Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  Constructs a HTTPWrappedResponder.
     *
     *  @param wrappedResponder The responder to wrap.
     */
    public function HTTPWrapperResponder(wrappedResponder:MessageResponder)
    {
        super();
        _wrappedResponder = wrappedResponder;
    }

    //--------------------------------------------------------------------------
    //
    // Variables
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private var _wrappedResponder:MessageResponder;


    /**
     * @private
     */
    private var resourceManager:IResourceManager =
                                    ResourceManager.getInstance();

    //--------------------------------------------------------------------------
    //
    // Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  Handles a result returned from the remote destination.
     *
     *  @param event The completion event from the associated URLLoader.
     */
    public function completeHandler(event:Event):void
    {
        var raw:String = String(URLLoader(event.target).data);
        var xmlData:XML = new XML(raw);
        var _decoder:AMFXDecoder = new AMFXDecoder();
        var packet:AMFXResult = _decoder.decode(xmlData);
        if (packet.result is ErrorMessage)
        {
            _wrappedResponder.status(ErrorMessage(packet.result));
        }
        else if (packet.result is AsyncMessage)
        {
            _wrappedResponder.result(AsyncMessage(packet.result));
        }
    }

    /**
     *  @private
     *  Handles an error for an outbound request.
     *
     *  @param event The error event from the associated URLLoader.
     */
    public function errorHandler(event:Event):void
    {
        var msg:ErrorMessage = new ErrorMessage();
        msg.correlationId = _wrappedResponder.message.messageId;
        msg.faultCode = "Server.Error.Request";
        /* msg.faultString = resourceManager.getString(
            "messaging", "httpRequestError"); */
        msg.faultString = "HTTP request error";
        var details:String = event.toString();
        if (_wrappedResponder.message is HTTPRequestMessage)
        {
            details += ". URL: ";
            details += HTTPRequestMessage(_wrappedResponder.message).url;
        }
        /* msg.faultDetail = resourceManager.getString(
            "messaging", "httpRequestError.details", [ details ]); */
        msg.faultDetail = StringUtil.substitute("Error: {0}", details);
        msg.rootCause = event;
        _wrappedResponder.status(msg);
    }
}


[ResourceBundle("messaging")]

/**
 *  @private
 *  This is an adapter for url loader that is used by the HTTPChannel.
 */
class HTTPMessageResponder extends MessageResponder
{
    //--------------------------------------------------------------------------
    //
    // Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  Constructs an HTTPMessageResponder.
     *
     *  @param agent The associated MessageAgent.
     *
     *  @param msg The message to send.
     *
     *  @param channel The Channel to send the message over.
     */
    public function HTTPMessageResponder
        (agent:MessageAgent, msg:IMessage, channel:HTTPChannel)
    {
        super(agent, msg, channel);
        decoder = new AMFXDecoder();
        urlLoader = new ChannelRequestLoader();
    }

    //--------------------------------------------------------------------------
    //
    // Variables
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private var decoder:AMFXDecoder;

    /**
     * @private
     */
    private var resourceManager:IResourceManager =
                                    ResourceManager.getInstance();

    //--------------------------------------------------------------------------
    //
    // Properties
    //
    //--------------------------------------------------------------------------

    /**
     *  The loader associated with this responder.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion BlazeDS 4
     *  @productversion LCDS 3 
     */
    public var urlLoader:ChannelRequestLoader;

    //--------------------------------------------------------------------------
    //
    // Overridden Protected Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override protected function resultHandler(response:IMessage):void
    {
        var errorMsg:ErrorMessage;

        if (response is AsyncMessage)
        {
            if (response is ErrorMessage)
            {
                agent.fault(ErrorMessage(response), message);
            }
            else if (AsyncMessage(response).correlationId == message.messageId)
            {
                agent.acknowledge(AcknowledgeMessage(response), message);
            }
            else
            {
                errorMsg = new ErrorMessage();
                errorMsg.faultCode = "Server.Acknowledge.Failed";
                /* errorMsg.faultString = resourceManager.getString(
                    "messaging", "ackFailed");
                errorMsg.faultDetail = resourceManager.getString(
                    "messaging", "ackFailed.details",
                    [ message.messageId, AsyncMessage(response).correlationId ]); */
                errorMsg.faultString = "Didn't receive an acknowledgement of message";
                errorMsg.faultDetail = StringUtil.substitute("Was expecting message '{0}' but received '{1}'.",
                    message.messageId, AsyncMessage(response).correlationId);
                agent.fault(errorMsg, message);
            }
        }
        else if (response != null)
        {
            errorMsg = new ErrorMessage();
            errorMsg.faultCode = "Server.Acknowledge.Failed";
            /* errorMsg.faultString = resourceManager.getString(
                "messaging", "noAckMessage");
            errorMsg.faultDetail = resourceManager.getString(
                "messaging", "noAckMessage.details",
                [ mx.utils.ObjectUtil.toString(response) ]); */
            errorMsg.faultString = "Didn't receive an acknowledge message";
            errorMsg.faultDetail = StringUtil.substitute("Was expecting mx.messaging.messages.AcknowledgeMessage, but received {0}",
                mx.utils.ObjectUtil.toString(response));
            agent.fault(errorMsg, message);
        }
    }

    /**
     *  @private
     *  Handle a request timeout by closing our associated URLLoader and
     *  faulting the message to the agent.
     */
    override protected function requestTimedOut():void
    {
        urlLoader.close();

        status(null);
        // send the ack
        var ack:AcknowledgeMessage = new AcknowledgeMessage();
        ack.correlationId = message.messageId;
        ack.headers[AcknowledgeMessage.ERROR_HINT_HEADER] = true; // hint there was an error
        agent.acknowledge(ack, message);
        // send the fault
        agent.fault(createRequestTimeoutErrorMessage(), message);
    }

    //--------------------------------------------------------------------------
    //
    // Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    final public function completeHandler(event:Event):void
    {
        result(null);

        var raw:String = String(URLLoader(event.target).data);
        var xmlData:XML = new XML(raw);
        var packet:AMFXResult = decoder.decode(xmlData);

        if (packet.result is IMessage)
        {
            resultHandler(IMessage(packet.result));
        }
    }

    /**
     *  @private
     */
    public function errorHandler(event:Event):void
    {
        status(null);
        // send the ack
        var ack:AcknowledgeMessage = new AcknowledgeMessage();
        ack.correlationId = message.messageId;
        ack.headers[AcknowledgeMessage.ERROR_HINT_HEADER] = true; // hint there was an error
        agent.acknowledge(ack, message);
        // send fault
        var msg:ErrorMessage = new ErrorMessage();
        msg.correlationId = message.messageId;
        msg.faultCode = "Server.Error.Request";
        /* msg.faultString = resourceManager.getString(
            "messaging", "httpRequestError"); */
        msg.faultString = "HTTP request error";
        var details:String = event.toString();
        if (message is HTTPRequestMessage)
        {
            details += ". URL: ";
            details += HTTPRequestMessage(message).url;
        }
        /* msg.faultDetail = resourceManager.getString(
            "messaging", "httpRequestError.details", [ details ]); */
        msg.faultDetail = StringUtil.substitute("Error: {0}", details);
        msg.rootCause = event;
        agent.fault(msg, message);
    }

    /**
     *  @private
     */
    public function ioErrorHandler(event:Event):void
    {
        status(null);
        // send the ack
        var ack:AcknowledgeMessage = new AcknowledgeMessage();
        ack.correlationId = message.messageId;
        ack.headers[AcknowledgeMessage.ERROR_HINT_HEADER] = true; // hint there was an error
        agent.acknowledge(ack, message);
        // send fault
        var msg:ErrorMessage = new ErrorMessage();
        msg.correlationId = message.messageId;
        msg.faultCode = "Server.Error.Request";
        /* msg.faultString = resourceManager.getString(
            "messaging", "httpRequestError"); */
        msg.faultString = "HTTP request error";
        var details:String = event.toString();
        if (message is HTTPRequestMessage)
        {
            details += ". URL: ";
            details += HTTPRequestMessage(message).url;
        }
        /* msg.faultDetail = resourceManager.getString(
            "messaging", "httpRequestError.details", [ details ]); */
        msg.faultDetail = StringUtil.substitute("Error: {0}", details);
        msg.rootCause = event;

        (channel as HTTPChannel).connectionError(msg);
        
        // already disconnected, now let the agent know the the message faulted
        // this is similar to the disconnect() and fault() in the NetConnectionChannel statusHandler
        agent.fault(msg, message);    
    }

    /**
     *  @private
     */
    public function securityErrorHandler(event:Event):void
    {
        status(null);
        // send the ack
        var ack:AcknowledgeMessage = new AcknowledgeMessage();
        ack.correlationId = message.messageId;
        ack.headers[AcknowledgeMessage.ERROR_HINT_HEADER] = true; // hint there was an error
        agent.acknowledge(ack, message);
        // send fault
        var msg:ErrorMessage = new ErrorMessage();
        msg.correlationId = message.messageId;
        msg.faultCode = "Channel.Security.Error";
        /* msg.faultString = resourceManager.getString(
            "messaging", "securityError");
        msg.faultDetail = resourceManager.getString(
            "messaging", "securityError.details", [ message.destination ]); */
        msg.faultString = "Security error accessing url";
        msg.faultDetail = StringUtil.substitute("Destination: {0}", message.destination);
        msg.rootCause = event;
        agent.fault(msg, message);
    }
}

/**
 *  @private
 *  Wraps an URLLoader and manages dispatching its events to the proper handlers.
 */
class ChannelRequestLoader
{
    //--------------------------------------------------------------------------
    //
    // Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  Constructs a ChannelRequestLoader.
     */
    public function ChannelRequestLoader()
    {
        super();
        _urlLoader = new URLLoader();
        _urlLoader.addEventListener(ErrorEvent.ERROR, errorHandler);
        _urlLoader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
        _urlLoader.addEventListener
            (SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
        _urlLoader.addEventListener(Event.COMPLETE, completeHandler);
    }

    //--------------------------------------------------------------------------
    //
    // Variables
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     *  The wrapped URLLoader.
     */
    private var _urlLoader:URLLoader;

    //--------------------------------------------------------------------------
    //
    // Properties
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    public var errorCallback:Function;

    /**
     *  @private
     */
    public var ioErrorCallback:Function;

    /**
     *  @private
     */
    public var securityErrorCallback:Function;

    /**
     *  @private
     */
    public var completeCallback:Function;

    /**
     *  @private
     */
    public var requestProcessedCallback:Function;

    //--------------------------------------------------------------------------
    //
    // Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    public function load(request:URLRequest):void
    {
        _urlLoader.load(request);
    }

    /**
     *  @private
     */
    public function close():void
    {
        _urlLoader.removeEventListener(ErrorEvent.ERROR, errorHandler);
        _urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
        _urlLoader.removeEventListener
            (SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
        _urlLoader.removeEventListener(Event.COMPLETE, completeHandler);
        _urlLoader.close();
    }

    /**
     *  @private
     */
    public function setErrorCallbacks(callback:Function):void
    {
        errorCallback = callback;
        ioErrorCallback = callback;
        securityErrorCallback = callback;
    }

    //--------------------------------------------------------------------------
    //
    // Private Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private function callRequestProcessedCallback(event:Event):void
    {
        if (requestProcessedCallback != null)
            requestProcessedCallback(this, event);
    }

    /**
     *  @private
     */
    private function callEventCallback(callback:Function, event:Event):void
    {
        if (callback != null)
            callback(event);
    }

    /**
     *  @private
     */
    private function errorHandler(event:Event):void
    {
        callRequestProcessedCallback(event);
        callEventCallback(requestProcessedCallback, event);
    }

    /**
     *  @private
     */
    private function ioErrorHandler(event:Event):void
    {
        callRequestProcessedCallback(event);
        callEventCallback(ioErrorCallback, event);
    }

    /**
     *  @private
     */
    private function securityErrorHandler(event:Event):void
    {
        callRequestProcessedCallback(event);
        callEventCallback(securityErrorCallback, event);
    }

    /**
     *  @private
     */
    private function completeHandler(event:Event):void
    {
        callRequestProcessedCallback(event);
        callEventCallback(completeCallback, event);
    }
}