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 /
|
Contents
This chapter looks at a simple problem, implementing it in a number of different ways
Applications often need to work out the type of a file, to see if it is a text file, an HTML document, an executable, etc. This can be done in two ways:
file
command use the
second method, and have a complex description file (such
as /etc/magic
or /usr/share/magic
)
to aid in this. Many other applications such as Web browsers,
mail readers (and even some operating systems!) use the first
method and work out a file's type based on its name.
A common file classification is into MIME types such as
text/plain
and image/gif
. There
are tables of ``official'' MIME types (unofficial ones can
be added on an adhoc basis), and there
are also tables of mappings from filename endings to corresponding
MIME types. These tables have entries such as
application/postscript ai eps ps
application/rtf rtf
application/zip zip
image/gif gif
image/jpeg jpeg jpg jpe
text/html html htm
text/plain txt
and are stored in files for applications to access.
This storage of tables separate from the applications that would use them is rated as bad from the O/O point of view, since each application would need to have code to interpret the tables. The multiplicity of these tables and the ability of users to modify them makes this a maintenance problem. It would be better to encapsulate at least the filename to MIME type mapping table in an object. We define a MIME class
package standalone;
/**
* MIMEType.java
*/
public class MIMEType {
/**
* A MIME type is made up of 2 parts
* contentType/subtype
*/
protected String contentType;
protected String subtype;
public MIMEType(String type) {
int slash = type.indexOf('/');
contentType = type.substring(0, slash-1);
subtype = type.substring(slash+1, type.length());
}
public MIMEType(String contentType, String subtype) {
this.contentType = contentType;
this.subtype = subtype;
}
public String toString() {
return contentType + "/" + subtype;
}
} // MIMEType
and a mapping class
package standalone;
/**
* FileClassifier.java
*/
public class FileClassifier {
static MIMEType getMIMEType(String fileName) {
if (fileName.endsWith(".gif")) {
return new MIMEType("image", "gif");
} else if (fileName.endsWith(".jpeg")) {
return new MIMEType("image", "jpeg");
} else if (fileName.endsWith(".mpg")) {
return new MIMEType("video", "mpeg");
} else if (fileName.endsWith(".txt")) {
return new MIMEType("text", "plain");
} else if (fileName.endsWith(".html")) {
return new MIMEType("text", "html");
} else
// fill in lots of other types,
// but eventually give up and
return null;
}
} // FileClassifier
This mapping class has no constructors, as it justs acts as a
lookup table via its static method getMIMEType()
.
Applications may make use these classes as they stand, by simply
compiling with them and having the class files available at
runtime. This will still result in duplication throughout JVMs,
possible multiple copies of the class files, and potentially
severe maintenance problems if applications need to be re-compiled.
It may be better to have the FileClassifier
as a
network service. What will be involved in this?
If we wish to make a version of FileClassifier
available
across the network, there are a number of possibilities. The client
will be asking for an instance of a class, and generally will not care
too much about the details of this instance. For example, it will
want an instance of a DiskDrive
or a Calendar
.
Usually it will not care which drive it gets, or which calendar. If it
requires further specification it can either ask for a subclass instance
(such as a SeagateDiskDrive
, or use an Entry
object
for this additional information.
Services will have particular implementations, and will upload these to the service locators. The uploaded service will be of a quite specific class, and may have associated entries.
There are several options that the client could use in trying to locate a suitable service.
interface
,
whereas the server deals in terms of a Java class that implement
's
the interface
.
The ideal mechanism in the Jini world is to specify services by interfaces, and have all clients know this interface. Then each service can be an implementation of this interface. This is simple in Java terms, simple in specification terms, and simple for maintenance. This is not the complete set of choices for the service, but is enough to allow a service to be specified and get on with building the client. One possibility for service implementation is looked at later in this chapter, and the next chapter is devoted to the full range of possibilities.
The client and any implementations of a service must share some common classes. For a file classification service the common classes are the classifier itself (which can be implemented as many different services) and the return value, the MIME type. These have to change very slightly from their standalone form.
The class MIMEType
is known to the client and to any file classifier
service. The class files can be expected to be known to the JVMs of all clients
and services. The method getMIMEType()
will return an object from
the service. There are implementation possibilities that can affect this object:
MIMEType
object
must be serialized for transport to the client JVM. For this to be possible,
it must implement the Serializable
interface
Serializable
then it can be used in both the
remote and local cases, but if it doesn't then it can only be used in the
local case.
Making decisions about interfaces based on future implementation concerns is
traditionally rated as poor design. However, a document from Sun
``A Note on Distributed Computing'' by Jim Waldo et al
argues that in the case of distributed objects it is important to determine
if the objects may be running remotely and adjust interfaces and classes
accordingly
This leads to an interface which adds the Serializable
interface
to the previous version.
package common;
import java.io.Serializable;
/**
* MIMEType.java
*/
public class MIMEType implements Serializable {
/**
* A MIME type is made up of 2 parts
* contentType/subtype
*/
protected String contentType;
protected String subtype;
public MIMEType(String type) {
int slash = type.indexOf('/');
contentType = type.substring(0, slash-1);
subtype = type.substring(slash+1, type.length());
}
public MIMEType(String contentType, String subtype) {
this.contentType = contentType;
this.subtype = subtype;
}
public String toString() {
return contentType + "/" + subtype;
}
} // MIMEType
Changes have to be made to the file classifier interface as well.
Firstly, interfaces cannot have
static
methods, so we shall have to turn the method
getMIMEType()
into
a public instance method. In addition, instances implementing this may need to be
shipped around as proxy objects, so it should extend Serializable
.
This is the same situation as with MIMEType
.
In addition, all methods are defined to throw a java.rmi.RemoteException
.
This type of exception is used by Java (not just the RMI component) to mean
``a network error has occurred''. This could be a lost connection, a missing
server, a class not downloadable, etc.
There is a little subtlety here, related to the java.rmi.Remote
class: the methods of Remote
must all throw a
RemoteException
, but the converse is not true. If all the methods
throw RemoteException
, it does not mean the class implements/extends
Remote
. It only means that an implementation may
be implemented as a remote (distributed) object, and this implementation might also
use the RMI Remote
interface.
This gives the following interface
package common;
import java.io.Serializable;
/**
* FileClassifier.java
*/
public interface FileClassifier extends Serializable {
public MIMEType getMIMEType(String fileName)
throws java.rmi.RemoteException;
} // FileClasssifier
Why does this interface throw a java.rmi.RemoteException
in the
getMIMEType()
method? Well, an interface is supposed to be above
all possible implementations, and should never change.
The implementation
discussed later in this chapter does not throw such an exception. However, other
implementations in other sections use a Remote
implementation,
and this will require that the method throws an java.rmi.RemoteException
.
Since it is not possible to just add a new exception in a subclass or interface
implementation, the possibility must be added in the interface specification.
The client is the same for all of the possible server implementations
discussed in throughout this book. The client does not
care how the server-side implementation is done, just as long as it
gets a service that it wants, and it specifies this by asking
for a FileClassifier
interface.
If there is a known service locator which will know about the service,
then there is no need to search. This doesn't mean that the location of
the service is known, only of the locator. For example, there may be
a (fictitious) organisation ``All About Files'' at
www.all_about_files.com
that
would know about various file services, keeping track of them as they come
on line, move, disappear, etc. A client would ask
the service locator running on this site for the service, wherever it is.
This uses the unicast lookup techniques.
package client;
import common.FileClassifier;
import common.MIMEType;
import net.jini.core.discovery.LookupLocator;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistration;
import java.rmi.RMISecurityManager;
import net.jini.core.lookup.ServiceTemplate;
/**
* TestUnicastFileClassifier.java
*/
public class TestUnicastFileClassifier {
public static void main(String argv[]) {
new TestUnicastFileClassifier();
}
public TestUnicastFileClassifier() {
LookupLocator lookup = null;
ServiceRegistrar registrar = null;
FileClassifier classifier = null;
try {
lookup = new LookupLocator("jini://www.all_about_files.com");
} catch(java.net.MalformedURLException e) {
System.err.println("Lookup failed: " + e.toString());
System.exit(1);
}
System.setSecurityManager(new RMISecurityManager());
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);
}
Class[] classes = new Class[] {FileClassifier.class};
ServiceTemplate template = new ServiceTemplate(null, classes, null);
try {
classifier = (FileClassifier) registrar.lookup(template);
} catch(java.rmi.RemoteException e) {
e.printStackTrace();
System.exit(1);
}
if (classifier == null) {
System.out.println("Classifier null");
System.exit(2);
}
MIMEType type;
try {
type = classifier.getMIMEType("file1.txt");
System.out.println("Type is " + type.toString());
} catch(java.rmi.RemoteException e) {
System.err.println(e.toString());
}
System.exit(0);
}
} // TestUnicastFileClassifier
The client's JVM looks like
It has objects that implement the interfaces ServiceRegistrar
and FileClassifier
, but it doesn't know - or need to know -
what classes they are.
When the service locator's JVM is added in, it looks like
This shows that the client gets its registrar
from the
JVM of the service locator. This object is not specified in detail.
Sun supply a service locator known as reggie
.
This locator implements the ServiceRegistrar
using an
implementation that neither clients nor services are expected to know.
The classes which implement this object
are contained in the file reggie-dl.jar
and are downloaded
to the clients and services using (typically) an HTTP server.
More likely, client will need to search through all
of the service locators till it finds one holding a service it
is looking for. It would need to use a multicast search for this.
If it only needs one occurrence of the service, then it can do something like
exit after using the service. More complex behaviour will be illustrated
in later examples.
The client does not need to have long-term persistence.
But it does need a user thread to remain in existence for long
enough to find service locators and find a suitable service.
So in main()
a user thread again sleeps for a short
period (ten seconds).
package client;
import common.FileClassifier;
import common.MIMEType;
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.ServiceTemplate;
/**
* TestFileClassifier.java
*/
public class TestFileClassifier implements DiscoveryListener {
public static void main(String argv[]) {
new TestFileClassifier();
// stay around long enough to receive replies
try {
Thread.currentThread().sleep(100000L);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
public TestFileClassifier() {
System.setSecurityManager(new RMISecurityManager());
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
}
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
Class [] classes = new Class[] {FileClassifier.class};
FileClassifier classifier = null;
ServiceTemplate template = new ServiceTemplate(null, classes,
null);
for (int n = 0; n < registrars.length; n++) {
System.out.println("Service found");
ServiceRegistrar registrar = registrars[n];
try {
classifier = (FileClassifier) registrar.lookup(template);
} catch(java.rmi.RemoteException e) {
e.printStackTrace();
continue;
}
if (classifier == null) {
System.out.println("Classifier null");
continue;
}
MIMEType type;
try {
String fileName;
fileName = "file1.txt";
type = classifier.getMIMEType(fileName);
printType(fileName, type);
fileName = "file2.rtf";
type = classifier.getMIMEType(fileName);
printType(fileName, type);
fileName = "file3.abc";
type = classifier.getMIMEType(fileName);
printType(fileName, type);
} catch(java.rmi.RemoteException e) {
System.err.println(e.toString());
continue;
}
// success
System.exit(0);
}
}
private void printType(String fileName, MIMEType type) {
System.out.print("Type of " + fileName + " is ");
if (type == null) {
System.out.println("null");
} else {
System.out.println(type.toString());
}
}
public void discarded(DiscoveryEvent evt) {
// empty
}
} // TestFileClassifier
A Jini program can generate a huge number of exceptions, often related to the network nature of Jini. Many programs adopt a somewhat cavalier attitude to exceptions: catch them, maybe put out an error message and continue - Java makes it easy to handle errors! More seriously, whenever an exception occurs the question has to be asked as to whether the program can continue, or has its state has been corrupted but not so badly that it cannot recover, or whether the program state has been damaged so much that the program must exit.
The multicast TestFileClassifier
of the last section can throw
exceptions at a number of places.
LookupDiscovery
constructor can fail.
This is indicative of some serious network error.
The created discover
object is needed to add a listener,
and if this cannot be done, then the program really can't do anything.
So it is appropriate to exit with an error value
ServiceRegistrar.lookup()
can fail.
This is indicative of some network error in the connection with a
particular service locator. While this may have failed, it is possible
that other network connections may succeed. The application can restore
a consistent state by skipping the rest of the code in this iteration
of the for()
loop by a continue
statement
FileClassifier.getMIMEType()
can fail. This can be caused
by a network error, or perhaps the service has simply gone away.
Whatever, consistent state can again be restored by skipping the rest
of this loop iteration
Finally, if one part of a program can exit with an abnormal (non-zero) error value, then a successful exit should signal its success with an exit value of zero. If this is not done, then the exit value becomes indeterminate, and of no value to other processes which may wish to establish if the program exited successfully or not.
A service will be delivered from out of a server. That is, a server will be started, create one or more service objects, register these with service locators, and then wait for network requests to come in for the service. What the server will actually export is usually a proxy for the service. The proxy is an object that will eventually run in a client, and will make calls back across the network to the service object within the server.
The service and its proxy are tightly integrated: they must communicate using a protocol known to them both, and must exchange information in an agreed manner. However, the relative size of each is up to the designer of a service and its proxy. For example, the proxy may be ``fat'' (or ``smart''), which means it does a lot of processing on the client side. The service itself is then typically ``thin'', not doing much at all. Alternatively, the proxy may be thin, doing little more (or nothing more) than passing requests between the client and the fat service.
As well as this choice of size, there is also a choice of communication mechanisms between the two. Client-server systems often have the choice of message-based or remote procedure call. These choices are also available between a Jini proxy and its service. Since they are both in Java, there is a standard RPC-like mechanism called RMI (Remote Method Invocation), and this can be used if wanted. There is no necessity to use this, but many implementations of Jini proxies will do so since it is easy. It does force a particular choice of thin proxy to a fat client, though, and this may not be ideal for all situations.
This chapter will look at one possibility only, where the proxy is fat and is the whole of the service. The next chapter will look in more detail at the other possibilities.
The file classifier service does not rely on any particular properties of its host - it is not hardware or operating system dependant, and does not make use of any files on the host side. In this case it is possible to upload the entire service to the client and let it run there. The proxy is the service, and no processing elements need to be left on the server.
The implementation of this is straightforward
package complete;
import common.MIMEType;
import common.FileClassifier;
/**
* FileClassifierImpl.java
*/
public class FileClassifierImpl implements FileClassifier {
public MIMEType getMIMEType(String fileName) {
if (fileName.endsWith(".gif")) {
return new MIMEType("image", "gif");
} else if (fileName.endsWith(".jpeg")) {
return new MIMEType("image", "jpeg");
} else if (fileName.endsWith(".mpg")) {
return new MIMEType("video", "mpeg");
} else if (fileName.endsWith(".txt")) {
return new MIMEType("text", "plain");
} else if (fileName.endsWith(".html")) {
return new MIMEType("text", "html");
} else
// fill in lots of other types,
// but eventually give up and
return null;
}
public FileClassifierImpl() {
// empty
}
} // FileClassifierImpl
The server for this needs to create an instance of the exportable
service, register this and keep the lease alive.
In the discovered()
method
it not only registers the service but also adds it to a
LeaseRenewalManager
, to keep the lease alive ``forever''.
This manager runs its own threads to keep re-registering the leases,
but these are daemon threads. So in the main()
method
the user thread goes to sleep for as long as we want the server
to stay around. Note that if the server does terminate, then the lease
will fail to be renewed and the exported service will be discarded
from lookup locators even though the server is not required for delivery
of the service.
The serviceID
is initially set to null
and
then extracted from the first lookup service that replies and is re-used.
Ideally it should also be stored (say on disk) and retrieved the next time
the service starts. Note that we get the service id from the
registration not the registrar.
package complete;
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 com.sun.jini.lease.LeaseRenewalManager; // Jini 1.0
// import com.sun.jini.lease.LeaseListener; // Jini 1.0
// import com.sun.jini.lease.LeaseRenewalEvent; // Jini 1.0
import net.jini.lease.LeaseListener; // Jini 1.1
import net.jini.lease.LeaseRenewalEvent; // Jini 1.1
import net.jini.lease.LeaseRenewalManager; // Jini 1.1
/**
* FileClassifierServer.java
*/
public class FileClassifierServer implements DiscoveryListener,
LeaseListener {
protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
protected ServiceID serviceID = null;
public static void main(String argv[]) {
new FileClassifierServer();
// 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 FileClassifierServer() {
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,
new FileClassifierImpl(),
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");
System.out.println("Class " + registrar.getClass().toString());
Object [] signers = registrar.getClass().getSigners();
if (signers == null) {
System.out.println("No signers");
} else {
System.out.println("Signers");
for (int m = 0; m < signers.length; m++)
System.out.println(signers[m].toString());
}
java.security.ProtectionDomain domain = registrar.
getClass().getProtectionDomain();
java.security.CodeSource codeSource = domain.getCodeSource();
System.out.println("CodeSource " + codeSource.toString());
RuntimePermission perm = new RuntimePermission("getProtectionDomain");
if (domain.implies(perm))
System.out.println("Permission allowed");
else
System.out.println("Permission not allowed");
// should be ok - we aren't in "domain", we have given permission
System.getSecurityManager().checkPermission(perm);
// set lease renewal in place
leaseManager.renewUntil(reg.getLease(), Lease.FOREVER, this);
// set the serviceID if necessary
if (serviceID == null) {
serviceID = reg.getServiceID();
}
}
}
public void discarded(DiscoveryEvent evt) {
}
public void notify(LeaseRenewalEvent evt) {
System.out.println("Lease expired " + evt.toString());
}
} // FileClassifierServer
The server by itself running in its JVM looks like
This receives an object implementing ServiceRegistrar
from the service locator (such as reggie
).
If we add in the service locator and the client in their JVM's, it becomes
The unknown FileClassifier
object in the client is here supplied by the
service object FileClassifierImpl
(via the
lookup service, where it is stored in passive form).
The client for this service was discussed earlier. The client does not need any special information about this implementation of the service and so can remain quite generic.
We have the classes
common.MIMEType
common.FileClassifier
complete.FileClassifierImpl
complete.FileClassifierServer
client.TestFileClassifier
FileClassifier
TestFileClassifier
FileClassifierImpl
to clients
The server running FileClassifierServer
needs to know the
following classes and interfaces
common.FileClassifier
interface
common.MIMEType
complete.FileClassifierServer
complete.FileClassifierImpl
CLASSPATH
of the server.
The class complete.FileClassifierImpl
will need to be accessible to an HTTP server, as discussed in the next
section.
The lookup service does not need to know any
of these classes. It just deals with them in the form of a
java.rmi.MarshalledObject
The client needs to know
common.FileClassifier
interface
common.MIMEType
client.TestFileClassifier
We now have a service FileClassifierServer
and a client
TestFleClassifier
to run. There should also be at least
one lookup locator already running.
The CLASSPATH
should be set for each to include
the classes discussed in the last section, in addition to the
standard ones.
A serialized instance of complete.FileClassifierImpl
will be passed from the server to the locator and then to the client.
Once on the client, it will need to be able to run the class file
for this object, so will need to load its class file from an HTTP
server. The location of this class file relative to the server's
DocumentRoot
will need to be specified by the service
invocation. For example, if it is stored in
/DocumentRoot/classes/complete/FileClassifierImpl.class
then the service will be started by:
java -Djava.rmi.codebase=http://hostname/classes \
complete.FileClassifierServer
In this, hostname
is the name of the host the
server is running on. Note that this host name cannot
be localhost
, because the local host for the server
will not be the local host for the client!
The client will be loading a class definition across the network. It will need to allow this in a security policy file by
java -Djava.security.policy=policy.all client.TestFileClassifier
The material of the previous chapters is put together in a simple example. The requirements of class structures for a Jini system are discussed. A discussion is also given of what classes need to be available to each component of a Jini system.
Copyright 1998, 1999, 2000 David Reilly
|
Privacy | Legal | Linking | Advertise! |
Last updated:
Monday, June 05, 2006
|