1//////////////////////////////////////////////////////////////////////////////// 2// 3// ADOBE SYSTEMS INCORPORATED 4// Copyright 2006-2007 Adobe Systems Incorporated 5// All Rights Reserved. 6// 7// NOTICE: Adobe permits you to use, modify, and distribute this file 8// in accordance with the terms of the license agreement accompanying it. 9// 10//////////////////////////////////////////////////////////////////////////////// 11 12package mx.messaging 13{ 14 15import flash.errors.IllegalOperationError; 16import flash.events.EventDispatcher; 17import flash.events.TimerEvent; 18import flash.utils.Dictionary; 19import flash.utils.Timer; 20 21import mx.core.mx_internal; 22import mx.events.PropertyChangeEvent; 23import mx.messaging.channels.NetConnectionChannel; 24import mx.messaging.channels.PollingChannel; 25import mx.messaging.config.ServerConfig; 26import mx.messaging.errors.NoChannelAvailableError; 27import mx.messaging.events.ChannelEvent; 28import mx.messaging.events.ChannelFaultEvent; 29import mx.messaging.events.MessageEvent; 30import mx.messaging.events.MessageFaultEvent; 31import mx.messaging.messages.AcknowledgeMessage; 32import mx.messaging.messages.CommandMessage; 33import mx.messaging.messages.ErrorMessage; 34import mx.messaging.messages.IMessage; 35import mx.resources.IResourceManager; 36import mx.resources.ResourceManager; 37import mx.rpc.AsyncDispatcher; 38import mx.rpc.AsyncToken; 39import mx.rpc.events.AbstractEvent; 40import mx.rpc.events.FaultEvent; 41import mx.rpc.events.ResultEvent; 42import mx.utils.Base64Encoder; 43 44use namespace mx_internal; 45 46[DefaultProperty("channels")] 47 48/** 49 * Dispatched after a Channel in the ChannelSet has connected to its endpoint. 50 * 51 * @eventType mx.messaging.events.ChannelEvent.CONNECT 52 */ 53[Event(name="channelConnect", type="mx.messaging.events.ChannelEvent")] 54 55/** 56 * Dispatched after a Channel in the ChannelSet has disconnected from its 57 * endpoint. 58 * 59 * @eventType mx.messaging.events.ChannelEvent.DISCONNECT 60 */ 61[Event(name="channelDisconnect", type="mx.messaging.events.ChannelEvent")] 62 63/** 64 * Dispatched after a Channel in the ChannelSet has faulted. 65 * 66 * @eventType mx.messaging.events.ChannelFaultEvent.FAULT 67 */ 68[Event(name="channelFault", type="mx.messaging.events.ChannelFaultEvent")] 69 70/** 71 * The result event is dispatched when a login or logout call successfully returns. 72 * @eventType mx.rpc.events.ResultEvent.RESULT 73 */ 74[Event(name="result", type="mx.rpc.events.ResultEvent")] 75 76/** 77 * The fault event is dispatched when a login or logout call fails. 78 * @eventType mx.rpc.events.FaultEvent.FAULT 79 */ 80[Event(name="fault", type="mx.rpc.events.FaultEvent")] 81 82/** 83 * Dispatched when a property of the ChannelSet changes. 84 * 85 * @eventType mx.events.PropertyChangeEvent.PROPERTY_CHANGE 86 */ 87[Event(name="propertyChange", type="mx.events.PropertyChangeEvent")] 88 89[ResourceBundle("messaging")] 90 91/** 92 * The ChannelSet is a set of Channels that are used to send messages to a 93 * target destination. The ChannelSet improves the quality of service on the 94 * client by hunting through its Channels to send messages in the face of 95 * network failures or individual Channel problems. 96 */ 97public class ChannelSet extends EventDispatcher 98{ 99 //-------------------------------------------------------------------------- 100 // 101 // Constructor 102 // 103 //-------------------------------------------------------------------------- 104 105 /** 106 * Constructs a ChannelSet. 107 * If the <code>channelIds</code> argument is provided, the ChannelSet will 108 * use automatically configured Channels obtained via <code>ServerConfig.getChannel()</code> 109 * to reach a destination. 110 * Attempting to manually assign Channels to a ChannelSet that uses configured 111 * Channels is not allowed. 112 * 113 * <p>If the <code>channelIds</code> argument is not provided or is null, 114 * Channels must be manually created and added to the ChannelSet in order 115 * to connect and send messages.</p> 116 * 117 * <p>If the ChannelSet is clustered using url-load-balancing (where each server 118 * declares a unique RTMP or HTTP URL and the client fails over from one URL to 119 * the next), the first time that a Channel in the ChannelSet successfully connects 120 * the ChannelSet will automatically make a request for all of the endpoints across 121 * the cluster for all member Channels and will assign these failover URLs to each 122 * respective Channel. 123 * This allows Channels in the ChannelSet to failover individually, and when failover 124 * options for a specific Channel are exhausted the ChannelSet will advance to the next 125 * Channel in the set to attempt to reconnect.</p> 126 * 127 * <p>Regardless of clustering, if a Channel cannot connect or looses 128 * connectivity, the ChannelSet will advance to its next available Channel 129 * and attempt to reconnect. 130 * This allows the ChannelSet to hunt through Channels that use different 131 * protocols, ports, etc., in search of one that can connect to its endpoint 132 * successfully.</p> 133 * 134 * @param channelIds The ids of configured Channels obtained from ServerConfig for this ChannelSet to 135 * use. If null, Channels must be manually added to the ChannelSet. 136 * 137 * @param clusteredWithURLLoadBalancing True if the Channels in the ChannelSet are clustered 138 * using url load balancing. 139 */ 140 public function ChannelSet(channelIds:Array = null, clusteredWithURLLoadBalancing:Boolean = false) 141 { 142 super(); 143 _clustered = clusteredWithURLLoadBalancing; 144 _connected = false; 145 _connecting = false; 146 _currentChannelIndex = -1; 147 if (channelIds != null) 148 { 149 _channelIds = channelIds; 150 _channels = new Array(_channelIds.length); 151 _configured = true; 152 } 153 else 154 { 155 _channels = []; 156 _configured = false; 157 } 158 _hasRequestedClusterEndpoints = false; 159 _hunting = false; 160 _messageAgents = []; 161 _pendingMessages = new Dictionary(); 162 _pendingSends = []; 163 _shouldBeConnected = false; 164 _shouldHunt = true; 165 } 166 167 //-------------------------------------------------------------------------- 168 // 169 // Variables 170 // 171 //-------------------------------------------------------------------------- 172 173 /** 174 * @private 175 * Helper MessageAgent used for direct authentication. 176 */ 177 private var _authAgent:AuthenticationAgent; 178 179 /** 180 * @private 181 * Flag indicating whether the ChannelSet is in the process of connecting 182 * over the current Channel. 183 */ 184 private var _connecting:Boolean; 185 186 /** 187 * @private 188 * Stored credentials to be set on the member channels. 189 */ 190 private var _credentials:String; 191 192 /** 193 * @private 194 * The character-set encoding used to create the credentials String. 195 */ 196 private var _credentialsCharset:String; 197 198 /** 199 * @private 200 * Current index into the _channels/_channelIds arrays. 201 */ 202 private var _currentChannelIndex:int; 203 204 /** 205 * @private 206 * This flag restricts our cluster request to only happen upon initial 207 * connect to the cluster. 208 */ 209 private var _hasRequestedClusterEndpoints:Boolean; 210 211 /** 212 * @private 213 * Timer used to issue periodic heartbeats to the remote host if the 214 * client is idle, and not actively sending messages. 215 */ 216 private var _heartbeatTimer:Timer; 217 218 /** 219 * @private 220 * Flag indicating whether the ChannelSet is in the process of hunting to a 221 * new Channel; this lets us control the "reconnecting" flag on 222 * CONNECT ChannelEvents that we dispatch when we hunt to a new 223 * Channel that isn't internally failing over. The new Channel doesn't know we're 224 * in a reconnect attempt when it makes its initial connect attempt so this lets 225 * us set "reconnecting" to true on the CONNECT event if it succeeds. 226 */ 227 private var _hunting:Boolean; 228 229 /** 230 * @private 231 * A dictionary of pending messages used to filter out duplicate 232 * messages passed to the ChannelSet to send while it is not connected. 233 * This allows agents to perform message resend behavior (i.e. Consumer resubscribe 234 * attempts) without worrying about duplicate messages queuing up and being sent to 235 * the server once a connection is established. 236 */ 237 private var _pendingMessages:Dictionary; 238 239 /** 240 * @private 241 * An array of PendingSend instances to pass into send() when a connection 242 * is (re)established. 243 */ 244 private var _pendingSends:Array; 245 246 /** 247 * @private 248 * A timer used to do a delayed reconnect for NetConnection channels. 249 */ 250 private var _reconnectTimer:Timer = null; 251 252 /** 253 * @private 254 * Flag indicating whether the ChannelSet should be connected. 255 * If true, the ChannelSet will attempt to hunt to the next available 256 * Channel when a disconnect or fault occurs. If false, hunting is not 257 * performed. 258 */ 259 private var _shouldBeConnected:Boolean; 260 261 /** 262 * @private 263 * Flag indicating whether a Channel disconnect/fault should trigger hunting or not; 264 * used when connected Channels are removed from the ChannelSet which should not trigger 265 * hunting. 266 */ 267 private var _shouldHunt:Boolean; 268 269 /** 270 * @private 271 */ 272 private var resourceManager:IResourceManager = 273 ResourceManager.getInstance(); 274 275 //-------------------------------------------------------------------------- 276 // 277 // Properties 278 // 279 //-------------------------------------------------------------------------- 280 281 //---------------------------------- 282 // authenticated 283 //---------------------------------- 284 285 /** 286 * @private 287 */ 288 private var _authenticated:Boolean; 289 290 [Bindable(event="propertyChange")] 291 /** 292 * Indicates whether the ChannelSet has an underlying Channel that successfully 293 * authenticated with its endpoint. 294 */ 295 public function get authenticated():Boolean 296 { 297 return _authenticated; 298 } 299 300 /** 301 * @private 302 */ 303 mx_internal function setAuthenticated(value:Boolean, creds:String, notifyAgents:Boolean=true):void 304 { 305 if (_authenticated != value) 306 { 307 var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "authenticated", _authenticated, value); 308 _authenticated = value; 309 310 if (notifyAgents) 311 { 312 var ma:MessageAgent; 313 for (var i:int = 0; i < _messageAgents.length; i++) 314 { 315 ma = MessageAgent(_messageAgents[i]); 316 ma.mx_internal::setAuthenticated(value, creds); 317 } 318 } 319 320 dispatchEvent(event); 321 } 322 } 323 324 //---------------------------------- 325 // channels 326 //---------------------------------- 327 328 /** 329 * @private 330 */ 331 private var _channels:Array; 332 333 /** 334 * Provides access to the Channels in the ChannelSet. 335 * This property may be used to assign a set of channels at once or channels 336 * may be added directly to the ChannelSet via addChannel() individually. 337 * If this ChannelSet is <code>configured</code> automatically the individual 338 * channels are created lazily and added to this property as needed. 339 * 340 * @throws flash.errors.IllegalOperationError If the ChannelSet is 341 * <code>configured</code>, assigning to this property is not allowed. 342 */ 343 public function get channels():Array 344 { 345 return _channels; 346 } 347 348 [ArrayElementType("mx.messaging.Channel")] 349 /** 350 * @private 351 */ 352 public function set channels(values:Array):void 353 { 354 if (configured) 355 { 356 var message:String = resourceManager.getString( 357 "messaging", "cannotAddWhenConfigured"); 358 throw new IllegalOperationError(message); 359 } 360 361 // Remove existing channels 362 var channelsToRemove:Array = _channels.slice(); 363 var n:int = channelsToRemove.length; 364 for (var i:int = 0; i < n; i++) 365 { 366 removeChannel(channelsToRemove[i]); 367 } 368 369 // Add new channels 370 if (values != null && values.length > 0) 371 { 372 var m:int = values.length; 373 for (var j:int = 0; j < m; j++) 374 { 375 addChannel(values[j]); 376 } 377 } 378 } 379 380 //---------------------------------- 381 // channelIds 382 //---------------------------------- 383 384 /** 385 * @private 386 */ 387 private var _channelIds:Array; 388 389 /** 390 * The ids of the Channels used by the ChannelSet. 391 */ 392 public function get channelIds():Array 393 { 394 if (_channelIds != null) 395 { 396 return _channelIds; 397 } 398 else 399 { 400 var ids:Array = []; 401 var n:int = _channels.length; 402 for (var i:int = 0; i < n; i++) 403 { 404 if (_channels[i] != null) 405 ids.push(_channels[i].id); 406 else 407 ids.push(null); 408 } 409 return ids; 410 } 411 } 412 413 //---------------------------------- 414 // currentChannel 415 //---------------------------------- 416 417 /** 418 * @private 419 */ 420 private var _currentChannel:Channel; 421 422 /** 423 * Returns the current Channel for the ChannelSet. 424 */ 425 public function get currentChannel():Channel 426 { 427 return _currentChannel; 428 } 429 430 //---------------------------------- 431 // channelFailoverURIs 432 //---------------------------------- 433 434 /** 435 * @private 436 */ 437 private var _channelFailoverURIs:Object; 438 439 /** 440 * @private 441 * Map of arrays of failoverURIs keyed by channel id for the Channels in this ChannelSet. 442 * This property is assigned to by the ClusterMessageResponder in order to update the 443 * member Channels with their failoverURIs. 444 */ 445 mx_internal function get channelFailoverURIs():Object 446 { 447 return _channelFailoverURIs; 448 } 449 450 /** 451 * @private 452 */ 453 mx_internal function set channelFailoverURIs(value:Object):void 454 { 455 _channelFailoverURIs = value; 456 // Update any existing Channels in the set with their current failover endpoint URIs. 457 var n:int = _channels.length; 458 for (var i:int = 0; i < n; i++) 459 { 460 var channel:Channel = _channels[i]; 461 if (channel == null) 462 { 463 break; // The rest of the Channels have not been loaded yet. 464 } 465 else if (_channelFailoverURIs[channel.id] != null) 466 { 467 channel.failoverURIs = _channelFailoverURIs[channel.id]; 468 } 469 } 470 } 471 472 //---------------------------------- 473 // configured 474 //---------------------------------- 475 476 /** 477 * @private 478 */ 479 private var _configured:Boolean; 480 481 /** 482 * Indicates whether the ChannelSet is using automatically configured 483 * Channels or manually assigned Channels. 484 */ 485 mx_internal function get configured():Boolean 486 { 487 return _configured; 488 } 489 490 //---------------------------------- 491 // connected 492 //---------------------------------- 493 494 /** 495 * @private 496 */ 497 private var _connected:Boolean; 498 499 [Bindable(event="propertyChange")] 500 /** 501 * Indicates whether the ChannelSet is connected. 502 */ 503 public function get connected():Boolean 504 { 505 return _connected; 506 } 507 508 /** 509 * @private 510 */ 511 protected function setConnected(value:Boolean):void 512 { 513 if (_connected != value) 514 { 515 var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "connected", _connected, value) 516 _connected = value; 517 dispatchEvent(event); 518 setAuthenticated(value && currentChannel && currentChannel.authenticated, _credentials, false /* Agents also listen for channel disconnects */); 519 if (!connected) 520 { 521 unscheduleHeartbeat(); 522 } 523 else if (heartbeatInterval > 0) 524 { 525 scheduleHeartbeat(); 526 } 527 } 528 } 529 530 //---------------------------------- 531 // clustered 532 //---------------------------------- 533 534 /** 535 * @private 536 */ 537 private var _clustered:Boolean; 538 539 /** 540 * Indicates whether the ChannelSet targets a clustered destination. 541 * If true, upon a successful connection the ChannelSet will query the 542 * destination for all clustered endpoints for its Channels and will assign 543 * failoverURIs to them. 544 * Channel ids are used to assign failoverURIs to the proper Channel instances 545 * so this requires that all Channels in the ChannelSet have non-null ids and an 546 * Error will be thrown when this property is set to true if this is not the case. 547 * If the ChannelSet is not using url load balancing on the client this 548 * property should not be set to true. 549 */ 550 public function get clustered():Boolean 551 { 552 return _clustered; 553 } 554 555 /** 556 * @private 557 */ 558 public function set clustered(value:Boolean):void 559 { 560 if (_clustered != value) 561 { 562 if (value) 563 { 564 // Cannot have a clustered ChannelSet that contains Channels with null ids. 565 var ids:Array = channelIds; 566 var n:int = ids.length; 567 for (var i:int = 0; i < n; i++) 568 { 569 if (ids[i] == null) 570 { 571 var message:String = resourceManager.getString( 572 "messaging", "cannotSetClusteredWithdNullChannelIds"); 573 throw new IllegalOperationError(message); 574 } 575 } 576 } 577 _clustered = value; 578 } 579 } 580 581 //---------------------------------- 582 // heartbeatInterval 583 //---------------------------------- 584 585 /** 586 * @private 587 */ 588 private var _heartbeatInterval:int = 0; 589 590 /** 591 * The number of milliseconds between heartbeats sent to the remote 592 * host while this ChannelSet is actively connected but idle. 593 * Any outbound message traffic will delay heartbeats temporarily, with 594 * this number of milliseconds elapsing after the last sent message before 595 * the next heartbeat is issued. 596 * <p> 597 * This property is useful for applications that connect to a remote host 598 * to received pushed updates and are not actively sending any messages, but 599 * still wish to be notified of a dropped connection even when the networking 600 * layer fails to provide such notification directly. By issuing periodic 601 * heartbeats the client can force the networking layer to report a timeout 602 * if the underlying connection has dropped without notification and the 603 * application can respond to the disconnect appropriately. 604 * </p> 605 * <p> 606 * Any non-positive value disables heartbeats to the remote host. 607 * The default value is 0 indicating that heartbeats are disabled. 608 * If the application sets this value it should prefer a longer rather than 609 * shorter interval, to avoid placing unnecessary load on the remote host. 610 * As an illustrative example, low-level TCP socket keep-alives generally 611 * default to an interval of 2 hours. That is a longer interval than most 612 * applications that enable heartbeats will likely want to use, but it 613 * serves as a clear precedent to prefer a longer interval over a shorter 614 * interval. 615 * </p> 616 * <p> 617 * If the currently connected underlying Channel issues poll requests to 618 * the remote host, heartbeats are suppressed because the periodic poll 619 * requests effectively take their place.</p> 620 */ 621 public function get heartbeatInterval():int 622 { 623 return _heartbeatInterval; 624 } 625 626 /** 627 * @private 628 */ 629 public function set heartbeatInterval(value:int):void 630 { 631 if (_heartbeatInterval != value) 632 { 633 var event:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "heartbeatInterval", _heartbeatInterval, value); 634 _heartbeatInterval = value; 635 dispatchEvent(event); 636 if (_heartbeatInterval > 0 && connected) 637 { 638 scheduleHeartbeat(); 639 } 640 } 641 } 642 643 //---------------------------------- 644 // initialDestinationId 645 //---------------------------------- 646 647 /** 648 * @private 649 */ 650 private var _initialDestinationId:String; 651 652 /** 653 * Provides access to the initial destination this ChannelSet is used to access. 654 * When the clustered property is true, this value is used to request available failover URIs 655 * for the configured channels for the destination. 656 */ 657 public function get initialDestinationId():String 658 { 659 return _initialDestinationId; 660 } 661 662 /** 663 * @private 664 */ 665 public function set initialDestinationId(value:String):void 666 { 667 _initialDestinationId = value; 668 } 669 670 //---------------------------------- 671 // messageAgents 672 //---------------------------------- 673 674 /** 675 * @private 676 */ 677 private var _messageAgents:Array; 678 679 /** 680 * Provides access to the set of MessageAgents that use this ChannelSet. 681 */ 682 public function get messageAgents():Array 683 { 684 return _messageAgents; 685 } 686 687 //-------------------------------------------------------------------------- 688 // 689 // Overridden Methods 690 // 691 //-------------------------------------------------------------------------- 692 693 /** 694 * Returns a String containing the ids of the Channels in the ChannelSet. 695 * 696 * @return String representation of the ChannelSet. 697 */ 698 override public function toString():String 699 { 700 var s:String = "[ChannelSet "; 701 for (var i:uint = 0; i < _channels.length; i++) 702 { 703 if (_channels[i] != null) 704 s += _channels[i].id + " "; 705 } 706 s += "]"; 707 return s; 708 } 709 710 //-------------------------------------------------------------------------- 711 // 712 // Methods 713 // 714 //-------------------------------------------------------------------------- 715 716 /** 717 * Adds a Channel to the ChannelSet. A Channel with a null id cannot be added 718 * to the ChannelSet if the ChannelSet targets a clustered destination. 719 * 720 * @param channel The Channel to add. 721 * 722 * @throws flash.errors.IllegalOperationError If the ChannelSet is 723 * <code>configured</code>, adding a Channel is not supported. 724 * This error is also thrown if the ChannelSet's <code>clustered</code> property 725 * is <code>true</code> but the Channel has a null id. 726 */ 727 public function addChannel(channel:Channel):void 728 { 729 if (channel == null) 730 return; 731 732 var message:String; 733 734 if (configured) 735 { 736 message = resourceManager.getString( 737 "messaging", "cannotAddWhenConfigured"); 738 throw new IllegalOperationError(message); 739 } 740 741 if (clustered && channel.id == null) 742 { 743 message = resourceManager.getString( 744 "messaging", "cannotAddNullIdChannelWhenClustered"); 745 throw new IllegalOperationError(message); 746 } 747 748 if (_channels.indexOf(channel) != -1) 749 return; // Channel already exists in the set. 750 751 _channels.push(channel); 752 if (_credentials) 753 channel.setCredentials(_credentials, null, _credentialsCharset); 754 } 755 756 /** 757 * Removes a Channel from the ChannelSet. If the Channel to remove is 758 * currently connected and being used by the ChannelSet, it is 759 * disconnected as well as removed. 760 * 761 * @param channel The Channel to remove. 762 * 763 * @throws flash.errors.IllegalOperationError If the ChannelSet is 764 * <code>configured</code>, removing a Channel is not supported. 765 */ 766 public function removeChannel(channel:Channel):void 767 { 768 if (configured) 769 { 770 var message:String = resourceManager.getString( 771 "messaging", "cannotRemoveWhenConfigured"); 772 throw new IllegalOperationError(message); 773 } 774 775 var channelIndex:int = _channels.indexOf(channel); 776 if (channelIndex > -1) 777 { 778 _channels.splice(channelIndex, 1); 779 // If the Channel being removed is currently in use, we need 780 // to null it out for re-hunting, and potentially disconnect it. 781 if ((_currentChannel != null) && (_currentChannel == channel)) 782 { 783 if (connected) 784 { 785 _shouldHunt = false; 786 disconnectChannel(); 787 } 788 _currentChannel = null; 789 _currentChannelIndex = -1; 790 } 791 } 792 } 793 794 /** 795 * Connects a MessageAgent to the ChannelSet. Once connected, the agent 796 * can use the ChannelSet to send messages. 797 * 798 * @param agent The MessageAgent to connect. 799 */ 800 public function connect(agent:MessageAgent):void 801 { 802 if ((agent != null) && (_messageAgents.indexOf(agent) == -1)) 803 { 804 _shouldBeConnected = true; 805 _messageAgents.push(agent); 806 agent.mx_internal::internalSetChannelSet(this); 807 // Wire up agent's channel event listeners to this ChannelSet. 808 addEventListener(ChannelEvent.CONNECT, agent.channelConnectHandler); 809 addEventListener(ChannelEvent.DISCONNECT, agent.channelDisconnectHandler); 810 addEventListener(ChannelFaultEvent.FAULT, agent.channelFaultHandler); 811 812 // If the ChannelSet is already connected, notify the agent. 813 if (connected && !agent.needsConfig) 814 agent.channelConnectHandler(ChannelEvent.createEvent(ChannelEvent.CONNECT, 815 _currentChannel, 816 false, 817 false, 818 connected)); 819 } 820 } 821 822 /** 823 * Disconnects a specific MessageAgent from the ChannelSet. If this is the 824 * last MessageAgent using the ChannelSet and the current Channel in the set is 825 * connected, the Channel will physically disconnect from the server. 826 * 827 * @param agent The MessageAgent to disconnect. 828 */ 829 public function disconnect(agent:MessageAgent):void 830 { 831 if (agent == null) // Disconnect the ChannelSet completely. 832 { 833 var allMessageAgents:Array = _messageAgents.slice(); 834 var n:int = allMessageAgents.length; 835 for (var i:int = 0; i < n; i++) 836 { 837 allMessageAgents[i].disconnect(); 838 } 839 if (_authAgent != null) 840 { 841 _authAgent.state = AuthenticationAgent.SHUTDOWN_STATE; 842 _authAgent = null; 843 } 844 } 845 else // Disconnect a specific MessageAgent. 846 { 847 var agentIndex:int = agent != null ? _messageAgents.indexOf(agent) : -1; 848 if (agentIndex != -1) 849 { 850 _messageAgents.splice(agentIndex, 1); 851 // Remove the agent as a listener to this ChannelSet. 852 removeEventListener(ChannelEvent.CONNECT, agent.channelConnectHandler); 853 removeEventListener(ChannelEvent.DISCONNECT, agent.channelDisconnectHandler); 854 removeEventListener(ChannelFaultEvent.FAULT, agent.channelFaultHandler); 855 856 if (connected || _connecting) // Notify the agent of the disconnect. 857 { 858 agent.channelDisconnectHandler(ChannelEvent.createEvent(ChannelEvent.DISCONNECT, 859 _currentChannel, false)); 860 } 861 else // Remove any pending sends for this agent. 862 { 863 var n2:int = _pendingSends.length; 864 for (var j:int = 0; j < n2; j++) 865 { 866 var ps:PendingSend = PendingSend(_pendingSends[j]); 867 if (ps.agent == agent) 868 { 869 _pendingSends.splice(j, 1); 870 j--; 871 n2--; 872 delete _pendingMessages[ps.message]; 873 } 874 } 875 } 876 // Shut down the underlying Channel connection if this ChannelSet has 877 // no more agents using it. 878 if (_messageAgents.length == 0) 879 { 880 _shouldBeConnected = false; 881 _currentChannelIndex = -1; 882 if (connected) 883 disconnectChannel(); 884 } 885 886 // Null out automatically assigned ChannelSet on agent; if manually assigned leave it alone. 887 if (agent.mx_internal::channelSetMode == MessageAgent.mx_internal::AUTO_CONFIGURED_CHANNELSET) 888 agent.mx_internal::internalSetChannelSet(null); 889 } 890 } 891 } 892 893 /** 894 * Disconnects all associated MessageAgents and disconnects any underlying Channel that 895 * is connected. 896 * Unlike <code>disconnect(MessageAgent)</code> which is invoked by the disconnect implementations 897 * of specific service components, this method provides a single, convenient point to shut down 898 * connectivity between the client and server. 899 */ 900 public function disconnectAll():void 901 { 902 disconnect(null); 903 } 904 905 /** 906 * Handles a CONNECT ChannelEvent and redispatches the event. 907 * 908 * @param event The ChannelEvent. 909 */ 910 public function channelConnectHandler(event:ChannelEvent):void 911 { 912 _connecting = false; 913 _connected = true; // Set internally to allow us to send pending messages before dispatching the connect event. 914 _currentChannelIndex = -1; // Reset index so that future disconnects are followed by hunting through all available options in order. 915 916 // Send any pending messages. 917 while (_pendingSends.length > 0) 918 { 919 var ps:PendingSend = PendingSend(_pendingSends.shift()); 920 delete _pendingMessages[ps.message]; 921 922 var command:CommandMessage = ps.message as CommandMessage; 923 if (command != null) 924 { 925 // Filter out any commands to trigger connection establishment, and ack them locally. 926 if (command.operation == CommandMessage.TRIGGER_CONNECT_OPERATION) 927 { 928 var ack:AcknowledgeMessage = new AcknowledgeMessage(); 929 ack.clientId = ps.agent.clientId; 930 ack.correlationId = command.messageId; 931 ps.agent.acknowledge(ack, command); 932 continue; 933 } 934 935 if (!ps.agent.configRequested && ps.agent.needsConfig && 936 (command.operation == CommandMessage.CLIENT_PING_OPERATION)) 937 { 938 command.headers[CommandMessage.NEEDS_CONFIG_HEADER] = true; 939 ps.agent.configRequested = true; 940 } 941 } 942 943 send(ps.agent, ps.message); 944 } 945 946 if (_hunting) 947 { 948 event.reconnecting = true; 949 _hunting = false; 950 } 951 952 // Redispatch Channel connect event. 953 dispatchEvent(event); 954 // Dispatch delayed "connected" property change event. 955 var connectedChangeEvent:PropertyChangeEvent = PropertyChangeEvent.createUpdateEvent(this, "connected", false, true) 956 dispatchEvent(connectedChangeEvent); 957 } 958 959 /** 960 * Handles a DISCONNECT ChannelEvent and redispatches the event. 961 * 962 * @param event The ChannelEvent. 963 */ 964 public function channelDisconnectHandler(event:ChannelEvent):void 965 { 966 _connecting = false; 967 setConnected(false); 968 969 // If we should be connected and the Channel isn't failing over 970 // internally and wasn't rejected, hunt and try to reconnect. 971 if (_shouldBeConnected && !event.reconnecting && !event.rejected) 972 { 973 if (_shouldHunt && hunt()) 974 { 975 event.reconnecting = true; 976 dispatchEvent(event); 977 if (_currentChannel is NetConnectionChannel) 978 { 979 // Insert slight delay for reconnect to allow NetConnection 980 // based channels to shut down and clean up in preparation 981 // for our next connect attempt. 982 if (_reconnectTimer == null) 983 { 984 _reconnectTimer = new Timer(1, 1); 985 _reconnectTimer.addEventListener(TimerEvent.TIMER, reconnectChannel); 986 _reconnectTimer.start(); 987 } 988 } 989 else // No need to wait with other channel types. 990 { 991 connectChannel(); 992 } 993 } 994 else // No more hunting options; give up and fault pending sends. 995 { 996 dispatchEvent(event); 997 faultPendingSends(event); 998 } 999 } 1000 else 1001 { 1002 dispatchEvent(event); 1003 // If the underlying Channel was rejected, fault pending sends. 1004 if (event.rejected) 1005 faultPendingSends(event); 1006 } 1007 // Flip this back to true in case it was turned off by an explicit Channel removal 1008 // that triggered the current disconnect event. 1009 _shouldHunt = true; 1010 } 1011 1012 /** 1013 * Handles a ChannelFaultEvent and redispatches the event. 1014 * 1015 * @param event The ChannelFaultEvent. 1016 */ 1017 public function channelFaultHandler(event:ChannelFaultEvent):void 1018 { 1019 if (event.channel.connected) 1020 { 1021 dispatchEvent(event); 1022 } 1023 else // The channel fault has resulted in disconnecting. 1024 { 1025 _connecting = false; 1026 setConnected(false); 1027 1028 // If we should be connected and the Channel isn't failing over 1029 // internally, hunt and try to reconnect. 1030 if (_shouldBeConnected && !event.reconnecting && !event.rejected) 1031 { 1032 if (hunt()) 1033 { 1034 event.reconnecting = true; 1035 dispatchEvent(event); 1036 if (_currentChannel is NetConnectionChannel) 1037 { 1038 // Insert slight delay for reconnect to allow 1039 // NetConnection based channels to shut down and clean 1040 // up in preparation for our next connect attempt. 1041 if (_reconnectTimer == null) 1042 { 1043 _reconnectTimer = new Timer(1, 1); 1044 _reconnectTimer.addEventListener(TimerEvent.TIMER, reconnectChannel); 1045 _reconnectTimer.start(); 1046 } 1047 } 1048 else // No need to wait with other channel types. 1049 { 1050 connectChannel(); 1051 } 1052 } 1053 else // No more hunting options; give up and fault pending sends. 1054 { 1055 dispatchEvent(event); 1056 faultPendingSends(event); 1057 } 1058 } 1059 else 1060 { 1061 dispatchEvent(event); 1062 // If the underlying Channel was rejected, fault pending sends. 1063 if (event.rejected) 1064 faultPendingSends(event); 1065 } 1066 } 1067 } 1068 1069 /** 1070 * Authenticates the ChannelSet with the server using the provided credentials. 1071 * Unlike other operations on Channels and the ChannelSet, this operation returns an 1072 * AsyncToken that client code may add a responder to in order to handle success or 1073 * failure directly. 1074 * If the ChannelSet is not connected to the server when this method is invoked it will 1075 * trigger a connect attempt, and if successful, send the login command to the server. 1076 * Only one login or logout operation may be pending at a time and overlapping calls will 1077 * generate an IllegalOperationError. 1078 * Invoking login when the ChannelSet is already authenticated will generate also generate 1079 * an IllegalOperationError. 1080 * 1081 * @param username The username. 1082 * @param password The password. 1083 * @param charset The character set encoding to use while encoding the 1084 * credentials. The default is null, which implies the legacy charset of 1085 * ISO-Latin-1. The only other supported charset is "UTF-8". 1086 * 1087 * @return Returns a token that client code may add a responder to in order to handle 1088 * success or failure directly. 1089 * 1090 * @throws flash.errors.IllegalOperationError in two situations; if the ChannelSet is 1091 * already authenticated, or if a login or logout operation is currently in progress. 1092 */ 1093 public function login(username:String, password:String, charset:String=null):AsyncToken 1094 { 1095 if (authenticated) 1096 throw new IllegalOperationError("ChannelSet is already authenticated."); 1097 1098 if ((_authAgent != null) && (_authAgent.state != AuthenticationAgent.LOGGED_OUT_STATE)) 1099 throw new IllegalOperationError("ChannelSet is in the process of logging in or logging out."); 1100 1101 if (charset != Base64Encoder.CHARSET_UTF_8) 1102 charset = null; // Use legacy charset, ISO-Latin-1. 1103 1104 var credentials:String = null; 1105 if (username != null && password != null) 1106 { 1107 var rawCredentials:String = username + ":" + password; 1108 var encoder:Base64Encoder = new Base64Encoder(); 1109 if (charset == Base64Encoder.CHARSET_UTF_8) 1110 encoder.encodeUTFBytes(rawCredentials); 1111 else 1112 encoder.encode(rawCredentials); 1113 credentials = encoder.drain(); 1114 } 1115 1116 var msg:CommandMessage = new CommandMessage(); 1117 msg.operation = CommandMessage.LOGIN_OPERATION; 1118 msg.body = credentials; 1119 if (charset != null) 1120 msg.headers[CommandMessage.CREDENTIALS_CHARSET_HEADER] = charset; 1121 1122 // A non-null, non-empty destination is required to send using an agent. 1123 // This value is ignored on the server and the message must be handled by an AuthenticationService. 1124 msg.destination = "auth"; 1125 1126 var token:AsyncToken = new AsyncToken(msg); 1127 if (_authAgent == null) 1128 _authAgent = new AuthenticationAgent(this); 1129 _authAgent.registerToken(token); 1130 _authAgent.state = AuthenticationAgent.LOGGING_IN_STATE; 1131 send(_authAgent, msg); 1132 return token; 1133 } 1134 1135 /** 1136 * Logs the ChannelSet out from the server. Unlike other operations on Channels 1137 * and the ChannelSet, this operation returns an AsyncToken that client code may 1138 * add a responder to in order to handle success or failure directly. 1139 * If logout is successful any credentials that have been cached for use in 1140 * automatic reconnects are cleared for the ChannelSet and its Channels and their 1141 * authenticated state is set to false. 1142 * If the ChannelSet is not connected to the server when this method is invoked it 1143 * will trigger a connect attempt, and if successful, send a logout command to the server. 1144 * 1145 * <p>The MessageAgent argument is present to support legacy logout behavior and client code that 1146 * invokes this method should not pass a MessageAgent reference. Just invoke <code>logout()</code> 1147 * passing no arguments.</p> 1148 * 1149 * <p>This method is also invoked by service components from their <code>logout()</code> 1150 * methods, and these components pass a MessageAgent reference to this method when they logout. 1151 * The presence of this argument is the trigger to execute legacy logout behavior that differs 1152 * from the new behavior described above. 1153 * Legacy behavior only sends a logout request to the server if the client is connected 1154 * and authenticated. 1155 * If these conditions are not met the legacy behavior for this method is to do nothing other 1156 * than clear any credentials that have been cached for use in automatic reconnects.</p> 1157 * 1158 * @param agent Legacy argument. The MessageAgent that is initiating the logout. 1159 * 1160 * @return Returns a token that client code may 1161 * add a responder to in order to handle success or failure directly. 1162 * 1163 * @throws flash.errors.IllegalOperationError if a login or logout operation is currently in progress. 1164 */ 1165 public function logout(agent:MessageAgent=null):AsyncToken 1166 { 1167 _credentials = null; 1168 if (agent == null) 1169 { 1170 if ((_authAgent != null) && (_authAgent.state == AuthenticationAgent.LOGGING_OUT_STATE 1171 || _authAgent.state == AuthenticationAgent.LOGGING_IN_STATE)) 1172 throw new IllegalOperationError("ChannelSet is in the process of logging in or logging out."); 1173 1174 // Clear out current credentials on the client. 1175 var n:int = _messageAgents.length; 1176 var i:int = 0; 1177 for (; i < n; i++) 1178 { 1179 _messageAgents[i].internalSetCredentials(null); 1180 } 1181 n = _channels.length; 1182 for (i = 0; i < n; i++) 1183 { 1184 if (_channels[i] != null) 1185 { 1186 _channels[i].internalSetCredentials(null); 1187 if (_channels[i] is PollingChannel) 1188 PollingChannel(_channels[i]).disablePolling(); 1189 } 1190 } 1191 1192 var msg:CommandMessage = new CommandMessage(); 1193 msg.operation = CommandMessage.LOGOUT_OPERATION; 1194 1195 // A non-null, non-empty destination is required to send using an agent. 1196 // This value is ignored on the server and the message must be handled by an AuthenticationService. 1197 msg.destination = "auth"; 1198 1199 var token:AsyncToken = new AsyncToken(msg); 1200 if (_authAgent == null) 1201 _authAgent = new AuthenticationAgent(this); 1202 _authAgent.registerToken(token); 1203 _authAgent.state = AuthenticationAgent.LOGGING_OUT_STATE; 1204 send(_authAgent, msg); 1205 return token; 1206 } 1207 else // Legacy logout logic. 1208 { 1209 var n2:int = _channels.length; 1210 for (var i2:int = 0; i2 < n2; i2++) 1211 { 1212 if (_channels[i2] != null) 1213 _channels[i2].logout(agent); 1214 } 1215 return null; // Legacy service logout() impls don't expect a token. 1216 } 1217 } 1218 1219 /** 1220 * Sends a message from a MessageAgent over the currently connected Channel. 1221 * 1222 * @param agent The MessageAgent sending the message. 1223 * 1224 * @param message The Message to send. 1225 * 1226 * @throws mx.messaging.errors.NoChannelAvailableError If the ChannelSet has no internal 1227 * Channels to use. 1228 */ 1229 public function send(agent:MessageAgent, message:IMessage):void 1230 { 1231 if (_currentChannel != null && _currentChannel.connected && !agent.needsConfig) 1232 { 1233 // Filter out any commands to trigger connection establishment, and ack them locally. 1234 if ((message is CommandMessage) && (CommandMessage(message).operation == CommandMessage.TRIGGER_CONNECT_OPERATION)) 1235 { 1236 var ack:AcknowledgeMessage = new AcknowledgeMessage(); 1237 ack.clientId = agent.clientId; 1238 ack.correlationId = message.messageId; 1239 new AsyncDispatcher(agent.acknowledge, [ack, message], 1); 1240 return; 1241 } 1242 1243 // If this ChannelSet targets a clustered destination, request the 1244 // endpoint URIs for the cluster. 1245 if (!_hasRequestedClusterEndpoints && clustered) 1246 { 1247 var msg:CommandMessage = new CommandMessage(); 1248 // Fetch failover URIs for the correct destination. 1249 if (agent is AuthenticationAgent) 1250 { 1251 msg.destination = initialDestinationId; 1252 } 1253 else 1254 { 1255 msg.destination = agent.destination; 1256 } 1257 msg.operation = CommandMessage.CLUSTER_REQUEST_OPERATION; 1258 _currentChannel.sendInternalMessage(new ClusterMessageResponder(msg, this)); 1259 _hasRequestedClusterEndpoints = true; 1260 } 1261 unscheduleHeartbeat(); 1262 _currentChannel.send(agent, message); 1263 scheduleHeartbeat(); 1264 } 1265 else 1266 { 1267 // Filter out duplicate messages here while waiting for the underlying Channel to connect. 1268 if (_pendingMessages[message] == null) 1269 { 1270 _pendingMessages[message] = true; 1271 _pendingSends.push(new PendingSend(agent, message)); 1272 } 1273 1274 if (!_connecting) 1275 { 1276 if ((_currentChannel == null) || (_currentChannelIndex == -1)) 1277 hunt(); 1278 1279 if (_currentChannel is NetConnectionChannel) 1280 { 1281 // Insert a slight delay in case we've hunted to a 1282 // NetConnection channel that doesn't allow a reconnect 1283 // within the same frame as a disconnect. 1284 if (_reconnectTimer == null) 1285 { 1286 _reconnectTimer = new Timer(1, 1); 1287 _reconnectTimer.addEventListener(TimerEvent.TIMER, reconnectChannel); 1288 _reconnectTimer.start(); 1289 } 1290 } 1291 else // No need to wait with other channel types. 1292 { 1293 connectChannel(); 1294 } 1295 } 1296 } 1297 } 1298 1299 /** 1300 * Stores the credentials and passes them through to every connected channel. 1301 * 1302 * @param credentials The credentials for the MessageAgent. 1303 * @param agent The MessageAgent that is setting the credentials. 1304 * @param charset The character set encoding used while encoding the 1305 * credentials. The default is null, which implies the legacy encoding of 1306 * ISO-Latin-1. 1307 * 1308 * @throws flash.errors.IllegalOperationError in two situations; if credentials 1309 * have already been set and an authentication is in progress with the remote 1310 * detination, or if authenticated and the credentials specified don't match 1311 * the currently authenticated credentials. 1312 */ 1313 public function setCredentials(credentials:String, agent:MessageAgent, charset:String=null):void 1314 { 1315 _credentials = credentials; 1316 var n:int = _channels.length; 1317 for (var i:int = 0; i < n; i++) 1318 { 1319 if (_channels[i] != null) 1320 _channels[i].setCredentials(_credentials, agent, charset); 1321 } 1322 } 1323 1324 //-------------------------------------------------------------------------- 1325 // 1326 // Internal Methods 1327 // 1328 //-------------------------------------------------------------------------- 1329 1330 /** 1331 * @private 1332 * Handles a successful login or logout operation for the ChannelSet. 1333 */ 1334 mx_internal function authenticationSuccess(agent:AuthenticationAgent, token:AsyncToken, ackMessage:AcknowledgeMessage):void 1335 { 1336 // Reset authentication state depending on whether a login or logout was successful. 1337 var command:CommandMessage = CommandMessage(token.message); 1338 var handlingLogin:Boolean = (command.operation == CommandMessage.LOGIN_OPERATION); 1339 var creds:String = (handlingLogin) ? String(command.body) : null; 1340 1341 if (handlingLogin) 1342 { 1343 // First, sync everything with the current credentials. 1344 _credentials = creds; 1345 var n:int = _messageAgents.length; 1346 var i:int = 0; 1347 for (; i < n; i++) 1348 { 1349 _messageAgents[i].internalSetCredentials(creds); 1350 } 1351 n = _channels.length; 1352 for (i = 0; i < n; i++) 1353 { 1354 if (_channels[i] != null) 1355 _channels[i].internalSetCredentials(creds); 1356 } 1357 1358 agent.state = AuthenticationAgent.LOGGED_IN_STATE; 1359 // Flip the currently connected channel to authenticated; this percolates 1360 // back up through the ChannelSet and agent's authenticated properties. 1361 currentChannel.setAuthenticated(true); 1362 } 1363 else // Logout. 1364 { 1365 // Shutdown the current logged out agent. 1366 agent.state = AuthenticationAgent.SHUTDOWN_STATE; 1367 _authAgent = null; 1368 disconnect(agent); 1369 1370 // Flip current channel to *not* authenticated; this percolates 1371 // back up through the ChannelSet and agent's authenticated properties. 1372 currentChannel.setAuthenticated(false); 1373 } 1374 1375 // Notify. 1376 var resultEvent:ResultEvent = ResultEvent.createEvent(ackMessage.body, token, ackMessage); 1377 dispatchRPCEvent(resultEvent); 1378 } 1379 1380 /** 1381 * @private 1382 * Handles a failed login or logout operation for the ChannelSet. 1383 */ 1384 mx_internal function authenticationFailure(agent:AuthenticationAgent, token:AsyncToken, faultMessage:ErrorMessage):void 1385 { 1386 var messageFaultEvent:MessageFaultEvent = MessageFaultEvent.createEvent(faultMessage); 1387 var faultEvent:FaultEvent = FaultEvent.createEventFromMessageFault(messageFaultEvent, token); 1388 // Leave the ChannelSet in its current auth state and dispose of the auth agent that failed. 1389 agent.state = AuthenticationAgent.SHUTDOWN_STATE; 1390 _authAgent = null; 1391 disconnect(agent); 1392 // And notify. 1393 dispatchRPCEvent(faultEvent); 1394 } 1395 1396 //-------------------------------------------------------------------------- 1397 // 1398 // Protected Methods 1399 // 1400 //-------------------------------------------------------------------------- 1401 1402 /** 1403 * @private 1404 * Helper method to fault pending messages. 1405 * The ErrorMessage is tagged with a __retryable__ header to indicate that 1406 * the error was due to connectivity problems on the client as opposed to 1407 * a server error response and the message can be retried (resent). 1408 * 1409 * @param event A ChannelEvent.DISCONNECT or a ChannelFaultEvent that is the root cause 1410 * for faulting these pending sends. 1411 */ 1412 protected function faultPendingSends(event:ChannelEvent):void 1413 { 1414 while (_pendingSends.length > 0) 1415 { 1416 var ps:PendingSend = _pendingSends.shift() as PendingSend; 1417 var pendingMsg:IMessage = ps.message; 1418 delete _pendingMessages[pendingMsg]; 1419 // Fault the message to its agent. 1420 var errorMsg:ErrorMessage = new ErrorMessage(); 1421 errorMsg.correlationId = pendingMsg.messageId; 1422 errorMsg.headers[ErrorMessage.RETRYABLE_HINT_HEADER] = true; 1423 errorMsg.faultCode = "Client.Error.MessageSend"; 1424 errorMsg.faultString = resourceManager.getString( 1425 "messaging", "sendFailed"); 1426 if (event is ChannelFaultEvent) 1427 { 1428 var faultEvent:ChannelFaultEvent = event as ChannelFaultEvent; 1429 errorMsg.faultDetail = faultEvent.faultCode + " " + 1430 faultEvent.faultString + " " + 1431 faultEvent.faultDetail; 1432 // This is to make streaming channels report authentication fault 1433 // codes correctly as they don't report connected until streaming 1434 // connection is established and hence end up here. 1435 if (faultEvent.faultCode == "Channel.Authentication.Error") 1436 errorMsg.faultCode = faultEvent.faultCode; 1437 } 1438 // ChannelEvent.DISCONNECT is treated the same as never 1439 // being able to connect at all. 1440 else 1441 { 1442 errorMsg.faultDetail = resourceManager.getString( 1443 "messaging", "cannotConnectToDestination"); 1444 } 1445 errorMsg.rootCause = event; 1446 ps.agent.fault(errorMsg, pendingMsg); 1447 } 1448 } 1449 1450 /** 1451 * Redispatches message events from the currently connected Channel. 1452 * 1453 * @param event The MessageEvent from the Channel. 1454 */ 1455 protected function messageHandler(event:MessageEvent):void 1456 { 1457 dispatchEvent(event); 1458 } 1459 1460 /** 1461 * @private 1462 * Schedules a heartbeat to be sent in heartbeatInterval milliseconds. 1463 */ 1464 protected function scheduleHeartbeat():void 1465 { 1466 if (_heartbeatTimer == null && heartbeatInterval > 0) 1467 { 1468 _heartbeatTimer = new Timer(heartbeatInterval, 1); 1469 _heartbeatTimer.addEventListener(TimerEvent.TIMER, sendHeartbeatHandler); 1470 _heartbeatTimer.start(); 1471 } 1472 } 1473 1474 /** 1475 * @private 1476 * Handles a heartbeat timer event by conditionally sending a heartbeat 1477 * and scheduling the next. 1478 */ 1479 protected function sendHeartbeatHandler(event:TimerEvent):void 1480 { 1481 unscheduleHeartbeat(); 1482 if (currentChannel != null) 1483 { 1484 sendHeartbeat(); 1485 scheduleHeartbeat(); 1486 } 1487 } 1488 1489 /** 1490 * @private 1491 * Sends a heartbeat request. 1492 */ 1493 protected function sendHeartbeat():void 1494 { 1495 // Current channel may be actively polling, which suppresses explicit heartbeats. 1496 var pollingChannel:PollingChannel = currentChannel as PollingChannel; 1497 if (pollingChannel != null && pollingChannel._shouldPoll) return; 1498 // Issue an explicit heartbeat and schedule the next. 1499 var heartbeat:CommandMessage = new CommandMessage(); 1500 heartbeat.operation = CommandMessage.CLIENT_PING_OPERATION; 1501 heartbeat.headers[CommandMessage.HEARTBEAT_HEADER] = true; 1502 currentChannel.sendInternalMessage(new MessageResponder(null /* no agent */, heartbeat)); 1503 } 1504 1505 /** 1506 * @private 1507 * Unschedules any currently scheduled pending heartbeat. 1508 */ 1509 protected function unscheduleHeartbeat():void 1510 { 1511 if (_heartbeatTimer != null) 1512 { 1513 _heartbeatTimer.stop(); 1514 _heartbeatTimer.removeEventListener(TimerEvent.TIMER, sendHeartbeatHandler); 1515 _heartbeatTimer = null; 1516 } 1517 } 1518 1519 //-------------------------------------------------------------------------- 1520 // 1521 // Private Methods 1522 // 1523 //-------------------------------------------------------------------------- 1524 1525 /** 1526 * @private 1527 * Helper method to connect the current internal Channel. 1528 */ 1529 private function connectChannel():void 1530 { 1531 _connecting = true; 1532 _currentChannel.connect(this); 1533 // Listen for any server pushed messages on the Channel. 1534 _currentChannel.addEventListener(MessageEvent.MESSAGE, messageHandler); 1535 } 1536 1537 /** 1538 * @private 1539 * Helper method to disconnect the current internal Channel. 1540 */ 1541 private function disconnectChannel():void 1542 { 1543 _connecting = false; 1544 // Stop listening for server pushed messages on the Channel. 1545 _currentChannel.removeEventListener(MessageEvent.MESSAGE, messageHandler); 1546 _currentChannel.disconnect(this); 1547 } 1548 1549 /** 1550 * @private 1551 * Helper method to dispatch authentication-related RPC events. 1552 * 1553 * @param event The event to dispatch. 1554 */ 1555 private function dispatchRPCEvent(event:AbstractEvent):void 1556 { 1557 event.callTokenResponders(); 1558 dispatchEvent(event); 1559 } 1560 1561 /** 1562 * @private 1563 * Helper method to hunt to the next available internal Channel for the 1564 * ChannelSet. 1565 * 1566 * @return True if hunting to the next available Channel was successful; false if hunting 1567 * exhausted available channels and has reset to the beginning of the set. 1568 * 1569 * @throws mx.messaging.errors.NoChannelAvailableError If the ChannelSet has no internal 1570 * Channels to use. 1571 */ 1572 private function hunt():Boolean 1573 { 1574 if (_channels.length == 0) 1575 { 1576 var message:String = resourceManager.getString( 1577 "messaging", "noAvailableChannels"); 1578 throw new NoChannelAvailableError(message); 1579 } 1580 1581 // Unwire from the current channel. 1582 if (_currentChannel != null) 1583 disconnectChannel(); 1584 1585 // Advance to next channel, and reset to beginning if all Channels in the set 1586 // have been attempted. 1587 if (++_currentChannelIndex >= _channels.length) 1588 { 1589 _currentChannelIndex = -1; 1590 return false; 1591 } 1592 1593 // If we've advanced past the first channel, indicate that we're hunting. 1594 if (_currentChannelIndex > 0) 1595 _hunting = true; 1596 1597 // Set current channel. 1598 if (configured) 1599 { 1600 if (_channels[_currentChannelIndex] != null) 1601 { 1602 _currentChannel = _channels[_currentChannelIndex]; 1603 } 1604 else 1605 { 1606 _currentChannel = ServerConfig.getChannel(_channelIds[ 1607 _currentChannelIndex], _clustered); 1608 _currentChannel.setCredentials(_credentials); 1609 _channels[_currentChannelIndex] = _currentChannel; 1610 } 1611 } 1612 else 1613 { 1614 _currentChannel = _channels[_currentChannelIndex]; 1615 } 1616 1617 // Ensure that the current channel is assigned failover URIs it if was lazily instantiated. 1618 if ((_channelFailoverURIs != null) && (_channelFailoverURIs[_currentChannel.id] != null)) 1619 _currentChannel.failoverURIs = _channelFailoverURIs[_currentChannel.id]; 1620 1621 return true; 1622 } 1623 1624 /** 1625 * @private 1626 * This method is invoked by a timer and it works around a reconnect issue 1627 * with NetConnection based channels within a single frame by reconnecting after a slight delay. 1628 */ 1629 private function reconnectChannel(event:TimerEvent):void 1630 { 1631 _reconnectTimer.stop(); 1632 _reconnectTimer.removeEventListener(TimerEvent.TIMER, reconnectChannel); 1633 _reconnectTimer = null; 1634 connectChannel(); 1635 } 1636} 1637 1638} 1639 1640//------------------------------------------------------------------------------ 1641// 1642// Private Classes 1643// 1644//------------------------------------------------------------------------------ 1645 1646import mx.core.mx_internal; 1647import mx.logging.Log; 1648import mx.messaging.ChannelSet; 1649import mx.messaging.MessageAgent; 1650import mx.messaging.MessageResponder; 1651import mx.messaging.events.ChannelEvent; 1652import mx.messaging.messages.IMessage; 1653import mx.messaging.messages.AcknowledgeMessage; 1654import mx.messaging.messages.CommandMessage; 1655import mx.messaging.messages.ErrorMessage; 1656import mx.rpc.AsyncToken; 1657import mx.collections.ArrayCollection; 1658 1659use namespace mx_internal; 1660 1661/** 1662 * @private 1663 * Clustered ChannelSets need to request the clustered channel endpoints for 1664 * the channels they contain upon a successful connect. However, Channels 1665 * require that all outbound messages be sent by a MessageAgent that their 1666 * internal MessageResponder implementations can callback to upon a response 1667 * or fault. The ChannelSet is not a MessageAgent, so in this case, it 1668 * circumvents the regular Channel.send() by passing its own custom responder 1669 * to Channel.sendUsingCustomResponder(). 1670 * 1671 * This is the custom responder. 1672 */ 1673class ClusterMessageResponder extends MessageResponder 1674{ 1675 //-------------------------------------------------------------------------- 1676 // 1677 // Constructor 1678 // 1679 //-------------------------------------------------------------------------- 1680 1681 /** 1682 * Constructor. 1683 */ 1684 public function ClusterMessageResponder(message:IMessage, channelSet:ChannelSet) 1685 { 1686 super(null, message); 1687 _channelSet = channelSet; 1688 } 1689 1690 //-------------------------------------------------------------------------- 1691 // 1692 // Variables 1693 // 1694 //-------------------------------------------------------------------------- 1695 1696 /** 1697 * @private 1698 * Gives the responder access to this ChannelSet, to pass it failover URIs for 1699 * its channels. 1700 */ 1701 private var _channelSet:ChannelSet; 1702 1703 //-------------------------------------------------------------------------- 1704 // 1705 // Methods 1706 // 1707 //-------------------------------------------------------------------------- 1708 1709 /** 1710 * Handles a cluster message response. 1711 * 1712 * @param message The response Message. 1713 */ 1714 override protected function resultHandler(message:IMessage):void 1715 { 1716 if ((message.body != null) && (message.body is Array || message.body is ArrayCollection)) 1717 { 1718 var channelFailoverURIs:Object = {}; 1719 var mappings:Array = message.body is Array? message.body as Array : (message.body as ArrayCollection).toArray(); 1720 var n:int = mappings.length; 1721 for (var i:int = 0; i < n; i++) 1722 { 1723 var channelToEndpointMap:Object = mappings[i]; 1724 for (var channelId:Object in channelToEndpointMap) 1725 { 1726 if (channelFailoverURIs[channelId] == null) 1727 channelFailoverURIs[channelId] = []; 1728 1729 channelFailoverURIs[channelId].push(channelToEndpointMap[channelId]); 1730 } 1731 } 1732 _channelSet.channelFailoverURIs = channelFailoverURIs; 1733 } 1734 } 1735} 1736 1737/** 1738 * @private 1739 * Stores a pending message to send when the ChannelSet does not have a 1740 * connected Channel to use immediately. 1741 */ 1742class PendingSend 1743{ 1744 //-------------------------------------------------------------------------- 1745 // 1746 // Constructor 1747 // 1748 //-------------------------------------------------------------------------- 1749 1750 /** 1751 * @private 1752 * Constructor. 1753 * 1754 * @param agent The MessageAgent sending the message. 1755 * 1756 * @param msg The Message to send. 1757 */ 1758 public function PendingSend(agent:MessageAgent, message:IMessage) 1759 { 1760 super(); 1761 this.agent = agent; 1762 this.message = message; 1763 } 1764 1765 //-------------------------------------------------------------------------- 1766 // 1767 // Properties 1768 // 1769 //-------------------------------------------------------------------------- 1770 1771 /** 1772 * @private 1773 * The MessageAgent. 1774 */ 1775 public var agent:MessageAgent; 1776 1777 /** 1778 * @private 1779 * The Message to send. 1780 */ 1781 public var message:IMessage; 1782 1783} 1784 1785/** 1786 * @private 1787 * Helper class for handling and redispatching login and logout results or faults. 1788 */ 1789class AuthenticationAgent extends MessageAgent 1790{ 1791 //-------------------------------------------------------------------------- 1792 // 1793 // Public Static Constants 1794 // 1795 //-------------------------------------------------------------------------- 1796 1797 // State constants. 1798 public static const LOGGED_OUT_STATE:int = 0; 1799 public static const LOGGING_IN_STATE:int = 1; 1800 public static const LOGGED_IN_STATE:int = 2; 1801 public static const LOGGING_OUT_STATE:int = 3; 1802 public static const SHUTDOWN_STATE:int = 4; 1803 1804 //-------------------------------------------------------------------------- 1805 // 1806 // Constructor 1807 // 1808 //-------------------------------------------------------------------------- 1809 1810 /** 1811 * Constructor. 1812 */ 1813 public function AuthenticationAgent(channelSet:ChannelSet) 1814 { 1815 _log = Log.getLogger("ChannelSet.AuthenticationAgent"); 1816 _agentType = "authentication agent"; 1817 // Must set log and agent type before assigning channelSet. 1818 this.channelSet = channelSet; 1819 } 1820 1821 //-------------------------------------------------------------------------- 1822 // 1823 // Variables 1824 // 1825 //-------------------------------------------------------------------------- 1826 1827 /** 1828 * Map of login/logout message Ids to associated tokens. 1829 */ 1830 private var tokens:Object = {}; 1831 1832 //-------------------------------------------------------------------------- 1833 // 1834 // Properties 1835 // 1836 //-------------------------------------------------------------------------- 1837 1838 private var _state:int = LOGGED_OUT_STATE; 1839 1840 /** 1841 * Returns the current state for the agent. 1842 * See the static state constants defined by this class. 1843 */ 1844 public function get state():int 1845 { 1846 return _state; 1847 } 1848 1849 public function set state(value:int):void 1850 { 1851 _state = value; 1852 if (value == SHUTDOWN_STATE) 1853 tokens = null; 1854 } 1855 1856 //-------------------------------------------------------------------------- 1857 // 1858 // Public Methods 1859 // 1860 //-------------------------------------------------------------------------- 1861 1862 /** 1863 * Registers an outbound login/logout message and its associated token for response/fault handling. 1864 */ 1865 public function registerToken(token:AsyncToken):void 1866 { 1867 tokens[token.message.messageId] = token; 1868 } 1869 1870 /** 1871 * Acknowledge message callback. 1872 */ 1873 override public function acknowledge(ackMsg:AcknowledgeMessage, msg:IMessage):void 1874 { 1875 if (state == SHUTDOWN_STATE) 1876 return; 1877 1878 var error:Boolean = ackMsg.headers[AcknowledgeMessage.ERROR_HINT_HEADER]; 1879 // Super will clean the error hint from the message. 1880 super.acknowledge(ackMsg, msg); 1881 // If acknowledge is *not* for a message that caused an error 1882 // dispatch a result event. 1883 if (!error) 1884 { 1885 var token:AsyncToken = tokens[msg.messageId]; 1886 delete tokens[msg.messageId]; 1887 channelSet.authenticationSuccess(this, token, ackMsg as AcknowledgeMessage); 1888 } 1889 } 1890 1891 /** 1892 * Fault callback. 1893 */ 1894 override public function fault(errMsg:ErrorMessage, msg:IMessage):void 1895 { 1896 if (state == SHUTDOWN_STATE) 1897 return; 1898 1899 // For some channel impls, when a logout request is processed the session at the remote host host 1900 // is invalidated which may trigger a disconnection/drop of the channel connection. 1901 // This channel disconnect may mask the logout ack. If the root cause for this error is a channel disconnect, 1902 // assume logout succeeded and locally acknowledge it. 1903 if (errMsg.rootCause is ChannelEvent && (errMsg.rootCause as ChannelEvent).type == ChannelEvent.DISCONNECT) 1904 { 1905 var ackMsg:AcknowledgeMessage = new AcknowledgeMessage(); 1906 ackMsg.clientId = clientId; 1907 ackMsg.correlationId = msg.messageId; 1908 acknowledge(ackMsg, msg); 1909 return; 1910 } 1911 1912 super.fault(errMsg, msg); 1913 1914 var token:AsyncToken = tokens[msg.messageId]; 1915 delete tokens[msg.messageId]; 1916 channelSet.authenticationFailure(this, token, errMsg as ErrorMessage); 1917 } 1918} 1919