Java Technology Home Page
A-Z Index

Java Developer Connection(SM)
Online Training

Downloads, APIs, Documentation
Java Developer Connection
Tutorials, Tech Articles, Training
Online Support
Community Discussion
News & Events from Everywhere
Products from Everywhere
How Java Technology is Used Worldwide
 

Writing Advanced Applications
Chapter 4: Remote Method Invocation

[<<BACK] [CONTENTS] [NEXT>>]

The Remote Method Invocation (RMI) application programming interface (API) enables client and server communications over the net between programs written in the JavaTM programming language. The Enterprise JavaBeansTM server transparently implements the necessary Remote Method Invocation (RMI) code so the client program can reference the Enterprise Beans running on the server and access them as if they are running locally to the client program.

Having RMI built into the Enterprise JavaBeans server is very convenient and saves you coding time, but if you need to use advanced RMI features or integrate RMI with an existing application, you need to override the default RMI implementation and write your own RMI code.

This chapter replaces the container-managed RegistrationBean from Chapter 2: Entity and Session Beans with an RMI-based registration server. The container-managed SellerBean from Chapter 2 is also changed to call the new RMI registration server using a Java 2 RMI lookup call.


About RMI

The RMI API lets you access a remote server object from a client program by making simple method calls on the server object. While other distributed architectures for accessing remote server objects such as Distributed Component Object Model (DCOM) and Common Object Request Broker Architecture (CORBA) return references to the remote object, the RMI API not only returns references, but provides these additional benefits.
  • The RMI API handles remote object references (call by reference) and can also return a copy of the object (call by value).

  • If the client program does not have local access to the class from which a local or remote object was instantiated, RMI services can download the class file.

Serialization and Data marshaling

To transfer objects, the RMI API uses the Serialization API to wrap (marshal) and unwrap (unmarshal) the objects. To marshal an object, the Serialization API converts the object to a stream of bytes, and to unmarshal an object, the Serialization API converts a stream of bytes into an object.

RMI over IIOP

One of the initial disadvantages to RMI was that its sole reliance on the Java platform to write the interfaces made integration into existing legacy systems difficult. However, RMI over Internet Inter-ORB Protocol (IIOP) discussed in Chapter 4: Lookup Services lets RMI communicate with any system or language that CORBA supports.

If you combine improved integration with the ability of RMI to work through firewalls using HTTP firewall proxying, you might find distributing your business logic using RMI is easier than a socket-based solution.


Note: Transfering code and data are key parts of the JiniTM System software specification. In fact, adding a discovery and join service to the RMI services would create something very similar to what you get in the Jini architecture.

RMI in the Auction Application

The RMI-based RegistrationServer has the following new methods:

  • A new create method for creating a new user.
  • A new find method for finding a user.
  • A new search method for the custom search of users in the database.
The new custom search passes results back to the calling client by way of an RMI callback. The RMI callback custom search is similar to the finder methods used in the Bean- and container-managed examples from Chapters 2 and 3, except in the RMI version, it can take more time to generate the results because the remote registration server calls a remote method exported by the RMI-based SellerBean client.

If the calling client is written in the Java programming language, and is not, for example, a web page, the server can update the client as soon as the results are ready. But, the HTTP protocol used in most browsers does not allow results to be pushed to the client without a request for those results. This means the results web page is not created until the results are ready, which can add a small delay.

Class Overview

The two main classes in the RMI-based auction implementation are SellerBean and the remote RegistrationServer. SellerBean is called from AuctionServlet to insert an auction item into the database, and check for low account balances.

The example models the Enterprise JavaBeans architecture in that a user's registration details are separate from the code to create and find the registration details. That is, the user's registration details provided by the Registration.java class are separate from the code to create and find a Registration object, which is in the RegistrationHome.java class.

The remote interface implementation in RegistrationHome.java is bound to the rmiregistry. When a client program wants to manipulate a user's registration details, it must first look up the reference to the RegistrationHome.java object in the rmiregistry.

File Summary

All the source code files for the RMI-based example are described in the bullet list below.
  • SellerBean.java: Client program that calls the RegistrationServer.verifypasswd and RegistrationServer.findLowCreditAccounts remote methods. SellerBean also exports its updateResults method that RegistrationServer calls when it completes its RegistrationServer.findLowCreditAccounts search.

  • RegistrationServer.java: Remote server object that implements the RegistrationHome and Registration remote interfaces.

  • Registration.java: Remote interface that declares the getUser, verifypasswd, and other remote methods for managing a user's registration details.

  • RegistrationHome.java: remote interface that declares the create, findByPrimaryKey, and findLowCreditAccounts remote methods that create or return instances of registration details.

  • RegistrationImpl.java: The RegistrationServer.java source file includes the implementation for the Registration remote interface as class RegistrationImpl

  • RegistrationPK.java: Class that represents a user's registration details using just the primary key of the database record.

  • ReturnResults.java: Remote interface that declares the updateResults method the SellerBean class implements as a callback.

  • AuctionServlet.java: Modified version of the original AuctionServlet class where registration accounts are created by calling the RMI RegistrationServer directly. The auction servlet also calls the SellerBean.auditAccounts method, which returns a list of users with a low account balance.

    The auditAccounts method is called with the following Uniform Resource Locator (URL), which does a simple check to verify the request came from the local host.

    http://phoenix.eng.sun.com:7001/
    	AuctionServlet?action=auditAccounts
    
You also need a java.policy security policy file to grant the permissions needed to run the example on the Java 2 platforms.

Most RMI applications need the two socket permissions for socket and HTTP access to the specified ports. The two thread permissions, were listed in a stack trace as being needed for the RegistrationImpl class to create a new inner thread.

In the Java 2 platform, when a program does not have all the permissions it needs, the Java1 virtual machine (VM) generates a stack trace that lists the permissions that need to be added to the security policy file. See Chapter 9: Program Signing and Security for more information.

grant {
  permission java.net.SocketPermission 
     "*:1024-65535", "connect,accept,resolve";
  permission java.net.SocketPermission "*:80", 
                        "connect";
  permission java.lang.RuntimePermission 
                         "modifyThreadGroup";
  permission java.lang.RuntimePermission 
                         "modifyThread";
};

Compile the Example

Before describing the RMI-based code for the above classes, here is the command sequence to compile the example on the Unix and Win32 platforms:
Unix:
javac registration/Registration.java
javac registration/RegistrationPK.java
javac registration/RegistrationServer.java
javac registration/ReturnResults.java
javac seller/SellerBean.java
rmic -d . registration.RegistrationServer
rmic -d . registration.RegistrationImpl
rmic -d . seller.SellerBean
Win32:
javac registration\Registration.java
javac registration\RegistrationPK.java
javac registration\RegistrationServer.java
javac registration\ReturnResults.java
javac seller\SellerBean.java
rmic -d . registration.RegistrationServer
rmic -d . registration.RegistrationImpl
rmic -d . seller.SellerBean

Start the RMI Registry

Because you are using your own RMI code, you have to explicitly start the RMI Registry so the SellerBean object can find the remote Enterprise Beans. The RegistrationServer uses the RMI Registry to register or bind enterprise Beans that can be called remotely. The SellerBean client contacts the registry to look up and get references to the remote AuctionItem and Registration Enterprise Beans.

Because RMI allows code and data to be transferred, you must be sure the system classloader does not load extra classes that could be mistakenly sent to the client. In this example, extra classes would be the Stub and Skel class files, and the RegistrationSever and RegistrationImpl classes, and to prevent them being mistakenly sent, they should not appear anywhere in the CLASSPATH when you start the RMI Registry. Because the current path could be included automatically, you need to start the RMI Registry away from the code workspace too.

The following commands prevent the sending of extra classes by unsetting the CLASSPATH before starting the RMI Registry on the default 1099 port. You can specify a different port by adding the port number as follows: rmiregistry 4321 &. If you specify a different port number, you must specify the same port number in both you client lookup and server rebind calls.

Unix:
export CLASSPATH=""
rmiregistry &
Win32:
unset CLASSPATH
start rmiregistry

Start the Remote Server

Once the rmiregistry is running, you can start the remote server, RegistrationServer. The RegistrationServer program registers the name registration2 with the rmiregistry name server, and any client can use this name to retrieve a reference to the remote server object, RegistrationHome.

To run the example, copy the RegistrationServer and RegistrationImpl classes and the associated stub classes a remotely accessible area and start the server program.

Unix:
cp *_Stub.class 
	/home/zelda/public_html/registration
cp RegistrationImpl.class 
/home/zelda/public_html/registration
cd /home/zelda/public_html/registration
java -Djava.server.hostname=
	phoenix.eng.sun.com RegistrationServer

Windows:
copy *_Stub.class 
	\home\zelda\public_html\registration
copy RegistrationImpl.class 
	\home\zelda\public_html\registration
cd \home\zelda\public_html\registration
java -Djava.server.hostname=
	phoenix.eng.sun.com RegistrationServer

The following key properties used to configure RMI servers and clients. These properties can be set inside the program or supplied as command line properties to the Java VM.

  • The java.rmi.server.codebase property specifies where the publicly accessible classes are located. On the server this can be a simple file URL to point to the directory or JAR file that contains the classes. If the URL points to a directory, the URL must terminate with a file separator character, "/".

    If you are not using a file URL, you will either need an HTTP server to download the remote classes or have to manually deliver the remote client stub and remote interface classes in, for example, a JAR file.

  • The java.rmi.server.hostname property is the complete host name of the server where the publicly accessible classes reside. This is only needed if the server has problems generating a fully qualified name by itself.

  • The java.rmi.security.policy property specifies the policy file with the permissions needed to run the remote server object and access the remote server classes for download.

Establishing Remote Communications

Client programs communicate with each other through the server. The server program consists of three files. The Registration.java and RegistrationHome.java remote interface files define the methods that can be called remotely, and the RegistrationServer.java class file defines the RegistrationServer and RegistrationImpl classes that implement the methods.

To establish remote communications, both the client and server programs need to access the remote interface classes. The server needs the interface classes to generate the interface implementation, and the client uses the remote interface class to call the remote server method implementation.

For example, SellerBean creates a reference to RegistrationHome, the interface, and not RegistrationServer, the implementation, when it needs to create a user registration.

Besides the server interfaces and classes, you need stub and skeleton classes to establish remote communications. The stub and skeleton classes needed in this example are generated when you run the rmic compiler command on the RegistrationServer and SellerBean classes.

The generated SellerBean, SellerBean_Stub.class and SellerBean_Skel.class classes are needed for the callback from the server to the SellerBean client. It is the _Stub.class file on the client that marshals data to and unmarshals it from the server, while the _Skel.class class does the same for the server.


Note: In the Java 2 platform, the server side, _Skel.class file is no longer needed because its function has been taken over by the Java Virtual Machine classes

Data Marshaling

Marshaling and unmarshaling data means that when you call the RegistrationHome.create method from SellerBean, this call is forwarded to the RegistrationServer_Stub.create method. The RegistrationServer_Stub.create method wraps the method arguments and sends a serialized stream of bytes to the RegistrationServer_Skel.create method.

The RegistrationServer_Skel.create method unwraps the serialized bytestream, re-creates the arguments to the original RegistrationHome.create call, and returns the result of calling the real RegistraionServer.create method back along the same route, but this time wrapping the data on the server side.

Marshaling and unmarshalling data is not without its complications. The first issue is serialized objects might be incompatible across Java Development Kit (JDKTM) releases. A Serialized object has an identifier stored with the object that ties the serialized object to its release. If the RMI client and server complain about incompatible serial IDs, you might need to generate backward compatible stubs and skeletons using the -vcompat option to the rmic compiler.

Another issue is not all objects are serialized by default. The initial Bean-managed RegistrationBean object this example is based on returns an Enumeration object that contains Registration elements in a Vector. Returning this list from a remote method works fine, but when you try to send a vector as a parameter to a remote method, you get a runtime Marshaling exception in the Java 2 platform.

Fortunately, in the Java 2 platform the Collections API offers alternatives to previously unmarshable objects. In this example, an ArrayList from the Collections API replaces the Vector. If the Collections API is not an option, you can create a wrapper class that extends Serializable and provides readObject and writeObject method implementations to convert the object into a bytestream.

RegistrationServer Class

The RegistrationServer class extends java.rmi.server.UnicastRemoteObject and implements the create, findByPrimaryKey and findLowCreditAccounts methods declared in the RegistrationHome interface. The RegistrationServer.java source file also includes the implementation for the Registration remote interface as class RegistrationImpl. RegistrationImpl also extends UnicastRemoteObject.

Exporting a Remote Object

Any object that you want to be remotely accessible needs to either extend java.rmi.server.UnicastRemoteObject or use the exportObject method from the UnicastRemoteObject class. If you extend UnicastRemoteObject, you also get the equals, toString and hashCode methods for the exported object.

Passing by Value and Passing by Reference

Although the RegistrationImpl class is not bound to the registry, it is still referenced remotely because it is associated with the RegistrationHome return results. Because RegistrationImpl extends UnicastRemoteObject, its results are passed by reference, and so only one copy of that user's registration Bean exists in the Java VM at any one time.

In the case of reporting results such as in the RegistrationServer.findLowCreditAccounts method, the RegistrationImpl class copy of the remote object could be used instead. By simply not extending UnicastRemoteObject in the RegistrationImpl class definition, a new Registration object would be returned for each request. In effect the values were passed but not the reference to the object on the server.

Distributed Garbage Collection

Using remote references to objects on the server from a client outside the server's garbage collector introduces some potential problems with memory leaks. How does the server know it is holding onto a reference to a Registration object that is no longer being used by any clients because they aborted or a network connection was dropped?

To avoid potential memory leaks on the server from clients, RMI uses a leasing mechanism when giving out references to exported objects. When exporting an object, the Java VM increases the count for the number of references to this object and sets an expiration time, or lease time, for the new reference to this object.

When the lease expires, the reference count of this object is decreased and if it reaches 0, the object is set for garbage collection by the Java VM. It is up to the client that maintains this weak reference to the remote object to renew the lease if it needs the object beyond the lease time. A weak reference is a way to refer to an object in memory without keeping it from being garbage collected.

This lease time value is a configurable property measured in milliseconds. If you have a fast network, you could shorten the default value and create a large number of transient object references.

The following code sets the lease timeout to 2 minutes.

    Property prop = System.getProperties();
    prop.put("java.rmi.dgc.leaseValue", 120000);

The create and findByPrimaryKey methods are practically identical to the other versions of the Registration Server. The main difference is that on the server side, the registration record is referenced as RegistrationImpl, which is the implementation of Registration. On the client side, Registration is used instead.

The findLowCreditAccounts method builds an ArrayList of serializable RegistrationImpl objects and calls a remote method in the SellerBean class to pass the results bacl. The results are generated by an inner Thread class so the method returns before the results are complete. The SellerBean object waits for the updateAccounts method to be called before displaying the HTML page. In a client written with the Java programming langauge, it would not need to wait, but could display the update in real time.

public class RegistrationServer 
	extends UnicastRemoteObject 
	implements RegistrationHome {

  public registration.RegistrationPK 
    create(String theuser, 
	String password, 
        String emailaddress, 
        String creditcard) 
	  throws registration.CreateException{
    // code to insert database record
  }

  public registration.Registration 
    findByPrimaryKey(registration.RegistrationPK pk) 
	throws registration.FinderException {
      if ((pk == null) || (pk.getUser() == null)) {
            throw new FinderException ();
      }
      return(refresh(pk));
  }

  private Registration refresh(RegistrationPK pk)
	throws FinderException {

    if(pk == null) {
      throw new FinderException ();
    }

    Connection con = null;
    PreparedStatement ps = null;
    try{
      con=getConnection();
      ps=con.prepareStatement("select password, 
	emailaddress, 
	creditcard, 
	balance from registration where theuser = ?");
      ps.setString(1, pk.getUser());
      ps.executeQuery();
      ResultSet rs = ps.getResultSet();
      if(rs.next()) {
        RegistrationImpl reg=null;
        try{
          reg= new RegistrationImpl();
        }catch (RemoteException e) {}
          reg.theuser = pk.getUser();
          reg.password = rs.getString(1);
          reg.emailaddress = rs.getString(2);
          reg.creditcard = rs.getString(3);
          reg.balance = rs.getDouble(4);
          return reg;
      }else{
          throw new FinderException ();
      }
    }catch (SQLException sqe) {
           throw new FinderException();
    }finally {
      try{
        ps.close();
        con.close();
      }catch (Exception ignore) {}
      }
  }

  public void findLowCreditAccounts(
                final ReturnResults client)  
	throws FinderException {
    Runnable bgthread = new Runnable() {
      public void run() {
        Connection con = null;
        ResultSet rs = null;
        PreparedStatement ps = null;
        ArrayList ar = new ArrayList();

        try{
          con=getConnection();
          ps=con.prepareStatement("select theuser, 
	     balance from registration 
	     where balance < ?");
          ps.setDouble(1, 3.00);
          ps.executeQuery();
          rs = ps.getResultSet();
          RegistrationImpl reg=null;
          while (rs.next()) {
            try{
              reg= new RegistrationImpl();
            }catch (RemoteException e) {}
              reg.theuser = rs.getString(1);
              reg.balance = rs.getDouble(2);
              ar.add(reg);
            }
          rs.close();
          client.updateResults(ar);
          }catch (Exception e) {
            System.out.println("findLowCreditAccounts: "+e);
            return;
          }
          finally {
            try{
              if(rs != null) {
                rs.close();
              }
              if(ps != null) {
                ps.close();
                       }
              if(con != null) {
                con.close();
              }
	  }catch (Exception ignore) {}
        }
      } //run
    };
    Thread t = new Thread(bgthread);
    t.start();
  }
}
The main method loads the JDBCTM pool driver. This version uses the Postgres database, installs the RMISecurityManager, and contacts the RMI registry to bind the the RegistrationHome remote object to the name registration2. It does not need to bind the remote interface, Registration because that class is loaded when it is referenced by RegistrationHome.

By default, the server name uses port 1099. If you want to use a different port number, you can add it with a colon as follows: kq6py:4321. If you change the port here, you must start the RMI Registry with the same port number.

The main method also installs a RMIFailureHandler. If the server fails to create a server socket then the failure handler returns true which instructs the RMI server to retry the operation.

  public static void main(String[] args){
     try {
        new pool.JDCConnectionDriver(
                "postgresql.Driver", 
		"jdbc:postgresql:ejbdemo",
		"postgres", "pass");
        } catch (Exception e){ 
           System.out.println(
             "error in loading JDBC driver");
           System.exit(1);
        }
        try {
           Properties env=System.getProperties();
           env.put("java.rmi.server.codebase", 
             "http://phoenix.eng.sun.com/registration");
           RegistrationServer rs= 
             new RegistrationServer();
           if (System.getSecurityManager() == null )  {
               System.setSecurityManager(
		new RMISecurityManager());
           }
           RMISocketFactory.setFailureHandler(
		new RMIFailureHandlerImpl());

           Naming.rebind("
		//phoenix.eng.sun.com/registration2",rs);
        }catch (Exception e) {
           System.out.println("Exception thrown "+e);
        }
   }
}

class RMIFailureHandlerImpl 
        implements RMIFailureHandler {
   public boolean failure(Exception ex ){
      System.out.println("exception "+ex+" caught");
      return true;
   }
}

Registration Interface

The Registration interface declares the methods implemented by RegistrationImpl in the RegistrationServer.java source file.
package registration;

import java.rmi.*;
import java.util.*;

public interface Registration extends Remote {
   boolean verifyPassword(String password) 
		throws RemoteException;
   String getEmailAddress() throws RemoteException;
   String getUser() throws RemoteException;
   int adjustAccount(double amount) 
         throws RemoteException;
   double getBalance() throws RemoteException;
}

RegistrationHome Interface

The RegistrationHome interface declares the methods implemented by theRegistrationServer class. These methods mirror the Home interface defined in the Enterprise JavaBeans example. The findLowCreditAccounts method takes a remote interface as its only parameter.
package registration;

import java.rmi.*;
import java.util.*;

public interface RegistrationHome extends Remote {
  RegistrationPK create(String theuser, 
	String password, 
	String emailaddress, 
	String creditcard) 
		throws CreateException, 
		RemoteException;

  Registration findByPrimaryKey(RegistrationPK theuser) 
		throws FinderException, RemoteException;

  public void findLowCreditAccounts(ReturnResults rr) 
		throws FinderException, RemoteException;
}

ReturnResults Interface

The ReturnResults interface declares the method implemented by the SellerBean class. The updateResults method is called from RegistrationServer.
package registration;

import java.rmi.*;
import java.util.*;

public interface ReturnResults extends Remote {
    public void updateResults(ArrayList results) 
	throws FinderException, RemoteException;
}

SellerBean Class

The SellerBean class includes the callback method implementation and calls the RegistrationServer object using RMI. The updateAccounts method is made accessible by a call to UnicastRemoteObject.exportObject(this);. The auditAccounts method waits on a Boolean object.

The updateAccounts method sends a notify to all methods waiting on the Boolean object when it has been called from the server and receives the search results.

package seller;

import java.rmi.RemoteException;
import java.rmi.*;
import javax.ejb.*;
import java.util.*;
import java.text.NumberFormat;
import java.io.Serializable;
import javax.naming.*;
import auction.*;
import registration.*;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;


public class SellerBean 
	implements SessionBean, ReturnResults {

    protected SessionContext ctx;
    javax.naming.Context ectx;
    Hashtable env = new Hashtable();
    AuctionServlet callee=null;
    Boolean ready=new Boolean("false");
    ArrayList returned;

    public int insertItem(String seller, 
	String password, 
	String description, 
	int auctiondays, 
	double startprice, 
	String summary) 
		throws RemoteException { 

      try{
        RegistrationHome regRef = (
		RegistrationHome)Naming.lookup(
		"//phoenix.eng.sun.com/registration2");
        RegistrationPK rpk= new RegistrationPK();
        rpk.setUser(seller);
        Registration newseller = (
	      Registration)regRef.findByPrimaryKey(rpk);
	if((newseller == null) || 
              (!newseller.verifyPassword(password))) {
            return(Auction.INVALID_USER);	
  	}

        AuctionItemHome home = (
		AuctionItemHome) ectx.lookup(
		                   "auctionitems");
        AuctionItem ai= home.create(seller, 
		description, 
		auctiondays, 
		startprice, 
		summary);
	if(ai == null) {
          return Auction.INVALID_ITEM;
	}else{
          return(ai.getId()); 
	}
        }catch(Exception e){ 
          System.out.println("insert problem="+e);
          return Auction.INVALID_ITEM;
        }
    }

    public void updateResults(java.util.ArrayList ar) 
		throws RemoteException {
       returned=ar; 
       synchronized(ready) {
          ready.notifyAll();
       }
    }

    public ArrayList auditAccounts() {
        this.callee=callee;
        try {
           RegistrationHome regRef = (
		RegistrationHome)Naming.lookup(
		"//phoenix.eng.sun.com/registration2");
           regRef.findLowCreditAccounts(this);
           synchronized(ready) {
              try {
                 ready.wait();
              } catch (InterruptedException e){}
           }
        return (returned);
       }catch (Exception e) {
          System.out.println("error in creditAudit "+e);
       }
      return null;
    }

    public void ejbCreate() 
		throws javax.ejb.CreateException, 
		RemoteException {
      env.put(
        javax.naming.Context.INITIAL_CONTEXT_FACTORY, 
        "weblogic.jndi.TengahInitialContextFactory");
      try{
        ectx = new InitialContext(env);
      } catch (NamingException e) {
        System.out.println(
          "problem contacting EJB server");
        throw new javax.ejb.CreateException();
      }
        Properties env=System.getProperties();
        env.put("java.rmi.server.codebase", 
	  "http://phoenix.eng.sun.com/registration");
        env.put("java.security.policy","java.policy");
        UnicastRemoteObject.exportObject(this);
    }

    public void setSessionContext(SessionContext ctx) 
		throws RemoteException {
      this.ctx = ctx;
    }

    public void unsetSessionContext() 
		throws RemoteException {   
        ctx = null;  
    } 

    public void ejbRemove() {}
    public void ejbActivate() throws RemoteException {
	System.out.println("activating seller bean");
    }
    public void ejbPassivate() throws RemoteException { 
	System.out.println("passivating seller bean");
    }
}

_______
1 As used on this web site, the terms "Java virtual machine" or "JVM" mean a virtual machine for the Java platform.

[TOP]



[ This page was updated: 28-Oct-99 ]

Products & APIs | Developer Connection | Docs & Training | Online Support
Community Discussion | Industry News | Solutions Marketplace | Case Studies
Glossary - Applets - Tutorial - Employment - Business & Licensing - Java Store - Java in the Real World
FAQ | Feedback | Map | A-Z Index
For more information on Java technology
and other software from Sun Microsystems, call:
(800) 786-7638
Outside the U.S. and Canada, dial your country's AT&T Direct Access Number first.
Sun Microsystems, Inc.
Copyright © 1995-99 Sun Microsystems, Inc.
All Rights Reserved. Legal Terms. Privacy Policy.