How to write a URC Client in Java

Page content:

  1. Introduction
  2. Create a Client
  3. Initialize UI
  4. Logger Utility
  5. Binding to and configuring a TUN Link
  6. Milestone: Basics Completed
  7. targetDiscovered
  8. Socket List/getSocketListWindow
  9. Open Session and the Session Panel
  10. UIBuilder and Interactors
  11. ResourceManager, User Preferences and Interactors contd.
  12. Create a Launcher class
  13. Milestone: A semi-working client
  14. targetDiscarded
  15. sessionAborted/disconnectSession
  16. Unbinding from a TUN link

1.   Introduction

This tutorial is designed for developers interested in writing a Universal Remote Console (URC) in Java based on the ANSI standards. The example code in this tutorial is a text-based Client. The tutorial follows step by step instructions in creating the Client.

2.   Create a Client

There are two ways defined in the sdk to create a client. One is to extend the GenericClient class and the other is to implement the IClient interface. For the text client in Java, we extend the GenericClient class (which implements the IClient interface. The GenericClient contains the required methods that communicate with the Target. It also has 3 methods that need to be overridden in the defined client class. These are

The client object or the launcher must implement the IClientListener interface. This interface listens to events from IClient.

An important input to the client used by the GenericClient class is code base. It is an input parameter in the constructor of the GenericClient.

public TextClient(URL codeBase) extends GenericClient implements IClientListener {
 super(codeBase);
}

Activate the link to view the entire TextClient code.

Note: The SDK defines a class known as CommonUtility which has a setter method for the codeBase. codeBase is a URL for the client packages location. More in detail under the launcher class.

3.  Initialize UI

The next step is to create the desired UI model. In the case of the text client, the System.in and System.out classes are used to get the user-input, and print output respectively. The input uses InputStreamReader and StreamTokenizer which are part of the java.io package. The output uses System.out.print statements to display information to the user. In the text client, we hard code all the labels and messages in English for convenience. To support internationalization, the strings can be bundled using a generic java resource bundle.

InputStreamReader isr = new InputStreamReader(System.in);
StreamTokenizer st = new StreamTokenizer(isr);
while (read &&st.nextToken() !=StreamTokenizer.TT_EOF) {
 switch (st.ttype) {
  case java.io.StreamTokenizer.TT_NUMBER:
   //Incase the input is a number value
  case java.io.StreamTokenizer.TT_WORD:
   //Incase the input is a word value
 }
} 

4.   Logger Utility

The SDK defines a LoggerUtil class in order to handle info, error, and warning messages to the user. Since the logger prints out to the System.out which is also the output for the text client, we use the logger only for error and warning conditions that the user must know about.

import java.util.logging.Logger;
import edu.wisc.trace.urcsdk.support.LoggerUtil;
private Logger logger = LoggerUtil.getSdkLogger();
logger.setLevel(Level.Warning);

The logger class comes at 3 levels for the display - info, warning, severe. If during testing of the text client, you want to print out interim variables or info statements, the logger level should be changed to info and logger.info() statements can be added in the code. A few logger.info statements in the code exist but don't get displayed. e.g. logger.info("Constructing Text Client");

5.   Binding to and configuring a TUN Link

Target-Urc Network (TUN) links should be created and added in order to create the link between the URC and the Target. This is necessary in order to discover the target and send information/commands to and from the client. Two options regarding TUN links are, creating a TUN link or using the one provided in the sdk. The provided TUN links are the Upnp2sClientTun and the UpnpDcpsTun. In our case, we use the Upnp2sClientTun. The client needs to call the bindToClientTun method in order to achieve this.

Map<String, Object> parameters = new HashMap<String, Object>();
parameters.put("UDN", "Text Client" + (new Date().toString()));
parameters.put("friendlyName", "Text Client");
try {
 textClient.bindToClientTun("edu.wisc.trace.urcsdk.client.upnp2s.Upnp2sClientTun", parameters);
} catch(UrcException e){
   logger.severe(e.getMessage());
} 

While binding to the TUN link, we also pass a parameter map which is a HashMap containing the parameter names and their values required by the TUN link.

In the case of the Upnp2sClientTun, the parameters are:

Note: The Upnp2sClientTun is a UPnP 2-Service TUN. The UPnP 2-Service TUN link uses two vendor-specific, discoverable UPnP devices, named “URCDevice” and “TargetDevice”. Each one includes a UPnP service, named “URCService” and “TargetService”, through which they offer actions that can be invoked by the other device. See package.html in package edu.wisc.trace.urcsdk.target.upnp2s for more information.

6.   Milestone: Basics Completed

At this point, a basic URC with no real functionality has been completed. To get the URC to work, the methods that the GenericClient defines need to be overridden and the UI for the different interactors has to be created.

We will start in the following order -

7.  targetDiscovered

This is the method that is invoked when the client discovers a target via the TUN link. In this method, you should implement all the functions that the URC should perform when it finds a Target. (e.g. add to the list of already existing targets and show up on the UI).

The targetDiscovered method takes a TargetMirror object as a parameter. The TargetMirror class defines a getSockets() method that builds the available sockets for the target. In the text-urc, we created a SocketListNode which contains the target socket and a list that contains all the SocketListNodes. This list contains all the available targets.The SocketListNode represents a socket item in the socket list. It contains all of the necessary information to render a socket item correctly. The next step is to update any UI and add the targets ResourceManager to the list of ResourceManagers.

private List<SocketListNode> socketListModel = new ArrayList<SocketListNode>;
try {
 for (UISocketMirror socket : target.getSockets()) {
  SocketListNode socketListNode = new SocketListNode(socket);
  socketListModel.add(socketListNode);
  //list of socketlistnodes
 }
} catch(Exception e ){
   System.out.println("Exception");
}
if (target == null)
 return; //do nothing!
targetResourceManagers.add(target.getResourceManager());
getSocketListWindow(); // UI for text-client  

8.   Socket List/getSocketListWindow

Now that the target has been discovered, we have to display the target in the socket list window. The socket list window contains all the targets that have been discovered and are active. It keeps track of the available targets using a variable called socketListModel. The socketListModel is an ArrayList that contains the list of available sockets. When a target is discovered, its sockets are added to the list and on being disconnected/discarded, the sockets are removed from the list. A few error checking has also been implemented. At this point, the socket list window waits for the user input to select a socket.

System.out.println("\n---------------------------");
System.out.println("List of available Targets: ");
for (int i = 0; i < socketListModel.size(); i++) {
 System.out.println(i + 1+ ": "+ socketListModel.get(i).getSocket().getLabel(this.getPreferences()));
}
InputStreamReader isr = new InputStreamReader(System.in);
StreamTokenizer st = new StreamTokenizer(isr);
if(socketListModel.size()==0) {
 System.out.println("No targets available");
}
else {
 System.out.print("Enter target choice(corresponding #): ");
}
int selection = 0;
boolean read = true;
try {
 while (read &&st.nextToken() !=StreamTokenizer.TT_EOF) {
  switch (st.ttype) {
   case java.io.StreamTokenizer.TT_NUMBER:
    selection = (int) st.nval;
    if (selection > socketListModel.size()|| selection == 0) {
     System.out.println("Invalid Entry, try again");
    }
    else {
     read = false;
     SocketListNode n = socketListModel.get(selection - 1);
     activateSocket(n.getSocket());
    }
    break;
   case java.io.StreamTokenizer.TT_WORD:
    System.out.println("Invalid Entry, try again");
    break;
  }
 }
}

9.    Open Session and the Session Panel

When the user selects a socket from the above socket list window, the activateSocket method is called which builds the UI for the session. The UI is referred to as Session Panel. It is responsible for managing all the widgets created by the UIBuilder. The activateSocket methods parameter is a UISocketMirror object. A session between the client and target is kept alive till a closeSession or abortSession is invoked or the target is discarded. Hence, before creating a new session every time the user selects a socket, a record of sessions and session panel is kept and reused. A session panel could be in Swing, text format, etc.  Code for creating a new session and session panel.

ClientSession session = null;
try {
 String sessionId = this.openSessionRequest(socket.getTarget().getTargetId(), socket.getName());
 session = this.getSessionById(sessionId);
} catch (UrcException e) {
 ErrorHandler.newException(this, e.getLocalizedMessage(), true);
}
SessionPanel sp = (SessionPanel) uiBuilder.buildUI(session);
sp.updateResources();
session.checkDependencies();

The buildUI for the UIBuilder class is passed the session object. The checkDependencies method is called on the client session so that the interactors that can be used are displayed. e.g. All interactors for a TV target are not active till the power is turned on.

10. UIBuilder and Interactors

The SDK defines an IUIBuilder interface which should be implemented to create a builder. (This approach is based on the Presentation Template of a Target. For alternative UIIDs there needs to be additional code written.) The SDK also defines a Widget Class which is the abstract version of a renderable UI component. This class must be extended to create renderable UI components in a URC implementation. In the text-client we have a TextUIBuilder which implements the IUIBuilder. The buildUI method builds the user interface for each of the interactors present in the socket and returns the Session Panel. The session panel can then be displayed to the user.

public Object buildUI(ClientSession s) {
 this.socket = s.getSocket();
 this.target = socket.getTarget();
 this.urc = (GenericClient)target.getUrc();
 this.sessionPanel = new SessionPanel(socket, s);
 for (IInteractor i : session.getInteractors()) {
  Widget w = createWidget(i);
  // calls the individual interactors constructors
  if (w == null)  continue;
  if (w instanceof TextModalDialog)
   ((TextModalDialog)w).setSessionPanel(sessionPanel);
  sessionPanel.add(w);
  //adds the widget to the session panel
 }
 return sessionPanel;
}

Interactors are the UI implementations of the controls available on the target, as defined in the Presentation Template specification. The SDK further binds the input interactors to the socket variables based on their type. e.g. Boolean, InputString, InputNumber all define the Input interactor of the Presentation Template Specification.

There are 21 interactors defined in the SDK and you may build as many as needed for the client. All the interactors are defined in the pret package of the sdk. Some of the most commonly used ones are described in the tutorial and implemented in the text client. All the defined Interactors extend an abstract Widget class which define all the required methods. In the session panel whenever an interactor is launched, a doChange() method is called which creates the UI if there is any need for it and waits for a response. Interactors have 3 main functions that decide their functionality- if it's writable, executable and readable. The Trigger interactor is the only interactor implemented that can be executable. The input interactors have to be writable in order to be displayed. The output interactors must be readable in order to be displayed.

The interactors implemented in the code are Boolean, Select1, Group, InputNumber, InputString, Output, Range, Trigger. The interactors display similar style in terms of UI. Each interactor when selected displays a list of options that can be chosen from and then modified. A few implemented interactors are defined in detail, the rest can be implemented using the same technique.

Boolean

The boolean interactor is used in the target side to represent a button that needs to be flipped, like a mute/power button. The doChange method flips the value on the client side as well as the target side.This is achieved by calling the setValueRequest method defined in IUISocketElement. The boolean value of the interactor is passed as a string to the setValueRequest method. The boolean interactor is an input interactor which binds to a socket variable of type xsd:boolean.

Group

The group interactor is used to group certain functionality together.For example, audioGroup for all volume related controls. In the text-urc a list of available controls is displayed and the user can then choose one of the controls. Since the group interactor is a pseudo-group, that encapsulates the interactors, the functionality of the controls themselves need not be implemented here. This interactor binds to the pret tag group.

// group.getInteractors()  gets all the interactors inside a group.
for (IInteractor ints : ((Group) i).getInteractors()) {
   Widget w = b.createWidget(ints);
   addWidget(w);
}

The above code shows the creation of the interactors present inside the group.

Select1

The select1 interactor is used in the target side to represent a drop down menu or a list of values that the interactor can be set to. In the text client, the select1 interactor displays a numbered list to the user from which the value desired can be selected. This interactor binds to the pret tag select1.The select1 interactor can have two types of inputs: static and dynamic. The static values can be obtained by a getStaticMap() call to the socket element.

((UISocketVariable) interactor.getSocketElement()).getStaticMap();

The dynamic variables can be obtained by using the getDynamicVars() method for the socket element and then the values for the variables can be obtained using the getUISocketElementByRef() method on the session object. The following code shows how the dynamic values in the text client are found.

private Map<String, String[]> getDynamicValues() {
  Map<String, String[]> ret = new HashMap<String, String[]>();
  UISocketVariable element = (UISocketVariable) interactor.getSocketElement();
  List<String> dynamicRefVars = element.getDynamicRefVars();
  for (String id : dynamicRefVars) {
    ElementRef ref = new ElementRef(socket.getName(), id);
    IUISocketElement valueList = interactor.getSocketElement()
                            .getSession().getUISocketElementByRef(ref);
    if (valueList == null) {
       logger.info("Dynamic values unaccounted for, skipping.");
       continue;
    }
    String value = valueList.getValue().toString();
    if (value == null || "".equals(value)) {
       logger.info("Dynamic values for " + ref + " were null, ignoring.");
       continue;
    }
    String[] values = value.split(" ");
    ret.put(id, values);
  }
  return ret;
}

InputNumber, InputInteger

This interactor allows the user to input a number. The swing implementation uses a form field to take the input. e.g. channel numbers. This is an input interactor that binds to a socket variable of type xsd:double and xsd:int, xsd:integer respectively. The client implements the two interactors with the same UI but the values supported are validated by the SDK.

InputString

Similar to InputNumer the user can input a string using this interactor. This is an input interactor that binds to a socket variable of type xsd:string.

Time

This interactor is similar to the input string interactor with the only difference being it checks if the user input is a valid time input before setting the time. This is an input interactor that binds to a socket variable of type xsd:time.

Date, DateTime, Month, Year, YearMonth, MonthDay, Day

This set of interactors are similar to each other and hence grouped! They are bound to a socket variable of type

xsd:date, xsd:dateTime, xsd:gMonth, xsd:gYear, xsd:gYearMonth, xsd:gMonthDay, xsd:gDay respectively. The SDK uses the XMLGregorianCalendar object to represent the values of each of the above socket elements. Hence the values propogated must be a valid XMLGregorianCalendar value.

Output

This interactor displays text to the user if readable. In the text client, this is a statement printed using System.out but in a swing client, this could be displayed as a pop-up/alert or simply be presented anywhere on the clients interface. This interactor binds to the pret tag output.

Range

The user can choose a number in a given range. e.g. contrast, volume. In a swing based client, this could be represented as a slider. In the client design the option of continuous change is not implemented (changes while the slider is being moved). This interactor binds to the pret tag range.

Trigger

A trigger interactor is used to represent a button on a target. The trigger relies on the executable attribute, and is not active if not executable. e.g. play, rewind etc.This interactor binds to the pret tag trigger.

TimedTrigger

A timedTrigger interactor is used to represent a button on a target as well. The difference between a timedTrigger and a trigger is that it implements the uiSocket:timedCommand feature. The timedCommand has an added feature to the normal command, it has an attribute called timeToComplete that is available during the inProgress state. The timeToComplete is the time sent by the target as an estimate on the amount of time taken to complete the action. timeToComplete is a Duration variable. It can not be set by the client.

((TimedCommand)interactor.getSocketElement()).getTimeToCompleteAsMillis();

ModalDialog

A modal dialog interactor is used to represent a notification to the user sent by the target. The notification is defined in the uisocket for the target and can require the user to explicitly acknowledge reciept of the notification. This is done using the notify objects getExplicitAck() method. A modal dialog is similar to the group interactor in the sense that it can have interactors present within itself. Hence the UI for the modal dialog is very similar to groups. In a Swing client, the modalDialog can be a pop up box that displays a message to the user and have an OK, Close and other interactors. When a notification is displayed to the user and the user selects OK (when getExplicitAck() evaluates to true), the acknowledgeNotification() method must be called in order to propogate the response to the target. The modal dialog, unlike other interactors can not be set by the client side. This interactor binds to the pret tag modalDialog and the notify tag in the uisocket.

try {
  notify.acknowledgeNotification();
} catch(UrcException ue) {
    logger.warning(ue.getMessage());
}

To get the interactors functional, we need to be able to -

11. ResourceManager, User Preferences and Interactors contd.

The ResourceManager in the Resources package of the SDK takes care of all resource management. The target interactor labels, access keys, images etc are stored in a HashMap by the ResourceManager based on the target defined Resource Descriptions. A Resource Description provides information to the URC client on how to display the Socket variables defined in your Socket Description. Resource Descriptios are definined in ANSI 393-2005. The ElementRef specifies a reference to a specific element in the resource description. In our case, it will be the name of the interactor.The valueRef specifies a reference to a specific value that the specified element can have. e.g. The valueRef for a power button are True and False.

Note: Name of the interactor is not the label for the interactor.

The reason for using valueRef is because the ResourceDescription can define different labels/access keys/image for the same interactor element for each of its values. If there is no valueRef, the same label/access keys/image is used for all values.

The UserPreferences provide convenience methods for accessing the clients preferences that are stored in maps. The main UserPreferences used for the interactor labels are role, type, language, format. The role can be label, access key. The type specifies the nature or genre of the interactor in the Resource Description. It can be text/image. The format specifies the digital manifestation of the interactor in the Resource Description as MIME type (as defined by http://www.isi.edu/in-notes/iana/assignments/media-types/media-types). In our case, the format and type are limited to text since images aren't supported. The language preference has been hardcoded to English since the client doesn't support other languages. But this feature can be implemented.

We can get the label for the interactor using the ResourceManagers getResource method. The getResource() method also includes a boolean includeNullValueRef. If this boolean is true, then the resource manager will retrieve resources that have a null valueRef else it will choose resources that match the valueRef passed as the parameter.

public String getLabel() {
 UserPreferences uPrefs = builder.getUrc().getPreferences();
 UISocketMirror uis = interactor.getSocketElement().getSocket();
 ResourceManager rm = uis.getTarget().getResourceManager();
 ElementRef elementRef = interactor.getSocketElement().getRef();
 String valueRef = interactor.getSocketElement().getValue().toString();
 valueRef = (valueRef.equals("")) ? null : valueRef;
 IResource label = rm.getResource(elementRef, valueRef, true, opRef,
                    Constants.ResourceRole.Label.toString(), uPrefs);
 return label.getValue().toString();
}

Interactors that have values associated to them are displayed in paranthesis with the interactor label. This is done using the IUISocketElement interface method getValue.

interactor.getSocketElement().getValue().toString();

The value for the interactors can be set using the IUISocketElement interface method setValueRequest.

interactor.getSocketElement().setValueRequest(Boolean.toString(newValue));

12. Create a Launcher class

The main purpose of the launcher class is to get the codeBase or location where the code resides as a java.net.URL object and instantiate a Client object. The reason this is done is that the SDK uses the codeBase throughout its code to establish its absolute path on the local machine it resides.

String codeLoc = "";
URL codeBase = null;
codeLoc = new File("").getAbsolutePath().replace('\\', '/') + "/";
// need to replace \ for URL conversion
codeLoc = (codeLoc.startsWith("/")) ? "file:" + codeLoc : "file:/"+ codeLoc;
try {
 codeBase = new URL(codeLoc);
} catch (MalformedURLException e) {
   e.printStackTrace();
}
textClient = new TextClient(codeBase);

Before we can create the client object, we need to do some preparations. For the client we need to specify 2 parameters, namely the UDN (Unique Device Name) and a friendly name. The parameters are stored in a HashMap object. After the parameters have been set, the client object needs to bind to a TUN (Target-Urc Network) link. The parameters are passed to the TUN link during binding.The TUN link is explained later.

Map<String, Object> parameters = new HashMap<String, Object>();
parameters.put("UDN", "ABCDEFGHIad23424UVWXYZ");
parameters.put("friendlyName", "Text URC Client");

It is also used to create any UI for the overall application, e.g. borders, menus for the application etc. In the text URC, we do not have any graphical UI. Activate the link to get the entire code for the TextClientLauncher.

13. Milestone: A semi-working client

At this point, the display of the target on the client side and modification to the interactors should be functional. Now a few functions left to implement are the ones that are required to remove a target from the client side. These include client side removing of a particular session (abortSession/removeSession) or target side disconnection (targetDiscarded).

14.  targetDiscarded

The targetDiscarded method is defined in the IClientListener and is called when a target is unbound from the TUN link. In this method all the functionality when a target disappears needs to be taken care of. A few include, close all the socket sessions for the target that are open. If the user is viewing a socket(sessionPanel) for the target that has been discarded, the user should be informed and the session closed. The text-client does not close the session of another present target and if being viewed, the user is not informed about the discarded target either. It is the socket list that is updated accordingly. If the user is viewing the discarded targets socket, a message is printed on the screen and the user is asked to go back to the socket list.

targetResourceManagers.remove(target.getResourceManager());
Object[] nodes = socketListModel.toArray();
try{
 List <UISocketMirror> sockets = target.getSockets();
 // iterate through the socket list and remove the sockets that have
 // been discarded.
 for (Object portalNode : nodes) {
  UISocketMirror portal = ((SocketListNode) portalNode).getSocket();
  for (UISocketMirror testPortal : sockets) {
   if (testPortal == portal){
    socketListModel.remove(portalNode);
    disconnectSession(testPortal);
   }
  }
 }
} 

15.  sessionAborted/disconnectSession

There are two ways a session can be disconnected or aborted. One is when the target is discarded as in the case above and the second is when the user wants to close a session for a particular socket. This method goes through all the session panels and removes the ones for the given socket.

for (int n = 0; n < openPanels.size(); n++) {
 SessionPanel sp = openPanels.get(n);
 if (sp == null) {
  logger.info("No SessionPanel in tab " + n);
  continue;
 }
 // Warning is displayed only if the user is currently viewing the session that needs
 // to be closed and removed.
 if(sp.getSession().getSessionId() == session.getSessionId() && currentScreen=="session"){
  System.out.println("Target has been disconnected, Please hit");
  System.out.println("any key to return to main menu");
 }
 // Already open session panel found.
 if (sp.getSocket() == socket) {
  sp.getSession().closeSession();
  sp.removeSession();
  openPanels.remove(n);
  return;
 }
}

16. Unbinding from a TUN Link

When the URC client wants to close,exit or not want to communicate with targets via a TUN link, it should unbind itself from that TUN link. The text client unbinds from the Unpnp2sClientTun when the user decides to exit the client.

try {
  textClient.unbindClientTun("edu.wisc.trace.urcsdk.client.upnp2s.Upnp2sClientTun");
  System.exit(1);
} catch (UrcException e) {
  logger.warning(e.getMessage());
}

Last updated: Hemanth Vijayan, 2006-10-09