This chapter looks at a simple example, 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 correspoding
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
/**
* MIMEType.java
*
*
* Created: Mon Mar 15 22:09:13 1999
*
* @author Jan Newmarch
* @version
*/
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
import MIMEType;
/**
* FileClassifier.java
*
*
* Created: Mon Mar 15 22:18:59 1999
*
* @author Jan Newmarch
* @version
*/
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 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 options
interface
,
whereas the server deals in terms of a Java class that implement
's
the interface
.
In the form given above, the FileClassifier
is basically
just a lookup table, with one static method. There is no need to
create instances of this type, and this could even have been eliminated
as a possibility by declaring the constructor as private
:
private FileClassifier() {
}
If we want to make this into a service that will be exported, we have
to make a couple of changes to this. Firstly, we will need to separate
out an interface class FileClassifier
and at least one
implementation class which we shall here give as
FileClassifierImpl
. Now interfaces cannot have
static
methods, so we shall have to turn the method into
a public instance method. In addition, a class object for this
interface, and later instances implementing this will need to be
shipped around by RMI, so it should extend Serializable
package option2;
import java.io.Serializable;
/**
* FileClassifier.java
*
*
* Created: Wed Mar 17 14:14:36 1999
*
* @author Jan Newmarch
* @version
*/
public interface FileClassifier extends Serializable {
public MIMEType getMIMEType(String fileName);
} // FileClasssifier
The implementation of this is straightforward
package option2;
/**
* FileClassifierImpl.java
*
*
* Created: Wed Mar 17 14:22:13 1999
*
* @author Jan Newmarch
* @version
*/
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 class MIMEType
does not need to be moved around, so
its definition remains unaltered
package option2;
/**
* MIMEType.java
*
*
* Created: Wed Mar 17 14:17:32 1999
*
* @author Jan Newmarch
* @version
*/
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
The server for this just needs to create an instance of the exportable
service and register this. Note that this server only sticks around
for 10 seconds (10,000 milliseconds) but it requests that the exported
service should last forever (Lease.FOREVER
).
If the lease request is granted, then there is no need for the server
to hang around longer than needed to upload the exported service.
This server tries to export the service to all the service locators
that it finds in its 10 seconds of life.
package option2;
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;
/**
* FileClassifierServer.java
*
*
* Created: Wed Mar 17 14:23:44 1999
*
* @author Jan Newmarch
* @version
*/
public class FileClassifierServer implements DiscoveryListener {
public static void main(String argv[]) {
new FileClassifierServer();
}
public FileClassifierServer() {
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
// stay around long enough to receive replies
try {
Thread.currentThread().sleep(10000L);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
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,
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());
}
System.out.println("service registered");
}
}
public void discarded(DiscoveryEvent evt) {
}
} // FileClassifierServer
The client will often need to search through all of the service locators till it finds one holding a service it is looking for. If it only needs one, then it can do something like exit after using the service. More complex behaviour will be dealt with later (when I find a good example???).
We have the classes
MIMEType
FileClassifier
FileClassifierImpl
FileClassifierServer
TestFileClassifier
FileClassifier
TestFileClassifier
The server running FileClassifierServer
needs to know the
following classes and interfaces
FileClassifier
interface
MIMEType
FileClassifierServer
FileClassifierImpl
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
FileClassifier
interface
MIMEType
We now have a service FileClassifierServer
and a client
TestFleClassifier
to run. There should also be at least
one lookup locator already running. Apart from CLASSPATH
setting again, nothing extra is needed. The service may be run by
java option2.FileClassifierServer
and the client by
java option2.TestFileClassifier
The code (and subsequent discussion) in this section are based on the
timeservice
code from Eran Davidov at
www.artima.com/jini/resources/timeservice.html
The third option was to upload a proxy to the service locator and eventually
to clients, while leaving a ``real'' file classifier running on the
original service machine, as in
The situation is more complex than this figure shows. It is easy enough to get the proxy over to the client, by just using the techniques of the last example. When the proxy runs, it must be able to run the ``real'' service back on its original server. But how does it find it? And how does it invoke it? There is no point in using Jini for this again, since we would just end up in a loop (or, more likely, a mess). Instead, we need to use a different mechanism for the proxy to communicate to its service. Any suitable protocol could be used, but Java supplies a ready-made one, in RMI. So in general, the proxy will access and invoke the ``real'' service using RMI calls.
There will already be an RMI daemon running on the service machine
since it has to receive Registrar
's back from the
lookup locator. A FileClassifierImpl
can register
itself with this. In order to be accessed from there, it must
implement the RMI Remote
interface. In addition
the FileClassifierProxy
must be aware of this service
in the RMI registry so that it can invoke it. This must all be
invisible to the client, which just wants to know about
FileClassifier
. For cleanness, since there may be
many implementations of the service, and they all have to
implement Remote
, make an intermediate interface
RemoteFileClassifier
. This gives a class diagram
of
The FileClassifier
interface remains unchanged as
package option3;
import java.io.Serializable;
/**
* FileClassifier.java
*
*
* Created: Wed Mar 17 14:14:36 1999
*
* @author Jan Newmarch
* @version
*/
public interface FileClassifier extends Serializable {
public MIMEType getMIMEType(String fileName)
throws java.rmi.RemoteException;
} // FileClasssifier
The interface RemoteFileClassifier
just adds the
Remote
interface
package option3;
import java.rmi.Remote;
/**
* RemoteFileClassifier.java
*
*
* Created: Wed Mar 17 14:14:36 1999
*
* @author Jan Newmarch
* @version
*/
public interface RemoteFileClassifier extends FileClassifier, Remote {
} // RemoteFileClasssifier
The class FileClassifierImpl
just changes its
inheritance:
package option3;
import java.rmi.server.UnicastRemoteObject;
/**
* FileClassifierImpl.java
*
*
* Created: Wed Mar 17 14:22:13 1999
*
* @author Jan Newmarch
* @version
*/
public class FileClassifierImpl extends UnicastRemoteObject implements RemoteFileClassifier {
public option3.MIMEType getMIMEType(String fileName)
throws java.rmi.RemoteException {
System.out.println("Called with " + 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 new MIMEType(null, null);
}
public FileClassifierImpl() throws java.rmi.RemoteException {
// empty
}
} // FileClassifierImpl
The FileClassifierProxy
is a little more complex and
will be discussed soon.
The class MIMEType
remains unchanged as
package option3;
import java.io.Serializable;
/**
* MIMEType.java
*
*
* Created: Wed Mar 17 14:17:32 1999
*
* @author Jan Newmarch
* @version
*/
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
The server has a more complex task. It has to perform the following steps
FileClassifierImpl
FileClassifierImpl
FileClassifierImpl
with knowledge of
the registration name of the FileClassifierImpl
FileClassifierImpl
object to the
location service
package option3;
import java.rmi.Naming;
import java.net.InetAddress;
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 java.rmi.RMISecurityManager;
/**
* FileClassifierServer.java
*
*
* Created: Wed Mar 17 14:23:44 1999
*
* @author Jan Newmarch
* @version
*/
public class FileClassifierServer implements DiscoveryListener {
// this is just a name - can be anything
// impl object forces search for Stub
static final String serviceName = "FileClassifier";
protected FileClassifierImpl impl;
protected FileClassifierProxy proxy;
public static void main(String argv[]) {
new FileClassifierServer();
}
public FileClassifierServer() {
try {
impl = new FileClassifierImpl();
} catch(Exception e) {
System.err.println("New impl: " + e.toString());
System.exit(1);
}
// register this with RMI registry
System.setSecurityManager(new RMISecurityManager());
try {
Naming.rebind(/* "//localhost/" + */ serviceName, impl);
} catch(java.net.MalformedURLException e) {
System.err.println("Binding: " + e.toString());
System.exit(1);
} catch(java.rmi.RemoteException e) {
System.err.println("Binding: " + e.toString());
System.exit(1);
}
System.out.println("bound");
// find where we are running
String address = null;
try {
address = InetAddress.getLocalHost().getHostName();
} catch(java.net.UnknownHostException e) {
System.err.println("Address: " + e.toString());
System.exit(1);
}
String registeredName = "//" + address + "/" + serviceName;
// make a proxy that knows the service address
proxy = new FileClassifierProxy(registeredName);
// now continue as before
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
System.out.println("waiting...");
// stay around long enough to receive replies
try {
Thread.currentThread().sleep(10000000L);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
for (int n = 0; n < registrars.length; n++) {
ServiceRegistrar registrar = registrars[n];
// export the proxy service
ServiceItem item = new ServiceItem(null,
proxy,
null);
ServiceRegistration reg = null;
try {
reg = registrar.register(item, Lease.FOREVER);
} catch(java.rmi.RemoteException e) {
System.err.println("Register exception: " + e.toString());
}
try {
System.out.println("service registered at " +
registrar.getLocator().getHost());
} catch(Exception e) {
}
}
}
public void discarded(DiscoveryEvent evt) {
}
} // FileClassifierServer
Now we can look at the FileClassifierProxy
.
This is uploaded to the lookup locator, and then onto a client.
The client will deserialize this using the proxy's
readObject
. At this point we attempt to find the
real service, using the RMI call lookup()
.
Later, when the client calls getMIMEType()
the
proxy can send a remote call to the real server to do this:
package option3;
import java.io.Serializable;
import java.io.IOException;
import java.rmi.Naming;
/**
* FileClassifierProxy.java
*
*
* Created: Thu Mar 18 14:32:32 1999
*
* @author Jan Newmarch
* @version
*/
public class FileClassifierProxy implements FileClassifier, Serializable {
protected String serviceLocation;
transient RemoteFileClassifier server = null;
public FileClassifierProxy(String serviceLocation) {
this.serviceLocation = serviceLocation;
}
private void readObject(java.io.ObjectInputStream stream)
throws java.io.IOException, ClassNotFoundException {
stream.defaultReadObject();
try {
Object obj = Naming.lookup(serviceLocation);
server = (RemoteFileClassifier) obj;
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
}
public MIMEType getMIMEType(String fileName)
throws java.rmi.RemoteException {
return server.getMIMEType(fileName);
}
} // FileClassifierProxy
A client using a proxy service does not know it is a proxy, and does not do anything different to what the earlier clients have done:
package option3;
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
*
*
* Created: Wed Mar 17 14:29:15 1999
*
* @author Jan Newmarch
* @version
*/
public class TestFileClassifier implements DiscoveryListener {
public static void main(String argv[]) {
new TestFileClassifier();
}
public TestFileClassifier() {
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
// stay around long enough to receive replies
try {
Thread.currentThread().sleep(1000000L);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
Class[] classes = new Class[1];
FileClassifier classifier = null;
try {
classes[0] = Class.forName("option3.FileClassifier");
} catch(ClassNotFoundException e) {
System.err.println("Class not found");
System.exit(1);
}
ServiceTemplate template = new ServiceTemplate(null, classes,
null);
for (int n = 0; n < registrars.length; n++) {
ServiceRegistrar registrar = registrars[n];
try {
classifier = (FileClassifier) registrar.lookup(template);
} catch(java.rmi.RemoteException e) {
System.exit(2);
}
if (classifier == null) {
System.out.println("Classifier null");
continue;
}
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);
}
}
public void discarded(DiscoveryEvent evt) {
// empty
}
} // TestFileClassifier
Again we have a server and a client to run. The client is easy again, with nothing special required
java option3.TestFileClassifier
The server is more complex, because it is making RMI calls such as
Binding.rebind()
, and these have additional requirements.
Firstly, RMI stubs must be generated during compilation.
Secondly, an rmiregistry
must be running on the server
machine so that the name ``FileClassifier'' can be bound in it,
and thirdly security rights must be set since an
RMISecurityManager
is used.
Although the FileClassifierImpl
is bound to an RMI
registry by Naming.rebind()
, it is not this class
file that is moved around. It continues to exist on the server
machine. Rather, a stub file is moved around,
and will run on the client machine. This stub is responsible for
sending the method requests back to the implementation class
on the server. This stub has to be generated from the implementation
class by the stub compiler rmic
:
rmic -v1.2 -d /web-root/stubs option3.FileClassifierImpl
where the -v1.2
option says to generate JDK 1.2 stubs
only, and the -d
option says where to place the resultant
stub class files so that they can be located by an rmiregistry
.
Note that the pathnames for directories here and later do not include
the package name of the class files. The class files (here
FileClassifierImpl_Stub.class
) will be placed/looked
for in the appropriate subdirectories.
The registry will need to be able to locate the stub files. Typically
this is done by setting the property java.rmi.server.codebase
when running the server. This will then communicate to the registry where
to look. The easiest way to set a property is by the -D
option to the Java runtime engine. Note that the registry
must not be able to locate the stub files
through its CLASSPATH
setting, or confusion results.
This can be done by running the registry in a different directory
or by unsetting the environment variable.
The value of java.rmi.server.codebase
must specify the
protocol used by the registry to find the class files. This could
be the file
protocol or the http
protocol.
For example, if the stub files are stored at
/home/jan/projects/jini/doc/option3/FileClassifierImpl_Stub.class
then the codebase
could be specified as
java.rmi.server.codebase=/home/jan/projects/jini/doc/
Note that in this case, there is no need for an http
server to be running. On the other hand, the class files may be
stored on my Web server's pages under
stubs/option3/FileClassifierImpl_Stub.class
.
In this case, the codebase would be specified as
java.rmi.server.codebase=http://localhost/stubs/
In this case, an http server would need to be running to be able to
deliver the files to the registry.
The server also sets a security manager. This is a restrictive one,
so it needs to be told to allow access. This can be done by setting
the property java.security.policy
to point to a
security policy file such as policy.all
.
Combining all these points leads to startups such as
(unset CLASSPATH; rmiregistry) &
java -Djava.rmi.server.codebase=/home/jan/projects/jini/doc/ \
-Djava.security.policy=policy.all \
FileClassifierServer
This file is Copyright ©Jan Newmarch
(http://jan.newmarch.name)
jan@newmarch.name