New to Java? We'll help you get started with our revised beginner's tutorial, or our free online textbook.


Get the latest Java books
h t t p : / /w w w . j a v a c o f f e e b r e a k . c o m /

Java Coffee Break

Chapter 19: User Interfaces for Jini Services

Contents

19.1. User Interfaces as Entries

Interaction with a service is specified by its interface. This will be the same across all implementations of the interface. This doesn't allow any flexibility, and of course it shouldn't. It is the bottom level for using this type of service. But service implementations do differ, and there is a need to allow for this. The mechanism used in Jini is to put these differences in Entry objects. Typical objects supplied by vendors may include Name and ServiceInfo.

Clients can make use of the interface and these additional entry items, primarily in the selection of a service. But once they have the service, are they just constrained to use it via the type interface? Many services will probably benefit from some sort of user interface. For example, a printer may supply a method to print a file, but it may have the capability to print multiple copies of the same file. Rather than relying on the client to be smart enough to figure this out, the printer vendor may want to call attention to this by supplying a user-interface object with a special component for ``number of copies''.

The user interface for a service cannot expect to have all details supplied by the client - at best a client could only manage a fairly generic user interface. It should come from the vendor, or maybe even third parties. When your video player becomes Jini enabled, it would be a godsend for someone to supply a decent user interface for it, since the video player vendors seem generally incapable of doing so! The Entry objects are not just restricted to providing static data - as Java objects they are perfectly capable of running as user-interface objects.

User interfaces are not yet part of the Jini standard. But the Jini community (with a semi-formal organisation as the ``Jini Community'') is coming towards a standard way of specifying many things, including user-interface standards and guidelines. Guideline number one from the ``serviceUI'' group is: user interfaces for a service should be given in Entry objects.

19.2. User Interfaces from Factory Objects

In the chapter on ``More Complex Examples'' some discussion was given to the location of code, using user-interface components as examples. The chapter suggested that user interfaces should not be created on the server side but on the client side. So the user-interface should be exported as a factory object that can create the user-interface on the client side.

More arguments can be given to support this:

  1. A service exported from a low resource computer, such as an embedded Java engine may not have the classes on the service side needed to create the user-interface (it may not have the Swing or even the AWT libraries)
  2. There may be many potential user-interfaces for any particular service: the Palm Pilot (with small grey-scale screen) requires a different interface to a high-end workstation with huge screen and enormous numbers of colours. It is not reasonable to expect the service to create every one of these, but it could export factories capable of doing so
  3. Localisation of internationalised services cannot be done on the service side, only on the client side

The service should export zero or more user-interface factories, with methods to create the interface, such as getJFrame(). The service and its user-interface factory entry will both be retrieved by the client. The client will then create the user-interface. Note that the factory will not know the service object beforehand - if it was given one during its construction (on the service side) it would end with a service-side copy of the service instead of a client-side copy! So when it is asked for a user-interface (on the client side), it should be passed the service as well in a parameter to user-interface creation. In fact, it should probably be passed all of the information about the service, as retrieved in the ServiceItem obtained from a lookup service.

A typical factory is the one that returns a JFrame. This is defined as


package net.jini.lookup.ui.factory;

import javax.swing.JFrame;

public interface JFrameFactory {

    JFrame getJFrame(Object roleObject);
}
The factory imports the minimum number of classes for the interface to compile and be exported. An implementation of this interface will probably use many more. The roleObject passes in any necessary information to the UI constructor. This is usually the ServiceItem, as this contains all the information (including the service) that was retrieved from a lookup service. The factory can then create a UI that acts as an interface to the service, and can use any additional information in the ServiceItem such as entries for ServiceInfo which could be shown, say, in an ``about'' box.

A factory that returns a visual component such as this should not make the component visible. This will allow its size and placement to be set before showing it. Similarly, a ``playable'' UI such as an audio file, should not be in a ``playing'' state.

19.3. Current Factories

A service may supply lots of these factories, each capable of creating a different user interface object. This is to allow for the differing capabilities of ``viewing'' devices, or even for different user preferences. One user may always like a Web-style interface, another may be content with an AWT interface, a third may want the accessibility mechanisms possible with a Swing interface, and so on.

The set of proposed factories currently includes

  1. DialogFactory which returns an instance of java.awt.Dialog (or one of its subclasses)
  2. FrameFactory which returns an instance of java.awt.Frame (or one of its subclasses)
  3. JComponentFactory which returns an instance of javax.swing.JComponent (or one of its subclasses such as a JList)
  4. JDialogFactory which returns an instance of javax.swing.JDialog (or one of its subclasses)
  5. JFrameFactory which returns an instance of javax.swing.JFrame (or one of its subclasses)
  6. PanelFactory which returns an instance of java.awt.Panel (or one of its subclasses)

These factories are all defined as interfaces. An implementation will define a getXXX() method that will return a user interface object. The current set of factories returns objects that belong to the Swing or AWT classes. Factories added in later iterations of the specification may return objects belonging to other user interface styles, such as speech objects. Although an interface may specify that a method such as getJFrame() will return a JFrame, an implementation will in fact return a subclass of this, which also implements a role interface.

19.4. Marshalling Factories

There may be many factories for a service, and each of them will generate a different user interface. These factories and their user interfaces will be different for each service. The standard factory interfaces will probably be known to both clients and services, but the actual implementations of these will only be known to services (or maybe to third-party vendors who add a user interface to a service).

If a client receives a ServiceItem containing entries with many factory implementation objects, it will need to download the class files for all of these, as it instantiates the entry objects. There is a strong chance that each factory may be bundled into a jar file that also contains the user interface objects themselves. So if the entries directly contain the factories, then the client will need to download a set of class files, before it even goes about the business of deciding which of the possible user interfaces it wants to select.

This downloading may take time on a slow connection, such as wireless or home network link. It may also cost memory which may be scarce in small devices such as PDA's. So it is advantageous to hide the actual factory classes until the client has decided that it does in fact want a particular class. Then, of course, it will have to download all of the class files needed by that factory.

In order to hide the factories, they are wrapped in a MarshalledObject. This keeps a representation of the factory, and also a reference to its codebase, so that when it is unwrapped the necessary classes can be located and downloaded. By putting it into entries in this form, no attempt is made to download its classes until it is unmarshalled.

The decision as to whether or not to unmarshall a class can be made on a separate piece of information, such as a set of String's which hold the names of the factory class (and all of its superclasses and interfaces). This level of indirection is a bit of a nuisance, but not too bad:


if (typeNames.contains("net.jini.lookup.ui.factory.JFrameFactory") {
    factory = (JFrameFactory) marshalledObject.get();
    ....
} 
A client that does not want to use a JFrameFactory will just not perform this test, unmarshalling or attempted coercion. This does place a responsibility on service-side programmers to ensure this coercion will be correct. In effect, this is a manoeuvre to circumvent the type-safe model of Java purely for optimisation purposes.

There is one final wrinkle on loading the class files for a factory: a running JVM may have many class loaders. When loading the files for a factory, you want to make sure that the class loader is one that will actually download the class files across the network as required. The class loader associated with the service itself will be the most appropriate for this.

19.5. UIDescriptor

An entry for a factory must contain the factory itself hidden in a MarshalledObject and some string representation of the factory's class(es). It may need other descriptive information about the factory. The UIDescriptor captures all this


package net.jini.lookup.entry;

public class UIDescriptor extends AbstractEntry {

    public String role;
    public String toolkit;
    public Set attributes;
    public MarshalledObject factory;

    public UIDescriptor();
    public UIDescriptor(String role, String toolkit, 
                        Set attributes, MarshalledObject factory);

    public final Object getUIFactory(ClassLoader parentLoader)
           throws IOException, ClassNotFoundException;
}

There are several features in the UIDescriptor that we haven't mentioned yet, and the factory type appears to be missing (it is one of the attributes).

19.5.1 Toolkit

A user interface will typically require a particular package to be present or it will just not function. For example, a factory that creates a JFrame will require the javax.swing package. This can provide a quick filter on whether or not to accept a factory: if it is based on a package the client doesn't have, then it can just reject this factory.

This isn't a bullet-proof means of selection. For example, the Java Media Framework is a fixed-size package designed to handle lots of different media types. So if your user interface is a Quicktime movie, then you might specify the JMF package. However, the media types it handles are not fixed, and can depend on native code libraries. For example, the current Solaris version of the JMF package has a native code library to handle MPEG movies, which is not present in the Linux version. So having the package specified by the toolkit does not guarantee that the class files for this user interface will be present.

19.5.2 Role

There are two clear roles for users of a service - the ordinary user and an administrator. There may be many others, not identified yet. The role is intended to cover this. This is specified as an interface, with one field role, which is the fully qualified path name of the interface. There are currently three interfaces specified:

  1. net.jini.lookup.ui.MainUI, with net.jini.lookup.ui.MainUI.role set to "net.jini.lookup.ui.MainUI". This role is for the standard user
  2. net.jini.lookup.ui.AdminUI for the service's administrator
  3. "net.jini.lookup.ui.AboutUI" for information about the service presentable by a user interface object

A service will specify a role for each of the user interfaces it supplies. The user interface should act in this role, and indicate that it does so by implementing the particular role specified. There is still discussion about this: the UIDescriptor contains the role, the factory knows nothing explicitly about this role, but must produce a user interface that implements the role. Clearly, the factory needs to know the role in order to produce the right user interface, but perhaps the user interface itself doesn't.

There has been much discussion about ``flavours'' to roles, such as an ``expert'' role, or a ``learner'' role. This has been deferred as too hard for the first version of the specification.

19.5.3 Attributes

The attributes section of a UIDescriptor can carry any other information about the user interface object that is deemed useful. Currently this includes

  1. A UIFactoryTypes which contains a set of String's. This contains the strings for the fully qualified class names of the factory this entry contains. The current factory hierarchy is very shallow so this may be just a singleton set, such as
    
            Set attribs = new HashSet();
            Set typeNames = new HashSet();
            typeNames.add(JFrameFactory.typeName);
            attribs.add(new UIFactoryTypes(typeNames));                             
    
    Note that a client is not usually interested in the actual type of the factory, but rather of the interface it implements. This is just like Jini services themselves, where we only need to know the methods that can be called, and are not concerned with the implementation details. At least, that is my current interpretation of this!
  2. An AccessibleUI object. Inclusion of this is a statement that the user interface implements javax.accessibility.Accessible and that the uer interface would work well with assistive technologies
  3. A Locales object which specifies the locales supported by the user interface
  4. A RequiredPackages object which contains information about all of the packages that the user interface needs to run. This is not a guarantee that the user interface will actually run, nor a guarantee that it will be a usable interface! But it may help a client decide whether or not to use a particular user interface

19.6. File Classifier Example

The file classifier has been used throughout as a simple example of a service, to illustrate various features of Jini. We can use this here too, by supplying simple user interfaces into the service. Such a user interface would consist of a text field to enter a filename, and a display of the MIME type of the filename. There is only a ``main'' role for this service, as no administration needs to be performed.

A user interface for a file classifer could look like

Figure 19.1: FileClassifier user interface
After the service has been invoked it pops up a dialog box
Figure 19.2: FileClassifier return dialog

A factory for the ``main'' role that will produce an AWT Frame is



/**
 * FileClassifierFrameFactory.java
 */

package ui;

import net.jini.lookup.ui.factory.FrameFactory;
import net.jini.lookup.entry.UIDescriptor;
import java.awt.Frame;
import net.jini.core.entry.Entry;
import net.jini.core.lookup.ServiceItem;

public class FileClassifierFrameFactory implements FrameFactory {

    // for any object, this should satisfy 
    // typeName.equals(this.getClass().toString())
    static public String typeName = "ui.FrameFileClassifierFactory";

    /**
     * Return a new FileClassifierFrame that implements the
     * MainUI role
     */
    public Frame getFrame(Object roleObject) {
	// we should check to see what role we have to return
	if (! (roleObject instanceof ServiceItem)) {
	    // unkown role type object
	    // can we return null?
	    return null;
	}
	ServiceItem item = (ServiceItem) roleObject;
	Entry[] entries = item.attributeSets;
	for (int n = 0; n < entries.length; n++) {
	    if (entries[n] instanceof UIDescriptor) {
		UIDescriptor desc = (UIDescriptor) entries[n];
		if (desc.role.equals(net.jini.lookup.ui.MainUI.role)) {
		    // Ok, we are in the MainUI role, so return a UI for that
		    Frame frame = new FileClassifierFrame(item, "File Classifier");
		    return frame;
		}
	    }
	}
	// couldn't find a role the factory can create
	return null;
    }
} // FileClassifierFrameFactory

The user interface object that performs this role is



/**
 * FileClassifierFrame.java
 */

package ui;

import java.awt.*;
import java.awt.event.*;
import net.jini.lookup.ui.MainUI;
import net.jini.core.lookup.ServiceItem;
import common.MIMEType;
import common.FileClassifier;
import java.rmi.RemoteException;

/**
 * Object implementing MainUI for FileClassifier.
 */
public class FileClassifierFrame extends Frame implements MainUI {
 
    ServiceItem item;
    TextField text;

    public FileClassifierFrame(ServiceItem item, String name) {
	super(name);

	Panel top = new Panel();
	Panel bottom = new Panel();
	add(top, BorderLayout.CENTER);
	add(bottom, BorderLayout.SOUTH);
	
	top.setLayout(new BorderLayout());
	top.add(new Label("Filename"), BorderLayout.WEST);
	text = new TextField(20);
	top.add(text, BorderLayout.CENTER);

	bottom.setLayout(new FlowLayout());
	Button classify = new Button("Classify");
	Button quit = new Button("Quit");
	bottom.add(classify);
	bottom.add(quit);

	// listeners
	quit.addActionListener(new QuitListener());
	classify.addActionListener(new ClassifyListener());

	// We pack, but don't make it visible	
	pack();
    }

    class QuitListener implements ActionListener {
	public void actionPerformed(ActionEvent evt) {
	    System.exit(0);
	}
    }

    class ClassifyListener implements ActionListener {
	public void actionPerformed(ActionEvent evt) {
	    String fileName = text.getText();
	    final Dialog dlg = new Dialog((Frame) text.getParent().getParent());
	    dlg.setLayout(new BorderLayout());
	    TextArea response = new TextArea(3, 20);

	    // invoke service
	    FileClassifier classifier = (FileClassifier) item.service;
	    MIMEType type = null;
	    try {
		type = classifier.getMIMEType(fileName);
		if (type == null) {
		    response.setText("The type of file " + fileName +
				     " is unknown");
		} else {
		    response.setText("The type of file " + fileName +
				     " is " + type.toString());
		}
	    } catch(RemoteException e) {
		response.setText(e.toString());
	    }

	    Button ok = new Button("ok");
	    ok.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    dlg.setVisible(false);
		}
	    });

	    dlg.add(response, BorderLayout.CENTER);
	    dlg.add(ok, BorderLayout.SOUTH);
	    dlg.setSize(300, 100);
	    dlg.setVisible(true);
	}
    }
    
} // FileClassifierFrame

The server which delivers both the service and the user interface has to prepare a UIDescriptor. In this case it only creates one such object for a single user interface, but if the server exported more interfaces then it would simply create more descriptors


/**
 * FileClassifierServer.java
 */

package ui;

import complete.FileClassifierImpl;

import net.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import net.jini.discovery.LookupDiscovery;
import net.jini.core.lookup.ServiceRegistrar;
import java.rmi.RemoteException;
import net.jini.lookup.ServiceIDListener;
import net.jini.lease.LeaseRenewalManager;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.discovery.DiscoveryEvent;
import net.jini.discovery.DiscoveryListener;
import net.jini.core.entry.Entry;

import net.jini.lookup.ui.MainUI;
import net.jini.lookup.ui.factory.FrameFactory;
import net.jini.lookup.entry.UIDescriptor;
import net.jini.lookup.ui.attribute.UIFactoryTypes;

import java.rmi.MarshalledObject;
import java.io.IOException;
import java.util.Set;
import java.util.HashSet;

public class FileClassifierServer 
    implements ServiceIDListener {
    
    public static void main(String argv[]) {
	new FileClassifierServer();

        // stay around forever
	Object keepAlive = new Object();
	synchronized(keepAlive) {
	    try {
		keepAlive.wait();
	    } catch(InterruptedException e) {
		// do nothing
	    }
	}
    }

    public FileClassifierServer() {

	JoinManager joinMgr = null;

	// The typenames for the factory
	Set typeNames = new HashSet();
	typeNames.add(FrameFactory.typeName);
	// should we bother with this?
	typeNames.add(FileClassifierFrameFactory.typeName);

	// The attributes set
	Set attribs = new HashSet();
	attribs.add(new UIFactoryTypes(typeNames));

	// The factory
	MarshalledObject factory = null;
	try {
	    factory = new MarshalledObject(new FileClassifierFrameFactory());
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(2);
	}
	UIDescriptor desc = new UIDescriptor(MainUI.role,
					     attribs,
					     factory);
	desc.toolkit = FileClassifierFrameFactory.toolkit; // wrong arg count in constructor

	Entry[] entries = {desc};

	try {
	    LookupDiscoveryManager mgr = 
		new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					   null /* unicast locators */,
					   null /* DiscoveryListener */);
	    joinMgr = new JoinManager(new FileClassifierImpl(), /* service */
				      entries /* attr sets */,
				      this /* ServiceIDListener*/,
				      mgr /* DiscoveryManagement */,
				      new LeaseRenewalManager());
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
    }

    public void serviceIDNotify(ServiceID serviceID) {
	// called as a ServiceIDListener
	// Should save the id to permanent storage
	System.out.println("got service ID " + serviceID.toString());
    }
    
} // FileClassifierServer

Finally, a client needs to look for and use this user interface. This client does a search for a user interface playing the ``main'' interface role and delivering an AWT Frame.



package client;

import common.FileClassifier;
import common.MIMEType;

import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.lookup.ClientLookupManager;
import net.jini.core.lookup.ServiceItem;
import net.jini.lease.LeaseRenewalManager;
import net.jini.core.entry.Entry;

import net.jini.lookup.ui.MainUI;
import net.jini.lookup.ui.factory.FrameFactory;
import net.jini.lookup.entry.UIDescriptor;
import net.jini.lookup.ui.attribute.UIFactoryTypes;

import java.awt.*;
import javax.swing.*;

import java.util.Set;
import java.util.Iterator;
import java.net.URL;

/**
 * TestFrameUI.java
 */

public class TestFrameUI {

    private static final long WAITFOR = 100000L;

    public static void main(String argv[]) {
	new TestFrameUI();

        // stay around long enough to receive replies
        try {
            Thread.currentThread().sleep(2*WAITFOR);
        } catch(java.lang.InterruptedException e) {
            // do nothing
        }
    }

    public TestFrameUI() {
	ClientLookupManager clientMgr = null;

	System.setSecurityManager(new RMISecurityManager());

        try {
            LookupDiscoveryManager mgr =
                new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
                                           null /* unicast locators */,
                                           null /* DiscoveryListener */);
	    clientMgr = new ClientLookupManager(mgr, 
						new LeaseRenewalManager());
	} catch(Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
  
	Class [] classes = new Class[] {FileClassifier.class};
	UIDescriptor desc = new UIDescriptor(MainUI.role, null, null);
	desc.toolkit =  FrameFactory.toolkit; // due to bug
	Entry [] entries = {desc};

	ServiceTemplate template = new ServiceTemplate(null, classes, 
						       entries);

	ServiceItem item = null;
	try {
	    item = clientMgr.lookup(template, 
				    null, /* no filter */ 
				    WAITFOR /* timeout */);
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
	
	if (item == null) {
	    // couldn't find a service in time
	    System.out.println("no service");
	    System.exit(1);
	}

System.out.println("Have service");
	Entry[] attributes = item.attributeSets;
	for (int m = 0; m < attributes.length; m++) {
	    Entry attr = attributes[m];
	    if (attr instanceof UIDescriptor) {
		showUI(item, (UIDescriptor) attr);
	    }
	}
    }

    private void showUI(ServiceItem item,
			UIDescriptor desc) {
System.out.println("Showing ui");
        // Find a UIFactoryTypes in the entry attributes
	Set attribs = desc.attributes;
	Iterator iter = attribs.iterator();
	while (iter.hasNext()) {
	    Object obj = iter.next();
	    if (obj instanceof UIFactoryTypes) {
		UIFactoryTypes types = (UIFactoryTypes) obj;
		// see if it produces an AWT Frame Factory
		if (types.isAssignableTo(FrameFactory.class)) {
		    FrameFactory factory = null;
		    try {
			factory = (FrameFactory) desc.getUIFactory(this.getClass().
								  getClassLoader());
		    } catch(Exception e) {
			e.printStackTrace();
			continue;
		    }
System.out.println("Found factory");

                    Frame frame = factory.getFrame(item); 
		    frame.setVisible(true);
System.out.println("showing frame");
		} 
	    }
	}
    }

} // TestFrameUI

19.7. Images

User interfaces often contain images. They may be used as icons in toolbars, for general images on the screen, or for the icon image when the application is iconified. When a user interface is created on the client, these images will also need to be created and installed in the relevant part of the application. Images are not serializable, so they cannot be created on the server and exported as live objects in some manner. They need to be created from scratch on the client.

The Swing package contains a convenience class ImageIcon. This can be instantiated from a byte array, a filename, or most interestingly here, from a URL. So if an image is stored where an HTTP server can find it, then the ImageIcon constructor can use this directly. There may be failures in this: the URL may be incorrect or malformed, or the image may fail to exist on the HTTP server. Suitable code is


        ImageIcon icon = null;
	try {
	    icon = new ImageIcon(new URL("http://localhost/images/mindstorms.jpg"));
	    switch (icon.getImageLoadStatus()) {
	    case MediaTracker.ABORTED:
	    case MediaTracker.ERRORED:
		System.out.println("Error");
		icon = null;
		break;
	    case MediaTracker.COMPLETE:
		System.out.println("Complete");
		break;
	    case MediaTracker.LOADING:
		System.out.println("Loading");
		break;
	    }
	} catch(java.net.MalformedURLException e) {
	    e.printStackTrace();
	}
	// icon is null or is a valid image

19.8. MindStorms

MindStorms stuff temporarily removed till the service ui is finalised

19.9. Summary

The ui-service group is evolving a standard mechanism for services to distribute user-interfaces for Jini services. The preference is to do this by entry objects.


This file is Copyright (©) 1999, 2000 by Jan Newmarch (http://pandonia.canberra.edu.au) jan@ise.canberra.edu.au.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v0.4 or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.

Back to main


Copyright 1998, 1999, 2000 David Reilly

Privacy | Legal | Linking | Advertise!

Last updated: Monday, June 05, 2006