How to attach a user interface to a Jini service

An in-depth look at the serviceui project from the Jini community

1 2 3 Page 2
Page 2 of 3

/* * PrinterJPanelFactory - An implementation of the UIFactory interface * that knows to cast the service object to type PrinterService * and instantiates and returns a new PrinterJPanel GUI object. */ package com.artima.printer;

import org.jini.ui.UIFactory; import org.jini.ui.InvalidServiceClassException; import org.jini.ui.UICreationException; import org.printerfolks.PrinterService;

public class PrinterJPanelFactory implements UIFactory {

public Object getUI(Object serviceObject) throws UICreationException {

PrinterService ps; try { ps = (PrinterService) serviceObject; } catch (ClassCastException e) { throw new InvalidServiceClassException(); }

try { return new PrinterJPanel(ps); } catch (Exception e) { throw new UICreationException(e); } } }

Each UI factory class knows the type of service object that should be passed to its getUI() method. The first thing every getUI() method should do is attempt to cast the passed object to the expected type. If the cast doesn't succeed, the client has passed in an inappropriate object and the method should throw the unchecked InvalidServiceClassException. Otherwise, the getUI() method should attempt to create the UI object, passing the reference to the service object to the UI object's constructor. If any exception is thrown, the getUI() method should create and throw a new UICreationException, encapsulating the original exception inside the UICreationException.

Step 2. Pass the service object to the UI

Your factory (shown above) instantiates and returns a UI object, in this case, a JPanel. The JPanel you write must accept a reference to the service object in its constructor. The reference will be stored in an instance variable of the JPanel object, so that the JPanel can interact with the service. The JPanel can then serve as an intermediary between the user and the service. Here's an example:

/* * PrinterJPanel - A GUI panel that allows the user to interact with * a printer across the network via a Jini service object */ package com.artima.printer;

import javax.swing.*; import javax.swing.event.*; import javax.swing.border.*; import javax.accessibility.*; import java.awt.*; import java.awt.event.*; import org.jini.ui.*; import org.printerfolks.*;

public class PrinterJPanel extends JPanel {

private PrinterService serviceObject;

//...

public PrinterJPanel(PrinterService serviceObject) {

this.serviceObject = serviceObject;

//... }

//... }

Step 3. Place the UIFactory in an entry

Lastly, you must place your factory object in an entry so that it can be attached to the service item. Eventually, you should be able to use well-known entry classes to hold your UI factory attributes. All UI factory entry classes will probably belong to a family of classes rooted on a class, like UIFactoryEntry, shown here:

// In file UIFactoryEntry.java package org.jini.ui.entry; import net.jini.entry.AbstractEntry; import java.util.Map;

public class UIFactoryEntry extends AbstractEntry {

public String name; public String function; public UIFactory factory; public Map properties;

UIFactoryEntry() { }

UIFactoryEntry(String name, String function, UIFactory factory, Map properties) {

this.name = name; this.function = function; this.factory = factory; this.properties = properties; } }

The name variable is a string that should be unique among the UIFactoryEntry objects associated with each service item. You use the name when searching for an appropriate UI from among the UIs associated with a single service, as described later in this proposal.

The function variable indicates the UI's role. Some services may have different UIs for different purposes and give them different function names that indicate the purpose. Some examples are main for the main UI, admin for an administration UI, and so on. The serviceui project should establish recommended names for common functions and set guidelines for service providers who want to use different function names.

The factory variable simply holds a reference to the UI factory object that will produce the UI object.

The properties variable holds a Map that enables service providers to associate with a UI (property objects) that describes the UI. Clients can search through these properties to help them locate the most appropriate UI object for their needs. A description of the process is provided later in this proposal.

The inheritance relationships of UIFactoryEntry subclasses will correspond to the inheritance relationships of the UI objects they generate. For example, given that java.awt.Panel subclasses java.awt.Component, the corresponding UI factory entries would have a similar relationship. For example, PanelFactoryEntry would subclass ComponentFactoryEntry. Here's how that might look:

// In file ComponentFactoryEntry.java package org.jini.ui.entry.java.awt; import org.jini.ui.UIFactoryEntry; import java.util.Map;

public class ComponentFactoryEntry extends UIFactoryEntry {

// Could add other public fields here that allow instances to // characterize themselves in a way that can be selected by // matching criteria in the ServiceTemplate passed to an // invocation of lookup() on a ServiceRegistrar.

ComponentFactoryEntry() { }

ComponentFactoryEntry(String name, String function, UIFactory factory, Map properties) {

super(name, function, factory, properties); } }

// In file PanelFactoryEntry.java package org.jini.ui.entry.java.awt; import java.util.Map;

public class PanelFactoryEntry extends ComponentFactoryEntry {

// Could add other public fields here that allow instances to // characterize themselves in a way that can be selected by // matching criteria in the ServiceTemplate passed to an // invocation of lookup() on a ServiceRegistrar.

PanelFactoryEntry() { }

PanelFactoryEntry(String name, String function, UIFactory factory, Map properties) {

super(name, function, factory, properties); } }

To avoid name conflicts, the package names of factory entry classes should be composed of a to-be-chosen UI entry-package prefix (here, org.jini.ui.entry), followed by a dot and the package name of the UI object class that the factory generates. Thus, the package name for a factory entry for javax.swing.JPanel would be org.jini.ui.entry.javax.swing. This package-naming scheme enables anyone to easily and immediately add a factory entry for a newly invented UI object to the well-known UI entry package (here, org.jini.ui.entry). You shouldn't worry that you will interfere with someone else who happened to pick the same name, nor should you have to apply to some committee that controls the namespace for UI factory entries. If IBM invents a new UI object that combines voice and graphics and gives it the fully qualified name com.ibm.ui.vng.VoiceAndGraphics, it could immediately name its UI factory entry VoiceAndGraphicsFactoryEntry and place it in the org.jini.ui.com.ibm.ui.vng package.

The name field of each UIFactoryEntry associated with a particular service should be unique. (In other words, the uniqueness of these names need only be enforced among the attributes of each individual service item.) The name field lets a client grab the UIFactory object from a specific UIFactoryEntry via the getFieldValues() method of the ServiceRegistrar. You'll see an example of this later.

The function field of the JPanelFactoryEntry lets different UI objects that serve different purposes be identified in an attribute search. Possible names for functions are main and admin. The serviceui project should define a set of standard function strings.

The properties field contains a mapping from strings to property objects that characterize the UI object. For example, you might define a UI property object that describes the minimum screen size on which a particular UI should be used:

// In file MinimumScreenSize.java package org.jini.ui;

public class MinimumScreenSize implements java.io.Serializable {

private java.awt.Dimension minScreenSize;

public MinimumScreenSize(java.awt.Dimension dim) {

this.minScreenSize = dim; }

public java.awt.Dimension getMinimumScreenSize() { return minScreenSize; } }

A MinimumScreenSize property could then be associated with a UI object by adding it to the properties map of the UI's entry object and associating it with a well-known key, such as minscreensize. Clients could then inspect this property and others by searching in the map by their well-known keys. The UI's name and function are two properties that should sit in every map. (In other words, the information stored in the name and function fields of the UIFactoryEntry object should also appear in the properties map, perhaps under the keys name and function.) Properties, such as MinimumScreenSize, give information about particular UIs to facilitate client searches for the best UI for a situation. (Note that because of name and function, all entries should have a properties map.)

Step 4. Have the service object implement Identifiable (optional)

Service providers can attach any number of UIs to a service object by including them in the service item as attributes. A service provider may want to offer multiple UIs to accommodate clients who have different capabilities and users who have different preferences. A service could attach a Swing JPanel geared toward users sitting on a desktop, an AWT Panel aimed at television screens and remote-control input devices, a different AWT Panel for small PDAs with touch-sensitive screens, a UI object that offers a voice-only user interface for telephone users, and so on. The client must be able to search through the service's UI options easily and quickly to find the best service that matches its capabilities and its user's preferences.

To search for an appropriate UI object, a client with sufficient resources can just use the ServiceMatches lookup(ServiceTemplate, int) method of the ServiceRegistrar to retrieve the entire attribute set of a service along with the service object and its ServiceID object. It can then search through its local copies of all the attributes to find an appropriate UI.

Many clients, however, will not want to download all the attributes. Those attributes take up memory, which can be a scarce resource in a small consumer device. In addition, downloading them may trigger unnecessary class files to be downloaded as well. (The JVM specification lets implementations load classes before they are used.) Such clients can use the Object lookup(ServiceTemplate) method of ServiceRegistrar, which, rather than returning an array of matching ServiceItems, returns only one matching service object. If more than one service matches the template passed to this lookup() version, one is selected arbitrarily and returned.

If such a resource-constrained client wants to display a UI for a service it got from calling Object lookup(ServiceTemplate), it must search through that service's attributes via methods declared in the ServiceRegistrar interface. Unfortunately, the client won't have enough information to uniquely identify the service whose object it has a local copy of. To be certain it can uniquely identify the service, the client needs the service's ServiceID. But when the client invokes lookup(), it just gets the service object, not the ServiceID. Thus, the only way this client can get the ServiceID is by asking the service and invoking a method on the service object.

For this reason, services that want to be friendly to resource-constrained clients should implement the Identifiable interface, which lets clients ask the service for its ServiceID:

package org.jini.ui; import net.jini.core.lookup.ServiceID;

public interface Identifiable {

ServiceID getServiceID(); }

Note: As an alternative to Identifiable, it might be a nice enhancement to the ServiceRegistrar interface to add a method that returns one service object and its associated ServiceID, but no attributes.

Use cases

A resource-constrained client

To find an appropriate UI for a service, a resource-constrained client would:

1 2 3 Page 2
Page 2 of 3