blob: 8803f6b8a94bdab1d941b1a367a988d16f1f84b0 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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
{
COMPILE::SWF
{
import flash.events.IOErrorEvent;
import flash.events.NetStatusEvent;
import flash.events.SecurityErrorEvent;
import flash.net.Responder;
}
COMPILE::JS
{
import mx.events.IOErrorEvent;
import mx.events.NetStatusEvent;
import mx.events.SecurityErrorEvent;
import mx.net.Responder;
}
import mx.core.mx_internal;
import mx.logging.Log;
import mx.messaging.MessageResponder;
import mx.messaging.RoyaleClient;
import mx.messaging.config.ConfigMap;
import mx.messaging.config.ServerConfig;
import mx.messaging.events.ChannelFaultEvent;
import mx.messaging.messages.AbstractMessage;
import mx.messaging.messages.CommandMessage;
import mx.messaging.messages.ErrorMessage;
import mx.messaging.messages.IMessage;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
use namespace mx_internal;
//[ResourceBundle("messaging")]
/**
* The AMFChannel class provides the AMF 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 AMFChannel relies on network services native to Flash Player and AIR,
* and exposed to ActionScript by the NetConnection class.
* This channel uses NetConnection exclusively, and creates a new NetConnection
* per instance.
* </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 AMFChannel extends NetConnectionChannel
{
//--------------------------------------------------------------------------
//
// 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 AMFChannel(id:String = null, uri:String = null)
{
super(id, uri);
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
* Flag used to indicate that the channel is in the process of reconnecting
* with the session id in the url.
*/
protected var _reconnectingWithSessionId:Boolean;
/**
* @private
* Flag used to control when we need to handle NetStatusEvents.
* If the channel has shutdown due to reaching a connect timeout we need to
* continue listening for events (such as 404s) but we've already shutdown so
* we must ignore them.
*/
private var _ignoreNetStatusEvents:Boolean;
/**
* @private
*/
private var resourceManager:IResourceManager =
ResourceManager.getInstance();
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// 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;
}
//----------------------------------
// 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;
}
//----------------------------------
// 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";
}
//--------------------------------------------------------------------------
//
// Overridden Public Methods
//
//--------------------------------------------------------------------------
/**
* @private
* Processes polling related configuration settings.
*/
override public function applySettings(settings:XML):void
{
super.applySettings(settings);
applyPollingSettings(settings);
}
/**
* @private
* Overriding to be able to keep track of the fact that the Channel is in
* the process of reconnecting with the session id, so the initial
* NetConnection call can be discarded properly in the resultHandler.
*/
override public function AppendToGatewayUrl(value:String):void
{
if (value != null && value != "" && _appendToURL != value)
{
super.AppendToGatewayUrl(value);
_reconnectingWithSessionId = true;
}
}
//--------------------------------------------------------------------------
//
// Protected Methods
//
//--------------------------------------------------------------------------
/**
* @private
* Attempts to connect to the endpoint specified for this channel.
*/
override protected function internalConnect():void
{
super.internalConnect();
_ignoreNetStatusEvents = false;
// 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 RoyaleClient id header.
setRoyaleClientIdOnMessage(msg);
netConnection.call(null, new Responder(resultHandler, faultHandler), msg);
if (Log.isDebug())
_log.debug("'{0}' pinging endpoint.", id);
}
/**
* @private
* Disconnects from the remote destination.
* Because this channel uses a stateless HTTP connection, it sends a fire-and-forget
* message to the server as it disconnects to allow the server to shut down any
* session or other resources that it may be managing on behalf of this channel.
*/
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));
}
// Shut down locally.
setConnected(false);
super.internalDisconnect(rejected);
}
/**
* @private
*/
override protected function internalSend(msgResp:MessageResponder):void
{
handleReconnectWithSessionId(); // Adjust the session id, in case it's needed.
super.internalSend(msgResp);
}
/**
* @private
* Shuts down the underlying NetConnection for the AMFChannel.
* The reason this override is necessary is because the NetConnection may dispatch
* a NetStatusEvent after it has been closed and if we're not registered to listen for
* that event the Player will throw an RTE.
* The only time this can occur when the channel has been shut down due to a connect
* timeout but an error (i.e. 404) response from the server returns later.
*/
override protected function shutdownNetConnection():void
{
_nc.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
_nc.removeEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
// Leave the NetStatusEvent statusHandler registered but set the ignore flag.
_ignoreNetStatusEvents = true;
_nc.close();
}
/**
* @private
* Called on the status event of the associated NetConnection when there is a
* problem with the connection for this channel.
*/
override protected function statusHandler(event:NetStatusEvent):void
{
if (_ignoreNetStatusEvents)
return; // Ignore NetStatusEvents that are dispatched after the NetConnection has been closed.
var channelFault:ChannelFaultEvent;
if (Log.isDebug())
_log.debug("'{0}' channel got status. {1}", id, event.info.toString());
var handled:Boolean = true;
// We should always have a non-null info object.
if (event.info != null)
{
var info:Object = event.info;
// If the level is error we couldn't communicate with the server.
if (info.level == "error")
{
// Suppress processing of "Client.Data.Underflow" status events which are dispatched for
// any outstanding AMF messages in HTTP responses that are not fully received because
// the underlying connection has closed mid-response.
if (info.code == "Client.Data.UnderFlow")
{
if (Log.isDebug())
_log.debug("'{0}' channel received a 'Client.Data.Underflow' status event.");
return; // Skip further processing.
}
if (connected)
{
if (info.code.indexOf("Call.Failed") != -1)
{
channelFault = ChannelFaultEvent.createEvent(this,
false, "Channel.Call.Failed", info.level,
info.code + ": " + info.description)
channelFault.rootCause = info;
// Dispatch the fault.
dispatchEvent(channelFault);
}
/*
* A NetConnection.Call.Failed indicates that the server is
* not running or the URL to the channel endpoint is incorrect.
*
* If we didn't receive a NetConnection.Call.Failed, and the status
* info object has a level of "error" then we must have received one
* of:
* NetConnection.Connect.AppShutdown
* NetConnection.Connect.Failed
* NetConnection.Connect.Rejected
* None of these have anything to do with call processing.
*
* In any case, at this point we need to indicate to the channel that
* it is disconnected which may trigger failover/hunting.
*/
internalDisconnect();
}
else
{
channelFault = ChannelFaultEvent.createEvent(this,
false, "Channel.Connect.Failed", info.level,
info.code + ": " + info.description + ": url: '" + endpoint + "'");
channelFault.rootCause = info;
connectFailed(channelFault);
}
}
else
{
// Ignore NetConnection.Connect.Closed events when the
// Channel is in the process of failing over to another url but
// it receives a delayed NetConnection.Connect.Closed for the
// previous failed url.
if (!connected)
handled = (info.level == "status" && info.code.indexOf("Connect.Closed") != -1);
else
handled = false;
}
}
else
{
handled = false;
}
// If we haven't handled the status event, perform default handling.
if (!handled)
{
var errorText:String = resourceManager.getString(
"messaging", "invalidURL");
connectFailed(ChannelFaultEvent.createEvent(this, false, "Channel.Connect.Failed", "error", errorText + " url: '" + endpoint + "'"));
}
}
//--------------------------------------------------------------------------
//
// Protected Methods
//
//--------------------------------------------------------------------------
/**
* @private
* Used by result and fault handlers to update the url of the underlying
* NetConnection with session id.
*/
protected function handleReconnectWithSessionId():void
{
if (_reconnectingWithSessionId)
{
_reconnectingWithSessionId = false;
shutdownNetConnection();
super.internalConnect(); // To avoid another ping request.
_ignoreNetStatusEvents = false;
}
}
/**
* @private
* Called in response to the server ping to check connectivity.
* An error indicates that although the endpoint uri is reachable the Channel
* is still not able to connect.
*/
protected function faultHandler(msg:ErrorMessage):void
{
if (msg != null)
{
var faultEvent:ChannelFaultEvent = null;
// An authentication fault means we reached it which
// still means we can connect.
if (msg.faultCode == "Client.Authentication")
{
resultHandler(msg);
faultEvent = ChannelFaultEvent.createEvent(this, false, "Channel.Authentication.Error", "warn", msg.faultString);
faultEvent.rootCause = msg;
dispatchEvent(faultEvent);
}
else
{
_log.debug("'{0}' fault handler called. {1}", id, msg.toString());
// Set the server assigned RoyaleClient 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);
}
faultEvent = ChannelFaultEvent.createEvent(this, false,
"Channel.Ping.Failed",
"error",
msg.faultString + " url: '" + endpoint
+ (_appendToURL == null ? "" : _appendToURL + "'") + "'");
faultEvent.rootCause = msg;
connectFailed(faultEvent);
}
}
handleReconnectWithSessionId();
}
/**
* @private
* This method will be called if the ping message sent to test connectivity
* to the server during the connection attempt succeeds.
*/
protected function resultHandler(msg:IMessage):void
{
// Update the ServerConfig with dynamic configuration
if (msg != null)
{
ServerConfig.updateServerConfigData(msg.body as ConfigMap, endpoint);
// Set the server assigned RoyaleClient 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);
}
}
handleReconnectWithSessionId();
connectSuccess();
if (credentials != null && !(msg is ErrorMessage))
setAuthenticated(true);
}
}
}