Appendix B. Example Code

The first rule of magic is simple: Don’t waste your time waving your hands and hoping when a rock or a club will do.

—McCloctnik the Lucid

The following pages contain the complete code for the examples used in the introductory chapters of this book. The sources are listed in alphabetical order by the full name, including the package name. For your convenience, here is a mapping from the simple class name to its fully-qualified class name:

ChatMessage........... chat.ChatMessage

ChatProxy............. chat.ChatProxy

ChatServer............ chat.ChatServer

ChatServerAdmin....... chat.ChatServerAdmin

ChatServerImpl........ chat.ChatServerImpl

ChatSpeaker........... chat.ChatSpeaker

ChatStream............ chat.ChatStream

ChatSubject........... chat.ChatSubject

Chatter............... chatter.Chatter

ChatterThread......... chatter.ChatterThread

FortuneAdmin.......... fortune.FortuneAdmin

FortuneStream......... fortune.FortuneStream

FortuneStreamImpl..... fortune.FortuneStreamImpl

FortuneTheme.......... fortune.FortuneTheme

MessageStream......... message.MessageStream

ParseUtil............. util.ParseUtil

StreamReader.......... client.StreamReader

You can also find the code at http://java.sun.com/docs/books/jini/

    package chat; 

import java.io.Serializable; 

/** 
 * A single message in the <CODE>ChatStream</CODE>. This is the 
 * type of <CODE>Object</CODE> returned by <CODE>ChatStream.nextMessage</CODE>. 
 * 
 * @see ChatStream 
 */ 
public class ChatMessage implements Serializable {
    /** 
     * The speaker of the message. 
     * @serial 
     */ 
    private String speaker; 

    /** 
     * The contents of the message. 
     * @serial 
     */ 
    private String[] content; 

    /** 
     * The serial version UID. Stating it explicitly is good. 
     * 
     * @see fortune.FortuneTheme#serialVersionUID 
     */ 
    static final long serialVersionUID = 
                            -1852351967189107571L; 

    /** 
     * Create a new <CODE>ChatMessage</CODE> with the given 
     * <CODE>speaker</CODE> and <CODE>content</CODE>. 
     */ 
    public ChatMessage(String speaker, String[] content) {
        this.speaker = speaker; 
        this.content = content; 
    } 

    /** 
     * Return the speaker of the message. 
     */ 
    public String getSpeaker() { return speaker; } 

    /** 
     * Return the content of the message. Each string in the array 
     * represents a single line of content. 
     */ 
    public String[] getContent() { return content; } 
    // inherit doc comment from superclass 
    public String toString() {
        StringBuffer buf = new StringBuffer(speaker); 
        buf.append(": "); 
        for (int i = 0; i < content.length; i++) 
            buf.append(content[i]).append('
'); 
        buf.setLength(buf.length() - 1); // strip newline 
        return buf.toString(); 
    } 
} 

package chat; 

import java.io.EOFException; 
import java.io.Serializable; 
import java.rmi.RemoteException; 

/** 
 * The client-side proxy for a <CODE>ChatServer</CODE>-based 
 * <CODE>ChatStream</CODE> service. This forwards most requests to the 
 * server, remembering the last successfully retrieved message index. 
 */ 
class ChatProxy implements ChatStream, Serializable {
    /** 
     * Reference to the remote server. 
     * @serial 
     */ 
    private final ChatServer server; 

    /** 
     * The index of the last entry successfully received. 
     * @serial 
     */ 
    private int lastIndex = -1; 

    /** 
     * Cache of the subject of the chat. 
     */ 
    private transient String subject; 

    /** 
     * Create a new proxy that will talk to the given server object. 
     */ 
    ChatProxy(ChatServer server) {
        this.server = server; 
    } 

    // inherit doc comment from ChatStream 
    public synchronized Object nextMessage() 
        throws RemoteException, EOFException 
    {
        ChatMessage msg = server.nextInLine(lastIndex); 
        lastIndex++; 
        return msg; 
    } 

    // inherit doc comment from ChatStream 
    public void add(String speaker, String[] msg) 
        throws RemoteException 
    {
        server.add(speaker, msg); 
    } 

    // inherit doc comment from ChatStream 
    public synchronized String getSubject() 
        throws RemoteException 
    {
        if (subject == null) 
            subject = server.getSubject(); 
        return subject; 
    } 

    // inherit doc comment from ChatStream 
    public String[] getSpeakers() throws RemoteException {
        return server.getSpeakers(); 
    } 

    public boolean equals(Object other) {
      if (other instanceof ChatProxy) 
          return server.equals(((ChatProxy) other).server); 
      else 
          return false; 
    } 

    public int hashCode() {
        return server.hashCode() + 1; 
    } 
} 

    package chat; 

import java.io.EOFException; 
import java.rmi.Remote; 
import java.rmi.RemoteException; 

/** 
 * The interface used by a <CODE>ChatProxy</CODE> to talk to its server. 
 * 
 * @see ChatProxy 
 */ 
interface ChatServer extends Remote {
    /** 
     * Return the next message after <CODE>lastIndex</CODE>. This call 
     * creates idempotency since repeated invocations with the same 
     * value of <CODE>lastIndex</CODE> will always return the same 
     * value. This blocks until a message is available. 
     * 
     * @see message.MessageStream#nextMessage 
     */ 
    ChatMessage nextInLine(int lastIndex) 
        throws EOFException, RemoteException; 

    /** 
     * Add a new message to end of the stream. The speaker 
     * will be added to the list of known speakers if not already 
     * in it. The resulting message will have the speaker as the 
     * first line of the message, with the rest of the message as 
     * the remaining lines. 
     * 
     * @see message.MessageStream#nextMessage 
     */ 
    void add(String speaker, String[] msg) 
        throws RemoteException; 

    /** 
     * Return the subject of the chat. The subject never changes. 
     */ 
    String getSubject() throws RemoteException; 

    /** 
     * Return the list of speakers with messages in the stream. 
     * The order is not significant. 
     */ 
    String[] getSpeakers() throws RemoteException; 
} 

    package chat; 

import util.ParseUtil; 

import java.io.BufferedInputStream; 
import java.io.BufferedOutputStream; 
import java.io.File; 
import java.io.FileInputStream; 
import java.io.FileOutputStream; 
import java.io.IOException; 
import java.io.ObjectInputStream; 
import java.io.ObjectOutputStream; 
import java.rmi.activation.Activatable; 
import java.rmi.activation.ActivationDesc; 
import java.rmi.activation.ActivationException; 
import java.rmi.activation.ActivationGroup; 
import java.rmi.activation.ActivationGroupDesc.CommandEnvironment; 
import java.rmi.activation.ActivationGroupDesc; 
import java.rmi.activation.ActivationGroupID; 
import java.rmi.activation.ActivationSystem; 
import java.rmi.MarshalledObject; 
import java.rmi.Remote; 
import java.rmi.RemoteException; 
import java.util.Properties; 

/** 
 * The administrative program that creates a new <CODE>ChatServerImpl</CODE> 
 * chat stream service. It's invocation is: 
 * <pre> 
 *      java [<i>java-options</i>] chat.ChatServerAdmin <i>dir subject</i> 
 *              [<i>groups|lookupURL classpath codebase policy-file</i>] 
 * </pre> 
 * Where the options are: 
 * <dl> 
 * <dt><i><CODE>java-options</CODE></i> 
 * <dd>Options to the Java VM that will run the admin program. Typically 
 * this includes a security policy property. 
 * <p> 
 * <dt><i><CODE>dir</CODE></i> 
 * <dd>The directory in which all the chats in the same group will live. 
 * <p> 
 * <dt><i><CODE>subject</CODE></i> 
 * <dd>The subject of the chat. This must be unique within the group. 
 * <p> 
 * <dt><i><CODE>groups</CODE></i>|<i><CODE>lookupURL</CODE></i> 
 * <dd>Either a comma-separated list of groups in which all the services 
 * in the group will be registered or a URL to a specific lookup service. 
 * <p> 
 * <dt><i><CODE>classpath</CODE></i> 
 * <dd>The classpath for the activated service (<CODE>ChatServerImpl</CODE> 
 * will be loaded from this). 
 * <p> 
 * <dt><i><CODE>codebase</CODE></i> 
 * <dd>The codebase for users of the service (<CODE>ChatProxy</CODE> will 
 * be loaded from this). 
 * <p> 
 * <dt><i><CODE>policy-file</CODE></i> 
 * <dd>The policy file for the activated service's virtual machine. 
 * </dl> 
 * <p>The last four parameters imply creation of a new group. If any 
 * are specified they must all be specified. If none are specified the 
 * new chat stream will be in the same activation group as the others 
 * who use the same storage directory, and so will use the same values 
 * for the last four parameters. 
 */ 
public class ChatServerAdmin {
    /** 
     * The main program for <CODE>ChatServerAdmin</CODE>. 
     */ 
    public static void main(String[] args) throws Exception 
    {
        if (args.length != 2 && args.length != 6) {
            usage();            // print usage message 
            System.exit(1); 
        } 

        File dir = new File(args[0]); 
        String subject = args[1]; 

        ActivationGroupID group = null; 
        if (args.length == 2) 
            group = getGroup(dir); 
        else {
            String[] groups = ParseUtil.parseGroups(args[2]); 
            String lookupURL = 
                (args[2].indexOf(':') > 0 ? args[2] : null); 
            String classpath = args[3]; 
            String codebase = args[4]; 
            String policy = args[5]; 
            group = createGroup(dir, groups, lookupURL, 
                classpath, codebase, policy); 
        } 

        File data = new File(dir, subject); 
        MarshalledObject state = new MarshalledObject(data); 
        ActivationDesc desc = 
            new ActivationDesc(group, "chat.ChatServerImpl", 
                               null, state, true); 
        Remote newObj = Activatable.register(desc); 
        ChatServer server = (ChatServer) newObj; 
        String s = server.getSubject(); // force server up 
        System.out.println("server created for " + s); 
    } 

    /** 
     * Print a usage message for the user. 
     */ 
    private static void usage() {
        System.out.println("usage: java [java-options] " + 
            ChatServerAdmin.class + " dir subject " + 
            " [groups|lookupURL classpath codebase policy-file]
"); 
    } 

    /** 
     * Create a new group with the given parameters. 
     */ 
    private static ActivationGroupID 
        createGroup(File dir, String[] groups, String lookupURL, 
                    String classpath, String codebase, 
                    String policy) 
        throws IOException, ActivationException 
    {
        if (!dir.isDirectory()) 
            dir.mkdirs(); 

        Properties props = new Properties(); 
        props.put("java.rmi.server.codebase", codebase); 
        props.put("java.security.policy", policy); 
        String[] argv = new String[] { "-cp", classpath }; 
        CommandEnvironment cmd = 
            new CommandEnvironment("java", argv); 
        ActivationSystem actSys = ActivationGroup.getSystem(); 
        ActivationGroupDesc groupDesc = 
            new ActivationGroupDesc(props, cmd); 
        ActivationGroupID id = actSys.registerGroup(groupDesc); 

        FileOutputStream fout = 
            new FileOutputStream(groupFile(dir)); 
        ObjectOutputStream out = new ObjectOutputStream(
            new BufferedOutputStream(fout)); 
        out.writeObject(id); 
        out.writeObject(groups); 
        out.writeObject(lookupURL); 
        out.flush();            // force bits out of buffer 
        fout.getFD().sync();    // force bits to the disk 
        out.close(); 

        return id; 
     } 
    /** 
     * Return a <CODE>File</CODE> object contains the group description. 
     * This assumes that nobody will create a group with the subject 
     * <CODE>"grpdesc"</CODE>. This is probably a bad assumption -- a 
     * fully robust implementation should either check this and forbid it 
     * or figure out a way to store this someplace that does not conflict 
     * with subject names. 
     */ 
    static File groupFile(File dir) {
        return new File(dir, "grpdesc"); 
    } 

    /** 
     * Get the ActivationGroupID for the existing group in the given 
     * directory. 
     */ 
    private static ActivationGroupID getGroup(File dir) 
        throws IOException, ClassNotFoundException 
    {
        ObjectInputStream in = null; 
        try {
            in = new ObjectInputStream(new BufferedInputStream(
                new FileInputStream(groupFile(dir)))); 
            return (ActivationGroupID) in.readObject(); 
        } finally {
            if (in != null) 
                in.close(); 
        } 
    } 
} 

    package chat; 

import net.jini.core.discovery.LookupLocator; 
import net.jini.core.entry.Entry; 
import net.jini.core.lookup.ServiceID; 

import net.jini.discovery.DiscoveryManagement; 
import net.jini.discovery.LookupDiscoveryManager; 
import net.jini.lease.LeaseRenewalManager; 
import net.jini.lookup.JoinManager; 
import net.jini.lookup.ServiceIDListener; 

import com.sun.jini.reliableLog.LogHandler; 
import com.sun.jini.reliableLog.ReliableLog; 

import java.io.File; 
import java.io.FileInputStream; 
import java.io.InputStream; 
import java.io.IOException; 
import java.io.ObjectInputStream; 
import java.io.ObjectOutputStream; 
import java.io.OutputStream; 
import java.rmi.activation.Activatable; 
import java.rmi.activation.ActivationID; 
import java.rmi.MarshalledObject; 
import java.util.ArrayList; 
import java.util.HashSet; 
import java.util.List; 
import java.util.Set; 

/** 
 * The implementation of <CODE>ChatServer</CODE>. This runs inside an 
 * activation group defined by the persistent state from the activation 
 * service. 
 * 
 * @exclude ReliableLogHandler 
 */ 
public class ChatServerImpl implements ChatServer {
    /** 
     * The join manager we're using. 
     */ 
    private JoinManager joinMgr; 

    /** 
     * Our subject of discussion. 
     */ 
    private String subject; 

    /** 
     * The set of known speakers. 
     */ 
    private Set speakers = new HashSet(); 

    /** 
     * The list of messages. 
     */ 
    private List messages = new ArrayList(); 

    /** 
     * The list of service attributes. 
     */ 
    private List attrs; 

    /** 
     * The service ID (or <CODE>null</CODE>). 
     */ 
    private ServiceID serviceID; 

    /** 
     * Our persistent storage. 
     */ 
    private ChatStore store; 

    /** 
     * Groups to register with (or an empty array). 
     */ 
    private String[] groups = new String[0]; 

    /** 
     * URL to specific join manager (or <CODE>null</CODE>). 
     */ 
    private String lookupURL; 

    /** 
     * The lease renewal manager for all servers in our group. 
     * We share it because this gives it more leases it might be 
     * able to compress into single renewal messages. 
     */ 
    private static LeaseRenewalManager 
        renewer = new LeaseRenewalManager(); 

    /** 
     * The storage for a <CODE>ChatServerImpl</CODE>. 
     */ 
    class ChatStore extends LogHandler 
        implements ServiceIDListener 
    {
        /** 
         * The reliable log in which we store our state. 
         */ 
    private ReliableLog log; 

    /** 
     * Create a new <CODE>ChatStore</CODE> object for the given 
     * directory. The directory is the full path for the specific 
     * storage for this chat on the subject. The parent directory 
     * is the one for the group. 
     */ 
    ChatStore(File dir) throws IOException {
         // If the directory exists, recover from it. Otherwise 
         // create it as a a new subject. 
         if (dir.exists()) {
              log = new ReliableLog(dir.toString(), this); 
              log.recover(); 
         } else {
              subject = dir.getName(); 
              log = new ReliableLog(dir.toString(), this); 
              attrs = new ArrayList(); 
              attrs.add(new ChatSubject(subject)); 
              log.snapshot(); 
        } 

        // Read in the lookup groups and lookupURL for our service 
        ObjectInputStream in = null; 
        try {
            in = new ObjectInputStream(
                new FileInputStream(
                      ChatServerAdmin.groupFile(dir.getParentFile()))); 
            in.readObject();        // skip over the group ID 
            groups = (String[]) in.readObject(); 
            lookupURL = (String) in.readObject(); 
        } catch (ClassNotFoundException e) {
            unexpectedException(e); 
        } catch (IOException e) {
            unexpectedException(e); 
        } finally {
            if (in != null) 
                in.close(); 
        } 
    } 

    /** 
     * Stores the current information in storage. In our case only 
     * the start state is snapshoted -- everything else is added 
     * incrementally anyway and so the log of changes is the 
     * state. Part of <CODE>ReliableLogHandler</CODE>. 
     */ 
    public void snapshot(OutputStream out) throws Exception {
        ObjectOutputStream oo = new ObjectOutputStream(out); 
        oo.writeObject(subject); 
        oo.writeObject(attrs); 
    } 

    /** 
     * Recovers the information from storage. Part of 
     * <CODE>ReliableLogHandler</CODE>. 
     * 
     * @see #snapshot 
     */ 
    public void recover(InputStream in) throws Exception {
        ObjectInputStream oi = new ObjectInputStream(in); 
        subject = (String) oi.readObject(); 
        attrs = (List) oi.readObject(); 
    } 

    /** 
     * Apply an update from the log during recovery. The types 
     * of data we add happen to all be distinct so we know exactly 
     * what something is based on its type alone (lucky us). Part 
     * of <CODE>ReliableLogHandler</CODE>. 
     */ 
    public void applyUpdate(Object update) throws Exception {
        if (update instanceof ChatMessage) {
            messages.add(update); 
            addSpeaker(((ChatMessage) update).getSpeaker()); 
        } else if (update instanceof Entry) {
            attrs.add(update); 
        } else if (update instanceof ServiceID) {
            serviceID = (ServiceID) update; 
        } else {
            throw new IllegalArgumentException(
                "Internal error: update type " + 
                update.getClass().getName() + ", " + update); 
        } 
    } 

    /** 
     * Invoked when the serviceID is first assigned to the service. 
     * Part of <CODE>ServiceIDListener</CODE>. 
     */ 
    public void serviceIDNotify(ServiceID serviceID) {
        try {
            log.update(serviceID); 
        } catch (IOException e) {
            unexpectedException(e); 
        } 
        ChatServerImpl.this.serviceID = serviceID; 
    } 

    /** 
     * Add a new speaker to the persistent storage log. 
     */ 
    synchronized void add(ChatMessage msg) {
        try {
            log.update(msg, true); 
        } catch (IOException e) {
            unexpectedException(e); 
        } 
    } 
} 

/** 
 * The activation constructor for <CODE>ChatServerImpl</CODE>. The 
 * <CODE>state</CODE> object contains the directory which is our 
 * reliable log directory. 
 */ 
public ChatServerImpl(ActivationID actID, 
                      MarshalledObject state) 
    throws IOException, ClassNotFoundException 
{
    File dir = (File) state.get(); 
    store = new ChatStore(dir); 
    ChatProxy proxy = new ChatProxy(this); 

    LookupLocator[] locators = null; 
    if (lookupURL != null) {
        LookupLocator loc = new LookupLocator(lookupURL); 
        locators = new LookupLocator[] { loc }; 
    } 
    DiscoveryManagement dm = 
        new LookupDiscoveryManager(groups, locators, null); 
    joinMgr = new JoinManager(proxy, getAttrs(), store, 
                              dm, renewer); 
    Activatable.exportObject(this, actID, 0); 
} 

/** 
 * Return the attributes as an array for use in JoinManager. 
 */ 
private Entry[] getAttrs() {
    return (Entry[]) attrs.toArray(new Entry[attrs.size()]); 
} 

// inherit doc comment from ChatServer 
public String getSubject() {
    return subject; 
} 

// inherit doc comment from ChatServer 
public String[] getSpeakers() {
        return (String[]) speakers.toArray(new String[speakers.size()]); 
    } 

    // inherit doc comment from ChatServer 
    public synchronized void add(String speaker, String[] lines) 
    {
        ChatMessage msg = new ChatMessage(speaker, lines); 
        store.add(msg); 
        addSpeaker(speaker); 
        messages.add(msg); 
        notifyAll(); 
    } 

    /** 
     * Add a speaker to the known list. If the speaker is already 
     * known, this does nothing. 
     */ 
    private synchronized void addSpeaker(String speaker) {
        if (speakers.contains(speaker)) 
            return; 
        speakers.add(speaker); 
        Entry speakerAttr = new ChatSpeaker(speaker); 
        attrs.add(speakerAttr); 
        joinMgr.addAttributes(new Entry[] { speakerAttr }); 
    } 

    // inherit doc comment from ChatServer 
    public synchronized ChatMessage nextInLine(int index) {
        try {
            int nextIndex = index + 1; 
            while (nextIndex >= messages.size()) 
                wait(); 
            return (ChatMessage) messages.get(nextIndex); 
        } catch (InterruptedException e) {
            unexpectedException(e); 
            return null; // keeps the compiler happy 
        } 
    } 

    /** 
     * Turn any unexpected exception into a runtime exception reflected 
     * back to the client. These are both unexpected and unrecoverable 
     * exception (such as "file system full"). 
     */ 
    private static void unexpectedException(Throwable e) {
        throw new RuntimeException("unexpected exception: " + e); 
    } 
} 

    package chat; 

import net.jini.entry.AbstractEntry; 
import net.jini.lookup.entry.ServiceControlled; 

/** 
 * An attribute for the <CODE>ChatStream</CODE> service that marks a 
 * speaker as being present in a particular stream. 
 * 
 * @see ChatStream 
 */ 
public class ChatSpeaker extends AbstractEntry 
    implements ServiceControlled 
{
    /** 
     * The serial version UID. Stating it explicitly is good. 
     * 
     * @see fortune.FortuneTheme#serialVersionUID 
     */ 
    static final long serialVersionUID = 
                            6748592884814857788L; 

    /** 
     * The speaker's name. 
     * @serial 
     */ 
    public String speaker; 

    /** 
     * Public no-arg constructor. Required for all <CODE>Entry</CODE> 
     * objects. 
     */ 
    public ChatSpeaker() { } 

    /** 
     * Create a new <CODE>ChatSpeaker</CODE> with the given speaker. 
     */ 
    public ChatSpeaker(String speaker) {
        this.speaker = speaker; 
    } 
} 

    package chat; 

import message.MessageStream; 

import java.rmi.RemoteException; 

/** 
 * A type of <CODE>MessageStream</CODE> whose contents are a chat 
 * session. The <CODE>nextMessage</CODE> method blocks if there is 
 * as yet no next message in the stream. The messages in the stream 
 * are ordered, so <CODE>nextMessage</CODE> must be idempotent -- should 
 * the client receive a <CODE>RemoteException</CODE>, the next invocation 
 * must return the next message that the client has not yet seen. 
 * <p> 
 * Each message returned by <CODE>nextMessage</CODE> is a 
 * <CODE>ChatMessage</CODE> object that has a speaker and what they 
 * said. 
 * 
 * @see ChatMessage 
 * @see ChatSpeaker 
 * @see ChatSubject 
 */ 
public interface ChatStream extends MessageStream {
    /** 
     * Add a new message to the stream. If the speaker is previously 
     * unknown in the stream, a <CODE>ChatSpeaker</CODE> attribute 
     * will be added to the service. 
     * 
     * @see ChatSpeaker 
     */ 
    public void add(String speaker, String[] message) 
        throws RemoteException; 

    /** 
     * Return the subject of the chat. This does not change during the 
     * lifetime of the service. This subject will also exist as a 
     * <CODE>ChatSubject</CODE> attribute on the service. 
     * 
     * @see ChatSubject 
     */ 
    public String getSubject() throws RemoteException; 

    /** 
     * Return the list of speakers currently known in the stream. 
     * The order is not significant. 
     * 
     * @see ChatSpeaker 
     */ 
    public String[] getSpeakers() throws RemoteException; 
} 

    package chat; 

import net.jini.entry.AbstractEntry; 
import net.jini.lookup.entry.ServiceControlled; 

/** 
 * An attribute for the <CODE>ChatStream</CODE> service that marks the 
 * subject of discussion. 
 * 
 * @see ChatStream 
 */ 
public class ChatSubject extends AbstractEntry 
    implements ServiceControlled 
{
    /** 
     * The serial version UID. Stating it explicitly is good. 
     * 
     * @see fortune.FortuneTheme#serialVersionUID 
     */ 
    static final long serialVersionUID = 
                            -4036337828321897774L; 

    /** 
     * The subject of the discussion. 
     * @serial 
     */ 
    public String subject; 

    /** 
     * Public no-arg constructor. Required for all <CODE>Entry</CODE> 
     * objects. 
     */ 
    public ChatSubject() { } 

    /** 
     * Create a new <CODE>ChatSubject</CODE> with the given subject. 
     */ 
    public ChatSubject(String subject) {
        this.subject = subject; 
    } 
} 

    package chatter; 

import chat.ChatStream; 
import chat.ChatMessage; 
import client.StreamReader; 
import message.MessageStream; 

import java.rmi.RemoteException; 

/** 
 * A client that talks to a <CODE>ChatStream</CODE>, allowing the user 
 * to add messages as well as read them. The user's login name is used 
 * as their name in the chat. The usage is: 
 * <pre> 
 *      java [java-options] chatter.Chatter args... 
 * </pre> 
 * The arguments are the same as those for <CODE>client.StreamReader</CODE> 
 * except that you cannot specify the <CODE>-c</CODE> option. The stream 
 * used will be at least a <CODE>chat.ChatStream</CODE> service. 
 * 
 * @see client.StreamReader 
 * @see ChatterThread 
 */ 
public class Chatter extends StreamReader {
    /** 
     * Start up the service. 
     */ 
    public static void main(String[] args) throws Exception 
    {
        String[] fullargs = new String[args.length + 3]; 
        fullargs[0] = "-c"; 
        fullargs[1] = String.valueOf(Integer.MAX_VALUE); 
        System.arraycopy(args, 0, fullargs, 2, args.length); 
        fullargs[fullargs.length - 1] = "chat.ChatStream"; 
        Chatter chatter = new Chatter(fullargs); 
        chatter.execute(); 
    } 

    /** 
     * Create a new <CODE>Chatter</CODE>. The <CODE>args</CODE> are 
     * passed to the superclass. 
     */ 
    private Chatter(String[] args) {
        super(args); 
    } 

    /** 
     * Overrides <CODE>readStream</CODE> to start up a 
     * <CODE>ChatterThread</CODE> when the stream is found. The 
     * <CODE>ChatterThread</CODE> lets the user type messages, while this 
     * thread continually reads them. 
     */ 
    public void readStream(MessageStream msgStream) 
        throws RemoteException 
    {
        ChatStream stream = (ChatStream) msgStream; 
        new ChatterThread(stream).start(); 
        super.readStream(stream); 
    } 

    /** 
     * Print out a message, marking the speaker for easy reading. 
     */ 
    public void printMessage(int msgNum, Object msg) {
        if (!(msg instanceof ChatMessage)) 
            super.printMessage(msgNum, msg); 
        else {
            ChatMessage cmsg = (ChatMessage) msg; 
            System.out.println(cmsg.getSpeaker() + ":"); 
            String[] lines = cmsg.getContent(); 
            for (int i = 0; i < lines.length; i++) {
                System.out.print(" "); 
                System.out.println(lines[i]); 
            } 
        } 
    } 
} 

    package chatter; 

import chat.ChatStream; 

import java.io.BufferedReader; 
import java.io.InputStreamReader; 
import java.io.IOException; 
import java.rmi.RemoteException; 
import java.util.ArrayList; 
import java.util.List; 

/** 
 * The thread that <CODE>Chatter</CODE> uses to let the user type 
 * new messages. 
 */ 
class ChatterThread extends Thread {
    /** 
     * The stream to which we're adding. 
     */ 
    private ChatStream stream; 

    /** 
     * Create a new <CODE>ChatterThread</CODE> to write to the given stream. 
     */ 
    ChatterThread(ChatStream stream) {
        this.stream = stream; 
    } 

    /** 
     * The thread's workhorse. Read what the user types and put it into 
     * the stream as messages from the user. The user's name is read from 
     * the <CODE>user.name</CODE> property. A message consists of a series 
     * of lines ending in backslash until one that doesn't. 
     */ 
    public void run() {
        BufferedReader in = new BufferedReader(
            new InputStreamReader(System.in)); 
        String user = System.getProperty("user.name"); 
        List msg = new ArrayList(); 
        String[] msgArray = new String[0]; 
        for (;;) {
            try {
                String line = in.readLine(); 
                if (line == null) 
                    System.exit(0); 

                boolean more = line.endsWith("\"); 
                if (more) {     // strip trailing backslash 
                    int stripped = line.length() - 1; 
                    line = line.substring(0, stripped); 
                } 
                msg.add(line); 
                if (!more) {
                    msgArray = (String[]) 
                        msg.toArray(new String[msg.size()]); 
                    stream.add(user, msgArray); 
                    msg.clear(); 
                } 
            } catch (RemoteException e) {
                System.out.println("RemoteException:retry"); 
                for (;;) {
                    try {
                        Thread.sleep(1000); 
                        stream.add(user, msgArray); 
                        msg.clear(); 
                        break; 
                    } catch (RemoteException re) {
                        continue;       // try again 
                    } catch (InterruptedException ie) {
                        System.exit(1); 
                    } 
                } 
            } catch (IOException e) {
                System.exit(1); 
            } 
        } 
    } 
} 

    package client; 

import net.jini.core.discovery.LookupLocator; 
import net.jini.core.entry.Entry; 
import net.jini.core.lookup.ServiceRegistrar; 
import net.jini.core.lookup.ServiceTemplate; 
import net.jini.core.lookup.ServiceItem; 

import net.jini.lookup.ServiceDiscoveryManager; 
import net.jini.lookup.ServiceDiscoveryListener; 
import net.jini.discovery.DiscoveryManagement; 
import net.jini.discovery.LookupDiscoveryManager; 
import net.jini.lookup.ServiceDiscoveryEvent; 

import message.MessageStream; 

import java.io.BufferedReader; 
import java.io.EOFException; 
import java.io.InputStreamReader; 
import java.io.Reader; 
import java.lang.reflect.Constructor; 
import java.lang.reflect.InvocationTargetException; 
import java.rmi.RemoteException; 
import java.rmi.RMISecurityManager; 
import java.util.HashSet; 
import java.util.LinkedList; 
import java.util.List; 
import java.util.Set; 
import java.util.StringTokenizer; 

/** 
 * This class provides a client that reads messages from a 
 * <code>MessageStream</code> service. It's use is: 
 * <pre> 
 *      java [<i>java-options</i>] client.StreamReader [-c <i>count</i>] 
 *              <i>groups|lookupURL</i> 
 *              [<i>service-type</i>|<i>attribute</i> ...] 
 * </pre> 
 * Where the options are: 
 * <dl> 
 * <dt><i><CODE>java-options</CODE></i> 
 * <dd>Options to the Java VM that will run the admin program. Typically 
 * this includes a security policy property. 
 * <p> 
 * <dt><i><CODE>-c <i>count</i></CODE></i> 
 * <dd>The number of messages to print. 
 * <p> 
 * <dt><i><CODE>groups</CODE></i>|<i><CODE>lookupURL</CODE></i> 
 * <dd>Either a comma-separated list of groups in which all the services 
 * in the group will be registered or a URL to a specific lookup service. 
 * <p> 
 * <dt><i><CODE>service-type</CODE></i>|<i><CODE>attribute</CODE></i> 
 * <dd>A combination (in any order) of service types and attribute definitions. 
 * Service types are specified as types that the service must be an instance of. 
 * Attribute definitions are either <CODE>Entry</CODE> type names, 
 * which declare that the service must have an attribute of that type, 
 * or <CODE>Entry</CODE> type names with a single <CODE>String</CODE> 
 * parameter for the constructor, as in 
 * <CODE><i>AttributeType</i>:<i>stringArg</i></CODE>. 
 * </dl> 
 * <p>The lookups are searched for a <CODE>MessageStream</CODE> that 
 * supports any additional service types specified and that matches all 
 * specified attributes. If one is found, then <CODE><i>count</i></CODE> 
 * messages are printed from it. If a <CODE>RemoteException</CODE> 
 * occurs the <CODE>nextMessage</CODE> invocation is retried up to 
 * a maximum number of times. 
 * <P> 
 * This class is designed to be subclassed. As an example, see 
 * <CODE>chatter.Chatter</CODE>. 
 * 
 * @see message.MessageStream 
 * @see chatter.Chatter 
 */ 
public class StreamReader 
    implements ServiceDiscoveryListener 
{
    /** 
     * The number of messages to print. 
     */ 
    private int count; 

    /** 
     * The lookup groups (or an empty array). 
     */ 
    private String[] groups = new String[0]; 

    /** 
     * The lookup URL (or <code>null</code>). 
     */ 
    private String lookupURL; 

    /** 
     * The stream and attribute types. 
     */ 
    private String[] typeArgs; 

    /** 
     * How long to wait for matches before giving up. 
     */ 
    private final static int MAX_WAIT = 5000;   // five seconds 
    /** 
     * Maximum number of retries of <code>nextMessage</code>. 
     */ 
    private final static int MAX_RETRIES = 5; 

    /** 
     * Run the program. 
     * 
     * @param args      The command-line arguments 
     * 
     * @see #StreamReader 
     */ 
    public static void main(String[] args) throws Exception 
    {
        StreamReader reader = new StreamReader(args); 
        reader.execute(); 
    } 

    /** 
     * Create a new <code>StreamReader</code> object from the 
     * given command line arguments. 
     */ 
    public StreamReader(String[] args) {
        // parse command into the fields count, groups, 
        // lookupURL, and typesArgs... 
        if (args.length == 0) {
            usage(); 
            throw new IllegalArgumentException(); 
        } 

        int start; 
        if (!args[0].equals("-c")) {
            count = 1; 
            start = 0; 
        } else {
            count = Integer.parseInt(args[1]); 
            start = 2; 
        } 

        if (args[start].indexOf(':') < 0) 
            groups = util.ParseUtil.parseGroups(args[start]); 
        else 
            lookupURL = args[start]; 
        typeArgs = new String[args.length - start - 1]; 
        System.arraycopy(args, start + 1, typeArgs, 0, typeArgs.length); 
    } 

    /** 
     * Print out a usage message. 
     */ 
    private void usage() {
        System.err.println("usage: java [java-options] " + StreamReader.class + 
            " [-c count] groups|lookupURL [service-type|attribute ...]"); 
    } 

    /** 
     * Execute the program by consuming messages. This spawns a 
     * <CODE>ServiceDiscoveryManager</CODE> to watch for services 
     * of the right type, trying to read those that are found. Once 
     * the <CODE>ServiceDiscoveryManager</CODE> is listening, this 
     * thread sleeps up to the maximum time and then exits with an error, 
     * since <CODE>serviceAdded</CODE> will exit first if it succeeds. 
     * 
     * @see #serviceAdded 
     */ 
    public void execute() throws Exception {
        if (System.getSecurityManager() == null) 
            System.setSecurityManager(new RMISecurityManager()); 

        LookupLocator[] locators = null; 
        if (lookupURL != null) {
            LookupLocator loc = new LookupLocator(lookupURL); 
            locators = new LookupLocator[] { loc }; 
        } 

        DiscoveryManagement dm =        // lookups to search 
            new LookupDiscoveryManager(groups, locators, null); 
        ServiceDiscoveryManager sdm =   // services to look for 
            new ServiceDiscoveryManager(dm, null); 
        ServiceTemplate serviceTmpl = buildTmpl(typeArgs); 
        sdm.createLookupCache(serviceTmpl, null, this); 

        Thread.sleep(MAX_WAIT); 
        exit(1, "No service found"); 
    } 

    /** 
     * Invoked by <CODE>ServiceDiscoveryManager</CODE> when it 
     * finds a service that matches our template. We try to 
     * use it, exiting successfully if it works. Otherwise we 
     * return and wait for the next service. This is synchronized 
     * so that nobody can exit while we're trying out a service, since 
     * <CODE>exit</CODE> is also synchronized. Otherwise we could be 
     * in the middle of reading the stream when the timer in 
     * <CODE>execute</CODE> went off. 
     * 
     * @see #execute 
     * @see #exit 
     */ 
    public synchronized void 
        serviceAdded(ServiceDiscoveryEvent ev) 
    {
        ServiceItem si = ev.getPostEventServiceItem(); 
        try {
            readStream((MessageStream) si.service); 
            exit(0, null); 
        } catch (RemoteException e) {
            return;     // ignore this one, try for another 
        } 
    } 

    // stub these out -- we don't need them 
    public void serviceChanged(ServiceDiscoveryEvent ev) { } 
    public void serviceRemoved(ServiceDiscoveryEvent ev) { } 

    /** 
     * Build up a <code>ServiceTemplate</code> object for 
     * matching based on the types listed on the command line. 
     */ 
    private ServiceTemplate buildTmpl(String[] typeNames) 
        throws ClassNotFoundException, IllegalAccessException, 
               InstantiationException, NoSuchMethodException, 
               InvocationTargetException 
    {
        Set typeSet = new HashSet();    // service types 
        Set attrSet = new HashSet();    // attribute objects 

        // MessageStream class is always required 
        typeSet.add(MessageStream.class); 

        for (int i = 0; i < typeNames.length; i++) {
            // break the type name up into name and argument 
            StringTokenizer tokens =    // breaks up string 
                new StringTokenizer(typeNames[i], ":"); 
            String typeName = tokens.nextToken(); 
            String arg = null;          // string argument 
            if (tokens.hasMoreTokens()) 
                arg = tokens.nextToken(); 
            Class cl = Class.forName(typeName); 

            // test if it is a type of Entry (an attribute) 
            if (Entry.class.isAssignableFrom(cl)) 
                attrSet.add(attribute(cl, arg)); 
            else 
                typeSet.add(cl); 
        } 

        // create the arrays from the sets 
        Entry[] attrs = (Entry[]) 
        attrSet.toArray(new Entry[attrSet.size()]); 
    Class[] types = (Class[]) 
        typeSet.toArray(new Class[typeSet.size()]); 

    return new ServiceTemplate(null, types, attrs); 
} 

/** 
 * Create an attribute from the class name and optional argument. 
 */ 
private Object attribute(Class cl, String arg) 
    throws IllegalAccessException, InstantiationException, 
           NoSuchMethodException, InvocationTargetException 
{
    if (arg == null) 
        return cl.newInstance(); 
    else {
        Class[] argTypes = new Class[] { String.class }; 
        Constructor ctor = cl.getConstructor(argTypes); 
        Object[] args = new Object[] { arg }; 
        return ctor.newInstance(args); 
    } 
} 

/** 
 * Read the required number of messages from the given stream. 
 */ 
public void readStream(MessageStream stream) 
    throws RemoteException 
{
    int errorCount = 0;     // # of errors seen this message 
    int msgNum = 0;         // # of messages 
    while (msgNum < count) {
        try {
            Object msg = stream.nextMessage(); 
            printMessage(msgNum, msg); 
            msgNum++;               // successful read 
            errorCount = 0;         // clear error count 
        } catch (EOFException e) {
            System.out.println("---EOF---"); 
            break; 
        } catch (RemoteException e) {
            e.printStackTrace(); 
            if (++errorCount > MAX_RETRIES) {
                if (msgNum == 0)    // got no messages 
                    throw e; 
                else 
                    exit(1, "too many errors"); 
            } 
            try {
                    Thread.sleep(1000); // wait 1 second, retry 
                } catch (InterruptedException ie) {
                    exit(1, "Interrupted"); 
                } 
            } 
        } 
    } 

    private synchronized void exit(int status, String msg) {
        if (msg != null) 
            System.err.println(msg); 
        System.exit(status); 
    } 

    /** 
     * Print out the message in a reasonable format. 
     */ 
    public void printMessage(int msgNum, Object msg) {
        if (msgNum > 0) // print separator 
            System.out.println("---"); 
        System.out.println(msg); 
    } 
} 

    package fortune; 

import message.MessageStream; 

import java.io.DataOutputStream; 
import java.io.File; 
import java.io.FileOutputStream; 
import java.io.IOException; 
import java.io.RandomAccessFile; 
import java.util.ArrayList; 
import java.util.List; 

import java.rmi.activation.ActivationException; 

/** 
 * Administer a <code>FortuneStreamImpl</code>. 
 * <pre> 
 *      java [<i>java options</i>] fortune.FortuneAdmin <i>database-dir</i> 
 * </pre> 
 * The database is initialized from the fortune set in the directory's 
 * <code>fortunes</code> file, creating a file named <code>pos</code> that 
 * contains each fortune's starting position. The <code>fortunes</code> 
 * file must be present. The <code>pos</code> file, if it exists, will 
 * be overwritten. 
 * 
 * @see FortuneStreamImpl 
 */ 
public class FortuneAdmin {
    /** 
     * Run the FortuneAdmin utility. The class comment describes the 
     * possibilities. 
     * 
     * @param args 
     *          The arguments passed on the command line 
     * 
     * @see FortuneAdmin 
     */ 
    public static void main(String[] args) throws Exception {
        if (args.length != 1) 
            usage(); 
        else 
            setup(args[0]); 
    } 

    /** 
     * Set up a directory, reading its <code>fortunes</code> file and 
     * creating a correct <code>pos</code> file. 
     * 
     * @param dir 
     *          The fortune database directory. 
     * @throws java.io.IOException 
     *          Some error accessing the database files. 
     */ 
    private static void setup(String dir) throws IOException {
        File fortuneFile = new File(dir, "fortunes"); 
        File posFile = new File(dir, "pos"); 
        if (posFile.exists() && 
            posFile.lastModified() > fortuneFile.lastModified()) 
        {
            System.out.println("positions up to date"); 
            return; 
        } 

        System.out.print("positions out of date, updating"); 
        // Open the fortunes file 
        RandomAccessFile fortunes = 
            new RandomAccessFile(new File(dir, "fortunes"), "r"); 

        // Remember the start of each fortune 
        List positions = new ArrayList(); 
        positions.add(new Long(0)); 
        String line; 
        while ((line = fortunes.readLine()) != null) 
            if (line.startsWith("%%")) 
                positions.add(new Long(fortunes.getFilePointer())); 
        fortunes.close(); 

        // Write the pos file 
        DataOutputStream pos = 
            new DataOutputStream(new FileOutputStream(new File(dir, "pos"))); 
        int size = positions.size(); 
        pos.writeLong(size); 
        for (int i = 0; i < size; i++) 
            pos.writeLong(((Long) positions.get(i)).longValue()); 
        pos.close(); 
        System.out.println(); 
    } 

    /** 
     * Print out a usage message. 
     */ 
    private static void usage() {
        System.out.println("usage: java [java-options] " + FortuneAdmin.class + 
            " database-dir"); 
    } 
} 

    package fortune; 

import message.MessageStream; 

import java.rmi.Remote; 
import java.rmi.RemoteException; 

/** 
 * A <CODE>FortuneStream</CODE> is a <CODE>MessageStream</CODE> whose 
 * <CODE>nextMessage</CODE> method returns a random saying on some theme. 
 * The theme is returned by the <CODE>getTheme</CODE> method. 
 * 
 * @see FortuneTheme 
 */ 
interface FortuneStream extends MessageStream, Remote {
    /** 
     * Return the theme of the stream. This is also represented in the 
     * lookup service as a <CODE>FortuneTheme</CODE> object. 
     */ 
    String getTheme() throws RemoteException; 
} 

    package fortune; 

import message.MessageStream; 
import util.ParseUtil; 

import net.jini.core.discovery.LookupLocator; 
import net.jini.core.entry.Entry; 

import net.jini.discovery.DiscoveryManagement; 
import net.jini.discovery.LookupDiscoveryManager; 
import net.jini.lease.LeaseRenewalManager; 
import net.jini.lookup.JoinManager; 
import net.jini.lookup.ServiceIDListener; 

import java.io.BufferedInputStream; 
import java.io.DataInputStream; 
import java.io.DataOutputStream; 
import java.io.EOFException; 
import java.io.File; 
import java.io.FileInputStream; 
import java.io.IOException; 
import java.io.RandomAccessFile; 
import java.rmi.Remote; 
import java.rmi.RMISecurityManager; 
import java.rmi.server.UnicastRemoteObject; 
import java.util.Random; 

/** 
 * Implement a <code>MessageStream</code> whose 
 * <code>nextMessage</code> method returns ''fortune cookie'' selected 
 * at random. The stream is an activatable remote object. It requires 
 * no special proxy because there is no client-side state or smarts --
 * the simple RMI stub works perfectly for this use. 
 * 
 * <code>FortuneStreamImpl</code> objects are created using the 
 * <code>create</code>. It's only public constructor is designed for 
 * use by the activation system itself. The class 
 * <code>FortuneAdmin</code> provides a program that will invoke 
 * <code>create</code>. 
 * 
 * @see FortuneAdmin 
 */ 
public class FortuneStreamImpl implements FortuneStream {
    /** 
     * Groups to register with (or an empty array). 
     */ 
    private String[] groups = new String[0]; 

    /** 
     * URL to specific join manager (or <CODE>null</CODE>). 
     */ 
    private String lookupURL; 

    /** 
     * The directory we work in. 
     */ 
    private String dir; 

    /** 
     * The theme of this stream. 
     */ 
    private String theme; 

    /** 
     * The random number generator we use. 
     */ 
    private Random random = new Random(); 

    /** 
     * The positions of the start of each fortune in the file. 
     */ 
    private long[] positions; 

    /** 
     * The file that contains the fortunes. 
     */ 
    private RandomAccessFile fortunes; 

    /** 
     * The join manager does most work required of services in Jini systems. 
     */ 
    private JoinManager joinMgr; 

    /** 
     * @param args      The command line arguments. 
     */ 
    public static void main(String[] args) throws Exception 
    {
        FortuneStreamImpl f = new FortuneStreamImpl(args); 
        f.execute(); 
    } 

    /** 
     * Create a stream that reads from the given directory. 
     * 
     * @param dir       The directory name. 
     */ 
    private FortuneStreamImpl(String args[]) 
        throws IOException 
    {
        // Set the groups, lookupURL, dir, and theme 
        // fields... 
        if (args.length != 3) {
            usage(); 
            throw new IllegalArgumentException(); 
        } 
        if (args[0].indexOf(':') < 0) 
            groups = util.ParseUtil.parseGroups(args[0]); 
        else 
            lookupURL = args[0]; 
        dir = args[1]; 
        theme = args[2]; 
    } 

    /** 
     * Print out a usage message. 
     */ 
    private void usage() {
        System.err.println("usage: java " + FortuneStreamImpl.class + 
            " groups|lookupURL database-dir theme"); 
    } 

    /** 
     * Export this service as a UnicastRemoteObject for debugging purposes. 
     * 
     * @see #main 
     */ 
    private void execute() throws IOException {
        System.setSecurityManager(new RMISecurityManager()); 
        UnicastRemoteObject.exportObject(this); 

        // set up the fortune database 
        setupFortunes(); 

        // set our FortuneTheme attribute 
        FortuneTheme themeAttr = new FortuneTheme(theme); 
        Entry[] initialAttrs = new Entry[] { themeAttr }; 

        LookupLocator[] locators = null; 
        if (lookupURL != null) {
            LookupLocator loc = new LookupLocator(lookupURL); 
            locators = new LookupLocator[] { loc }; 
        } 
        DiscoveryManagement dm = 
            new LookupDiscoveryManager(groups, locators, null); 
        joinMgr = new JoinManager(this, initialAttrs, 
            (ServiceIDListener) null, dm, null); 
    } 

    /** 
     * Called when the database needs to be set up. This can be called 
     * multiple times, for example if the database has been modified while 
     * the service is running. 
     * 
     * @throws java.io.IOException 
     *          Some problem occurred accessing the database files. 
     */ 
    private synchronized void setupFortunes() throws IOException {
        // Read in the position of each fortune 
        File posFile = new File(dir, "pos"); 
        DataInputStream in = new DataInputStream(
            new BufferedInputStream(new FileInputStream(posFile))); 
        int count = (int) in.readLong(); 
        positions = new long[count]; 
        for (int i = 0; i < positions.length; i++) 
            positions[i] = in.readLong(); 
        in.close(); 

        // Close the fortune file if previously opened 
        if (fortunes != null) 
            fortunes.close(); 
        // Open up the fortune file 
        fortunes = new RandomAccessFile(new File(dir, "fortunes"), "r"); 
    } 

    /** 
     * Return the next message from the stream. Since messages are 
     * selected at random, any message is as good as any other and so 
     * this is idempotent by contract: there will be no violation of 
     * the contract if the client calls it a second time after getting 
     * a <code>RemoteException</code>. The <CODE>Object</CODE> returned 
     * is a <CODE>String</CODE> with embeded newlines, but no trailing 
     * newline. 
     * 
     * @throws java.io.EOFException 
     *          The database has been corrupted -- no more messages 
     *          from this stream. 
     */ 
    public synchronized Object nextMessage() throws EOFException {
        try {
            int which = random.nextInt(positions.length); 
            fortunes.seek(positions[which]); 
            StringBuffer buf = new StringBuffer(); 
            String line; 
            while ((line = fortunes.readLine()) != null && !line.equals("%%")) {
                if (buf.length() > 0) 
                    buf.append('
'); 
                buf.append(line); 
            } 
            return buf.toString(); 
        } catch (IOException e) {
            throw new EOFException("directory not available:" + e.getMessage()); 
        } 
    } 

    // inherit doc comment from interface 
    public String getTheme() {
        return theme; 
    } 
} 

    package fortune; 

import net.jini.entry.AbstractEntry; 
import net.jini.lookup.entry.ServiceControlled; 

/** 
 * This class is used as an attribute in the lookup system to tell 
 * the user what theme of fortunes a stream generates. 
 */ 
public class FortuneTheme extends AbstractEntry 
    implements ServiceControlled 
{
    /** 
     * The serial version UID. Stating it explicitly allows future 
     * evolution with a guaranteed consistency of the UID itself. It 
     * is also more efficient since otherwise the UID must be calculated 
     * when the class is serialized. A good specification should include 
     * the serial version UID of each class. 
     */ 
    static final long serialVersionUID = 
                            -1696813496901296488L; 

    /** 
     * The theme of this collection of fortunes. 
     * 
     * @see fortune.FortuneStream#getTheme 
     * @serial 
     */ 
    public String theme; 

    /** 
     * Public no-arg constructor. Required for all <CODE>Entry</CODE> 
     * objects. 
     */ 
    public FortuneTheme() { } 

    /** 
     * Create a new <CODE>FortuneTheme</CODE> with the given theme. 
     */ 
    public FortuneTheme(String theme) {
        this.theme = theme; 
    } 
} 

    package message; 

import java.io.EOFException; 
import java.rmi.RemoteException; 

/** 
 * This interface defines a message stream service. Successive 
 * invocations of <code>nextMessage</code> return the next message in 
 * turn. Subinterfaces may add methods to rewind the stream or 
 * otherwise move around within the stream if appropriate. 
 */ 
public interface MessageStream {
    /** 
     * Return the next message in the stream. Each message is an 
     * object whose default method of display is a string returned by 
     * its <CODE>toString</CODE> method. This method is idempotent: if 
     * the client receives a <code>RemoteException</code>, the next 
     * invocation from the client should return an equivalent message. 
     * A service may specify which kinds of messages will be returned. 
     * 
     * @returns The next message as an <CODE>Object</CODE>. 
     * @throws  java.io.EOFException 
     *          The end of the stream has been reached. 
     * @throws  java.rmi.RemoteException 
     *          A remote exception has occurred. 
     */ 
    Object nextMessage() 
        throws EOFException, RemoteException; 
} 

    package util; 

import java.util.HashSet; 
import java.util.Set; 
import java.util.StringTokenizer; 

/** 
 * This class holds the static <CODE>parseGroups</CODE> method. 
 */ 
public class ParseUtil {
    /** 
     * Break up a comma-separated list of groups into an array of strings. 
     * 
     * @param groupDesc A comma-separated list of groups. 
     * @returns         An array of strings (empty if none were specified). 
     */ 
    public static String[] parseGroups(String groupDesc) {
        if (groupDesc.equals("")) 
            return new String[] {""}; 
        Set groups = new HashSet(); 
        StringTokenizer strs = new StringTokenizer(groupDesc, ", 	
"); 
        while (strs.hasMoreTokens()) 
            groups.add(strs.nextToken()); 
        return (String[]) groups.toArray(new String[groups.size()]); 
    } 
} 

“In my egotisitcal opinion, most people’s C programs should be indented six feet down and covered with dirt!”

——Blair P. Houghton, on C program indentation styles

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.89.30