Use the Kaazing WebSocket Gateway Objective-C JMS Client API

In this procedure, you will learn how to create an iOS JMS client using the Kaazing WebSocket Gateway Objective-C JMS client library. You will learn how to create an Xcode project and add the necessary frameworks in order to use the Objective-C JMS client library. You will learn how to implement the Objective-C JMS client library methods to enable your client to send and receive messages to your Apache ActiveMQ through the Gateway.

For information about deploying your Objective-C (iOS) client on devices with the arm64 architecture, see Convert Your Objective-C (iOS) Client to a 64-Bit Runtime Environment.

Note: For this how-to, you can use any JMS-compliant message broker. By default, the Gateway is configured to connect to the server on tcp://localhost:61613. You can configure the connect URL in the file GATEWAY_HOME/conf/gateway-config.xml. See About Integrating Kaazing WebSocket Gateway and JMS-Compliant Message Brokers for more information.

In this procedure you will do the following:

  1. Set up your development environment using the Gateway and Xcode 4.4 or later.
  2. Review the components that will be used to create the Objective-C client.
  3. Create a new Xcode project.
  4. Add the Kaazing WebSocket Gateway Objective-C framework.
  5. Add CFNetwork.framework to the project.
  6. Add the -ObjC value to the Other Linker Flags build setting.
  7. Build the interface for the client using MainStoryboard.storyboard.
  8. Add the actions for the buttons to the view controller header file.
  9. Add the StompConnectionListener to the JMSViewController.m implementation file.
  10. Add the MessageListener to the JMSViewController.m implementation file.
  11. Add the ExceptionListener to the JMSViewController.m implementation file.
  12. Update the JMSViewController class in JMSViewController.m to use the StompConnectionListener, MessageListener, and ExceptionListener code.
  13. Start Apache ActiveMQ and Kaazing WebSocket Gateway.
  14. Build and run the client in the iPhone Simulator.

For information about the Kaazing WebSocket Gateway Objective-C JMS client library, see Objective-C JMS Client API.

Notes:

Before You Begin

This procedure is part of Checklist: Build Objective-C (iOS) JMS Clients Using Kaazing WebSocket Gateway, that includes the following steps:

  1. Use the Objective-C JMS Client API
  2. Secure Your Objective-C Client
  3. Troubleshoot Your Objective-C JMS Client
  4. Add APNs Support to Your Objective-C JMS Client
  5. Display Logs for the Objective-C JMS Client

Note: Learn about supported browsers, operating systems, and platform versions in the Release Notes.

To Use the Kaazing WebSocket Gateway Objective-C JMS Client API

  1. Set up your development environment using the following:
    • Download and install Kaazing WebSocket Gateway, as described in Setting Up Kaazing WebSocket Gateway.
    • Download and install Apple Xcode 4.4 or later (requires Mac OS X 10.7.4 or later). The Xcode bundle includes the iOS SDK.
  2. Review the components that will be used to create the Objective-C client. A quick review of these components will give you an overview of how the client is constructed.

    ComponentDescription
    KGWebSocket.frameworkKaazing WebSocket Gateway Objective-C WebSocket framework.
    KMStompJMS.frameworkKaazing WebSocket Gateway Objective-C JMS framework.
    CFNetwork.frameworkCore Foundation framework that provides a library of abstractions for network protocols. For more information on CFNetwork, see Introduction to CFNetwork Programming Guide.
    MainStoryboard.storyboardThe storyboard for the user interface look and feel and the interactive controls.
    JMSViewController.hThe header file that contains the target-action mechanism: how the user interface elements send an action message to an object that knows how to perform the corresponding action method (defined in JMSViewController.m). In this file, user interface objects and outlet connections (connections between user interface objects and custom controller objects) are defined for the user interface controls. For more information, see Outlets.
    JMSViewController.mThe implementation file (sometimes called a source file) where the architecture of the client is defined, including how the client responds to different events.
    StompConnectionListener classThe JMS listener interface for connection events, including the instance method implementations.
    MessageListener classThe JMS message listener interface, including the onMessage:() method.
    ExceptionListener classThe JMS exception listener interface, including the onException:() method.
    JMSViewController classThe fundamental view-management model for the client. This class defines how the client’s data and its visual appearance are linked. For more information, see UIViewController Class.
  3. Launch Xcode.
  4. Create a new Xcode project.

    1. Click File, then New, and then Project.
    2. Under iOS, click Application, and click Single View Application. Click Next. The project options page appears.

      Figure: Options for the Single View Application project
    3. Enter a name for your product in Product Name, such as JMSDemo, and a class prefix such as JMS in Class Prefix. Xcode uses the product name you entered to name your project and the client, and the class prefix to name the classes and files it generates for you.
    4. In Company Identifier, enter the name of your company.
    5. In Devices, make sure that iPhone is selected.
    6. Make sure that the Use Storyboards and Use Automatic Reference Counting options are selected and that the Include Unit Tests option is unselected.
    7. Click Next.
    8. Specify a location for your project (leave the Source Control option unselected) and then click Create.

      The new project is created along with the default files.

      Figure: New Xcode Project
  5. Next, add the Kaazing WebSocket Gateway Objective-C framework to the project.

    1. Navigate to the location of the Kaazing WebSocket Gateway Objective-C frameworks:

      GATEWAY_HOME/lib/client/ios/
    2. Double-click the KMStompJMS.dmg and KGWebSocket.dmg images to mount them.
    3. Drag the KGWebSocket.framework folder from the mounted volume into the Frameworks folder in the Xcode project navigator.
    4. In the Choose options for adding these files dialog that appears, enable the Copy items into destination group’s folder checkbox, select your project in Add to targets, and click Finish.

    5. Repeat steps c and d for the KMStompJMS.framework.
    6. Xcode adds the frameworks to the project navigator, updates the Framework Search Paths setting in Build Settings with the path to the frameworks, and updates the Link Binary With Libraries settings in Build Phases automatically.

      Note: You can also choose to add the KGWebSocket.framework and KMStompJMS.framework files into your local /Library/Frameworks/ folder or a network share before adding them to your project. This is a common practice for managing frameworks.

  6. Add CFNetwork.framework to the project. CFNetwork is a framework in the iOS Core Services framework that provides a library of abstractions for network protocols.

    1. In the project navigator, select the target to which you want to add a library or framework. In this example, JMSDemo.
    2. Click Build Phases at the top of the project editor.
    3. Open the Link Binary With Libraries section.
    4. Click the Add (+) button to add a library or framework.
    5. Enter CFNetwork.framework in the search field, select CFNetwork.framework in the results, and click Add.

      The CFNetwork.framework is now listed in the Frameworks folder in the project navigator.

  7. Add the -ObjC value to the Other Linker Flags build setting because the Kaazing WebSocket Gateway Objective-C API code you add links against an Objective-C static library that contains categories. You must add this value to prevent a runtime exception of "selector not recognized". For more information, see Building Objective-C static libraries with categories.

    1. In the project navigator, select the target to which you want to add a library or framework. In this example, JMSDemo.
    2. Click the Build Settings tab and scroll down to the Linking section.
    3. In Other Linker Flags, add the value -ObjC.
  8. Apply the -fobjc-arc-exceptions flag. By default in Objective-C, ARC is not exception-safe for normal releases. If the -fobjc-arc-exceptions flag is not specified, then the lifetime of __strong variables are not ended when their scopes are abnormally terminated by an exception. To enable exceptions, on the Build Settings tab, under Apple LLVM compiler 4.2 Language, expand Other C Flags, click the + button next to Release and add the flag -fobjc-arc-exceptions.

    For more information, see http://clang.llvm.org/docs/AutomaticReferenceCounting.html#exceptions.

  9. Build the interface for the client using MainStoryboard.storyboard and the View Controller.

    1. Click MainStoryboard.storyboard in the project navigator. A blank scene appears.
    2. Expand View Controller in the editor area and click View.
    3. Click the Utility view to display the Utility area.
    4. Show the Object Library, and choose Controls from the pop-up menu.
    5. Drag the following controls into the scene, and give them the values listed in the following table. You might want to add a background color to the scene in order to display controls clearly.

      ControlPurposeValue
      LabelURI text labelURI:
      Text FieldField where users enter a WebSocket addressws://localhost:8001/jms
      LabelDestination labelDestination:
      ButtonConnect buttonConnect
      ButtonSubscribe buttonSubscribe
      ButtonSend buttonSend
      Text FieldField where users enter the JMS destination/topic/destination
      LabelMessage labelMessage:
      Text FieldField where users enter a messageHello, WebSocket
      Text ViewArea where the connection and subscription state is displayed, and where sent and received messages are displayedNo value
      ButtonClear button that clears previous connection and message infoClear

      When you are finished, the scene should look like the following:

      Figure: Completed View Controller Scene
  10. Add the actions for the buttons to the view controller header file. In our example, the file is named JMSViewController.h.

    1. Modify the JMSViewController class as follows:

      #import <UIKit/UIKit.h>
                      
      // Declare the JMSViewController class and add the log and updateUI methods
      @interface JMSViewController : UIViewController<UITextFieldDelegate>
      - (void) log:(NSString *)str;
      - (void) updateUI:(BOOL)connectStatus;
      - (void) cleanup;
    2. Control-drag the UI controls into the JMSViewController class extension in JMSViewController.h to create actions and outlet connections (an outlet describes a connection between two objects). Configure the actions and outlet connections so that JMSViewController.h appears as follows:

      /* Add the action method declarations. Corresponding stub methods
      are added to JMSViewController.m automatically */
      - (IBAction)connectOrDisconnect:(id)sender;
      - (IBAction)subscribe:(id)sender;
      - (IBAction)send:(id)sender;
      - (IBAction)clear:(id)sender;
      
      /* Add the outlet connections. Corresponding statements are added to the
      viewDidLoad method in JMSViewController.m
      automatically */
      @property (weak, nonatomic) IBOutlet UIButton *connectDisconnectButton;
      @property (weak, nonatomic) IBOutlet UIButton *sendButton;
      @property (weak, nonatomic) IBOutlet UIButton *subscribeButton;
      @property (weak, nonatomic) IBOutlet UIButton *clearButton;
      
      @property (weak, nonatomic) IBOutlet UITextField *locationField;
      @property (weak, nonatomic) IBOutlet UITextField *destinationField;
      @property (weak, nonatomic) IBOutlet UITextField *messageField;
      @property (weak, nonatomic) IBOutlet UITextView *logView;
      
      @end

      When you Control-drag the UI controls in the JMSViewController, the popover control appears:

      Figure: The Xcode popover control

      For action method declarations (IBAction), choose Action in the the Connection drop-down. For outlet connections (IBOutlet), choose Outlet in the Connection drop-down. This is a common Xcode procedure. If you are unfamiliar with this procedure, see A Beginner’s Guide to Storyboard.

      Notes:
      • For all of the buttons, ensure that Touch Up Inside is selected in the Sent Events section of the Connections Inspector. Xcode will likely configure this automatically.
      • While you can paste the above code into your header file, if you control-drag (press and hold the Control key while you drag the button to the implementation file in the assistant editor pane) the UI elements into the code from the scene and use the popover control to specify the outlet connections you can ensure that you have all the settings correct. This is a common Xcode procedure. If you are unfamiliar with this procedure, see A Beginner’s Guide to Storyboard.
      • When you add the action methods, corresponding stub methods are added to the JMSViewController.m implementation file automatically. You will update these methods in JMSViewController.m with the Kaazing WebSocket Gateway Objective-C API in later steps.
      • Some iOS clients define the interface in the implementation file instead of the header file. The client in this procedure defines the interface in the header file and the implementation methods in the implementation file.
    3. Set the delegate for the text fields. In the view, Control-drag each text field to the yellow sphere in the scene dock (the view controller object). Select delegate in the Outlets section of the panel that appears. For more information, see A Beginner’s Guide to Storyboard.
  11. Add the APNs files JMSAppDelegate.h and JMSAppDelegate.m to the project. The APNs functionality is discussed in another topic, Add APNs Support to Your Objective-C JMS Client, but the code you will add in the following steps includes some APNs functionality and without JMSAppDelegate.h and JMSAppDelegate.m Xcode will throw errors during building.

    To add these files, drag them from GATEWAY_HOME/demo/ios/src/jms/JMSDemo to the JMSDemo project folder in Xcode.

  12. In Xcode, click JMSViewController.m. You are going to modify this file to manage JMS communication to the Apache ActiveMQ via the Gateway.

  13. Replace the code in JMSViewController.m with the following code sample. The new code includes the following changes:

    1. Add the import directive and class declaration below the import statement for JMSViewController.h (#import "JMSViewController.h") (starts at line 1).
    2. Declare the StompConnectionListener class (starts at line 72).
    3. Declare the MessageListener class after the StompConnectionListener class (starts at line 111).
    4. Declare the ExceptionListener class after the MessageListener class (starts at line 174).
    5. Replace the JMSViewController class in JMSViewController.m with the JMSViewController class in the code sample to use the StompConectionListener, MessageListener, and ExceptionListener code (starts at line 201).
  14. #import "JMSViewController.h"
    #import "JMSAppDelegate.h"
    #import <KMStompJMS/KMStompJMS.h>
    #import <KGWebSocket/WebSocket.h>
    
    @class JMSViewController;
    
    
    // --------------- LoginHandler Implementation ---------------
    @interface DemoLoginHandler : KGLoginHandler
    @end
    
    @implementation DemoLoginHandler {
      int            _buttonIndex;
      NSString      *_username;
      NSString      *_password;
      UIAlertView   *_alertView;
    }
    
    -(void) dealloc {
    }
    
    - (id) init {
      NSLog(@"[DemoLoginHandler init]");
      self = [super init];
      return self;
    }
    
    -(NSURLCredential *) credentials {
      
      _buttonIndex = -1;
      dispatch_async(dispatch_get_main_queue(), ^{
        [self popupLogin];
      });
      
      // Wait for click
      while (_buttonIndex < 0) {
        [NSThread sleepForTimeInterval:.2];
      }
    
      // Clicked the Submit button
      if (_buttonIndex != 0) {
        return [[NSURLCredential alloc] initWithUser:_username password:_password persistence:NSURLCredentialPersistenceNone];
      } else {
        return nil;
      }
    }
    
    - (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
      _username = [_alertView textFieldAtIndex:0].text;
      _password = [_alertView textFieldAtIndex:1].text;
      _buttonIndex = buttonIndex;
    }
    
    - (void) popupLogin {
      _buttonIndex = -1;
    
      _alertView = [[UIAlertView alloc] initWithTitle:@"Please Login:" message:nil
      delegate:self
      cancelButtonTitle:@"Cancel"
      otherButtonTitles:@"OK", nil];
    
      _alertView.alertViewStyle = UIAlertViewStyleLoginAndPasswordInput;
      
      // Show alert on screen.
      [_alertView show];
    }
    
    @end
    
    
    // --------------- StompConnectionListener Implementation ---------------
    
    @interface JMSConnectionListener : NSObject<KMStompConnectionListener> {
      JMSViewController    *_controller;
    }
    @end
    
    @implementation JMSConnectionListener
    
    - (id) initWithController:(JMSViewController *)controller {
      self = [super init];
      if (self) {
        _controller = controller;
      }
      
      return self;
    }
    
    - (void) onConnect:(KMConnection *)connection {
      [_controller log:@"CONNECTED"];
      [_controller updateUI:YES];
    }
    
    - (void) onStart:(KMConnection *)connection {
      [_controller log:@"CONNECTION STARTED"];
    }
    
    - (void) onStop:(KMConnection *)connection {
      [_controller log:@"STOPPED"];
    }
    
    - (void) onClose:(KMConnection *)connection {
      [_controller log:@"DISCONNECTED\n"];
      [((JMSViewController *)_controller) cleanup];
      [_controller updateUI:NO];
    }
    
    @end
    
    // -------------------- MessageListener Implementation -----------------
    @interface JMSMessageListener : NSObject<KMMessageListener>
    @end
    
    @implementation JMSMessageListener {
      JMSViewController *_controller;
    }
    
    - (id) initWithController:(JMSViewController *)controller {
      self = [super init];
      if (self) {
        _controller = controller;
      }
      
      return self;
    }
    
    - (NSString *) typeFromObject:(NSObject *)value {
      if (value == nil) {
        return @"nil";
      } else if ([value isKindOfClass:[NSString class]]) {
        return @"NSString";
      } else if ([value isKindOfClass:[NSNumber class]]) {
        return @"NSNumber";
      } else if ([value isKindOfClass:[NSData class]]) {
        return @"NSData";
      } else if ([value isKindOfClass:[NSNull class]]) {
        return @"NSNull";
      } else {
        return @"UNKNOWN TYPE";
      }
    }
    
    - (void) onMessage:(KMMessage *)message {
      if ([message isKindOfClass:[KMTextMessage class]]) {
        KMTextMessage *textMessage = (KMTextMessage *)message;
        [_controller log:[NSString stringWithFormat:@"RECEIVED KMTextMessage: %@", [textMessage text]]];
          
      } else if ([message isKindOfClass:[KMBytesMessage class]])  {
        KMBytesMessage *bytesMessage = (KMBytesMessage *)message;
        NSString *utf8String = [bytesMessage readUTF];
        [_controller log:[NSString stringWithFormat:@"RECEIVED KMBytesMessage: %@", utf8String]];
    
      } else if ([message isKindOfClass:[KMMapMessage class]]) {
        KMMapMessage *mapMessage = (KMMapMessage *)message;
        [_controller log:[NSString stringWithFormat:@"RECEIVED KMMapMessage:"]];
    
        int count = 0;
        for (NSString *key in [mapMessage mapNames]) {
          id value = [mapMessage getObject:key];
          NSString *type = [self typeFromObject:value];
          [_controller log:[NSString stringWithFormat:@"  %@: %@ (%@)", key, value, type]];
          count++;
        }
        [_controller log:[NSString stringWithFormat:@"%d entries", count]];
          
      } else {
        [_controller log:@"RECEIVED UNKNOWN MESSAGE"];
      }
    }
    
    @end
    
    // -------------------- ExceptionListener Implementation -----------------
    @interface JMSExceptionListener : NSObject<KMExceptionListener>
    @end
    
    @implementation JMSExceptionListener {
      JMSViewController *_controller;
    }
    
    - (id) initWithController:(JMSViewController *)controller {
      self = [super init];
      if (self) {
        _controller = controller;
      }
      
      return self;
    }
    
    - (void) onException:(KMJMSException *)exception {
      NSString *msg = [exception reason];
      [_controller log:[NSString stringWithFormat:@"EXCEPTION: %@", msg]];
      if ([exception isKindOfClass:[KMConnectionDisconnectedException class]]) {
        [((JMSViewController *)_controller) cleanup];
      }
    }
    
    @end
    
    // -------------------- JMSViewController Implementation ---------------
    
    @interface JMSViewController ()
    -(KMDestination*) createDestination:(NSString*)destinationName;
    @end
    
    @implementation JMSViewController
    
    @synthesize connectDisconnectButton;
    @synthesize sendButton;
    @synthesize subscribeButton;
    @synthesize clearButton;
    
    @synthesize locationField;
    @synthesize destinationField;
    @synthesize messageField;
    @synthesize logView;
    
    bool                      connected = NO;
    KMStompConnectionFactory *factory = nil;
    KMConnection             *conn = nil;
    KMSession                *session = nil;
    KMMessageProducer        *producer = nil;
    KMMessageConsumer        *consumer = nil;
    KMNotifyingSession       *notifyingSession = nil;
    KMMessageConsumer        *notifyingConsumer = nil;
    
    
    - (void) viewDidLoad {
      subscribeButton.enabled = NO;
      subscribeButton.alpha = 0.5f;
      sendButton.enabled = NO;
      sendButton.alpha = 0.5f;
      
      [[NSNotificationCenter defaultCenter] addObserver:self
      selector:@selector(suspendOrResume:)
      name:@"suspendOrResume"
      object:nil];
      [super viewDidLoad];
    }
    
    - (void) viewDidUnload {
      [self setLocationField:nil];
      [self setDestinationField:nil];
      [self setMessageField:nil];
      [self setLogView:nil];
      [self setConnectDisconnectButton:nil];
      [self setSendButton:nil];
      [self setSubscribeButton:nil];
      [self setClearButton:nil];
      
      subscribeButton.enabled = NO;
      subscribeButton.alpha = 0.5f;
      sendButton.enabled = NO;
      sendButton.alpha = 0.5f;
      
      [super viewDidUnload];
      // Release any retained subviews of the main view.
    }
    
    #pragma mark <UIViewController Methods>
    
    - (BOOL) shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
      if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
      } else {
        return YES;
      }
    }
    
    #pragma mark <UITextFieldDelegate Method>
    
    - (BOOL) textFieldShouldReturn:(UITextField *)theTextField {
      if ((theTextField == self.locationField)    || 
      (theTextField == self.destinationField) ||
      (theTextField == self.messageField)) {
        [theTextField resignFirstResponder];
      }
      return YES;
    }
    
    #pragma mark <JMSViewController Methods>
    
    - (IBAction) connectOrDisconnect:(id)sender {
      // Cache values from the UI widgets in the main thread itself to avoid warnings/errors
      // related to UIKit invoked from secodary thread.
      NSString *location = [locationField text];
    
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if (!connected) {
          NSURL                       *url = [[NSURL alloc] initWithString:location];
          KMStompConnectionProperties *props = [[KMStompConnectionProperties alloc] init];
              
          props->_connectionTimeout = 5000;
              
          @try {
            factory = [[KMStompConnectionFactory alloc] initWithUrl:url properties:props];
                  
            KGWebSocketFactory   *webSocketFactory = [factory webSocketFactory];
            KGChallengeHandler   *challengeHandler = [self createBasicChallengeHandler];
            JMSAppDelegate       *delegate = [[UIApplication sharedApplication] delegate];
            NSData               *devToken = [delegate deviceToken];
                  
            [webSocketFactory setDefaultChallengeHandler:challengeHandler];
    
            if (devToken != nil) {
              // App is configured to receive push notifications via APNS.
              NSString *apnsExtensionName = [[KGApnsExtension apnsExtension] name];
              NSArray  *enabledExtensions = [NSArray arrayWithObjects:apnsExtensionName, nil];
              NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];
    
              [webSocketFactory setDefaultEnabledExtensions:enabledExtensions];
              [webSocketFactory setDefaultParameter:[KGApnsExtension deviceToken] value:devToken];
              [webSocketFactory setDefaultParameter:[KGApnsExtension bundleId] value:bundleId];
            }
    
            [self log:@"CONNECTING"];
            id<KMStompConnectionListener> listener = [[JMSConnectionListener alloc] initWithController:self];
            conn = [factory createConnectionWithListener:listener];
            [conn setExceptionListener:[[JMSExceptionListener alloc] initWithController:self]];
            [conn start];
            session = (KMSession *)[conn createSession:KMSessionAutoAcknowledge transacted:NO];
    
            if (devToken != nil) {
              // Create a notifying-session for APNS.
              notifyingSession = (KMNotifyingSession *) [conn createSession:KMSessionNotifyAcknowledge transacted:NO];
            }
          }
          @catch (NSException *ex) {
            [self log:[NSString stringWithFormat:@"EXCEPTION: %@", [ex reason]]];
            NSLog(@"Exception: %@", [ex reason]);
                  
            if (notifyingSession != nil) {
              [notifyingSession close];
            }
                  
            if (session != nil) {
              [session close];
            }
                  
            if (conn != nil) {
              [conn close];
            }
                  
            notifyingSession = nil;
            session = nil;
            conn = nil;
            factory = nil;
          }
        } else {
          connected = NO;
          [self log:@"CLOSING"];
              
          if (notifyingSession != nil) {
            [notifyingSession close];
          }
          [session close];
          [conn close];
        }
      });
    }
    
    - (IBAction) subscribe:(id)sender {
      // Cache values from the UI widgets in the main thread itself to avoid warnings/errors
      // related to UIKit invoked from secondary thread.
      NSString      *destinationName = [destinationField text];
      
      // Subscribing can be a blocking operation, so perform it in the background.
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @try {
          // Create a destination.
          KMDestination *dest = [self createDestination:destinationName];
          if (dest == nil) {
            return;
          }
              
          // Create a consumer and attach a listener.
          consumer = [session createConsumer:dest];
          [consumer setMessageListener:[[JMSMessageListener alloc] initWithController:self]];
    
          if (notifyingSession != nil) {
            // Create a notifying-consumer for APNS.
            notifyingConsumer = [notifyingSession createDurableNotification:(KMTopic *)dest
            durableName:@"notifyingDurableSubscriber"
            notificationPayload:[self payload]];
          }
    
          // Update the UI. These calls will schedule themselves in the main queue.
          [self log:[NSString stringWithFormat:@"SUBSCRIBED TO: %@", destinationName]];
          [self disableSubscribeButton];
        }
        @catch (NSException *exception) {
          NSString *msg = [exception reason];
          [self log:[NSString stringWithFormat:@"EXCEPTION: %@", msg]];
              
          if (consumer != nil) {
            [consumer close];
          }
              
          if (notifyingConsumer != nil) {
            [notifyingConsumer close];
          }
              
          consumer = nil;
          notifyingConsumer = nil;
        }
      });
    }
    
    - (KGNotificationPayload *)payload {
      KGSimpleAlertNotification  *alert = [[KGSimpleAlertNotification alloc]
      initWithBody:@"Hello from APNs"];
      KGNotificationPayload *payload = [[KGNotificationPayload alloc]
      initWithAlert:alert];
      return payload;
    }
    
    - (IBAction) send:(id)sender {
      // Cache values from the UI widgets in the main thread itself to avoid warnings/errors
      // related to UIKit invoked from secondary thread.
      NSString *destinationName = [destinationField text];
      NSString *messageText = [messageField text];
    
      // Sending a message is a blocking operation, so we do not want to
      // do that in the UI thread. Instead, we want to do it in the background.
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @try {
          // Create a destination.
          KMDestination *dest = [self createDestination:destinationName];
          if (dest == nil) {
            return;
          }
    
          // Create a producer and message.
          producer = [session createProducer:dest];
          KMTextMessage *message = [session createTextMessage];
              
          // Set the message text and send it away.
          [message setText:messageText];
          [producer send:message];
              
          // Update the UI. This call will schedule itself in the main queue.
          [self log:[NSString stringWithFormat:@"MESSAGE SENT TO %@: %@", destinationName, messageText]];
        }
        @catch (NSException *exception) {
          NSString *msg = [exception reason];
          [self log:[NSString stringWithFormat:@"EXCEPTION: %@", msg]];
              
          if (producer != nil) {
            [producer close];
          }
              
          producer = nil;
        }
      });
    }
    
    - (IBAction) clear:(id)sender {
      dispatch_async(dispatch_get_main_queue(), ^{
        [logView setText:@""];
      });
    }
    
    #pragma mark<Private Methods>
    
    - (KGChallengeHandler *) createBasicChallengeHandler {
      // Set up ChallengeHandlers to handle authentication challenge.
      KGLoginHandler          *loginHandler = [[DemoLoginHandler alloc] init];
      KGBasicChallengeHandler *challengeHandler = [KGBasicChallengeHandler create];
      
      [challengeHandler setLoginHandler:loginHandler];
      return challengeHandler;
    }
    
    // Create a KMTopic or KMQueue depending on the text prefix inside the destination field.
    - (KMDestination *) createDestination:(NSString*)destinationName {
      if ([destinationName hasPrefix:@"/topic/"]) {
        return [session createTopic:destinationName];
      }
      else if ([destinationName hasPrefix:@"/queue/"]) {
        return [session createQueue:destinationName];
      }
      
      // Make sure that the error dialog is shown in the main UI thread.
      dispatch_async(dispatch_get_main_queue(), ^{
        UIAlertView *badPrefixAlert = [[UIAlertView alloc] initWithTitle:@"Invalid destination"
        message:@"Destination must begin with /topic/ or /queue/"
        delegate:nil
        cancelButtonTitle:@"OK"
        otherButtonTitles:nil];
        [badPrefixAlert show];
      });
      
      return nil;
    }
    
    - (void) cleanup {
      if (producer != nil) {
        [producer close];
      }
      
      if (consumer != nil) {
        [consumer close];
      }
      
      if (notifyingConsumer != nil) {
        [notifyingConsumer close];
      }
      
      if (notifyingSession != nil) {
        [notifyingSession close];
      }
      
      if (session != nil) {
        [session close];
      }
      
      if (conn != nil) {
        [conn close];
      }
      
      producer = nil;
      consumer = nil;
      notifyingConsumer = nil;
      notifyingSession = nil;
      session = nil;
      conn = nil;
      factory = nil;
    }
    
    - (void) log:(NSString *)str {
      dispatch_async(dispatch_get_main_queue(), ^{
        NSString *text = str;
        NSString *log = [logView text];
          
        if ((log != nil) && ([log length] > 0)) {
          text = [NSString stringWithFormat:@"%@\n%@", [logView text], str];
        }
          
        // remove old text if text field is too large
        if ([[logView text] length] > 5000) {
          text = [text substringFromIndex:3000];
        }
          
        [logView setText:text];
        [logView scrollRangeToVisible:NSMakeRange([logView.text length], 0)];
      });
    }
    
    - (void) disableSubscribeButton {
      dispatch_async(dispatch_get_main_queue(), ^{
        subscribeButton.enabled = NO;
        subscribeButton.alpha = 0.5f;
      });
    }
    
    - (void) suspendOrResume:(NSNotification *)notification {
      NSDictionary      *dict = [notification userInfo];
      NSString          *str = (NSString *) [dict valueForKey:@"CONNECTION_STATE"];
      JMSAppDelegate    *delegate = [[UIApplication sharedApplication] delegate];
      NSData            *devToken = [delegate deviceToken];
      
      if ([str isEqualToString:@"resume"]) {
        @try {
          if (devToken == nil) {
            // Application switched from background to foreground. Start the connection
            // if it was earlier connected.
            if (conn != nil) {
              [conn start];
            }
            return;
          }
          // App is configured to receive push notifications via APNS.
              
          NSString                 *location = [locationField text];
          NSURL                    *url = [[NSURL alloc] initWithString:location];
          KMStompConnectionFactory *factory = [[KMStompConnectionFactory alloc] initWithUrl:url];
          KGWebSocketFactory       *webSocketFactory = [factory webSocketFactory];
          KGChallengeHandler       *challengeHandler = [self createBasicChallengeHandler];
              
          [webSocketFactory setDefaultChallengeHandler:challengeHandler];
          NSString *apnsExtensionName = [[KGApnsExtension apnsExtension] name];
          NSArray  *enabledExtensions = [NSArray arrayWithObjects:apnsExtensionName, nil];
          NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];
                  
          [webSocketFactory setDefaultEnabledExtensions:enabledExtensions];
          [webSocketFactory setDefaultParameter:[KGApnsExtension deviceToken] value:devToken];
          [webSocketFactory setDefaultParameter:[KGApnsExtension bundleId] value:bundleId];
              
          [self log:@"CONNECTING"];
          id<KMStompConnectionListener> listener = [[JMSConnectionListener alloc] initWithController:self];
          conn = [factory createConnectionWithListener:listener];
          [conn setExceptionListener:[[JMSExceptionListener alloc] initWithController:self]];
          [conn start];
          session = (KMSession *)[conn createSession:KMSessionAutoAcknowledge transacted:NO];
    
          // Create a notifying-session for APNS.
          notifyingSession = (KMNotifyingSession *) [conn createSession:KMSessionNotifyAcknowledge transacted:NO];
              
          // Create a destination.
          NSString      *destinationName = [destinationField text];
          KMDestination *dest = [self createDestination:destinationName];
          if (dest == nil) {
            return;
          }
              
          // Create a consumer and attach a listener.
          consumer = [session createConsumer:dest];
          [consumer setMessageListener:[[JMSMessageListener alloc] initWithController:self]];
              
          if (notifyingSession != nil) {
            // Create a notifying-consumer for APNS. Notifications are ONLY
            // supported for non-durable topics.
            notifyingConsumer = [notifyingSession createDurableNotification:(KMTopic *)dest
            durableName:@"notifyingDurableSubscriber"
            notificationPayload:[self payload]];
          }
              
          // Update the UI. These calls will schedule themselves in the main queue.
          [self log:[NSString stringWithFormat:@"SUBSCRIBED TO: %@", destinationName]];
          connected = YES;
          [self updateUI:YES];
          [self disableSubscribeButton];
        }
        @catch (NSException *ex) {
          [self log:[NSString stringWithFormat:@"EXCEPTION: %@", [ex reason]]];
          NSLog(@"Exception: %@", [ex reason]);
              
          if (consumer != nil) {
            [consumer close];
          }
              
          if (notifyingConsumer != nil) {
            [notifyingConsumer close];
          }
              
          if (notifyingSession != nil) {
            [notifyingSession close];
          }
              
          if (session != nil) {
            [session close];
          }
              
          if (conn != nil) {
            [conn close];
          }
              
          consumer = nil;
          notifyingConsumer = nil;
          notifyingSession = nil;
          session = nil;
          conn = nil;
        }
      }
      else { // suspend
        @try {
          if (devToken == nil) {
            // Application switched from foreground to background. Stop the connection
            // if it was connected.
            if (conn != nil) {
              [conn stop];
            }
            return;
          }
              
          // APNS is being used. So, we should close the connection explicitly to
          // receive notifications.
          connected = NO;
          [self log:@"CLOSING"];
          [conn close];
          // conn = nil;
          [self updateUI:NO];
        }
        @catch (NSException *ex) {
          [self log:[NSString stringWithFormat:@"EXCEPTION: %@", [ex reason]]];
          NSLog(@"Exception: %@", [ex reason]);
        }
      }
    }
    
    - (void) updateUI:(BOOL)connectStatus {
      dispatch_async(dispatch_get_main_queue(), ^{
        if (connectStatus) {
          [connectDisconnectButton setTitle:@"Disconnect" forState:UIControlStateNormal];
          connected = YES;
          subscribeButton.enabled = YES;
          subscribeButton.alpha = 1.0f;
          sendButton.enabled = YES;
          sendButton.alpha = 1.0f;
        } else {
          [connectDisconnectButton setTitle:@"Connect" forState:UIControlStateNormal];
          connected = NO;
          subscribeButton.enabled = NO;
          subscribeButton.alpha = 0.5f;
          sendButton.enabled = NO;
          sendButton.alpha = 0.5f;
        }
      });
    }
    
    @end
    
  15. Start Apache ActiveMQ. For steps on starting Apache ActiveMQ, see the setup information for Apache ActiveMQ.
  16. Confirm that the Kaazing WebSocket Gateway has the default jms service in GATEWAY_HOME/conf/gateway-config.xml, and then start the Gateway.

    <service>
        <accept>ws://${gateway.hostname}:${gateway.extras.port}/jms</accept>
        <type>jms</type>
        <properties>
            <connection.factory.name>ConnectionFactory</connection.factory.name>
            <context.lookup.topic.format>dynamicTopics/%s</context.lookup.topic.format>
            <context.lookup.queue.format>dynamicQueues/%s</context.lookup.queue.format>
            <env.java.naming.factory.initial>
            org.apache.activemq.jndi.ActiveMQInitialContextFactory
            </env.java.naming.factory.initial>
            <env.java.naming.provider.url>
            tcp://localhost:61616
            </env.java.naming.provider.url>
        </properties>
    
        <realm-name>demo</realm-name>
        <!--
        <authorization-constraint>
         <require-role>AUTHORIZED</require-role>
        </authorization-constraint>
        -->
        <cross-site-constraint>
             <allow-origin>${gateway.hostname}:${gateway.extras.port}</allow-origin>
        </cross-site-constraint>
    </service>    
    
  17. Build and run the client in the iPhone Simulator.

    1. In the Scheme menu, select iPhone 5.1 Simulator (you might have a more recent version such as iPhone 6.1).
      Figure: Scheme using iPhone 5.1 Simulator
    2. Click Run.

      The iPhone Simulator displays the client.

      Figure: Your JMS Demo client on the iPhone Simulator
    3. Click Connect. The client connects to the JMS service via a publicly available instance of the Gateway.
    4. Click Subscribe. You are now subscribed to the topic.
    5. Click Send. The message is sent and the received instantly because you are a subscriber.
    6. Click Disconnect to end the connection.

Migrate iOS Applications to Kaazing WebSocket Gateway 4.x

If you wish to migrate your Kaazing WebSocket Gateway 3.3-3.5 iOS clients to Kaazing WebSocket Gateway 4.x and use its new library, do the following:

  1. Add the new KGWebSocket.framework in Gateway 4.x to your client, as described in the above procedure.
  2. Add the new KMStompJMS.framework in Gateway 4.x to your client, as described in the above procedure. This has replaced the KMStompJMS.framework from 3.3-3.5.
  3. Change any KMStompJMS references to both KGWebSocket and KMStompJMS, such as in the import statements in JMSViewController.m above (#import <KGWebSocket/WebSocket.h>, #import <KMStompJMS/KMStompJMS.h>). You do not need to change any instances of KMStompConnectionFactory.
  4. Modify challenge handlers. In Kaazing WebSocket Gateway 4.x, the KGChallengeHandlers class from 3.3-3.5 was replaced with by the KGChallengeHandler modifier of the WebSocketFactory class. The KGChallengeHandler modifier is used during authentication for connections and subsequent revalidation that occurs at regular intervals.

    Kaazing WebSocket Gateway 3.3-3.5:

    // Set up ChallengeHandlers to handle authentication challenge.
    KGLoginHandler          *loginHandler = [[DemoLoginHandler alloc] init];
    KGBasicChallengeHandler *challengeHandler = [KGChallengeHandlers load:@"KGBasicChallengeHandler"];
    
    [challengeHandler setLoginHandler:loginHandler];
    [KGChallengeHandlers setDefault:challengeHandler];
    

    Kaazing WebSocket Gateway 4.x:

    - (KGChallengeHandler *) createBasicChallengeHandler {
        // Set up ChallengeHandlers to handle authentication challenge.
        KGLoginHandler          *loginHandler = [[DemoLoginHandler alloc] init];
        KGBasicChallengeHandler *challengeHandler = [KGBasicChallengeHandler create];
    
        [challengeHandler setLoginHandler:loginHandler];
        return challengeHandler;
    }
    
  5. Review the Objective-C JMS Client API.

Durable Subscribers

Note: Currently, the Gateway does not support durable subscribers with Apache ActiveMQ. You may use durable subscribers with TIBCO EMS or Informatica UM. For more information, see Durable Subscribers.

If your JMS client needs to receive all of the messages published on a topic, including the ones published while the subscriber is inactive because it is not being used or has lost connections (which is common when using mobile devices), create a durable KMTopicSubscriber using the createDurableSubscriber method on the KMSession object.

To unsubscribe from a durable topic, use the - unsubscribe method of the KMSession object.

The JMS provider retains a separate record of each durable subscription and ensures that all messages from the topic's publishers are retained until they are acknowledged by each durable subscriber or they have expired. Whether messages have been acknowledged is tracked separately for each durable subscriber, and each durable subscriber is identified by the combination of its name and the clientID (if any) set on the Connection. Ensure your application confirms that the clientID (if used) is unique to the user or device, or, if it does not use clientID's, ensure that the durable name is unique to the user or device.

Note: The following code example is not intended to be integrated into the iOS JMS client created earlier in this topic. The following example is a separate iOS JMS client that demonstrates durable subscribers.

Example

The following code shows the interface and implementation of the JMSDurableSubscriberMessageListener API.

@interface JMSDurableSubscriberMessageListener : NSObject<KMMessageListener>
@end

@implementation JMSDurableSubscriberMessageListener {
    NSString *_expectedValue;
}

- (void) onMessage:(KMMessage *)message {
    KMTextMessage *textMsg = (KMTextMessage *)message;
    NSString      *text = [textMsg text];
    NSLog(@"RECEIVED MESSAGE: %@", text);
}
@end

The following snippet, which is not part of the preceding class, creates the connection and the durable subscription. It provides examples of creating a durable subscription with and without a message selector.

.
.
.
- (void) receiveMessageUsingDurableSubscriber:(BOOL)withMessageSelector {
    // Create a KMConnectionListener.
    id<KMStompConnectionListener> listener = [
        [JMSDurableSubscriptionConnectionListener alloc] init];

    KMConnection *conn = [super createConnectionWithListener:listener];
    [conn start];
    
    KMSession *session = [conn createSession:KMSessionAutoAcknowledge
        transacted:NO];
    KMTopic *topic = [session createTopic:@"/topic/destination"];
    
    // Create a durable subscriber.
    KMMessageConsumer *consumer = nil;
    NSString *expectedValue = nil;

    // Without Message Selector
    if (!withMessageSelector) {
        consumer = [session createDurableSubscriber:topic name:@"durable1"];
        expectedValue = @"Hello, Durable Subscriber!";
    }
    
    // With Message Selector
    else {
        consumer = [session createDurableSubscriber:topic 
        name:@"durable1" 
        messageSelector:@"symbol='KZNG'" 
        noLocal:NO];
        expectedValue = @"Hello, Durable Subscriber with MessageSelector!";
    }

    [consumer setMessageListener:[[JMSDurableSubscriberMessageListener alloc] 
        initWithTestcase:self expectedValue:expectedValue]];
    
    [consumer close];
    
    // Unsubscribe durable subscriber
    [session unsubscribe:@"durable1"];
    
    sleep(3);
    
    [session close];
    [conn close];
}

When using message selectors, the same string value is used by methods sending or receiving messages with a message selector. You can see that the receiveMessageUsingDurableSubscriber method above uses a message selector string (line 26). A send method should have a condition such as:

if (withMessageSelector) {
    [textMsg setStringProperty:@"symbol" value:@"KZNG"];
}

Durable subscriptions do not require message selectors. Nor are message selectors limited to durable subscriptions. The preceding example simply shows a common combination of durable subscriptions and message selectors.

Convert Your Objective-C (iOS) Client to a 64-Bit Runtime Environment

iPhone 5s, iPad Air and iPad mini (2nd generation) both run on a completely new processor architecture: arm64. arm64 is the standard, 64-Bit architecture in Xcode 5.0.1. You can use Xcode 5.0.1 to update your Objective-C (iOS) client to support arm64. For more information, see Converting Your App to a 64-Bit Binary.

To update your Objective-C (iOS) client to support arm64:

  1. Install Xcode 5.0.1 or later from the Mac App Store.
  2. Open your Objective-C (iOS) client project. Xcode prompts you to modernize your project. Modernizing the project adds new warnings and errors that are important when compiling your app for 64-bit.
  3. Update your project settings to support iOS 5.1.1 or later. You cannot build a 64-bit project if it targets an iOS version earlier than iOS 5.1. Change the Architectures build setting in your project to Standard Architectures (including 64-bit). Set the Deployment Target to 7.1. For more information, see Converting Your App to a 64-Bit Binary.

Next Step

Secure Your Objective-C Client

Notes

  • Clients built using Kaazing WebSocket Gateway 3.x libraries will work against Kaazing WebSocket Gateway 4.x. If you wish to upgrade your 3.x client to the 4.x libraries, please note that the 3.x clients used a single Stomp JMS library and 4.x clients include and use separate WebSocket and Stomp JMS libraries. Update your client library file and code references to include both the WebSocket and Stomp JMS libraries, as described in the 4.x documentation.
  • For information about the dispatch_get_global_queue and dispatch_get_main_queue functions used in the JMSViewController implementation, see Dispatch Queues in the Concurrency Programming Guide of the iOS Developer Library.
  • iOS Development: Troubleshooting
  • TemporaryTopic and TemporaryQueue objects are destroyed when the client loses its connection to the Gateway, or when the JMS-compliant message broker loses its connection to the Gateway. To address this, monitor the client's exception listener to handle recovery for your application. Once the connection is re-established, recreate TemporaryTopic and TemporaryQueue. ConnectionDroppedException and ConnectionInterruptedException are delivered to the connection's exception listener via onException, indicating that messages in flight might be lost, depending on message delivery options. ConnectionRestoredException is delivered to indicate that the connection through to the JMS-compliant message broker has been re-established. TemporaryTopic and TemporaryQueue should be recreated at that time to resume operations.
TOP