Chapter 17: 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

17.1. Using Localhost

It is common to do initial development on a single computer. In that case it is tempting to use the machine name localhost. I use my laptop as major development machine and I run it in a number of different environments (home, various subnets at work) with different IP addresses and names. Rather than changing addresses in source files and recompiling, it is often much easier to use localhost. Students at my university use a shared file system, but are always using different computers in the laboratories, and so they have the same problem.

However, as soon as you move to a multi-computer system, using localhost causes problems:

  1. The lookup service may be running on a different machine to any client or service
  2. The client and any service may be running on different machines
  3. An HTTP server to deliver files may be on yet another machine
This can cause problems when an object has been sent across the network to run on a different computer. localhost at the destination is not the same as localhost at the origin, and services can break when they look on the wrong computer for resources such as class files.

The use of localhost is strongly discouraged in Jini. If you need to use "the same computer as this one", then instead of using localhost you should use a utility that returns the IP address of the current host. One of these is supplied by the InetAddress class in package java.net:


String InetAddress.getLocalHost() throws UnknownHostException

If you need to find the IP address of the current host, and concatenate that with some other strings then the com.sun.jini.config.ConfigUtil class may be useful


class ConfigUtil {
    static String concat(Object[] objects);
    static String getHostName();
} 
A version of the unicast lookup (from Chapter 3) that tries to find a lookup service on the local machine is


package basic;

import net.jini.core.discovery.LookupLocator;
import net.jini.core.lookup.ServiceRegistrar;
import java.rmi.RMISecurityManager;
import com.sun.jini.config.ConfigUtil;
import java.net.UnknownHostException;

/**
 * UnicastLocalRegistrar.java
 */

public class UnicastLocalRegister  {
    
    static public void main(String argv[]) {
        new UnicastLocalRegister();
    }
   
    public UnicastLocalRegister() {
	LookupLocator lookup = null;
	ServiceRegistrar registrar = null;

	System.setSecurityManager(new RMISecurityManager());

	String jiniUrl = null;
	try {
	    // get the url for "jini://local-host-name"
	    jiniUrl = ConfigUtil.concat(new Object[] {"jini://",
					              ConfigUtil.getHostName()
					             }
					);
	    // or
	    // jiniUrl = "jini://" + InetAddress.getLocalHost();
	} catch(UnknownHostException e) {
	    System.err.println("Can't get local host name");
	    System.exit(1);
	}

        try {
            lookup = new LookupLocator(jiniUrl);
        } catch(java.net.MalformedURLException e) {
            System.err.println("Lookup failed: " + e.toString());
	    System.exit(1);
        }

	try {
	    registrar = lookup.getRegistrar();
	} catch (java.io.IOException e) {
            System.err.println("Registrar search failed: " + e.toString());
	    System.exit(1);
	} catch (java.lang.ClassNotFoundException e) {
            System.err.println("Registrar search failed: " + e.toString());
	    System.exit(1);
	}
	System.out.println("Registrar found");

	// the code takes separate routes from here for client or service
    }
   
} // UnicastRegister

This shows that there is no need to use the literal string "localhost" anywhere.

17.2. Configuration

In the chapter on Jeri, configuration was used to select an implementation of RMI at runtime based on a configuration file. While a configuration may be found in many ways, the default in Jini is to get it from a ConfigurationFile.

A configuration file contains instructions in a language based on Java but considerably simplified. A simple example is


import net.jini.jrmp.*; 

ConfigDemo {
    exporter = new JrmpExporter(); 
}

The simplified language has no conditional statements or loops, but allows you to declare and construct objects

A configuration is usually found from a ConfigurationProvider such as


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

        // get the configuration (by default a FileConfiguration) 
        Configuration config = ConfigurationProvider.getInstance(configArgs); 
and then used by

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

This mechanism can be used for other configurable properties though. 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. One way is to create an empty array and ask for its class

    Class cls = (new Entry[] {}).getClass();
An alternative is to use the syntax for arrays from Java reflection

    Class cls = Class.forName("[Lnet.jini.core.entry.Entry;");
Whichever mechanism is used to specify the class, the retrieval is the same

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

17.3. 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.

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! The Jini configuration mechanism can be used as part of such a rewrite mechanism, but does not supply all the code.

What follows is "fragile code": if you write your code the same way as I have, then this should work. I would appreciate more robust versions!

The ServiceID class has a toString() method that prints the ID as an ASCII string. Unfortunately it has no constructor which takes this string. If we are going to save the ID in text files then we cannot use the binary format alone. So we first need a convenience class to construct a ServiceID from a string



/**
 * ServiceIDFactory.java
 */

package util;

import net.jini.core.lookup.ServiceID;
import java.util.StringTokenizer;

public class ServiceIDFactory {
    private ServiceIDFactory (){
    }

    public static ServiceID getServiceID(String id) {
	if (id == null)
	    return null;

	StringTokenizer st = new StringTokenizer(id, "-");
	try {
	    long low = Long.parseLong(st.nextToken(), 16);
	    long mid = Long.parseLong(st.nextToken(), 16);
	    long version = Long.parseLong(st.nextToken(), 16);
	    long variant = Long.parseLong(st.nextToken(), 16);
	    long node = Long.parseLong(st.nextToken(), 16);
	    
	    long hi = (low << 32) | (mid << 16) | version;
	    long lo = (variant << 48) | node;
	    return new ServiceID(hi, lo);
	} catch(Exception e) {
	    return null;
	}
    }
}// ServiceIDFactory

With this convenience class in place we can write configuration classes that have a null or actual service ID in place:


import util.ServiceIDFactory;

ConfigDemo {
    serviceID = ServiceIDFactory.getServiceID(null);
}

and

import util.ServiceIDFactory;

ConfigDemo {
    serviceID = ServiceIDFactory.getServiceID("07d78fb8-b8ae-4575-b5bd-66729c98b40b");
}

This is okay so far. The fragile part is what the following program tries to do: it takes a configuration file and looks for a service ID. If it finds one, it supplies the ID to the JoinManager. If it doesn't then, it installs a ServiceIDListener. When called, it attempts to rewrite the configuration file by replacing the (ServiceIDFactory.getServiceID(null) pattern with one containing the service ID. This will only work if the syntax used in the configuration file matches the pattern sought. If the pattern isn't there, no harm is done: no rewrite will take place and the service will ask for a new ID the next time it is run.


package http;

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 java.rmi.RMISecurityManager;
import java.rmi.Remote;
import java.net.URL;
import net.jini.lookup.entry.*;
import net.jini.core.entry.Entry;
import net.jini.core.discovery.LookupLocator;

import net.jini.config.*; 
import net.jini.export.*; 

import java.util.StringTokenizer;
import java.io.*;

/**
 * HttpFileSourceServer.java
 */

public class HttpFileSourceServer 
    implements ServiceIDListener {

    // explicit proxy for Jini 2.0
    protected Remote proxy;
    protected HttpSourceImpl impl;
    private static String configFile;
    private Entry[] entries;

    public static void main(String argv[]) {
	configFile = argv[0];

	new HttpFileSourceServer(argv);

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

    public HttpFileSourceServer(String[] argv) {
	URL url = null;
	Exporter exporter = null;
	ServiceID serviceID = null;

	if (argv.length != 1) {
	    System.err.println("Usage: HttpFileSourceServer config_file");
	    System.exit(1);
	}

	try {
	} catch(Exception e) {
            System.err.println("New impl: " + e.toString());
            System.exit(1);
	}

	String[] configArgs = argv;
	try {
	    // get the configuration (by default a FileConfiguration) 
	    Configuration config = ConfigurationProvider.getInstance(configArgs); 

	    // and use this to construct an exporter
	    exporter = (Exporter) config.getEntry( "HttpFileSourceServer", 
							    "exporter", 
							    Exporter.class);

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

	    serviceID = (ServiceID) config.getEntry("HttpFileSourceServer",
					"serviceID",
					ServiceID.class);

	    Class cls = Class.forName("[Lnet.jini.core.entry.Entry;");
	    System.out.println(cls.toString());
	    entries = (Entry []) config.getEntry("HttpFileSourceServer",
						 "entries",
						 cls);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

	impl = new HttpSourceImpl(url);

	try {
	    // export an object of this class
	    proxy = exporter.export(impl);
	} catch(java.rmi.server.ExportException e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}

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

	JoinManager joinMgr = null;
	try {
	    LookupDiscoveryManager mgr = 
		new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					   new LookupLocator[] {
					       new LookupLocator("jini://jannote.jan.home/")},
					   // 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 serviceIDNotify(ServiceID serviceID) {
	// called as a ServiceIDListener
	System.out.println("got service ID " + serviceID.toString());
	
	// Should save the id to permanent storage
	saveID(configFile, serviceID.toString());
    }

    private void saveID(String configFile, String serviceID) {
	try {
	    File cFile = new File(configFile);
	    File tmpFile = File.createTempFile("config", null, new File("."));
	    File backFile = new File(configFile + ".bak");

	    BufferedReader in = new BufferedReader(
				    new InputStreamReader(
					new FileInputStream(cFile)));
	    PrintStream out = new PrintStream(new FileOutputStream(tmpFile));

	    String line;
	    while ((line = in.readLine()) != null) {
		System.out.println("Line " + line);
		String[] split;
		split = line.split("ServiceIDFactory.getServiceID\\(null\\)", 2);
		if (split.length == 1) {
		    // no match
		    out.println(line);
		} else {
		    System.out.println("Matched!");
		    out.print(split[0]);
		    out.print("ServiceIDFactory.getServiceID(\"" +
			      serviceID + "\")");
		    out.println(split[1]);
		}
	    }
	    in.close();
	    out.close();
	    System.out.println("Copy made");
	    
	    cFile.renameTo(backFile);
	    tmpFile.renameTo(new File(configFile));
	    System.out.println("Files renamed");
	} catch(IOException e) {
	    // ignore
	    e.printStackTrace();
	    return;
	}
    }
    
} // HttpFileSourceServer


If you found this chapter of value, the full book is available from APress or Amazon . There is a review of the book at Java Zone . The current edition of the book does not yet deal with Jini 2.0, but the next edition will.


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

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.