/*
 * LunchServer
 *
 * the main server implementation class.
 *
 * Authors:  Steve Fritzinger and Brian Jeltema
 */

import net.jini.core.entry.*;
import net.jini.entry.*;
import net.jini.core.lookup.*;
import net.jini.lookup.*;
import net.jini.lookup.entry.*;
import net.jini.discovery.*;
import net.jini.core.discovery.*;
import net.jini.core.event.* ;
import net.jini.core.lease.* ;
import com.sun.jini.lookup.*;
import com.sun.jini.lease.*;
import com.sun.jini.lease.landlord.* ;
import java.io.*;
import java.rmi.*;
import java.rmi.server.*;
import java.util.*;
import java.net.* ;

public class LunchServer extends UnicastRemoteObject 
implements LunchServerInterface {

    private Hashtable lunchers;
    private ServiceID id;
    public LunchSocketServer lunchSocketServer ;
    long eventCounter = 0 ;
    private EventLandlord landlord ;

    // LunchSocketServer runs in the background. Every time a user connects,
    // a thread is dedicated to that user to process protocol messages.

    class LunchSocketServer implements Runnable {

        private final static int REGISTER = 1 ; 
        private final static int CHANGE   = 2 ; 
        private final static int GET      = 3 ; 
        private final static int CLEAR    = 4 ; 
        private final static int DUMP     = 5 ; 
        private final static int EVENTREG = 6 ;

        // CommandHandler is the per-user thread which parses and
        // generates the socket protocol commands

	class CommandHandler extends Thread {

	    Socket clientSocket ;

	    CommandHandler(Socket clientSocket) {
		super() ;
		this.clientSocket = clientSocket ;
	    }

// note - its a good idea to open output and flush before opening input
// to avoid a deadlock

	    public void run() {
		ObjectInputStream is = null ;
		ObjectOutputStream os = null ;
		int commandValue = 0 ;
		try {
		    os = new ObjectOutputStream(clientSocket.getOutputStream()) ;
                os.flush() ;
		    is = new ObjectInputStream(clientSocket.getInputStream()) ;
		    String name ;
		    String food ;

                while (true) {
			commandValue = is.readInt() ;
			switch (commandValue) {
			    case REGISTER:
				name = is.readUTF() ;
				food = is.readUTF() ;
				lunchers.put(name,food) ;
				sendEvents() ;
				break ;

			    case CHANGE:
				name = is.readUTF() ;
				food = is.readUTF() ;
				lunchers.remove(name);
				lunchers.put(name,food) ;
				sendEvents() ;
				break ;

			    case GET:
				os.writeInt(lunchers.size()) ;
				for (Enumeration enum = lunchers.keys() ; enum.hasMoreElements() ; ) {
				    String key = (String)enum.nextElement() ;
				    os.writeUTF(key) ;
				    os.writeUTF((String)lunchers.get(key)) ;
				}
				os.flush() ;
				break ;

			    case CLEAR:
				lunchers = new Hashtable() ;
				sendEvents() ;
				break ;

			    case DUMP:
				dump() ;
				break ;

                      case EVENTREG:
                        MarshalledObject mo = (MarshalledObject)is.readObject() ;
				long duration = is.readLong() ;
				EventRegistration r = registerListener((RemoteEventListener)mo.get(),duration) ;
				os.writeObject(new MarshalledObject(r)) ;
				break ;

			    default:
				System.out.println("Unrecognized protocol token") ;
				break ;
			}
		    }
		}
		catch (Exception e) {
                if (! (e instanceof EOFException || e instanceof SocketException)) // EOF is expected
		        System.out.println("SockServer exception: " + e) ;
		}
	    }
	}

        String ipAddress ;
        int port ;
        Thread handlerThread ;
        ServerSocket socket ;
       
        // create a server socket and start a thread to listen for
        // connection requests

        public LunchSocketServer() {
	    try {
		socket = new ServerSocket(0) ;
	    }
	    catch (Exception e) {
		System.out.println("exception opening socket: " + e) ;
	    }
	    handlerThread = new Thread(this) ;
	    handlerThread.start() ;
        }

	public int getPort() {
	    return socket.getLocalPort() ;
	}

	public void run() {
	    while (true) {
		try {
		    Socket clientSocket = socket.accept() ;
		    CommandHandler c = new CommandHandler(clientSocket) ;
		    c.start() ;
		}
		catch (Exception e) {
		    System.out.println("Exception in socket accept loop " + e) ;
		}
	    }
	}
    }

    // construct a LunchServer. Also starts the threads which supports
    // the socket protocol handlers and does all of the service joins

    public LunchServer () throws RemoteException 
    {
        super ();

        Entry[] aeAttributes;
        LunchCoordinator proxy ;

        // Create Hashtable which will store food preferences
        // by name of users

        lunchers = new Hashtable();
        lunchSocketServer = new LunchSocketServer() ;

        try {

	    // first, set up the leasing infrastructure

	    landlord = new EventLandlord() ;

          // now create the socket based protocol proxy using the port
	    // obtained lunchSocketServer

            proxy = new LunchServerSocketProxy(InetAddress.getLocalHost(),lunchSocketServer.getPort()) ;

	    // and join the socket protocol proxy into the djinn
	    

            aeAttributes = new Entry[1]; // create new Entry
            aeAttributes[0] = new Protocol("Socket");
            ServiceID id = getPersistantID("Socket") ;
            if (id == null) {
		new JoinManager(
		    proxy, 
		    aeAttributes, 
		    new IDNotifier("Socket"), 
		    new LeaseRenewalManager ()
		);
	    }
	    else {
		new JoinManager(
		    id,
		    proxy, 
		    aeAttributes, 
		    new String[]{""},
		    null,
		    new LeaseRenewalManager ()
		);
	    }
            System.out.println("Socket proxy join started.");

          // create the RMI proxy and Protocol attribute
          // Note that there is a subtle requirement to create a new
	    // entry attribute rather than reusing the one created
	    // earlier. Otherwise, both proxies are registered
	    // with the 'RMI' protocol attribute.

            aeAttributes = new Entry[1];
            aeAttributes[0] = new Protocol("RMI");
            proxy = new LunchServerProxy(this) ;

          // Use JoinManager helper class to join RMI proxy. 
	    // Though I don't save the JoinManager reference obtained
	    // by new here, in a real application typically the reference
	    // would be retained to manipulate attributes or groups, etc.

            id = getPersistantID("RMI") ;
            if (id == null) {
		new JoinManager(
		    proxy, 
		    aeAttributes, 
		    new IDNotifier("RMI"), 
		    new LeaseRenewalManager ()
		);
	    }
	    else {
		new JoinManager(
		    id,
		    proxy, 
		    aeAttributes, 
		    new String[]{""},
		    null,
		    new LeaseRenewalManager ()
		);
	    }
            System.out.println("RMI proxy join started.");

        } catch (Exception e) {
            System.out.println("server: LunchServer.main(): Exception " + e);
        }
    }

    // Register a lunchers food preference
    public void registerPref(String name, String food)
    throws RemoteException  // LunchServiceInterface
    {
        lunchers.put(name, food);
	sendEvents() ;
    }

    // Change a lunchers food preference
    public void changePref(String name, String food)
    throws RemoteException  // LunchServiceInterface
    {
        lunchers.remove(name);
        lunchers.put(name, food);
	sendEvents() ;
    }

    // Administrative interface for cleaning out store of lunchers
    public void clearServer()
    throws RemoteException  // LunchServiceInterface
    {
        lunchers = new Hashtable();
	sendEvents() ;
    }

    // Administrative interface for listing store of lunchers
    public void dump()
    throws RemoteException
    {
        Enumeration enum;
        String name;

        System.out.println();
        enum = lunchers.keys();
        while (enum.hasMoreElements()){
            name = (String) enum.nextElement();
            System.out.println(name + " " + lunchers.get(name));
        }
    }

    // Returns the list of lunchers and their food preferences
    public Hashtable getLunchers()
    throws RemoteException  // LunchServiceInterface
    {
        return lunchers;
    }

// The service ID listener, called when a service ID has been assigned
// to a new service. The service is responsible for
// persisting this information and reusing it on subsequent service starts.
// Here is persist it in /tmp, kind of an oxymoron

    protected class IDNotifier implements ServiceIDListener,Runnable {
	String protocol ;
	ServiceID id ;
	
	public IDNotifier(String protocol) {
	    this.protocol = protocol ;
	}

	public void serviceIDNotify (ServiceID idIn)      // ServiceIDListener
	{
	    id = idIn ;
	    Thread t = new Thread(this) ;
	    t.start() ;
	    System.out.println("Service registered for " + protocol + " protocol") ;
	}

        public void run() {
	    try {
		FileOutputStream fs = new FileOutputStream("/tmp/" + protocol + ".id") ;
		ObjectOutputStream os = new ObjectOutputStream(fs) ;
		os.writeObject(id) ;
		os.close() ;
		System.out.println("ServiceID persisted for " + protocol + " protocol") ;
	    }
	    catch (Exception e) {
		System.out.println("ServiceIDListener exception: " + e) ;
	    }
	}
    }

    public ServiceID getPersistantID(String protocol) {
	String name = "/tmp/" + protocol + ".id" ;
	File idFile = new File(name) ;
	ServiceID id = null ;
	try {
	    if (idFile.exists()) {
		FileInputStream fs = new FileInputStream(idFile) ;
		ObjectInputStream is = new ObjectInputStream(fs) ;
		id = (ServiceID)is.readObject() ;
		is.close() ;
		System.out.println("ServiceID retrieved for " + protocol + " protocol") ;
	    }
	}
	catch (Exception e) {
	    System.out.println("getPersistantID exception: " + e) ;
	}
	return id ;
    }

    public EventRegistration registerListener(RemoteEventListener listener,long duration) throws RemoteException {
        Lease lease = null ;
        try {
            lease = landlord.reserveListener(listener,duration) ;
        }
        catch (LeaseDeniedException e) {
	    System.out.println("Lease request denied") ;
	    return null ;
	}
	EventRegistration reg = new EventRegistration(0L,LunchServer.this,lease,eventCounter) ;
	return reg ;
    }

// the events should be sent in a separate thread to decouple the event handling
// from the base protocol. The sockets protocol, especially, deadlocks otherwise

    protected void sendEvents() {
	Thread eventSender = new EventSender() ;
	eventSender.start() ;
    }

// note that an attempt to call a listener will throw an exception if that
// listener no longer exists, in which case I cancel its lease, which removes it
// from the list.

    protected class EventSender extends Thread {
        public void run() {
            try {
		RemoteEvent event = new RemoteEvent(LunchServer.this,0L,eventCounter++,new MarshalledObject(lunchers)) ;
                for (Enumeration enum=landlord.getEnumeration() ; enum.hasMoreElements() ; ) {
		    EventResource r = (EventResource)enum.nextElement() ;
		    RemoteEventListener l = r.getListener() ;
                    try {
		        l.notify(event) ;
                    }
                    catch (Exception e) {
		        landlord.cancel(r.getCookie()) ;
                    }
		}
	    }
	    catch (Exception e) {
                System.out.println("EventSender exception: " + e) ;
	    }
	}
    }

    public static void main (String[] args)
    {
        try {

            if (System.getSecurityManager() == null)
                System.setSecurityManager (new RMISecurityManager ());
            new LunchServer ();

        } catch (Exception e) {
            System.out.println("server: LunchServer.main(): Exception " + e);
        }
    }
}

