Chapter 19: Configuration

Many Jini programs end up with hard-coded strings, objects and classes, which should really belong to runtime configuration. The Jini Configuration class allows these values to be deferred to runtime

19.1. Configuration

Most applications have runtime configuration mechanisms. For example, most Web browsers allow you to set the home page, which proxy server is used, default font sizes, etc. When an application starts it must be able to pick up these configuration options somehow. They are generally specified on the command line, put in a file or picked up from a database. Options on a command line are usually very simple, of the form vbl=value. For example, Sun's Java compiler takes command line options of the form -Dproperty=value. Configuration values stored in files can be more complex: while many applications will just use lines of vbl=value, it is possible to have complete programs in an interpreted programming language. For example, Netscape stores configuration values in liprefs.js as JavaScript function calls.

Jini 2.0 has mechanisms for support of runtime configuration. There is a spectrum of choices between simple values and a full programming language. From the programmer's viewpoint, accessing configuration information is basically restricted to getting the value of parameters by methods such as


Object Configuration.getEntry(String component, String name, Class type)

While simple, this is still quite powerful: you don't just get strings (like you get from Java properties or from command line arguments) - you get full Java objects. These could be URL objects for specifying unicast lookup services, protocol objects such as JrmpExporter, or any other Java objects such as arrays of hashmaps!

Configuration is an interface. You get an implementation of this interface by calling ConfigurationProvider.getInstance(configArgs). For example,


        String[] configArgs = new String[] {...};

        Configuration config = ConfigurationProvider.getInstance(configArgs); 

	Exporter exporter = (Exporter) config.getEntry( "JeriExportDemo", 
							"exporter", 
							Exporter.class); 

The implementation could support anything from simple variable/value pairs to a full programming language. The default implementation is a FileConfiguration.

19.2. Configuration File

The ConfigurationFile has chosen a middle route between variable/value pairs and full programming language. It uses a syntax based on Java that allows

  1. Values can be assigned to variables
  2. Objects can be created
  3. Static methods can be called
  4. Some access to the calling environment is allowed
Procedural constructs such as loops and conditional statements are not in this language. While constructors and calling static methods are included, general method calls are not. The full syntax is given in the API documentation for ConfigurationFile.

For example, the file jeri/jeri.config contains


import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.tcp.TcpServerEndpoint;

JeriExportDemo {
    exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
                                     new BasicILFactory()); 
}

This imports all classes needed. It defines a component JeriExportDemo, and within this component is an entry defining the identifier exporter. The identifier is assigned an expression which contains two constructors BasicJeriExporter() and BasicILFactory(). It also contains a static method call TcpServerEndpoint.getInstance().

This mechanism is not restricted to getting an exporter for RMI proxies. It can be used for any other configurable properties. Suppose a program wishes to use a particular url. Instead of passing it as a command-line parameter this can be placed in the configuration file


import net.jini.jrmp.*; 
import java.net.URL;

ConfigDemo {
    exporter = new JrmpExporter(); 
    url = new URL("http://jan.newmarch.name/internetdevices/jmf/test.wav");
}

and used by

        url = (URL) config.getEntry("ConfigDemo",
		                    "url",
				    URL.class);

In a similar manner a configuration can also be used to specify other strings and objects to a program. It can also be used to specify arrays of objects, though the syntax gets a little more complex. For example, suppose a set of Entry objects is required for a service. Since they are by definition additional information for a service, they should not be hardcoded into a program.Okay, so put them in the configuration file


import net.jini.jrmp.*; 
import java.net.URL;
import net.jini.core.entry.Entry;
import net.jini.lookup.entry.*;

ConfigDemo {
    exporter =  new JrmpExporter();
    url = new URL("http://localhost/internetdevices/jmf/test.wav");
    entries = new Entry[] {new Name("Jan Newmarch"),
	                   new Comment("Author of Jini book")};
}

The hard part is getting the array out of the configuration: the last argument to getEntry() is a Class object, which here has to be a class object for an array. The simplest way is

    Class cls = Entry[].class;
Retrieval follows the same pattern

    entries = (Entry []) config.getEntry("ConfigDemo",
				         "entries",
				         cls);

19.3. Specifying the configuration

The default configuration implementation is a ConfigurationFile. In order to find this, the ConfigurationProvider.getInstance() method has to be given a file name as parameter. But there are other possibilities: the configuration could be stored in a database, in which case the argument to ConfigurationProvider.getInstance() should be a database handle. Or it could be stored on a Web site, in which case it should be a URL. None of these other possibilities are at present supported, but there are hooks so that Jini (or any programmer) can provide implementations of Configuration that have these other behaviours.

In order to avoid tying down an implementation by explicitly hardcoding a filename into an application, this too should be left as a runtime parameter. But of course, we can't use configuration to specify a configuration - we need a bootstrapping mechanism. For this, we could fall back to command line arguments or Java properties.

Using a command line where the configuration file is given as the first command line argument, code would look like this


        if (args.length == 0) {
            System.err.println("No configuration specified");
            System.exit(1);
        }
        String[] configArgs = new String[] {args[0]};

        Configuration config = ConfigurationProvider.getInstance(configArgs); 

	Exporter exporter = (Exporter) config.getEntry( "JeriExportDemo", 
							"exporter", 
							Exporter.class); 

19.4. Service ID

A recommended practice is for a service to have a persistent service ID. Even if it stops and restarts it should have the same ID (unless a restart really represents a distinct service). The JoinManager class has different constructors to support this: a constructor for first-time registration and a constructor which supplies an earlier ID.

A service can get its service ID from several places. It may be pre-supplied by a vendor, but is most likely generated by the first LUS it is registered with. The ServiceIDListener interface is provided for a service to determine what ID it has been assigned. Once it has an ID, the service is expected to save this in "persistent storage" and reuse it later. The details of this persistent storage are of course unspecified by Jini. In an earlier chapter a binary representation of this was stored in an ".id" file and retrieved (if possible) when the service was restarted.

Hardcoding the ".id" filename is imposing a compile-time decision on what should be a runtime option. So the filename could be stored in a configuration file and extracted from there.

The configuration mechanism makes it tempting to store the ID in the configuration file, and pull it out of the there. If it is not in the file, then ask an LUS for the ID and rewrite the file to store it there for next time. This is quite a tall order for a general purpose system, especially since configurations may not be stored in files at all!

A better way is to store the persistent storage filename in the configuration. A configuration file could look like


import java.io.*;

ServiceIdDemo {
    serviceIdFile = new File("serviceId.id");
}

and a program using this configuration could be

package config;

import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistration;
import net.jini.core.lease.Lease;
import net.jini.core.lookup.ServiceID ;
import net.jini.lease.LeaseListener;             
import net.jini.lease.LeaseRenewalEvent;         
import net.jini.lease.LeaseRenewalManager;       
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.config.ConfigurationProvider;

import java.io.*;

/**
 * FileClassifierServerIDConfig.java
 */

public class FileClassifierServerIDConfig implements DiscoveryListener, 
                                             LeaseListener {
    
    protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
    protected ServiceID serviceID = null;
    protected complete.FileClassifierImpl impl;
    protected File serviceIdFile;

    public static void main(String args[]) {
	FileClassifierServerIDConfig s = new FileClassifierServerIDConfig(args);
	
        // keep server running forever to 
	// - allow time for locator discovery and
	// - keep re-registering the lease
	Object keepAlive = new Object();
	synchronized(keepAlive) {
	    try {
		keepAlive.wait();
	    } catch(java.lang.InterruptedException e) {
		// do nothing
	    }
	}
    }

    public FileClassifierServerIDConfig(String[] args) {
	// Create the service
	impl = new complete.FileClassifierImpl();

        if (args.length == 0) {
            System.err.println("No configuration specified");
            System.exit(1);
        }
        String[] configArgs = new String[] {args[0]};

        Configuration config = null;
	try {
	    config = ConfigurationProvider.getInstance(configArgs); 
	    serviceIdFile = (File) config.getEntry("ServiceIdDemo", 
						   "serviceIdFile", 
						   File.class); 
	} catch(ConfigurationException e) {
	    System.err.println("Configuration error: " + e.toString());
	    System.exit(1);
	}

	// Try to load the service ID from file.
	// It isn't an error if we can't load it, because
	// maybe this is the first time this service has run
	DataInputStream din = null;
	try {
	    din = new DataInputStream(new FileInputStream(serviceIdFile));
	    serviceID = new ServiceID(din);
	    System.out.println("Found service ID in file " + serviceIdFile);
	    din.close();
	} catch(Exception e) {
	    // ignore
	}

        System.setSecurityManager(new RMISecurityManager());

	LookupDiscovery discover = null;
        try {
            discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
        } catch(Exception e) {
            System.err.println("Discovery failed " + e.toString());
            System.exit(1);
        }

        discover.addDiscoveryListener(this);
    }
    
    public void discovered(DiscoveryEvent evt) {

        ServiceRegistrar[] registrars = evt.getRegistrars();

        for (int n = 0; n < registrars.length; n++) {
            ServiceRegistrar registrar = registrars[n];

	    ServiceItem item = new ServiceItem(serviceID,
					       impl, 
					       null);
	    ServiceRegistration reg = null;
	    try {
		reg = registrar.register(item, Lease.FOREVER);
	    } catch(java.rmi.RemoteException e) {
		System.err.println("Register exception: " + e.toString());
		continue;
	    }
	    System.out.println("Service registered with id " + reg.getServiceID());

	    // set lease renewal in place
	    leaseManager.renewUntil(reg.getLease(), Lease.FOREVER, this);

	    // set the serviceID if necessary
	    if (serviceID == null) {
		System.out.println("Getting service ID from lookup service");
		serviceID = reg.getServiceID();

		// try to save the service ID in a file
		DataOutputStream dout = null;
		try {
		    dout = new DataOutputStream(new FileOutputStream(serviceIdFile));
		    serviceID.writeBytes(dout);
		    dout.flush();
		    dout.close();
		    System.out.println("Service id saved in " +  serviceIdFile);
		} catch(Exception e) {
		    // ignore
		}

	    }
	}
    }

    public void discarded(DiscoveryEvent evt) {

    }

    public void notify(LeaseRenewalEvent evt) {
	System.out.println("Lease expired " + evt.toString());
    }   
    
} // FileClassifierServer

This could be run by

java FileClassifierServerIDConfig config/serviceid.config

19.5. Specifying codebase

A Jini service needs to specify the java.rmi.server.codebase property so that clients can pick up class definitions. In previous chapters where the command line to start a service has been shown, this has always been done by specifying a property at the command line


java -Djava.rmi.server.codebase=http://... ...
The Java runtime handles this parsing the command line, extracting the property and its value and using these to set the property value.

This can be done by the configuration mechanism. A server can pickup the codebase as in


package config;

import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistration;
import net.jini.core.lease.Lease;
import net.jini.core.lookup.ServiceID ;
import net.jini.lease.LeaseListener;             
import net.jini.lease.LeaseRenewalEvent;         
import net.jini.lease.LeaseRenewalManager;       
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.config.ConfigurationProvider;

import java.io.*;

/**
 * FileClassifierServerConfig.java
 */

public class FileClassifierServerCodebaseConfig implements DiscoveryListener, 
                                             LeaseListener {
    
    protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
    protected complete.FileClassifierImpl impl;
    protected File serviceIdFile;

    public static void main(String args[]) {
	FileClassifierServerCodebaseConfig s = new FileClassifierServerCodebaseConfig(args);
	
        // keep server running forever to 
	// - allow time for locator discovery and
	// - keep re-registering the lease
	Object keepAlive = new Object();
	synchronized(keepAlive) {
	    try {
		keepAlive.wait();
	    } catch(java.lang.InterruptedException e) {
		// do nothing
	    }
	}
    }

    public FileClassifierServerCodebaseConfig(String[] args) {
	// Create the service
	impl = new complete.FileClassifierImpl();

        if (args.length == 0) {
            System.err.println("No configuration specified");
            System.exit(1);
        }
        String[] configArgs = new String[] {args[0]};

        Configuration config = null;
	String codebase = null;
	try {
	    config = ConfigurationProvider.getInstance(configArgs); 
	    codebase = (String) config.getEntry("ServiceCodebaseDemo",
						"codebase",
						String.class);
	} catch(ConfigurationException e) {
	    System.err.println("Configuration error: " + e.toString());
	    System.exit(1);
	}
	System.setProperty("java.rmi.manager.codebase", codebase);

        System.setSecurityManager(new RMISecurityManager());

	LookupDiscovery discover = null;
        try {
            discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
        } catch(Exception e) {
            System.err.println("Discovery failed " + e.toString());
            System.exit(1);
        }

        discover.addDiscoveryListener(this);
    }
    
    public void discovered(DiscoveryEvent evt) {

        ServiceRegistrar[] registrars = evt.getRegistrars();

        for (int n = 0; n < registrars.length; n++) {
            ServiceRegistrar registrar = registrars[n];

	    ServiceItem item = new ServiceItem(null,
					       impl, 
					       null);
	    ServiceRegistration reg = null;
	    try {
		reg = registrar.register(item, Lease.FOREVER);
	    } catch(java.rmi.RemoteException e) {
		System.err.println("Register exception: " + e.toString());
		continue;
	    }
	    System.out.println("Service registered with id " + reg.getServiceID());

	    // set lease renewal in place
	    leaseManager.renewUntil(reg.getLease(), Lease.FOREVER, this);
	}
    }

    public void discarded(DiscoveryEvent evt) {

    }

    public void notify(LeaseRenewalEvent evt) {
	System.out.println("Lease expired " + evt.toString());
    }   
    
} // FileClassifierServer

19.6. Localhost

In a development environment, it is quite common to build clients and services all on the same machine. My main computer is my laptop, and I keep moving from one IP domain to another. So my IP address keeps changing, and when I upload code to another machine it changes again. In these circumstances it is quite common to use localhost for my current machine. But as soon as you distribute an application, use of localhost often breaks: my localhost is not your localhost, and distributed applications will often get confused.

Within an application, localhost can always be resolved to a "real" host name by


InetAddress localhost = InetAdress.getLocalHost();
String loclaHostName = localhost.getHostName();
However, this cannot be used in configuration files since it involves a call to an instance method, and only static method calls are allowed.

Jini 2.0 includes a class ConfigUtils which wraps this particular instance method by a static method ConfigUtils.getHostName(). It also includes a static method to concatenate strings (which would otherwise be done by "+" on instance objects). I would expect the methods in this class to grow as more uses are made of Jini configuration - this class is in the com.sun.jini.config package, so it is not a finalised part of Jini.

A configuration to set the codebase to localhost might contain


codebase = ConfigUtil.concat(new String[] {
                                    "http://",
                                    ConfigUtil.getHostName(),
                                    ":80/classes"
                                 }
                             );

19.7. A generic server

The configuration machanism can be used to place all runtime information in a configuration file. This can even include the service - all that the server needs to know is that the service implements the Remote interface. For example, information about the service could be given in a file such as config/generic.config


import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.tcp.TcpServerEndpoint;
import com.sun.jini.config.ConfigUtil;
import net.jini.core.discovery.LookupLocator;
import net.jini.core.entry.Entry;
import net.jini.lookup.entry.*;
import java.io.File;

GenericServer {

    // If the HTTP server for classes is running on the
    // local machine, use this for the codebase 
    localhost = ConfigUtil.getHostName();
    port = "80";
    directory = "/classes";

    // codebase = http://"localhost":80/classes
    codebase =   ConfigUtil.concat(new String[] {
                                           "http://",
                                           localhost,
                                           ":",
                                           port,
                                           directory
                                       }
                                  );   

    exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
                                     new BasicILFactory()); 

    /* Groups to join
     * Could be e.g.
     *     groups  = new String[] {"admin", "sales"};
     */
    groups = LookupDiscovery.ALL_GROUPS;

    /* Unicast lookup services
     */
    unicastLocators = new LookupLocator[] { // empty
                                          };	
    /* Entries
     */ 
     entries = new Entry[] {new Name("Jan Newmarch"),
 	                    new Comment("Author of Jini book")
	                   };

    /* Service ID file
     */
    serviceIdFile = new File("serviceId.id");

    /* The service
     */
    service = new rmi.FileClassifierImpl();

}

A server using such a configuration file could be


package config;

import net.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import net.jini.core.discovery.LookupLocator;
import net.jini.core.entry.Entry;
import net.jini.lookup.ServiceIDListener;
import net.jini.lease.LeaseRenewalManager;
import net.jini.discovery.LookupDiscoveryManager;
import java.rmi.RMISecurityManager;
import java.rmi.Remote;
import net.jini.export.Exporter;
import net.jini.core.lookup.ServiceID;

import java.io.*;

import net.jini.config.*; 

/**
 * GenericServer.java
 */

public class GenericServer 
    implements ServiceIDListener {

    private static final String SERVER = "GenericServer";

    private Remote proxy;
    private Remote impl;
    private Exporter exporter;
    private String[] groups;
    private Entry[] entries;
    private LookupLocator[] unicastLocators;
    private File serviceIdFile;
    private String codebase;
    private ServiceID serviceID;

    public static void main(String args[]) {
	new GenericServer(args);

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

    public GenericServer(String[] args) {

        if (args.length == 0) {
            System.err.println("No configuration specified");
            System.exit(1);
        }
        String[] configArgs = new String[] {args[0]};

	getConfiguration(configArgs);

	// set codebase
	System.setProperty("java.rmi.manager.codebase", codebase);

	// export a service object
	try {
	    proxy = exporter.export(impl);
	} catch(java.rmi.server.ExportException e) {
	    e.printStackTrace();
	    System.exit(1);
	}

	// install suitable security manager
	System.setSecurityManager(new RMISecurityManager());
	
	tryRetrieveServiceId();

	JoinManager joinMgr = null;
	try {
	    LookupDiscoveryManager mgr = 
		new LookupDiscoveryManager(groups,
					   unicastLocators,  // unicast locators
					   null); // DiscoveryListener
	    if (serviceID != null) {
		joinMgr = new JoinManager(proxy, // service proxy
					  entries,  // attr sets
					  serviceID,  // ServiceID
					  mgr,   // DiscoveryManager
					  new LeaseRenewalManager());
	    } else {
		joinMgr = new JoinManager(proxy, // service proxy
					  entries,  // attr sets
					  this,  // ServiceIDListener
					  mgr,   // DiscoveryManager
					  new LeaseRenewalManager());
	    }
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
    }

    public void tryRetrieveServiceId() {
	// Try to load the service ID from file.
	// It isn't an error if we can't load it, because
	// maybe this is the first time this service has run
	DataInputStream din = null;
	try {
	    din = new DataInputStream(new FileInputStream(serviceIdFile));
	    serviceID = new ServiceID(din);
	    System.out.println("Found service ID in file " + serviceIdFile);
	    din.close();
	} catch(Exception e) {
	    // ignore
	}
    }

    public void serviceIDNotify(ServiceID serviceID) {
	// called as a ServiceIDListener
	// Should save the id to permanent storage
	System.out.println("got service ID " + serviceID.toString());
	
	// try to save the service ID in a file
	if (serviceIdFile != null) {
	    DataOutputStream dout = null;
	    try {
		dout = new DataOutputStream(new FileOutputStream(serviceIdFile));
		serviceID.writeBytes(dout);
		dout.flush();
	    dout.close();
	    System.out.println("Service id saved in " +  serviceIdFile);
	    } catch(Exception e) {
		// ignore
	    } 
	}
    }

    private void getConfiguration(String[] configArgs) {
	Configuration config = null;

	// We have to get a configuration file or
	// we can't continue
	try {
	    config = ConfigurationProvider.getInstance(configArgs); 
	} catch(ConfigurationException e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}
	    
	// The config file must have an exporter, a service and a codebase
	try {
	    exporter = (Exporter) config.getEntry(SERVER, 
						  "exporter", 
						  Exporter.class); 
	    impl = (Remote) config.getEntry(SERVER, 
					    "service", 
					    Remote.class); 

	    codebase = (String) config.getEntry(SERVER,
						"codebase",
						String.class);
	} catch(NoSuchEntryException  e) {
	    System.err.println("No config entry for " + e);
	    System.exit(1);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(2);
	}

	// These fields can fallback to a default value 
	try {
	    unicastLocators = (LookupLocator[]) 
		config.getEntry("GenericServer", 
				"unicastLocators", 
				LookupLocator[].class,
				null); // default
	    
	    entries = (Entry[]) 
		config.getEntry("GenericServer", 
				"entries", 
				Entry[].class,
				null); // default
	    serviceIdFile = (File) 
		config.getEntry("GenericServer", 
				"serviceIdFile", 
				File.class,
				null); // default 
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(2);
	}

    }
    
} // GenericServer

19.8. Copyright

If you found this chapter of value, the full book "Foundations of Jini 2 Programming" is available from APress or Amazon .

This file is Copyright (©) 1999, 2000, 2001, 2003, 2004, 2005 by Jan Newmarch (http://jan.newmarch.name) jan@newmarch.name.

Creative Commons License This work is licensed under a Creative Commons License, the replacement for the earlier Open Content License.