/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.sun.jini.test.share;

import com.sun.jini.qa.harness.QAConfig;
import com.sun.jini.qa.harness.AdminManager;
import com.sun.jini.qa.harness.TestException;

import com.sun.jini.start.HTTPDStatus;
import com.sun.jini.start.ServiceStarter;

import com.sun.jini.thread.TaskManager;

import com.sun.jini.discovery.ClientSubjectChecker;
import com.sun.jini.discovery.Discovery;
import com.sun.jini.discovery.DiscoveryConstraints;
import com.sun.jini.discovery.DiscoveryProtocolException;
import com.sun.jini.discovery.EncodeIterator;
import com.sun.jini.discovery.MulticastAnnouncement;
import com.sun.jini.discovery.MulticastRequest;
import com.sun.jini.discovery.UnicastResponse;

import net.jini.discovery.Constants;
import net.jini.discovery.IncomingMulticastRequest;
import net.jini.discovery.IncomingUnicastRequest;
import net.jini.discovery.OutgoingMulticastAnnouncement;
import net.jini.discovery.OutgoingUnicastResponse;

import net.jini.core.constraint.MethodConstraints;
import net.jini.core.discovery.LookupLocator;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEventListener;
import net.jini.core.lookup.ServiceID;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceMatches;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceRegistration;
import net.jini.core.lookup.ServiceTemplate;

import com.sun.jini.test.services.lookupsimulator.LookupSimulatorProxyInterface;

import net.jini.constraint.BasicMethodConstraints;
import net.jini.io.UnsupportedConstraintException;
import net.jini.config.Configuration;
import net.jini.config.NoSuchEntryException;
import net.jini.config.ConfigurationException;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import com.sun.jini.config.Config;
import net.jini.security.Security;
import net.jini.security.AuthenticationPermission;

import java.lang.reflect.Array;

import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.BindException;

import java.nio.ByteBuffer;
import java.nio.BufferUnderflowException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

import java.rmi.activation.ActivationGroupDesc;
import java.rmi.activation.ActivationGroupDesc.CommandEnvironment;
import java.rmi.activation.ActivationGroup;
import java.rmi.activation.ActivationGroupID;
import java.rmi.activation.ActivationDesc;
import java.rmi.activation.Activatable;
import java.rmi.activation.ActivationException;
import java.rmi.MarshalledObject;
import java.rmi.RemoteException;

import java.security.Permission;
import java.security.Principal;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.StringTokenizer;

import java.util.logging.Logger;
import java.util.logging.Level;

import javax.security.auth.login.LoginContext;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import java.security.PrivilegedExceptionAction;
import java.security.PrivilegedActionException;


/**
 * Instances of this class provide general-purpose functions related to the
 * discovery protocol. This utility class is intended to be useful to all
 * categories of tests that wish to simulate the multicast and unicast
 * message exchange between a client or service and a lookup service -- 
 * from the point of view of the lookup service. (For provide the
 * analogous functionality from the point of view of the client,
 * use the <code>net.jini.discovery.LookupDiscovery</code> and the
 * <code>net.jini.discovery.LookupLocatorDiscovery</code> utilities.
 *
 * @see net.jini.core.discovery.LookupLocator
 *
 * @see net.jini.discovery.IncomingMulticastAnnouncement
 * @see net.jini.discovery.IncomingMulticastRequest
 * @see net.jini.discovery.IncomingUnicastRequest
 * @see net.jini.discovery.IncomingUnicastResponse
 *
 * @see net.jini.discovery.OutgoingMulticastAnnouncement
 * @see net.jini.discovery.OutgoingMulticastRequest
 * @see net.jini.discovery.OutgoingUnicastRequest
 * @see net.jini.discovery.OutgoingUnicastResponse
 */
public class DiscoveryProtocolSimulator {

    /** Maximum minMax lease duration for both services and events */
    private static final long MAX_LEASE = 1000L * 60 * 60 * 24 * 365 * 1000;
    /** Maximum minimum renewal interval */
    private static final long MAX_RENEW = 1000L * 60 * 60 * 24 * 365;
    /** Default maximum size of multicast packets to send and receive */
    private static final int DEFAULT_MAX_PACKET_SIZE = 512;
    /** Default time to live value to use for sending multicast packets */
    private static volatile int DEFAULT_MULTICAST_TTL;
    /** Default timeout to set on sockets used for unicast discovery */
    private static final int DEFAULT_SOCKET_TIMEOUT = 1*60*1000;

    private static Logger logger = Logger.getLogger("com.sun.jini.qa.harness");

    private volatile InetAddress thisInetAddress = null;
    /** Current number of multicast announcements sent by this class */
    private volatile int nAnnouncements = 0;

    /** Port for unicast discovery 
     * lockLookupLocator for synch
     */
    private volatile int unicastPort = 0;
    /** The locator to send */
    private volatile LookupLocator lookupLocator = null;
    /** The member groups to send */
    private volatile String[] memberGroups = {};

    /** Thread to receive/process multicast requests from client or service */
    final MulticastThread multicastRequestThread;
    /** Thread to receive/process unicast requests from client or service 
     * write by lockLookupLocator synch
     * read by volatile
     */
    volatile UnicastThread unicastRequestThread;
    /** Thread to send multicast announcements to from client or service */
    final AnnounceThread multicastAnnouncementThread;

    /** Task manager for sending events and discovery responses */
    private final TaskManager taskMgr = new TaskManager(10, 1000 * 15, 1.0f);

    /** Proxy for the "fake" lookup service that is sent */
    private volatile LookupSimulatorProxyInterface lookupProxy;
    /** The service ID to assign to the lookup service that is sent */
    private volatile ServiceID lookupServiceID = null;

    /** Socket timeout for unicast discovery request processing */
    private volatile int unicastTimeout =
	Integer.getInteger("com.sun.jini.reggie.unicastTimeout",
			   1000 * 60).intValue();
    /* For synchronization, instead of ReadersWriter locks used by reggie */
    private final Object lockNAnnouncements = new Object();
    private final Object lockLookupProxy    = new Object();
    private final Object lockLookupLocator  = new Object();
    private final Object lockMemberGroups   = new Object();

    /* new fields taken from the davis reggie */

    /** Network interfaces to use for multicast discovery */
    private volatile NetworkInterface[] multicastInterfaces;
    /** Flag indicating whether network interfaces were explicitly specified */
    private volatile boolean multicastInterfacesSpecified;
    private volatile Discovery protocol2;
    /** Constraints specified for incoming multicast requests */
    private volatile DiscoveryConstraints multicastRequestConstraints;
    /** Constraints specified for outgoing multicast announcements */
    private volatile DiscoveryConstraints multicastAnnouncementConstraints;
    /** Constraints specified for handling unicast discovery */
    private volatile DiscoveryConstraints unicastDiscoveryConstraints;
    /** Client subject checker to apply to incoming multicast requests */
    private volatile ClientSubjectChecker multicastRequestSubjectChecker;
    /** Client subject checker to apply to unicast discovery attempts */
    private volatile ClientSubjectChecker unicastDiscoverySubjectChecker;
    /** Interval to wait in between sending multicast announcements */
    private volatile long multicastAnnouncementInterval = 1000 * 60 * 2;


    public DiscoveryProtocolSimulator(QAConfig config,
                                      String[] memberGroups,
				      AdminManager manager)
                                       throws ActivationException, IOException, TestException
    {
        this(config,memberGroups,manager,0);
    }//end constructor
    
    public DiscoveryProtocolSimulator(QAConfig config,
                                      String[] memberGroups,
				      AdminManager manager,
                                      int unicastPort)
                                       throws ActivationException, IOException, TestException
    {
        this.memberGroups = memberGroups;
        this.unicastPort  = unicastPort;
        // start LUS before switching identity to reggie
        lookupProxy = (LookupSimulatorProxyInterface) manager.startLookupService();
	LoginContext context = null;
	Configuration c = config.getConfiguration();
	try {
	    context = (LoginContext) c.getEntry("test",
						"reggieLoginContext", 
						LoginContext.class,
						null);
	} catch (ConfigurationException e) {
	    throw new RuntimeException("Bad configuration", e);
	}
	if (context == null) {
		init(config);
        } else {
	    try {
		Principal reggie = 
		    (Principal) c.getEntry("principal", "reggie", Principal.class);
		// allow the simulator, running as reggie, to authenticate as reggie
		// XXX Should the permission be obtained from the configuration???
		Security.grant(lookupProxy.getClass(), 
			       new Principal[]{reggie}, 
			       new Permission[] {
				   new AuthenticationPermission(
						 Collections.singleton(reggie),
						 Collections.singleton(reggie),
						 "connect")});
		context.login();
	    } catch (LoginException e) {
		throw new RuntimeException("LoginFailed", e);
	    } catch (ConfigurationException e) {
		throw new RuntimeException("Configuration error", e);
	    }
	    try {
		final QAConfig finalConfig = config;
		Subject.doAsPrivileged(context.getSubject(),
			     	new PrivilegedExceptionAction() {
				    public Object run() throws ActivationException, IOException {
					init(finalConfig);
				 	return null;
				    }
				},
				null);
	    } catch (PrivilegedActionException e) {
		Throwable t = e.getException();
		if (t instanceof ActivationException) {
		    throw (ActivationException) t;
		}
		if (t instanceof IOException) {
		    throw (IOException) t;
		}
	    }
	}
        // Create and Start remaining threads.
        multicastAnnouncementThread = new AnnounceThread();
        multicastRequestThread = new MulticastThread();
        multicastRequestThread.start();
        multicastAnnouncementThread.start();
    }//end constructor

    public void stopAnnouncements() {
        stopAnnouncements(null);
    }//end stopAnnouncements
    public void stopAnnouncements(String testname) {
        logger.log(Level.FINE, "   stopAnnouncements entered");
        /* terminate all daemons */
        unicastRequestThread.interrupt();
	multicastRequestThread.interrupt();
	multicastAnnouncementThread.interrupt();
        logger.log(Level.FINE, "     interrupted all threads");
	taskMgr.terminate();
        logger.log(Level.FINE, "     terminated task manager");
	try {
            logger.log(Level.FINE, "     unicastRequestThread.join()");
            unicastRequestThread.join();
            logger.log(Level.FINE, "     multicastRequestThread.join()");
            multicastRequestThread.join();
            logger.log(Level.FINE, 
                              "     multicastAnnouncementThread.join()");
            multicastAnnouncementThread.join();
        } catch (InterruptedException e) { }
        logger.log(Level.FINE, "     close all request sockets");
        closeRequestSockets(taskMgr.getPending());
        logger.log(Level.FINE, "   stopAnnouncements exited");
        Thread.currentThread().interrupt(); //Restore interrupt.
    }//end stopAnnouncements

//    public void destroyLookup() throws RemoteException {
//        lookupProxy.destroy();
//    }//end destroyLookup

    public int getNAnnouncementsSent() {
        synchronized(lockNAnnouncements) {
            return nAnnouncements;
        }//end sync
    }//end getNAnnouncementsSent

    public ServiceRegistrar getLookupProxy() {
        synchronized(lockLookupProxy) {
            return lookupProxy;
        }//end sync
    }//end getLookupProxy

    public LookupLocator getLookupLocator() {
        synchronized(lockLookupLocator) {
            return lookupLocator;
        }//end sync
    }//end getLookupLocator

    public String[] getMemberGroups() {
        synchronized(lockMemberGroups) {
            return memberGroups; // don't clone, never modified once created
        }//end sync
    }//end getMemberGroups

    public void addMemberGroups(String[] groups) throws RemoteException {
        String[] tmpArray = null;
        /* Change the member groups locally, then replace them remotely */
        synchronized(lockMemberGroups) {
            for (int i=0;i<groups.length;i++) {
                if (indexOf(memberGroups, groups[i]) < 0)
                    memberGroups = (String[])arrayAdd(memberGroups,groups[i]);
            }
            tmpArray = new String[memberGroups.length];
            System.arraycopy(memberGroups,0,tmpArray,0,memberGroups.length);
        }//end sync
        /* Replace the groups remotely - no remote calls in sync block*/
        lookupProxy.setMemberGroups(tmpArray);
        synchronized (multicastAnnouncementThread) {
            multicastAnnouncementThread.notify();
        }//end sync
    }//end addMemberGroups

    public void removeMemberGroups(String[] groups) throws RemoteException {
        String[] tmpArray = null;
        /* Change the member groups locally, then replace them remotely */
        synchronized(lockMemberGroups) {
            for (int i=0;i<groups.length;i++) {
                int j = indexOf(memberGroups, groups[i]);
                if (j >= 0) memberGroups = (String[])arrayDel(memberGroups,j);
            }
            tmpArray = new String[memberGroups.length];
            System.arraycopy(memberGroups,0,tmpArray,0,memberGroups.length);
        }//end sync
        /* Replace the groups remotely - no remote calls in sync block*/
        lookupProxy.setMemberGroups(tmpArray);
        synchronized (multicastAnnouncementThread) {
            multicastAnnouncementThread.notify();
        }//end sync
    }//end removeMemberGroups

    public void setMemberGroups(String[] groups) throws RemoteException {
        String[] tmpArray = null;
        /* Change the member groups locally, then replace them remotely */
        synchronized(lockMemberGroups) {
            memberGroups = (String[])removeDups(groups);
            tmpArray = new String[memberGroups.length];
            System.arraycopy(memberGroups,0,tmpArray,0,memberGroups.length);
        }//end sync
        /* Replace the groups remotely - no remote calls in sync block*/
        lookupProxy.setMemberGroups(memberGroups);
        synchronized (multicastAnnouncementThread) {
            multicastAnnouncementThread.notify();
        }//end sync
    }//end setMemberGroups

    public int getUnicastPort() {
        synchronized(lockLookupLocator) {
            return unicastPort;
        }//end sync
    }//end getUnicastPort

    public void setUnicastPort(int port) throws IOException {
	if (port == unicastPort) return;
        LookupLocator tmpLocator = null;
        synchronized(lockLookupLocator) {
            if(    (    (port == 0)
                     && (unicastRequestThread.port == Constants.discoveryPort))
                || (port == unicastRequestThread.port) )
            {
                unicastPort = port;
                return;
            }
            /* create a UnicastThread that listens on the new port */
            UnicastThread newUnicastRequestThread = new UnicastThread(port);
            /* terminate the current UnicastThread listening on the old port */
            unicastRequestThread.interrupt();
            try {
                unicastRequestThread.join();
            } catch (InterruptedException e) { 
                
            }
            /* start the UnicastThread listening on the new port */
            unicastRequestThread = newUnicastRequestThread;
            unicastRequestThread.start();
            unicastPort = port;
            lookupLocator = QAConfig.getConstrainedLocator(lookupLocator.getHost(),
                                                           unicastRequestThread.port);
            /* Equality (same port&host ==> lookupLocator.equals(tmpLocator) */
            tmpLocator = QAConfig.getConstrainedLocator(lookupLocator.getHost(),
                                                        unicastRequestThread.port);
        }//end sync
        /* Set unicast port remotely - no remote calls in sync block*/
        lookupProxy.setLocator(tmpLocator); // also sets unicastPort
        synchronized (multicastAnnouncementThread) {
            multicastAnnouncementThread.notify();
        }//end sync
    }//end setUnicastPort

    /** Multicast discovery request thread code. */
    private class MulticastThread extends Thread {
	/** Multicast socket to receive packets */
	private final MulticastSocket socket;

	/**
	 * Create a high priority daemon thread.  Set up the socket now
	 * rather than in run, so that we get any exception up front.
	 */
	public MulticastThread() throws IOException {
	    super("multicast request");
	    setDaemon(true);
	    if (multicastInterfaces != null && multicastInterfaces.length == 0)
	    {
		socket = null;
		return;
	    }
	    InetAddress requestAddr = Constants.getRequestAddress();
	    socket = new MulticastSocket(Constants.discoveryPort);
	    socket.setTimeToLive(
		multicastAnnouncementConstraints.getMulticastTimeToLive(
		    DEFAULT_MULTICAST_TTL));
	    if (multicastInterfaces != null) {
		Level failureLogLevel =
		    multicastInterfacesSpecified ? Level.WARNING : Level.FINE;
		for (int i = 0; i < multicastInterfaces.length; i++) {
		    try {
			socket.setNetworkInterface(multicastInterfaces[i]);
			socket.joinGroup(requestAddr);
		    } catch (IOException e) {
			logger.log(
			    failureLogLevel,
			    "exception configuring multicast interface", e);
		    }
		}
	    } else {
		// REMIND: tolerate failure here?
		socket.joinGroup(requestAddr);
	    }
	}

	/** True if thread has been interrupted */
	private boolean interrupted = false;

	/* This is a workaround for Thread.interrupt not working on
	 * MulticastSocket.receive on all platforms.
	 */
	public synchronized void interrupt() {
	    interrupted = true;
	    socket.close();
	}

	public synchronized boolean isInterrupted() {
	    return interrupted;
	}

	public void run() {
	    if (multicastInterfaces != null && multicastInterfaces.length == 0)
	    {
		return;
	    }
	    byte[] buf = new byte[
		multicastRequestConstraints.getMulticastMaxPacketSize(
		    DEFAULT_MAX_PACKET_SIZE)];
	    DatagramPacket dgram = new DatagramPacket(buf, buf.length);
	    while (!isInterrupted()) {
		try {
		    dgram.setLength(buf.length);
		    try {
			socket.receive(dgram);
		    } catch (NullPointerException e) {
			break; // workaround for bug 4190513
		    }
		    int pv;
		    try {
			pv = ByteBuffer.wrap(dgram.getData(),
					     dgram.getOffset(),
					     dgram.getLength()).getInt();
		    } catch (BufferUnderflowException e) {
			throw new DiscoveryProtocolException(null, e);
		    }
		    multicastRequestConstraints.checkProtocolVersion(pv);

		    MulticastRequest req = 
			getDiscovery(pv).decodeMulticastRequest(
			    dgram,
			    multicastRequestConstraints.
				getUnfulfilledConstraints(),
			    multicastRequestSubjectChecker);
		    if ((req.getGroups().length != 0 &&
			 !overlap(memberGroups, req.getGroups())) ||
			indexOf(req.getServiceIDs(), lookupServiceID) >= 0)
			continue;
		    logger.log(Level.FINE, "Received valid multicast for " + lookupLocator);
		    taskMgr.addIfNew(
			new AddressTask(InetAddress.getByName(req.getHost()),
					req.getPort()));
		} catch (InterruptedIOException ignore) {
		    break;
		} catch (DiscoveryProtocolException ignore) {
		    break;
		} catch (Exception e) {
		    if (isInterrupted()) {
			break;
		    }
		    logger.log(Level.FINE,
			       "exception receiving multicast request", e);
		}
	    }
	    socket.close();
	}
    }

    /** Unicast discovery request thread code. */
    private class UnicastThread extends Thread {
	/** Server socket to accepts connections on. */
	private final ServerSocket listen;
	/** Listen port */
	public final int port;

	/**
	 * Create a daemon thread.  Set up the socket now rather than in run,
	 * so that we get any exception up front.
	 */
	public UnicastThread(int port) throws IOException {
	    super("unicast request");
            ServerSocket listen = null;
	    setDaemon(true);
	    if (port == 0) {
		try {
		    listen = new ServerSocket(Constants.discoveryPort);
                } catch (IOException ex){
                    logger.log(Level.FINE, "Failed to bind to default port: " + Constants.discoveryPort, ex);
                }
	    } 
            if (listen == null) {
                try {
                    listen = new ServerSocket(port);
                } catch (BindException ex){
                    ex.fillInStackTrace();
                    throw new IOException("Failed to bind to default port: " + port, ex);
                }
	    }
            this.listen = listen;
	    this.port = listen.getLocalPort();
	}

	/** True if thread has been interrupted */
	private boolean interrupted = false;

	/* This is a workaround for Thread.interrupt not working on
	 * ServerSocket.accept on all platforms.  ServerSocket.close
	 * can't be used as a workaround, because it also doesn't work
	 * on all platforms.
	 */
	public synchronized void interrupt() {
	    interrupted = true;
	    try {
		(new Socket(InetAddress.getLocalHost(), port)).close();
	    } catch (IOException e) {
	    }
	}

	public synchronized boolean isInterrupted() {
	    return interrupted;
	}

	public void run() {
	    while (!isInterrupted()) {
		try {
		    Socket socket = listen.accept();
		    if (isInterrupted()) {
			try {
			    socket.close();
			} catch (IOException e) {
			    logger.log(Level.FINE,
				       "exception closing socket", e);
			}
			break;
		    }
		    logger.log(Level.FINE, "Adding socket task for " + lookupLocator);
		    taskMgr.add(new SocketTask(socket));
		} catch (InterruptedIOException e) {
		    break;
		} catch (Exception e) {
		    logger.log(Level.FINE, "exception listening on socket", e);
		}
		/* if we fail in any way, just forget about it */
	    }
	    try {
		listen.close();
	    } catch (IOException e) {
	    }
	}
    }

    /** Multicast discovery announcement thread code. */
    private class AnnounceThread extends Thread {
	/** Multicast socket to send packets on */
	private final MulticastSocket socket;

	private boolean interrupted = false;

	/* This is a workaround for Thread.interrupt not working due
	 * to the logging system sometimes throwing away InterruptedIOException
	 */
	public synchronized void interrupt() {
	    interrupted = true;
	    super.interrupt();
	}

	public synchronized boolean isInterrupted() {
	    return interrupted;
	}

	/**
	 * Create a daemon thread.  Set up the socket now rather than in run,
	 * so that we get any exception up front.
	 */
	public AnnounceThread() throws IOException {
	    super("discovery announcement");
	    setDaemon(true);
	    if (multicastInterfaces == null || multicastInterfaces.length > 0)
	    {
		socket = new MulticastSocket();
		socket.setTimeToLive(
		    multicastAnnouncementConstraints.getMulticastTimeToLive(
			DEFAULT_MULTICAST_TTL));
	    } else {
		socket = null;
	    }
	}

	public synchronized void run() {
	    if (multicastInterfaces != null && multicastInterfaces.length == 0)
	    {
		return;
	    }
	    try {
		while (!isInterrupted() && announce(memberGroups)) {
		    wait(multicastAnnouncementInterval);
		}
	    } catch (InterruptedException e) {
	    }
// disable this to allow simulation of disappearance of multicast announcements
//	    if (memberGroups.length > 0) 
//		announce(new String[0]);//send NO_GROUPS just before shutdown
	    socket.close();
	}

	/**
	 * Announce membership in the specified groups, and return false if
	 * interrupted, otherwise return true.
	 */
	private boolean announce(String[] groups) {
	    // REMIND: cache latest announcement to skip re-encoding
	    List packets = new ArrayList();
	    Discovery disco;
	    try {
		disco = getDiscovery(
		    multicastAnnouncementConstraints.chooseProtocolVersion());
	    } catch (DiscoveryProtocolException e) {
		throw new AssertionError(e);
	    }
	    EncodeIterator ei = disco.encodeMulticastAnnouncement(
		new MulticastAnnouncement(System.currentTimeMillis(),
					  lookupLocator.getHost(),
					  lookupLocator.getPort(),
					  groups,
					  lookupServiceID),
		multicastAnnouncementConstraints.getMulticastMaxPacketSize(
					  DEFAULT_MAX_PACKET_SIZE),
		multicastAnnouncementConstraints.getUnfulfilledConstraints());
	    while (ei.hasNext()) {
		try {
		    packets.addAll(Arrays.asList(ei.next()));
		} catch (Exception e) {
		// UnsupportedConstraintException is expected and normal
                    if (! (e instanceof UnsupportedConstraintException)) {
		        logger.log(Level.INFO,
			           "exception encoding multicast announcement", e);
                    }
		}
	    }
	    try {
	        logger.log(Level.FINE, "Sending packets from " + lookupLocator);
		send((DatagramPacket[])
		    packets.toArray(new DatagramPacket[packets.size()]));
		    synchronized(lockNAnnouncements) {
                        nAnnouncements++;
                    }
	    } catch (InterruptedIOException e) {
		return false;
	    } catch (IOException e) {
		logger.log(Level.INFO,
			   "exception sending multicast announcement", e);
	    }
	    return true;
	}

	/**
	 * Attempts to multicast the given packets on each of the configured
	 * network interfaces.
	 */
	private void send(DatagramPacket[] packets)
	    throws InterruptedIOException
	{
	    if (multicastInterfaces != null) {
		Level failureLogLevel =
		    multicastInterfacesSpecified ? Level.WARNING : Level.FINE;
		for (int i = 0; i < multicastInterfaces.length; i++) {
		    try {
			socket.setNetworkInterface(multicastInterfaces[i]);
			send(packets, failureLogLevel);
		    } catch (SocketException e) {
			logger.log(failureLogLevel,
				   "exception setting interface", e);
		    }
		}
	    } else {
		send(packets, Level.WARNING);
	    }
	}

	/**
	 * Attempts to multicast the given packets on the currently set network
	 * interface, logging failures at the specified logging level.
	 */
	private void send(DatagramPacket[] packets, Level failureLogLevel)
	    throws InterruptedIOException
	{
	    for (int i = 0; i < packets.length; i++) {
		try {
		    socket.send(packets[i]);
		} catch (InterruptedIOException e) {
		    throw e;
		} catch (IOException e) {
		    logger.log(failureLogLevel, "exception sending packet", e);
		}
	    }
	}
    }

    /** Returns Discovery instance implementing the given protocol version */
    private Discovery getDiscovery(int version)
	throws DiscoveryProtocolException
    {
	switch (version) {
	    case Discovery.PROTOCOL_VERSION_1:
		return Discovery.getProtocol1();
	    case Discovery.PROTOCOL_VERSION_2:
		return protocol2;
	    default:
		throw new DiscoveryProtocolException(
		    "unsupported protocol version: " + version);
	}
    }

    private final class AddressTask implements TaskManager.Task {

	/** The address */
	public final InetAddress addr;
	/** The port */
	public final int port;

	/** Simple constructor */
	public AddressTask(InetAddress addr, int port) {
	    this.addr = addr;
	    this.port = port;
	}

	public int hashCode() {
	    return addr.hashCode();
	}

	/** Two tasks are equal if they have the same address and port */
	public boolean equals(Object obj) {
	    if (!(obj instanceof AddressTask))
		return false;
	    AddressTask ua = (AddressTask)obj;
	    return addr.equals(ua.addr) && port == ua.port;
	}

	/** Connect and then process a unicast discovery request */
	public void run() {
	    try {
                logger.log(Level.FINE, "Responding to multicast with unicast from " + lookupLocator);
		respond(new Socket(addr, port));
	    } catch (IOException e) {
	    } catch (SecurityException e) {
	    }
	}

	/** No ordering */
	public boolean runAfter(List tasks, int size) {
	    return false;
	}
    }//end class AddressTask

    /** Socket for unicast discovery response. */
    private final class SocketTask implements TaskManager.Task {

	/** The socket */
	public final Socket socket;

	/** Simple constructor */
	public SocketTask(Socket socket) {
	    this.socket = socket;
	}

	/** Process a unicast discovery request */
	public void run() {
	    respond(socket);
	}

	/** No ordering */
	public boolean runAfter(List tasks, int size) {
	    return false;
	}
    }//end class SocketTask

    /** Process a unicast discovery request, and respond. */
    private void respond(Socket socket) {
	try {
	    socket.setSoTimeout(
		unicastDiscoveryConstraints.getUnicastSocketTimeout(
		    DEFAULT_SOCKET_TIMEOUT));

	    int pv = new DataInputStream(socket.getInputStream()).readInt();
	    unicastDiscoveryConstraints.checkProtocolVersion(pv);
	    getDiscovery(pv).handleUnicastDiscovery(
		new UnicastResponse(lookupLocator.getHost(),
				    lookupLocator.getPort(),
				    memberGroups,
				    lookupProxy),
		socket,
		unicastDiscoveryConstraints.getUnfulfilledConstraints(),
		unicastDiscoverySubjectChecker,
		Collections.EMPTY_LIST);
	    logger.log(Level.FINE, "Responded to unicast request for " + lookupLocator);

	} catch (Exception e) {
	    try {
		if (InetAddress.getLocalHost().equals(socket.getInetAddress())) {
		    logger.log(Level.FINE, 
			       "exception handling unicast discovery from "
			       + socket.getInetAddress() + ":" 
			       + socket.getPort(),
			       e);
		} else {
		    logger.log(Level.FINE, 
			       "Ignoring spurious request packet from " 
			       + socket.getInetAddress());
		}
	    } catch (UnknownHostException ue) {
		logger.log(Level.SEVERE, "Unknown host!", ue);
	    }
	} finally {
	    try {
		socket.close();
	    } catch (IOException e) {
		logger.log(Level.FINE, "exception closing socket", e);
	    }
	}
    }

    /** Close any sockets that were sitting in the task queue. */
    private void closeRequestSockets(ArrayList tasks) {
	for (int i = tasks.size(); --i >= 0; ) {
	    Object obj = tasks.get(i);
	    if (obj instanceof SocketTask) {
		try {
		    ((SocketTask)obj).socket.close();
		} catch (IOException e) {
		}
	    }
	}
    }//end closeRequestSockets

    /** Return a new array containing the elements of the given array
     *  plus the given element added to the end.
     */
    private static Object[] arrayAdd(Object[] array, Object elt) {
	int len = array.length;
	Object[] narray =
	    (Object[])Array.newInstance(array.getClass().getComponentType(),
					len + 1);
	System.arraycopy(array, 0, narray, 0, len);
	narray[len] = elt;
	return narray;
    }//end arrayAdd

    /** Return a new array containing all the elements of the given array
     *  except the one at the specified index.
     */
    private static Object[] arrayDel(Object[] array, int i) {
	int len = array.length - 1;
	Object[] narray =
	    (Object[])Array.newInstance(array.getClass().getComponentType(),
					len);
	System.arraycopy(array, 0, narray, 0, i);
	System.arraycopy(array, i + 1, narray, i, len - i);
	return narray;
    }//end ArrayDel

    /** Returns the first index of elt in the array, else -1. */
    private static int indexOf(Object[] array, Object elt) {
	return indexOf(array, array.length, elt);
    }//end indexOf

    /** Returns the first index of elt in the array if < len, else -1. */
    private static int indexOf(Object[] array, int len, Object elt) {
	for (int i = 0; i < len; i++) {
	    if (elt.equals(array[i]))
		return i;
	}
	return -1;
    }//end indexOf

    /** Return true if some object is an element of both arrays */
    private static boolean overlap(Object[] arr1, Object[] arr2) {
	for (int i = arr1.length; --i >= 0; ) {
	    if (indexOf(arr2, arr1[i]) >= 0)
		return true;
	}
	return false;
    }//end overlap

    /** Weed out duplicates. */
    private static Object[] removeDups(Object[] arr) {
	for (int i = arr.length; --i >= 0; ) {
	    if (indexOf(arr, i, arr[i]) >= 0)
		arr = arrayDel(arr, i);
	}
	return arr;
    }//end removeDups

    /**
     * Sends the given packet data on the given <code>MulticastSocket</code>
     * through each of the network interfaces corresponding to elements of
     * the given array of IP addresses. If the given array of IP addresses
     * is <code>null</code> or empty, then the data will be sent through only
     * the default network interface.
     *
     * @param mcSocket   the <code>MulticastSocket</code> on which the data
     *                   will be sent
     * @param packet     <code>DatagramPacket</code> array whose elements are
     *                   the data to send 
     * @param addresses  <code>InetAddress</code> array whose elements
     *                   represent the Internet Protocol (IP) addresses
     *                   corresponding to the network interfaces (NICs)
     *                   through which the multicast data should be sent
     *
     * @throws java.io.InterruptedIOException
     */
    private static void sendPacketByNIC(MulticastSocket mcSocket,
                                        DatagramPacket[] packet,
                                        InetAddress[] addresses)
                                                 throws InterruptedIOException
    {
        if( (addresses != null) && (addresses.length > 0) ) {
            for(int i=0;i<addresses.length;i++) {
                try {
                    mcSocket.setInterface(addresses[i]);
                } catch(SocketException e) {
                    continue;//skip to next interface address
                }
                sendPacket(mcSocket,packet);
            }//end loop
        } else {//use default interface
            sendPacket(mcSocket,packet);
        }//endif
    }//end sendPacketByNIC

    /**
     * Sends the given packet data on the given <code>MulticastSocket</code>
     * through the network interface that is currently set.
     *
     * @param mcSocket the <code>MulticastSocket</code> on which the data
     *                 will be sent
     * @param packet   <code>DatagramPacket</code> array whose elements are 
     *                 the data to send 
     *
     * @throws java.io.InterruptedIOException
     */
    private static void sendPacket(MulticastSocket mcSocket,
                                   DatagramPacket[] packet)
                                                throws InterruptedIOException
    {
        for(int i=0;i<packet.length;i++) {
            try {
                mcSocket.send(packet[i]);
            } catch(InterruptedIOException e) {
                throw e;
            } catch(IOException e) {
            }
        }//end loop
    }//end sendPacket

    /**
     * Retrieves and parses the <code>-Dnet.jini.discovery.interface</code>
     * system property, converting each parsed value to an instance of
     * <code>InetAddress</code>, and returning the results of each conversion
     * in an array.
     *
     * @return <code>InetAddress</code> array in which each element represents
     *         the Internet Protocol (IP) address corresponding to a network
     *         interface.
     *
     * @throws java.net.UnknownHostException
     */
    private static InetAddress[] getNICAddresses() throws UnknownHostException
    {
        String str = null;
	try {
	    str = System.getProperty("net.jini.discovery.interface");
	} catch (SecurityException e) { /* ignore */ }
        if (str == null) return null;
        InetAddress[] addrs = null;
        String delimiter = ",";
        StringTokenizer st = null;
        st = new StringTokenizer(str,delimiter);
        int n = st.countTokens();
        if (n > 0) {
            addrs = new InetAddress[n];
            for(int i=0;((st.hasMoreTokens())&&(i<n));i++) {
                addrs[i] = InetAddress.getByName(st.nextToken());
            }
            return addrs;
        } else {
            return addrs;
        }
    }//end getNICAddresses

    /* Note that the QAConfig is named qaConfig here to avoid cut/paste
     * screwups pulling davis configuration entries into the code, which
     * use the name 'config' for the Configuration object.
     */
    private void init(QAConfig qaConfig)
                                       throws ActivationException, IOException
    {
        if (qaConfig == null) throw new NullPointerException("QAConfig cannot be null");
        String host = System.getProperty("java.rmi.server.hostname");
        try {
            if (host == null) {
                host = InetAddress.getLocalHost().getHostName();
            }
            thisInetAddress = InetAddress.getByName(host);
        } catch (NullPointerException e){
            try {
                thisInetAddress = InetAddress.getByName(null);
                if (host == null){
                    host = thisInetAddress.getCanonicalHostName();
                }
            } catch (NullPointerException ex){
                ex.fillInStackTrace();
                throw new IOException("InetAddress host name resolution failed", ex);
            }
        }
        
        unicastRequestThread = new UnicastThread(unicastPort);
        lookupLocator = QAConfig.getConstrainedLocator(host, unicastRequestThread.port);
        /* start an activatable lookup service simulation */
        if (lookupServiceID == null) {
            lookupServiceID = lookupProxy.getServiceID();
        }
        if( (lookupProxy == null) || (lookupServiceID == null) ) {
            throw new ActivationException("failure creating lookup service");
        }
	// the code block was a noop for unicastPort > 0, because 
	// setUnicastPort does nothing if the argument is unicastPort
//          if(unicastPort > 0) {
//              /* Change the locator port for this lookup service. */
//              setUnicastPort(unicastPort);
//          } else {
//              /* Port is already set (randomly). need to set only the locator. */
//              lookupProxy.setLocator(lookupLocator);
//          }//endif
	// change to set unconditionally
        lookupProxy.setLocator(lookupLocator);

	/* add new configration entries from the davis reggie implementation */
	Configuration config = qaConfig.getConfiguration();
	MethodConstraints discoveryConstraints = null;
	try {
	    try {
	        multicastInterfaces = (NetworkInterface[]) config.getEntry(
	    	    "test", "multicastInterfaces", NetworkInterface[].class);
	        multicastInterfacesSpecified = true;
	    } catch (NoSuchEntryException e) {
	        List l = Collections.list(NetworkInterface.getNetworkInterfaces());
	        multicastInterfaces = (NetworkInterface[])
		    l.toArray(new NetworkInterface[l.size()]);
	        multicastInterfacesSpecified = false;
	    }
//	    multicastAnnouncementInterval = Config.getLongEntry(
//	        config, "test", "multicastAnnouncementInterval",
//	        multicastAnnouncementInterval, 1, Long.MAX_VALUE);
	    multicastAnnouncementInterval = 
	        qaConfig.getLongConfigVal("net.jini.discovery.announce", 120000);
	    discoveryConstraints = 
	        (MethodConstraints) config.getEntry(
		    "test", "discoveryConstraints",
		    MethodConstraints.class, null);
	    if (discoveryConstraints == null) {
	        discoveryConstraints = 
		    new BasicMethodConstraints(InvocationConstraints.EMPTY);
	    }
	    try {
	        multicastRequestSubjectChecker =
		    (ClientSubjectChecker) Config.getNonNullEntry(
		        config, "test", "multicastRequestSubjectChecker",
		        ClientSubjectChecker.class);
	    } catch (NoSuchEntryException e) {
	        // leave null
	    }
	    try {
	        unicastDiscoverySubjectChecker =
		    (ClientSubjectChecker) Config.getNonNullEntry(
		        config, "test", "unicastDiscoverySubjectChecker",
		        ClientSubjectChecker.class);
	    } catch (NoSuchEntryException e) {
	        // leave null
	    }
	} catch (ConfigurationException ce) {
	    throw new RuntimeException("Configuration error", ce);
	}
	protocol2 = Discovery.getProtocol2(null);
	multicastRequestConstraints = DiscoveryConstraints.process(
	    discoveryConstraints.getConstraints(
		DiscoveryConstraints.multicastRequestMethod));
	multicastAnnouncementConstraints = DiscoveryConstraints.process(
	    discoveryConstraints.getConstraints(
		DiscoveryConstraints.multicastAnnouncementMethod));
	unicastDiscoveryConstraints = DiscoveryConstraints.process(
	    discoveryConstraints.getConstraints(
		DiscoveryConstraints.unicastDiscoveryMethod));

	try {
            DEFAULT_MULTICAST_TTL = Config.getIntEntry(
	        config, "multicast", "ttl", 1, 0, 15);
	} catch (ConfigurationException ce) {
            DEFAULT_MULTICAST_TTL = 1;
	}

        /* start the discovery-related threads */
        
        /* start the threads */
        unicastRequestThread.start();
        
    }//end init

}//end class DiscoveryProtocolSimulator


