/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 1.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://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.html.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */
package net.jini.discovery;

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 com.sun.jini.logging.Levels;
import com.sun.jini.thread.TaskManager;
import com.sun.jini.thread.TaskManager.Task;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.MulticastSocket;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.rmi.RemoteException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.config.EmptyConfiguration;
import net.jini.config.NoSuchEntryException;
import net.jini.constraint.BasicMethodConstraints;
import net.jini.core.constraint.ConstraintAlternatives;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.MethodConstraints;
import net.jini.core.discovery.LookupLocator;
import net.jini.core.lookup.ServiceID;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.io.UnsupportedConstraintException;
import net.jini.security.BasicProxyPreparer;
import net.jini.security.ProxyPreparer;
import net.jini.security.Security;
import net.jini.security.SecurityContext;

/**
 * This class is a helper utility class that encapsulates the functionality 
 * required of an entity that wishes to employ multicast discovery to 
 * find lookup services located within the entity's "multicast radius" 
 * (roughly, the number of hops beyond which neither the multicast requests
 * from the entity, nor the multicast announcements from the lookup service,
 * will propagate). This class helps make the process of acquiring references
 * to lookup services - based on no information other than lookup service
 * group membership - much simpler for both services and clients.
 *
 * @com.sun.jini.impl <!-- Implementation Specifics -->
 *
 * The following implementation-specific items are discussed below:
 * <ul><li> <a href="#ldConfigEntries">Configuring LookupDiscovery</a>
 *     <li> <a href="#ldLogging">Logging</a>
 * </ul>
 *
 * <a name="ldConfigEntries">
 * <p>
 * <b><font size="+1">Configuring LookupDiscovery</font></b>
 * <p>
 * </a>
 * 
 * This implementation of <code>LookupDiscovery</code> supports the
 * following configuration entries; where each configuration entry name
 * is associated with the component name
 * <code>net.jini.discovery.LookupDiscovery</code>. Note that the
 * configuration entries specified here are specific to this implementation
 * of <code>LookupDiscovery</code>. Unless otherwise stated, each entry
 * is retrieved from the configuration only once per instance of this
 * utility, where each such retrieval is performed in the constructor.
 *
 * <a name="discoveryConstraints">
 * <table summary="Describes the discoveryConstraints
 *                configuration entry" border="0" cellpadding="2">
 *   <tr valign="top">
 *     <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
 *     <th scope="col" align="left" colspan="2"> <font size="+1">
 *     <code>discoveryConstraints</code></font>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Type: <td> {@link net.jini.core.constraint.MethodConstraints}
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Default: <td> <code>null</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Description:
 *       <td> Constraints to apply to the multicast request, multicast
 *         	announcement and unicast discovery protocols.  Multicast
 *         	request constraints are derived by calling
 *         	{@link MethodConstraints#getConstraints getConstraints} on the
 *         	obtained <code>MethodConstraints</code> instance with a
 *         	<code>Method</code> object for the
 *    {@link com.sun.jini.discovery.DiscoveryConstraints#multicastRequest
 *              multicastRequest} method; multicast announcement and unicast
 *              discovery constraints are similarly obtained by passing
 *              <code>Method</code> objects for the
 *    {@link com.sun.jini.discovery.DiscoveryConstraints#multicastAnnouncement
 *              multicastAnnouncement} and
 *    {@link com.sun.jini.discovery.DiscoveryConstraints#unicastDiscovery
 *              unicastDiscovery} methods, respectively.  A <code>null</code>
 *              value is interpreted as	mapping all methods to empty
 *              constraints.
 *         	<p>
 *         	This class supports the use of the following constraint types
 *              to control discovery behavior:
 *         	<ul>
 *         	  <li> {@link com.sun.jini.discovery.DiscoveryProtocolVersion}:
 *         	       this constraint can be used to control which version(s)
 *         	       of the multicast request, multicast announcement and
 *         	       unicast discovery protocols are used.
 *         	  <li> {@link com.sun.jini.discovery.MulticastMaxPacketSize}:
 *         	       this constraint can be used to control the maximum size
 *         	       of multicast request packets to send; it can also be
 *         	       used to specify the size of the buffer used to receive
 *         	       incoming multicast announcement packets.
 *         	  <li> {@link com.sun.jini.discovery.MulticastTimeToLive}: this
 *         	       constraint can be used to control the time to live (TTL)
 *         	       value set on outgoing multicast request packets.
 *         	  <li> {@link com.sun.jini.discovery.UnicastSocketTimeout}:
 *         	       this constraint can be used to control the read timeout
 *         	       set on sockets over which unicast discovery is
 *         	       performed.
 *         	</ul>
 *         	Constraints other than those listed above are passed on to the
 *         	underlying implementations of versions 1 and 2 of the discovery
 * </table>
 *
 * <a name="finalMulticastRequestInterval">
 * <table summary="Describes the finalMulticastRequestInterval
 *                configuration entry" border="0" cellpadding="2">
 *   <tr valign="top">
 *     <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
 *     <th scope="col" align="left" colspan="2"> <font size="+1">
 *     <code>finalMulticastRequestInterval</code></font>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Type: <td> <code>long</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Default: <td> <code>2*60*1000 (2 minutes)</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Description:
 *       <td> With respect to when this utility is started, as well
 *            as when the set of groups to discover is changed, this
 *            entry represents the number of milliseconds to wait
 *            after sending the n-th multicast request where n is
 *            equal to the value of the <a href="#multicastRequestMax">
 *            <code>multicastRequestMax</code></a> entry of this component.
 * </table>
 * <a name="multicastAnnouncementInterval">
 * <table summary="Describes the multicastAnnouncementInterval
 *                configuration entry" border="0" cellpadding="2">
 *   <tr valign="top">
 *     <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
 *     <th scope="col" align="left" colspan="2"> <font size="+1">
 *     <code>multicastAnnouncementInterval</code></font>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Type: <td> <code>long</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Default: <td> <code>2*60*1000 (2 minutes)</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Description:
 *       <td> A lookup service will send out multicast packets
 *            announcing its existence every N milliseconds; for some
 *            value of N. The value of this entry controls how often
 *            this utility examines the multicast announcements from
 *            previously discovered lookup services for <i>liveness</i>.
 * </table>
 *
 * <a name="multicastInterfaceRetryInterval">
 * <table summary="Describes the multicastInterfaceRetryInterval
 *                configuration entry" border="0" cellpadding="2">
 *   <tr valign="top">
 *     <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
 *     <th scope="col" align="left" colspan="2"> <font size="+1">
 *     <code>multicastInterfaceRetryInterval</code></font>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Type: <td> <code>int</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Default: <td> <code>5*60*1000 (5 minutes)</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Description:
 *       <td> With respect to any network interface this utility is configured
 *            to use to send and receive multicast packets (see entry  
 *            <a href="#multicastInterfaces">
 *            <code>multicastInterfaces</code></a>), if failure is encountered
 *            upon the initial attempt to set the interface or join the
 *            desired multicast group, this utility will retry the failed
 *            interface every <a href="#multicastInterfaceRetryInterval">
 *            <code>multicastInterfaceRetryInterval</code></a> milliseconds
 *            until success is encountered.
 * </table>
 *
 * <a name="multicastInterfaces">
 * <table summary="Describes the multicastInterfaces
 *                configuration entry" border="0" cellpadding="2">
 *   <tr valign="top">
 *     <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
 *     <th scope="col" align="left" colspan="2"> <font size="+1">
 *     <code>multicastInterfaces</code></font>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Type: <td> {@link java.net.NetworkInterface NetworkInterface[]}
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Default: <td> <code>new </code>
 *              {@link java.net.NetworkInterface NetworkInterface[]}
 *                     &nbsp <code>{all currently supported interfaces}</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Description:
 *       <td> Each network interface that is represented by an element
 *            in the array corresponding to this configuration item
 *            will be used to send and receive multicast packets when
 *            this utility is participating in the multicast discovery
 *            process. When not set, this utility will use all of the
 *            network interfaces in the system. When this entry is set
 *            to a zero length array, multicast discovery is effectively
 *            <b><i>disabled</i></b>. And when set to <code>null</code>,
 *            the interface to which the operating system defaults will be
 *            used.
 * </table>
 *
 * <a name="multicastRequestHost">
 * <table summary="Describes the multicastRequestHost
 *                configuration entry" border="0" cellpadding="2">
 *   <tr valign="top">
 *     <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
 *     <th scope="col" align="left" colspan="2"> <font size="+1">
 *     <code>multicastRequestHost</code></font>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Type: <td> <code>String</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Default: <td> 
 *    <code>{@link java.net.InetAddress}.getLocalHost().getHostAddress()</code>
 *
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Description:
 *       <td> This entry specifies the host name to include in multicast
 *            requests if participating in <b><i>version 2</i></b> of the
 *            multicast request protocol.
 * </table>
 *
 * <a name="multicastRequestInterval">
 * <table summary="Describes the multicastRequestInterval
 *                configuration entry" border="0" cellpadding="2">
 *   <tr valign="top">
 *     <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
 *     <th scope="col" align="left" colspan="2"> <font size="+1">
 *     <code>multicastRequestInterval</code></font>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Type: <td> <code>long</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Default: <td> <code>5000</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Description:
 *     <td> With respect to when this utility is started, as well as
 *          when the set of groups to discover is changed, this entry
 *          represents the number of milliseconds to wait after
 *          sending the n-th multicast request, and before sending
 *          the (n+1)-st request, where n is less than the value of the
 *          <a href="#multicastRequestMax"><code>multicastRequestMax</code></a>
 *          entry of this component.
 * </table>
 *
 * <a name="multicastRequestMax">
 * <table summary="Describes the multicastRequestMax
 *                configuration entry" border="0" cellpadding="2">
 *   <tr valign="top">
 *     <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
 *     <th scope="col" align="left" colspan="2"> <font size="+1">
 *     <code>multicastRequestMax</code></font>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Type: <td> <code>int</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Default: <td> <code>7</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Description:
 *       <td> The maximum number multicast requests to send when this
 *            utility is started for the first time, and whenever the
 *            groups to discover is changed.
 * </table>
 *
 * <a name="registrarPreparer">
 * <table summary="Describes the registrarPreparer configuration entry" 
 *                border="0" cellpadding="2">
 *   <tr valign="top">
 *     <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
 *     <th scope="col" align="left" colspan="2"> <font size="+1">
 *     <code>registrarPreparer</code></font>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *       Type: <td> {@link net.jini.security.ProxyPreparer}
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *       Default: <td> <code>new {@link net.jini.security.BasicProxyPreparer}()
 *                     </code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *   Description:
 *     <td> Preparer for the proxies to the lookup services that are
 *          discovered and used by this utility. 
 *          <p>
 *          This preparer should perform all operations required to use a
 *          newly received proxy to a lookup service, which may including
 *          verifying trust in the proxy, granting permissions, and setting
 *          constraints.
 *          <p>
 *          The following methods of the 
 *          {@link net.jini.core.lookup.ServiceRegistrar ServiceRegistrar}
 *          returned by this preparer are invoked by this implementation of
 *          <code>LookupDiscovery</code>:
 *       <ul>
 *         <li>{@link net.jini.core.lookup.ServiceRegistrar#getServiceID
 *                                                           getServiceID}
 *         <li>{@link net.jini.core.lookup.ServiceRegistrar#getGroups
 *                                                           getGroups}
 *         <li>{@link net.jini.core.lookup.ServiceRegistrar#getLocator
 *                                                           getLocator}
 *       </ul>
 * </table>
 *
 * <a name="taskManager">
 * <table summary="Describes the taskManager configuration entry" 
 *                border="0" cellpadding="2">
 *   <tr valign="top">
 *     <th scope="col" summary="layout"> <font size="+1">&#X2022;</font>
 *     <th scope="col" align="left" colspan="2"> <font size="+1">
 *     <code>taskManager</code></font>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Type: <td> {@link com.sun.jini.thread.TaskManager}
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Default: <td> <code>new 
 *             {@link com.sun.jini.thread.TaskManager#TaskManager()
 *                                   TaskManager}(15, (15*1000), 1.0f)</code>
 * 
 *   <tr valign="top"> <td> &nbsp <th scope="row" align="right">
 *     Description:
 *       <td> The object that pools and manages the various threads
 *            executed by this utility. The default manager creates a
 *            maximum of 15 threads, waits 15 seconds before removing
 *            idle threads, and uses a load factor of 1.0 when
 *            determining whether to create a new thread. This object
 *            should not be shared with other components in the
 *            application that employs this utility.
 * </table>
 *
 * <a name="ldLogging">
 * <p>
 * <b><font size="+1">Logging</font></b>
 * <p>
 * </a>
 *
 * This implementation of <code>LookupDiscovery</code> uses the {@link Logger}
 * named <code>net.jini.discovery.LookupDiscovery</code> to log information
 * at the following logging levels: <p>
 * 
 * <table border="1" cellpadding="5"
 *       summary="Describes the information logged by LookupDiscovery, and
 *                 the levels at which that information is logged">
 * 
 * <caption halign="center" valign="top">
 *   <b><code>net.jini.discovery.LookupDiscovery</code></b>
 * </caption>
 *
 * <tr> <th scope="col"> Level</th>
 *      <th scope="col"> Description</th>
 * </tr>
 * 
 * <tr>
 *   <td>{@link java.util.logging.Level#SEVERE SEVERE}</td>
 *   <td>
 *     when this utility is configured to use either the default network
 *     interface assigned by the system, or a specific list of network
 *     interfaces, if one of those interfaces is bad or not configured for
 *     multicast, or if a runtime exception occurs while either sending
 *     multicast requests, or while configuring one of the interfaces to
 *     receive multicast announcements, that fact will be logged at this
 *     level
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#INFO INFO}</td>
 *   <td>
 *     when any exception other than an <code>InterruptedIOException</code>,
 *     <code>InterruptedException</code> or
 *     <code>UnsupportedConstraintException</code> occurs in a thread or task
 *     while attempting to marshal an outgoing multicast request
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#INFO INFO}</td>
 *   <td>
 *     when any exception other than an <code>InterruptedIOException</code> or
 *     <code>SocketTimeoutException</code> occurs in a non-interrupted thread
 *     while attempting to receive an incoming multicast packet
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#INFO INFO}</td>
 *   <td>
 *     when any exception other than an <code>InterruptedIOException</code>
 *     occurs while attempting unicast discovery
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#INFO INFO}</td>
 *   <td>
 *     when this utility is configured to use either the default network
 *     interface assigned by the system, or a specific list of network
 *     interfaces, with respect to any such interface, if failure is
 *     encountered upon the initial attempt to set the interface or join
 *     the desired multicast group, the interface will be periodically
 *     retried, and successful recovery will be logged at this level
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#INFO INFO}</td>
 *   <td>when any exception occurs while attempting to prepare a proxy</td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#CONFIG CONFIG}</td>
 *   <td>
 *     when the <code>multicastInterfaces</code> entry is configured to 
 *     be <code>null</code>, multicast packets will be sent and received
 *     through the default network interface assigned by the system, and
 *     that fact will be logged at this level
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#CONFIG CONFIG}</td>
 *   <td>
 *     when the <code>multicastInterfaces</code> entry is configured to 
 *     be a zero length array, multicast discovery will be disabled, and
 *     and that fact will be logged at this level
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#CONFIG CONFIG}</td>
 *   <td>
 *     when the <code>multicastInterfaces</code> entry contains a specific
 *     list of network interfaces, multicast packets will be sent and
 *     received through only the network interfaces contained in that list,
 *     and those interfaces will be logged at this level
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#CONFIG CONFIG}</td>
 *   <td>
 *     when the <code>multicastInterfaces</code> entry is excluded from
 *     the configuration, multicast packets will be sent and received
 *     through all interfaces in the system, and those interfaces will
 *     be logged at this level
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link com.sun.jini.logging.Levels#HANDLED HANDLED}</td>
 *   <td>
 *     when this utility is configured to use all network interfaces enabled
 *     in the system, if one of those interfaces is bad or not configured for
 *     multicast, or if a runtime exception occurs while either sending
 *     multicast requests, or while configuring one of the interfaces to
 *     receive multicast announcements, that fact will be logged at this
 *     level
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link com.sun.jini.logging.Levels#HANDLED HANDLED}</td>
 *   <td>
 *     when any exception occurs while attempting to unmarshal an incoming
 *     multicast announcement
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link com.sun.jini.logging.Levels#HANDLED HANDLED}</td>
 *   <td>
 *     when an <code>UnsupportedConstraintException</code> occurs while
 *     marshalling an outgoing multicast request, indicating that the provider
 *     that threw the exception will not be used for encoding that request
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link com.sun.jini.logging.Levels#HANDLED HANDLED}</td>
 *   <td>
 *     when an <code>InterruptedIOException</code> occurs during unicast
 *     discovery
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link com.sun.jini.logging.Levels#HANDLED HANDLED}</td>
 *   <td>
 *     when an <code>IOException</code> occurs upon attempting to close the
 *     socket after the thread that listens for multicast responses is asked
 *     to terminate
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#FINE FINE}</td>
 *   <td>
 *     when this utility is configured to use all network interfaces enabled
 *     in the system, with respect to any such interface, if failure is
 *     encountered upon the initial attempt to set the interface or join
 *     the desired multicast group, the interface will be periodically
 *     retried, and successful recovery will be logged at this level
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#FINEST FINEST}</td>
 *   <td>whenever any thread or task is started</td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#FINEST FINEST}</td>
 *   <td>
 *     whenever any thread (except the <code>Notifier</code> thread) or task
 *     completes successfully
 *   </td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#FINEST FINEST}</td>
 *   <td>whenever a discovered, discarded, or changed event is sent</td>
 * </tr>
 * <tr>
 *   <td>{@link java.util.logging.Level#FINEST FINEST}</td>
 *   <td>whenever a proxy is prepared</td>
 * </tr>
 * </table>
 * <p>
 *
 * @author Sun Microsystems, Inc.
 *
 * @see net.jini.core.lookup.ServiceRegistrar
 * @see DiscoveryChangeListener
 * @see DiscoveryManagement
 * @see DiscoveryGroupManagement
 * @see DiscoveryListener
 * @see DiscoveryEvent
 * @see DiscoveryPermission
 */
public class LookupDiscovery implements DiscoveryManagement,
                                        DiscoveryGroupManagement
{
    /* Name of this component; used in config entry retrieval and the logger.*/
    private static final String COMPONENT_NAME
                                        = "net.jini.discovery.LookupDiscovery";
    /* Logger used by this utility. */
    private static final Logger logger = Logger.getLogger(COMPONENT_NAME);
    /** Convenience constant used to request that attempts be made to
     *  discover all lookup services that are within range, and which
     *  belong to any group. Must define this constant here as well as in
     *  <code>DiscoveryGroupManagement</code> for compatibility with
     *  earlier releases.
     */
    public static final String[] ALL_GROUPS 
                                       = DiscoveryGroupManagement.ALL_GROUPS;
    /** Convenience constant used to request that discovery by group
     *  membership be halted (or not started, if the group discovery
     *  mechanism is simply being instantiated). Must define this constant
     *  here as well as in <code>DiscoveryGroupManagement</code> for
     *  compatibility with earlier releases.
     */
    public static final String[] NO_GROUPS 
                                        = DiscoveryGroupManagement.NO_GROUPS;
    /** Maximum number of concurrent tasks that can be run in any task
     *  manager created by this class.
     */
    private static final int MAX_N_TASKS = 15;
    /** 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 final int DEFAULT_MULTICAST_TTL = 15;
    /** Default timeout to set on sockets used for unicast discovery. */
    private static final int DEFAULT_SOCKET_TIMEOUT = 1*60*1000;

    /** Flag indicating whether or not this class is still functional. */
    private boolean terminated = false;
    /** Set of listeners to be sent discovered/discarded/changed events. */
    private ArrayList listeners = new ArrayList(1);
    /** The groups to discover. Empty set -- NO_GROUPS, null -- ALL_GROUPS */
    private Set groups = null;
    /** Map from ServiceID to UnicastResponse. */
    private Map registrars = new HashMap(11);
    /** 
     * Set that takes one of the following:
     * <p><ul>
     * <li> Socket (discovery from unicast request/response exchange)
     * <li> LookupLocator (discovery from multicast announcement)
     * <li> CheckGroupsMarker (discarded/changed from announcement)
     * <li> CheckReachabilityMarker (announcements stopped, tests reachability)
     * </ul><p>
     * Each element of this set represents a potential (or pending) discovered,
     * discarded, or changed event. Instances of UnicastDiscoveryTask retrieve
     * the next available element from this set and, based on the object type
     * of the element, determines the processing to perform and what event
     * type to send to the registered listeners.
     */
    private Set pendingDiscoveries = new HashSet(11);
    /** Thread that handles pending notifications. */
    private Notifier notifierThread;
    /** Notifications to be sent to listeners. */
    private LinkedList pendingNotifies = new LinkedList();
    /** Task manager for running UnicastDiscoveryTasks and
     *  DecodeAnnouncementTasks.
     */
    private TaskManager taskManager;
    /** Thread that handles incoming multicast announcements. */
    private AnnouncementListener announceeThread;
    /** Collection that contains instances of the Requestor Thread class,
     *  each of which participate in multicast discovery by periodically
     *  sending out multicast discovery requests for a finite period of time.
     */
    private Collection requestors = new LinkedList();
    /** Thread that manages incoming multicast responses. Runs only when
     *  there are Requestor threads running.
     */
    private ResponseListener respondeeThread = null;
    /** Security context that contains the access control context to restore
     * for callbacks, etc.
     */
    private final SecurityContext securityContext = Security.getContext();
    /** Map from ServiceID to multicast announcement time stamps; used by the
     *  process that monitors multicast announcements from already-discovered
     *  lookup services, and determines when those announcements have stopped.
     */
    private HashMap regTimes = new HashMap(11);
    /** Thread that monitors multicast announcements from already-discovered
     *  lookup services and, upon determining that those announcements have
     *  stopped, queues a reachability test with the UnicastDiscoveryTask
     *  which will ultimately result in the lookup service being discarded
     *  if the reachability test indicates that the lookup service is
     *  actually down.
     */
    private AnnouncementTimerThread announcementTimerThread;
    /* Preparer for the proxies to the lookup services that are discovered
     * and used by this utility.
     */
    private ProxyPreparer registrarPreparer;
    /* Utility for participating in version 2 of discovery protocols. */
    private Discovery protocol2 = Discovery.getProtocol2(null);
    /* Maximum number multicast requests to send when this utility is started
     * for the first time, and whenever the groups to discover are changed.
     */
    private int multicastRequestMax = 7;
    /* With respect to when this utility is started, as well as when the set
     * of groups to discover is changed, the value of this field represents
     * the number of milliseconds to wait after sending the n-th multicast
     * request, and before sending the (n+1)-st request, where n is less than
     * the value of <code)multicastRequestMax</code>.
     */
    private long multicastRequestInterval = 5000L;
    /* With respect to when this utility is started, as well as when the set
     * of groups to discover is changed, the value of this field represents
     * the number of milliseconds to wait after sending the n-th multicast
     * request, where n is equal to the value of 
     * <code)multicastRequestMax</code>.
     */
    private long finalMulticastRequestInterval = 2*60*1000L;
    /* Name of requesting host to include in multicast request if
     * participating in version 2 of multicast request protocol.
     */
    private String multicastRequestHost;
    /* Constraints specified for outgoing multicast requests. */
    private DiscoveryConstraints multicastRequestConstraints;
    /* The network interfaces (NICs) through which multicast packets will
     * be sent.
     */
    private NetworkInterface[] nics;
    /* NICs that initially failed are retried after this number of millisecs.*/
    private int nicRetryInterval = 5*60*1000;
    /* Controls how often (in milliseconds) this utility examines the
     * multicast announcements from previously discovered lookup services
     * for "liveness".
     */
    private long multicastAnnouncementInterval = 2*60*1000L;
    /* Constraints specified for incoming multicast announcements. */
    private DiscoveryConstraints multicastAnnouncementConstraints;
    /* Constraints specified for unicast discovery. */
    private DiscoveryConstraints unicastDiscoveryConstraints;

    /** Constants used to tell the notifierThread the type of event to send */
    private static final int DISCOVERED = 0;
    private static final int DISCARDED  = 1;
    private static final int CHANGED    = 2;

    /** Constants used to indicate the set of network interfaces being used */
    private static final int NICS_USE_ALL  = 0;//use all NICs in the system
    private static final int NICS_USE_SYS  = 1;//use NIC assigned by the system
    private static final int NICS_USE_LIST = 2;//use list of NICs from config
    private static final int NICS_USE_NONE = 3;//multicast disabled
    /** Flag that indicates how the set of network interfaces was configured */
    private int nicsToUse = NICS_USE_ALL;

    /** Data structure containing task data processed by the Notifier Thread */
    private static class NotifyTask {
	/** The set of listeners to notify */
	public final ArrayList listeners;
	/** Map of discovered registrars-to-groups in which each is a member */
	public final Map groupsMap;
	/** The type of event to send: DISCOVERED, DISCARDED, CHANGED */
	public final int eventType;
	public NotifyTask(ArrayList listeners, Map groupsMap, int eventType) {
	    this.listeners = listeners;
	    this.groupsMap = groupsMap;
	    this.eventType = eventType;
	}
    }//end class NotifyTask

    /** Thread that retrieves data structures of type NotifyTask from a
     *  queue and, based on the contents of the data structure, sends the
     *  appropriate event (discovered/discarded/changed) to each registered
     *  listener.
     *  <p>
     *  Only 1 instance of this thread is run.
     */
    private class Notifier extends Thread {
	/** Create a daemon thread */
	public Notifier() {
	    super("event listener notification");
	    setDaemon(true);
	}//end constructor

	public void run() {
            logger.finest("LookupDiscovery - Notifier thread started");
	    while (true) {
		final NotifyTask task;
		synchronized (pendingNotifies) {
		    if (pendingNotifies.isEmpty()) {
			notifierThread = null;
			return;
		    }//endif
		    task = (NotifyTask)pendingNotifies.removeFirst();
		}//end sync
                /* The call to notify() on the registered listeners is
                 * performed inside a doPrivileged block that restores the
                 * access control context that was in place when this utility
                 * was created.
                 * 
                 * This is done because the notify() method called below 
                 * executes in the client. But the listener object that
                 * defines that notify() method may have been obtained by
                 * the client from some (possibly untrusted) 3rd party. With
                 * respect to 3rd party code executing in the client, it is
                 * not desirable to allow such code to execute with more
                 * priviledges than the client that created this utility.
                 * Therefore, before executing notify() on any of the 
                 * registered listeners, the client's Subject should be
                 * restored, and the listner code should be restricted to
                 * doing nothing more than the client itself is allowed to do.
                 */
		AccessController.doPrivileged
                (securityContext.wrap(new PrivilegedAction() {
		    public Object run() {
                        boolean firstListener = true;
			for (Iterator iter = task.listeners.iterator();
			     iter.hasNext(); )
			{
			    DiscoveryListener l =
                                             (DiscoveryListener)iter.next();
                            /* Always send discovered and discarded events */
                            if(     (task.eventType == CHANGED)
                                && !(l instanceof DiscoveryChangeListener) )
                            {
                                continue;
                            }//endif
			    DiscoveryEvent e =
				new DiscoveryEvent
                                        ( LookupDiscovery.this,
                                          deepCopy((HashMap)task.groupsMap) );
                            /* Log the event info about the lookup(s) */
                            if(     firstListener
                                && (logger.isLoggable(Level.FINEST)) )
                            {
                                String eType =
                                       new String[]{"discovered",
                                                    "discarded",
                                                    "changed"}[task.eventType];
                                ServiceRegistrar[] regs = e.getRegistrars();
                                logger.finest(eType+" event  -- "+regs.length
                                                                +" lookup(s)");
                                Map groupsMap = e.getGroups();
                                for(int i=0;i<regs.length;i++) {
                                    LookupLocator loc = null;
                                    try {
                                        loc = regs[i].getLocator();
                                    } catch (Throwable ex) { /* ignore */ }
                                    String[] groups = 
                                             (String[])groupsMap.get(regs[i]);
                                    logger.finest("    "+eType+" locator  = "
                                                              +loc);
                                    if(groups.length == 0) {
                                        logger.finest("    "+eType+" group    "
                                                            +"= NO_GROUPS");
                                    } else {
                                        for(int j=0;j<groups.length;j++) {
                                            logger.finest("    "+eType
                                                          +" group["+j+"] = "
                                                          +groups[j]);
                                        }//end loop
                                    }//endif(groups.length)
                                }//end loop
                            }//endif(firstListener && isLoggable(Level.FINEST)
                            switch(task.eventType) {
                                case DISCOVERED:
                                    l.discovered(e);
                                    break;
                                case DISCARDED:
                                    l.discarded(e);
                                    break;
                                case CHANGED:
                                    ((DiscoveryChangeListener)l).changed(e);
                                    break;
                            }//end switch(eventType)
                            firstListener = false;
			}//end loop
			return null;
		    }//end run
		}),//end PrivilegedAction and wrap
                securityContext.getAccessControlContext());//end doPrivileged
	    }//end loop
	}//end run
    }//end class Notifier

    /** Thread that listens for multicast announcements from lookup services.
     *  <p>
     *  If the announcements are from a lookup service that has not already
     *  been discovered, and if it is determined that the lookup service 
     *  belongs to at least one group of interest, a "pendingDiscovery" is
     *  queued for the UnicastDiscoveryTask to process asynchronously,
     *  completing the discovery process by performing unicast discovery.
     *  <p>
     *  If the announcements are from a lookup service that has already 
     *  been discovered, the lookup service's member groups - as indicated
     *  in the announcements - are analyzed for changes that may result
     *  in either the lookup service being discarded, or in a changed event
     *  being sent.
     *  <p>
     *  Only 1 instance of this thread is run.
     */
    private class AnnouncementListener extends Thread {
	/** Multicast socket for receiving packets */
	private MulticastSocket sock;
        /* Set of interfaces whose elements also belong to the nics[] array,
         * which encountered failure when setting the interface or joining
         * the desired multicast group, and which will be retried periodically.
         */
        private ArrayList retryNics = null;

	/** Create a daemon thread */
	public AnnouncementListener() throws IOException {
	    super("multicast discovery announcement listener");
	    setDaemon(true);
	    sock = new MulticastSocket(Constants.discoveryPort);
            switch(nicsToUse) {
                case NICS_USE_ALL:
                    /* Using all interfaces. Skip (but report) any interfaces
                     * that are "bad" or not configured for multicast.
                     */
                    for(int i=0;i<nics.length;i++) {
                        try {
                            sock.setNetworkInterface(nics[i]);
                            sock.joinGroup(Constants.getAnnouncementAddress());
                        } catch(IOException e) {
                            if(retryNics == null) {
                                retryNics = new ArrayList(nics.length);
                            }//endif
                            retryNics.add(nics[i]);
                            if( logger.isLoggable(Levels.HANDLED) ) {
                                LogRecord logRec = 
                                  new LogRecord(Levels.HANDLED,
						"network interface "
                                                +"is bad or not configured "
                                                +"for multicast: {0}");
                                logRec.setParameters(new Object[]{nics[i]});
                                logRec.setThrown(e);
                                logger.log(logRec);
                            }//endif
                        }
                    }//end loop
                    break;
                case NICS_USE_LIST:
                    /* Using a configured list of specific interfaces. Skip
                     * (but report) any interfaces that are "bad" or not
                     * configured for multicast.
                     */
                    for(int i=0;i<nics.length;i++) {
                        try {
                            sock.setNetworkInterface(nics[i]);
                            sock.joinGroup(Constants.getAnnouncementAddress());
                        } catch(IOException e) {
                            if(retryNics == null) {
                                retryNics = new ArrayList(nics.length);
                            }//endif
                            retryNics.add(nics[i]);
                            if( logger.isLoggable(Level.SEVERE) ) {
                                LogRecord logRec = 
                                  new LogRecord(Level.SEVERE,
                                                "network interface is bad or "
                                                +"not configured for "
                                                +"multicast: {0}");
                                logRec.setParameters(new Object[]{nics[i]});
                                logRec.setThrown(e);
                                logger.log(logRec);
                            }//endif
                        }
                    }//end loop
                    break;
                case NICS_USE_SYS:
                    /* Using the system-dependent default interface. Don't
                     * need to specifically set the interface. If that
                     * interface is "bad" or not configured for multicast,
                     * log it and try again later.
                     */
                    try {
                        sock.joinGroup(Constants.getAnnouncementAddress());
                    } catch(IOException e) {
                        retryNics = new ArrayList(0);
                        if( logger.isLoggable(Level.SEVERE) ) {
                            logger.log(Level.SEVERE, "system default network "
                                       +"interface is bad or not configured "
                                       +"for multicast", e);
                        }//endif
                    }
                    break;
                case NICS_USE_NONE:
                    break;//multicast disabled, do nothing
                default:
                    throw new AssertionError("nicsToUse flag out of range "
                                             +"(0-3): "+nicsToUse);
            }//end switch(nicsToUse)
	}//end constructor

	/** 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 void interrupt() {
	    interrupted = true;
	    sock.close();
	}//end interrupt

        /** Accessor method that returns the <code>interrupted</code> flag. */
	public boolean isInterrupted() {
	    return interrupted;
	}//end isInterrupted

        /** Convenience method that retries any previously failed interfaces.*/
	private void retryBadNics() {
            if(retryNics == null) return;//no failed NICs to retry
            if( !retryNics.isEmpty() ) {
                String recoveredStr = "network interface has recovered "
                                      +"from previous failure: {0}";
                ArrayList tmpList = (ArrayList)retryNics.clone();
                retryNics.clear();
                for(int i=0; i<tmpList.size(); i++) {
                    NetworkInterface nic =(NetworkInterface)tmpList.get(i);
                    try {
                        sock.setNetworkInterface(nic);
                        sock.joinGroup(Constants.getAnnouncementAddress());
                        if(nicsToUse == NICS_USE_LIST) {
                            logger.log(Level.INFO, recoveredStr, nic);
                        } else {
                            logger.log(Level.FINE, recoveredStr, nic);
                        }//endif
                    } catch(IOException e1) {
                        retryNics.add(nic);//put back for another retry later
                    }
                }//end loop
                if(retryNics.isEmpty()) retryNics = null;//future retries off
            } else {//(retryNics.size() == 0) ==> sys default interface
                try {
                    sock.joinGroup(Constants.getAnnouncementAddress());
                    retryNics = null;
                    logger.log(Level.INFO, "system default network "
                                           +"interface has recovered from "
                                           +"previous failure");
                } catch(IOException e1) { }
            }//endif(!retryNics.isEmpty())
	}//end retryBadNics

	public void run() {
            logger.finest("LookupDiscovery - AnnouncementListener thread "
                          +"started");
	    byte[] buf = new byte[
		multicastAnnouncementConstraints.getMulticastMaxPacketSize(
		    DEFAULT_MAX_PACKET_SIZE)];
	    DatagramPacket pkt = new DatagramPacket(buf, buf.length);
            long endTime = System.currentTimeMillis() + nicRetryInterval;
	    while (!isInterrupted()) {
		try {
                    int delta_t = 0;
                    if(retryNics != null) {//bad NICs, retry when time's up
                        delta_t = (int)(endTime - System.currentTimeMillis());
                        if( delta_t <= 0) {
                            retryBadNics();
                            if(retryNics != null) {//still bad, reset timer
                                delta_t = nicRetryInterval;
                                endTime = System.currentTimeMillis() + delta_t;
                            } else {//all NICs recovered, turn off timer
                                delta_t = 0;
                            }//endif
                        }//endif
                    }//endif
                    sock.setSoTimeout(delta_t);
		    pkt.setLength(buf.length);
		    try {
			sock.receive(pkt);
		    } catch (NullPointerException e) {
			break; // workaround for bug 4190513
		    }
		    taskManager.add(new DecodeAnnouncementTask(pkt));

		    buf = new byte[buf.length];
		    pkt = new DatagramPacket(buf, buf.length);

		} catch (SocketTimeoutException e) {//continue/retry bad NICs
		} catch (InterruptedIOException e) {
		    break;
		} catch (Exception e) {//ignore
                    if( isInterrupted() )  break;
                    logger.log(Level.INFO,
                               "exception while listening for multicast "
                               +"announcements",
                               e);
		}
	    }//end loop(!interrupted)
	    sock.close();
	    sock = null;
            logger.finest("LookupDiscovery - AnnouncementListener thread "
                          +"completed");
	}//end run
    }//end class AnnouncementListener

    /** Thread that listens for multicast responses to the multicast requests
     *  sent out by the Requestor Thread class. Upon receiving a multicast
     *  response, the socket that accepted the connection request associated
     *  with the the multicast response is added to the set of
     *  pendingDiscoveries so that it (the socket) will be used by the
     *  UnicastDiscoveryTask to complete the discovery process asynchronously.
     *  <p>
     *  Only 1 instance of this thread is run.
     */
    private class ResponseListener extends Thread {
	/** Server socket for accepting connections */
	public ServerSocket serv;
	
	/** Create a daemon thread */
	public ResponseListener() throws IOException {
	    super("multicast discovery response listener");
	    setDaemon(true);
	    serv = new ServerSocket(0);
	}//end constructor

	/** 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 void interrupt() {
	    interrupted = true;
	    try {
		(new Socket(InetAddress.getLocalHost(), getPort())).close();
	    } catch (IOException e) { /* ignore */ }
	}//end interrupt

        /** Accessor method that returns the <code>interrupted</code> flag. */
	public boolean isInterrupted() {
	    return interrupted;
	}//end isInterrupt

	public void run() {
            logger.finest("LookupDiscovery - ResponseListener thread started");
	    while (!isInterrupted()) {
		try {
		    Socket sock = serv.accept();
		    if (isInterrupted()) {
			try {
			    sock.close();
			} catch (IOException e) { }
			break;
		    }//end if
		    synchronized (pendingDiscoveries) {
			pendingDiscoveries.add(sock);
                        taskManager.add(new UnicastDiscoveryTask());
		    }//end sync
		} catch (InterruptedIOException e) {
		    break;
		} catch (Exception e) {//ignore
                    logger.log(Level.INFO,
                               "exception while listening for muliicast "
                               +"response",
                               e);
		}
	    }//end loop(!isInterrupted)
	    try {
		serv.close();
	    } catch (IOException e) {//ignore
                logger.log(Levels.HANDLED,
                           "IOException while attempting a socket close",
                           e);
	    }
            logger.finest("LookupDiscovery - ResponseListener thread "
                          +"completed");
	}//end run

	/** Return the local port of the socket */
	public int getPort() {
	    return serv.getLocalPort();
	}//end getPort
    }//end class ResponseListener

    /** Thread that periodically sends out multicast requests for a limited
     *  period of time, and then exits.
     *  <p>
     *  An instance of this thread is run at startup, and each time the
     *  set of groups to discover is changed.
     */
    private class Requestor extends Thread {
	/** Multicast socket for sending packets */
	private MulticastSocket sock;
	/** Unicast response port */
	private int responsePort;
	/** Groups to request */
	private String[] groups;

	/** Create a daemon thread */
	public Requestor(String[] groups, int port) throws IOException {
	    super("multicast discovery request");
	    setDaemon(true);
	    sock = new MulticastSocket(Constants.discoveryPort);
	    sock.setTimeToLive(
		multicastRequestConstraints.getMulticastTimeToLive(
		    DEFAULT_MULTICAST_TTL));
	    responsePort = port;
	    this.groups = groups == null ? new String[0] : groups;
	}//end constructor

	/** This method sends out N (for small N) multicast requests. Until
         *  the last request is sent out, this method sleeps for 5 seconds
         *  after each request is sent. After the last request is sent,
         *  this method sleeps for 2 minutes to allow the ResponseListener
         *  time to receive and process any multicast responses sent in
         *  reply to the multicast requests. Before sending a request, a
         *  new multicast request is constructed so that updates
         *  can be made to the set of service IDs of the lookup services
         *  discovered due to previous requests. 
         *  <p>
	 *  After all requests have been sent, and the ResponseListener
         *  has been given the appropriate time to receive and process
         *  any multicast responses, if there are no more active instances
         *  of this thread, this method terminates (interrupts) the
         *  ResponseListener. Note that although it is more desirable to
         *  have the ResponseListener set a timeout on the server socket
         *  (using setSoTimeout) and then simply exit after a period of 
         *  time in which both the ResponseListener has been idle, and
         *  there have been no active Requestor threads, using setSoTimeout
         *  in this way can cause random hangs on the Solaris(TM) operating
         *  system.
	 */
	public void run() {
            logger.finest("LookupDiscovery - Requestor thread started");
	    int count; // bug 4084783/4187594
	    try {
		for (count = multicastRequestMax;
                                          --count >= 0 && !isInterrupted(); )
                {
                    DatagramPacket[] reqs = encodeMulticastRequest
			(new MulticastRequest(multicastRequestHost,
					      responsePort,
					      groups,
					      getServiceIDs()));
                    sendPacketByNIC(sock, reqs);
		    Thread.sleep(count > 0 ?
                      multicastRequestInterval:finalMulticastRequestInterval);
		}//end loop
	    } catch (InterruptedException e) {//terminate gracefully
	    } catch (InterruptedIOException e) {//terminate gracefully
            } catch (Exception e) {
                logger.log(Level.INFO,"exception while marshalling outgoing "
                                      +"multicast request", e);
	    } finally {
		synchronized (requestors) {
		    requestors.remove(Thread.currentThread());
		    if (respondeeThread != null && requestors.isEmpty()) {
			respondeeThread.interrupt();
			respondeeThread = null;
		    }
		}//end sync
		sock.close();
                logger.finest("LookupDiscovery - Requestor thread completed");
	    }//end try/catch/finally
	}//end run
    }//end class Requestor

    /**
     * This thread monitors the multicast announcements sent from the
     * lookup service(s) that have already been discovered by this class,
     * looking for indications that those announcements have terminated.
     * 
     * The data structure used to map the discovered lookup services to
     * the time of arrival of the most recent multicast announcement from
     * each such lookup service is examined at regular intervals; dependent
     * on the system property <code>net.jini.discovery.announce</code>.
     *
     * If the difference between the current time and the last time of
     * arrival for any announcement exceeds a predetermined threshold, the
     * corresponding lookup is polled for its current set of member groups.
     * If that lookup service is unreachable, or if it is reachable but its
     * member groups have been replaced, the lookup service is discarded.
     */
    private class AnnouncementTimerThread extends Thread {
        /* Number of interval to exceed for declaring announcements stopped */
        private static final long N_INTERVALS = 3;
        /** Create a daemon thread */
        public AnnouncementTimerThread() {
            super("multicast announcement timer");
            setDaemon(true);
        }
        public synchronized void run() {
            long timeThreshold = N_INTERVALS*multicastAnnouncementInterval;
            try {
                while(!isInterrupted()) {
                    wait(multicastAnnouncementInterval);
                    long curTime = System.currentTimeMillis();
                    synchronized (registrars) {
                        /* can't modify regTimes while iterating over it, 
                         * so clone it
                         */
                        HashMap regTimesClone = (HashMap)(regTimes.clone());
                        Set eSet = regTimesClone.entrySet();
                        for(Iterator itr = eSet.iterator(); itr.hasNext(); ) {
                            Map.Entry pair   = (Map.Entry)itr.next();
                            ServiceID srvcID = (ServiceID)pair.getKey();
                            long tStamp = ((Long)pair.getValue()).longValue();
                            long deltaT = curTime - tStamp;
                            if(deltaT > timeThreshold) {
                                /* announcements stopped, queue reachability
                                 * test and potential discarded event
                                 */
                                UnicastResponse resp =
				    (UnicastResponse)registrars.get(srvcID);
                                synchronized (pendingDiscoveries) {
                                    if( pendingDiscoveries.add(
					  new CheckReachabilityMarker(resp)) )
				    {
                                        taskManager.add(
					    new UnicastDiscoveryTask());
                                    }//endif
                                }//end sync
                            }//end if
                        }//end loop (itr)
                    }//end sync
                }//end loop (!isInterrupted)
            } catch (InterruptedException e) { }
        }//end run
    }//end class AnnouncementTimerThread

    /**
     * Marker object placed in pendingDiscoveries set to indicate to
     * UnicastDiscoveryTask that the groups of the lookup service which sent
     * the contained announcement need to be verified.
     */
    private static class CheckGroupsMarker {
	/** Announcement sent by lookup service to check groups of */
	final MulticastAnnouncement announcement;

	CheckGroupsMarker(MulticastAnnouncement announcement) {
	    this.announcement = announcement;
	}

	public int hashCode() {
	    return announcement.getServiceID().hashCode();
	}

	public boolean equals(Object obj) {
	    return obj instanceof CheckGroupsMarker &&
		   announcement.getServiceID().equals(
		      ((CheckGroupsMarker) obj).announcement.getServiceID());
	}
    }

    /**
     * Marker object placed in pendingDiscoveries set to indicate to
     * UnicastDiscoveryTask that reachability of the lookup service which sent
     * the contained unicast response needs to be verified.
     */
    private static class CheckReachabilityMarker {
	/** Response sent by lookup service to check reachability of */
	final UnicastResponse response;

	CheckReachabilityMarker(UnicastResponse response) {
	    this.response = response;
	}

	public int hashCode() {
	    return response.getRegistrar().hashCode();
	}

	public boolean equals(Object obj) {
	    return obj instanceof CheckReachabilityMarker &&
		   response.getRegistrar().equals(
		      ((CheckReachabilityMarker) obj).response.getRegistrar());
	}
    }

    /**
     * Task which decodes received multicast announcement packets.  This is
     * separated into a task to allow the AnnouncementListener thread to
     * quickly loop and receive new announcement packets; the act of decoding
     * packets may involve relatively slow cryptographic operations such as
     * signature verification, and would impede the packet receiving loop if it
     * were performed inline.
     */
    private class DecodeAnnouncementTask implements TaskManager.Task {

	private final DatagramPacket datagram;

	/**
	 * Creates a task for decoding the given multicast announcement packet.
	 */
	public DecodeAnnouncementTask(DatagramPacket datagram) {
	    this.datagram = datagram;
	}

	/**
	 * Decodes this task's multicast announcement packet.  If the
	 * constraints for decoding multicast announcements are satisfied and
	 * the announcement merits further processing, an appropriate object is
	 * added to the pendingDiscoveries set, and control is transferred to a
	 * UnicastDiscoveryTask.
	 */
	public void run() {
	    MulticastAnnouncement ann;
	    try {
		ann = decodeMulticastAnnouncement(datagram);
	    } catch (Exception e) {
		if (!(e instanceof InterruptedIOException)) {
		    logger.log(Levels.HANDLED,
			       "exception decoding multicast announcement", e);
		}
		return;
	    }

	    /* If the registrars map contains the service ID of the registrar
	     * that sent the current announcement then that registrar has
	     * already been discovered.
	     * 
	     * Determine if the member groups of the already-discovered
	     * registrar have been replaced by a set containing none of the
	     * desired groups. If yes, then discard the registrar.
	     * 
	     * If the registrar that sent the current announcement has not
	     * already been discovered, then check to see if any of the
	     * group(s) in which the registrar is a member are in the set of
	     * desired groups to discover. If yes, then queue the registrar for
	     * unicast discovery.
	     */
	    // REMIND: ignore announcements with old sequence numbers
	    Object pending = null;
	    ServiceID srvcID = ann.getServiceID();
	    synchronized (registrars) {
		UnicastResponse resp =
		    (UnicastResponse) registrars.get(srvcID);
		if (resp != null) {
		    // already in discovered set, timestamp announcement
		    regTimes.put(srvcID, new Long(System.currentTimeMillis()));
		    // check for potential discarded or changed event
		    pending = new CheckGroupsMarker(ann);
		} else if (groupsOverlap(ann.getGroups())) {
		    // newly discovered
		    pending = new LookupLocator(ann.getHost(), ann.getPort());
		}
	    }
	    if (pending != null) {
		// enqueue and handle pending action, if not already enqueued
		boolean added;
		synchronized (pendingDiscoveries) {
		    added = pendingDiscoveries.add(pending);
		}
		if (added) {
		    new UnicastDiscoveryTask().run();
		}
	    }
	}

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

    /** Task which retrieves elements from the set of pendingDiscoveries and
     *  performs the appropriate processing based on the object type of
     *  the element.
     *  <p>
     *  Each element of the set of pendingDiscoveries is one of the following
     *  object types: Socket, LookupLocator, CheckGroupsMarker,
     *  or CheckReachabilityMarker.
     *  <p>
     *  When the element to process is a Socket, the element was a result
     *  of a unicast request/response exchange (see the Requestor and
     *  ResponseListener Thread classes). In this case, this task completes
     *  the discovery of the associated lookup service by performing the
     *  final stage of unicast discovery, ultimately resulting in a discovered
     *  event being sent to all registered listeners.
     *  <p>
     *  When the element to process is a LookupLocator, the element was a
     *  result of a multicast announcement received from a lookup service -
     *  belonging to at least one group of interest - which has not already
     *  been discovered. In this case, this task also completes the discovery
     *  of the lookup service referenced in the announcement by performing the
     *  final stage of unicast discovery, ultimately resulting in a discovered
     *  event being sent to all registered listeners.
     *  <p>
     *  When the element to process is a CheckGroupsMarker, the
     *  element was a result of a multicast announcement received from an
     *  already-discovered lookup service whose member groups have changed
     *  in some way. In this case, this task determines how those member
     *  groups have changed and, based on how they have changed, whether 
     *  (or not) to send a discarded event or a changed event to the
     *  appropriate registered listeners.
     *  <p>
     *  When the element to process is a CheckReachabilityMarker, the
     *  element was a result of a determination that the multicast 
     *  announcements from an already-discovered lookup service have
     *  stopped being received (see the AnnouncementTimerThread class).
     *  In this case, this task determines if the affected lookup service
     *  is still available ("reachable"). If this task cannot communicate
     *  with the lookup service, a discarded event is queued to be sent
     *  to all registered listeners.
     *  <p>
     *  Rather than performing unicast discovery synchronously, after multicast
     *  discovery has occurred in either the AnnouncementListener thread 
     *  (the multicast announcement protocol) or the ResponseListener thread
     *  (the multicast request protocol), the unicast discovery processing 
     *  that is required to complete the discovery process is queued for
     *  asynchronous execution in this task. Unicast discovery is performed
     *  asynchronously because unicast discovery can take quite a while to
     *  fail if a lookup service "disappears" (because the network or the
     *  lookup service itself has crashed) between the time a multicast
     *  announcement or response indicates the existence of a lookup service
     *  eligible for unicast discovery, and the time unicast discovery 
     *  actually starts. If unicast discovery is performed synchronously
     *  in the threads that implement the multicast announcement and multicast
     *  request protocols, other multicast announcements (as well as other
     *  unicast discoveries) will be missed whenever a lookup service
     *  disappears prior to the commencement of the unicast discovery stage.
     */
    private class UnicastDiscoveryTask implements TaskManager.Task {
        public UnicastDiscoveryTask() { }

	public void run() {
            logger.finest("LookupDiscovery - UnicastDiscoveryTask started");
            while (true) {
		Object req = null;
		try {
                    /* If no more pendingDiscoveries to process, remove this
                     * this task from the TaskManager and return
                     */
		    synchronized (pendingDiscoveries) {
                        Iterator iter = pendingDiscoveries.iterator();
                        if(!iter.hasNext()) {
			    return;
			}//endif(!iter.hasNext)
                        req = iter.next();
                        /* Remove the current pendingDiscovery "task" from
                         * the set so that other unicastDiscoveryTasks don't
                         * try to process it.
                         */
                        iter.remove();
		    }//end sync(pendingDiscoveries)
		    Socket sock = null;
                    MulticastAnnouncement announcement = null;
                    UnicastResponse response = null;
		    if (req instanceof Socket) {
			sock = (Socket)req;
		    } else if(req instanceof LookupLocator) {
			LookupLocator loc = (LookupLocator)req;
			sock = new Socket(loc.getHost(), loc.getPort());
                    } else if(req instanceof CheckGroupsMarker) {
                        announcement = ((CheckGroupsMarker)req).announcement;
                    } else if(req instanceof CheckReachabilityMarker) { 
                        response = ((CheckReachabilityMarker)req).response;
                    }//endif
                    if(sock != null) {//do unicast stage of discovery protocol
                        UnicastResponse resp;
                        try {
                            sock.setSoTimeout(unicastDiscoveryConstraints.
				getUnicastSocketTimeout(
				    DEFAULT_SOCKET_TIMEOUT));
			    resp = doUnicastDiscovery(sock);
                        } finally {
                            try {
                                sock.close();
                            } catch (IOException e) { /* ignore */ }
                        }
                        maybeAddNewRegistrar(resp);
                    } else if(announcement != null) {//handle group changes
                        ServiceID srvcID = announcement.getServiceID();
		        synchronized (registrars) {
                            UnicastResponse resp =
				(UnicastResponse)registrars.get(srvcID);
                            if(resp != null) {
                                maybeSendEvent(resp, announcement.getGroups());
                            }//endif
                        }//end sync
                    } else if(response != null) {//test reachability
		        synchronized (registrars) {
                            maybeSendEvent(response, null);
                        }//end sync
                    }//endif
		} catch (InterruptedIOException e) {
                    logger.log(Levels.HANDLED,
                               "exception occurred during unicast discovery",
                               e);
		    break;
		} catch (Throwable e) {
                    logger.log(Level.INFO,
                               "exception occurred during unicast discovery",
                               e);
		}//end try/catch
	    }//end loop(true)
            logger.finest("LookupDiscovery - UnicastDiscoveryTask completed");
	}//end run

        /** Returns true if current instance must be run after task(s) in
         *  task manager queue.
         *  @param tasks the tasks to consider.
         *  @param size  elements with index less than size are considered.
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter
    }//end class UnicastDiscoveryTask

    /**
     * Construct a new lookup discovery object, set to discover the
     * given set of groups.  The set is represented as an array of
     * strings.  This array may be empty, which is taken as the empty
     * set, and discovery is not performed.  The reference passed in
     * may be null, which is taken as no set, and in which case
     * discovery of all reachable lookup services is performed.
     * Otherwise, the array contains the names of groups to discover.
     * The caller must have DiscoveryPermission for each group (or
     * for all groups, if the array is null).
     *
     * @param groups the set of group names to discover (null for no
     *               set, empty for no discovery)
     *
     * @throws java.lang.NullPointerException input array contains at least
     *         one null element
     *
     * @throws java.io.IOException an exception occurred in starting discovery
     *
     * @see #NO_GROUPS
     * @see #ALL_GROUPS
     * @see #setGroups
     * @see DiscoveryPermission
     */
    public LookupDiscovery(String[] groups) throws IOException {
        try {
            beginDiscovery(groups, EmptyConfiguration.INSTANCE);
        } catch(ConfigurationException e) { /* swallow this exception */ }
    }//end constructor

    /**
     * Constructs a new lookup discovery object, set to discover the
     * given set of groups, and having the given <code>Configuration</code>.
     * <p>
     * The set of groups to discover is represented as an array of
     * strings.  This array may be empty, which is taken as the empty
     * set, and discovery is not performed.  The reference passed in
     * may be <code>null</code>, which is taken as no set, and in which
     * case discovery of all reachable lookup services is performed.
     * Otherwise, the array contains the names of groups to discover.
     * The caller must have <code>DiscoveryPermission</code> for each
     * group (or for all groups, if the array is <code>null</code>).
     *
     * @param groups the set of group names to discover (null for no
     *               set, empty for no discovery)
     *
     * @param config an instance of <code>Configuration</code>, used to
     *               obtain the objects needed to configure the current
     *               instance of this class
     *
     * @throws java.lang.NullPointerException input array contains at least
     *         one <code>null</code> element or <code>null</code> is input
     *         for the configuration
     *
     * @throws java.io.IOException an exception occurred in starting discovery
     *
     * @throws net.jini.config.ConfigurationException indicates an exception
     *         occurred while retrieving an item from the given
     *         <code>Configuration</code>
     *
     * @see #NO_GROUPS
     * @see #ALL_GROUPS
     * @see #setGroups
     * @see DiscoveryPermission
     * @see net.jini.config.Configuration
     * @see net.jini.config.ConfigurationException
     */
    public LookupDiscovery(String[] groups, Configuration config)
                                   throws IOException, ConfigurationException
    {
        beginDiscovery(groups, config);
    }//end constructor

    /**
     * Register a listener as interested in receiving DiscoveryEvent
     * notifications.
     *
     * @param l the listener to register
     *
     * @throws java.lang.NullPointerException this exception occurs when
     *         <code>null</code> is input to the listener parameter
     *         <code>l</code>.
     *
     * @throws java.lang.IllegalStateException this exception occurs when
     *         this method is called after the <code>terminate</code>
     *         method has been called.
     * 
     * @see DiscoveryEvent
     * @see #removeDiscoveryListener
     */
    public void addDiscoveryListener(DiscoveryListener l) {
        if(l == null) {
            throw new NullPointerException("can't add null listener");
        }
	synchronized (registrars) {
            if (terminated) {
                throw new IllegalStateException("discovery terminated");
            }
	    if (listeners.indexOf(l) >= 0) return; //already have this listener
	    listeners.add(l);
	    if (registrars.isEmpty()) return;//nothing to send the new listener
            HashMap groupsMap = new HashMap(registrars.size());
	    Iterator iter = registrars.values().iterator();
	    for (int i = 0; iter.hasNext(); i++) {
                UnicastResponse resp = (UnicastResponse)iter.next();
                groupsMap.put(resp.getRegistrar(),resp.getGroups());
	    }
	    ArrayList list = new ArrayList(1);
	    list.add(l);
	    addNotify(list, groupsMap, DISCOVERED);
	}
    }//end addDiscoveryListener

    /**
     * Indicate that a listener is no longer interested in receiving
     * DiscoveryEvent notifications.
     *
     * @param l the listener to unregister
     *
     * @throws java.lang.IllegalStateException this exception occurs when
     *         this method is called after the <code>terminate</code>
     *         method has been called.
     * 
     * @see #addDiscoveryListener
     */
    public void removeDiscoveryListener(DiscoveryListener l) {
	synchronized (registrars) {
            if (terminated) {
                throw new IllegalStateException("discovery terminated");
            }
	    listeners.remove(l);
	}
    }//end removeDiscoveryListener

    /**
     * Returns an array of instances of <code>ServiceRegistrar</code>, each
     * corresponding to a proxy to one of the currently discovered lookup
     * services. For each invocation of this method, a new array is returned.
     *
     * @return array of instances of <code>ServiceRegistrar</code>, each
     *         corresponding to a proxy to one of the currently discovered
     *         lookup services
     *
     * @throws java.lang.IllegalStateException this exception occurs when
     *         this method is called after the <code>terminate</code>
     *         method has been called.
     * 
     * @see net.jini.core.lookup.ServiceRegistrar
     * @see net.jini.discovery.DiscoveryManagement#removeDiscoveryListener
     */
    public ServiceRegistrar[] getRegistrars() {
        synchronized (registrars) {
            if (terminated) {
                throw new IllegalStateException("discovery terminated");
            }
            if (registrars.isEmpty()) {
                return new ServiceRegistrar[0];
            }
            Iterator iter = registrars.values().iterator();
            ServiceRegistrar[] regs = new ServiceRegistrar[registrars.size()];
            for (int i=0;iter.hasNext();i++) {
                regs[i] = ((UnicastResponse)iter.next()).getRegistrar();
            }
            return regs;
        }
    }//end getRegistrars

    /**
     * Discard a registrar from the set of registrars already
     * discovered.  This does not prevent that registrar from being
     * rediscovered; it is intended to be used to clear unreachable
     * entries from the set. <p>
     *
     * If the registrar has been discovered using this LookupDiscovery
     * object, each listener registered with this object will have its
     * discarded method called with the given registrar as parameter.
     *
     * @param reg the registrar to discard
     *
     * @throws java.lang.IllegalStateException this exception occurs when
     *         this method is called after the <code>terminate</code>
     *         method has been called.
     * 
     * @see DiscoveryListener#discarded
     */
    public void discard(ServiceRegistrar reg) {
        synchronized (registrars) {
            if (terminated) {
                throw new IllegalStateException("discovery terminated");
            }
            if(reg == null) return;
            sendDiscarded(reg,null);
        }//end sync
    }//end discard

    /** Terminate the discovery process. */
    public void terminate() {
	synchronized (registrars) {
	    if (terminated)  return;
	    terminated = true;
	}
	nukeThreads();
    }//end terminate

    /**
     * Return the set of group names this LookupDiscovery instance is
     * trying to discover.  If this method returns the empty array,
     * that value is guaranteed to be referentially equal to
     * LookupDiscovery.NO_GROUPS.
     *
     * @return the set of groups to be discovered (null for all, empty
     *         for no discovery)
     *
     * @throws java.lang.IllegalStateException this exception occurs when
     *         this method is called after the <code>terminate</code>
     *         method has been called.
     * 
     * @see #NO_GROUPS
     * @see #ALL_GROUPS
     * @see #setGroups
     */
    public String[] getGroups() {
	synchronized (registrars) {
            if (terminated) {
                throw new IllegalStateException("discovery terminated");
            }
	    if (groups == null)
		return ALL_GROUPS;
	    if (groups.isEmpty())
		return NO_GROUPS;
	    return collectionToStrings(groups);
	}
    }//end getGroups

    /**
     * Add a set of groups to the set to be discovered.
     * The caller must have DiscoveryPermission for each group.
     *
     * @param newGroups the groups to add
     *
     * @throws java.io.IOException the multicast request protocol failed
     *         to start
     *
     * @throws java.lang.IllegalStateException this exception occurs when
     *         this method is called after the <code>terminate</code>
     *         method has been called.
     * 
     * @throws java.lang.UnsupportedOperationException there is no set of
     *         groups to add to
     *
     * @see DiscoveryPermission
     */
    public void addGroups(String[] newGroups) throws IOException {
        testArrayForNullElement(newGroups);
	checkGroups(newGroups);
	synchronized (registrars) {
	    if (terminated)
		throw new IllegalStateException("discovery terminated");
	    if (groups == null)
		throw new UnsupportedOperationException(
					  "can't add to \"any groups\"");
	    Collection req = new ArrayList(newGroups.length);
	    for (int i = 0; i < newGroups.length; i++) {
		if (groups.add(newGroups[i]))
		    req.add(newGroups[i]);
	    }
	    if (!req.isEmpty())
		requestGroups(req);
	}
    }//end addGroups

    /**
     * Change the set of groups to be discovered to correspond to the
     * given set.  The set is represented as an array of strings.
     * This array may be empty, which is taken as the empty set, and
     * discovery is not performed.  The reference passed in may be
     * null, which is taken as no set, and in which case discovery of
     * all reachable lookup services is performed.  Otherwise, the
     * array contains the names of groups to discover.
     * The caller must have DiscoveryPermission for each group (or
     * for all groups, if the array is null).
     *
     * @param newGroups the new set of groups to discover (null for
     *                  all, empty array for no discovery)
     *
     * @throws java.io.IOException an exception occurred when starting
     *         multicast discovery
     *
     * @throws java.lang.IllegalStateException this exception occurs when
     *         this method is called after the <code>terminate</code>
     *         method has been called.
     * 
     * @see #LookupDiscovery
     * @see #ALL_GROUPS
     * @see #NO_GROUPS
     * @see DiscoveryPermission
     * @see #getGroups
     */
    public void setGroups(String[] newGroups) throws IOException {
        testArrayForNullElement(newGroups);
	checkGroups(newGroups);
	boolean maybeDiscard = false;
	Set newGrps = null;
	if (newGroups != null) {	
	    newGrps = new HashSet(newGroups.length * 2);
	    for (int i = 0; i < newGroups.length; i++) {
		newGrps.add(newGroups[i]);
	    }
	}
	synchronized (registrars) {
	    if (terminated)
		throw new IllegalStateException("discovery terminated");
	    if (newGroups == null) {
		if (groups != null) {
		    groups = null;
		    requestGroups(null);
		}
		return;
	    }
	    if (groups == null) {
		groups = new HashSet(11);
		maybeDiscard = true;
	    }
	    Set toAdd = new HashSet(newGrps);
	    toAdd.removeAll(groups);
	    // Figure out which groups to get rid of.  We start off
	    // with the full set for which we are already listening,
	    // and eliminate any that are in both the new set and the
	    // current set.
	    Collection toRemove = new HashSet(groups);
	    toRemove.removeAll(newGrps);
	    // Add new groups before we remove any old groups, because
	    // removeGroups will start a new round of multicast requests
	    // if the set of groups becomes empty, and we don't want it
	    // to do so without reason.
	    groups.addAll(toAdd);
	    if (!toRemove.isEmpty())
		maybeDiscard |= removeGroupsInt(collectionToStrings(toRemove));
	    if (!toAdd.isEmpty())
		requestGroups(toAdd);
	}
	if (maybeDiscard)
	    maybeDiscardRegistrars();
    }//end setGroups

    /**
     * Remove a set of groups from the set to be discovered.
     *
     * @param oldGroups groups to remove
     *
     * @throws java.lang.IllegalStateException this exception occurs when
     *         this method is called after the <code>terminate</code>
     *         method has been called.
     * 
     * @throws java.lang.UnsupportedOperationException there is no set of
     *         groups from which to remove
     */
    public void removeGroups(String[] oldGroups) {
        testArrayForNullElement(oldGroups);
	boolean maybeDiscard;
	synchronized (registrars) {
	    if (terminated)
		throw new IllegalStateException("discovery terminated");
	    if (groups == null)
		throw new UnsupportedOperationException(
					   "can't remove from \"any groups\"");
	    maybeDiscard = removeGroupsInt(oldGroups);
	}
	if (maybeDiscard)
	    maybeDiscardRegistrars();
    }//end removeGroups

    /**
     * Sends the given packet data on the given <code>MulticastSocket</code>
     * through each of the network interfaces corresponding to elements of
     * the array configured when this utility was constructed.
     *
     * @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 void sendPacketByNIC(MulticastSocket mcSocket,
                                 DatagramPacket[] packet)
                                                 throws InterruptedIOException
    {
        switch(nicsToUse) {
            case NICS_USE_ALL:
                /* Using all interfaces. Skip (but report) any interfaces
                 * that are "bad" or not configured for multicast.
                 */
                for(int i=0;i<nics.length;i++) {
                    try {
                        mcSocket.setNetworkInterface(nics[i]);
                        sendPacket(mcSocket,packet);
                    } catch(InterruptedIOException e) {
                        throw e;//to signal a graceful exit
                    } catch(IOException e) {
                        if( logger.isLoggable(Levels.HANDLED) ) {
                            LogRecord logRec = 
                              new LogRecord(Levels.HANDLED,
					    "network interface is "
                                            +"bad or not configured for "
                                            +"multicast: {0}");
                            logRec.setParameters(new Object[]{nics[i]});
                            logRec.setThrown(e);
                            logger.log(logRec);
                        }//endif
                    } catch(Exception e) {
                        if( logger.isLoggable(Levels.HANDLED) ) {
                            LogRecord logRec = 
                              new LogRecord(Levels.HANDLED, "exception while "
                                            +"sending packet through network "
                                            +"interface: {0}");
                            logRec.setParameters(new Object[]{nics[i]});
                            logRec.setThrown(e);
                            logger.log(logRec);
                        }//endif
                    }
                }//end loop
                break;
            case NICS_USE_LIST:
                /* Using a configured list of specific interfaces. Skip (but
                 * always report) any interfaces that are "bad" or not
                 * configured for multicast.
                 */
                for(int i=0;i<nics.length;i++) {
                    try {
                        mcSocket.setNetworkInterface(nics[i]);
                        sendPacket(mcSocket,packet);
                    } catch(InterruptedIOException e) {
                        throw e;//to signal a graceful exit
                    } catch(IOException e) {
                        if( logger.isLoggable(Level.SEVERE) ) {
                            LogRecord logRec = 
                              new LogRecord(Level.SEVERE,"network interface "
                                            +"is bad or not configured for "
                                            +"multicast: {0}");
                            logRec.setParameters(new Object[]{nics[i]});
                            logRec.setThrown(e);
                            logger.log(logRec);
                        }//endif
                    } catch(Exception e) {
                        if( logger.isLoggable(Level.SEVERE) ) {
                            LogRecord logRec = 
                              new LogRecord(Level.SEVERE,"exception while "
                                            +"sending packet through network "
                                            +"interface: {0}");
                            logRec.setParameters(new Object[]{nics[i]});
                            logRec.setThrown(e);
                            logger.log(logRec);
                        }//endif
                    }
                }//end loop
                break;
            case NICS_USE_SYS:
                /* Using the system-dependent default interface. Don't need
                 * to specifically set the interface. If that interface is
                 * "bad" or not configured for multicast, always report it.
                 */
                try {
                    sendPacket(mcSocket,packet);
                } catch(InterruptedIOException e) {
                    throw e;//to signal a graceful exit
                } catch(IOException e) {
                    if( logger.isLoggable(Level.SEVERE) ) {
                        logger.log(Level.SEVERE, "system default network "
                                   +"interface is bad or not configured "
                                   +"for multicast", e);
                    }//endif
                } catch(Exception e) {
                    if( logger.isLoggable(Level.SEVERE) ) {
                        logger.log(Level.SEVERE, "exception while sending "
                                   +"packet through system default network "
                                   +"interface", e);
                    }//endif
                }
                break;
            case NICS_USE_NONE:
                break;//multicast disabled, do nothing
            default:
                throw new AssertionError("nicsToUse flag out of range (0-3): "
                                         +nicsToUse);
        }//end switch(nicsToUse)
    }//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.IOException
     */
    private static void sendPacket(MulticastSocket mcSocket,
                                   DatagramPacket[] packet) throws IOException
    {
        for(int i=0;i<packet.length;i++) {
            mcSocket.send(packet[i]);
        }//end loop
    }//end sendPacket

    /** Returns the local host name. */
    private static String getLocalHost() throws UnknownHostException {
	try {
	    return ((InetAddress) Security.doPrivileged(
		new PrivilegedExceptionAction() {
		    public Object run() throws UnknownHostException {
			return InetAddress.getLocalHost();
		    }
		})).getHostAddress();
	} catch (PrivilegedActionException e) {
	    throw (UnknownHostException) e.getCause();
	}
    }

    /** Determines if the caller has discovery permission for each group. */
    private static void checkGroups(String[] groups) {
	SecurityManager sm = System.getSecurityManager();
	if (sm == null)  return;
	if (groups != null) {
	    for (int i = 0; i < groups.length; i++) {
		sm.checkPermission(new DiscoveryPermission(groups[i]));
	    }//end loop
	} else {
	    sm.checkPermission(new DiscoveryPermission("*"));
	}//endif
    }//end checkGroups

    /** Converts a collection to an array of strings. */
    private static final String[] collectionToStrings(Collection c) {
	return c == null ? null : (String[]) c.toArray(new String[c.size()]);
    }//end collectionToStrings

    /** Determines if two sets of registrar member groups have identical
     *  contents. Assumes there are no duplicates, and the sets can never
     *  be null.
     *
     *  @param groupSet0    <code>String</code> array containing the group
     *                      names from the first set used in the comparison
     *  @param groupSet1    <code>String</code> array containing the group
     *                      names from the second set used in the comparison
     * 
     *  @return <code>true</code> if the contents of each set is identical;
     *          <code>false</code> otherwise
     */
    private static boolean groupSetsEqual(String[] groupSet0,
                                          String[] groupSet1)
    {
        if(groupSet0.length != groupSet1.length) return false;
        /* is every element of one set contained in the other set? */
        iLoop:
        for(int i=0;i<groupSet0.length;i++) {
            for(int j=0;j<groupSet1.length;j++) {
                if( groupSet0[i].equals(groupSet1[j]) ) {
                    continue iLoop;
                }
            }//end loop(j)
            return false;
        }//end loop(i)
        return true;
    }//end groupSetsEqual

    /** Returns true if the registrars contained in the given (possibly null)
     *  UnicastResponse instances are equals() to one another.
     */
    private static boolean registrarsEqual(UnicastResponse resp1,
					   UnicastResponse resp2)
    {
	return resp1 != null && resp2 != null &&
	       resp2.getRegistrar().equals(resp1.getRegistrar());
    }//end registrarsEqual

    /**
     * Remove the specified groups from the set of groups to discover, and
     * return true if any were actually removed.
     */
    private boolean removeGroupsInt(String[] oldGroups) {
	boolean removed = false;
	for (int i = 0; i < oldGroups.length; i++) {
	    removed |= groups.remove(oldGroups[i]);
	}
	return removed;
    }//end removeGroupsInt

    /** Returns the service IDs of the lookup service(s) discovered to date. */
    private ServiceID[] getServiceIDs() {
	synchronized (registrars) {
	    return (ServiceID[])
		registrars.keySet().toArray(new ServiceID[registrars.size()]);
	}//end sync
    }//end getServiceIDs

    /**
     * Indicate whether any of the group names in the given array match
     * any of the groups of interest.
     *
     * @param possibilities the set of group names to compare to the set
     *                      of groups to discover (must not be null)
     */
    private boolean groupsOverlap(String[] possibilities) {
	/* Match if we're interested in any group, or if we're
	 * interested in none and there are no possibilities.
         */
	if (groups == null)  return true;
	for (int i = 0; i < possibilities.length; i++) {
	    if (groups.contains(possibilities[i]))  return true;
	}//end loop
	return false;
    }//end groupsOverlap

    /** Called at startup and whenever the set of groups to discover is
     *  changed. This method executes the multicast request protocol by
     *  starting the ResponseListener thread to listen for multicast 
     *  responses; and starting a Requestor thread to send out multicast
     *  requests for the set of groups contained in the given Collection.
     */
    private void requestGroups(final Collection req) throws IOException {
	try {
            Security.doPrivileged(new PrivilegedExceptionAction() {
		public Object run() throws Exception {
		    Thread t;
		    synchronized (requestors) {
			if (respondeeThread == null) {
			    respondeeThread = new ResponseListener();
			    respondeeThread.start();
			}//endif
			t = new Requestor(collectionToStrings(req),
					  respondeeThread.getPort());
			requestors.add(t);
		    }//end sync
		    t.start();
		    return null;
		}//run
	    });//end doPrivileged
	} catch (PrivilegedActionException e) {
	    throw (IOException)e.getException();
	}//end try/catch
    }//end requestGroups

    /**
     * If the lookup service associated with the given UnicastResponse
     * is not in the set of already-discovered lookup services, this method
     * adds it to that set, and each registered listener is notified.
     *
     * @param resp the UnicastResponse associated with the lookup
     *             service to add
     */
    private void maybeAddNewRegistrar(UnicastResponse resp) {
        /* If the group names contained in the given incoming unicast response
         * don't match any of the groups of interest, then don't waste time 
         * performing an unnecessary proxy preparation; simply return.
         */
        synchronized(registrars) {
            if( !groupsOverlap(resp.getGroups()) )  return;
        }//end sync(registrars)

        /* Proxy preparation - 
         *
         * The given incoming unicast response contains a proxy to a lookup
         * service that belongs to at least one of the groups of interest.
         * Before adding that proxy to the managed set of discovered lookup
         * services, and before notifying any of the registered listeners,
         * that proxy should be prepared. This is necessary in this utility
         * because that lookup service may be tested for reachability at
         * some point. Since that test involves a remote call (to getGroups())
         * through the proxy, the proxy should be prepared.
         *
         * The preparation of that proxy is performed inside a doPrivileged
         * block that restores the access control context that was in place
         * when this utility was created. In this way, any code that is
         * executed as a part of preparing the proxy will be executed with
         * no additional permissions beyond the permissions that were granted
         * to the client that created this utility. This is done because the
         * proxy preparer executed below is provided by the deployer and thus
         * can be viewed as an artifact of the client. Therefore, before
         * executing the preparer's code in this utility, the client's
         * Subject should be restored, and the preparer code should be
         * restricted to doing nothing more than the client itself is
         * allowed to do.
         *
         * Note that it's okay to modify the state of the given incoming
         * unicast response here because, prior to modification and storage
         * in the managed set of registrars in this method, it is assumed that
         * that object is not accessed by any other thread.
         */
        try {
	    final ServiceRegistrar srcReg = resp.getRegistrar();
            ServiceRegistrar prepReg
		= (ServiceRegistrar)AccessController.doPrivileged
		    ( securityContext.wrap( new PrivilegedExceptionAction() {
                        public Object run() throws RemoteException {
                            Object proxy = registrarPreparer.prepareProxy
                                                            (srcReg);
                            logger.log(Level.FINEST, "LookupDiscovery - "
                                       +"prepared lookup service proxy: {0}",
                                       proxy);
                            return proxy;
                        }//end run
                    }),//end PrivilegedExceptionAction and wrap
                  securityContext.getAccessControlContext());//end doPrivileged
	    if (prepReg != srcReg) {
		resp = new UnicastResponse(resp.getHost(),
					   resp.getPort(),
					   resp.getGroups(),
					   prepReg);
	    }
	} catch (Exception e) {
            Exception e1 = ( (e instanceof PrivilegedActionException) ? 
                            ((PrivilegedActionException)e).getException() : e);
            logger.log(Level.INFO,
                       "exception while preparing lookup service proxy",
                       e1);
	    return;
	}
        /* Add any newly discovered registrars to the managed set and notify
         * all listeners.
         */
	synchronized (registrars) {
	    if(groupsOverlap(resp.getGroups()) &&
	       !registrarsEqual(resp,
				(UnicastResponse) registrars.put
				   (resp.getRegistrar().getServiceID(), resp)))
	    {
                /* Time stamp the service ID. The first time stamp associated
                 * with the current service ID occurs here. All other time
                 * stamps for that service ID will occur when multicast
                 * announcements for that service ID arrive (in the
                 * AnnouncementListener thread).
                 *
                 * Note that if the time stamp for the service ID were
                 * initialized upon the arrival of the first announcement,
                 * rather than here when it is first discovered, the
                 * AnnouncementTimerThread would not be able to detect the
                 * termination of announcements for the case where the 
                 * termination happens to occur between the time the lookup 
                 * is first discovered here, and the time the first
                 * announcement was supposed to have arrived. This can
                 * happen because a multicast request from the client can
                 * cause the lookup to be discovered before the first
                 * announcement arrives.
                 */
                regTimes.put(resp.getRegistrar().getServiceID(),
                             new Long(System.currentTimeMillis()) );
                if(!listeners.isEmpty()) {
		    addNotify((ArrayList)listeners.clone(),
                              mapRegToGroups(resp.getRegistrar(),
					     resp.getGroups()),
                              DISCOVERED);
                }//endif
	    }//endif
	}//end sync(registrars)
    }//end maybeAddNewRegistrar

    /** Determine if any of the already-discovered registrars are no longer
     *  members of any of the groups to discover, and discard those registrars
     *  that are no longer members of any of those groups.
     */
    private void maybeDiscardRegistrars() {
	synchronized (registrars) {
            HashMap groupsMap = new HashMap(registrars.size());
	    for(Iterator iter=registrars.values().iterator();iter.hasNext(); ){
		UnicastResponse ent = (UnicastResponse)iter.next();
		if(!groupsOverlap(ent.getGroups())) { // not interested anymore
                    groupsMap.put(ent.getRegistrar(),ent.getGroups());
                    regTimes.remove(ent.getRegistrar().getServiceID());
		    iter.remove(); // remove (srvcID,response) mapping
		}//endif
	    }//end loop
            if( !groupsMap.isEmpty() && !listeners.isEmpty() ) {
		addNotify((ArrayList)listeners.clone(), groupsMap, DISCARDED);
	    }//endif
	}//end sync
    }//end maybeDiscardRegistrars

    /**
     * Add a notification task to the pending queue, and start an instance of
     * the Notifier thread if one isn't already running.
     */
    private void addNotify(ArrayList notifies, Map groupsMap, int eventType) {
	synchronized (pendingNotifies) {
	    pendingNotifies.addLast(new NotifyTask(notifies,
                                                   groupsMap,
                                                   eventType));
	    if (notifierThread == null) {
                Security.doPrivileged(new PrivilegedAction() {
		    public Object run() {
			notifierThread = new Notifier();
			notifierThread.start();
			return null;
		    }//end run
		});//end doPrivileged
	    }//endif
	}//end sync
    }//end addNotify

    /** Terminates (interrupts) all currently-running threads. */
    private void nukeThreads() {
        Security.doPrivileged(new PrivilegedAction() {
	    public Object run() {
                if(announcementTimerThread != null) {
                    announcementTimerThread.interrupt();
                }//endif
		synchronized (requestors) {
		    for (Iterator iter = requestors.iterator();
			 iter.hasNext(); )
		    {
			Thread t = (Thread) iter.next();
			t.interrupt();
		    }
		    if (respondeeThread != null)
			respondeeThread.interrupt();
		}
                if(announceeThread != null) {
                    announceeThread.interrupt();
                }//endif
		synchronized (pendingDiscoveries) {
                    terminateTaskMgr();
		}//end sync
		return null;
	    }//end run
	});//end doPrivileged
    }//end nukeThreads

    /** This method removes all pending and active tasks from the TaskManager
     *  for this instance. It also clears the set of pendingDiscoveries, and
     *  closes all associated sockets.
     */ 
    private void terminateTaskMgr() {
        synchronized(taskManager) {
            /* Remove all pending tasks */
            ArrayList pendingTasks = taskManager.getPending();
            for(int i=0;i<pendingTasks.size();i++) {
                taskManager.remove((TaskManager.Task)pendingTasks.get(i));
            }//end loop
            /* Clear pendingDiscoveries and close all associated sockets */
            synchronized (pendingDiscoveries) {
                for(Iterator iter = pendingDiscoveries.iterator();
                    iter.hasNext();)
                {
                    Object req = iter.next();
                    iter.remove();
                    if (req instanceof Socket) {
                        try {
                            ((Socket)req).close();
                        } catch (IOException e) { /* ignore */ }
                    }//endif
                }//end loop
            }//end sync
            /* Interrupt active TaskThreads, prepare the taskManager for GC. */
            taskManager.terminate();
        }//end sync(taskManager)
        synchronized(pendingNotifies) {
            pendingNotifies.clear();
        }//end sync
    }//end terminateTaskMgr

    /** After a possible change in the member groups of the 
     *  <code>ServiceRegistrar</code> corresponding to the given 
     *  <code>UnicastResponse</code> parameter, this method
     *  determines whether or not the registrar's member groups have
     *  changed in such a way that either a changed event or a discarded
     *  event is warranted.
     *  <p>
     *  Note that even if the contents of the new set of groups initially 
     *  indicate that the corresponding registrar is a candidate for
     *  a discarded or a changed event, further analysis must be performed.
     *  This is because there is no guarantee that the new set of member
     *  groups have not been "split" across the multicast announcements
     *  sent by the lookup service; and so there is no guarantee that the
     *  contents of the new group set actually reflect a change that warrants
     *  an event. To guarantee that the new group set accurately reflects
     *  the registrar's member groups, this method makes a remote call to
     *  the registrar to retrieve its actual member groups.
     *  <p>
     *  There is one situation where it is not necessary to query the
     *  registrar for its current member groups. That situation is 
     *  when the set of groups input to the <code>newGroups</code> parameter
     *  is equivalent to NO_GROUPS. If that new group set is equivalent
     *  to NO_GROUPS, it is guaranteed that the registrar's member groups
     *  have not been split across the multicast announcements.
     *
     * @param response  instance of <code>UnicastResponse</code> 
     *                  corresponding to the registrar whose current and
     *                  previous member groups are to be compared
     * @param newGroups <code>String</code> array containing the new
     *                  member groups of the registrar corresponding to the 
     *                  <code>response</code> parameter (just after a
     *                  possible change)
     */
    private void maybeSendEvent(UnicastResponse response, String[] newGroups) {
        ServiceRegistrar reg = response.getRegistrar();
        String[] oldGroups   = response.getGroups();
        boolean getActual    = true;
        if(newGroups == null) { // newGroups null means get actual groups now
            newGroups = getActualGroups(reg);
            if(newGroups == null) return; // if null, then it was discarded
            getActual = false;
        }//endif

        if(groupSetsEqual(oldGroups,newGroups)) return;

        String[] actualGroups = newGroups;
        if( getActual && (newGroups.length > 0) ) {
            actualGroups = getActualGroups(reg);
            if(actualGroups == null) return; // null ==> was already discarded
        }//endif
        notifyOnGroupChange(reg, oldGroups, actualGroups);
    }//end maybeSendEvent

    /** After a possible change in the member groups of the given
     *  <code>ServiceRegistrar</code> parameter, this method compares
     *  the registrar's original set of member groups to its new set
     *  of member groups.
     *  <p>
     *  If the criteria shown below is satisfied, either a discarded event
     *  or a changed event will be sent to any registered listeners. The
     *  criteria is based on whether the old and new groups are equal,
     *  and whether one or more elements of the new group set also belong
     *  to the set of groups to discover (the new groups are "still of
     *  interest"). The criteria is as follows:
     *  <p>
     *  if (old groups and new groups)
     *  <p><ul>
     *       <li> (not equal but stillInterested) --> send a changed event
     *       <li> (!stillInterested)              --> send a discarded event
     *    </ul>
     *  <p>
     *
     * @param reg       instance of <code>ServiceRegistrar</code> 
     *                  corresponding to the registrar whose current and
     *                  previous member groups are to be compared; and 
     *                  whose corresponding service ID is used as the key
     *                  into the various data structures that contain
     *                  pertinent information about that registrar
     * @param oldGroups <code>String</code> array containing the member
     *                  groups of the <code>reg</code> parameter prior to
     *                  being changed
     * @param oldGroups <code>String</code> array containing the current
     *                  member groups of the <code>reg</code> parameter
     *                  (just after a possible change)
     */
    private void notifyOnGroupChange(ServiceRegistrar reg,
                                     String[]         oldGroups,
                                     String[]         newGroups)
    {
        boolean equal           = groupSetsEqual(oldGroups,newGroups);
        boolean stillInterested = groupsOverlap(newGroups);
        if(!equal && stillInterested) {
            sendChanged(reg,newGroups);
        } else if(!stillInterested) {
            sendDiscarded(reg,newGroups);
        }//endif
    }//end notifyOnGroupChange

    /** Convenience method that sends a discarded event containing only
     *  one registrar to all registered listeners. This method must be
     *  called from within a block that is synchronized on the registrars
     *  map.
     * 
     *  @param reg       instance of <code>ServiceRegistrar</code> 
     *                   corresponding to the registrar to include in the
     *                   event
     *  @param curGroups <code>String</code> array containing the current
     *                   member groups of the registrar referenced by the 
     *                   <code>reg</code> parameter
     */
    private void sendDiscarded(ServiceRegistrar reg, String[] curGroups) {
        ServiceID srvcID = reg.getServiceID();
        if(curGroups == null) { // discard request is from external source
            UnicastResponse resp = (UnicastResponse)registrars.get(srvcID);
            if(resp == null) return;
            curGroups = resp.getGroups();
        }//endif
        if( registrars.remove(srvcID) != null ) { 
            regTimes.remove(srvcID);
            if( !listeners.isEmpty() ) {
                addNotify((ArrayList)listeners.clone(),
                           mapRegToGroups(reg,curGroups), DISCARDED);
            }//endif
        }//endif
    }//end sendDiscarded

    /** Convenience method that sends a changed event containing only
     *  one registrar to all registered listeners that are interested in
     *  such events. This method must be called from within a block that
     *  is synchronized on the registrars map.
     * 
     *  @param reg       instance of <code>ServiceRegistrar</code> 
     *                   corresponding to the registrar to include in the
     *                   event
     *  @param curGroups <code>String</code> array containing the current
     *                   member groups of the registrar referenced by the 
     *                   <code>reg</code> parameter
     */
    private void sendChanged(ServiceRegistrar reg, String[] curGroups) {
        /* replace old groups with new; prevents repeated changed events */
        UnicastResponse resp = 
                   (UnicastResponse)registrars.get(reg.getServiceID());
	registrars.put(reg.getServiceID(),
		       new UnicastResponse(resp.getHost(),
					   resp.getPort(),
					   curGroups,
					   resp.getRegistrar()));
        if( !listeners.isEmpty() ) {
            addNotify((ArrayList)listeners.clone(), 
                       mapRegToGroups(reg,curGroups), CHANGED);
        }//endif
    }//end sendChanged

    /** Creates and returns a deep copy of the input parameter. This method
     *  assumes the input map is a HashMap of the registrar-to-groups mapping;
     *  and returns a clone not only of the map, but of each key-value pair
     *  contained in the mapping.
     *
     * @param groupsMap mapping from a set of registrars to the member groups
     *                  of each registrar 
     * 
     *  @return clone of the input map, and of each key-value pair contained
     *          in the input map
     */
    private Map deepCopy(HashMap groupsMap) {
        /* clone the input HashMap */
        HashMap newMap = (HashMap)(groupsMap.clone());
        /* clone the values of each mapping in place */
        Set eSet = newMap.entrySet();
        for(Iterator itr = eSet.iterator(); itr.hasNext(); ) {
            Map.Entry pair = (Map.Entry)itr.next();
            /* only need to clone the value of the order pair */
            pair.setValue( ((String[])pair.getValue()).clone() );
        }
        return newMap;
    }//end deepCopy

    /** This method retrieves from the given <code>ServiceRegistrar</code>,
     *  the current groups in which that registrar is a member. If the
     *  registrar is un-reachable, then this method will discard the
     *  registrar.
     *
     * @param ServiceRegistrar instance of <code>ServiceRegistrar</code> 
     *                         referencing the registrar whose member 
     *                         groups are to be retrieved and returned
     * 
     *  @return <code>String</code> array containing the current member groups
     *          of the registrar referenced by the <code>reg</code> parameter
     */
    private String[] getActualGroups(final ServiceRegistrar reg) {
        /* The retrieval of the member groups of the given ServiceRegistrar
         * is performed inside a doPrivileged block that restores the access
         * control context that was in place when this utility was created.
         * 
         * This is done because the call to getGroups() below is executed
         * on a proxy to the given ServiceRegistrar; which may be downloaded
         * code supplied by a 3rd party. With respect to downloaded, 3rd party
         * code, it is not desirable to allow such code to execute with more
         * priviledges than the client that created this utility. Therefore,
         * before executing getGroups() on the given proxy, the client's
         * Subject should be restored, and the proxy code should be
         * restricted to doing nothing more than the client itself is
         * allowed to do.
         */
        try {
            return (String[])AccessController.doPrivileged
                     ( securityContext.wrap(new PrivilegedExceptionAction()
                           { public Object run() throws RemoteException {
                                   return reg.getGroups();
                               }//end run
                           }),//end PrivilegedExceptionAction and wrap
                       securityContext.getAccessControlContext());//end doPriv
        } catch(Throwable e) {
            /* A RemoteException, wrapped in a PriviligedActionException,
             * occurred. This means that the reg is unreachable; discard it.
             */
            discard(reg);
            return null;
        }//end try

    }//end getActualGroups

    /** Convenience method that creates and returns a mapping of a single
     *  <code>ServiceRegistrar</code> instance to a set of groups.
     * 
     *  @param reg       instance of <code>ServiceRegistrar</code> 
     *                   corresponding to the registrar to use as the key
     *                   to the mapping
     *  @param curGroups <code>String</code> array containing the current
     *                   member groups of the registrar referenced by the 
     *                   <code>reg</code> parameter; and which is used
     *                   as the value of the mapping
     *
     *   @return <code>Map</code> instance containing a single mapping from
     *           a given registrar to its current member groups
     */
    private Map mapRegToGroups(ServiceRegistrar reg, String[] curGroups) {
        HashMap groupsMap = new HashMap(1);
        groupsMap.put(reg,curGroups);
        return groupsMap;
    }//end mapRegToGroups

    /**
     * This method is used by the public methods of this class that are
     * specified to throw a <code>NullPointerException</code> when the given
     * array of group names contains one or more <code>null</code> elements;
     * in which case, this method throws a <code>NullPointerException</code>
     * which should be allowed to propagate outward.
     * 
     * @throws java.lang.NullPointerException this exception occurs when
     *         one or more of the elements of the <code>groupArray</code>
     *         parameter is <code>null</code>.
     */
    private void testArrayForNullElement(String[] groupArray) {
        if(groupArray == null) return;
        for(int i=0;i<groupArray.length;i++) {
            if(groupArray[i] == null) {
                throw new NullPointerException("null element in group array");
            }//endif
        }//end loop
    }//end testArrayForNullElement

    /**
     * Using the given <code>Configuration</code>, initializes the current
     * instance of this utility, and initiates the discovery process for
     * the given set of groups.
     *
     * @param groups the set of group names to discover
     *
     * @param config an instance of <code>Configuration</code>, used to
     *               obtain the objects needed to configure this utility
     *
     * @throws java.lang.NullPointerException input array contains at least
     *         one <code>null</code> element or <code>null</code> is input
     *         for the configuration
     *
     * @throws java.io.IOException an exception occurred when initiating
     *         discovery processing
     *
     * @throws net.jini.config.ConfigurationException indicates an exception
     *         occurred while retrieving an item from the given
     *         <code>Configuration</code>
     */
    private void beginDiscovery(String[] groups, Configuration config)
                                    throws IOException, ConfigurationException
    {
        testArrayForNullElement(groups);
	checkGroups(groups);
	if (groups != null) {
	    this.groups = new HashSet(groups.length * 2);
	    for (int i = 0; i < groups.length; i++) {
		this.groups.add(groups[i]);
	    }//end loop
	}//endif
        init(config);
        if(nicsToUse ==  NICS_USE_NONE)  return;//disable discovery
	try {
            Security.doPrivileged(new PrivilegedExceptionAction() {
		public Object run() throws Exception {
		    announceeThread = new AnnouncementListener();
                    announcementTimerThread = new AnnouncementTimerThread();
		    return null;
		}//end run
	    });//end doPrivileged
	} catch (PrivilegedActionException e) {
	    throw (IOException)e.getException();
	}
	if (this.groups == null || !this.groups.isEmpty()) {
            requestGroups(this.groups);
        }//endif
	announceeThread.start();
	announcementTimerThread.start();
    }//end beginDiscovery

    /* Convenience method that encapsulates the retrieval of the configurable
     * items from the given <code>Configuration</code> object.
     */
    private void init(Configuration config) throws IOException,
                                                   ConfigurationException
    {
        if(config == null)  throw new NullPointerException("config is null");
        /* Lookup service proxy preparer */
        registrarPreparer = (ProxyPreparer)config.getEntry
                                                    (COMPONENT_NAME,
                                                     "registrarPreparer",
                                                     ProxyPreparer.class,
                                                     new BasicProxyPreparer());
	/* constraints */
	MethodConstraints constraints = (MethodConstraints)config.getEntry
						    (COMPONENT_NAME,
						     "discoveryConstraints",
						     MethodConstraints.class,
						     null);
	if (constraints == null) {
	    constraints = 
		new BasicMethodConstraints(InvocationConstraints.EMPTY);
	}
	multicastRequestConstraints = DiscoveryConstraints.process(
	    constraints.getConstraints(
		DiscoveryConstraints.multicastRequestMethod));
	multicastAnnouncementConstraints = DiscoveryConstraints.process(
	    constraints.getConstraints(
		DiscoveryConstraints.multicastAnnouncementMethod));
	unicastDiscoveryConstraints = DiscoveryConstraints.process(
	    constraints.getConstraints(
		DiscoveryConstraints.unicastDiscoveryMethod));

        /* Task manager */
        taskManager = new TaskManager(MAX_N_TASKS,(15*1000),1.0f);
        try {
            taskManager = (TaskManager)config.getEntry(COMPONENT_NAME,
						       "taskManager",
						       TaskManager.class,
						       taskManager);
        } catch(ConfigurationException e) { /* use default */ }

        /* Multicast request-related configuration items */
        multicastRequestMax
         = ( (Integer)config.getEntry
                             (COMPONENT_NAME,
                              "multicastRequestMax",
                              int.class,
                              new Integer(multicastRequestMax) ) ).intValue();
        multicastRequestInterval
         = ( (Long)config.getEntry
                            (COMPONENT_NAME,
                            "multicastRequestInterval",
                            long.class,
                            new Long(multicastRequestInterval) ) ).longValue();
        finalMulticastRequestInterval
         = ( (Long)config.getEntry
                      (COMPONENT_NAME,
                       "finalMulticastRequestInterval",
                       long.class,
                       new Long(finalMulticastRequestInterval) ) ).longValue();
	multicastRequestHost
	 = (String)config.getEntry(COMPONENT_NAME,
				   "multicastRequestHost",
				   String.class,
				   getLocalHost());
        /* Configuration items related to the network interface(s) */
        try {
            nics = (NetworkInterface[])config.getEntry
                                                   (COMPONENT_NAME,
                                                    "multicastInterfaces",
                                                    NetworkInterface[].class);
            if(nics == null) {
                nicsToUse = NICS_USE_SYS;
                logger.config("LookupDiscovery - using system default network "
                              +"interface for multicast");
            } else {//(nics != null)
                if( nics.length == 0 ) {
                    nicsToUse = NICS_USE_NONE;
                    logger.config("LookupDiscovery - MULTICAST DISABLED");
                } else {//(nics.length > 0), use the given specific list
                    nicsToUse = NICS_USE_LIST;
                    if( logger.isLoggable(Level.CONFIG) ) {
                        logger.log(Level.CONFIG,
                               "LookupDiscovery - multicast network "
                               +"interface(s): {0}", Arrays.asList(nics) );
                    }//endif
                }//endif
            }//endif
        } catch(NoSuchEntryException e) {// no config item, use default - all
	    Enumeration en = NetworkInterface.getNetworkInterfaces();
	    List nicList = (en != null) ?
		Collections.list(en) : Collections.EMPTY_LIST;
            nics = (NetworkInterface[])(nicList.toArray
                                     (new NetworkInterface[nicList.size()]) );
            nicsToUse = NICS_USE_ALL;
            if( logger.isLoggable(Level.CONFIG) ) {
                logger.log(Level.CONFIG,"LookupDiscovery - multicast network "
                                        +"interface(s): {0}", nicList);
            }//endif
        }
        nicRetryInterval
         = ( (Integer)config.getEntry
                                (COMPONENT_NAME,
                                 "multicastInterfaceRetryInterval",
                                 int.class,
                                 new Integer(nicRetryInterval) ) ).intValue();
        /* Multicast announcement-related configuration items */
        multicastAnnouncementInterval
         = ( (Long)config.getEntry
		      (COMPONENT_NAME,
		       "multicastAnnouncementInterval",
		       long.class,
		       new Long(multicastAnnouncementInterval) ) ).longValue();
    }//end init

    /**
     * Decodes received multicast announcement packet, applying configured
     * security constraints (if any).
     */
    private MulticastAnnouncement decodeMulticastAnnouncement(
						    final DatagramPacket pkt)
	throws IOException
    {
	// REMIND: cache recently received announcements to skip re-decoding?
	int pv;
	try {
	    pv = ByteBuffer.wrap(
		pkt.getData(), pkt.getOffset(), pkt.getLength()).getInt();
	} catch (BufferUnderflowException e) {
	    throw new DiscoveryProtocolException(null, e);
	}
	multicastAnnouncementConstraints.checkProtocolVersion(pv);
	final Discovery disco = getDiscovery(pv);

	try {
	    return (MulticastAnnouncement) AccessController.doPrivileged(
		securityContext.wrap(new PrivilegedExceptionAction() {
		    public Object run() throws IOException {
			return disco.decodeMulticastAnnouncement(
			    pkt,
			    multicastAnnouncementConstraints.
				getUnfulfilledConstraints());
		    }
		}), securityContext.getAccessControlContext());
	} catch (PrivilegedActionException e) {
	    throw (IOException) e.getCause();
	}
    }

    /**
     * Encodes outgoing multicast requests based on protocol in use, applying
     * configured security constraints (if any).
     */
    private DatagramPacket[] encodeMulticastRequest(final MulticastRequest req)
	throws IOException
    {
	// REMIND: cache latest request to skip re-encoding
	final Discovery disco = getDiscovery(
	    multicastRequestConstraints.chooseProtocolVersion());
	final List packets = new ArrayList();
	AccessController.doPrivileged(
	    securityContext.wrap(new PrivilegedAction() {
		public Object run() {
		    EncodeIterator ei = disco.encodeMulticastRequest(
			req, 
			multicastRequestConstraints.getMulticastMaxPacketSize(
			    DEFAULT_MAX_PACKET_SIZE),
			multicastRequestConstraints.getUnfulfilledConstraints()
		    );
		    while (ei.hasNext()) {
			try {
			    packets.addAll(Arrays.asList(ei.next()));
			} catch (Exception e) {
			    logger.log(
				(e instanceof UnsupportedConstraintException) ?
				    Levels.HANDLED : Level.INFO,
				"exception encoding multicast request", e);
			}
		    }
		    return null;
		}
	    }), securityContext.getAccessControlContext());

	if (packets.isEmpty()) {
	    throw new DiscoveryProtocolException("no encoded requests");
	}
	return (DatagramPacket[]) packets.toArray(
	    new DatagramPacket[packets.size()]);
    }

    /**
     * Performs unicast discovery over given socket based on protocol in use,
     * applying configured security constraints (if any).
     */
    private UnicastResponse doUnicastDiscovery(final Socket socket)
	throws IOException, ClassNotFoundException
    {
	final Discovery disco =
	    getDiscovery(unicastDiscoveryConstraints.chooseProtocolVersion());
	try {
	    return (UnicastResponse) AccessController.doPrivileged(
		securityContext.wrap(new PrivilegedExceptionAction() {
		    public Object run() throws Exception {
			return disco.doUnicastDiscovery(
			    socket, 
			    unicastDiscoveryConstraints.
				getUnfulfilledConstraints(),
			    null,
			    null,
			    null);
		    }
		}), securityContext.getAccessControlContext());
	} catch (PrivilegedActionException e) {
	    Throwable t = e.getCause();
	    if (t instanceof IOException) {
		throw (IOException) t;
	    } else if (t instanceof ClassNotFoundException) {
		throw (ClassNotFoundException) t;
	    } else {
		throw new AssertionError(t);
	    }
	}
    }

    /**
     * Returns Discovery instance for the given version, or throws
     * DiscoveryProtocolException if the version is unsupported.
     */
    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);
	}
    }

}//end class LookupDiscovery
