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
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:
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.
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);
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.