/*
 * 
 * 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 com.sun.jini.fiddler;

import com.sun.jini.fiddler.Fiddler;
import com.sun.jini.fiddler.FiddlerAdminProxy;
import com.sun.jini.fiddler.FiddlerProxy;
import com.sun.jini.fiddler.FiddlerRegistration;
import com.sun.jini.fiddler.FiddlerRenewResults;

import com.sun.jini.config.Config;

import com.sun.jini.constants.ThrowableConstants;
import com.sun.jini.constants.TimeConstants;
import com.sun.jini.constants.VersionConstants;

import com.sun.jini.logging.Levels;

import com.sun.jini.lookup.entry.BasicServiceType;
import com.sun.jini.lookup.entry.LookupAttributes;

import com.sun.jini.proxy.ThrowThis;

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

import com.sun.jini.start.LifeCycle;

import com.sun.jini.thread.ReadersWriter;
import com.sun.jini.thread.ReadersWriter.ConcurrentLockException;
import com.sun.jini.thread.TaskManager;

import net.jini.activation.ActivationExporter;
import net.jini.activation.ActivationGroup;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationProvider;
import net.jini.config.ConfigurationException;
import net.jini.config.NoSuchEntryException;

import net.jini.discovery.DiscoveryEvent;
import net.jini.discovery.DiscoveryChangeListener;
import net.jini.discovery.DiscoveryGroupManagement;
import net.jini.discovery.DiscoveryLocatorManagement;
import net.jini.discovery.DiscoveryManagement;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.discovery.LookupDiscoveryRegistration;
import net.jini.discovery.RemoteDiscoveryEvent;

import net.jini.export.Exporter;
import net.jini.export.ProxyAccessor;

import net.jini.id.Uuid;
import net.jini.id.UuidFactory;

import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.InvocationLayerFactory;
import net.jini.jeri.ServerEndpoint;
import net.jini.jeri.tcp.TcpServerEndpoint;

import net.jini.lookup.JoinManager;
import net.jini.lookup.entry.Comment;
import net.jini.lookup.entry.ServiceInfo;
import net.jini.lookup.entry.Status;
import net.jini.lookup.entry.StatusType;

import net.jini.security.BasicProxyPreparer;
import net.jini.security.ProxyPreparer;
import net.jini.security.TrustVerifier;
import net.jini.security.proxytrust.ServerProxyTrust;
import net.jini.security.proxytrust.TrustEquivalence;

import net.jini.core.discovery.LookupLocator;
import net.jini.core.entry.Entry;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEventListener;
import net.jini.core.lease.Lease;
import net.jini.core.lease.UnknownLeaseException;
import net.jini.core.lookup.ServiceID;
import net.jini.core.lookup.ServiceRegistrar;

import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import java.lang.reflect.Array;

import java.net.InetAddress;
import java.net.MalformedURLException;

import java.io.InterruptedIOException;
import java.io.InputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;

import java.rmi.activation.ActivationException;
import java.rmi.activation.ActivationID;
import java.rmi.activation.ActivationSystem;
import java.rmi.MarshalledObject;
import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;
import java.rmi.Remote;
import java.rmi.server.ExportException;
import java.rmi.server.RemoteObject;

import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This class is the server side of an implementation of the lookup
 * discovery service. Multiple client-side proxy classes are used to
 * interact and communicate with this backend server. Those proxy
 * classes are: <code>FiddlerProxy</code> (the proxy for the 
 * <code>LookupDiscoveryService</code> interface which defines how clients
 * register with the lookup discovery service), <code>FiddlerAdminProxy</code>
 * (the proxy for the <code>FiddlerAdmin</code> interface which specifies 
 * the methods through which administration duties such as joining, persistent
 * state logging policy, and shutting down the lookup discovery service can
 * be performed), and <code>FiddlerRegistration</code> (the proxy for the 
 * <code>LookupDiscoveryRegistration</code> interface which defines the
 * methods through which clients can perform duties such as group and
 * locator management, state retrieval, and discarding discovered but
 * unavailable lookup services so they are eligible for re-discovery).
 * <p>
 * It is through the proxies that communicate with this class that clients
 * interact with the lookup discovery service. When a client makes a method
 * invocation on one of the proxies, the proxy makes a corresponding call
 * on the methods specified in the <code>Fiddler</code> interface which
 * are implemented in this class, ultimately executing on the backend server.
 * <p>
 *
 * @com.sun.jini.impl
 *
 * This implementation of the lookup discovery service employs a number of
 * {@link java.util.logging.Logger}s. The details of each such
 * {@link java.util.logging.Logger} are described
 * <a href="./package-summary.html#fiddlerLoggers">here</a>.
 * <p>
 * The configuration entries supported by this implementation are described
 * <a href="./package-summary.html#fiddlerConfigEntries">here</a>.
 *
 * @author Sun Microsystems, Inc.
 */
class FiddlerImpl implements ServerProxyTrust, ProxyAccessor, Fiddler {

    /* Name of this component; used in config entry retrieval and the logger.*/
    private static final String COMPONENT_NAME = "com.sun.jini.fiddler";
    /* Loggers used by this implementation of the service. */
    static final Logger problemLogger      = Logger.getLogger(COMPONENT_NAME+
                                                              ".problem");
    static final Logger startupLogger      = Logger.getLogger(COMPONENT_NAME+
                                                              ".startup");
    static final Logger tasksLogger        = Logger.getLogger(COMPONENT_NAME+
                                                              ".tasks");
    static final Logger eventsLogger       = Logger.getLogger(COMPONENT_NAME+
                                                              ".events");
    static final Logger groupsLogger       = Logger.getLogger(COMPONENT_NAME+
                                                              ".groups");
    static final Logger locatorsLogger     = Logger.getLogger(COMPONENT_NAME+
                                                              ".locators");
    static final Logger discardLogger      = Logger.getLogger(COMPONENT_NAME+
                                                              ".discard");
    static final Logger leaseLogger        = Logger.getLogger(COMPONENT_NAME+
                                                              ".lease");
    static final Logger registrationLogger = Logger.getLogger(COMPONENT_NAME+
                                                              ".registration");
    static final Logger persistLogger      = Logger.getLogger(COMPONENT_NAME+
                                                              ".persist");
    /** Data structure - associated with a <code>ServiceRegistrar</code> -
     *  containing the <code>LookupLocator</code> and the member groups of
     *  the registrar
     */
    protected final class LocatorGroupsStruct {
        public LookupLocator locator;
        public String[]      groups;
        LocatorGroupsStruct(LookupLocator locator, String[] groups) {
            this.locator = locator;
            this.groups  = groups;
        }
    }//end class LocatorGroupsStruct

    /* ServiceInfo values */
    private static final String PRODUCT      = "Lookup Discovery Service";
    private static final String MANUFACTURER = "Sun Microsystems, Inc.";
    private static final String VENDOR       = MANUFACTURER;
    private static final String VERSION      = VersionConstants.SERVER_VERSION;

    /** When re-setting the bound on lease durations, that bound cannot be
     *  set to a value larger than this value */
    private static final long MAX_LEASE = 1000L * 60 * 60 * 24 * 365 * 1000;
    /** Log format version */
    private static final int LOG_VERSION = 2;

    /** The outer (smart) proxy to this server */
    private FiddlerProxy outerProxy;
    /** The inner proxy (stub or dynamic proxy) to this server */
    private Fiddler innerProxy;
    /** The admin proxy to this server */
    private FiddlerAdminProxy adminProxy;
    /** The service ID associated with this service when it is registered
     *  with any lookup service.
     */
    private ServiceID serviceID = null;
    /** The activation id of the current instance of the lookup discovery
     *  service, if it happens to be and activatable instance
     */
    private ActivationID activationID;
    /* Holds the prepared proxy to the ActivationSystem */
    private ActivationSystem activationSystem;
    /** The unique identifier generated (or recovered) when an instance of
     *  this service is constructed. This ID is typically used to determine
     *  equality between the proxies of any two instances of this service.
     */
    private Uuid proxyID = null;
    /** Map from the set of all currently discovered registrars to their 
     *  corresponding [locator,member groups] pairs (locatorGroupsStuct).
     */
    private final HashMap allDiscoveredRegs = new HashMap(11);
    /** Map from registrationID to registrationInfo.  Every registration is in
     *  this map under its registrationID.
     */
    private final HashMap registrationByID = new HashMap(11);
    /** Map from registrationInfo to registrationInfo (that is, to itself),
     *  where the elements of the map are ordered by lease expiration time.
     */
    private final TreeMap registrationByTime = new TreeMap();
    /** Performs all group and locator discovery on behalf of clients */
    private LookupDiscoveryManager discoveryMgr = null; 
    /** The listener registered for both group discovery events and locator
     *  discovery events.
     */
    private final LookupDiscoveryListener discoveryListener 
                                               = new LookupDiscoveryListener();
    /** For each registration created by the lookup discovery service, an
     *  event identifier that uniquely maps the registration to the
     *  registration's listener and managed sets will be generated and
     *  associated with the registration through the EventRegistration
     *  field of the registrationInfo. This event ID is unique across
     *  all registrations with the current instance of the lookup discovery
     *  service.
     */
    private long curEventID = 0;
    /** Earliest expiration time over all active registrations */
    private long minExpiration = Long.MAX_VALUE;
    /** The lookup discovery manager this service's join manager will use */
    private DiscoveryManagement joinMgrLDM;
    /** Manager for discovering and registering with lookup services */
    private JoinManager joinMgr;
    /** Task manager for sending remote discovery events */
    private TaskManager taskMgr;
    /** Registration lease expiration thread */
    private Thread leaseExpireThread;
    /** Snapshot-taking thread */
    private Thread snapshotThread;

    /** Concurrent object to control read and write access */
    private final ReadersWriter concurrentObj = new ReadersWriter();
    /** Object for synchronizing with the registration expire thread */
    private final Object leaseExpireThreadSyncObj = new Object();
    /** Object on which the snapshot-taking thread will synchronize */
    private final Object snapshotThreadSyncObj = new Object();

    /** Reliable log object to hold persistent state of the service.
     *  This object is also used as a flag: non-null ==> persistent service
     *                                      null ==> non-persistent service
     */
    private ReliableLog log = null;
    /** Flag indicating whether system is in a state of recovery */
    private boolean inRecovery;
    /** Current number of records in the Log File since the last snapshot */
    private int logFileSize = 0;
    /** The name of the directory to which the persistent modes of this service
     *  will log the service's state (using ReliableLog).
     */
    private String persistDir;
    /** least upper bound applied to all granted lease durations */
    private long leaseBound = 1000 * 60 * 30;
    /** Weight factor applied to snapshotSize when deciding to take snapshot */
    private float snapshotWt = 10;
    /** Log File must contain this many records before snapshot allowed */
    private int snapshotThresh = 200;
    /** Groups whose members are lookup services this service should join.
     *  Unless configured otherwise, this service will initially join the
     *  un-named public group. The desired join groups for this service can
     *  be set administratively after start up.
     */
    private String[] thisServicesGroups = new String[] {""};
    /** Locators of specific lookup services this service should join.
     *  This service will initially join no lookups found through locator
     *  discovery. The locators of the specific lookup services that are
     *  desired for this service to join should be set administratively
     *  after start up.
     */
    private LookupLocator[] thisServicesLocators = {};
    /** The attributes to use when joining lookup service(s) */
    private Entry[] thisServicesAttrs
            = new Entry[]
                 { new ServiceInfo(PRODUCT,MANUFACTURER,VENDOR,VERSION,"",""),
                   new BasicServiceType("Lookup Discovery Service")
                 };
    /* Object used to obtain the configuration items for this service. */
    private Configuration config;
    /* The JAAS login context to use when performing a JAAS login. */
    private LoginContext loginContext = null;
    /* The exporter used to export this service. */
    private Exporter serverExporter;
    /* Maximum value of upper bound on lease durations.*/
    private long leaseMax = MAX_LEASE;
    /* Flag indicating this service is being started for the very 1st time */
    private boolean initialStartup = true;
    /** Object that, if non-<code>null</code>, will cause the object's
     *  <code>unregister</code> method to be invoked during service shutdown
     *  to notify the service starter framework that the reference to this
     *  service's implementation can be 'released' for garbage collection;
     *  the framework is notified that it does not have to hold on to the
     *  service reference any longer.  Note hat this object is used only 
     *  in the non-activatable case.
     */
    private LifeCycle lifeCycle = null;
    /* Preparer for proxies to remote listeners newly registered with this
     * service.
     */
    private static ProxyPreparer listenerPreparer;
    /* Preparer for proxies to remote listeners, previously prepared, and
     * recovered from this service's persisted state.
     */
    private static ProxyPreparer recoveredListenerPreparer;
    /* Preparer for initial and new lookup locators this service should
     * discover and join.
     */
    private static ProxyPreparer locatorToJoinPreparer;
    /* Preparer for lookup locators this service should discover and join
     * that were previously prepared and which were recovered from this
     * service's persisted state.
     */
    private static ProxyPreparer recoveredLocatorToJoinPreparer;
    /* Preparer for initial and new lookup locators this service should
     * discover on behalf of the clients that register with it.
     */
    private static ProxyPreparer locatorToDiscoverPreparer;
    /* Preparer for lookup locators this service should discover on behalf
     * of its registered clients that were previously prepared and which
     * were recovered from this service's persisted state.
     */
    private static ProxyPreparer recoveredLocatorToDiscoverPreparer;

    /* ************************* BEGIN Constructors ************************ */
    /**
     * Constructs a new instance of FiddlerImpl. This version of the
     * constructor is used to create an activatable instance of the lookup
     * discovery service that logs its state information to persistent storage.
     * <p>
     * A constructor having this signature is required for the class to be
     * activatable. This constructor is automatically called by the 
     * activation group when the lookup discovery service is activated.
     * 
     * @param activationID the activation ID generated by the activation
     *                     system and assigned to the instance of the server
     *                     being activated
     * @param data         state data (represented as a 
     *                     <code>MarshalledObject</code>) which is needed to
     *                     re-activate this server
     *
     * @throws IOException            this exception can occur when there is
     *                                a problem recovering data from disk,
     *                                exporting the server that's being
     *                                activated, or when unmarshalling the
     *                                given <code>data</code> parameter.
     * @throws ConfigurationException this exception can occur when a
     *                                problem occurs while retrieving an item
     *                                from the <code>Configuration</code>
     *                                generated from the contents of the
     *                                given <code>data</code> parameter
     * @throws ActivationException    this exception can occur when a problem
     *                                occurs while activating the service
     * @throws LoginException         this exception occurs when authentication
     *                                fails while performing a JAAS login for
     *                                this service
     * @throws ClassNotFoundException this exception can occur while 
     *                                unmarshalling the given <code>data</code>
     *                                parameter; when a class needed in the
     *                                unmarshalling process cannot be found.
     * @throws ClassCastException     this exception can occur while
     *                                unmarshalling the given <code>data</code>
     *                                parameter; when the contents of that
     *                                parameter is not a <code>String</code>
     *                                array.
     */
    FiddlerImpl(ActivationID activationID,
                MarshalledObject data) throws IOException,
                                              ActivationException,
                                              ConfigurationException,
                                              LoginException,
                                              ClassNotFoundException
    {
        this.activationID = activationID;
        try {
            activationSystem = ActivationGroup.getSystem();
            init( (String[])data.get(), true );//true ==> persistent
        } catch(Throwable e) {
            cleanupInitFailure();
            handleActivatableInitThrowable(e);
        }
    }//end activatable constructor

    /**
     * Constructs a new instance of FiddlerImpl. This version of the
     * constructor is used to create a NON-activatable instance of the
     * lookup discovery service.
     *
     * @param configArgs <code>String</code> array whose elements are
     *                   the arguments to use when creating this version of
     *                   the server
     * @param lifeCycle  instance of <code>LifeCycle</code> that, if 
     *                   non-<code>null</code>, will cause this object's
     *                   <code>unregister</code> method to be invoked during
     *                   shutdown to notify the service starter framework that
     *                   the reference to this service's implementation can be
     *                   'released' for garbage collection. A value of 
     *                   <code>null</code> for this argument is allowed.
     * @param persistent if <code>true</code>, then the service should persist
     *                   its state.
     *
     * @throws IOException            this exception can occur when there is
     *                                a problem recovering data from disk, or
     *                                while exporting the server that's being
     *                                created.
     * @throws ConfigurationException this exception can occur when an
     *                                problem occurs while retrieving an item
     *                                from the <code>Configuration</code>
     *                                generated from the contents of the
     *                                given <code>configArgs</code> parameter
     * @throws LoginException         this exception occurs when authentication
     *                                fails while performing a JAAS login for
     *                                this service
     */
    FiddlerImpl(String[] configArgs, LifeCycle lifeCycle, boolean persistent)
                                             throws IOException,
                                                    ConfigurationException,
                                                    LoginException
    {
        try {
            this.lifeCycle = lifeCycle;
            init(configArgs, persistent);
        } catch(Throwable e) {
            cleanupInitFailure();
            handleInitThrowable(e);
        }
    }//end non-activatable constructor
    /* ************************** END Constructors ************************* */

    /* ******************* BEGIN Inner Class Definitions ******************* */
    /** Class which is used to communicate the status of this service to
     *  interested entities. In particular, when certain errors occur during
     *  operation, an instance of this class will be registered as an 
     *  attribute in all of the lookup services with which this service
     *  is registered. By registering for notification (from the lookup
     *  services) of the existence of this attribute, interested entities
     *  such as administrative clients and clients wishing to use this
     *  service will be informed when this service can not proceed with its
     *  processing, and can take appropriate action.
     */
    private static class FiddlerStatus extends Status {
        private static final long serialVersionUID = -8511826097053446749L;
        public FiddlerStatus(StatusType severity) {
            super(severity);
        }
    }//end class FiddlerStatus

    /** Class whose discovered() method is invoked by threads in the 
     *  LookupDiscovery class whenever a new lookup service is discovered
     *  on behalf of a client registration
     */
    private class LookupDiscoveryListener implements DiscoveryChangeListener {
        public LookupDiscoveryListener() {
            super();
        }
        public void discovered(DiscoveryEvent event) {
            taskMgr.add(new DiscoveredEventTask(event));
        }
        public void discarded(DiscoveryEvent event) {
            taskMgr.add(new DiscardedEventTask(event));
        }
        public void changed(DiscoveryEvent event) {
            taskMgr.add(new ChangedEventTask(event));
        }
    }//end class LookupDiscoveryListener

    /** This class acts as a record of one registration with the lookup
     *  discovery service; containing all of the information about that
     *  registration.
     */
    private final static class RegistrationInfo
                                          implements Comparable, Serializable
    {
        private static final long serialVersionUID = 2L;

        /** The unique identifier assigned to the registration to which the
         *  data in the current implementation of this class corresponds.
         *  This identifier is unique across all other active registrations
         *  generated with the current instance of the lookup discovery
         *  service.
         *  @serial
         */
        public final Uuid registrationID;
        /** Map from the set of instances of the <code>ServiceRegistrar</code>
         *  interface, to the set of marshalled instances of the
         *  <code>ServiceRegistrar</code> interface, where each key and
         *  each value (which is the marshalled form of its corresponding
         *  key) is a proxy to one of the lookup service(s) that have been
         *  discovered for the current registration. The contents of
         *  this set represents the 'remote state' of the registration's
         *  currently discovered lookup service(s).
         *  @serial
         */
        public HashMap discoveredRegsMap;
        /** The managed set containing the names of the groups whose
         *  members are the lookup services the lookup discovery service
         *  should attempt to discover for the current registration.
         *  (HashSet is used to prevent duplicates.)
         *  @serial
         */
        public HashSet groups;
        /** The managed set containing the locators of the specific lookup
         *  services the lookup discovery service should attempt to discover
         *  for the current registration. (HashSet is used to prevent
         *  duplicates.)
         *  @serial
         */
        public HashSet locators;
        /** The ID of the lease placed on the current registration.
         *  @serial
         */
        public final Uuid leaseID;
        /** The absolute expiration time of the current lease.
         *  @serial
         */
        public long leaseExpiration;
        /** The identifier that maps the current registration to the remote
         *  event listener and the managed set of groups and locators.
         */
        public long eventID;
        /** The current sequence number of the set of remote discovery events
         *  sent to the current registration's listener. When a registration
         *  is granted, this class is instantiated to contain the information
         *  related to that particular registration. The event sequence
         *  number is initialized to 0 upon instantiation because the
         *  remote discovery events are sent to the listeners of each 
         *  separate registration. Thus, each registration has its own
         *  sequence of events.
         *  @serial
         */
        public long seqNum;
        /** The handback object returned with every remote discovery event
         *  sent to the current registration's listener.
         *  @serial
         */
        public final MarshalledObject handback;
        /** When the lookup discovery service discards a registrar as a 
         *  result of some internal condition (such as multicast announcements
         *  ceasing) and not as a result of a request from a registration,
         *  every registration configured for group discovery of that discarded
         *  registrar will be sent a remote discarded event. On the other
         *  hand, for the case where a registrar is discarded as a result
         *  of a request from a registration, only those registrations that
         *  actually request that the registrar be discarded will be sent
         *  a remote discarded event. This flag is used to determine whether
         *  to send a remote discarded event to one or multiple listeners.
         */
        public boolean discardFlag;
        /** The remote event listener registered by the client. This field
         *  is transient because it is marshalled separately from the rest
         *  of this class when being serialized. (See the description for
         *  <code>writeObject</code> below.)
         */
        public transient RemoteEventListener listener;

        /** Constructs an instance of this class and stores the information
         *  related to the current registration: IDs, managed sets, lease
         *  information, and event registration information.
         */
        public RegistrationInfo(Uuid registrationID,
                                String[] groups,
                                LookupLocator[] locators,
                                Uuid leaseID,
                                long leaseExpiration,
                                long eventID,
                                MarshalledObject handback,
                                RemoteEventListener listener)
        {
            this.registrationID = registrationID;
            /* Initialize the groups field, removing nulls and duplicates */
            if(groups != null) {
                this.groups = new HashSet();
                for(int i=0;i<groups.length;i++) {
                    if(groups[i] == null) continue;
                    this.groups.add(groups[i]);
                }
            }
            /* Initialize the locators field, removing nulls and duplicates */
            this.locators = new HashSet();
            if( (locators != null) && (locators.length > 0) ) {
                for(int i=0;i<locators.length;i++) {
                    if(locators[i] == null) continue;
                    this.locators.add(locators[i]);
                }
            }
            this.discoveredRegsMap = new HashMap(11);
            this.leaseID = leaseID;
            this.leaseExpiration = leaseExpiration;

            this.eventID = eventID;
            this.seqNum = 0; // initialize to 0
            this.handback = handback;
            this.discardFlag = false;//set true only on first discard request
            this.listener = listener;
        }//end constructor

        /** Attempts to marshal each element of the input set of instances of
         *  the <code>ServiceRegistrar</code> interface and then map the
         *  registrar to its marshalled form, and store the mapping in this
         *  registration's <code>discoveredRegsMap</code> field.
         *  <p>
         *  This method is typically invoked to handle discovered (as opposed
         *  to discarded) registrars. Note that if a particular registrar
         *  cannot be serialized (marshalled), it is not included in the
         *  mapping; nor is it included in the return set.
         * 
         * @param regMapIn mapping in which the key values are the registrars
         *                 to serialize and store, and the map values are data
         *                 structures of type <code>LocatorGroupsStruct</code>
         *                 that contain the locator and member groups of the
         *                 corresponding registrar key
         * 
         *  @return a <code>HashMap</code> whose keys are the registrars
         *          whose marshalled form and un-marshalled form were inserted
         *          as key/value pairs into the <code>discoveredRegsMap</code>
         *          field of this regInfo; and whose values are the member
         *          groups of each corresponding registrar key.
         */
        public HashMap addToDiscoveredRegs(HashMap regMapIn) {
            HashMap regMapOut = new HashMap(regMapIn.size());
            Iterator itr = (regMapIn.entrySet()).iterator();
            nextReg:
            for(int i=0;itr.hasNext();i++) {
                Map.Entry pair = (Map.Entry)itr.next();
                ServiceRegistrar reg = (ServiceRegistrar)pair.getKey();
                /* If reg is already in map, go to next registrar */
                if( discoveredRegsMap.containsKey(reg) ) continue nextReg;
                /* It doesn't contain it, try to marshal it */
                MarshalledObject mReg = null;
                try {
                    mReg = new MarshalledObject(reg);
                } catch(IOException e) { continue nextReg; } //failed, next reg
                /* Succeeded, map registrar to its marshalled form */
                discoveredRegsMap.put(reg,mReg);
                /* Map the registrar to its member groups for the return map */
                regMapOut.put(reg,
                              ((LocatorGroupsStruct)pair.getValue()).groups);
            }//end loop
            return regMapOut;
        }//end addToDiscoveredRegs

        /** Performs a primary sort by leaseExpiration, and a secondary sort
         *  by registrationID. The secondary sort is immaterial, except to
         *  ensure a total order (required by <code>TreeMap</code>).
         */
        public int compareTo(Object obj) {
            RegistrationInfo regInfo = (RegistrationInfo)obj;
            if (this == regInfo)  return 0;
            if (    (leaseExpiration < regInfo.leaseExpiration)
                 || (    (leaseExpiration == regInfo.leaseExpiration)
		      && (eventID < regInfo.eventID) )  )
            {
                return -1;
            }//endif
            return 1;
        }//end compareTo

        /** When a registration is granted to a client, the client registers
         *  a remote listener with the lookup discovery service so that the
         *  lookup discovery service may send remote discovery events to the
         *  client. The client typically annotates the listener with an RMI
         *  codebase from which the backend server can download the remote
         *  listener's proxy (stub). When the current registration is logged
         *  to persistent storage (for example, a snapshot is taken), the
         *  listener is written to the output snapshot or log file through
         *  an <code>ObjectOutputStream</code> which only serializes the
         *  listener; it does not marshal the listener. Thus, when the
         *  listener field of this class is logged, unless special action
         *  is taken, the codebase from which to retrieve the listener will
         *  not be included in the output. 
         *
         *  In order to include the codebase with the listener when saving
         *  state, the following custom <code>writeObject</code> method
         *  is provided which first serializes the current instance of
         *  this class (excluding the transient <code>listener</code> field),
         *  and then explicitly marshals the listener to preserve the
         *  codebase upon writing to the file. In this way, the listener --
         *  along with its codebase -- is persisted through a mechanism that
         *  is separate from the normal mechanism applied to the remaining
         *  fields of this class.
         */
        private void writeObject(ObjectOutputStream stream) throws IOException{
            stream.defaultWriteObject();
            stream.writeObject(new MarshalledObject(listener));
        }//end writeObject

        /** When this class is deserialized, this method is invoked. This
         *  method first deserializes the non-transient elements of this
         *  class, and then unmarshals the remote event listener. (See the
         *  description for <code>writeObject</code> above.)
         */
        private void readObject(ObjectInputStream stream)
                                    throws IOException, ClassNotFoundException
        {
            stream.defaultReadObject();
            MarshalledObject mo = (MarshalledObject)stream.readObject();
            try {
                listener = (RemoteEventListener)mo.get();
                /* Re-prepare the recovered listener */
                listener = 
                 (RemoteEventListener)recoveredListenerPreparer.prepareProxy
                                                                   (listener);
            } catch (Throwable e) {
                problemLogger.log(Level.INFO, "problem recovering listener "
                                  +"for recovered registration", e);
                if((e instanceof Error) && (ThrowableConstants.retryable(e)
                                             == ThrowableConstants.BAD_OBJECT))
                {
                   throw (Error)e;
                }//endif
            }
            /* Prepare the locators recovered from the stream */
            int nUnprepared = (locators).size();
            locators = (HashSet)prepareOldLocators
                                         ( recoveredLocatorToDiscoverPreparer,
                                           locators );
            if( nUnprepared != (locators).size() ) {
                /* Failure occurred when preparing one of the locs. Because
                 * this breaks the contract with the client, this registration
                 * will not be recovered so that the client will eventually
                 * be notified. To facilitate this, the listener is set to
                 * null so that the registration will not be added to the
                 * managed set, and so that when the client eventually attempts
                 * to renew the lease on that registration, an exception will
                 * occur; causing the client to be "notified" that there was
                 * a problem with that registration. The client can then
                 * retry the registration; and if problems still exist, the
                 * exception the client receives may give the client more
                 * useful information from which the client can determine
                 * how to proceed.
                 */
                listener = null;
                if( problemLogger.isLoggable(Level.WARNING) ) {
                    problemLogger.log(Level.WARNING, "failure preparing "
                                      +"locator while recovering registration"
                                      +"... discarding recovered"
                                      +"registration");
                }//endif
            }//endif
        }//end readObject
    }//end class RegistrationInfo

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool. Instances of this class are placed on the task queue when
     *  registrations are granted. 
     *  <p>
     *  The <code>run</code> method of this class will determine if any of
     *  the new registration's desired lookup service(s) have already been
     *  discovered, and will send the appropriate remote discovery event to
     *  the registration's listener.
     */
    private final class NewRegistrationTask implements TaskManager.Task {
        /** The data structure record corresponding to the new registration */
        public final RegistrationInfo regInfo;
        /** Constructs an instance of this class and stores the registration
         *  information.
         */
        public NewRegistrationTask(RegistrationInfo regInfo) {
            this.regInfo = regInfo;
        }//end constructor
        /** This method processes the information associated with the new
         *  registration and determines, based on the current state of the
         *  set of 'already-discovered' lookup service(s), whether to send
         *  a <code>RemoteDiscoveryEvent</code> to the new registration's
         *  listener.
         */
        public void run() {
            concurrentObj.writeLock();
            try {
                logInfoTasks("NewRegistrationTask.run(): "
                             +"new Registration added");
                maybeSendDiscoveredEvent(regInfo,allDiscoveredRegs);
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter
    }//end class NewRegistrationTask

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool. An instance of this class is placed on the task queue when a
     *  <code>DiscoveryEvent</code> instance indicating a discovered event
     *  is received from the local discovery process. 
     *  <p>
     *  The <code>run</code> method of this class will process discovery
     *  event information and determine to which active registrations the
     *  appropriate <code>RemoteDiscoveryEvent</code> should be sent; and
     *  then sends that event.
     */
    private final class DiscoveredEventTask implements TaskManager.Task {
        /** The local event sent by the discovery manager. */
        public final DiscoveryEvent event;
        /** Constructs an instance of this class and stores the event*/
        public DiscoveredEventTask(DiscoveryEvent event) {
            this.event = event;
        }//end constructor
        /** This method processes the local discovery event information and
         *  determines, based on the current state of each active 
         *  registration, to which such registration the appropriate
         *  <code>RemoteDiscoveryEvent</code> should be sent. After making
         *  the determination, the remote event appropriate for each 
         *  registration is constructed and sent.
         */
        public void run() {
            /* Get locators before sync block (no remote calls in sync block)*/
            Map groupsMap = event.getGroups();
            ServiceRegistrar[] regs = event.getRegistrars();
            HashMap regMap = new HashMap(regs.length);
            for(int i=0;i<regs.length;i++) {
                try {
                    LookupLocator regLoc = regs[i].getLocator();
                    String[] regGroups = (String[])groupsMap.get(regs[i]);
                    LocatorGroupsStruct regLocGroups
                                  = new LocatorGroupsStruct(regLoc,regGroups);
                    regMap.put(regs[i],regLocGroups);
                } catch(Exception e) {
                    problemLogger.log(Levels.FAILED,
                                       "problem retrieving locator "
                                      +"from discovered lookup service ... "
                                      +"discarded the lookup service", e);
                    discoveryMgr.discard(regs[i]);
                }
            }//end loop
            /* Synchronization block -- no remote calls here */
            concurrentObj.writeLock();
            logInfoTasks("DiscoveredEventTask.run(): processing DISCOVERED "
                         +"event from discovery manager");
            try {
                /* Update the global allDiscoveredRegs map with new pairs */
                Set eSet = regMap.entrySet();
                for(Iterator itr = eSet.iterator(); itr.hasNext(); ) {
                    Map.Entry pair = (Map.Entry)itr.next();
                    allDiscoveredRegs.put(pair.getKey(),pair.getValue());
                }//end loop
                /* Loop thru regInfo's, adding only those not already known */
                for( Iterator itr=registrationByID.values().iterator();
                                                              itr.hasNext(); )
                {
                    RegistrationInfo regInfo = (RegistrationInfo)itr.next();
                    /* Build and send the "discovered event" if appropriate */
                    maybeSendDiscoveredEvent(regInfo,regMap);
                }//end loop
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter
    }//end class DiscoveredEventTask

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool.  An instance of this class is placed on the task queue when a
     *  <code>DiscoveryEvent</code> instance indicating a discarded event
     *  is received from the local discovery process. 
     *  <p>
     *  The <code>run</code> method of this class will process event
     *  information resulting from the "discarding" of one or more
     *  lookup services (registrars), and will determine to which active 
     *  registrations the appropriate <code>RemoteDiscoveryEvent</code>
     *  should be sent; and then sends that event.
     */
    private final class DiscardedEventTask implements TaskManager.Task {
        /** The local event sent by the discovery manager. */
        public final DiscoveryEvent event;
        /** Constructs an instance of this class and stores the event*/
        public DiscardedEventTask(DiscoveryEvent event) {
            this.event = event;
        }//end constructor
        /** This method processes the local discovery event information and
         *  determines, based on the current state of each active 
         *  registration, to which such registration the appropriate
         *  <code>RemoteDiscoveryEvent</code> should be sent. After making
         *  the determination, the remote event appropriate for each 
         *  registration is constructed and sent.
         */
        public void run() {
            concurrentObj.writeLock();
            logInfoTasks("DiscardedEventTask.run(): processing DISCARDED "
                         +"event from discovery manager");
            try {
                /* Get the registrars that were just discarded */
                Map groupsMap = event.getGroups();
                HashSet allDiscardedRegs = new HashSet(groupsMap.size());
                /* Determine if we're here because of an external request for
                 * discard from one of the regInfo's (an active communication
                 * discard), or because the discovery manager has determined
                 * one or more of the discovered registrars has become 
                 * unreachable (a passive communication discard)
                 */
                RegistrationInfo regInfo = externalDiscardRequest();
                /* If an external request, send the discarded event to only
                 * the regInfo that requested the discard; otherwise, send
                 * it to all regInfo's that might be interested.
                 */
                if(regInfo != null) {
                    /* Send discard event to only this one registration */
                    HashSet discardedRegs = maybeSendDiscardedEvent
                                                    (regInfo,groupsMap,true);
                    /* Transfer the just-discarded regs to the summary set */
                    for(Iterator jtr=discardedRegs.iterator();jtr.hasNext(); ){
                        allDiscardedRegs.add(jtr.next());
                    }
                } else {
                    /* Send discard event to each "eligible" registration */
                    for( Iterator itr=registrationByID.values().iterator();
                                                              itr.hasNext(); )
                    {
                        regInfo = (RegistrationInfo)itr.next();
                        HashSet discardedRegs = maybeSendDiscardedEvent
                                                     (regInfo,groupsMap,false);
                        /* Transfer the just-discarded regs to summary set */
                        for(Iterator jtr=discardedRegs.iterator();
                                                             jtr.hasNext(); ) {
                            allDiscardedRegs.add(jtr.next());
                        }
                    }//end loop
                }//endif
                maybeRemoveDiscardedRegsFromGlobalSet(allDiscardedRegs);
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run

        /** This method determines, based on the current state of the 
         * <code>regInfo</code> parameter, whether or not to send a
         * remote discarded event to the regInfo's listener, and then builds
         * and sends the event if appropriate. This method is called in
         * response to one of the following situations:
         * <p>
         * 1 after invocation of the public <code>discard</code> method
         * 2 after receipt of a "passive" discarded event from the discovery
         *   manager.
         * <p>
         * For case 1, such an event typically indicates what is referred to
         * as an "active, communication" discarded event. This term is used
         * in this situation because the regInfo takes the specific action
         * of requesting that a registrar the client has determined is
         * unreachable be discarded.
         * <p>
         * For case 2, such an event typically indicates what is referred to
         * as a "passive, communication" discarded event. This term is used
         * here because the discovery manager - not the client - has determined
         * that one or more of the previously discovered registrars are now
         * unreachable. In this case, the client remains "passive", and it
         * is the discovery manager that discards the unreachable registrars
         * and notifies the client(s).
         * 
         * @param regInfo    the data structure record corresponding to the 
         *                   registration whose listener will receive the event
         * @param groupsMap  mapping from the registrars referenced in the 
         *                   just-received event to their corresponding set of
         *                   member groups
         * @param active     flag indicating whether the event is an "active"
         *                   or a "passive" discarded event
         *
         * @return set of registrars that were discarded for the given regInfo
         */
        private HashSet maybeSendDiscardedEvent(RegistrationInfo regInfo,
                                                Map groupsMap,
                                                boolean active)
        {
            HashSet discardedRegs = new HashSet(groupsMap.size()); //return val
            /* If no interest in groups or locators, go to next regInfo*/
            if(     (regInfo.groups != null) && ((regInfo.groups).size() == 0)
                 && ((regInfo.locators).size() == 0) )
            {
                return discardedRegs;
            }
            HashMap discardMap = new HashMap(groupsMap.size());
            /* loop thru the (registrar,groups) pairs, find regs to discard */
            Set eSet = groupsMap.entrySet();
            for(Iterator itr = eSet.iterator(); itr.hasNext(); ) {
                Map.Entry pair = (Map.Entry)itr.next();
                ServiceRegistrar reg = (ServiceRegistrar)pair.getKey();
                /* Include the current reg in the discard map only if that
                 * reg is in the regInfo's discovered set.
                 */
                if( (regInfo.discoveredRegsMap).containsKey(reg) ) {
                    /* The groups corresponding to the discarded registrar that
                     * arrived in the event may be more up-to-date than the
                     * groups associated with the registrar in the global map.
                     * Thus, if the event is a passive communication discarded
                     * event, when determining whether the regInfo is still
                     * interested in the discarded registrar, use the old group
                     * info rather than the group info sent in the event.
                     */
                    String[] regGroups = (active ? (String[])pair.getValue() :
                    ((LocatorGroupsStruct)allDiscoveredRegs.get(reg)).groups );

                    if( active || interested(regGroups,regInfo.groups) ) {
                        discardMap.put(reg,regGroups);
                        discardedRegs.add(reg);
                        (regInfo.discoveredRegsMap).remove(reg);
                    }//end if
                }//end if
            }//end loop
            /* Build and send the "discarded event" */
            RemoteDiscoveryEvent event = buildEvent(regInfo,discardMap,true);
            if(event != null) {
                queueEvent(regInfo,event);
                logInfoEvents("DiscardedEventTask.run(): "
                              +"DISCARDED Event SENT to regInfo\n");
            }
            return discardedRegs;
        }//end maybeSendDiscardedEvent

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter

    }//end class DiscardedEventTask

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool. Instances of this class are placed on the task queue when
     *  registrations request that a given registrar be discarded. 
     *  <p>
     *  The <code>run</code> method of this class will remove the indicated
     *  registrar from the registration's set of discovered registrars and
     *  if successfully removed, will build and send a remote discarded event
     *  to the registration's listener.
     */
    private final class DiscardRegistrarTask implements TaskManager.Task {
        /** Data structure record corresponding to the registration that has
         *  requested to have one of its discovered registrars discarded
         */
        public final RegistrationInfo regInfo;
        /** The registrar to discard */
        public final ServiceRegistrar registrar;
        /** Constructs an instance of this class and stores the registration
         *  information.
         */
        public DiscardRegistrarTask(RegistrationInfo regInfo,
                                    ServiceRegistrar registrar)
        {
            this.regInfo   = regInfo;
            this.registrar = registrar;
        }//end constructor
        /** This method attempts to remove the indicated registrar from
         *  the registration's set of discovered registrars. If successful,
         *  this method builds and sends a remote discarded event to the
         *  registration's listener.
         */
        public void run() {
            concurrentObj.writeLock();
            try {
                logInfoTasks("DiscardRegistrarTask.run(): "
                             +"registrar requested to be discarded");
                /* Remove registrar from regInfo's set and send event */
                if( (regInfo.discoveredRegsMap).remove(registrar) != null) {
                    HashMap groupsMap = mapRegToGroups(registrar,
               ((LocatorGroupsStruct)allDiscoveredRegs.get(registrar)).groups);

                    RemoteDiscoveryEvent event = buildEvent
                                                      (regInfo,groupsMap,true);
                    if(event != null) {
                        queueEvent(regInfo,event);
                        logInfoEvents("DiscardRegistrarTask.run(): "
                                      +"DISCARDED Event was SENT\n");
                    }//endif
                    maybeRemoveDiscardedRegFromGlobalSet(registrar);
                }
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter
    }//end class DiscardRegistrarTask

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool.  An instance of this class is placed on the task queue when a
     *  <code>DiscoveryEvent</code> instance indicating a changed event
     *  is received from the local discovery process. 
     *  <p>
     *  The <code>run</code> method of this class will process event
     *  information resulting from a change in the state of the member
     *  groups of one or more lookup services (registrars). This task
     *  analyzes the group information in the event and, based on that
     *  information, determines which active registrations are no longer
     *  interested in the registrars referenced in the event. A
     *  <code>RemoteDiscoveryEvent</code> indicating a discarded event
     *  will be sent to each active registration that has lost interest
     *  in any of the registrars of the event.
     */
    private final class ChangedEventTask implements TaskManager.Task {
        /** The local event sent by the discovery manager. */
        public final DiscoveryEvent event;
        /** Constructs an instance of this class and stores the event*/
        public ChangedEventTask(DiscoveryEvent event) {
            this.event = event;
        }//end constructor
        /** This method processes the local discovery event information and
         *  determines, based on the current state of each active 
         *  registration, to which such registration the appropriate
         *  <code>RemoteDiscoveryEvent</code> should be sent. After making
         *  the determination, the remote event appropriate for each 
         *  registration is constructed and sent.
         */
        public void run() {
            concurrentObj.writeLock();
            logInfoTasks("ChangedEventTask.run(): processing CHANGED "
                         +"event from discovery manager");
            try {
                Map groupsMap = event.getGroups();
                HashSet allDiscardedRegs = new HashSet(groupsMap.size());
                HashMap locatorMap = new HashMap(groupsMap.size());
                /* Retrieve the locators of each registrar in the event */
                for(Iterator itr = (groupsMap.keySet()).iterator();
                                                            itr.hasNext(); )
                {
                    ServiceRegistrar reg = (ServiceRegistrar)itr.next();
                    locatorMap.put(reg,
                    ((LocatorGroupsStruct)allDiscoveredRegs.get(reg)).locator);
                }//end loop

                for( Iterator itr=registrationByID.values().iterator();
                                                              itr.hasNext(); )
                {
                    RegistrationInfo regInfo = (RegistrationInfo)itr.next();
                    HashSet discardedRegs = maybeSendDiscardedEvent
                                                (regInfo,groupsMap,locatorMap);
                    /* Transfer the just-discarded regs to the summary set */
                    for(Iterator jtr=discardedRegs.iterator();jtr.hasNext(); ){
                        allDiscardedRegs.add(jtr.next());
                    }//end loop
                }//end loop
                maybeRemoveDiscardedRegsFromGlobalSet(allDiscardedRegs);
                updateGroupsInGlobalSet(groupsMap); //replace with new groups
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run

        /** This method determines, based on the current state of the 
         * <code>regInfo</code> parameter, whether or not to send a
         * remote discarded event to the regInfo's listener, and then builds
         * and sends the event if appropriate. This method is called in
         * response to the receipt of a changed event from the discovery
         * manager. 
         * <p>
         * Such an event may indicate what is referred to as a
         * "passive, no interest" discard; passive because the event 
         * resulted from action taken by the discovery manager rather than
         * the client, and no interest because the discovery manager sends
         * such an event when it determines that one or more of the 
         * previously discovered registrars - although still reachable -
         * have changed their member groups in such a way that they may
         * now be of no interest to one or more of the client registrations.
         * <p>
         * Note that changed events can be sent for registrars having a
         * locator and member groups that the current regInfo never asked
         * to be discovered. This can happen because some other regInfo
         * asked that the registrar's locator or groups be discovered. 
         * <p>
         * If a particular registrar is contained in the discovered set 
         * of the given regInfo, then we know that that regInfo must
         * have requested discovery of the registrar (through either
         * locator or group discovery). If the registrar is not contained
         * in that set, then there's no need to proceed with the processing
         * of the registrar since we don't want to send a discarded event
         * to a regInfo that was never interested in that registrar in the
         * first place.
         * <p>
         * If the locator of the registrar is contained in the regInfo's
         * set of locators to discover, then that regInfo is considered
         * "still interested" in the registrar; and so no discarded event
         * is sent to the regInfo.
         * <p>
         * Thus, a discarded event is sent to the given regInfo only
         * if the regInfo is not interested in discovering the registrar
         * through locator discovery, and the registrar's member groups have
         * changed in such a way that it now belongs to groups that
         * the regInfo is not interested in discovering and joining.
         *
         * @param regInfo    the data structure record corresponding to the 
         *                   registration whose listener will receive the event
         * @param groupsMap  mapping from the registrars referenced in the 
         *                   just-received event to their corresponding set of
         *                   member groups
         * @param locatorMap mapping from the registrars referenced in the 
         *                   just-received event to their corresponding locator
         *
         * @return the registrars that were discarded for the given regInfo
         */
        private HashSet maybeSendDiscardedEvent(RegistrationInfo regInfo,
                                                Map groupsMap,
                                                Map locatorMap)
        {
            /* For each registrar discard candidate, send a discarded event if:
             *   The candidate is in the discovered set and
             *    a. regInfo is configured for at least group discovery
             *    b. candidate is NOT to be discovered by locator discovery
             *    c. regInfo is no longer interested in the candidate's groups
             */
            HashSet discardedRegs = new HashSet(groupsMap.size()); //return val
            /* If this regInfo isn't interested in groups, go to next regInfo*/
            if( (regInfo.groups != null) && ((regInfo.groups).size() == 0) )
            {
                return discardedRegs;
            }
            HashMap discardMap = new HashMap(groupsMap.size());
            /* loop thru the (registrar,groups) pairs, find regs to discard */
            Set eSet = groupsMap.entrySet();
            for(Iterator itr = eSet.iterator(); itr.hasNext(); ) {
                Map.Entry pair = (Map.Entry)itr.next();
                ServiceRegistrar reg = (ServiceRegistrar)pair.getKey();
                String[] regGroups = (String[])pair.getValue();
                LookupLocator regLoc = (LookupLocator)locatorMap.get(reg);
                /* Include the current reg in the discard map only if that
                 * reg is in the regInfo's discovered set, the regInfo
                 * is not interested in discovering the reg through locator
                 * discovery, and the reg's member groups have changed in
                 * such a way that it now belongs to groups that the regInfo
                 * is not interested in discovering and joining.
                 */
                if(    ( (regInfo.discoveredRegsMap).containsKey(reg) )
                    && (!interested(regLoc,regGroups,
                                         regInfo.locators,regInfo.groups)) )
                {
                    discardMap.put(reg,regGroups);
                    discardedRegs.add(reg);
                    (regInfo.discoveredRegsMap).remove(reg);
                }
            }//end loop
            /* Build and send the "discarded event" */
            RemoteDiscoveryEvent event = buildEvent(regInfo,discardMap,true);
            if(event != null) {
                queueEvent(regInfo,event);
                logInfoEvents("ChangedEventTask.run(): "
                              +"DISCARDED Event was SENT\n");
            }//endif
            return discardedRegs;
        }//end maybeSendDiscardedEvent

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter

    }//end class ChangedEventTask

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool. Instances of this class are placed on the task queue when
     *  a registration has requested the augmentation of the set of groups
     *  that currently will be discovered for it.
     */
    private final class AddGroupsTask implements TaskManager.Task {
        /** Data structure record of the registration that made the request */
        public final RegistrationInfo regInfo;
        /** The group set with which to replace the registration's old set */
        public final String[] groups;
        /** Constructs an instance of this class and stores the input */
        public AddGroupsTask(RegistrationInfo regInfo, String[] groups) {
            this.regInfo = regInfo;
            this.groups  = groups;
        }//end constructor
        public void run() {
            /* For the regInfo associated with the current instance of this
             * task, do the following:
             * a. in the given regInfo data structure, add the new groups,
             *    with duplicates removed, to that regInfo's current set of
             *    desired groups
             * b. from the global mapping of all currently discovered 
             *    registrars to (locator,groups) pairs, retrieve the elements
             *    that contain registrars belonging to groups that regInfo
             *    should now be interested in as a result of the call to
             *    addGroups
             * c. for each registrar-to-(locator,groups) mapping retrieved in
             *    b. above, add that mapping to the given regInfo data
             *    structure's discovered state (these are the registrars that
             *    were previously discovered for OTHER regInfo's, not the
             *    current regInfo)
             * d. for each of the registrars previously discovered for other
             *    registrations that belong to any of the new groups regInfo
             *    is now interested in as a result of the call to addGroups,
             *    queue a remote discovery event to be sent to that regInfo's
             *    listener
             * e. if any of the new groups regInfo is now interested in as
             *    as a result of the call to addGroups were not previously
             *    in the local discovery manager's managed set of groups,
             *    (and the local discovery manager is currently not configured
             *    to discover ALL_GROUPS), add the new groups to the local
             *    discovery manager so that when that manager does discover
             *    one of those groups in the future, a remote discovered
             *    event will be sent to the given regInfo's listener
             */
            concurrentObj.writeLock();
            try {
                HashSet newGroupSet = addRegInfoGroups(regInfo,groups);  // a.
                if(newGroupSet.size() > 0) {
                    logInfoTasks("AddGroupsTask.run(): adding to the "
                                 +"registration's groups");
                    HashMap discoveredRegs = getDesiredRegsByGroup
                                                              (regInfo); // b.
                    HashMap regsAdded = regInfo.addToDiscoveredRegs
                                                       (discoveredRegs); // c.
                    RemoteDiscoveryEvent event = buildEvent
                                              (regInfo,regsAdded,false); // d.
                    if(event != null) {
                        queueEvent(regInfo,event);                       // d.
                        logInfoEvents("AddGroupsTask.run(): DISCOVERED "
                                      +"Event was SENT\n");
                    }//endif
                    updateDiscoveryMgrGroups();                          // e.
                }//endif(newGroupSet.size() > 0)
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run

        /** Augments the registration's managed set of groups with the new
         *  groups.
         *
         * @return the set of new groups added to regInfo's desired groups
         */
        private HashSet addRegInfoGroups(RegistrationInfo regInfo,
                                         String[] groups)
        {
            /* Build a HashSet (removes duplicates) from the input groups */
            HashSet newGroupSet = new HashSet(1);
            for(int i=0;i<groups.length;i++) {
                newGroupSet.add(groups[i]);
            }//end loop
            /* If the input set was not empty, add the new groups to the 
             * registration's managed set of groups.
             */
            if( newGroupSet.size() > 0 ) {
                (regInfo.groups).addAll(newGroupSet);
            }//endif
            return newGroupSet;
        }//end addRegInfoGroups

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter
    }//end class AddGroupsTask

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool. Instances of this class are placed on the task queue when
     *  a registration has requested the replacement of the set of groups
     *  that currently will be discovered for it.
     */
    private final class SetGroupsTask implements TaskManager.Task {
        /** Data structure record of the registration that made the request */
        public final RegistrationInfo regInfo;
        /** The group set with which to replace the registration's old set */
        public final String[] groups;
        /** Constructs an instance of this class and stores the input */
        public SetGroupsTask(RegistrationInfo regInfo, String[] groups) {
            this.regInfo = regInfo;
            this.groups  = groups;
        }//end constructor
        public void run() {
            /* For the regInfo associated with the current instance of this
             * task, do the following:
             * a. from the global mapping of all currently discovered 
             *    registrars to their (locator,groups) pair, retrieve the
             *    elements that contain registrars belonging to groups that
             *    regInfo was interested in PRIOR to the call to setGroups
             * b. in the given regInfo data structure, replace that regInfo's
             *    current set of desired groups with the new set of desired
             *    groups that resulted from the call to setGroups
             * c. again from the global mapping of all currently discovered 
             *    registrars to (locator,groups) pairs, retrieve the elements
             *    that contain registrars belonging to groups that regInfo
             *    should now be interested in as a result of the call to
             *    setGroups
             * d. for each registrar-to-(locator,groups) mapping retrieved in
             *    c. above, add that mapping to the given regInfo data
             *    structure's state (these are the registrars that were
             *    previously discovered for OTHER regInfo's, not the current
             *    regInfo)
             * e. for each of the registrars previously discovered for other
             *    registrations that belong to any of the new groups regInfo
             *    is now interested in as a result of the call to setGroups,
             *    queue a remote discovery event to be sent to that regInfo's
             *    listener
             * f. from the mapping of already-discovered registrars that 
             *    regInfo was interested in prior to the call to setGroups
             *    (the mapping retrieved in a. above), retrieve the elements
             *    that contain registrars belonging to groups that regInfo is
             *    no longer interested in due to the call to setGroups
             * g. for each registrar-to-(locator,groups) mapping retrieved in
             *    f. above, remove that mapping from the given regInfo data
             *    structure's state, and queue a remote discarded event to be
             *    sent to that regInfo's listener
             * h. if any of the new groups regInfo is now interested in as
             *    as a result of the call to setGroups were not previously
             *    in the local discovery manager's managed set of groups,
             *    add those groups to that discovery manager so that when
             *    that manager does discover one of those groups in the  
             *    future, a remote discovered event will be sent to the given
             *    regInfo's listener
             */
            concurrentObj.writeLock();
            try {
                logInfoTasks("SetGroupsTask.run(): setting the "
                             +"registration's groups");
                Map oldDesiredRegs = getDesiredRegsByGroup(regInfo);     // a.
                setRegInfoGroups(regInfo,groups);                        // b.
                HashMap newDesiredRegs = getDesiredRegsByGroup(regInfo); // c.
                HashMap regsAdded = regInfo.addToDiscoveredRegs
                                                       (newDesiredRegs); // d.
                RemoteDiscoveryEvent event = buildEvent
                                              (regInfo,regsAdded,false); // e.
                if(event != null) {
                    queueEvent(regInfo,event);                           // e.
                    logInfoEvents("SetGroupsTask.run(): DISCOVERED "
                                  +"Event was SENT\n");
                }//endif
                Map discardRegs = getUndesiredRegsByGroup
                                               (oldDesiredRegs,regInfo); // f.
                for(Iterator itr = (discardRegs.keySet()).iterator();
                                                             itr.hasNext(); )
                {
                    (regInfo.discoveredRegsMap).remove(itr.next());     // g.
                }//end loop
                event = buildEvent(regInfo,discardRegs,true);           // g.
                if(event != null) {
                    queueEvent(regInfo,event);                          // g.
                    logInfoEvents("SetGroupsTask.run(): "
                                  +"DISCARDED Event was SENT\n");
                }//endif
                updateDiscoveryMgrGroups();                             // h.
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run

        /** Replaces the registration's managed set of groups with the new
         *  groups (even if the new set of groups is empty -- this just means
         *  group discovery will be "turned off" for this registration).
         */
        private void setRegInfoGroups(RegistrationInfo regInfo,
                                      String[] groups)
        {
            if(groups == DiscoveryGroupManagement.ALL_GROUPS) {
                regInfo.groups = null;
            } else {
                /* Build a HashSet from the input set */
                HashSet newGroups = new HashSet();
                for(int i=0;i<groups.length;i++) {
                    newGroups.add(groups[i]);
                }//end loop
                /* Prepare the registration's managed set for replacement */
                if(regInfo.groups == null) {
                    regInfo.groups = new HashSet();
                } else {
                    (regInfo.groups).clear();
                }//endif
                /* Replace the registration's managed set with the new set */
                (regInfo.groups).addAll(newGroups);
            }//end if (groups == DiscoveryGroupManagement.ALL_GROUPS)
        }//end setRegInfoGroups

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter
    }//end class SetGroupsTask

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool. Instances of this class are placed on the task queue when
     *  a registration has requested the removal of a set of groups from
     *  the current set of groups to discover for it.
     */
    private final class RemoveGroupsTask implements TaskManager.Task {
        /** Data structure record of the registration that made the request */
        public final RegistrationInfo regInfo;
        /** The groups to remove from the registration's old set */
        public final String[] groups;
        /** Constructs an instance of this class and stores the input */
        public RemoveGroupsTask(RegistrationInfo regInfo, String[] groups) {
            this.regInfo = regInfo;
            this.groups  = groups;
        }//end constructor
        public void run() {
            concurrentObj.writeLock();
            try {
                if(groups.length == 0) return; // nothing from which to remove
               logInfoTasks("RemoveGroupsTask.run(): removing groups from "
                            +"the registration's current group set");
                /* regInfo's discovered regs (by group) previously desired */
                Map oldDesiredRegs = getDesiredRegsByGroup(regInfo);
                /* update regInfo's desired regs */
                removeRegInfoGroups(regInfo,groups);
                /* regInfo's discovered regs (by group) no longer desired */
                Map discardRegs = getUndesiredRegsByGroup(oldDesiredRegs,
                                                          regInfo);
                /* remove regInfo's undesired regs from its discovered map */
                for(Iterator itr = (discardRegs.keySet()).iterator();
                                                              itr.hasNext(); )
                {
                    (regInfo.discoveredRegsMap).remove(itr.next());
                }//end loop
                RemoteDiscoveryEvent event = buildEvent
                                                    (regInfo,discardRegs,true);
                if(event != null) {
                    queueEvent(regInfo,event);
                    logInfoEvents("RemoveGroupsTask.run(): "
                                  +"DISCARDED Event was SENT\n");
                }//endif
                updateDiscoveryMgrGroups(); // may send more discards
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run

        /** Removes the elements of the given set from the given registration's
         *  current set of groups to discover.
         */
        private void removeRegInfoGroups(RegistrationInfo regInfo,
                                         String[] groups)
        {
            HashSet removeSet = new HashSet();
            for(int i=0;i<groups.length;i++) {
                removeSet.add(groups[i]);
            }//end loop
            (regInfo.groups).removeAll(removeSet);
        }//end setRegInfoGroups

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter
    }//end class RemoveGroupsTask

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool. Instances of this class are placed on the task queue when
     *  a registration has requested the augmentation of the set of locators
     *  that currently will be discovered for it.
     */
    private final class AddLocatorsTask implements TaskManager.Task {
        /** Data structure record of the registration that made the request */
        public final RegistrationInfo regInfo;
        /** The locator set with which to replace the registration's old set */
        public final LookupLocator[] locators;
        /** Constructs an instance of this class and stores the input */
        public AddLocatorsTask(RegistrationInfo regInfo,
                               LookupLocator[]  locators)
        {
            this.regInfo = regInfo;
            this.locators  = locators;
        }//end constructor
        public void run() {
            /* For the regInfo associated with the current instance of this
             * task, do the following:
             * a. in the given regInfo data structure, add the new locators,
             *    with duplicates removed, to that regInfo's current set of
             *    desired locators
             * b. from the global mapping of all currently discovered 
             *    registrars to (locator,groups) pairs, retrieve the elements
             *    that contain registrars having locators that regInfo
             *    should now be interested in as a result of the call to
             *    addLocators
             * c. for each registrar-to-(locator,groups) mapping retrieved in
             *    b. above, add that mapping to the given regInfo data
             *    structure's discovered state (these are the registrars that
             *    were previously discovered for OTHER regInfo's, not the
             *    current regInfo)
             * d. for each of the registrars previously discovered for other
             *    registrations that have locators equal to any of the new
             *    locators regInfo is now interested in as a result of the
             *    call to addLocators, queue a remote discovery event to be
             *    sent to that regInfo's listener
             * e. if any of the new locators regInfo is now interested in as
             *    as a result of the call to addLocators were not previously
             *    in the local discovery manager's managed set of locators,
             *    add the new locators to the local discovery manager so that
             *     when that manager does discover one of those locators in
             *    the future, a remote discovered event will be sent to the
             *    given regInfo's listener
             */
            concurrentObj.writeLock();
            try {
                HashSet newLocSet = addRegInfoLocators(regInfo,locators);// a.
                if(newLocSet.size() > 0) {
                    logInfoTasks("AddLocatorsTask.run(): adding to the "
                                 +"registration's locators");
                    HashMap discoveredRegs = getDesiredRegsByLocator
                                                              (regInfo); // b.
                    HashMap regsAdded = regInfo.addToDiscoveredRegs
                                                       (discoveredRegs); // c.
                    RemoteDiscoveryEvent event = buildEvent
                                              (regInfo,regsAdded,false); // d.
                    if(event != null) {
                        queueEvent(regInfo,event);                       // d.
                        logInfoEvents("AddLocatorsTask.run(): DISCOVERED "
                                      +"Event was SENT\n");
                    }//endif
                    updateDiscoveryMgrLocators();                        // e.
                }//endif(newLocSet.size() > 0)
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run

        /** Augments the registration's managed set of locators with the new
         *  locators.
         *
         * @return the set of new locators added to regInfo's desired locators
         */
        private HashSet addRegInfoLocators(RegistrationInfo regInfo,
                                           LookupLocator[]  locators)
        {
            /* Build a HashSet (removes duplicates) from the input locators */
            HashSet newLocSet = new HashSet(1);
            for(int i=0;i<locators.length;i++) {
                newLocSet.add(locators[i]);
            }//end loop
            /* If the input set was not empty, add the new locators to the 
             * registration's managed set of locators.
             */
            if( newLocSet.size() > 0 ) {
                (regInfo.locators).addAll(newLocSet);
            }//endif
            return newLocSet;
        }//end addRegInfoLocators

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter
    }//end class AddLocatorsTask

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool. Instances of this class are placed on the task queue when
     *  a registration has requested the replacement of the set of locators
     *  that currently will be discovered for it.
     */
    private final class SetLocatorsTask implements TaskManager.Task {
        /** Data structure record of the registration that made the request */
        public final RegistrationInfo regInfo;
        /** The locator set with which to replace the registration's old set */
        public final LookupLocator[] locators;
        /** Constructs an instance of this class and stores the input */
        public SetLocatorsTask(RegistrationInfo regInfo,
                               LookupLocator[] locators)
        {
            this.regInfo  = regInfo;
            this.locators = locators;
        }//end constructor
        public void run() {
            /* For the regInfo associated with the current instance of this
             * task, do the following:
             * a. from the global mapping of all currently discovered 
             *    registrars to their (locator,groups) pair, retrieve the
             *    elements that contain registrars having locators that
             *    regInfo was interested in PRIOR to the call to setGroups
             * b. in the given regInfo data structure, replace that regInfo's
             *    current set of desired locators with the new set of desired
             *    locators that resulted from the call to setLocators
             * c. again from the global mapping of all currently discovered 
             *    registrars to (locator,groups) pairs, retrieve the elements
             *    that contain registrars having locators that regInfo should
             *    now be interested in as a result of the call to setLocators
             * d. for each registrar-to-(locator,groups) mapping retrieved in
             *    c. above, add that mapping to the given regInfo data
             *    structure's state (these are the registrars that were
             *    previously discovered for OTHER regInfo's, not the current
             *    regInfo)
             * e. for each of the registrars previously discovered for other
             *    registrations that have locators equal to the new locators
             *    regInfo is now interested in as a result of the call to
             *    setLocators, queue a remote discovery event to be sent to
             *    that regInfo's listener
             * f. from the mapping of already-discovered registrars that 
             *    regInfo was interested in prior to the call to setLocators
             *    (the mapping retrieved in a. above), retrieve the elements
             *    that contain registrars having locators that regInfo is
             *    no longer interested in due to the call to setLocators
             * g. for each registrar-to-(locator,groups) mapping retrieved in
             *    f. above, remove that mapping from the given regInfo data
             *    structure's state, and queue a remote discarded event to be
             *    sent to that regInfo's listener
             * h. if any of the new locators regInfo is now interested in as
             *    as a result of the call to setLocators were not previously
             *    in the local discovery manager's managed set of locators,
             *    add those locators to that discovery manager so that when
             *    that manager does discover one of those locators in the  
             *    future, a remote discovery event will be sent to the given
             *    regInfo's listener
             */
            concurrentObj.writeLock();
            try {
                if(locators.length == 0) return; // nothing to replace
                logInfoTasks("SetLocatorsTask.run(): setting the "
                             +"registration's locators");
                Map oldDesiredRegs = getDesiredRegsByLocator(regInfo);    // a.
                setRegInfoLocators(regInfo,locators);                     // b.
                HashMap newDesiredRegs = getDesiredRegsByLocator(regInfo);// c.
                HashMap regsAdded = regInfo.addToDiscoveredRegs
                                                       (newDesiredRegs);  // d.
                RemoteDiscoveryEvent event = buildEvent
                                              (regInfo,regsAdded,false);  // e.
                if(event != null) {
                    queueEvent(regInfo,event);                            // e.
                    logInfoEvents("SetLocatorsTask.run(): DISCOVERED "
                                  +"Event was SENT\n");
                }//endif
                Map undesiredRegs = getUndesiredRegsByLocator
                                               (oldDesiredRegs,regInfo);  // f.
                HashMap discardRegs = new HashMap(undesiredRegs.size());
                for(Iterator itr = (undesiredRegs.keySet()).iterator();
                                                              itr.hasNext(); )
                {
                    ServiceRegistrar reg = (ServiceRegistrar)itr.next();
                    (regInfo.discoveredRegsMap).remove(reg);              // g.
                    discardRegs.put(reg,
                    ((LocatorGroupsStruct)allDiscoveredRegs.get(reg)).groups);

                }//end loop
                event = buildEvent(regInfo,discardRegs,true);             // g.
                if(event != null) {
                    queueEvent(regInfo,event);                            // g.
                    logInfoEvents("SetLocatorsTask.run(): "
                                  +"DISCARDED Event was SENT\n");
                }//endif
                updateDiscoveryMgrLocators();                             // h.
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run

        /** Replaces the registration's managed set of locators with the new
         *  locators (even if the new set of locators is empty -- this just
         *  means locator discovery will be "turned off" for this registration)
         */
        private void setRegInfoLocators(RegistrationInfo regInfo,
                                        LookupLocator[]  locators)
        {
            /* Build a HashSet from the input set */
            HashSet newLocSet = new HashSet();
            for(int i=0;i<locators.length;i++) {
                newLocSet.add(locators[i]);
            }//end loop
            /* Prepare the registration's managed set for replacement */
            if(regInfo.locators == null) {
                regInfo.locators = new HashSet();
            } else {
                (regInfo.locators).clear();
            }//endif
            /* Replace the registration's managed set with the new set */
            (regInfo.locators).addAll(newLocSet);
        }//end setRegInfoLocators

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter
    }//end class SetLocatorsTask

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool. Instances of this class are placed on the task queue when
     *  a registration has requested the removal of a set of locators
     *  from the current set of locators to discover for it.
     */
    private final class RemoveLocatorsTask implements TaskManager.Task {
        /** Data structure record of the registration that made the request */
        public final RegistrationInfo regInfo;
        /** The locators to remove from the registration's old set */
        public final LookupLocator[] locators;
        /** Constructs an instance of this class and stores the input */
        public RemoveLocatorsTask(RegistrationInfo regInfo,
                               LookupLocator[] locators)
        {
            this.regInfo  = regInfo;
            this.locators = locators;
        }//end constructor
        public void run() {
            concurrentObj.writeLock();
            try {
                if(locators.length == 0) return; //nothing from which to remove

                logInfoTasks("RemoveLocatorsTask.run(): removing locators "
                             +"from the registration's current locator set");
                /* regInfo's discovered regs (by locator) previously desired */
                Map oldDesiredRegs = getDesiredRegsByLocator(regInfo);
                /* update regInfo's desired regs */
                removeRegInfoLocators(regInfo,locators);
                /* regInfo's discovered regs (by locator) no longer desired */
                Map undesiredRegs = getUndesiredRegsByLocator
                                                    (oldDesiredRegs,regInfo);
                /* remove regInfo's undesired regs from its discovered map,
                 * and construct the registrars-to-groups map for the event
                 */
                HashMap discardRegs = new HashMap(undesiredRegs.size());
                for(Iterator itr = (undesiredRegs.keySet()).iterator();
                                                              itr.hasNext(); )
                {
                    ServiceRegistrar reg = (ServiceRegistrar)itr.next();
                    (regInfo.discoveredRegsMap).remove(reg);
                    discardRegs.put(reg,
                    ((LocatorGroupsStruct)allDiscoveredRegs.get(reg)).groups);
                }//end loop
                /* Construct the registrars-to-groups map for the event */
                RemoteDiscoveryEvent event = buildEvent
                                                    (regInfo,discardRegs,true);
                if(event != null) {
                    queueEvent(regInfo,event);
                    logInfoEvents("SetLocatorsTask.run(): "
                                  +"DISCARDED Event was SENT\n");
                }//endif
                updateDiscoveryMgrLocators(); // may send more discards
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run

        /** Removes the elements of the given set from the given registration's
         *  current set of locators to discover.
         */
        private void removeRegInfoLocators(RegistrationInfo regInfo,
                                           LookupLocator[]  locators)
        {
            /* Build a HashSet from the input set */
            HashSet removeSet = new HashSet();
            for(int i=0;i<locators.length;i++) {
                removeSet.add(locators[i]);
            }//end loop
            (regInfo.locators).removeAll(removeSet);
        }//end removeRegInfoLocators

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter
    }//end class RemoveLocatorsTask

    /** This class represents a <code>Task</code> object that is placed
     *  in the <code>TaskManager</code> queue for processing in the thread
     *  pool. Instances of this class are placed on the task queue when
     *  a remote event is to be sent to a given registration. 
     *  <p>
     *  Remote events are sent in a separate task such as this to avoid
     *  making the remote call to the registration's listener within a
     *  synchronization block.
     */
    private final class SendEventTask implements TaskManager.Task {
        /** Data structure record corresponding to registration to get event */
        public final RegistrationInfo     regInfo;
        /** The remote event to send to the given registration's listener */
        public final RemoteDiscoveryEvent event;
        /** Constructs an instance of this class and stores the registration
         *  information.
         */
        public SendEventTask(RegistrationInfo     regInfo,
                             RemoteDiscoveryEvent event)
        {
            this.regInfo = regInfo;
            this.event   = event;
        }//end constructor
        /** This method sends a <code>RemoteDiscoveryEvent</code> to the
         *  listener of the registration that corresponds to the
         *  <code>regInfo</code> field of this class. This method handles
         *  all exceptions and error conditions in the appropriate manner.
         */
        public void run() {
            try {
                regInfo.listener.notify(event);
            } catch (Throwable e) {
                problemLogger.log(Level.INFO, "Exception in SendEventTask", e);
                switch (ThrowableConstants.retryable(e)) {
                    case ThrowableConstants.BAD_OBJECT:
                        if(e instanceof Error)  throw (Error)e;
                    case ThrowableConstants.BAD_INVOCATION:
                    case ThrowableConstants.UNCATEGORIZED:
                    /* If the listener throws UnknownEvent or some other
                     * definite exception, or the listener is gone, it's
                     * okay to cancel the lease.
                     */
                    concurrentObj.writeLock();
                    try {
                        try {
                            logInfoEvents
                                       ("  Cancelling lease on registration: "
                                        +" (registrationID,leaseID) = ("
                                        +regInfo.registrationID+", "
                                        +regInfo.leaseID+")");
                            cancelLeaseDo(regInfo,regInfo.leaseID);
                            addLogRecord(new LeaseCancelledLogObj
                                                      (regInfo.registrationID,
                                                       regInfo.leaseID));
                        } catch (UnknownLeaseException ee) {
                        } catch (IOException ee) { }
                    } finally {
                        concurrentObj.writeUnlock();
                    }
                }//end switch
            }//end try
        }//end run

        /** This method returns true if the current instance of this class
         *  must be run after at least one task in the input task list with
         *  an index less than the <code>size</code> parameter (size may be
         *  less than tasks.size()).
         *  <p>
         *  Note that using List.get will be more efficient than List.iterator.
         *
         *  @param tasks the tasks to consider.  A read-only List, with all
         *         elements being an instanceof Task.
         *  @param size elements with index less than size should be considered
         */
        public boolean runAfter(List tasks, int size) {
            return false;
        }//end runAfter
    }//end class SendEventTask

    /**
     * Handler class for the persistent storage facility.
     * <p>
     * At any point during processing in this service, there will exist
     * both a 'snapshot' of the service's state and a set of records
     * detailing each significant change that has occurred to the state
     * since the snapshot was taken. The snapshot information and the
     * incremental change information will be stored in separate files
     * called, respectively, the snapshot file and the log file. Together,
     * these files are used to recover the state of the service after a
     * crash or a network outage (or if the service or its ActivationGroup
     * is un-registered and then re-registered through the Activation Daemon).
     * <p>
     * This class contains the methods that are used to record and recover
     * the snapshot of the service's state; as well as the method used to
     * apply the state changes that were recorded in the log file.
     * <p>
     * When the ReliableLog class is instantiated, a new instance of this
     * class is passed to its constructor so that the methods of this
     * class may be invoked by the methods defined in the ReliableLog.
     * Because this class extends the LogHandler class associated with
     * the ReliableLog class, this class must provide implementations of
     * the abstract methods declared in the LogHandler. Also, some of the
     * methods defined in this class override the methods of the LogHandler
     * in order to customize the handling of snapshot creation and
     * retrieval.
     * <p>
     * Each significant change to the service's state is written to the
     * log file as an individual record (when addLogRecord() is invoked).
     * After the number of records logged exceeds a pre-defined threshold,
     * a snapshot of the state is recorded by invoking -- through the
     * ReliableLog and its LogHandler -- the snapshot() method defined in
     * this class. After the snapshot is taken, the log file is cleared
     * and the incremental log process starts over.
     * <p>
     * The contents of the snapshot file reflect the DATA contained in
     * the fields making up the current state of the service. That data
     * represents many changes -- over time -- to the service's state.
     * On the other hand, each record written to the log file is an object
     * that reflects both the data used and the ACTIONS taken to make one
     * change to the service's state at a particular point in time.
     * <p>
     * During recovery, the state of the service at the time of a crash
     * or outage is re-constructed by first retrieving the 'base' state from
     * the snapshot file; and then modifying that base state according to
     * the records retrieved from the log file. The reconstruction of the
     * base state is achieved by invoking the recover() method defined in
     * this class. The modifications recorded in the log file are then
     * applied to the base state by invoking the applyUpdate() method
     * defined in this class. Both recover() and applyUpdate() are invoked
     * through the ReliableLog and its associated LogHandler.
     * <p>
     * NOTE: The following lines must be added to the service's policy file
     * <pre>
     *     permission java.io.FilePermission "dirname",   "read,write,delete";
     *     permission java.io.FilePermission "dirname/-", "read,write,delete";
     * </pre>
     *     where 'dirname' is the name of the directory path (relative or
     *     absolute) where the snapshot and log file will be maintained.
     */
    private class LocalLogHandler extends LogHandler {
        /** No-arg public constructor */
        public LocalLogHandler() { }

        /* Overrides snapshot() defined in ReliableLog's LogHandler class. */
        public void snapshot(OutputStream out) throws IOException {
            takeSnapshot(out);
        }//end snapshot

        /* Overrides recover() defined in ReliableLog's LogHandler class. */
        public void recover(InputStream in)
                            throws IOException, ClassNotFoundException
        {
            recoverSnapshot(in);
        }//end recover

        /**
         * Required method that implements the abstract applyUpdate()
         * defined in ReliableLog's associated LogHandler class.
         * <p>
         * During state recovery, the recover() method defined in the
         * ReliableLog class is invoked. That method invokes the method
         * recoverUpdates() which invokes the method readUpdates(). Both
         * of those methods are defined in ReliableLog. The method
         * readUpdates() retrieves a record from the log file and then
         * invokes this method.
         * <p>
         * This method invokes the version of the method apply() that
         * corresponds to the particular type of 'log record' object
         * that is input as the first argument. The log record object and its
         * corresponding apply() method are defined in one of the so-called
         * LogObj classes. Any instance of one the LogObj classes is an
         * implementation of the LogRecord interface. The particular
         * implementation that is input to this method is dependent on the
         * type of record that was originally logged. The apply() method
         * will then modify the state of the service in a way dictated
         * by the type of record that was retrieved.
         */
        public void applyUpdate(Object logRecObj) {
            ((LogRecord)logRecObj).apply(FiddlerImpl.this);
        }//end applyUpdate
    }//end class LocalLogHandler
    /* ******************* END Inner Class Definitions ********************* */

    /* ******************* BEGIN Thread Class Definitions ****************** */
    /** Thread which is used to monitor the current leases in effect and
     *  cancel (expire) those leases with expiration times that have exceeded
     *  the current time.
     */
    private class LeaseExpireThread extends Thread {
        /** True if thread has been interrupted */
        private boolean interrupted = false;

        /** Create a daemon thread */
        public LeaseExpireThread() {
            super("lease expire");
            setDaemon(true);
        }//end constructor

        public synchronized void interrupt() {
            interrupted = true;
            super.interrupt();
        }//end interrupt

	public synchronized boolean isInterrupted() {
	    return interrupted;
	}//end isInterrupted

        public void run() {
            try {
                concurrentObj.writeLock();
            } catch (ConcurrentLockException e) {
                return;
            }
            try {
                while (!isInterrupted()) {
                    long curTime  = System.currentTimeMillis();
                    minExpiration = Long.MAX_VALUE;
                    /* Loop through registrationByTime removing registrations
                     * with expiration times that are earlier than the current
                     * time. The logic of this loop relies on the fact that 
                     * registrationByTime is a TreeMap in which the elements
                     * are ordered (in ascending order) by the lease expiration
                     * times. Thus, when one registration is encountered with
                     * an expiration time that is later than the current time,
                     * it can be assumed that all remaining registrations have
                     * expiration times that are also later than the current
                     * time; and the loop can be exited. Until such a
                     * registration is encountered, each registration is
                     * removed from its various storage locations.
                     */
                    while (!registrationByTime.isEmpty()) {
                        RegistrationInfo regInfo
                             = (RegistrationInfo)registrationByTime.firstKey();
                        if (regInfo.leaseExpiration > curTime) {
                            minExpiration = regInfo.leaseExpiration;
                            break;
                        }
	                /* The removal of a registration typically involves the
                         * the modification of the managed sets in the
                         * discovery manager, which usually involves starting
                         * the discovery protocol. An IOException can occur
                         * when the discovery protocol fails to start. When
                         * such an exception does occur, register an ERROR
                         * status attribute (along with a Comment attribute
                         * describing the nature of the problem) to all lookup
                         * services with which this service is registered. 
                         *
                         * Administrative clients, as well as clients that use
                         * this service should have registered for notification
                         * of the existence of this attribute.
	                 */
                        try {
                            removeRegistration(regInfo);
                        } catch(IOException e) {
                            String eStr = "Failure while removing "
                                          +"registration (ID = "
                                          +regInfo.registrationID
                                          +") from service state";
                            if( problemLogger.isLoggable(Level.INFO) ) {
                                problemLogger.log(Level.INFO, eStr, e);
                            }//endif
                            Entry[] errorAttrs
                                    = new Entry[]
                                        { new FiddlerStatus(StatusType.ERROR),
                                          new Comment(eStr)
                                        };
                            joinMgr.addAttributes(errorAttrs,true);
                        }
                    }//end while
                    try {
                        concurrentObj.writerWait(leaseExpireThreadSyncObj,
                                                 (minExpiration - curTime));
                    } catch (ConcurrentLockException e) {
                        return;
                    }
                }//end while
            } finally {
                concurrentObj.writeUnlock();
            }
        }//end run
    }//end class LeaseExpireThread

    /**
     * Snapshot-taking thread. 
     * <p>
     * A snapshot is taken when -- after writing a new record to the 
     * log file -- it is determined that the size of the log file has 
     * exceeded a certain threshold. The code which adds the new record 
     * to the log file and which, in turn, decides that a snapshot
     * must be taken is "wrapped" in a writer mutex. That is, synchronization
     * of processing is achieved in this service through a "reader/writer"
     * mutex construct. This construct allows only one writer at any one
     * time; but allows an unlimited number of simultaneous readers as
     * long as no writer has locked the mutex. During steady-state, it is
     * anticipated that far more "read actions" will occur (e.g. discovery
     * events being sent) than "write actions" (e.g. modifying the managed
     * sets). Since the process of taking a snapshot can be time-consuming,
     * if the whole snapshot-taking process occupies that single writer
     * mutex, then a significant number of read actions will be un-necessarily
     * blocked; possibly resulting in an unacceptable degradation in
     * response time. 
     * <p>
     * It is for the above reason that the process of taking a snapshot is
     * performed in a separate thread. The thread waits on the monitor
     * belonging to the snapshotThreadSyncObj instance until it is notified
     * (or "signalled") that a snapshot must be taken. The notification
     * is sent by another thread, created by this service, which determines
     * when the conditions are right for a snapshot. The notification takes
     * the form of an interrupt indicating that the snapshot monitor is
     * available. Although the interrupt is sent while the writer mutex is
     * locked, the act of sending the notification is less time-consuming
     * than the act of taking the snapshot itself. When the thread receives
     * a notification, it awakens and requests a lock on the reader mutex
     * (this is all done in the readerWait() method). Because a reader -- not
     * a writer -- mutex is locked, read-only processes still have access
     * to the system state, so discovery events can be sent and the service's
     * state can be queried; but the reader mutex prevents changes to the
     * state while the snapshot is in progress.  
     * <p>
     * Note that the current snapshot is guaranteed to complete before the
     * next snapshot request is received. This is because even though
     * the act of taking a snapshot can be viewed as a writer process, 
     * the fact that the next snapshot notification will be wrapped in a
     * writer mutex, combined with the fact that a writer mutex can not
     * be locked while a reader mutex is locked, allows the snapshot to
     * be treated as a reader process.
     */
    private class SnapshotThread extends Thread {
        /** True if thread has been interrupted */
        private boolean interrupted = false;

        /** Create a daemon thread */
        public SnapshotThread() {
            super("snapshot thread");
            setDaemon(true);
        }

        public synchronized void interrupt() {
            interrupted = true;
            super.interrupt();
        }//end interrupt

	public synchronized boolean isInterrupted() {
	    return interrupted;
	}//end isInterrupted

        public void run() {
            try {
                concurrentObj.readLock();
            } catch (ConcurrentLockException e) {
                return;
            }
            try {
                while (!isInterrupted()) {
                    try {
                        concurrentObj.readerWait(snapshotThreadSyncObj,
                                                 Long.MAX_VALUE);
                        try {
                            log.snapshot();
                            logFileSize = 0;
                        } catch (Exception e) {
                            if (isInterrupted())  return;
                            /* If taking the snapshot fails for any reason,
                             * then register an ERROR status attribute (along
                             * with a Comment attribute describing the nature
                             * of the problem) to all lookup services with
                             * which this service is registered. 
                             *
                             * Administrative clients, as well as clients that
                             * use this service should have registered for
                             * notification of the existence of this attribute.
	                     */
                            String eStr = "Failure while taking a snapshot of "
                                          +"the service state";
                            problemLogger.log(Level.INFO, eStr, e);
                            Entry[] errorAttrs
                                    = new Entry[]
                                        { new FiddlerStatus(StatusType.ERROR),
                                          new Comment(eStr)
                                        };
                            joinMgr.addAttributes(errorAttrs,true);
                        }
                    } catch (ConcurrentLockException e) {
                        return;
                    }
                }//end while
            } finally {
                concurrentObj.readUnlock();
            }
        }//end run
    }//end class SnapshotThread

    /** Thread which is used to terminate the current executing instance
     *  of the Fiddler implementation of the lookup discovery service. 
     *  Termination processing is performed in a separate thread (that is,
     *  in an instance of this class) in order to avoid deadlock that 
     *  can occur because ActivationGroup.inactive will block until all 
     *  in-progress RMI calls have completed.
     */
    private class DestroyThread extends Thread {
        /** Maximum delay for unexport attempts */
        private static final long MAX_UNEXPORT_DELAY = 2*TimeConstants.MINUTES;

	/** Constructor that creates a non-daemon thread */
	public DestroyThread() {
	    super("destroy");
	    /* override inheritance from RMI daemon thread */
	    setDaemon(false);
	}

	public void run() {
	    /* Must unregister before unexporting. Unregistering makes sure
             * that the object corresponding to the given activation ID can
             * no longer be activated through that ID.
             */
	    if (activationID != null) {
		try {
                    activationSystem.unregisterObject(activationID);
		} catch (RemoteException e) {
		    return;//give up until we can at least unregister
		} catch (ActivationException e) { }
	    }
            /* Unexport the object. This removes the object from the RMI
             * runtime so that the object can no longer accept incoming RMI
             * calls. 
             * 
             * An attempt to 'gracefully' unexport the object is initially
             * made. That is, for a finite period of time, an attempt is
             * made to allow all calls to the object that are in progress
             * or pending to complete before the object is unexported. If,
             * after that finite period of time, the object has not been
             * successfully unexported, the object is 'forcibly' unexported;
             * that is, the object is unexported even if there are calls to
             * the object that are in progress or still pending.
             */
            final long endTime = System.currentTimeMillis()+MAX_UNEXPORT_DELAY;
            boolean unexported = false;
                /* Unexport only if there are no pending or in-progress calls*/
                while(!unexported && (System.currentTimeMillis() < endTime)) {
                    unexported = serverExporter.unexport(false);
                    if(!unexported) Thread.yield();
                }//end loop
            if(!unexported) {//Not yet unexported. Forcibly unexport
                serverExporter.unexport(true);
            }//endif
	    /* all daemons must terminate before deleting persistent store */
	    leaseExpireThread.interrupt();
	    if(log != null) snapshotThread.interrupt();
	    taskMgr.terminate();
	    joinMgr.terminate();
            joinMgrLDM.terminate();
            discoveryMgr.terminate();
	    try {
		leaseExpireThread.join();
		if(log != null) snapshotThread.join();
	    } catch (InterruptedException e) { }
	    if(log != null) log.deletePersistentStore();
	    if (activationID != null) {
                /* Inform the activation system that the object corresponding
                 * to the given activation ID is no longer active.
                 */
		try {
		    ActivationGroup.inactive(activationID, serverExporter);
		} catch (RemoteException e) {
		} catch (ActivationException e) { }
            } else {//not activatable, tell starter it's ok to release for gc
                if(lifeCycle != null)  lifeCycle.unregister(FiddlerImpl.this);
            }//endif(activationID != null)
            /* If applicable, logout of the JAAS login session */
            if(loginContext != null) {
                try {
                    loginContext.logout();
                } catch(Exception e) {
                    startupLogger.log(Level.INFO,"Problem logging out of "
                                                 +"JAAS login session",e);
                }
            }//endif
            logInfoShutdown();
        }//end run
    }//end class DestroyThread
    /* ******************* END Thread Class Definitions ******************** */

    /* ************************ BEGIN Public Methods *********************** */
    /* -------------------------------------------------------------------- 
     * BEGIN net.jini.security.proxytrust.ServerProxyTrust
     */
    /** 
     * Returns a <code>TrustVerifier</code> specific to this service which
     * can be used to verify that a given proxy to this service can be
     * trusted.
     * <p>
     * The verifier returned by this method contains the method
     * {@link TrustVerifier#isTrustedObject isTrustedObject}. That method 
     * can be called with a candidate proxy as the first argument, and
     * {@link net.jini.security.TrustVerifier.Context}
     * as the second argument. When called in this way, the 
     * <code>isTrustedObject</code> determines whether or not the input
     * proxy is trusted. Thus, the verifier returned by this method should
     * be able to verify as trusted, all proxies to this service; including
     * proxies such as leases, event registrations, and administrative
     * proxies.
     * 
     * @return a <code>TrustVerifier</code> which can be used to verify that
     *         a given proxy to this service can be trusted.
     * 
     * @throws UnsupportedOperationException if the server proxy does not
     *	       implement both
     *         {@link net.jini.core.constraint.RemoteMethodControl}
     *         and {@link TrustEquivalence}
     *
     * @see net.jini.security.proxytrust.ServerProxyTrust#getProxyVerifier
     */
    public TrustVerifier getProxyVerifier() {
	return new ProxyVerifier(innerProxy, proxyID);
    }//end getProxyVerifier
    /*  END net.jini.security.proxytrust.ServerProxyTrust                   */
    /* -------------------------------------------------------------------- */

    /* -------------------------------------------------------------------- 
     * BEGIN net.jini.export.ProxyAccessor
     */
    /**
     * Public method that facilitates the use of the mechanism provided by
     * {@link com.sun.jini.start.ServiceStarter} to create an activatable
     * instance of this server.
     * 
     * @return the inner proxy (stub or dynamic proxy) for the server
     */
    public Object getProxy() {
        return innerProxy;
    }//end getProxy
    /*  END net.jini.export.ProxyAccessor                                   */
    /* -------------------------------------------------------------------- */

    /* -------------------------------------------------------------------- 
     * BEGIN com.sun.jini.fiddler.Fiddler --> net.jini.admin.Administrable
     */
    /** 
     * Returns a proxy to the current instance of this class through which
     * a client may administer the lookup discovery service
     *
     * @return a proxy object through which the lookup discovery service
     *         may be administered.
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         server.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#getAdmin
     * @see net.jini.admin.Administrable#getAdmin
     */
    public Object getAdmin() throws RemoteException {
        return adminProxy;
    }
    /*  END com.sun.jini.fiddler.Fiddler --> net.jini.admin.Administrable   */
    /* -------------------------------------------------------------------- */

    /* -------------------------------------------------------------------- 
     * BEGIN com.sun.jini.fiddler.Fiddler
     *                                --> com.sun.jini.fiddler.FiddlerAdmin
     *                                         --> net.jini.admin.JoinAdmin
     */
    /** 
     * Returns the current attribute sets for the lookup discovery service. 
     * 
     * @return array of net.jini.core.entry.Entry containing the current
     *         attribute sets for the lookup discovery service
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#getLookupAttributes
     * @see net.jini.admin.JoinAdmin#getLookupAttributes
     */
    public Entry[] getLookupAttributes() throws RemoteException {
        concurrentObj.readLock();
        try {
            return thisServicesAttrs;
        } finally {
            concurrentObj.readUnlock();
        }
    }//end getLookupAttributes

    /** 
     * Adds attribute sets to the current set of attributes associated
     * with the lookup discovery service. The resulting set will be used
     * for all future registrations with lookup services. The new attribute
     * sets are also added to the lookup discovery service's attributes
     * on each lookup service with which the lookup discovery service
     * is currently registered.
     *
     * @param  attrSets array of net.jini.core.entry.Entry containing the
     *         attribute sets to add
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         attributes may or may not have been added successfully.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#addLookupAttributes
     * @see net.jini.admin.JoinAdmin#addLookupAttributes
     */
    public void addLookupAttributes(Entry[] attrSets) throws RemoteException {
        concurrentObj.writeLock();
        try {
            joinMgr.addAttributes(attrSets, true);
            thisServicesAttrs = joinMgr.getAttributes();
	    addLogRecord(new LookupAttrsAddedLogObj(this,attrSets));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end addLookupAttributes

    /** 
     * Modifies the current set of attributes associated with the lookup
     * discovery service. The resulting set will be used for all future
     * registrations with lookup services. The same modifications are 
     * also made to the lookup discovery service's attributes on each
     * lookup service with which the lookup discovery service is currently
     * registered.
     *
     * @param  attrSetTemplates  array of net.jini.core.entry.Entry containing
     *         the templates to use for selecting the attributes (contained
     *         within the set of existing attributes) that are to be
     *         modified
     * @param  attrSets array of net.jini.core.entry.Entry containing the
     *         modifications to make to matching sets
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         attributes may or may not have been modified successfully.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#modifyLookupAttributes
     * @see net.jini.admin.JoinAdmin#modifyLookupAttributes
     */
    public void modifyLookupAttributes(Entry[] attrSetTemplates,
				       Entry[] attrSets) throws RemoteException
    {
        concurrentObj.writeLock();
        try {
            joinMgr.modifyAttributes(attrSetTemplates, attrSets, true);
            thisServicesAttrs = joinMgr.getAttributes();
            addLogRecord
               (new LookupAttrsModifiedLogObj(this,attrSetTemplates,attrSets));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end modifyLookupAttributes

    /** 
     * Get the names of the groups whose members are lookup services the
     * lookup discovery services wishes to register with (join).
     * 
     * @return String array containing the names of the groups whose members
     *         are lookup services the lookup discovery service wishes to
     *         join.
     * <p>
     *         If the array returned is empty, the lookup discovery service
     *         is configured to join no groups. If null is returned, the
     *         lookup discovery service is configured to join all groups.
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#getLookupGroups
     * @see net.jini.admin.JoinAdmin#getLookupGroups
     */
    public String[] getLookupGroups() throws RemoteException {
        concurrentObj.readLock();
        try {
            return thisServicesGroups;
        } finally {
            concurrentObj.readUnlock();
        }
    }//end getLookupGroups

    /** 
     * Add new names to the set consisting of the names of groups whose
     * members are lookup services the lookup discovery service wishes
     * to register with (join). Any lookup services belonging to the
     * new groups that the lookup discovery service has not yet registered
     * with, will be discovered and joined.
     *
     * @param  String array containing the names of the groups to add
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         group names may or may not have been added successfully.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#addLookupGroups
     * @see net.jini.admin.JoinAdmin#addLookupGroups
     */
    public void addLookupGroups(String[] groups) throws RemoteException {
        concurrentObj.writeLock();
        try {
            try {
                ((DiscoveryGroupManagement)joinMgrLDM).addGroups(groups);
            } catch (IOException e) {
                throw new RuntimeException(e.toString());
            }
            thisServicesGroups
                         = ((DiscoveryGroupManagement)joinMgrLDM).getGroups();
            addLogRecord(new LookupGroupsChangedLogObj(thisServicesGroups));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end addLookupGroups

    /** 
     * Remove a set of group names from lookup discovery service's managed
     * set of groups (the set consisting of the names of groups whose
     * members are lookup services the lookup discovery service wishes
     * to join). Any leases granted to the lookup discovery service by
     * lookup services that are not members of the groups whose names 
     * remain in the managed set will be cancelled at those lookup services.
     *
     * @param  String array containing the names of the groups to remove
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         group names may or may not have been removed successfully.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#removeLookupGroups
     * @see net.jini.admin.JoinAdmin#removeLookupGroups
     */
    public void removeLookupGroups(String[] groups) throws RemoteException {
        concurrentObj.writeLock();
        try {
            ((DiscoveryGroupManagement)joinMgrLDM).removeGroups(groups);
            thisServicesGroups
                         = ((DiscoveryGroupManagement)joinMgrLDM).getGroups();
            addLogRecord(new LookupGroupsChangedLogObj(thisServicesGroups));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end removeLookupGroups

    /** 
     * Replace the lookup discovery service's managed set of groups with a
     * new set of group names. Any leases granted to the lookup discovery
     * service by lookup services that are not members of the groups whose
     * names are in the new managed set will be cancelled at those lookup
     * services. Lookup services that are members of groups reflected in
     * the new managed set will be discovered and joined.
     *
     * @param  String array containing the names of the new groups
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         group names may or may not have been replaced successfully.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#setLookupGroups
     * @see net.jini.admin.JoinAdmin#setLookupGroups
     */
    public void setLookupGroups(String[] groups) throws RemoteException {
        concurrentObj.writeLock();
        try {
            try {
                ((DiscoveryGroupManagement)joinMgrLDM).setGroups(groups);
            } catch (IOException e) {
                throw new RuntimeException(e.toString());
            }
            thisServicesGroups 
                         = ((DiscoveryGroupManagement)joinMgrLDM).getGroups();
            addLogRecord(new LookupGroupsChangedLogObj(thisServicesGroups));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end setLookupGroups

    /** 
     * Get the lookup discovery service's managed set of locators. The
     * managed set of locators is the set of LookupLocator objects
     * corresponding to the specific lookup services with which the lookup
     * discovery service wishes to register (join).
     * 
     * @return array of objects of type net.jini.core.discovery.LookupLocator,
     *         each of which corresponds to a specific lookup service the
     *         lookup discovery service wishes to join.
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#getLookupLocators
     * @see net.jini.admin.JoinAdmin#getLookupLocators
     */
    public LookupLocator[] getLookupLocators() throws RemoteException {
        concurrentObj.readLock();
        try {
            return thisServicesLocators;
        } finally {
            concurrentObj.readUnlock();
        }
    }//end getLookupLocators

    /** 
     * Add a set of LookupLocator objects to the lookup discovery service's
     * managed set of locators. The managed set of locators is the set of
     * LookupLocator objects corresponding to the specific lookup services
     * with which the lookup discovery service wishes to register (join).
     * <p>
     * Any lookup services corresponding to the new locators that the lookup
     * discovery service has not yet joined, will be discovered and joined.
     *
     * @param  array of net.jini.core.discovery.LookupLocator objects to add
     *         to the managed set of locators
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         new locators may or may not have been added successfully.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#addLookupLocators
     * @see net.jini.admin.JoinAdmin#addLookupLocators
     */
    public void addLookupLocators(LookupLocator[] locators)
                                                      throws RemoteException
    {
        /* Prepare outside of sync block because of possible remote call */
        prepareNewLocators(locatorToJoinPreparer,locators);
        concurrentObj.writeLock();
        try {
            ((DiscoveryLocatorManagement)joinMgrLDM).addLocators(locators);
            thisServicesLocators 
                     = ((DiscoveryLocatorManagement)joinMgrLDM).getLocators();
            addLogRecord
                     (new LookupLocatorsChangedLogObj(thisServicesLocators));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end addLookupLocators

    /** 
     * Remove a set of LookupLocator objects from the lookup discovery
     * service's managed set of locators. The managed set of locators is the
     * set of LookupLocator objects corresponding to the specific lookup
     * services with which the lookup discovery service wishes to register
     * (join).
     * <p>
     * Note that any leases granted to the lookup discovery service by
     * lookup services that do not correspond to any of the locators
     * remaining in the managed set will be cancelled at those lookup
     * services.
     *
     * @param  array of net.jini.core.discovery.LookupLocator objects to
     *         remove from the managed set of locators
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         new locators may or may not have been removed successfully.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#removeLookupLocators
     * @see net.jini.admin.JoinAdmin#removeLookupLocators
     */
    public void removeLookupLocators(LookupLocator[] locators)
                                                       throws RemoteException
    {
        /* Prepare outside of sync block because of possible remote call */
        prepareNewLocators(locatorToJoinPreparer,locators);
        concurrentObj.writeLock();
        try {
            ((DiscoveryLocatorManagement)joinMgrLDM).removeLocators(locators);
            thisServicesLocators 
                     = ((DiscoveryLocatorManagement)joinMgrLDM).getLocators();
            addLogRecord
                     (new LookupLocatorsChangedLogObj(thisServicesLocators));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end removeLookupLocators

    /** 
     * Replace the lookup discovery service's managed set of locators with
     * a new set of locators. The managed set of locators is the set of
     * LookupLocator objects corresponding to the specific lookup services
     * with which the lookup discovery service wishes to register (join).
     * <p>
     * Note that any leases granted to the lookup discovery service by
     * lookup services whose corresponding locator is removed from the
     * managed set will be cancelled at those lookup services. The lookup
     * services corresponding to the new locators in the managed set
     * will be discovered and joined.
     *
     * @param  array of net.jini.core.discovery.LookupLocator objects with
     *         which to replace the current managed set of locators
     *         remove from the managed set of locators
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         locators in the managed set may or may not have been replaced
     *         successfully.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#setLookupLocators
     * @see net.jini.admin.JoinAdmin#setLookupLocators
     */
    public void setLookupLocators(LookupLocator[] locators)
                                                       throws RemoteException
    {
        /* Prepare outside of sync block because of possible remote call */
        prepareNewLocators(locatorToJoinPreparer,locators);
        concurrentObj.writeLock();
        try {
            ((DiscoveryLocatorManagement)joinMgrLDM).setLocators(locators);
            thisServicesLocators 
                    = ((DiscoveryLocatorManagement)joinMgrLDM).getLocators();
            addLogRecord
                     (new LookupLocatorsChangedLogObj(thisServicesLocators));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end setLookupLocators
    /* END com.sun.jini.fiddler.Fiddler --> com.sun.jini.fiddler.FiddlerAdmin
     *                                           --> net.jini.admin.JoinAdmin
     * -------------------------------------------------------------------- */

    /* -------------------------------------------------------------------- 
     * BEGIN com.sun.jini.fiddler.Fiddler
     *                            --> com.sun.jini.fiddler.FiddlerAdmin
     *                                  --> com.sun.jini.admin.DestroyAdmin
     */
    /**
     * Destroy the lookup discovery service, if possible, including its
     * persistent storage. This method will typically spawn a separate
     * thread to do the actual work asynchronously, so a successful
     * return from this method usually does not mean that the service
     * has been destroyed.
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         lookup discovery service may or may not have been successfully
     *         destroyed.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#destroy
     * @see com.sun.jini.admin.DestroyAdmin#destroy
     */
    public void destroy() throws RemoteException {
        destroyDo();
    }//end destroy
    /* END com.sun.jini.fiddler.Fiddler
     *                          --> com.sun.jini.fiddler.FiddlerAdmin
     *                                  --> com.sun.jini.admin.DestroyAdmin
     * -------------------------------------------------------------------- */

    /* -------------------------------------------------------------------- 
     * BEGIN com.sun.jini.fiddler.Fiddler
     *                                --> com.sun.jini.fiddler.FiddlerAdmin
     */
    /**
     * Changes the least upper bound applied to all lease durations granted
     * by the lookup discovery service.
     * <p>
     * This method is a mechanism for an entity with the appropriate
     * privileges to administratively change the value of the least upper
     * bound that will be applied by the Fiddler implementation of the lookup
     * discovery service when determining the duration to assign to the lease
     * on a requested registration.
     *
     * @param newBound <code>long</code> value representing the new least
     *        upper bound (in milliseconds) on the set of all possible
     *        lease durations that may be granted
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         bound value may or may not have been changed successfully.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#setLeaseBound
     * @see com.sun.jini.fiddler.FiddlerAdmin#setLeaseBound
     */
    public void setLeaseBound(long newBound) throws RemoteException {
        concurrentObj.writeLock();
        try {
	    if (newBound > leaseMax) {
                throw new IllegalArgumentException("max duration exceeded");
            }//endif
            leaseBound = newBound;
            addLogRecord(new LeaseBoundSetLogObj(newBound));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end setLeaseBound

    /**
     * Retrieves the least upper bound applied to all lease durations granted
     * by the lookup discovery service.
     *
     * @return <code>long</code> value representing the current least
     *         upper bound (in milliseconds) on the set of all possible
     *         lease durations that may be granted
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#getLeaseBound
     * @see com.sun.jini.fiddler.FiddlerAdmin#getLeaseBound
     */
    public long getLeaseBound() throws RemoteException {
        concurrentObj.readLock();
        try {
            return leaseBound;
        } finally {
            concurrentObj.readUnlock();
        }
    }//end getLeaseBound

    /**
     * Change the weight factor applied by the lookup discovery service
     * to the snapshot size during the test to determine whether or not
     * to take a "snapshot" of the system state.
     *
     * @param weight weight factor for snapshot size
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         weight factor may or may not have been changed successfully.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#setPersistenceSnapshotWeight
     * @see com.sun.jini.fiddler.FiddlerAdmin#setPersistenceSnapshotWeight
     */
    public void setPersistenceSnapshotWeight(float weight)
                                                      throws RemoteException
    {
        concurrentObj.writeLock();
        try {
            snapshotWt = weight;
            addLogRecord(new SnapshotWeightSetLogObj(weight));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end setPersistenceSnapshotWeight

    /**
     * Retrieve the weight factor applied by the lookup discovery service
     * to the snapshot size during the test to determine whether or not to
     * take a "snapshot" of the system state.
     * 
     * @return float value corresponding to the weight factor for snapshot
     *         size
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy#getPersistenceSnapshotWeight
     * @see com.sun.jini.fiddler.FiddlerAdmin#getPersistenceSnapshotWeight
     */
    public float getPersistenceSnapshotWeight() throws RemoteException {
        concurrentObj.readLock();
        try {
            return snapshotWt;
        } finally {
            concurrentObj.readUnlock();
        }
    }//end getPersistenceSnapshotWeight

    /**
     * Change the value of the size threshold of the snapshot; which is
     * employed by the lookup discovery service in the test to determine
     * whether or not to take a "snapshot" of the system state.
     *
     * @param threshold size threshold for taking a snapshot
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         threshold may or may not have been changed successfully.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy
     *                                       #setPersistenceSnapshotThreshold
     * @see com.sun.jini.fiddler.FiddlerAdmin#setPersistenceSnapshotThreshold
     */
    public void setPersistenceSnapshotThreshold(int threshold)
                                                        throws RemoteException
    {
        concurrentObj.writeLock();
        try {
            snapshotThresh = threshold;
            addLogRecord(new SnapshotThresholdSetLogObj(threshold));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end setPersistenceSnapshotThreshold

    /**
     * Retrieve the value of the size threshold of the snapshot; which is
     * employed by the lookup discovery service in the test to determine
     * whether or not to take a "snapshot" of the system state.
     * 
     * @return int value corresponding to the size threshold of the snapshot
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service.
     *
     * @see com.sun.jini.fiddler.FiddlerAdminProxy
     *                                       #getPersistenceSnapshotThreshold
     * @see com.sun.jini.fiddler.FiddlerAdmin#getPersistenceSnapshotThreshold
     */
    public int getPersistenceSnapshotThreshold() throws RemoteException {
        concurrentObj.readLock();
        try {
	    return snapshotThresh;
        } finally {
            concurrentObj.readUnlock();
        }
    }//end getPersistenceSnapshotThreshold

    /* END com.sun.jini.fiddler.Fiddler --> com.sun.jini.fiddler.FiddlerAdmin
     * -------------------------------------------------------------------- */

    /* -------------------------------------------------------------------- 
     * BEGIN com.sun.jini.fiddler.Fiddler
     *                          --> com.sun.jini.start.ServiceProxyAccessor
     */
    /**
     * Public method that facilitates the use of the mechanism provided by
     * {@link com.sun.jini.start.ServiceStarter} to create an activatable
     * instance of this server.
     * 
     * @return the outer (smart) proxy for the server
     */
    public Object getServiceProxy() {
        return outerProxy;
    }//end getServiceProxy

    /*  END com.sun.jini.fiddler.Fiddler 
     *                        --> com.sun.jini.start.ServiceProxyAccessor   */
    /* -------------------------------------------------------------------- */

    /* -------------------------------------------------------------------- 
     * BEGIN com.sun.jini.fiddler.Fiddler
     */
    /**
     * Returns the unique identifier generated (or recovered) by the backend
     * implementation of the lookup discovery service when an instance of
     * that service is constructed. This ID is typically used to determine
     * equality between the proxies of any two instances of the lookup
     * discovery service.
     * 
     * @return the unique ID that was generated (or recovered) by the
     *         backend implementation of the lookup discovery service
     *         at creation time
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service.
     */
    public Uuid getProxyID() throws RemoteException {
        return proxyID;
    }//end getProxyID

    /**
     * Registers with the lookup discovery service. When a client invokes
     * this method, it requests that the lookup discovery service perform
     * discovery processing on its behalf.
     *
     * @param groups        String array, none of whose elements may be null,
     *                      consisting of zero or more names of groups to
     *                      which lookup services to discover belong.
     *                      A null value or an empty array
     *                      (DiscoveryGroupManagement.ALL_GROUPS or 
     *                      DiscoveryGroupManagement.NO_GROUPS) are both
     *                      acceptable.
     * @param locators      array of zero or more non-null LookupLocator
     *                      objects, each corresponding to a specific lookup
     *                      service to discover. If either the empty array
     *                      or null is passed to this argument, then no
     *                      locator discovery will be performed for the
     *                      associated registration.
     * @param listener      a non-null instance of RemoteEventListener. This 
     *                      argument specifies the entity that will receive
     *                      events notifying the registration that a lookup
     *                      service of interest has been discovered. A 
     *                      non-null value must be passed to this argument,
     *                      otherwise a NullPointerException will be thrown
     *                      and the registration.
     * @param handback      null or an instance of MarshalledObject. This
     *                      argument specifies an object that will be 
     *                      included in the notification event that the
     *                      lookup discovery service sends to the registered
     *                      listener.
     * @param leaseDuration long value representing the amount of time (in
     *                      milliseconds) for which the resources of the
     *                      lookup discovery service are being requested.
     *
     * @return an instance of FiddlerRegistration which implements the
     *         LookupDiscoveryRegistration interface, and acts as a proxy
     *         to the registration-related methods of the backend server
     *         of the Fiddler implementation of the lookup discovery service
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         server. When this exception does occur, the registration may
     *         or may not have completed successfully.
     *
     * @throws java.lang.NullPointerException this exception occurs when
     *         null is input to the <code>listener</code> parameter, as well
     *         as when one or more of the elements of the <code>groups</code>
     *         parameter is null.
     *
     * @throws java.lang.IllegalArgumentException this exception occurs when
     *         the value input to the <code>leaseDuration</code> parameter
     *         is neither positive, Lease.FOREVER, nor Lease.ANY.
     *
     * @see net.jini.discovery.LookupDiscoveryService
     */
    public LookupDiscoveryRegistration register(String[] groups,
                                                LookupLocator[] locators,
                                                RemoteEventListener listener,
                                                MarshalledObject handback,
                                                long leaseDuration)
                                                        throws RemoteException
    {
        /* The spec says that a null locators array implies no loc discovery */
        if( locators == null) {
            locators = new LookupLocator[0];
        }//endif
        if(containsNullElement(groups)) {
            throw new NullPointerException(" on call to register() method, at "
                                          +"least one null element in groups");
        } else if (containsNullElement(locators)) {
            throw new NullPointerException(" on call to register() method, at "
                                        +"least one null element in locators");
        } else if (listener == null) {
            throw new NullPointerException(" null listener input to "
                                           +"register() method");
        }//endif
        /* Prepare the locators associated with the requested registration 
         * outside of the sync block because of possible remote call.
         */
        prepareNewLocators(locatorToDiscoverPreparer,locators);
        LookupDiscoveryRegistration reg = null;
        concurrentObj.writeLock();
        try {
            /*  Grant the registration request and add the registration to
             *  to this service's state.
             *
             *  The addition of a registration to this service's state
             *  typically involves the modification of the managed sets in 
             *  the discovery manager, which usually involves starting the 
             *  discovery protocol. An IOException can occur when the 
             *  discovery protocol fails to start. When such an exception 
             *  does occur, register an ERROR status attribute (along with 
             *  a Comment attribute describing the nature of the problem) to 
             *  all lookup services with which this service is registered. 
             *
             *  Administrative clients, as well as clients that use this 
             *  service should have registered for notification of the 
             *  existence of this attribute.
             */
            reg = registerDo(groups,locators,listener,handback,leaseDuration);
        } catch(RemoteException e) {
            /* Catch, log, and rethrow so RemoteException is not included
             * in the catch block for IOException below.
             */
            problemLogger.log(Level.INFO,
                              "cannot grant registration request", e);
            throw e;
        } catch(IOException e) {
            problemLogger.log(Level.INFO, "cannot grant registration "
                              +"request - multicast problem", e);
            Entry[] errorAttrs 
                  = new Entry[] { new FiddlerStatus(StatusType.ERROR),
                                  new Comment("Failure during registration")
                                };
            joinMgr.addAttributes(errorAttrs,true);
        } finally {
            concurrentObj.writeUnlock();
        }
        return reg;
    }//end register

    /**
     * This method is the "backend" server counterpart to the method of
     * the same name provided by the <code>LookupDiscoveryRegistration</code> 
     * proxy (an instance of <code>FiddlerRegistration</code>) that is
     * returned by this service when a client requests a registration.
     * <p>
     * This method returns an array consisting of proxies to the lookup
     * service(s) that have already been discovered for the registration
     * corresponding to the <code>registrationID</code> input parameter.
     * Each element of the return set is a marshalled instance of the
     * <code>ServiceRegistrar</code> interface.
     *
     * @param registrationID unique identifier assigned to the registration
     *                       from which the set of registrars is being 
     *                       retrieved
     * 
     * @return an array of MarshalledObject objects where each element is
     *         is a marshalled instance of ServiceRegistrar.
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service.
     * 
     * @throws com.sun.jini.proxy.ThrowThis which is a non-remote "wrapper"
     *         class used to wrap various remote exceptions (for example,
     *         NoSuchObjectException) that this method wishes to throw.
     *         When a service is implemented as a smart proxy with a
     *         backend server, and a method on the backend which was invoked
     *         through the proxy wishes to explicitly throw a particular
     *         remote exception, it cannot simply throw that exception if
     *         it wishes that exception to be visible to the proxy running
     *         on the "client side". This is because when the backend throws
     *         any remote exception, the RMI sub-system automatically wraps
     *         that exception in a java.rmi.ServerException. Thus, the proxy
     *         will only be able to "see" the ServerException (the actual
     *         exception that the backend tried to throw is "buried" in the
     *         detail field of the ServerException). Thus, in order to allow
     *         the proxy access to the actual remote exception this method
     *         throws, that exception wraps the desired remote exception in
     *         the non-remote exception ThrowThis; which will not be wrapped
     *         in a ServerException.
     *
     *         This method throws a NoSuchObjectException wrapped in a
     *         ThrowThis exception whenever the <code>registrationID</code>
     *         parameter references an invalid or non-existent registration.
     *
     * @see com.sun.jini.fiddler.FiddlerRegistration#getRegistrars
     * @see net.jini.discovery.LookupDiscoveryRegistration#getRegistrars
     */
    public MarshalledObject[] getRegistrars(Uuid registrationID)
                                              throws RemoteException,ThrowThis
    {
        concurrentObj.readLock();
        try {
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new ThrowThis
                        (new NoSuchObjectException("Invalid registration "
                                                   +"ID on call to "
                                                   +"getRegistrars() method"));
            }//endif
            Collection mVals = (regInfo.discoveredRegsMap).values(); 
            return ( (MarshalledObject[])(mVals).toArray
                                        (new MarshalledObject[mVals.size()]) );
        } finally {
            concurrentObj.readUnlock();
        }
    }//end getRegistrars

    /**
     * This method is the "backend" server counterpart to the method of
     * the same name provided by the <code>LookupDiscoveryRegistration</code> 
     * proxy (an instance of <code>FiddlerRegistration</code>) that is
     * returned by this service when a client requests a registration.
     * <p>
     * This method returns an array consisting of the names of the groups
     * whose members are lookup services the lookup discovery service will
     * attempt to discover for the registration corresponding to the current
     * instance of this class. This set of group names is referred to as the
     * registration's 'managed set of groups'.
     * <p>
     * If the registration's managed set of groups is currently empty, then
     * the empty array is returned. If the lookup discovery service currently
     * has no managed set of groups for the registration through which the
     * request is being made, then null will be returned.
     *
     * @param registrationID unique identifier assigned to the registration
     *                       from which the set of groups is being retrieved
     * 
     * @return a String array containing the elements of the managed set of
     *         groups for the registration.
     *
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service.
     * 
     * @throws java.rmi.NoSuchObjectException wrapped in an instance of
     *         com.sun.jini.proxy.ThrowThis exception whenever the
     *         <code>registrationID</code> parameter references an invalid
     *         or non-existent registration. Refer to the description of the
     *         <code>getRegistrars</code> method for more information on
     *         this exception.
     *
     * @see com.sun.jini.fiddler.FiddlerRegistration#getGroups
     * @see net.jini.discovery.LookupDiscoveryRegistration#getGroups
     */
    public String[] getGroups(Uuid registrationID) 
                                              throws RemoteException,ThrowThis
    {
        concurrentObj.readLock();
        try {
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new ThrowThis
                            (new NoSuchObjectException("Invalid registration "
                                                       +"ID on call to "
                                                       +"getGroups() method"));
            }//endif
            String[] groups = null;
            if(regInfo.groups == null) {
                groups = DiscoveryGroupManagement.ALL_GROUPS;
            } else {
                groups = (String[])(regInfo.groups).toArray
                                           (new String[regInfo.groups.size()]);
            }//endif (regInfo.groups == null)
            return groups;
        } finally {
            concurrentObj.readUnlock();
        }
    }//end getGroups

    /**
     * This method is the "backend" server counterpart to the method of
     * the same name provided by the <code>LookupDiscoveryRegistration</code> 
     * proxy (an instance of <code>FiddlerRegistration</code>) that is
     * returned by this service when a client requests a registration.
     * <p>
     * This method returns an array consisting of the the LookupLocator
     * objects corresponding to specific lookup services the lookup discovery
     * service will attempt to discover for for the registration
     * corresponding to the current instance of this class. This set of
     * locators is referred to as the registration's 'managed set of locators'.
     * <p>
     * If the registration's managed set of locators is currently empty, then
     * the empty array is returned. If the lookup discovery service currently
     * has no managed set of locators for the registration through which the
     * request is being made, then null will be returned.
     *
     * @param registrationID unique identifier assigned to the registration
     *                       from which the set of locators is being retrieved
     * 
     * @return array consisting of net.jini.core.discovery.LookupLocator
     *         objects corresponding to the elements of the managed set of
     *         locators for the registration.
     *
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service.
     * 
     * @throws java.rmi.NoSuchObjectException wrapped in an instance of
     *         com.sun.jini.proxy.ThrowThis exception whenever the
     *         <code>registrationID</code> parameter references an invalid
     *         or non-existent registration. Refer to the description of the
     *         <code>getRegistrars</code> method for more information on
     *         this exception.
     *
     * @see com.sun.jini.fiddler.FiddlerRegistration#getLocators
     * @see net.jini.discovery.LookupDiscoveryRegistration#getLocators
     */
    public LookupLocator[] getLocators(Uuid registrationID)
                                              throws RemoteException,ThrowThis
    {
        concurrentObj.readLock();
        try {
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new ThrowThis
                        (new NoSuchObjectException("Invalid registration "
                                                   +"ID on call to "
                                                   +"getLocators() method"));
            }//endif
            return (LookupLocator[])(regInfo.locators).toArray
                                  (new LookupLocator[regInfo.locators.size()]);
        } finally {
            concurrentObj.readUnlock();
        }
    }//end getLocators

    /**
     * This method is the "backend" server counterpart to the method of
     * the same name provided by the <code>LookupDiscoveryRegistration</code> 
     * proxy (an instance of <code>FiddlerRegistration</code>) that is
     * returned by this service when a client requests a registration.
     * <p>
     * This method first tests the input set of group names for validity and 
     * throws the appropriate exception should any irregularities be found.
     * It then adds the input set of group names to the managed set of groups
     * associated with the registration.
     * 
     * @param registrationID unique identifier assigned to the registration
     *                       to which the set of groups being augmented
     *                       corresponds
     * @param groups         a String array, none of whose elements may be
     *                       null, consisting of the group names with which to
     *                       augment the registration's managed set of groups.
     * <p>
     *                       If any element of this parameter duplicates any
     *                       other element of this parameter, the duplicate
     *                       will be ignored. If any element of this parameter
     *                       duplicates any element of the registration's
     *                       current managed set of groups, the duplicate will
     *                       be ignored.
     * <p>
     *                       If the empty set is input, then the registration's
     *                       managed set of groups will not change. If null is
     *                       input, this method will throw a
     *                       <code>NullPointerException</code>.
     * 
     * @throws java.lang.IllegalStateException this exception occurs when
     *         the <code>addGroups</code> method of the discovery
     *         manager is invoked after the <code>terminate</code> method 
     *         of that manager is called. When this happens, in addition to
     *         propagating this exception, this method also registers an 
     *         ERROR status attribute (along with a Comment attribute
     *         describing the nature of the problem) with all lookup services
     *         with which this service is registered. Administrative clients,
     *         as well as clients that use this service should register
     *         for notification of the existence of this attribute.
     * 
     * @throws java.lang.UnsupportedOperationException this exception 
     *         occurs when the registration corresponding to the
     *         <code>registrationID</code> parameter has no managed set of
     *         groups to which to add the elements of the input parameter.
     *         That is, the registration's current managed set of groups is
     *         null. When a registration's managed set of groups is null,
     *         it means that all groups are being discovered for that 
     *         registration; thus, requesting that a set of groups be added
     *         to the set of all groups makes no sense.
     *
     * @throws java.lang.NullPointerException this exception occurs when
     *         either null is input to the <code>groups</code> parameter, 
     *         or one or more of the elements of the <code>groups</code> 
     *         parameter is null. If a null <code>groups</code> parameter 
     *         is input, the registration is requesting that all groups be 
     *         added to its current managed set of groups; which is not 
     *         allowed. (Note that if a registration wishes to change its 
     *         managed set of groups from a finite set of names to "all 
     *         groups", it should invoke setGroups with a null input.)
     *
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         registration's managed set of groups may or may not have been
     *         successfully augmented.
     *
     * @throws java.rmi.NoSuchObjectException wrapped in an instance of
     *         com.sun.jini.proxy.ThrowThis exception whenever the
     *         <code>registrationID</code> parameter references an invalid
     *         or non-existent registration. Refer to the description of the
     *         <code>getRegistrars</code> method for more information on
     *         this exception.
     *
     * @see com.sun.jini.fiddler.FiddlerRegistration#addGroups
     * @see net.jini.discovery.LookupDiscoveryRegistration#addGroups
     */
    public void addGroups(Uuid     registrationID,
                          String[] groups)  throws RemoteException,ThrowThis
    {
        concurrentObj.writeLock();
        try {
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new ThrowThis
                        (new NoSuchObjectException("Invalid registration "
                                                   +"ID on call to "
                                                   +"addGroups() method"));
            }//endif
            /* Check the input for validity */
            if(groups == null) { // asking that all groups be added
                throw new NullPointerException(" on call to addGroups() "
                                               +"method, cannot add "
                                               +"'ALL_GROUPS' (the null set) "
                                               +"to a registration's set of "
                                               +"groups to discover");
            } else if(containsNullElement(groups)) { // null element
                throw new NullPointerException(" on call to addGroups() "
                                               +"method, at least one null "
                                               +"element in groups parameter");
            } else if (regInfo.groups == null) { // all groups being discovered
                throw new UnsupportedOperationException
                                              (" on call to addGroups() "
                                               +"method, cannot add a set of"
                                               +"groups to a set already "
                                               +"configured for 'ALL_GROUPS' "
                                               +"(the null set)");
            }//endif
            /* Augment the current set of groups with the input set */
            addGroupsDo(regInfo, groups);
            addLogRecord(new GroupsAddedToRegistrationLogObj
                                             (regInfo.registrationID,groups));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end addGroups

    /**
     * This method is the "backend" server counterpart to the method of
     * the same name provided by the <code>LookupDiscoveryRegistration</code> 
     * proxy (an instance of <code>FiddlerRegistration</code>) that is
     * returned by this service when a client requests a registration.
     * <p>
     * This method first tests the input set of group names for validity and 
     * throws the appropriate exception should any irregularities be found.
     * It then queues a <code>SetGroupsTask</code> which performs the 
     * actual replacement.
     * <p>
     * 
     * @param registrationID unique identifier assigned to the registration
     *                       to which the set of groups being replaced
     *                       corresponds
     * @param groups         a String array, none of whose elements may be 
     *                       null, consisting of the group names with which to 
     *                       replace the names in this registration's managed 
     *                       set of groups.
     * <p>
     *                       If any element of this parameter duplicates any 
     *                       other element of this parameter, the duplicate 
     *                       will be ignored.
     * <p>
     *                       If the empty set is input, then group discovery 
     *                       for the registration will cease. If null is input,
     *                       the lookup discovery service will attempt to 
     *                       discover all as yet undiscovered lookup services 
     *                       located within its multicast radius and, upon 
     *                       discovery of any such lookup service, will send 
     *                       to the registration's listener an event signaling
     *                       that discovery.
     * 
     * @throws java.lang.NullPointerException this exception occurs when one
     *         or more of the elements of the groups parameter is null.
     *
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         registration's managed set of groups may or may not have been
     *         successfully replaced.
     *
     * @throws java.rmi.NoSuchObjectException wrapped in an instance of
     *         com.sun.jini.proxy.ThrowThis exception whenever the
     *         <code>registrationID</code> parameter references an invalid
     *         or non-existent registration. Refer to the description of the
     *         <code>getRegistrars</code> method for more information on
     *         this exception.
     *
     * @see com.sun.jini.fiddler.FiddlerRegistration#setGroups
     * @see net.jini.discovery.LookupDiscoveryRegistration#setGroups
     */
    public void setGroups(Uuid     registrationID,
                          String[] groups)  throws RemoteException,ThrowThis
    {
        concurrentObj.writeLock();
        try {
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new ThrowThis
                        (new NoSuchObjectException("Invalid registration "
                                                   +"ID on call to "
                                                   +"setGroups() method"));
            }//endif
            /* Check the input for validity */
            if(containsNullElement(groups)) { // null element
                throw new NullPointerException(" on call to setGroups() "
                                               +"method, at least one null "
                                               +"element in groups parameter");
            } else if ((groups == null) && (regInfo.groups == null)) {
                /* null input, but already set to ALL groups; do nothing */
                return;
            }//endif
            /* Replace the current groups with the current groups */
            setGroupsDo(regInfo, groups);
            addLogRecord(new GroupsSetInRegistrationLogObj
                                             (regInfo.registrationID,groups));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end setGroups

    /**
     * This method is the "backend" server counterpart to the method of
     * the same name provided by the <code>LookupDiscoveryRegistration</code> 
     * proxy (an instance of <code>FiddlerRegistration</code>) that is
     * returned by this service when a client requests a registration.
     * <p>
     * This method first tests the input set of group names for validity and 
     * throws the appropriate exception should any irregularities be found.
     * It then queues a <code>RemoveGroupsTask</code> which performs the 
     * actual removal.
     * 
     * @param registrationID unique identifier assigned to the registration
     *                       to which the set of groups being removed
     *                       corresponds
     * @param groups         a String array, none of whose elements may be
     *                       null, consisting of the group names to delete 
     *                       from the registration's managed set of groups.
     * <p>
     *                       If any element of this parameter duplicates any 
     *                       other element of this parameter, the duplicate 
     *                       will be ignored. If any element of this parameter
     *                       is not currently contained in the registration's
     *                       managed set, no action is taken with respect to
     *                       that element.
     * <p>
     *                       If the empty set is input, the registration's 
     *                       managed set of groups will not change. If null is 
     *                       input, this method will throw a 
     *                       <code>NullPointerException</code>.
     * 
     * @throws java.lang.UnsupportedOperationException this exception 
     *         occurs when the registration corresponding to the
     *         <code>registrationID</code> parameter has no managed set 
     *         of groups from which to remove elements of the input parameter.
     *         That is, the registration's current managed set of groups is
     *         null. Thus, requesting that a set of groups be removed from
     *         null set makes no sense. 
     *
     * @throws java.lang.NullPointerException this exception occurs when
     *         either null is input to the <code>groups</code> parameter, 
     *         or one or more of the elements of the <code>groups</code> 
     *         parameter is null. If a null <code>groups</code> parameter 
     *         is input, the registration is requesting that all groups be 
     *         removed from its current managed set of groups; which is not 
     *         allowed. (Note that if a registration wishes to change its 
     *         managed set of groups from "all groups" to "no groups", it
     *         it should invoke setGroups with a zero length 
     *         <code>String</code> input.)
     *
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         registration's managed set of groups may or may not have been
     *         successfully modified.
     * 
     * @throws java.rmi.NoSuchObjectException wrapped in an instance of
     *         com.sun.jini.proxy.ThrowThis exception whenever the
     *         <code>registrationID</code> parameter references an invalid
     *         or non-existent registration. Refer to the description of the
     *         <code>getRegistrars</code> method for more information on
     *         this exception.
     *
     * @see com.sun.jini.fiddler.FiddlerRegistration#removeGroups
     * @see net.jini.discovery.LookupDiscoveryRegistration#removeGroups
     */
    public void removeGroups(Uuid     registrationID,
                             String[] groups) throws RemoteException,ThrowThis
    {
        concurrentObj.writeLock();
        try {
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new ThrowThis
                        (new NoSuchObjectException("Invalid registration "
                                                   +"ID on call to "
                                                   +"removeGroups() method"));
            }//endif
            /* Check the input for validity */
            if(groups == null) { // asking that all groups be removed
                throw new NullPointerException(" on call to removeGroups() "
                                               +"method, cannot remove "
                                               +"'ALL_GROUPS' (the null set) "
                                               +"from a registration's set of "
                                               +"groups to discover");
            } else if(containsNullElement(groups)) { // null element
                throw new NullPointerException(" on call to removeGroups() "
                                               +"method, at least one null "
                                               +"element in groups parameter");
            } else if (regInfo.groups == null) { // all groups being discovered
                throw new UnsupportedOperationException
                                            (" on call to removeGroups() "
                                             +"method, cannot remove a set of"
                                             +"groups from a set already "
                                             +"configured for 'ALL_GROUPS' "
                                             +"(the null set)");
            }//endif
            /* Remove the requested groups */
            removeGroupsDo(regInfo, groups);
            logInfoGroups("\nAfter Group Removal --");
            addLogRecord(new GroupsRemovedFromRegistrationLogObj
                                             (regInfo.registrationID,groups));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end removeGroups

    /**
     * This method is the "backend" server counterpart to the method of
     * the same name provided by the <code>LookupDiscoveryRegistration</code> 
     * proxy (an instance of <code>FiddlerRegistration</code>) that is
     * returned by this service when a client requests a registration.
     * <p>
     * This method first tests the input set of group names for validity and 
     * throws the appropriate exception should any irregularities be found.
     * It then adds the input set of LookupLocator objects to the managed set
     * of locators associated with the registration.
     * 
     * @param registrationID unique identifier assigned to the registration
     *                       to which the set of locators being augmented
     *                       corresponds
     * @param locators       an array, none of whose elements may be null, 
     *                       consisting of the LookupLocator objects with 
     *                       which to augment the registration's managed set 
     *                       of locators.
     * <p>
     *                       If any element of this parameter duplicates any 
     *                       other element of this parameter, the duplicate 
     *                       will be ignored. If any element of this parameter 
     *                       duplicates any element of the registration's 
     *                       managed set of locators, the duplicate will be 
     *                       ignored.
     * <p>
     *                       If the empty set is input, then the registration's
     *                       managed set of locators will not change. If null
     *                       is input, this method will throw a
     *                       <code>NullPointerException</code>.
     * 
     * @throws java.lang.IllegalStateException this exception occurs when
     *         the <code>addLocators</code> method of the discovery
     *         manager is invoked after the <code>terminate</code> method 
     *         of that manager is called. When this happens, in addition to
     *         propagating this exception, this method also registers an 
     *         ERROR status attribute (along with a Comment attribute
     *         describing the nature of the problem) with all lookup services
     *         with which this service is registered. Administrative clients,
     *         as well as clients that use this service should register
     *         for notification of the existence of this attribute.
     * 
     * @throws java.lang.NullPointerException this exception occurs when
     *         either null is input to the <code>locators</code> parameter, 
     *         or one or more of the elements of the <code>locators</code> 
     *         parameter is null.
     *
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         registration's managed set of locators may or may not have
     *         been successfully augmented.
     * 
     * @throws java.rmi.NoSuchObjectException wrapped in an instance of
     *         com.sun.jini.proxy.ThrowThis exception whenever the
     *         <code>registrationID</code> parameter references an invalid
     *         or non-existent registration. Refer to the description of the
     *         <code>getRegistrars</code> method for more information on
     *         this exception.
     *
     * @see com.sun.jini.fiddler.FiddlerRegistration#addLocators
     * @see net.jini.discovery.LookupDiscoveryRegistration#addLocators
     */
    public void addLocators(Uuid            registrationID,
                            LookupLocator[] locators)  throws RemoteException,
                                                              ThrowThis
    {
        /* Prepare outside of sync block because of possible remote call */
        prepareNewLocators(locatorToDiscoverPreparer,locators);
        concurrentObj.writeLock();
        try {
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new ThrowThis
                        (new NoSuchObjectException("Invalid registration "
                                                   +"ID on call to "
                                                   +"addLocators() method"));
            }//endif
            /* Check the input for validity */
            if(locators == null) {
                throw new NullPointerException(" on call to addLocators() "
                                               +"method, cannot add null "
                                               +"to a registration's set of "
                                               +"locators to discover");
            } else if(containsNullElement(locators)) { // null element
                throw new NullPointerException(" on call to addLocators() "
                                             +"method, at least one null "
                                             +"element in locators parameter");
            }//endif(locators == null)
            /* Augment the current set of locators with the input set */
            addLocatorsDo(regInfo, locators);
            addLogRecord(new LocsAddedToRegistrationLogObj
                                           (regInfo.registrationID,locators));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end addLocators

    /**
     * This method is the "backend" server counterpart to the method of
     * the same name provided by the <code>LookupDiscoveryRegistration</code> 
     * proxy (an instance of <code>FiddlerRegistration</code>) that is
     * returned by this service when a client requests a registration.
     * <p>
     * This method first tests the input set of locators for validity and 
     * throws the appropriate exception should any irregularities be found.
     * It then queues a <code>SetLocatorsTask</code> which performs the 
     * actual replacement.
     * 
     * @param registrationID unique identifier assigned to the registration
     *                       to which the set of locators being replaced
     *                       corresponds
     * @param locators       an array, none of whose elements may be null,
     *                       consisting of the LookupLocator objects with 
     *                       which to replace the locators in the 
     *                       registration's managed set of locators.
     * <p>
     *                       If any element of this parameter duplicates any 
     *                       other element of this parameter, the duplicate 
     *                       will be ignored.
     * <p>
     *                       If the empty array is input, then locator 
     *                       discovery for the registration will cease. If 
     *                       null is input, this method will throw a 
     *                       <code>NullPointerException</code>.
     * 
     * @throws java.lang.NullPointerException this exception occurs when
     *         either null is input to the <code>locators</code> parameter, 
     *         or one or more of the elements of the <code>locators</code> 
     *         parameter is null.
     *
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         registration's managed set of locators may or may not have
     *         been successfully replaced.
     * 
     * @throws java.rmi.NoSuchObjectException wrapped in an instance of
     *         com.sun.jini.proxy.ThrowThis exception whenever the
     *         <code>registrationID</code> parameter references an invalid
     *         or non-existent registration. Refer to the description of the
     *         <code>getRegistrars</code> method for more information on
     *         this exception.
     *
     * @see com.sun.jini.fiddler.FiddlerRegistration#setLocators
     * @see net.jini.discovery.LookupDiscoveryRegistration#setLocators
     */
    public void setLocators(Uuid            registrationID,
                            LookupLocator[] locators)  throws RemoteException,
                                                              ThrowThis
    {
        /* Prepare outside of sync block because of possible remote call */
        prepareNewLocators(locatorToDiscoverPreparer,locators);
        concurrentObj.writeLock();
        try {
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new ThrowThis
                        (new NoSuchObjectException("Invalid registration "
                                                   +"ID on call to "
                                                   +"setLocators() method"));
            }//endif
            /* Check the input for validity */
            if(locators == null) {
                throw new NullPointerException(" on call to setLocators() "
                                               +"method, cannot replace a "
                                               +"registration's current set "
                                               +"of locators with null");
            } else if(containsNullElement(locators)) { // null element
                throw new NullPointerException(" on call to setLocators() "
                                             +"method, at least one null "
                                             +"element in locators parameter");
            }//endif(locators == null)
            setLocatorsDo(regInfo, locators);
            addLogRecord(new LocsSetInRegistrationLogObj
                                           (regInfo.registrationID,locators));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end setLocators

    /**
     * This method is the "backend" server counterpart to the method of
     * the same name provided by the <code>LookupDiscoveryRegistration</code> 
     * proxy (an instance of <code>FiddlerRegistration</code>) that is
     * returned by this service when a client requests a registration.
     * <p>
     * This method first tests the input set of locators for validity and 
     * throws the appropriate exception should any irregularities be found.
     * It then queues a <code>RemoveLocatorsTask</code> which performs the 
     * actual removal.
     * 
     * @param registrationID unique identifier assigned to the registration
     *                       to which the set of locators being removed
     *                       corresponds
     * @param locators       an array, none of whose elements may be null,
     *                       consisting of the LookupLocator objects to remove
     *                       from the registration's managed set of locators.
     * <p>
     *                       If any element of this parameter duplicates any
     *                       other element of this parameter, the duplicate
     *                       will be ignored.
     * <p>
     *                       If the empty set is input, the managed set of
     *                       locators will not change. If null is input,
     *                       this method will throw a 
     *                       <code>NullPointerException</code>.
     * 
     * @throws java.lang.NullPointerException this exception occurs when
     *         either null is input to the <code>locators</code> parameter, 
     *         or one or more of the elements of the <code>locators</code> 
     *         parameter is null.
     *
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, the
     *         registration's managed set of locators may or may not have
     *         been successfully modified.
     * 
     * @throws java.rmi.NoSuchObjectException wrapped in an instance of
     *         com.sun.jini.proxy.ThrowThis exception whenever the
     *         <code>registrationID</code> parameter references an invalid
     *         or non-existent registration. Refer to the description of the
     *         <code>getRegistrars</code> method for more information on
     *         this exception.
     *
     * @see com.sun.jini.fiddler.FiddlerRegistration#removeLocators
     * @see net.jini.discovery.LookupDiscoveryRegistration#removeLocators
     */
    public void removeLocators(Uuid            registrationID,
                               LookupLocator[] locators)
                                               throws RemoteException,ThrowThis
    {
        /* Prepare outside of sync block because of possible remote call */
        prepareNewLocators(locatorToDiscoverPreparer,locators);
        concurrentObj.writeLock();
        try {
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new ThrowThis
                       (new NoSuchObjectException("Invalid registration "
                                                  +"ID on call to "
                                                  +"removeLocators() method"));
            }//endif
            /* Check the input for validity */
            if(locators == null) {
                throw new NullPointerException(" on call to removeLocators() "
                                               +"method, cannot remove null "
                                               +"from a registration's set of "
                                               +"locators to discover");
            } else if(containsNullElement(locators)) { // null element
                throw new NullPointerException(" on call to removeLocators() "
                                             +"method, at least one null "
                                             +"element in locators parameter");
            }//endif(locators == null)
            /* Remove the requested set of locators from the current set */
            removeLocatorsDo(regInfo, locators);
            addLogRecord(new LocsRemovedFromRegistrationLogObj
                                           (regInfo.registrationID,locators));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end removeLocators

    /**
     * This method is the "backend" server counterpart to the method of
     * the same name provided by the <code>LookupDiscoveryRegistration</code> 
     * proxy (an instance of <code>FiddlerRegistration</code>) that is
     * returned by this service when a client requests a registration.
     * <p>
     * This method informs the lookup discovery service of the existence of
     * an unavailable lookup service and requests that the lookup discovery
     * service discard the unavailable lookup service and make it eligible
     * to be re-discovered.
     * 
     * @param registrationID unique identifier assigned to the registration
     *                       making the current discard request
     * @param registrar      a reference to the lookup service that the lookup
     *                       discovery service is being asked to discard.
     * <p>
     *                       If this parameter equals none of the lookup
     *                       services contained in the managed set of lookup
     *                       services for this registration, no action will
     *                       be taken.
     * 
     * @throws java.lang.NullPointerException this exception occurs when
     *         null is input to the registrar parameter.
     *
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         lookup discovery service. When this exception does occur, 
     *         the lookup service may or may not have been successfully
     *         discarded.
     * 
     * @throws java.rmi.NoSuchObjectException wrapped in an instance of
     *         com.sun.jini.proxy.ThrowThis exception whenever the
     *         <code>registrationID</code> parameter references an invalid
     *         or non-existent registration. Refer to the description of the
     *         <code>getRegistrars</code> method for more information on
     *         this exception.
     *
     * @see com.sun.jini.fiddler.FiddlerRegistration#discard
     * @see net.jini.discovery.LookupDiscoveryRegistration#discard
     */
    public void discard(Uuid registrationID,
                        ServiceRegistrar registrar)
                                              throws RemoteException,ThrowThis
    {
        concurrentObj.writeLock();
        try {
            logInfoDiscard("\ndiscard: ",registrationID);
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new ThrowThis
                            (new NoSuchObjectException("Invalid registration "
                                                       +"ID on call to "
                                                       +"discard() method"));
            }//endif
            if(registrar == null) {
                throw new NullPointerException(" on call to discard() "
                                               +"method, null input for "
                                               +"registrar to discard");
            }//endif
            if( regIsElementOfRegSet(registrar,discoveryMgr.getRegistrars()) ){
                /* This must be the first discard request for this registrar 
                 * because the discovery manager has not discarded it yet.
                 * When the discovery manager discards the registrar, a
                 * local discarded event is sent to the listener.discarded
                 * method which queues a DiscardedEventTask which will 
                 * remove the discarded registrar from the registration's
                 * set of discovered registrars and then send a remote
                 * discarded event to the registration's listener.
                 */
                logInfoDiscard("  Registrar IS an element of Mgr's "
                               +"discovered registrars ... discarding "
                               +"from discovery manager");
                regInfo.discardFlag = true; //discard due to external request
                discoveryMgr.discard(registrar);
            } else {
                logInfoDiscard("  Registrar NOT an element of Mgr's "
                               +"discovered registrars ... queuing "
                               +"new DiscardRegistrarTask");
                /* For all subsequent discard requests, remove the registrar
                 * from the registration's set of discovered registrars and
                 * send a remote discarded event, but don't ask the discovery
                 * manager to discard the registrar.
                 */
                taskMgr.add(new DiscardRegistrarTask(regInfo,registrar));
            }//endif
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end discard

    /**
     * This method is the "backend" server counterpart to the 
     * <code>renew</code> method specified by the <code>Lease</code> interface,
     * implemented in the <code>com.sun.jini.lease.AbstractLease</code> class,
     * and invoked by way of the <code>doRenew</code> method of the
     * <code>FiddlerLease</code> class; an instance of which is
     * returned by the <code>getLease</code> method of the
     * <code>LookupDiscoveryRegistration</code> proxy (an instance of
     * <code>FiddlerRegistration</code>) that is returned by this service
     * when a client requests a registration.
     * <p>
     * This method renews the lease corresponding to the given 
     * <code>registrationID</code> and <code>leaseID</code> parameters,
     * granting a new duration that is less than or equal to the requested
     * duration value contained in the <code>duration</code> parameter.
     *
     * @param registrationID unique identifier assigned to the registration
     *                       to which the lease being renewed corresponds
     * @param leaseID        identifier assigned by the lease grantor to the
     *                       lease being renewed
     * @param duration       the requested duration for the lease being renewed
     *
     * @return <code>long</code> value representing the actual duration that
     *         was granted for the renewed lease. Note that the actual
     *         duration granted and returned by this method may be less than
     *         the duration requested.
     *
     * @throws net.jini.core.lease.UnknownLeaseException this exception occurs
     *         when the lease being renewed does not exist, or is unknown
     *         to the lease grantor; typically because the lease has expired.
     *         
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         server. When this exception does occur, the lease may or may
     *         not have been renewed successfully.
     *
     * @see net.jini.core.lease.Lease#renew
     * @see com.sun.jini.lease.AbstractLease#renew
     * @see com.sun.jini.lease.AbstractLease#doRenew
     * @see com.sun.jini.fiddler.FiddlerLease#doRenew
     */
    public long renewLease(Uuid registrationID,
                           Uuid leaseID,
                           long duration)
                                throws UnknownLeaseException, RemoteException
    {
        concurrentObj.priorityWriteLock();
        try {
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new UnknownLeaseException
                                 ("\n    Invalid registration ID on call to "
                                  +"cancelLease() method"
                                  +"\n    The lease may have expired or been "
                                  +"cancelled");
            }//endif
            /* Renew the lease */
            long newDuration = 0;
            newDuration = renewLeaseDo(regInfo, leaseID, duration);
            logInfoLease("Renewed lease: ",registrationID,leaseID);
            /* The call to addLogRecord is in renewLeaseDo */
            return newDuration;
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end renewLease

    /**
     * This methods renews all leases from a <code>LeaseMap</code>, 
     * where each element of the map is a lease on a registration with
     * ID corresponding to an element of the <code>registrationIDs</code> 
     * parameter.
     * <p>
     * This method is the "backend" server counterpart to the 
     * <code>renewAll</code> method specified by the
     * <code>LeaseMap</code> interface, implemented in the 
     * <code>com.sun.jini.lease.AbstractLeaseMap</code> class, and 
     * invoked by way of the <code>renewAll</code> method of the
     * <code>FiddlerLease</code> class; an instance of which is
     * returned by the <code>getLease</code> method of the
     * <code>LookupDiscoveryRegistration</code> proxy (an instance of
     * <code>FiddlerRegistration</code>) that is returned by this service
     * when a client requests a registration.
     *
     * @param registrationIDs array containing the unique identifiers assigned
     *                        to the each registration to which each lease 
     *                        to be renewed corresponds
     * @param leaseIDs        array containing the identifiers assigned by the
     *                        lease grantor to each lease being renewed
     * @param durations       array containing the requested durations for 
     *                        each lease being renewed
     * 
     * @return an instance of FiddlerRenewResults containing data corresponding
     *         to the results (granted durations or exceptions) of each
     *         renewal attempt
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         server. When this exception does occur, this method may or
     *         may not have complete its processing successfully.
     *
     * @see net.jini.core.lease.LeaseMap#renewAll
     */
    public FiddlerRenewResults renewLeases(Uuid[] registrationIDs,
                                           Uuid[] leaseIDs,
                                           long[] durations)
                                                        throws RemoteException
    {
        concurrentObj.priorityWriteLock();
        try {
            return renewLeasesDo(registrationIDs, leaseIDs, durations);
            /* The call to addLogRecord is in renewLeasesDo */
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end renewLeases

    /**
     * This method is the "backend" server counterpart to the 
     * <code>cancel</code> method specified by the <code>Lease</code>
     * interface and implemented in the <code>FiddlerLease</code> class; an
     * instance of which is returned by the <code>getLease</code> method
     * of the <code>LookupDiscoveryRegistration</code> proxy (an instance of
     * <code>FiddlerRegistration</code>) that is returned by this service
     * when a client requests a registration.
     * <p>
     * This method cancels the lease corresponding to the given 
     * <code>registrationID</code> and <code>leaseID</code> parameters.
     *
     * The cancellation of a lease typically involves the modification of the
     * managed sets in the discovery manager, which usually involves starting
     * the discovery protocol.  An IOException can occur when the discovery
     * protocol fails to start. When such an exception does occur, this 
     * method registers an ERROR status attribute (along with a Comment
     * attribute describing the nature of the problem) to all lookup services
     * with which this service is registered. 
     *
     * Administrative clients, as well as clients that use this service should
     * have registered for notification of the existence of this attribute.
     *
     * @param registrationID unique identifier assigned to the registration
     *                       to which the lease being cancelled corresponds
     * @param leaseID        identifier assigned by the lease grantor to the
     *                       lease that is to be cancelled
     *
     * @throws net.jini.core.lease.UnknownLeaseException this exception occurs
     *         when the lease being cancelled is unknown to the lease grantor.
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         server. When this exception does occur, the lease may or may
     *         not have been cancelled successfully.
     *
     * @see net.jini.core.lease.Lease#cancel
     */
    public void cancelLease(Uuid registrationID,
                            Uuid leaseID)
                             throws UnknownLeaseException, RemoteException
    {
        concurrentObj.writeLock();
        try {
            RegistrationInfo regInfo
                   = (RegistrationInfo)(registrationByID.get(registrationID));
            if(regInfo == null) {
                throw new UnknownLeaseException
                                 ("\n    Invalid registration ID on call to "
                                  +"cancelLease() method"
                                  +"\n    The lease may have expired or been "
                                  +"cancelled");
            }//endif
            /* Cancel the lease */
            try {
                cancelLeaseDo(regInfo, leaseID);
                logInfoLease("Cancelled lease: ",registrationID,leaseID);
	    } catch(IOException e) {
                String eStr = "Failure while cancelling the lease on "
                              +"registration with ID = "+registrationID;
                if( problemLogger.isLoggable(Level.INFO) ) {
                    problemLogger.log(Level.INFO, eStr, e);
                }//endif
                Entry[] errorAttrs = 
                           new Entry[] { new FiddlerStatus(StatusType.ERROR),
                                         new Comment(eStr)
                                       };
                joinMgr.addAttributes(errorAttrs,true);
	    }
            addLogRecord(new LeaseCancelledLogObj
                                          (regInfo.registrationID, leaseID));
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end cancelLease

    /**
     * Cancels all leases from a <code>LeaseMap</code>.
     * <p>
     * For each element in the <code>registrationIDs</code> parameter,
     * this method will cancel the corresponding element in the
     * <code>leaseIDs</code> parameter.
     *
     * @param registrationIDs array containing the unique identifiers assigned
     *                        to the each registration to which each lease 
     *                        to be cancelled corresponds
     * @param leaseIDs        array containing the identifiers assigned by the
     *                        lease grantor to each lease being cancelled
     * 
     * @throws java.rmi.RemoteException typically, this exception occurs when
     *         there is a communication failure between the client and the
     *         server. When this exception does occur, this method may or
     *         may not have complete its processing successfully.
     * 
     * @return array consisting of any exceptions that may have occurred 
     *         while attempting to cancel one of the leases in the map.
     *
     * @see net.jini.core.lease.LeaseMap#cancelAll
     */
    public Exception[] cancelLeases(Uuid[] registrationIDs,
                                    Uuid[] leaseIDs)  throws RemoteException
    {
        concurrentObj.writeLock();
        try {
            /* don't bother to weed out unknown leases, so log first */
            addLogRecord( new LeasesCancelledLogObj(registrationIDs,leaseIDs));
            return cancelLeasesDo(registrationIDs, leaseIDs);
        } finally {
            concurrentObj.writeUnlock();
        }
    }//end cancelLeases
    /* END com.sun.jini.fiddler.Fiddler ----------------------------------- */
    /* ************************* END Public Methods *********************** */

    /* **************** BEGIN Private Static Utility Methods ************** */
    /** Return a new array containing the elements of the input array parameter
     *  with the input element parameter appended to the end of the array.
     */
    private static Object[] appendArray(Object[] array, Object elt) {
	int len = array.length;
	Object[] newArray =
	    (Object[])Array.newInstance(array.getClass().getComponentType(),
					len + 1);
	System.arraycopy(array, 0, newArray, 0, len);
	newArray[len] = elt;
	return newArray;
    }//end appendArray

    /** Bounds the duration by the value of the <code>bound</code> parameter,
     *  and checks for negative value.
     */
    private static long applyBoundToLeaseDuration(long leaseDuration,
                                                  long bound)
    {
        long newLeaseDuration = leaseDuration;
        if ( (leaseDuration == Lease.ANY) || (leaseDuration > bound) ) {
            newLeaseDuration = bound;
        } else if (leaseDuration < 0) {
            throw new IllegalArgumentException("negative lease duration");
        }//endif
        return newLeaseDuration;
    }//end applyBoundToLeaseDuration

    /** Determines if any element in the input array is null.
     *  @param arr Object array to examine for null elements
     *  @return true if any element is found to be null, false otherwise.
     *          Note that this means that if the array itself is null
     *          or has a non-positive length, false is returned (because
     *          the input parameter still does not contain a null element).
     */
    private static boolean containsNullElement(Object[] arr) {
        if( (arr == null) || (arr.length == 0) ) return false;
        for(int i=0;i<arr.length;i++) {
            if(arr[i] == null) return true;
        }//end loop
        return false;
    }//end containsNullElement

    /** This method determines if a particular registration (regInfo) is
     *  interested in discovering, through group discovery, the registrar
     *  belonging to a given set of member groups.
     *
     *  @param regGroups     array of the member groups from the registrar
     *                       (cannot be null)
     *  @param desiredGroups groups the registration wishes to discover
     *                       (can be null = ALL_GROUPS)
     * 
     *  @return <code>true</code> if at least one of the registrar's member
     *          groups is contained in the registration's set of groups to
     *          discover; <code>false</code> otherwise
     */
    private static boolean interested(String[] regGroups, Set desiredGroups) {
        if(desiredGroups == null) return true;
        if(desiredGroups.size() == 0) return false;
	for(int i=0;i<regGroups.length;i++) {
            if( desiredGroups.contains(regGroups[i]) ) return true;
        }//end loop
	return false;
    }//end interested

    /** This method determines if a particular registration (regInfo) is
     *  interested in discovering, through either locator discovery
     *  or group discovery, the registrar having a given locator and 
     *  belonging to a given set of member groups.
     *
     * @param regLoc          locator of the registrar (cannot be null)
     * @param regGroups       array of the member groups from the registrar
     *                        (cannot be null)
     * @param desiredLocators locators the registration wishes to discover
     * @param desiredGroups   groups the registration wishes to discover
     *                        (can be null = ALL_GROUPS)
     * 
     *  @return <code>true</code> if either the registrar's locator is
     *          contained in the registration's set of locators to discover,
     *          or at least one of the registrar's member groups is contained
     *          in the registration's set of groups to discover;
     *          <code>false</code> otherwise
     */
    private static boolean interested(LookupLocator regLoc,
                                      String[] regGroups,
                                      Set desiredLocators,
                                      Set desiredGroups)
    {
        if(desiredLocators.contains(regLoc)) return true; // discover by loc
        return interested(regGroups,desiredGroups);
    }//end interested

    /** This method returns a mapping in which the key values are registrars,
     *  and the map values are the member groups of the corresponding
     *  registrar key. The registrar and member groups from the input map
     *  are selected to be included in the returned mapping if and only if
     *  the key value under consideration is a registrar that belongs to none
     *  of the desired groups of the given registration (<code>regInfo</code>).
     *  That is, the registrars referenced in the returned mapping are the
     *  registrars that are no longer of interest - through group discovery
     *  - to the given registration.
     *
     * @param regsMap map whose key values are registrars, and whose map
     *                values are data structures of type
     *                <code>LocatorGroupsStruct</code> that contain the
     *                associated locator and member groups of the
     *                corresponding registrar key; the elements of the
     *                return map are selected from this mapping
     * @param regInfo the data structure record corresponding to the 
     *                registration whose groups-to-discover will be used
     *                to select the elements from <code>regMap</code> to
     *                include in the return mapping
     * 
     *  @return a registrar-to-groups map in which each registrar in the map
     *          is from the <code>regMap</code> parameter, and belongs to none
     *          of the desired groups referenced in the <code>regInfo</code>
     *          parameter
     */
    private static HashMap getUndesiredRegsByGroup(Map regMap, 
                                                   RegistrationInfo regInfo)
    {
        HashSet desiredGroups = regInfo.groups;
        HashMap undesiredRegMap = new HashMap(regMap.size());
        Set eSet = regMap.entrySet();
        for(Iterator itr = eSet.iterator(); itr.hasNext(); ) {
            Map.Entry pair = (Map.Entry)itr.next();
            String[] regGroups = ((LocatorGroupsStruct)pair.getValue()).groups;
            if( !interested(regGroups,desiredGroups) ) {
                undesiredRegMap.put((ServiceRegistrar)pair.getKey(),regGroups);
            }//endif
        }//end loop
        return undesiredRegMap;
    }//end getUndesiredRegsByGroup

    /** This method returns a subset of the given registrar-to-locators
     *  mapping (<code>regMap</code>). An element of the given mapping is
     *  selected to be included in the returned mapping if and only if
     *  the key value of the element is a registrar whose locator equals
     *  none of the desired locators of the given registration
     *  (<code>regInfo</code>). That is, the registrars referenced in
     *  the returned mapping are the registrars that are no longer of
     *  interest - through locator discovery - to the given registration.

     *  This method returns a mapping in which the key values are registrars,
     *  and the map values are the locators of the corresponding registrar
     *  key. The registrar and locators from the input map are selected to
     *  be included in the returned mapping if and only if the key value
     *  under consideration is a registrar whose locator equals none of the
     *  desired locators of the given registration (<code>regInfo</code>).
     *  That is, the registrars referenced in the returned mapping are the
     *  registrars that are no longer of interest - through locator discovery
     *  - to the given registration.
     *
     * @param regsMap map whose key values are registrars, and whose map
     *                values are data structures of type
     *                <code>LocatorGroupsStruct</code> that contain the
     *                associated locator and member groups of the
     *                corresponding registrar key; the elements of the
     *                return map are selected from this mapping
     * @param regInfo the data structure record corresponding to the 
     *                registration whose locators-to-discover will be used
     *                to select the elements from <code>regMap</code> to
     *                include in the return mapping
     * 
     *  @return a registrars-to-locators map in which each registrar key in
     *          the map is from the <code>regMap</code> parameter, and has
     *          a locator equal to none of the desired locators referenced
     *          in the <code>regInfo</code> parameter
     */
    private static Map getUndesiredRegsByLocator(Map regMap, 
                                                 RegistrationInfo regInfo)
    {
        HashSet desiredLocators = regInfo.locators;
        HashMap undesiredRegMap = new HashMap(regMap.size());
        Set eSet = regMap.entrySet();
        for(Iterator itr = eSet.iterator(); itr.hasNext(); ) {
            Map.Entry pair = (Map.Entry)itr.next();
            LookupLocator regLocator
                             = ((LocatorGroupsStruct)pair.getValue()).locator;
            if( !desiredLocators.contains(regLocator) ) {
                undesiredRegMap.put((ServiceRegistrar)pair.getKey(),
                                    regLocator);
            }//endif
        }//end loop
        return undesiredRegMap;
    }//end getUndesiredRegsByLocator

    /**
     * Marshals each element of the <code>Entry[]</code> array parameter.
     * This method is <code>static</code> so that it may called from
     * the <code>static</code> <code>LogRecord</code> classes when a set
     * of attributes is being logged to persistent storage.
     *
     * @param fiddlerImpl reference to the current instance of this service
     * @param attrs       <code>Entry[]</code> array consisting of the
     *                    attributes to marshal
     * @return array of <code>MarshalledObject[]</code>, where each element
     *         corresponds to an attribute in marshalled form 
     */
    private static MarshalledObject[] marshalAttributes
                                                   (FiddlerImpl fiddlerImpl,
                                                    Entry[] attrs)
    {
        if(attrs == null) return new MarshalledObject[0];
        ArrayList marshalledAttrs = new ArrayList();
        for(int i=0;i<attrs.length;i++) {
            /* Do not let an attribute problem prevent the service from
             * continuing to operate
             */
            try {
                marshalledAttrs.add(new MarshalledObject(attrs[i]));
            } catch(Throwable e) {
                if( problemLogger.isLoggable(Level.INFO) ) {
                    problemLogger.log(Level.INFO,
                                      "Error while marshalling attribute["+i
                                      +"] ("+attrs[i]+")", e);
                }//endif
            }
        }//end loop
        return ((MarshalledObject[])(marshalledAttrs.toArray
                             (new MarshalledObject[marshalledAttrs.size()])));
    }//end marshalAttributes

    /**
     * Unmarshals each element of the <code>MarshalledObject[]</code> array
     * parameter. This method is <code>static</code> so that it may called
     * from the <code>static</code> <code>LogRecord</code> classes when a
     * set of attributes is being recovered from persistent storage.
     *
     * @param fiddlerImpl     reference to the current instance of this service
     * @param marshalledAttrs <code>MarshalledObject[]</code> array consisting
     *                        of the attributes to unmarshal
     * @return array of <code>Entry[]</code>, where each element corresponds
     *         to an attribute that was successfully unmarshalled
     */
    private static Entry[] unmarshalAttributes
                                          (FiddlerImpl fiddlerImpl,
                                           MarshalledObject[] marshalledAttrs)
    {
        if(marshalledAttrs == null) return new Entry[0];
        ArrayList attrs = new ArrayList();
        for(int i=0;i<marshalledAttrs.length;i++) {
            /* Do not let an attribute problem prevent the service from
             * continuing to operate
             */
            try {
                attrs.add( (Entry)( marshalledAttrs[i].get() ) );
            } catch(Throwable e) {
                if( problemLogger.isLoggable(Level.INFO) ) {
                    problemLogger.log(Level.INFO,
                                      "Error while unmarshalling attribute["+i
                                      +"]", e);
                }//endif
            }
        }//end loop
        return ((Entry[])(attrs.toArray(new Entry[attrs.size()])));
    }//end unmarshalAttributes

    /** Using the given <code>ProxyPreparer</code>, attempts to prepare each
     *  element of the given <code>LookupLocator</code> array; replacing the
     *  original element of the array with the result of the call to the
     *  method <code>ProxyPreparer.prepareProxy</code>. If any attempt to
     *  prepare an element of the given array fails due to an exception,
     *  this method will propagate that exception.
     *
     *  This method is a convenience method that is typically used to
     *  prepare new locators the service should discover and join that
     *  are inserted into, or removed from, the service's state through
     *  the use of one of the following methods:
     *  <ul><li> <code>addLookupLocators</code>
     *      <li> <code>setLookupLocators</code>
     *      <li> <code>removeLookupLocators</code>
     *      <li> <code>addLocators</code>
     *      <li> <code>setLocators</code>
     *      <li> <code>removeLocators</code>
     *  </ul>
     * 
     * @param preparer the preparer to use to prepare each element of the
     *                 input array
     * @param locators array of <code>LookupLocator</code> instances in which
     *                 each element will be prepared.
     * 
     * @throws RemoteException   when preparation of any of the elements
     *                           of the input array fails because of a
     *                           <code>RemoteException</code>
     * @throws SecurityException when preparation of any of the elements
     *                           of the input array fails because of a
     *                           <code>SecurityException</code>
     */
    private static void prepareNewLocators(ProxyPreparer preparer,
                                           LookupLocator[] locators) 
                                                       throws RemoteException 
    {
        for (int i=0; i<locators.length; i++) {
            locators[i] = (LookupLocator)preparer.prepareProxy(locators[i]);
        }//end loop
    }//end prepareNewLocators

    /** Using the given <code>ProxyPreparer</code>, attempts to prepare each
     *  element of the given <code>LookupLocator</code> array; and returns
     *  a new array containing the prepared locators. If any attempt to
     *  prepare an element of the given array fails due to an exception,
     *  this method will skip to the next locator in that input array.
     *
     *  This method is a convenience method that is typically used to
     *  re-prepare the previously prepared locators that are retrieved
     *  from the service's persisted state during recovery.
     * 
     * @param preparer the preparer to use to prepare each element of the
     *                 input array
     * @param locators array of <code>LookupLocator</code> instances in which
     *                 each element will be prepared.
     * 
     * @return array of <code>LookupLocator</code> instances in which each
     *         element of the returned array is the result of successful proxy
     *         preparation of the corresponding element of the input array
     */
    private static LookupLocator[] prepareOldLocators(ProxyPreparer preparer,
                                                      LookupLocator[] locators)
    {
        ArrayList locsList = new ArrayList(locators.length);
        for(int i=0; i<locators.length; i++) {
            try {
                locsList.add(preparer.prepareProxy(locators[i]) );
            } catch(Throwable e) {
                if( problemLogger.isLoggable(Level.INFO) ) {
                    problemLogger.log(Level.INFO,"failure preparing recovered "
                                                 +"lookup locator["+i+"]", e);
                }//endif
            }
        }//end loop
        if(locators.length != locsList.size()) {
            if( problemLogger.isLoggable(Levels.HANDLED) ) {
                problemLogger.log(Levels.HANDLED,
                                  "number of requested recovered "
                                  +"lookup locators = "+locators.length);
                problemLogger.log(Levels.HANDLED, "number of successfully "
                                  +"prepared recovered lookup locators = "
                                  +locsList.size());
                for(int i=0; i<locsList.size(); i++) {
                    problemLogger.log(Levels.HANDLED, "successfully prepared "
                                      +"recovered lookup locator = "
                                      +locsList.get(i));
                }//end loop
            }//endif
        }//endif
        return ( (LookupLocator[])locsList.toArray
                                       (new LookupLocator[locsList.size()]) );
    }//end prepareOldLocators

    /** Using the given <code>ProxyPreparer</code>, attempts to prepare each
     *  element of the given <code>Set</code> of <code>LookupLocator</code>
     *  instances; and returns a new <code>Set</code> containing the prepared
     *  locators. If any attempt to prepare an element of the given 
     *  <code>Set</code> fails due to an exception, this method will skip
     *  to the next locator in that input <code>Set</code>.
     *
     *  This method is a convenience method that is typically used to
     *  re-prepare the previously prepared locators that are retrieved
     *  from the service's persisted state during recovery.
     * 
     * @param preparer the preparer to use to prepare each element of the
     *                 input array
     * @param locators <code>Set</code> of <code>LookupLocator</code>
     *                 instances in which each element will be prepared.
     * 
     * @return <code>Set</code> of <code>LookupLocator</code> instances in
     *         which each element of the returned <code>Set</code> is the
     *         result of successful proxy preparation of the corresponding
     *         element of the input <code>Set</code>
     */
    private static Set prepareOldLocators(ProxyPreparer preparer,
                                          Set locators)
    {
        Set locSet = new HashSet(locators.size());
        LookupLocator[] locsArray =
             prepareOldLocators( preparer,
                                (LookupLocator[])locators.toArray
                                        (new LookupLocator[locators.size()]) );
        for(int i=0; i<locsArray.length; i++) {
            locSet.add(locsArray[i]);
        }//end loop
        return locSet;
    }//end prepareOldLocators
    /* **************** END Private Static Utility Methods ***************** */

    /* ************** BEGIN Private NON-Static Utility Methods ************* */
    /* BEGIN Private Startup Methods --------------------------------------- */
    /** Common entry point for initialization of the service in any of its
     *  possible modes: transient, non-activatable-persistent, or 
     *  activatable-persistent; with or without performing a JAAS login.
     */
    private void init(String[] configArgs, boolean persistent)
                                           throws IOException,
                                                  ConfigurationException,
                                                  LoginException
    {
        config = ConfigurationProvider.getInstance
                                       ( configArgs,
                                         (this.getClass()).getClassLoader() );
        try {
            loginContext = (LoginContext)Config.getNonNullEntry
                                                         (config,
                                                          COMPONENT_NAME,
                                                          "loginContext",
                                                          LoginContext.class);
            initWithLogin(config, persistent, loginContext);
        } catch (NoSuchEntryException e) {
            doInit(config, persistent);
        }
    }//end init

    /** Initialization with JAAS login as the <code>Subject</code> referenced
     *  in the given <code>loginContext</code>.
     */
    private void initWithLogin(final Configuration config, 
                               final boolean persistent,
                                     LoginContext loginContext)
                                                 throws IOException,
                                                        ConfigurationException,
                                                        LoginException
    {
        loginContext.login();
        try {
            Subject.doAsPrivileged( loginContext.getSubject(),
                                    new PrivilegedExceptionAction() {
                                        public Object run() throws Exception {
                                            doInit(config, persistent);
                                            return null;
                                        }//end run
                                    },
                                    null );//end doAsPrivileged
        } catch (Throwable e) {
            if(e instanceof PrivilegedExceptionAction)  e = e.getCause();
            if(e instanceof IOException)  throw (IOException)e;
            if(e instanceof ConfigurationException) 
                                          throw (ConfigurationException)e;
            throw new RuntimeException(e);
        }
    }//end initWithLogin

    /** Initialization common to all modes in which instances of this service
     *  runs: activatable/persistent, non-activatable/persistent, and
     *  transient (non-activatable /non-persistent).
     */
    private void doInit(final Configuration config, final boolean persistent)
                                                 throws IOException,
                                                        ConfigurationException
    {
        /* Note that two discovery managers are retrieved/created in this
         * method. One manager is used internally by this service to provide
         * the lookup discovery capabilities offered to the clients that
         * register to use this service. That manager (discoveryMgr) is
         * considered part of this service's implementation, and thus is
         * created rather than retrieved from the configuration. When
         * created here, this manager is configured to initially discover
         * NO_GROUPS and NO LOCATORS. The sets of groups and locators to
         * discover on behalf of the registered clients will be populated
         * during recovery; and then the discovery mechanism provided by
         * this manager will be started later, after recovery and
         * configuration retrieval has occurred, and after this service
         * has been exported. Thus, it is important that this manager be
         * created prior to recovery; as is done below.
         * 
         * The second discovery manager (joinMgrLDM) is passed to the join
         * manager that is employed by this service to advertise itself to
         * clients through lookup services. This discovery manager is
         * retrieved from the configuration, and must satisfy the following
         * requirements: must be an instance of both DiscoveryGroupManagement
         * and DiscoveryLocatorManagment, and should be initially configured
         * to discover NO_GROUPS and NO LOCATORS. This discovery manager is
         * retrieved from the configuration after recovery of any persistent
         * state, and after retrieval of any initial configuration items.
         */
        discoveryMgr = new LookupDiscoveryManager
                                          (DiscoveryGroupManagement.NO_GROUPS,
                                           new LookupLocator[0],
                                           null,
                                           config);

        /* Get the proxy preparers for the remote event listeners */
        listenerPreparer = (ProxyPreparer)Config.getNonNullEntry
                                                   (config,
                                                    COMPONENT_NAME,
                                                    "listenerPreparer",
                                                    ProxyPreparer.class,
                                                    new BasicProxyPreparer());
        /* Get the proxy preparers for the lookup locators to join */
        locatorToJoinPreparer = (ProxyPreparer)Config.getNonNullEntry
                                                   (config,
                                                    COMPONENT_NAME,
                                                    "locatorToJoinPreparer",
                                                    ProxyPreparer.class,
                                                    new BasicProxyPreparer());
        /* Get the proxy preparers for the lookup locators to discover */
        locatorToDiscoverPreparer = (ProxyPreparer)Config.getNonNullEntry
                                                 (config,
                                                  COMPONENT_NAME,
                                                  "locatorToDiscoverPreparer",
                                                  ProxyPreparer.class,
                                                  new BasicProxyPreparer());
        if(persistent) {
            /* Retrieve the proxy preparers that will only be applied during
             * the state recovery process below, when any previously stored
             * listeners or locators are recovered from persistent storage.
             */
            recoveredListenerPreparer = 
             (ProxyPreparer)Config.getNonNullEntry(config,
                                                   COMPONENT_NAME,
                                                   "recoveredListenerPreparer",
                                                   ProxyPreparer.class,
                                                   new BasicProxyPreparer());
            recoveredLocatorToJoinPreparer =
             (ProxyPreparer)Config.getNonNullEntry
                                             (config,
                                              COMPONENT_NAME,
                                              "recoveredLocatorToJoinPreparer",
                                              ProxyPreparer.class,
                                              new BasicProxyPreparer());
            recoveredLocatorToDiscoverPreparer = 
             (ProxyPreparer)Config.getNonNullEntry
                                         (config,
                                          COMPONENT_NAME,
                                          "recoveredLocatorToDiscoverPreparer",
                                          ProxyPreparer.class,
                                          new BasicProxyPreparer());


            /* Get the log directory for persisting this service's state */
            persistDir = (String)Config.getNonNullEntry(config,
                                                        COMPONENT_NAME,
                                                        "persistenceDirectory",
                                                        String.class);
            /* Recover the state that was persisted on prior runs (if any) */
            log = new ReliableLog(persistDir, new LocalLogHandler());
            inRecovery = true;
            log.recover();
            inRecovery = false;
        }//endif(persistent)

        /* For the two persistent versions of this service (activatable and
         * non-activatable), state recovery is complete. For the non-persistent
         * version of this service, no state recovery occurred (because it
         * wasn't necessary).
         *
         * For the two persistent versions, there is a circumstance in which
         * 'one time', initial items must be retrieved from the configuration:
         * when the service is started for the very first time. For the
         * non-persistent version, those items will be retrieved every time
         * the service is started.
         *
         * The flag 'initialStartup' is used below to determine whether
         * or not to retrieve the initial configuration items. This is the
         * only purpose for that flag. 
         *
         * For either persistent version of the service, the flag's value
         * will be changed to false during the startup process only when
         * there already exists a 'snapshot' of the service's state from
         * a previous run. This is because the flag's value is only
         * changed during the recovery of the snapshot (see the method
         * recoverSnapshot()). Note that the only time such a snapshot
         * should NOT already exist at startup, is when the service is
         * being started for the very first time. Thus, when either
         * persistent version of the service is started for the first
         * time, the service's configuration is consulted for the initial
         * values of the items below; otherwise, when the service is being
         * re-started (after a crash for example), the values used for
         * those items will be the values retrieved above during recovery
         * of the service's persistent state.
         * 
         * With respect to the non-persistent version of the service, the
         * values of the items below will always be retrieved at startup.
         * This is because the non-persistent version of the service never
         * attempts to recover previously stored state; thus, the flag's
         * value will never change. Note that this will be true even if a
         * snapshot exists from a previous run of one of the persistent
         * versions of the service. 
         *
         * The service's Uuid is also handled here.
         */
        if(initialStartup) {
            if(log != null) {
                snapshotWt = ((Float)config.getEntry
                                         (COMPONENT_NAME,
                                          "initialPersistenceSnapshotWeight",
                                          float.class,
                                          new Float(snapshotWt))).floatValue();
                snapshotThresh =
                       Config.getIntEntry
                                  (config,
                                   COMPONENT_NAME,
                                   "initialPersistenceSnapshotThreshold",
                                   snapshotThresh, 0, Integer.MAX_VALUE);
            }//endif(log != null)
            leaseBound = Config.getLongEntry(config,
                                             COMPONENT_NAME,
                                             "initialLeaseBound",
                                             leaseBound, 0, Long.MAX_VALUE);
            /* Get any additional attributes with which to associate this
             * service when registering it with any lookup services.
             */
            Entry[] initAttrs = (Entry[])config.getEntry
                                                   (COMPONENT_NAME,
                                                    "initialLookupAttributes",
                                                    Entry[].class,
                                                    null );
            if(initAttrs != null) {
                ArrayList attrsList
                   = new ArrayList(thisServicesAttrs.length+initAttrs.length);
                for(int i=0;i<thisServicesAttrs.length;i++) {
                    attrsList.add(thisServicesAttrs[i]);
                }//end loop
                for(int i=0;i<initAttrs.length;i++) {
                    attrsList.add(initAttrs[i]);
                }//end loop
                thisServicesAttrs = (Entry[])attrsList.toArray
                                                (new Entry[attrsList.size()]);
            }//endif(initAttrs != null)

            /* Get the initial groups this service should join. */
            thisServicesGroups =
                 (String[])config.getEntry(COMPONENT_NAME, 
                                           "initialLookupGroups", 
                                           String[].class, 
                                           thisServicesGroups);
            /* Get the initial locators this service should join. */
            thisServicesLocators =
                     (LookupLocator[])config.getEntry(COMPONENT_NAME, 
                                                      "initialLookupLocators", 
                                                      LookupLocator[].class, 
                                                      new LookupLocator[0]);
            if(thisServicesLocators == null) {
                thisServicesLocators = new LookupLocator[0];
            }//endif

            /* Generate the private, universally unique (over space and time)
             * ID that will be used by the outer proxy to test for equality
             * with other proxies.
             */
            proxyID = UuidFactory.generate();
        }//endif(initialStartup)

        /* The proxyID should never be null at this point. It should have
         * been either recovered from the persisted state, or generated above.
         */
        if(proxyID == null) throw new NullPointerException("proxyID == null");

        /* Get the various configurable constants */
        leaseMax = Config.getLongEntry(config,
                                       COMPONENT_NAME,
                                       "leaseMax",
                                       leaseMax, 0, Long.MAX_VALUE);
        /* Take a snapshot of the current state to "clean up" the log file,
         * and to record the items set above.
         */
	if(log != null) log.snapshot();
        /* The service ID used to register this service with lookup services
         * is always derived from the proxyID that is associated with the
         * service for the lifetime of the service.
         */
        serviceID = new ServiceID(proxyID.getMostSignificantBits(),
                                  proxyID.getLeastSignificantBits());
        /* Get a general-purpose task manager for this service */
        taskMgr = (TaskManager)Config.getNonNullEntry
                                          (config,
                                           COMPONENT_NAME,
                                           "taskManager",
                                           TaskManager.class,
                                           new TaskManager(10,1000*15,1.0f) );
        /* Get the discovery manager to pass to this service's join manager. */
        try {
            joinMgrLDM  = 
                (DiscoveryManagement)Config.getNonNullEntry
                                                  (config,
                                                   COMPONENT_NAME,
                                                   "discoveryManager",
                                                   DiscoveryManagement.class);
            if( joinMgrLDM instanceof DiscoveryGroupManagement ) {
                String[] groups0 =
                           ((DiscoveryGroupManagement)joinMgrLDM).getGroups();
                if(    (groups0 == DiscoveryGroupManagement.ALL_GROUPS)
                    || (groups0.length != 0) )
                {
                    throw new ConfigurationException
                                 ("discoveryManager entry must be configured "
                                  +"to initially discover/join NO_GROUPS");
                }//endif
            } else {// !(joinMgrLDM instanceof DiscoveryGroupManagement)
                throw new ConfigurationException
                                        ("discoveryManager entry must "
                                         +"implement DiscoveryGroupManagment");
            }//endif
            if( joinMgrLDM instanceof DiscoveryLocatorManagement ) {
                LookupLocator[] locs0 =
                        ((DiscoveryLocatorManagement)joinMgrLDM).getLocators();
                if( (locs0 != null) && (locs0.length != 0) ) {
                    throw new ConfigurationException
                                 ("discoveryManager entry must be configured "
                                  +"to initially discover/join no locators");
                }//endif
            } else {// !(joinMgrLDM instanceof DiscoveryLocatorManagement)
                throw new ConfigurationException
                                     ("discoveryManager entry must "
                                      +"implement DiscoveryLocatorManagement");
            }//endif
        } catch (NoSuchEntryException e) {
            joinMgrLDM
               = new LookupDiscoveryManager(DiscoveryGroupManagement.NO_GROUPS,
                                            new LookupLocator[0], null,config);
        }

        /* Handle items and duties related to exporting this service. */
        ServerEndpoint endpoint = TcpServerEndpoint.getInstance(0);
        InvocationLayerFactory ilFactory = new BasicILFactory();
        Exporter defaultExporter = new BasicJeriExporter(endpoint,
                                                         ilFactory,
                                                         false,
                                                         true);
        /* For the activatable server */
        if(activationID != null) {
            ProxyPreparer aidPreparer =
              (ProxyPreparer)Config.getNonNullEntry(config,
                                                    COMPONENT_NAME,
                                                    "activationIdPreparer",
                                                    ProxyPreparer.class,
                                                    new BasicProxyPreparer());
            ProxyPreparer aSysPreparer = 
              (ProxyPreparer)Config.getNonNullEntry(config,
                                                    COMPONENT_NAME,
                                                    "activationSystemPreparer",
                                                    ProxyPreparer.class,
                                                    new BasicProxyPreparer());
            activationID = (ActivationID)aidPreparer.prepareProxy
                                                               (activationID);
            activationSystem = (ActivationSystem)aSysPreparer.prepareProxy
                                                            (activationSystem);
            defaultExporter = new ActivationExporter(activationID,
                                                     defaultExporter);
        }//endif(activationID != null)

        /* Get the exporter that will be used to export this service */
        try {
            serverExporter = (Exporter)Config.getNonNullEntry(config,
                                                              COMPONENT_NAME,
                                                              "serverExporter",
                                                              Exporter.class,
                                                              defaultExporter,
                                                              activationID);
        } catch(ConfigurationException e) {// exception, use default
            throw new ExportException("Configuration exception while "
                                      +"retrieving service's exporter",
                                      e);
        }
        /* Export this service */
        innerProxy = (Fiddler)serverExporter.export(this);

        /* Create the outer (smart) proxy that is registered with lookups */
        outerProxy = FiddlerProxy.createServiceProxy(innerProxy, proxyID);
        /* Create the proxy that can be used to administer this service */
        adminProxy = FiddlerAdminProxy.createAdminProxy(innerProxy, proxyID);

        /* Create the following threads here, after a possible JASS login,
         * rather than in the constructor, before the login. This must
         * be done so that the threads will have the correct subject.
         */
        leaseExpireThread = new LeaseExpireThread();
        if(log != null) snapshotThread = new SnapshotThread();

        /* Start the discovery mechanism for all recovered registrations */
        discoveryMgr.addDiscoveryListener(discoveryListener);

        /* Advertise the services provided by this entity */
	joinMgr = new JoinManager(outerProxy, thisServicesAttrs,
                                  serviceID, joinMgrLDM, null,
                                  config);
        ((DiscoveryLocatorManagement)joinMgrLDM).setLocators
                                                        (thisServicesLocators);
        ((DiscoveryGroupManagement)joinMgrLDM).setGroups(thisServicesGroups);

	/* start up all the daemon threads */
	leaseExpireThread.start();
        if(log != null) snapshotThread.start();
        logInfoStartup();
    }//end doInit
    /* END Private Startup Methods ----------------------------------------- */

    /* BEGIN Private Shutdown Methods -------------------------------------- */
    /* Called in the constructor when failure occurs during the initialization
     * process. Un-does any work that may have already been completed; for
     * example, un-exports the service if it has already been exported,
     * terminates any threads that may have been started, etc.
     */
    private void cleanupInitFailure() {
        if(innerProxy != null)  {
            try {
                serverExporter.unexport(true);
            } catch(Throwable t) { }
        }//endif

        if(taskMgr != null)  {
            try {
                taskMgr.terminate();
            } catch(Throwable t) { }
        }//endif

        if(joinMgr != null)  {
            try {
                joinMgr.terminate();
            } catch(Throwable t) { }
        }//endif

        if(joinMgrLDM != null)  {
            try {
                joinMgrLDM.terminate();
            } catch(Throwable t) { }
        }//endif

        if(discoveryMgr != null)  {
            try {
                discoveryMgr.terminate();
            } catch(Throwable t) { }
        }//endif

        if(leaseExpireThread != null)  {
            try {
                leaseExpireThread.interrupt();
                leaseExpireThread.join();
            } catch(Throwable t) { }
        }//endif

        if(snapshotThread != null)  {
            try {
                snapshotThread.interrupt();
                snapshotThread.join();
            } catch(Throwable t) { }
        }//endif
    }//end cleanupInitFailure

    /* Convenience method called in the constructor or the activatable version
     * of this service when failure occurs during the initialization process.
     * Logs and rethrows the given <code>Throwable</code> so the constructor
     * doesn't have to.
     */
    private void handleActivatableInitThrowable(Throwable t) 
                                            throws IOException,
                                                   ActivationException,
                                                   ConfigurationException,
                                                   LoginException,
                                                   ClassNotFoundException
    {
        handleInitThrowable(t);
        if (t instanceof ActivationException) {
            throw (ActivationException)t;
        } else if (t instanceof LoginException) {
            throw (ClassNotFoundException)t;
        } else {
            throw new AssertionError(t);
        }//endif
    }//end handleInitThrowable

    /* Convenience method called in the constructor or the non-activatable 
     * version of this service when failure occurs during the initialization
     * process. Logs and rethrows the given <code>Throwable</code> so the
     * constructor doesn't have to.
     */
    private void handleInitThrowable(Throwable t) 
                                            throws IOException,
                                                   ConfigurationException,
                                                   LoginException
    {
        problemLogger.log(Level.SEVERE, "cannot initialize the service", t);
        if (t instanceof IOException) {
            throw (IOException)t;
        } else if (t instanceof ConfigurationException) {
            throw (ConfigurationException)t;
        } else if (t instanceof LoginException) {
            throw (LoginException)t;
        } else if (t instanceof RuntimeException) {
            throw (RuntimeException)t;
        } else if (t instanceof Error) {
            throw (Error)t;
        }//endif
    }//end handleInitThrowable

    /**
     * Called by the public method <code>destroy</code> as well as by the
     * <code>apply</code> method in the various LogObj classes that,
     * during recovery, modify the managed sets of the discovery manager;
     * and, while doing so, experience an IOException when the multicast
     * request protocol fails to start during recovery (an un-recoverable
     * exception).
     *
     * This method destroys the lookup discovery service, if possible,
     * including its persistent storage. This method spawns a separate
     * thread to do the actual work asynchronously, so a successful
     * return from this method usually does not mean that the service
     * has been destroyed.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#destroy
     */
    private void destroyDo() {
        (new DestroyThread()).start();
    }//end destroyDo
    /* END Private Shutdown Methods ---------------------------------------- */

    /* BEGIN Private Registration Methods ---------------------------------- */
    /**
     * This method is called by the public method <code>register</code>.
     * This method creates a registration object that is an instance of
     * <code>FiddlerRegistration</code> which implements the interface
     * <code>LookupDiscoveryRegistration</code>, and acts as a proxy to the
     * registration-related methods of the backend server of the Fiddler
     * implementation of the lookup discovery service.
     * <p>
     * This method also associates with the registration all information
     * needed by the registration to participate in the lookup discovery
     * service, information such as: IDs, lease information, event information.
     * 
     * @param groups        names of groups whose members are the lookup
     *                      services to discover.
     * @param locators      instances of LookupLocator, each corresponding to
     *                      a specific lookup service to discover.
     * @param listener      the entity that will receive events notifying the
     *                      registration that a lookup service of interest has
     *                      been discovered.
     * @param handback      the object that will be included in every
     *                      notification event sent to the registered listener.
     * @param leaseDuration long value representing the amount of time (in
     *                      milliseconds) for which the resources of the
     *                      lookup discovery service are being requested.
     *                      
     * @return an instance of FiddlerRegistration which implements the
     *         LookupDiscoveryRegistration interface, and acts as a proxy
     *         to the registration-related methods of the backend server
     *         of the Fiddler implementation of the lookup discovery service
     *
     * @throws java.io.IOException this exception occurs when the multicast
     *         request protocol fails to start.
     *
     * @throws java.rmi.RemoteException this exception occurs when the
     *         attempt to prepare the listener fails due to a
     *         <code>RemoteException</code>
     * 
     * @throws java.lang.SecurityException this exception occurs when the
     *         attempt to prepare the listener fails due to a
     *         <code>SecurityException</code>
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#register
     */
    private LookupDiscoveryRegistration registerDo
                                             (String[] groups,
                                              LookupLocator[] locators,
                                              RemoteEventListener listener,
                                              MarshalledObject handback,
                                              long leaseDuration)
                                                       throws RemoteException,
                                                              IOException
    {
        /* Input okay. Create the registration and associated information */
	long curTime    = System.currentTimeMillis();
        leaseDuration   = applyBoundToLeaseDuration(leaseDuration,
                                                    leaseBound);
        Uuid regID      = UuidFactory.generate();
        Uuid leaseID    = regID;//use same ID since Reg "wraps" the lease
        long expiration = curTime + leaseDuration;

        /* Prepare the new listener */
        listener = (RemoteEventListener)listenerPreparer.prepareProxy
                                                                   (listener);
        RegistrationInfo regInfo = new RegistrationInfo( regID,
                                                         groups,locators,
                                                         leaseID,expiration,
                                                         curEventID,handback,
                                                         listener );
        curEventID++;
	addRegistration(regInfo);
        logInfoRegistration("\nadded registration:  registrationID = ",regID);
        addLogRecord(new RegistrationGrantedLogObj(regInfo));
        /* Queue task for sending a discovered event */
        taskMgr.add(new NewRegistrationTask(regInfo));
	/* See if the expire thread needs to wake up earlier */
	if (expiration < minExpiration) {
	    minExpiration = expiration;
	    concurrentObj.waiterNotify(leaseExpireThreadSyncObj);
	}
        FiddlerLease regLease =
                   FiddlerLease.createLease
                           (innerProxy, proxyID, regID, leaseID, expiration);
        EventRegistration eventReg = new EventRegistration( regInfo.eventID,
                                                            outerProxy,
                                                            regLease,
                                                            regInfo.seqNum );
        logInfoGroups();
        logInfoLocators();
        FiddlerRegistration regObj = FiddlerRegistration.createRegistration
                                                (innerProxy, regID, eventReg);
        logInfoRegistration("\ncreated registration: registrationID = ",
                            regObj);
        return regObj;
    }//end registerDo

    /** 
     * Places the registration corresponding to the <code>regInfo</code> 
     * parameter in both the <code>registrationByID</code> map and the 
     * <code>registrationByTime</code> map. This method also updates the 
     * managed sets in the discovery manager in the appropriate way. This 
     * should be called whenever a new registration has been created 
     * and needs to be added to the data base (for example, the methods
     * <code>registerDo</code>, <code>recoverSnapshot</code> and the 
     * <code>apply</code> method of the <code>RegistrationGrantedLogObj</code>
     * class all call this method).
     * 
     * @param regInfo  the data structure record corresponding to the
     *                 registration that is to be added to the data base
     *
     * @throws java.io.IOException this exception occurs when the multicast
     *         request protocol fails to start.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#registerDo
     * @see com.sun.jini.fiddler.FiddlerImpl#recoverSnapshot
     */
    private void addRegistration(RegistrationInfo regInfo) throws IOException {
        if (regInfo.listener == null) {
	    /* failed to recover from log */
            if( problemLogger.isLoggable(Level.INFO) ) {
                problemLogger.log(Level.INFO, "cannot add registration (ID = "
                                  +regInfo.registrationID+"); failed to "
                                  +"unmarshal listener during recovery");
            }//endif
            return;
        }//endif
        /* First add the indicated registration */
        registrationByID.put(regInfo.registrationID, regInfo);
        registrationByTime.put(regInfo,regInfo);
        /* Update the set of groups managed by the discovery manager */
        updateDiscoveryMgrGroups();
        /* Update the set of locators managed by the discovery manager */
        updateDiscoveryMgrLocators();
    }//end addRegistration

    /** 
     * Removes the registration corresponding to the <code>regInfo</code> 
     * parameter from this service's state. Removes the registration from 
     * both the <code>registrationByID</code> map and the 
     * <code>registrationByTime</code> map. This method also updates the 
     * managed sets in the discovery manager in the appropriate way. This 
     * should be called whenever a current registration needs to be removed
     * from the data base (for example, when the registration's lease is
     * is expired in the <code>LeaseExpireThread</code>, and when the
     * registration's lease is cancelled in the <code>cancelLeaseDo</code>
     * method).
     * 
     * @param regInfo  the data structure record corresponding to the
     *                 registration that is to be removed from the data base
     *
     * @throws java.io.IOException this exception occurs when the multicast
     *         request protocol fails to start.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#cancelLeaseDo
     */
    private void removeRegistration(RegistrationInfo regInfo)
                                                         throws IOException
    {
        /* First remove the current registration */
        registrationByID.remove(regInfo.registrationID);
        registrationByTime.remove(regInfo);
        logInfoRegistration("\nremoved registration: registrationID = ",
                            regInfo.registrationID);
        /* Update the set of groups managed by the discovery manager */
        updateDiscoveryMgrGroups();
        /* Update the set of locators managed by the discovery manager */
        updateDiscoveryMgrLocators();
        logInfoGroups();
        logInfoLocators();
    }//end removeRegistration
    /* END Private Registration Methods ------------------------------------ */

    /* BEGIN Private Group Management Methods ------------------------------ */
    /**
     * Called by the public method <code>addGroups</code>. This method
     * queues an <code>AddGroupsTask</code> which performs the actual
     * augmentation of the given registration's desired groups.
     * 
     * @param regInfo the data structure record corresponding to the
     *                registration whose managed set of groups is to be
     *                augmented
     * @param groups  a String array, none of whose elements may be null,
     *                consisting of the group names with which to augment the
     *                registration's managed set of groups.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#addGroups
     * @see com.sun.jini.fiddler.FiddlerRegistration#addGroups
     * @see net.jini.discovery.LookupDiscoveryRegistration#addGroups
     */
    private void addGroupsDo(RegistrationInfo regInfo, String[] groups) {
        taskMgr.add(new AddGroupsTask(regInfo,groups));
    }//end addGroupsDo

    /**
     * Called by the <code>apply</code> method of the class
     * <code>GroupsAddedToRegistrationLogObj</code> (which is invoked 
     * during state recovery). This method queues an
     * <code>AddGroupsTask</code> which performs the actual augmentation
     * of the given registration's desired groups.
     * <p>
     * @param registrationID   the ID of the data structure record
     *                         corresponding to the registration whose
     *                         managed set of groups is to be augmented
     * @param registrationByID the map containing all active registrations
     *                         managed by this service
     * @param groups           a <code>String</code> array, none of whose 
     *                         elements may be null, consisting of the group
     *                         names with which to augment the registration's
     *                         managed set of groups.
     * 
     * @see com.sun.jini.fiddler.FiddlerImpl#addGroupsDo
     * @see com.sun.jini.fiddler.FiddlerImpl#addGroups
     * @see com.sun.jini.fiddler.FiddlerRegistration#addGroups
     * @see net.jini.discovery.LookupDiscoveryRegistration#addGroups
     */
    private void addGroupsDo(Uuid registrationID,
                             HashMap registrationByID,
                             String[] groups)
    {
        addGroupsDo((RegistrationInfo)(registrationByID.get(registrationID)),
                     groups);
    }//end addGroupsDo

    /**
     * Called by the public method <code>setGroups</code>. This method
     * queues a <code>SetGroupsTask</code> which performs the actual
     * replacement.
     * 
     * @param regInfo the data structure record corresponding to the
     *                registration whose managed set of groups is to be
     *                replaced
     * @param groups  a String array, none of whose elements may be null, 
     *                consisting of the group names with which to replace the
     *                registration's managed set of groups.
     * 
     * @see com.sun.jini.fiddler.FiddlerImpl#setGroups
     * @see com.sun.jini.fiddler.FiddlerRegistration#setGroups
     * @see net.jini.discovery.LookupDiscoveryRegistration#setGroups
     */
    private void setGroupsDo(RegistrationInfo regInfo, String[] groups) {
        taskMgr.add(new SetGroupsTask(regInfo,groups));
    }//end setGroupsDo

    /**
     * Called by the <code>apply</code> method of the class
     * <code>GroupsSetInRegistrationLogObj</code> (which is invoked 
     * during state recovery). This method queues a
     * <code>SetGroupsTask</code> which performs the actual replacement.
     * <p>
     * @param registrationID   the ID of the data structure record
     *                         corresponding to the registration whose
     *                         managed set of groups is to be replaced
     * @param registrationByID the map containing all active registrations
     *                         managed by this service
     * @param groups           a <code>String</code> array, none of whose 
     *                         elements may be null, consisting of the group
     *                         names with which to replace the registration's
     *                         managed set of groups.
     * 
     * @see com.sun.jini.fiddler.FiddlerImpl#setGroupsDo
     * @see com.sun.jini.fiddler.FiddlerImpl#setGroups
     * @see com.sun.jini.fiddler.FiddlerRegistration#setGroups
     * @see net.jini.discovery.LookupDiscoveryRegistration#setGroups
     */
    private void setGroupsDo(Uuid registrationID,
                             HashMap registrationByID,
                             String[] groups)
    {
        setGroupsDo((RegistrationInfo)(registrationByID.get(registrationID)),
                     groups);
    }//end setGroupsDo

    /**
     * Called by the public method <code>removeGroups</code>. This method
     * queues a <code>RemoveGroupsTask</code> which performs the actual
     * removal.
     * 
     * @param regInfo the data structure record corresponding to the
     *                registration from whose managed set of groups the input
     *                set of groups is to be removed
     * @param groups  a String array, none of whose elements may be null,
     *                consisting of the group names to remove from the 
     *                registration's managed set of groups
     * 
     * @see com.sun.jini.fiddler.FiddlerImpl#removeGroups
     * @see com.sun.jini.fiddler.FiddlerRegistration#removeGroups
     * @see net.jini.discovery.LookupDiscoveryRegistration#removeGroups
     */
    private void removeGroupsDo(RegistrationInfo regInfo, String[] groups) {
        taskMgr.add(new RemoveGroupsTask(regInfo,groups));
    }//end removeGroupsDo

    /**
     * Called by the <code>apply</code> method of the class
     * <code>GroupsRemovedFromRegistrationLogObj</code> (which is
     * invoked during state recovery). This method queues a
     * <code>RemoveGroupsTask</code> which performs the actual removal.
     * <p>
     * @param registrationID   the ID of the data structure record
     *                         corresponding to the registration from whose
     *                         managed set of groups the input set of groups
     *                         is to be removed
     * @param registrationByID the map containing all active registrations
     *                         managed by this service
     * @param groups           a <code>String</code> array, none of whose 
     *                         elements may be null, consisting of the group
     *                         names to remove from the registration's managed
     *                         set of groups
     * 
     * @see com.sun.jini.fiddler.FiddlerImpl#removeGroupsDo
     * @see com.sun.jini.fiddler.FiddlerImpl#removeGroups
     * @see com.sun.jini.fiddler.FiddlerRegistration#removeGroups
     * @see net.jini.discovery.LookupDiscoveryRegistration#removeGroups
     */
    private void removeGroupsDo(Uuid registrationID,
                                HashMap registrationByID,
                                String[] groups)
    {
        removeGroupsDo
                 ((RegistrationInfo)(registrationByID.get(registrationID)),
                   groups);
    }//end removeGroupsDo

    /** Builds a set containing the groups from all registrations. 
     *
     *  Iterates through all the active registrations, retrieving the groups
     *  to discover for each registration (minus duplicates). If at least one
     *  registration has requested that all groups be discovered, stops
     *  iterating (because the set must be ALL_GROUPS) and returns null.
     */
    private String[] getGroupsFromAllRegs() {
        HashSet  groupSet = new HashSet();
        for(Iterator itr=registrationByID.values().iterator();itr.hasNext();) {
            RegistrationInfo rInfo = (RegistrationInfo)itr.next();
            if(rInfo.groups == null) {
                return DiscoveryGroupManagement.ALL_GROUPS;
            } else {
                groupSet.addAll(rInfo.groups);
            }//end if
        }//end loop
        return (String[])groupSet.toArray(new String[groupSet.size()]);
    }//end getGroupsFromAllRegs

    /** This method returns a registrar-to-data-structure map in which each
     *  registrar key in the returned map is one of the keys from the
     *  the global map <code>allDiscoveredRegs</code>, and the corresponding 
     *  value is the (locator,groups) pair that corresponds to that registrar
     *  key in <code>allDiscoveredRegs</code>
     *  
     *  An element of <code>allDiscoveredRegs</code> is selected to have
     *  its registrar and associated (locator,groups) pair be included in the
     *  returned mapping if and only if the key value of the element is a
     *  registrar that belongs to at least one of the desired groups of the
     *  given registration (<code>regInfo</code> parameter). That is, the
     *  registrars referenced in the returned mapping are the registrars that
     *  are of interest - through group discovery - to the given registration.
     *
     *  Note that this method must be called from within a synchronization
     *  block.
     *
     * @param regInfo the data structure record corresponding to the
     *                registration whose groups-to-discover will be used to
     *                select the elements from <code>allDiscoveredRegs</code>
     *                to include in the return mapping
     * 
     *  @return a mapping in which the key values are registrars that,
     *          in addition to being elements of the global map
     *          <code>allDiscoveredRegs</code>, also belong to at least one
     *          of the desired groups referenced in the <code>regInfo</code>
     *          parameter; and whose map values are data structures of type
     *          <code>LocatorGroupsStruct</code> that contain the associated
     *          locator and member groups of the corresponding registrar key
     */
    private HashMap getDesiredRegsByGroup(RegistrationInfo regInfo) {
        HashSet desiredGroups = regInfo.groups;
        HashMap desiredRegMap = new HashMap(allDiscoveredRegs.size());
        Set eSet = allDiscoveredRegs.entrySet();
        for(Iterator itr = eSet.iterator(); itr.hasNext(); ) {
            Map.Entry pair = (Map.Entry)itr.next();
            LocatorGroupsStruct locGroupsStruct
                                       = (LocatorGroupsStruct)pair.getValue();
            if( interested(locGroupsStruct.groups,desiredGroups) ) {
                desiredRegMap.put((ServiceRegistrar)pair.getKey(),
                                  locGroupsStruct);
            }//endif
        }//end loop
        return desiredRegMap;
    }//end getDesiredRegsByGroup

    /** Modifies the discovery manager's managed set of groups in the
     *  appropriate way; that is, adds to the discovery manager's managed
     *  set of groups, any new groups-of-interest, across all registrations,
     *  and removes any groups that are no longer of interest to any of the
     *  registrations. This method is typically called after the groups of
     *  a particular registration have been modified.
     *
     *  Note that this method must be called from within a synchronization
     *  block.
     */
    private void updateDiscoveryMgrGroups() {
        /* Build the set containing the groups from all registrations. */
        String[] groupsAcrossAllRegs = getGroupsFromAllRegs();
        /* Update the discovery manager's set of groups to discover. Let
         * the manager sort out duplicates and any already-discovered groups.
         */
        try {
            discoveryMgr.setGroups( groupsAcrossAllRegs );
        } catch (IOException e) {
            String warnStr = "IOException: on call to setGroups() "
                             +"method of discovery manager";
            problemLogger.log(Level.INFO, warnStr, e);
            Entry[] warnAttrs = new Entry[]
                                      { new FiddlerStatus(StatusType.WARNING),
                                        new Comment(warnStr)
                                      };
            joinMgr.addAttributes(warnAttrs,true);
        } catch(IllegalStateException e) {
            String errStr = "IllegalStateException: discovery "
                            +"manager's setGroups() method was called "
                            +"after the manager was terminated";
            problemLogger.log(Level.INFO, errStr, e);
            Entry[] errorAttrs = new Entry[]
                                        { new FiddlerStatus(StatusType.ERROR),
                                          new Comment(errStr)
                                        };
            joinMgr.addAttributes(errorAttrs,true);
            throw new IllegalStateException(" discovery manager's "
                                            +"setGroups() method was "
                                            +"called after the manager "
                                            +"was terminated");
        }//end try
    }//end updateDiscoveryMgrGroups
    /* END Private Group Management Methods -------------------------------- */

    /* BEGIN Private Locator Management Methods ---------------------------- */
    /**
     * Called by the public method <code>addLocators</code>. This method
     * queues an <code>AddLocatorsTask</code> which performs the actual
     * augmentation of the given registration's desired locators.
     * 
     * @param regInfo  the data structure record corresponding to the
     *                 registration whose managed set of locators is to be
     *                 augmented
     * @param locators an array, none of whose elements may be null,
     *                 consisting of the <code>LookupLocator</code> objects
     *                 with which to augment the registration's managed set
     *                 of locators.
     * 
     * @see com.sun.jini.fiddler.FiddlerImpl#addLocators
     * @see com.sun.jini.fiddler.FiddlerRegistration#addLocators
     * @see net.jini.discovery.LookupDiscoveryRegistration#addLocators
     */
    private void addLocatorsDo(RegistrationInfo regInfo,
                               LookupLocator[]  locators)
    {
        taskMgr.add(new AddLocatorsTask(regInfo,locators));
    }//end addLocatorsDo

    /**
     * Called by the <code>apply</code> method of the class
     * <code>LocsAddedToRegistrationLogObj</code> (which is invoked 
     * during state recovery). This method queues an
     * <code>AddLocatorsTask</code> which performs the actual augmentation
     * of the given registration's desired locators.
     * <p>
     * @param registrationID   the ID of the data structure record
     *                         corresponding to the registration whose
     *                         managed set of locators is to be augmented
     * @param registrationByID the map containing all active registrations
     *                         managed by this service
     * @param locators         an array, none of whose elements may be null,
     *                         consisting of the <code>LookupLocator</code>
     *                         objects with which to augment the
     *                         registration's managed set of locators.
     * 
     * @see com.sun.jini.fiddler.FiddlerImpl#addLocatorsDo
     * @see com.sun.jini.fiddler.FiddlerImpl#addLocators
     * @see com.sun.jini.fiddler.FiddlerRegistration#addLocators
     * @see net.jini.discovery.LookupDiscoveryRegistration#addLocators
     */
    private void addLocatorsDo(Uuid registrationID,
                               HashMap registrationByID,
                               LookupLocator[] locators)
    {
        addLocatorsDo((RegistrationInfo)(registrationByID.get(registrationID)),
                      locators);
    }//end addLocatorsDo

    /**
     * Called by the public method <code>setLocators</code>. This method
     * queues a <code>SetLocatorsTask</code> which performs the 
     * actual replacement.
     * 
     * @param regInfo  the data structure record corresponding to the
     *                 registration whose managed set of locators is to be
     *                 replaced
     * @param locators an array, none of whose elements may be null,
     *                 consisting of the <code>LookupLocator</code> objects
     *                 with which to replace the registration's managed set
     *                 of locators.
     * 
     * @see com.sun.jini.fiddler.FiddlerImpl#setLocators
     * @see com.sun.jini.fiddler.FiddlerRegistration#setLocators
     * @see net.jini.discovery.LookupDiscoveryRegistration#setLocators
     */
    private void setLocatorsDo(RegistrationInfo regInfo,
                               LookupLocator[]  locators)
    {
        taskMgr.add(new SetLocatorsTask(regInfo,locators));
    }//end setLocatorsDo

    /**
     * Called by the <code>apply</code> method of the class
     * <code>LocsSetInRegistrationLogObj</code> (which is invoked 
     * during state recovery). This method queues a
     * <code>SetLocatorsTask</code> which performs the actual replacement.
     * <p>
     * @param registrationID   the ID of the data structure record
     *                         corresponding to the registration whose
     *                         managed set of locators is to be replaced
     * @param registrationByID the map containing all active registrations
     *                         managed by this service
     * @param locators         an array, none of whose elements may be null,
     *                         consisting of the <code>LookupLocator</code>
     *                         objects with which to replace the
     *                         registration's managed set of locators.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#setLocatorsDo
     * @see com.sun.jini.fiddler.FiddlerImpl#setLocators
     * @see com.sun.jini.fiddler.FiddlerRegistration#setLocators
     * @see net.jini.discovery.LookupDiscoveryRegistration#setLocators
     */
    private void setLocatorsDo(Uuid registrationID,
                               HashMap registrationByID,
                               LookupLocator[] locators)
    {
        setLocatorsDo((RegistrationInfo)(registrationByID.get(registrationID)),
                      locators);
    }//end setLocatorsDo

    /**
     * Called by the public method <code>removeLocators</code>. This method
     * queues a <code>RemoveLocatorsTask</code> which performs the 
     * actual removal.
     * 
     * @param regInfo  the data structure record corresponding to the
     *                 registration from whose managed set of locators the
     *                 input set of locators is to be removed
     * @param locators an array, none of whose elements may be null,
     *                 consisting of the <code>LookupLocator</code> objects
     *                 to remove from the registration's managed set of 
     *                 locators.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#removeLocators
     * @see com.sun.jini.fiddler.FiddlerRegistration#removeLocators
     * @see net.jini.discovery.LookupDiscoveryRegistration#removeLocators
     */
    private void removeLocatorsDo(RegistrationInfo regInfo,
                                  LookupLocator[]  locators)
    {
        taskMgr.add(new RemoveLocatorsTask(regInfo,locators));
    }//end removeLocatorsDo

    /**
     * Called by the <code>apply</code> method of the class
     * <code>LocsRemovedFromRegistrationLogObj</code> (which is
     * invoked during state recovery). This method queues a
     * <code>RemoveLocatorsTask</code> which performs the actual removal.
     * <p>
     * @param registrationID   the ID of the data structure record
     *                         corresponding to the registration from whose
     *                         managed set of groups the input set of locators
     *                         is to be removed
     * @param registrationByID the map containing all active registrations
     *                         managed by this service
     * @param locators         an array, none of whose elements may be null,
     *                         consisting of the <code>LookupLocator</code>
     *                         objects to remove from the registration's
     *                         managed set of locators.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#removeLocatorsDo
     * @see com.sun.jini.fiddler.FiddlerImpl#removeLocators
     * @see com.sun.jini.fiddler.FiddlerRegistration#removeLocators
     * @see net.jini.discovery.LookupDiscoveryRegistration#removeLocators
     */
    private void removeLocatorsDo(Uuid registrationID,
                                  HashMap registrationByID,
                                  LookupLocator[] locators)
    {
        removeLocatorsDo
                   ((RegistrationInfo)(registrationByID.get(registrationID)),
                    locators);
    }//end removeLocatorsDo

    /** Builds a set containing the locators from all registrations. 
     *
     *  Iterates through all the active registrations, retrieving the locators
     *  to discover for each registration (minus duplicates).
     */
    private LookupLocator[] getLocatorsFromAllRegs() {
        HashSet locatorSet = new HashSet();
        for(Iterator itr=registrationByID.values().iterator();itr.hasNext();) {
            RegistrationInfo rInfo = (RegistrationInfo)itr.next();
            if(rInfo.locators == null) {
                throw new AssertionError
                            ("registration contains a null set of locators");
            } else {
                locatorSet.addAll(rInfo.locators);
            }//end if
        }//end loop
        return (LookupLocator[])locatorSet.toArray
                                        (new LookupLocator[locatorSet.size()]);
    }//end getLocatorsFromAllRegs

    /** This method returns a registrar-to-data-structure map in which each
     *  registrar key in the returned map is one of the keys from the
     *  the global map <code>allDiscoveredRegs</code>, and the corresponding 
     *  value is the (locator,groups) pair that corresponds to that registrar
     *  key in <code>allDiscoveredRegs</code>
     *  
     *  An element of <code>allDiscoveredRegs</code> is selected to have
     *  its registrar and associated (locator,groups) pair be included in the
     *  returned mapping if and only if the key value under consideration is
     *  a registrar whose locator equals one of the desired locators of the
     *  given registration (<code>regInfo</code> parameter). That is, the
     *  registrars referenced in the returned mapping are the registrars that
     *  are of interest - through locator discovery - to the given
     *  registration.
     *
     * @param regInfo the data structure record corresponding to the
     *                registration whose locators-to-discover will be used to
     *                select the elements from <code>allDiscoveredRegs</code>
     *                to include in the return mapping
     * 
     *  @return a mapping in which the key values are registrars that,
     *          in addition to being elements of the global map
     *          <code>allDiscoveredRegs</code>, have locators that are
     *          elements of the set of desired locators referenced in the
     *          <code>regInfo</code> parameter; and whose map values are
     *          data structures of type <code>LocatorGroupsStruct</code> that
     *          contain the associated locator and member groups of the
     *          corresponding registrar key
     */
    private HashMap getDesiredRegsByLocator(RegistrationInfo regInfo) {
        HashSet desiredLocators = regInfo.locators;
        HashMap desiredRegMap = new HashMap(allDiscoveredRegs.size());
        Set eSet = allDiscoveredRegs.entrySet();
        for(Iterator itr = eSet.iterator(); itr.hasNext(); ) {
            Map.Entry pair = (Map.Entry)itr.next();
            LocatorGroupsStruct locGroupsStruct
                                      = (LocatorGroupsStruct)pair.getValue();
            if( desiredLocators.contains(locGroupsStruct.locator) ) {
                desiredRegMap.put((ServiceRegistrar)pair.getKey(),
                                  locGroupsStruct);
            }//endif
        }//end loop
        return desiredRegMap;
    }//end getDesiredRegsByLocator

    /** Modifies the discovery manager's managed set of locators in the
     *  appropriate way; that is, adds to the discovery manager's managed
     *  set of locators, any new locators-of-interest, across all
     *  registrations, and removes any locators that are no longer of
     *  interest to any of the registrations. This method is typically
     *  called after the locators of a particular registration have been
     *  modified.
     *
     *  Note that this method must be called from within a synchronization
     *  block.
     */
    private void updateDiscoveryMgrLocators() {
        /* Build the set containing the locators from all registrations. */
        LookupLocator[] locsAcrossAllRegs = getLocatorsFromAllRegs();
        /* Update the discovery manager's set of locators to discover. Let
         * the manager sort out duplicates and any already-discovered locators.
         */
        try {
            discoveryMgr.setLocators( locsAcrossAllRegs );
        } catch(IllegalStateException e) {
            String errStr = "IllegalStateException: discovery "
                            +"manager's setLocators() method was called "
                            +"after the manager was terminated";
            problemLogger.log(Level.INFO, errStr, e);
            Entry[] errorAttrs = new Entry[]
                                        { new FiddlerStatus(StatusType.ERROR),
                                          new Comment(errStr)
                                        };
            joinMgr.addAttributes(errorAttrs,true);
            throw new IllegalStateException(" discovery manager's "
                                            +"setLocators() method was "
                                            +"called after the manager "
                                            +"was terminated");
        }//end try
    }//end updateDiscoveryMgrLocators
    /* END Private Locator Management Methods ------------------------------ */

    /* BEGIN Private Lease Renewal Methods --------------------------------- */
    /**
     * Called by the public method <code>renewLease</code>. This method renews
     * the lease corresponding to the <code>registrationID</code> and 
     * <code>leaseID</code> parameters, granting a new duration that is
     * less than or equal to the requested duration value contained in
     * the <code>duration</code> parameter.
     * 
     * @param regInfo  the data structure record corresponding to the
     *                 registration whose lease is to be renewed
     * @param leaseID  identifier assigned by the lease grantor to the lease
     *                 that is to be renewed
     * @param duration the requested duration for the lease being renewed
     *
     * @throws net.jini.core.lease.UnknownLeaseException this exception occurs
     *         when the lease being renewed is unknown to the lease grantor.
     *
     * @return <code>long</code> value representing the actual duration that
     *         was granted for the renewed lease. Note that the actual
     *         duration granted and returned by this method may be less than
     *         the duration requested.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#renewLease
     */
    private long renewLeaseDo(RegistrationInfo regInfo,
                              Uuid leaseID,
                              long duration) throws UnknownLeaseException
    {
        long curTime       = System.currentTimeMillis();
        long newExpiration = renewLeaseInt(regInfo,leaseID,duration,curTime);
        addLogRecord(new LeaseRenewedLogObj
                           (regInfo.registrationID, leaseID, newExpiration));
        return (newExpiration - curTime);
    }//end renewLeaseDo

    /**
     * Called by the method <code>renewLeaseDo</code>. This method takes
     * a requested duration for the lease corresponding to the
     * <code>registrationID</code> and <code>leaseID</code> parameters,
     * converts that duration to an absolute expiration (based on the local
     * clock) with the appropriate bound applied, and attempts to renew 
     * the lease, granting the new expiration time on the lease.
     * 
     * @param regInfo  the data structure record corresponding to the
     *                 registration whose lease is to be renewed
     * @param leaseID  identifier assigned by the lease grantor to the lease
     *                 that is to be renewed
     * @param duration the requested duration for the lease being renewed
     *
     * @return <code>long</code> value representing the new absolute
     *         expiration time granted for the renewed lease.
     *
     * @throws net.jini.core.lease.UnknownLeaseException this exception occurs
     *         when the lease being renewed is unknown to the lease grantor.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#renewLeaseDo
     */
    private long renewLeaseInt(RegistrationInfo regInfo,
                               Uuid leaseID,
                               long duration,
                               long curTime)  throws UnknownLeaseException
    {
        if(duration == Lease.ANY) {
            duration = leaseBound;
        } else if(duration <= 0) {
            throw new IllegalArgumentException("non-positive lease duration");
        }//endif
        if (    (regInfo == null) 
             || ( !(regInfo.leaseID).equals(leaseID) )
             || (regInfo.leaseExpiration <= curTime) ) {
            throw new UnknownLeaseException();
        }//endif
        if(    (duration > leaseBound)
	    && (duration > (regInfo.leaseExpiration - curTime)) ) {
            duration = Math.max( (regInfo.leaseExpiration - curTime),
                                 leaseBound );
        }//endif
        long newExpiration = curTime + duration;
        /* Force a re-sort in the registrationByTime map */
        registrationByTime.remove(regInfo);       // force first re-sort
        regInfo.leaseExpiration = newExpiration;  // modify outside of map
        registrationByTime.put(regInfo, regInfo); // insert & force new sort
        /* see if the expire thread needs to wake up earlier */
        if (newExpiration < minExpiration) {
            minExpiration = newExpiration;
            concurrentObj.waiterNotify(leaseExpireThreadSyncObj);
        }//endif
        return newExpiration;
    }//end renewLeaseInt

    /**
     * This method performs the final steps in the process of renewing the
     * lease on the registration corresponding to the <code>regInfo</code>
     * and <code>leaseID</code> parameters, granting a requested absolute
     * expiration time for that lease.
     * 
     * @param regInfo    the data structure record corresponding to the
     *                   registration whose lease is to be renewed
     * @param leaseID    identifier assigned by the lease grantor to the lease
     *                   that is to be renewed
     * @param expiration the requested absolute expiration time for the lease
     *                   being renewed
     */
    private void renewLeaseAbs(RegistrationInfo regInfo,
                               Uuid leaseID,
                               long expiration)
    {
        if ( (regInfo == null) || (regInfo.leaseID != leaseID) ) return;
        /* The act of renewing a registration's lease simply involves 
         * changing the expiration time stored in the registration's
         * data structure. But because the registrationByTime sorts its
         * elements by time, changing the value of the leaseExpiration field
         * of the regInfo parameter "in place" would typically result in 
         * the registrationByTime map being out of order. Contrast this with
         * the modification of the registration's non-time-dependent fields
         * (for example, adding, replacing or removing groups or locators),
         * where modifying the input regInfo parameter will modify the
         * appropriate field of the corresponding element of both the
         * registrationByID and registrationByTime maps; and the order 
         * will still be valid in the registrationByTime map because the
         * time-dependent data has not changed.
         * 
         * Thus, in order to maintain a valid ordering in the 
         * registrationByTime map, a re-sort is forced by first removing
         * from that map the element corresponding to regInfo, resetting
         * the leaseExpiration field of that object, and then re-inserting
         * the modified regInfo into the map, which forces another sort.
         */
        registrationByTime.remove(regInfo);       // force first re-sort
        regInfo.leaseExpiration = expiration;     // modify outside of map
        registrationByTime.put(regInfo, regInfo); // insert & force new sort
    }//end renewLeaseAbs

    /**
     * Called by the <code>apply</code> method of the class
     * <code>LeaseRenewedLogObj</code> (which is invoked during state
     * recovery). This method performs the final steps in the process
     * of renewing the lease on the registration corresponding to the
     * <code>registrationID</code> and <code>leaseID</code> parameters,
     * granting a requested absolute expiration time for that lease.
     * 
     * @param registrationID   the ID of the data structure record
     *                         corresponding to the registration whose
     *                         lease is to be renewed
     * @param registrationByID the map containing all active registrations
     *                         managed by this service
     * @param leaseID          identifier assigned by the lease grantor to
     *                         the lease that is to be renewed
     * @param expiration       the requested absolute expiration time for the
     *                         lease being renewed
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#renewLeaseDo
     */
    private void renewLeaseAbs(Uuid registrationID,
                               HashMap registrationByID,
                               Uuid leaseID,
                               long expiration)
    {
        renewLeaseAbs((RegistrationInfo)(registrationByID.get(registrationID)),
                      leaseID,expiration);
    }//end renewLeaseAbs

    /**
     * Called by the public method renewLeases to renew all of
     * the leases from a <code>LeaseMap</code> that correspond to the
     * elements of the <code>registrationIDs</code> and <code>leaseIDs</code>
     * parameters, granting as new durations the corresponding elements
     * of the <code>duration</code> parameter.
     * <p>
     * Note that all of the input parameters must be the same length.
     *
     * @param registrationIDs array containing the unique identifiers assigned
     *                        to the each registration to which each lease 
     *                        to be renewed corresponds
     * @param leaseIDs        array containing the identifiers assigned by the
     *                        lease grantor to each lease being renewed
     * @param durations       array containing the requested durations for 
     *                        each lease being renewed
     * 
     * @return an instance of FiddlerRenewResults containing data corresponding
     *         to the results (granted durations or exceptions) of each
     *         renewal attempt
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#renewLeases
     */
    private FiddlerRenewResults renewLeasesDo(Uuid[] registrationIDs,
				              Uuid[] leaseIDs,
				              long[] durations)
    {
        long curTime = System.currentTimeMillis();
        Exception[] exceptions = null;
        for (int i = 0; i < registrationIDs.length; i++) {
            RegistrationInfo regInfo
                = (RegistrationInfo)(registrationByID.get(registrationIDs[i]));
            try {
                durations[i] = renewLeaseInt(regInfo, leaseIDs[i],
                                             durations[i], curTime);
            } catch (Exception e) {
                durations[i] = -1;
                if (exceptions == null) {
                    exceptions = new Exception[]{e};
                } else {
                    exceptions = (Exception[])appendArray(exceptions, e);
                }//endif
            }
	}//end loop
        /* don't bother to weed out problem leases */
        addLogRecord
             ( new LeasesRenewedLogObj(registrationIDs,leaseIDs,durations) );
        for (int i = registrationIDs.length; --i >= 0; ) {
            if (durations[i] >= 0) {
                durations[i] -= curTime;
            }//endif
        }//end loop
        return new FiddlerRenewResults(durations, exceptions);
    }//end renewLeasesDo

    /**
     * Using the absolute expiration times contained in the
     * <code>expirations</code> parameter, renews all of the leases 
     * from a <code>LeaseMap</code> that correspond to the elements 
     * of the <code>registrationIDs</code> and <code>leaseIDs</code>
     * parameters; skipping any negative expiration times.
     * <p>
     * Note that all of the input parameters must be the same length.
     *
     * @param registrationIDs array containing the unique identifiers assigned
     *                        to the each registration to which each lease 
     *                        to be renewed corresponds
     * @param leaseIDs        array containing the identifiers assigned by the
     *                        lease grantor to each lease being renewed
     * @param expirations     array containing the absolute expiration times
     *                        to which to renew each lease
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#renewLeases
     * @see com.sun.jini.fiddler.FiddlerImpl#renewLeaseAbs
     */
    private void renewLeasesAbs(Uuid[] registrationIDs,
                                Uuid[] leaseIDs,
                                long[] expirations)
    {
        for (int i = registrationIDs.length; --i >= 0; ) {
            long expiration = expirations[i];
            if (expiration < 0)  continue;
            RegistrationInfo regInfo
                = (RegistrationInfo)(registrationByID.get(registrationIDs[i]));
            renewLeaseAbs(regInfo, leaseIDs[i], expiration);
        }//end loop
    }//end renewLeasesAbs
    /* END Private Lease Renewal Methods ----------------------------------- */

    /* BEGIN Private Lease Cancellation Methods ---------------------------- */
    /**
     * Called by the public method <code>cancelLease</code>. This method
     * cancels the lease on the registration corresponding to the 
     * <code>regInfo</code> and <code>leaseID</code> parameters.
     * 
     * @param regInfo the data structure record corresponding to the
     *                registration whose lease is to be cancelled
     * @param leaseID identifier assigned by the lease grantor to the lease
     *                that is to be cancelled
     *
     * @throws net.jini.core.lease.UnknownLeaseException this exception occurs
     *         when the lease being cancelled is unknown to the lease grantor.
     * 
     * @throws java.io.IOException this exception occurs when the multicast
     *         request protocol experiences a failure.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#cancelLease
     */
    private void cancelLeaseDo(RegistrationInfo regInfo, Uuid leaseID)
                                     throws UnknownLeaseException, IOException
    {
        long curTime = System.currentTimeMillis();
        if ( (regInfo == null) || (regInfo.leaseExpiration <= curTime) ) {
            throw new UnknownLeaseException();
        }//endif
        removeRegistration(regInfo);
        /* wake up thread if this might be the (only) earliest time */
        if (regInfo.leaseExpiration == minExpiration) {
            concurrentObj.waiterNotify(leaseExpireThreadSyncObj);
        }//endif
    }//end cancelLeaseDo

    /**
     * Called by the <code>apply</code> method of the class
     * <code>LeaseCancelledLogObj</code> (which is invoked during state
     * recovery). This method cancels the lease on the registration
     * corresponding to the <code>registrationID</code> and
     * <code>leaseID</code> parameters.
     * 
     * @param regInfo the data structure record corresponding to the
     *                registration whose lease is to be cancelled
     * @param registrationID   the ID of the data structure record
     *                         corresponding to the registration whose
     *                         lease is to be cancelled
     * @param registrationByID the map containing all active registrations
     *                         managed by this service
     * @param leaseID          identifier assigned by the lease grantor to
     *                         the lease that is to be cancelled
     *
     * @throws net.jini.core.lease.UnknownLeaseException this exception occurs
     *         when the lease being cancelled is unknown to the lease grantor.
     * 
     * @throws java.io.IOException this exception occurs when the multicast
     *         request protocol experiences a failure.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#cancelLeaseDo
     * @see com.sun.jini.fiddler.FiddlerImpl#cancelLease
     */
    private void cancelLeaseDo(Uuid registrationID,
                               HashMap registrationByID,
                               Uuid leaseID)
                                     throws UnknownLeaseException, IOException
    {
        cancelLeaseDo((RegistrationInfo)(registrationByID.get(registrationID)),
                      leaseID);
    }//end cancelLeaseDo

    /**
     * Called by the public method cancelLeases to the cancel all of
     * the leases from a <code>LeaseMap</code> that correspond to the
     * elements of the <code>registrationIDs</code> and <code>leaseIDs</code>
     * parameters.
     * <p>
     * The two input arrays must be the same length. If there are no
     * exceptions, the return value is null. Otherwise, the return value
     * has the same length as the <code>registrationIDs</code> parameter,
     * and has nulls for the leases that were successfully renewed.
     * 
     * @return an array containing the instances of Exception (or null for
     *         successfully renewed leases), each corresponding to the 
     *         exception that occurred upon attempting renewal.
     *
     * @see com.sun.jini.fiddler.FiddlerImpl#cancelLeases
     */
    private Exception[] cancelLeasesDo(Uuid[] registrationIDs,
                                       Uuid[] leaseIDs)
    {
        Exception[] exceptions = null;
        for (int i = registrationIDs.length; --i >= 0; ) {
            try {
                RegistrationInfo regInfo =
                     (RegistrationInfo)(registrationByID.get
                                                         (registrationIDs[i]));
                cancelLeaseDo(regInfo, leaseIDs[i]);
            } catch (Exception e) {
                if (exceptions == null) {
                    exceptions = new Exception[registrationIDs.length];
                }//endif
                exceptions[i] = e;
            }
        }//end loop
        return exceptions;
    }//end cancelLeasesDo
    /* END Private Lease Cancellation Methods ------------------------------ */

    /* BEGIN Private Event-Related Methods --------------------------------- */
    /** Returns the set of registrars common to both input parameters */
    private ServiceRegistrar[] intersectRegSets(ServiceRegistrar[] regs0,
                                                ServiceRegistrar[] regs1)
    {
        HashSet regSet = new HashSet(); //no duplicates
        /* Compare each input element of regs0 with each element of regs1,
         * storing matches along the way
         */
        nextRegistrar:
        for(int i=0;i<regs0.length;i++) {
            for(int j=0;j<regs1.length;j++) {
                if( (regs0[i]).equals(regs1[j]) ) {
                    regSet.add(regs0[i]);
                    continue nextRegistrar; // match found, next reg
                }//endif
            }//end loop(j)
        }//end loop(i)
        return ( (ServiceRegistrar[])(regSet).toArray
                                      (new ServiceRegistrar[regSet.size()]) );
    }//end intersectRegSets

    /** Returns true if the input registrar is an element of the given array */
    private boolean regIsElementOfRegSet(ServiceRegistrar   reg,
                                         ServiceRegistrar[] regSet)
    {
        for(int i=0;i<regSet.length;i++) {
            if( (regSet[i]).equals(reg) )  return true;
        }//end loop
        return false;
    }//end regIsElementOfRegSet

    /**
     * This method determines which of the registrars in the
     * <code>regsMap</code> parameter belong to the set of registrars
     * the given <code>regInfo</code> parameter wishes to discover. It
     * then adds those registrars to the regInfo's map of discovered
     * registrars (including only those registrars that can be successfully
     * serialized). Finally, a discovered event containing the discovery
     * information that is of interest to the regInfo is built and sent
     * to the regInfo's listener.
     * 
     * @param regInfo the data structure record corresponding to the
     *                registration whose listener will receive the event
     * @param regsMap mapping in which the key values are previously discovered
     *                registrars, and the map values are data structures of
     *                type <code>LocatorGroupsStruct</code> that contain the
     *                locator and member groups of the corresponding 
     *                registrar key
     */
    private void maybeSendDiscoveredEvent(RegistrationInfo regInfo,
                                          Map regsMap)
    {
        /* Determine the registrars that are common to both the input set of
         * registrars (the currently or newly discovered registrars) and the
         * registration's set of desired registrars. These are the registrars
         * to send in the event.
         */
        HashMap regsToAdd = new HashMap(regsMap.size());
        Set eSet = regsMap.entrySet();
        for(Iterator itr = eSet.iterator(); itr.hasNext(); ) {
            Map.Entry pair = (Map.Entry)itr.next();
            ServiceRegistrar reg = (ServiceRegistrar)pair.getKey();
            LocatorGroupsStruct curStructVal
                                      = (LocatorGroupsStruct)pair.getValue();
            /* If regInfo wants ALL_GROUPS, then every registrar is desired */
            if(regInfo.groups == null) { // ALL_GROUPS
                regsToAdd.put(reg,curStructVal);
            } else {//not ALL_GROUPS, reg's locator/groups desired by regInfo?
                if( interested(curStructVal.locator,curStructVal.groups,
                               regInfo.locators,regInfo.groups) )
                {
                    regsToAdd.put(reg,curStructVal);
		}//endif
	    }//endif
        }//end loop
        /* Add the registrars that are both desired and discovered to the
         * registration's map of discovered registrars. If any registrars
         * cannot be serialized, drop them.
         */
        HashMap regsAdded = regInfo.addToDiscoveredRegs(regsToAdd);
        /* Build and send a "discovered event" */
        RemoteDiscoveryEvent event = buildEvent(regInfo,regsAdded,false);
        if(event != null) {
            queueEvent(regInfo,event);
            logInfoEvents("NewReg/Discovered EventTask.run(): "
                          +"DISCOVERED Event was SENT\n");
        }//endif
    }//end maybeSendDiscoveredEvent

    /** 
     * Examines the <code>discardFlag</code> for each active registration
     * until a value of <code>true</code> is encountered or the set of
     * registrations is exhausted. If that flag is <code>false</code> for
     * all registrations, this method will return <code>null</code>;
     * otherwise, it will return the registration found, but with the
     * the value of the flag set back to <code>false</code>.
     *
     * @return the first instance of <code>RegistrationInfo</code> whose 
     *         <code>discardFlag</code> is set to <code>true</code>;
     *         returns <code>null</code> if no such registration exists.
     */
    private RegistrationInfo externalDiscardRequest() {
        /* Loop thru regInfo's, until one is found with a true flag*/
        for(Iterator itr=registrationByID.values().iterator();itr.hasNext();) {
            RegistrationInfo regInfo = (RegistrationInfo)itr.next();
            if(regInfo.discardFlag == true) {
                logInfoDiscard("\nexternalDiscardRequest: "
                               +"discardFlag == true\n");
                regInfo.discardFlag = false;
                return regInfo;
            }//endif
        }//end loop
        return null;
    }//end externalDiscardRequest

    /**
     * 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 HashMap mapRegToGroups(ServiceRegistrar reg, String[] groups) {
        HashMap groupsMap = new HashMap(1);
        groupsMap.put(reg,groups);
        return groupsMap;
    }//end mapRegToGroups

    /** Given a set of registrars that have just been discarded, this method
     *  determines which of those registrars are contained in none of the
     *  discovered sets of the active registrations; and then removes
     *  those registrars from the global maps of registrars that are
     *  maintained across all registrations.
     *
     *  Note that this method must be called from within a synchronization
     *  block.
     *
     * @param discardedRegs set of registrars that were just discarded
     */
    private void maybeRemoveDiscardedRegsFromGlobalSet(Set discardedRegs) {
        for(Iterator itr = discardedRegs.iterator();itr.hasNext(); ) {
            maybeRemoveDiscardedRegFromGlobalSet(itr.next());
        }//end loop(itr)
    }//end maybeRemoveDiscardedRegsFromGlobalSet

    /** Given a registrar that has just been discarded, this method
     *  determines if that registrar is contained in none of the discovered
     *  sets of the active registrations; and then removes that registrar
     *  from the global maps of registrars that are maintained across all
     *  registrations.
     *
     *  Note that this method must be called from within a synchronization
     *  block.
     *
     * @param discardedRegs set of registrars that were just discarded
     */
    private void maybeRemoveDiscardedRegFromGlobalSet(Object dReg) {
        for(Iterator jtr=registrationByID.values().iterator();jtr.hasNext();){
            RegistrationInfo regInfo = (RegistrationInfo)jtr.next();
            if( (regInfo.discoveredRegsMap).containsKey(dReg) ) {
                return; // this dReg is in at least 1 regInfo, goto next reg
            }//endif
        }//end loop(jtr)
        /* discarded reg contained in no regInfo, remove it from map */
        allDiscoveredRegs.remove(dReg);
    }//end maybeRemoveDiscardedRegFromGlobalSet

    /** For each registrar referenced in the global map allDiscoveredRegs,
     *  this method replaces the associated set of member groups with the
     *  corresponding set in the given registrars-to-groups map. This
     *  method should be called only after a changed event is received 
     *  from the discovery manager.
     *
     *  Note that this method must be called from within a synchronization
     *  block.
     *
     * @param groupsMap map containing the registrars and their new groups
     */
    private void updateGroupsInGlobalSet(Map groupsMap) {
        Set eSet = groupsMap.entrySet();
        for(Iterator itr = eSet.iterator(); itr.hasNext(); ) {
            Map.Entry pair = (Map.Entry)itr.next();
            ServiceRegistrar reg = (ServiceRegistrar)pair.getKey();
            if(allDiscoveredRegs.containsKey(reg)) {
                LookupLocator loc
                  = ((LocatorGroupsStruct)allDiscoveredRegs.get(reg)).locator;
                String[] newGroups = (String[])pair.getValue();
                LocatorGroupsStruct locGroups
                                     = new LocatorGroupsStruct(loc,newGroups);
                allDiscoveredRegs.put(reg,locGroups);
            }//endif
        }//end loop
    }//end updateGroupsInGlobalSet

    /**
     * This method constructs the appropriate remote discovery event from the
     * information contained in the input parameters. This method encapsulates
     * common exception-handling functionality. If the remote discovery
     * event cannot be successfully constructed, null will be returned.
     * 
     * @param regInfo   the data structure record corresponding to the 
     *                  registration whose listener will receive the event
     * @param groupsMap map containing the registrars, and their corresponding
     *                  member groups, to include in the event
     * @param discarded flag indicating whether the registrars included in
     *                  the event have been discarded or discovered
     *
     * @return instance of <code>RemoteDiscoveryEvent</code> containing the
     *         appropriate information corresponding to the input registration
     *         record
     */
    private RemoteDiscoveryEvent buildEvent(RegistrationInfo regInfo,
                                            Map groupsMap,
                                            boolean discarded)
    {
        RemoteDiscoveryEvent newEvent = null;
        if(groupsMap.size() > 0) {
            try {
                newEvent = new RemoteDiscoveryEvent(outerProxy,
                                                    regInfo.eventID,
                                                    ++regInfo.seqNum,
                                                    regInfo.handback,
                                                    discarded,
                                                    groupsMap);
                logInfoEvents(groupsMap,regInfo.eventID,regInfo.seqNum,
                              regInfo.handback,discarded,
                              eventsLogger, Level.FINE);
            } catch (IOException e) {
                /* The constructor for <code>RemoteDiscoveryEvent</code>
                 * throws an <code>IOException</code> if the constructor
                 * fails to successfully serialize even one registrar from
                 * the input set (which means there is no need to send the
                 * event since there are no marshalled registrars to send).
                 * When such a situation occurs, register an ERROR status
                 * attribute (along with a Comment attribute describing the
                 * nature of the problem) to all lookup services with which
                 * this service is registered. 
                 *
                 * Administrative clients, as well as clients that use this 
                 * service should have registered for notification of the 
                 * existence of this attribute.
                 */
                String eStr = "Failed to serialize ALL registrars during "
                              +"event construction ... could not send event";
                problemLogger.log(Level.INFO, eStr, e);
                Entry[] errorAttrs =
                new Entry[] { new FiddlerStatus(StatusType.WARNING),
                              new Comment(eStr)
                            };
                joinMgr.addAttributes(errorAttrs,true);
            }//end try
        }//endif
        return newEvent;
    }//end buildEvent

    /**
     * This method simply queues a new <code>SendEventTask</code> instance
     * that will send the given remote event to the given registration's
     * listener.
     * 
     * @param regInfo the data structure record corresponding to the 
     *                registration whose listener will receive the event
     * 
     * @param event   the instance of <code>RemoteDiscoveryEvent</code> to
     *                send to the registration's listener
     */
    private void queueEvent(RegistrationInfo regInfo,
                           RemoteDiscoveryEvent event)
    {
        taskMgr.add(new SendEventTask(regInfo,event));
    }//end queueEvent
    /* END Private Event-Related Methods ----------------------------------- */

    /* BEGIN Persistent State Logging Interfaces & Classes ----------------- */
    /**
     * Writes the current state of this service to persistent storage.
     * <p>
     * A 'snapshot' of the service's current state is represented by
     * the data contained in certain fields defined in the service. 
     * That data represents many changes -- over time -- to the service's
     * state. This method will record that data to a file referred to as
     * the snapshot file.
     * <p>
     * The data written by this method to the snapshot file -- as well as
     * the format of the file -- is shown below:
     * <ul>
     * <li> the service's class name
     * <li> log format version number
     * <li> the unique ID of the proxy to the current instance of this service
     * <li> the current event ID
     * <li> the current join state of this service
     * <li> the current configuration parameters of this service
     * <li> contents of the container holding the current set of registrations
     * <li> null (termination 'marker' for the set of registrations)
     * </ul>
     * Each data item is written to the snapshot file in serialized form.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private void takeSnapshot(OutputStream  out) throws IOException {
        ObjectOutputStream stream = new ObjectOutputStream(out);
        /* Stable information about the current instance of this service */
        stream.writeUTF(FiddlerImpl.class.getName());
        stream.writeInt(LOG_VERSION);
        stream.writeObject(proxyID);
        /* The current event ID assigned to the discovery events being sent */
        stream.writeLong(curEventID);
        /* The current join state of this service */
        stream.writeObject(thisServicesGroups);
        stream.writeObject(thisServicesLocators);
        stream.writeObject( marshalAttributes(this,thisServicesAttrs) );
        /* The current configuration parameters of this service */
        stream.writeLong(leaseBound);
        stream.writeInt(snapshotThresh);
        stream.writeFloat(snapshotWt);
        /* The current set of registrations */
        for(Iterator iter=registrationByID.values().iterator();iter.hasNext();)
        {
            stream.writeObject(iter.next());
        }//end loop
        stream.writeObject(null); //termination marker
        stream.flush();
    }//end takeSnapshot

    /**
     * Retrieve the contents of the snapshot file and reconstitute the 'base'
     * state of this service from the retrieved data.
     * <p>
     * Refer to <code>takeSnapshot</code> for a description of the data
     * retrieved by this method from the snapshot file.
     * <p>
     * During recovery, the state of the service at the time of a crash
     * or outage is re-constructed by first reconstituting the 'base state'
     * from the snapshot file; and then modifying that base state according
     * to the records retrieved from the log file. This method is invoked to
     * perform the first step in that reconstruction. As each registered
     * service or event is retrieved, it is inserted into its appropriate
     * container object.
     * <p>
     * Because events can be generated before the next snapshot is taken,
     * the event sequence numbers must be incremented. This is because the
     * event specification requires that a set of event sequence numbers
     * corresponding to an particular event type (a particular event ID)
     * be monotonically increasing. Since the number of events that might
     * have been sent is arbitrary, each sequence number will be incremented
     * by a 'large' number so as to guarantee adherence to the specification.
     * 
     * @see FiddlerImpl.LocalLogHandler#takeSnapshot
     * @see FiddlerImpl.LocalLogHandler
     */
    private void recoverSnapshot(InputStream in)
                                 throws IOException, ClassNotFoundException
    {
        ObjectInputStream stream = new ObjectInputStream(in);
        /* Retrieve the service's stable information */
        if (!FiddlerImpl.class.getName().equals(stream.readUTF())) {
            throw new IOException("log from wrong implementation");
        }//endif
        if (stream.readInt() != LOG_VERSION) {
            throw new IOException("wrong log format version");
        }//endif
        proxyID = (Uuid)stream.readObject();
        /* Retrieve the current event ID */
        curEventID = stream.readLong();
        /* Retrieve the current join state of this service. Groups first. */
        thisServicesGroups   = (String[])stream.readObject();
        /* Retrieve and re-prepare the locators to join (drop bad locs) */
        thisServicesLocators = prepareOldLocators
                                     ( recoveredLocatorToJoinPreparer,
                                       (LookupLocator[])stream.readObject() );
        /* Retrieve the attributes to register with each lookup service. */
        MarshalledObject[] marshalledAttrs
                                    = (MarshalledObject[])stream.readObject();
        thisServicesAttrs = unmarshalAttributes(this, marshalledAttrs);
        /* Retrieve the current configuration parameters of this service */
        leaseBound     = stream.readLong();
        snapshotThresh = stream.readInt();
        snapshotWt     = stream.readFloat();
        RegistrationInfo regInfo;
        while ((regInfo = (RegistrationInfo)stream.readObject()) != null) {
            regInfo.seqNum += Integer.MAX_VALUE;
            addRegistration(regInfo);
        }//end loop
        initialStartup = false;
    }//end recoverSnapshot

    /**
     * Add a state-change record to persistent storage.
     * <p>
     * Whenever a significant change occurs to Fiddler's state, this
     * method is invoked to record that change in a file called a log file.
     * Each record written to the log file is an object reflecting both
     * the data used and the ACTIONS taken to make one change to that
     * state at a particular point in time. If the number of records
     * contained in the log file exceeds the pre-defined threshold,
     * a snapshot of Fiddler's current state will be recorded.
     * <p>
     * Whenever one of the following state changes occurs, this method
     * will be invoked with the appropriate implementation of the
     * LogRecord interface as the input argument.
     * <ul>
     * <li> new attributes were added to Fiddler's attributes with lookup
     * <li> Fiddler's existing attributes with lookup were modified
     *
     * <li> new groups were added to the set of groups Fiddler should join
     * <li> Fiddler's existing groups to join were replaced with a new set
     * <li> Fiddler's existing groups to join were removed
     *
     * <li> new locators were added to the set of locators Fiddler should join
     * <li> Fiddler's existing locators to join were replaced with a new set
     * <li> Fiddler's existing locators to join were removed
     *
     * <li> the bound on granted lease durations was set to a new value
     * <li> weight factor used to determine when to take a snapshot was changed
     * <li> threshold used to determine when to take a snapshot was changed
     *
     * <li> a registration request for use of Fiddler's resources was granted
     *
     * <li> groups were added to a registration's set of groups to discover
     * <li> the set of groups to discover for a registration was replaced
     * <li> the set of groups to discover for a registration was removed
     *
     * <li> locators were added to a registration's set of locators to discover
     * <li> the set of locators to discover for a registration was replaced
     * <li> the set of locators to discover for a registration was removed
     *
     * <li> a lease on a registration with Fiddler was renewed
     * <li> a set of registration leases with Fiddler were renewed
     * <li> a lease on a registration with Fiddler was cancelled
     * <li> a set of registration leases with Fiddler were cancelled
     *
     * </ul>
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private void addLogRecord(LogRecord rec) {
        if(log == null) return;//not persistent, don't log

        logInfoAddLogRecord(rec);
        try {
            log.update(rec, true);
            if (++logFileSize >= snapshotThresh) {
                int snapshotSize = registrationByID.size();
                if ((float)logFileSize >= snapshotWt*((float)snapshotSize)) {
                    /* take snapshot */
                    concurrentObj.waiterNotify(snapshotThreadSyncObj);
                }//endif
            }//endif
        } catch (Exception e) {
            if (!Thread.currentThread().isInterrupted()) {
                /* If log updating fails, then register an ERROR status
                 * attribute (along with a Comment attribute describing the
                 * nature of the problem) to all lookup services with which
                 * this service is registered. 
                 *
                 * Administrative clients, as well as clients that use this
                 * service should have registered for notification of the
                 * existence of this attribute.
                 */
                String eStr = "Failure while updating the persistent log "
                              +"containing the service state";
                problemLogger.log(Level.INFO, eStr, e);
                Entry[] errorAttrs = new Entry[]
                      { new FiddlerStatus(StatusType.ERROR),
                        new Comment(eStr)
                      };
                joinMgr.addAttributes(errorAttrs,true);
            }//endif
        }
    }//end addLogRecord

    /**
     * Interface defining the method(s) that must be implemented by each of
     * the concrete LogObj classes. This allows for the definition of
     * object-dependent invocations of the appropriate implementation of
     * the method(s) declared in this interface.
     */
    private static interface LogRecord extends Serializable {
        void apply(FiddlerImpl fiddlerImpl);
    }

    /* 1. Definitions related to state changes generated by JoinAdmin */

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * new attributes are added to this service's existing set of attributes.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class LookupAttrsAddedLogObj implements LogRecord {
        private static final long serialVersionUID = 4983778026976938585L;
        /** The attributes that were added to each lookup service with which
         *  this service is registered, written out in marshalled form.
         *  @serial
         */
        private MarshalledObject[] marshalledAttrs;
        /** Constructs this class and stores the attributes that were added */
        public LookupAttrsAddedLogObj(FiddlerImpl fiddlerImpl, Entry[] attrs) {
            this.marshalledAttrs = marshalAttributes(fiddlerImpl,attrs);
        }
        /** Modifies this service's state by adding (after unmarshalling) the
         *  elements of marshalledAttrs to the service's existing set of
         *  attributes.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply adding lookup attributes");
            Entry[] attrs = unmarshalAttributes(fiddlerImpl, marshalledAttrs);
            fiddlerImpl.thisServicesAttrs 
                  = LookupAttributes.add(fiddlerImpl.thisServicesAttrs,attrs);
        }
    }//end LookupAttrsAddedLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * the attributes currently associated with this service in the lookup
     * services with which it is registered are modified.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class LookupAttrsModifiedLogObj implements LogRecord {
        private static final long serialVersionUID = 2671139518188470469L;
	/** The attribute set templates used to select the attributes (from
         *  the service's existing set of attributes) that were modified,
         *  written out in marshalled form.
	 *  @serial
	 */
        private MarshalledObject[] marshalledAttrTmpls;
        /** The attributes with which this service's existing attributes 
         *  were modified, written out in marshalled form.
         *  @serial
         */
        private MarshalledObject[] marshalledModAttrs;
        /** Constructs this class and stores the modified attributes */
        public LookupAttrsModifiedLogObj(FiddlerImpl fiddlerImpl,
                                         Entry[] attrTmpls,
                                         Entry[] modAttrs)
        {
            this.marshalledAttrTmpls = marshalAttributes
                                                       (fiddlerImpl,attrTmpls);
            this.marshalledModAttrs = marshalAttributes(fiddlerImpl,modAttrs);
        }
        /** Modifies this service's state by modifying (after unmarshalling)
         *  the service's existing attributes according to the contents of
         *  marshalledAttrTmpls and marshalledModAttrs.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply modifying lookup attributes");
            Entry[] attrTmpls = unmarshalAttributes
                                            (fiddlerImpl, marshalledAttrTmpls);
            Entry[] modAttrs  = unmarshalAttributes
                                            (fiddlerImpl, marshalledModAttrs);
            fiddlerImpl.thisServicesAttrs 
                       = LookupAttributes.modify(fiddlerImpl.thisServicesAttrs,
                                                 attrTmpls, modAttrs);
        }
    }//end LookupAttrsModifiedLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * the set containing the names of the groups whose members are lookup
     * services the lookup discovery service wishes to register with (join)
     * is modified in some way; for example, through the invocation of:
     * <code>JoinAdmin.addLookupGroups</code>,
     * <code>JoinAdmin.removeLookupGroups</code> or
     * <code>JoinAdmin.setLookupGroups</code>.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class LookupGroupsChangedLogObj implements LogRecord {
        private static final long serialVersionUID = -6973676200404539919L;
        /** The new groups whose members are the lookup services this
         *  service should join.
         *  @serial
         */
        private String[] groups;
	/** Constructs this class and stores the new groups */
        public LookupGroupsChangedLogObj(String[] groups) {
            this.groups = groups;
        }
        /** Modifies this service's state by modifying this service's existing
         *  set of 'groups to join'.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist
                       ("Log recovery: apply changing lookup groups to join");
            fiddlerImpl.thisServicesGroups = groups; 
        }
    }//end LookupGroupsChangedLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * the set containing the instances of <code>LookupLocator</code> that
     * correspond to specific lookup services the lookup discovery service
     * wishes to register with (join) is modified in some way; for example,
     * through the invocation of:
     * <code>JoinAdmin.addLookupLocators</code>,
     * <code>JoinAdmin.removeLookupLocators</code> or
     * <code>JoinAdmin.setLookupLocators</code>.
     *
     *  @see FiddlerImpl.LocalLogHandler
     */
    private static class LookupLocatorsChangedLogObj implements LogRecord {
        private static final long serialVersionUID = 6448427261140043291L;
        /** The locators that correspond to the new lookup services this
         *  service should join.
         *  @serial
         */
        private LookupLocator[] locators;
        /** Constructs this class and stores the new locators */
        public LookupLocatorsChangedLogObj(LookupLocator[] locators) {
            this.locators = locators;
        }
        /** Modifies this service's state by modifying this service's existing
         *  set of 'locators to join'.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist
                     ("Log recovery: apply changing lookup locators to join");
            fiddlerImpl.thisServicesLocators =
                  prepareOldLocators(recoveredLocatorToJoinPreparer,locators);
        }
    }//end LookupLocatorsChangedLogObj

    /* 2. Definitions related to state changes generated by FiddlerAdmin */

    /**
     * LogObj class whose instances are recorded to the log file whenever a
     * new value is assigned to the least upper bound applied during the
     * determination of the duration of the lease on a requested registration.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class LeaseBoundSetLogObj implements LogRecord {
        private static final long serialVersionUID = 6084059678114714281L;
        /** The new least upper bound used to determine a lease's duration.
         *  @serial
         */
        private long newLeaseBound;
        /** Constructs this class and stores the new lease duration bound */
        public LeaseBoundSetLogObj(long newLeaseBound) {
            this.newLeaseBound = newLeaseBound;
        }
        /** Modifies this service's state by setting the value of the private
         *  leaseBound field to the value of the new bound.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist
                         ("Log recovery: apply changing lease duration bound");
            fiddlerImpl.leaseBound = newLeaseBound; 
        }
    }//end LeaseBoundSetLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever a
     * new value is assigned to the Weight Factor applied to the Snapshot File
     * size used in the "time-to-take-a-snapshot determination" expression.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class SnapshotWeightSetLogObj implements LogRecord {
        private static final long serialVersionUID = -4079318959709953285L;
        /** The new snapshot weight factor.
         * @serial
         */
        private float newWeight;
	/** Constructs this class and stores the new weight factor */
        public SnapshotWeightSetLogObj(float newWeight) {
            this.newWeight = newWeight;
        }
        /** Modifies this service's state by setting the value of the private
         *  snapshotWt field to the value of the new weight factor.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist
                      ("Log recovery: apply changing snapshot weight factor");
            fiddlerImpl.snapshotWt = newWeight; 
        }
    }//end SnapshotWeightSetLogObj

    /**
     * LogObj class whose instances are recorded to the log file 
     * whenever a new value is assigned to the Threshold used in the
     * "time-to-take-a-snapshot determination" expression.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class SnapshotThresholdSetLogObj implements LogRecord {
        private static final long serialVersionUID = 2952948867001823559L;
        /** The new snapshot threshold.
         *  @serial
         */
        private int newThreshold;
        /** Constructs this class and stores the new snapshot threshold */
        public SnapshotThresholdSetLogObj(int newThreshold) {
            this.newThreshold = newThreshold;
        }
        /** Modifies this service's state by setting the value of the private
         *  snapshotThresh field to the value of the new threshold.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist
                    ("Log recovery: apply changing log-to-snapshot threshold");
            fiddlerImpl.snapshotThresh = newThreshold; 
        }
    }//end SnapshotThresholdSetLogObj

    /* 3. Definitions related to state changes made to the set of active
     *    registrations, generated by client requests made through the
     *    <code>LookupDiscoveryRegistration</code> interface.
     */
    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a registration is created and returned to a client.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class RegistrationGrantedLogObj implements LogRecord {
        private static final long serialVersionUID = 3983963008572188308L;
        /** Object which acts as a record of the current registration with the
         *  lookup discovery service; containing all of the information about
         *  that registration: IDs, managed sets, lease information, and
         *  event registration information.
         *  @serial
         */
        private RegistrationInfo regInfo;
        /** Constructs this class and stores the registration information */
        public RegistrationGrantedLogObj(RegistrationInfo regInfo) {
            this.regInfo = regInfo;
        }
        /** Modifies this service's state by registering the information
         *  stored in the regInfo parameter; and by updating both the event
         *  sequence number and the event ID for the registration.
         *
         *  Note that the granting of a registration to a client typically
         *  involves the modification of the managed sets in the discovery
         *  manager, which usually involves starting the discovery protocol.
         *  Since an IOException can occur when the discovery protocol fails
         *  to start, and since such a situation is un-recoverable, this
         *  method does the following: catches the exception, informs this
         *  service's administrator by displaying the stack trace, and exits.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply recording granted registration"
                           +" info\n              (ID = "
                           +regInfo.registrationID+")");
            regInfo.seqNum += Integer.MAX_VALUE;
            try {
                /* Note that the locators of this recovered registration
                 * were already successfully prepared in the method
                 * RegistrationInfo.readObject() when this object was
                 * deserialized during recovery.
                 */
                fiddlerImpl.addRegistration(regInfo);
            } catch(IOException e) {
                if( problemLogger.isLoggable(Level.SEVERE) ) {
                    problemLogger.log(Level.SEVERE, "During log recovery "
                                      +"(apply addRegistration) -- failure in "
                                      +"multicast request protocol\n", e);
                }//endif
                fiddlerImpl.destroyDo();
            }
            fiddlerImpl.curEventID++;
        }
    }//end RegistrationGrantedLogObj

    /* 4. Definitions related to state changes made to the set of active
     *    registrations, generated by client requests made through the
     *    <code>LookupDiscoveryService</code> interface.
     */

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * the managed set of groups corresponding to a registration is 
     * is augmented with new elements.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class GroupsAddedToRegistrationLogObj implements LogRecord {
        private static final long serialVersionUID = 2L;
        /** The ID of the data structure record corresponding to the
         *  registration whose managed set of groups was augmented.
         *  @serial
         */
        private Uuid registrationID;
        /** The set of groups added to the registration's managed set of groups
         *  @serial
         */
        private String[] groups;
        /** Constructs this class and stores the ID and new set of groups */
        public GroupsAddedToRegistrationLogObj(Uuid registrationID,
                                               String[] groups)
        {
            this.registrationID = registrationID;
            this.groups         = groups;
        }
        /** Modifies this service's state by adding the set of group names to
         *  registration's managed set of groups, as well as by updating the
         *  set of all groups (across all registrations) to discover.
         *
         *  Note that the augmentation of a registration's set of groups
         *  typically involves the modification of the managed sets in the
         *  discovery manager, which usually involves starting the discovery
         *  protocol. Since an IOException can occur when the discovery
         *  protocol fails to start, and since such a situation is
         *  un-recoverable, this method does the following: catches the
         *  exception, informs this service's administrator by displaying
         *  the stack trace, and exits.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply adding to groups to discover "
                           +"for registration\n              (ID = "
                           +registrationID+")");
            try {
                fiddlerImpl.addGroupsDo(registrationID,
                                        fiddlerImpl.registrationByID,
                                        groups);
            } catch(Exception e) {
                if( problemLogger.isLoggable(Level.SEVERE) ) {
                    problemLogger.log(Level.SEVERE, "During log recovery "
                                      +"(apply addGroupsDO) -- failure in "
                                      +"multicast request protocol\n", e);
                }//endif
                fiddlerImpl.destroyDo();
            }
        }//end apply
    }//end GroupsAddedToRegistrationLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a the managed set of groups corresponding to a registration is 
     * is replaced (set) with a new set of group names.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class GroupsSetInRegistrationLogObj implements LogRecord {
        static final long serialVersionUID = 2L;
        /** The ID of the data structure record corresponding to the
         *  registration whose managed set of groups was replaced.
         *  @serial
         */
        private Uuid registrationID;
        /** The set of groups that replaced the registration's current
         *  managed set of groups.
         *  @serial
         */
        private String[] groups;
        /** Constructs this class and stores the ID and new set of groups */
        public GroupsSetInRegistrationLogObj(Uuid registrationID,
                                             String[] groups)
        {
            this.registrationID = registrationID;
            this.groups         = groups;
        }
        /** Modifies this service's state by replacing the registration's
         *  current managed set of groups with the set of group names 
         *  stored in this class by the constructor, as well as by updating
         *  the set of all groups (across all registrations) to discover.
         *
         *  Note that the replacement of a registration's set of groups
         *  typically involves the modification of the managed sets in the
         *  discovery manager, which usually involves starting the discovery
         *  protocol. Since an IOException can occur when the discovery
         *  protocol fails to start, and since such a situation is
         *  un-recoverable, this method does the following: catches the
         *  exception, informs this service's administrator by displaying
         *  the stack trace, and exits.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply replacing groups to discover "
                           +"for registration\n              (ID = "
                           +registrationID+") ...");
            try {
                fiddlerImpl.setGroupsDo(registrationID,
                                        fiddlerImpl.registrationByID,
                                        groups);
            } catch(Exception e) {
                if( problemLogger.isLoggable(Level.SEVERE) ) {
                    problemLogger.log(Level.SEVERE, "Failure during log "
                                      +"recovery (apply setGroups) -- \n", e);
                }//endif
                fiddlerImpl.destroyDo();
            }
        }
    }//end GroupsSetInRegistrationLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * one or more elements of the managed set of groups corresponding to a
     * registration are removed.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class GroupsRemovedFromRegistrationLogObj
                                                        implements LogRecord
    {
        private static final long serialVersionUID = 2L;
        /** The ID of the data structure record corresponding to the
         *  registration with managed set from which groups were removed.
         *  @serial
         */
        private Uuid registrationID;
        /** The set of groups removed from the registration's managed set 
         *  of groups.
         *  @serial
         */
        private String[] groups;
        /** Constructs this class and stores the ID and groups to remove */
        public GroupsRemovedFromRegistrationLogObj(Uuid registrationID,
                                                   String[] groups)
        {
            this.registrationID = registrationID;
            this.groups         = groups;
        }
        /** Modifies this service's state by removing the set of group names
         *  from registration's managed set of groups, as well as by updating
         *  the set of all groups (across all registrations) to discover.
         *
         *  Note that the removal of one or more group names from a
         *  registration's set of groups typically involves the modification 
         *  of the managed sets in the discovery manager, which usually 
         *  involves starting the discovery protocol. Since an IOException 
         *  can occur when the discovery protocol fails to start, and since 
         *  such a situation is un-recoverable, this method does the 
         *  following: catches the exception, informs this service's 
         *  administrator by displaying the stack trace, and exits.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply removing from groups to "
                           +"discover for registration\n              (ID = "
                           +registrationID+")");
            try {
                fiddlerImpl.removeGroupsDo(registrationID,
                                           fiddlerImpl.registrationByID,
                                           groups);
	    } catch(Exception e) {
                if( problemLogger.isLoggable(Level.SEVERE) ) {
                    problemLogger.log(Level.SEVERE, "Failure during log "
                                    +"recovery (apply removeGroups) -- \n", e);
                }//endif
	        fiddlerImpl.destroyDo();
	    }
        }
    }//end GroupsRemovedFromRegistrationLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * the managed set of locators corresponding to a registration is 
     * is augmented with new elements.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class LocsAddedToRegistrationLogObj implements LogRecord {
        private static final long serialVersionUID = 2L;
        /** The ID of the data structure record corresponding to the
         *  registration whose managed set of locators was augmented.
         *  @serial
         */
        private Uuid registrationID;
        /** The set of locators added to the registration's managed set
         *  @serial
         */
        private LookupLocator[] locators;
        /** Constructs this class and stores the ID and new set of locators */
        public LocsAddedToRegistrationLogObj(Uuid registrationID,
                                             LookupLocator[] locators)
        {
            this.registrationID = registrationID;
            this.locators       = locators;
        }
        /** Modifies this service's state by adding the set of locators to
         *  registration's managed set of locators, as well as by updating the
         *  set of all locators (across all registrations) to discover.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply adding to locators to discover"
                           +" for registration\n              (ID = "
                           +registrationID+")");
            int nUnprepared = locators.length;
            /* Prepare the recovered locators */
            locators = 
              prepareOldLocators(recoveredLocatorToDiscoverPreparer, locators);
            /* If all the locs were successfully prepared, add them to the
             * associated registration; otherwise, remove the registration
             * from the managed set. (For more information, see the comment
             * in RegistrationInfo.readObject()).
             */
            if(nUnprepared == locators.length) {
                fiddlerImpl.addLocatorsDo( registrationID,
                                           fiddlerImpl.registrationByID,
                                           locators );
            } else {
                if( problemLogger.isLoggable(Level.WARNING) ) {
                    problemLogger.log(Level.WARNING, "failure preparing "
                                      +"locator while recovering "
                                      +"LocsAddedToRegistrationLogObj "
                                      +"... removing registration with ID = "
                                      +registrationID);
                }//endif
                try {
                    fiddlerImpl.removeRegistration
                       ( (RegistrationInfo)(fiddlerImpl.registrationByID.get
                                                           (registrationID)) );
                } catch(IOException e) {
                    String eStr = "failure removing registration (ID = "
                                  +registrationID
                                  +") after locator preparation failure";
                    if( problemLogger.isLoggable(Level.INFO) ) {
                        problemLogger.log(Level.INFO, eStr, e);
                    }//endif
                    Entry[] errorAttrs
                            = new Entry[]
                                        { new FiddlerStatus(StatusType.ERROR),
                                          new Comment(eStr)
                                        };
                    fiddlerImpl.joinMgr.addAttributes(errorAttrs,true);
                }
            }//endif
        }//end apply
    }//end LocsAddedToRegistrationLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * the managed set of locators corresponding to a registration is 
     * is replaced (set) with a new set of locators.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class LocsSetInRegistrationLogObj implements LogRecord {
        private static final long serialVersionUID = 2L;
        /** The ID of the data structure record corresponding to the
         *  registration whose managed set of locators was replaced.
         *  @serial
         */
        private Uuid registrationID;
        /** The set of locators that replaced the registration's current
         *  managed set of locators.
         *  @serial
         */
        private LookupLocator[] locators;
        /** Constructs this class and stores the ID and new set of locators */
        public LocsSetInRegistrationLogObj(Uuid registrationID,
                                           LookupLocator[] locators)
        {
            this.registrationID = registrationID;
            this.locators       = locators;
        }
        /** Modifies this service's state by replacing the registration's
         *  current managed set of locators with the set of locators 
         *  stored in this class by the constructor, as well as by updating
         *  the set of all locators (across all registrations) to discover.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply replacing locators to discover"
                           +" for registration\n              (ID = "
                           +registrationID+")");
            int nUnprepared = locators.length;
            /* Prepare the recovered locators */
            locators = 
              prepareOldLocators(recoveredLocatorToDiscoverPreparer, locators);
            /* If all the locs were successfully prepared, set them in the
             * associated registration; otherwise, remove the registration
             * from the managed set. (For more information, see the comment
             * in RegistrationInfo.readObject()).
             */
            if(nUnprepared == locators.length) {
                fiddlerImpl.setLocatorsDo( registrationID,
                                           fiddlerImpl.registrationByID,
                                           locators );
            } else {
                if( problemLogger.isLoggable(Level.WARNING) ) {
                    problemLogger.log(Level.WARNING,
                                      "failure preparing locator while "
                                      +"recovering LocsSetInRegistrationLogObj"
                                      +" ... removing registration with ID = "
                                      +registrationID);
                }//endif
                try {
                    fiddlerImpl.removeRegistration
                       ( (RegistrationInfo)(fiddlerImpl.registrationByID.get
                                                           (registrationID)) );
                } catch(IOException e) {
                    String eStr = "failure removing registration (ID = "
                                  +registrationID
                                  +") after locator preparation failure";
                    if( problemLogger.isLoggable(Level.WARNING) ) {
                        problemLogger.log(Level.WARNING, eStr, e);
                    }//endif
                    Entry[] errorAttrs
                            = new Entry[]
                                        { new FiddlerStatus(StatusType.ERROR),
                                          new Comment(eStr)
                                        };
                    fiddlerImpl.joinMgr.addAttributes(errorAttrs,true);
                }
            }//endif
        }
    }//end LocsSetInRegistrationLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * one or more elements of the managed set of locators corresponding to a
     * registration are removed.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class LocsRemovedFromRegistrationLogObj
                                                         implements LogRecord 
    {
        private static final long serialVersionUID = 2L;
        /** The ID of the data structure record corresponding to the
         *  registration with managed set from which locators were removed.
         *  @serial
         */
        private Uuid registrationID;
        /** The set of locators removed from the registration's managed set 
         *  of locators.
         *  @serial
         */
        private LookupLocator[] locators;
        /** Constructs this class and stores the ID and locators to remove */
        public LocsRemovedFromRegistrationLogObj(Uuid registrationID,
                                                 LookupLocator[] locators)
        {
            this.registrationID = registrationID;
            this.locators       = locators;
        }
        /** Modifies this service's state by removing the set of locators
         *  from registration's managed set of locators, as well as by updating
         *  the set of all locators (across all registrations) to discover.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply removing from locators to "
                           +"discover for registration\n              (ID = "
                           +registrationID+") ...");
            int nUnprepared = locators.length;
            /* Prepare the recovered locators */
            locators = 
              prepareOldLocators(recoveredLocatorToDiscoverPreparer, locators);
            /* If all the locs were successfully prepared, remove them from
             * the associated registration; otherwise, remove the registration
             * from the managed set. (For more information, see the comment
             * in RegistrationInfo.readObject()).
             */
            if(nUnprepared == locators.length) {
                fiddlerImpl.removeLocatorsDo( registrationID,
                                              fiddlerImpl.registrationByID,
                                              locators );
            } else {
                if( problemLogger.isLoggable(Level.WARNING) ) {
                    problemLogger.log(Level.WARNING, "failure preparing "
                                      +"locator while recovering"
                                      +"LocsRemovedFromRegistrationLogObj "
                                      +"... removing registration with ID = "
                                      +registrationID);
                }//endif
                try {
                    fiddlerImpl.removeRegistration
                       ( (RegistrationInfo)(fiddlerImpl.registrationByID.get
                                                           (registrationID)) );
                } catch(IOException e) {
                    String eStr = "failure removing registration (ID = "
                                  +registrationID
                                  +") after locator preparation failure";
                    if( problemLogger.isLoggable(Level.WARNING) ) {
                        problemLogger.log(Level.WARNING, eStr, e);
                    }//endif
                    Entry[] errorAttrs
                            = new Entry[]
                                        { new FiddlerStatus(StatusType.ERROR),
                                          new Comment(eStr)
                                        };
                    fiddlerImpl.joinMgr.addAttributes(errorAttrs,true);
                }
            }//endif
        }
    }//end LocsRemovedFromRegistrationLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a lease on an existing registration (granted by the current backend
     * server of the lookup discovery service -- the lease grantor) is renewed.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class LeaseRenewedLogObj implements LogRecord {
        private static final long serialVersionUID = 2L;
        /** The ID of the data structure record corresponding to the
         *  registration whose lease was renewed.
         *  @serial
         */
        private Uuid registrationID;
        /** The identifier assigned by the lease grantor to the lease that was
         *  renewed.
         *  @serial
         */
        private Uuid leaseID;
        /** The new absolute time of expiration of the lease that was renewed.
         *  @serial
         */
        private long expiration;
        /** Constructs this class and stores the IDs and the expiration time */
        public LeaseRenewedLogObj(Uuid registrationID,
                                  Uuid leaseID,
                                  long expiration)
        {
            this.registrationID = registrationID;
            this.leaseID        = leaseID;
            this.expiration     = expiration;
        }
        /** Modifies this service's state by renewing the lease with ID equal
         *  to this class' leaseID field, and which corresponds to the 
         *  regInfo record. The lease will be renewed to have a new expiration
         *  time equal to the value of the expiration field.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply renewing lease for "
                           +"registration\n              (ID = "
                           +registrationID+")");
            fiddlerImpl.renewLeaseAbs(registrationID, 
                                      fiddlerImpl.registrationByID,
                                      leaseID, expiration);
        }
    }//end LeaseRenewedLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a set of leases from a <code>LeaseMap</code> are renewed.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class LeasesRenewedLogObj implements LogRecord {
        private static final long serialVersionUID = 2L;
        /** The set of unique identifiers each assigned to a registration that
         *  corresponds to one of the leases that was renewed.
         *  @serial
         */
        private Uuid[] registrationIDs;
        /** The set of identifiers each assigned by the lease grantor to one
         *  of the leases that was renewed.
         *  @serial
         */
        private Uuid[] leaseIDs;
        /** The set of new absolute expiration times of each lease that was
         *  renewed.
         *  @serial
         */
        private long[] expirations;
        /** Constructs this class and stores the sets of IDs and the set of
         *  expiration times.
         */
        public LeasesRenewedLogObj(Uuid[] registrationIDs,
                                   Uuid[] leaseIDs,
                                   long[] expirations)
        {
            this.registrationIDs = registrationIDs;
            this.leaseIDs        = leaseIDs;
            this.expirations     = expirations;
        }
        /** Modifies this service's state by renewing, with the corresponding
         *  expiration time, each of the leases specified by the stored IDs.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply renewing leases corresponding "
                           +"to "+registrationIDs.length
                           +" registration IDs");
            fiddlerImpl.renewLeasesAbs(registrationIDs, leaseIDs, expirations);
        }
    }//end LeasesRenewedLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a lease on an existing registration (granted by the current backend
     * server of the lookup discovery service -- the lease grantor) is
     * cancelled.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class LeaseCancelledLogObj implements LogRecord {
        private static final long serialVersionUID = 2L;
        /** The ID of the data structure record corresponding to the
         *  registration whose lease was cancelled.
         *  @serial
         */
        private Uuid registrationID;
        /** The identifier assigned by the lease grantor to the lease that was
         *  cancelled.
         *  @serial
         */
        private Uuid leaseID;
        /** Constructs this class and stores the IDs corresponding to the
         *  lease that was cancelled.
         */
        public LeaseCancelledLogObj(Uuid registrationID,
                                    Uuid leaseID) {
            this.registrationID = registrationID;
            this.leaseID        = leaseID;
        }
        /** Modifies this service's state by canceling the lease on the
         *  registration with ID equal to that stored by the constructor,
         *  as well as by updating the managed set of groups and locators 
         *  (across all registrations) in the appropriate way.
         *
         *  Note that the cancellation of a lease typically involves the
         *  modification of the managed sets in the discovery manager, which
         *  usually involves starting the discovery protocol. Since an
         *  IOException can occur when the discovery protocol fails to start,
         *  and since such a situation is un-recoverable, this method does
         *  the following: catches the exception, informs this service's
         *  administrator by displaying the stack trace, and exits.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            try {
                logInfoPersist("Log recovery: apply cancelling lease for "
                               +"registration\n              (ID = "
                               +registrationID+")");
                fiddlerImpl.cancelLeaseDo(registrationID, 
                                          fiddlerImpl.registrationByID,
                                          leaseID);
            } catch(IOException e) {
                if( problemLogger.isLoggable(Level.SEVERE) ) {
                    problemLogger.log(Level.SEVERE, "During log recovery "
                                      +"(apply cancelLease) -- failure in "
                                      +"multicast request protocol\n", e);
                }//endif
                fiddlerImpl.destroyDo();
            } catch (UnknownLeaseException e) {
                /* this exception should never occur when recovering */
            }
        }
    }//end LeaseCancelledLogObj

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a set of leases from a <code>LeaseMap</code> are cancelled.
     * 
     * @see FiddlerImpl.LocalLogHandler
     */
    private static class LeasesCancelledLogObj implements LogRecord {
        private static final long serialVersionUID = 2L;
        /** The set of unique identifiers each assigned to a registration that
         *  corresponds to one of the leases that was cancelled.
         *  @serial
         */
        private Uuid[] registrationIDs;
        /** The set of identifiers each assigned by the lease grantor to one
         *  of the leases that was cancelled.
         *  @serial
         */
        private Uuid[] leaseIDs;
        /** Constructs this class and stores the IDs corresponding to the
         *  leases that were cancelled.
         */
        public LeasesCancelledLogObj(Uuid[] registrationIDs,
                                     Uuid[] leaseIDs)
        {
            this.registrationIDs = registrationIDs;
            this.leaseIDs        = leaseIDs;
        }
        /** Modifies this service's state by canceling each of the leases
         *  specified by the stored IDs.
         *  @see FiddlerImpl.LocalLogHandler#applyUpdate
         */
        public void apply(FiddlerImpl fiddlerImpl) {
            logInfoPersist("Log recovery: apply cancelling leases "
                           +"corresponding to "+registrationIDs.length
                           +" registration IDs");
            /* Because unknown leases were not weeded out, exceptions can
             * occur and be propagated upward, but they can be ignored.
             */
            fiddlerImpl.cancelLeasesDo(registrationIDs, leaseIDs);
        }
    }//end LeasesCancelledLogObj
    /* END Persistent State Logging Interfaces & Classes ------------------- */

    /* BEGIN Private Logging Facility Methods ------------------------------ */
    /* Returns a String containing the elements of the array */
    private static String writeArrayElementsToString(Object[] arr) {
        if(arr == null) return new String("[]");
        if(arr.length <= 0) {
            return new String("[]");
        }//endif
        StringBuffer strBuf = new StringBuffer("["+arr[0]);
        for(int i=1;i<arr.length;i++){
            strBuf.append(", ").append(arr[i]);
        }//end loop
        strBuf.append("]");
        return strBuf.toString();
    }//end writeArrayElementsToString

    /* Logs the elements of the array to a single line of output */
    private static void writeArrayElements(Object[] arr,
                                           Logger   logger,
                                           Level    level)
    {
        if((arr == null) || (logger == null) || !(logger.isLoggable(level))) {
            return;
        }
        String writeStr = writeArrayElementsToString(arr);
        logger.log(level,writeStr);
    }//end writeArrayElements

    /* Returns a String containing the group names in writable form */
    private static String writeGroupArrayToString(String[] groups) {
        if(groups == null) {
            return new String("[ALL_GROUPS]");
        }//endif
        if(groups.length <= 0) {
            return new String("[]");
        }//endif
        StringBuffer strBuf = null;
        if(groups[0].compareTo("") == 0) {
            strBuf = new StringBuffer("[The PUBLIC Group");
        } else {
            strBuf = new StringBuffer("["+groups[0]);
        }//endif
        for(int i=1;i<groups.length;i++) {
            if(groups[i].compareTo("") == 0) {
                strBuf.append(", The PUBLIC Group");
            } else {
                strBuf.append(", ").append(groups[i]);
            }//endif
        }//end loop
        strBuf.append("]");
        return strBuf.toString();
    }//end writeGroupArrayToString

    /* Logs the group names to a single line of output */
    private static void writeGroupArray(String[] groups,
                                        Logger   logger,
                                        Level    level)
    {
        if( (logger == null) || !(logger.isLoggable(level)) ) return;
        String writeStr = writeGroupArrayToString(groups);
        logger.log(level,writeStr);
    }//end writeGroupArray

    /* Logs each registrar's locator on a single line of output */
    private static void writeRegistrarsArray(ServiceRegistrar[] regs,
                                             Logger             logger,
                                             Level              level)
    {
        if((regs == null) || (logger == null) || !(logger.isLoggable(level))) {
            return;
        }
        if(regs.length == 0) {
            logger.log(level, "[NO REGISTRARS for Event]");
        }//endif
        if(regs.length == 1) {
            try{
                LookupLocator loc = regs[0].getLocator();
                logger.log(level, "["+loc+"]");
            }catch(SecurityException e){
                logger.log(level, "[SecurityException]");
            }catch(Exception e){
                logger.log(level, "[Exception]");
            }
        }//endif(regs.length == 1)
        if(regs.length > 1) {
            try{
                LookupLocator loc = regs[0].getLocator();
                logger.log(level, "["+loc+",");
            }catch(SecurityException e){
                logger.log(level, "[SecurityException,");
            }catch(Exception e){
                logger.log(level, "[Exception,");
            }
            for(int i=1;i<(regs.length-1);i++){
                try{
                    LookupLocator loc = regs[i].getLocator();
                    logger.log(level, loc+",");
                }catch(SecurityException e){
                    logger.log(level, "SecurityException,");
                }catch(Exception e){
                    logger.log(level, "Exception,");
                }
            }//end loop
            try{
                LookupLocator loc = regs[regs.length-1].getLocator();
                logger.log(level, loc+"]");
            }catch(SecurityException e){
                logger.log(level, "SecurityException]");
            }catch(Exception e){
                logger.log(level, "Exception]");
            }
        }//endif(regs.length > 1)
    }//end writeRegistrarsArray

    /* Logs data on a single attribute to a file or standard output */
    private static void writeAttribute(Entry  attr,
                                       Logger logger,
                                       Level  level)
    {
        if((attr == null) || (logger == null) || !(logger.isLoggable(level))) {
            return;
        }
        if(attr instanceof BasicServiceType) {
            logger.log(level, "  attribute = BasicServiceType");
            logger.log(level, "    Display Name = "
                               +((BasicServiceType)(attr)).getDisplayName());
            logger.log(level, "    Description  = "
                          +((BasicServiceType)(attr)).getShortDescription());
        } else if(attr instanceof ServiceInfo) {
            logger.log(level, "  attribute = ServiceInfo");
            logger.log(level, "    Service Name         = "
                                       +((ServiceInfo)(attr)).name);
            logger.log(level, "    Service Manufacturer = "
                                      +((ServiceInfo)(attr)).manufacturer);
            logger.log(level, "    Service Vendor       = "
                                       +((ServiceInfo)(attr)).vendor);
            logger.log(level, "    Service Version      = "
                                       +((ServiceInfo)(attr)).version);
            logger.log(level, "    Service Model        = "
                                       +((ServiceInfo)(attr)).model);
            logger.log(level, "    Service Serial #     = "
                                      +((ServiceInfo)(attr)).serialNumber);
        } else {
            logger.log(level, "  attribute = "+attr);
        }//endif
    }//end writeAttribute

    /* Logs data on each attribute in a set to a file or standard output */
    private static void writeAttributes(Entry[] attrs,
                                        Logger  logger,
                                        Level   level)
    {
        if((attrs == null) || (logger == null) || !(logger.isLoggable(level))){
            return;
        }
        for(int i=0;i<attrs.length;i++) {
            if(attrs[i] == null) continue;
            writeAttribute(attrs[i],logger,level);
        }//end loop
        logger.log(level,"");
    }//end writeAttributes

    /* Logs startup data to file or standard output */
    private void logInfoStartup() {
        if (startupLogger.isLoggable(Level.INFO)) {
            startupLogger.log
                   (Level.INFO, "Fiddler started: {0}, {1}, {2}",
                    new Object[]
                        { (FiddlerImpl.this.serviceID).toString(),
                          writeGroupArrayToString(thisServicesGroups),
                          writeArrayElementsToString(thisServicesLocators) } );
        }//endif
        if( startupLogger.isLoggable(Level.CONFIG) ) {
            if(persistDir != null) {
                startupLogger.log(Level.CONFIG,
                                  " Persistent state directory:  {0}",
                                  persistDir);
            }//endif
            startupLogger.log(Level.CONFIG,
                            "Attributes to register in each lookup service: ");
            writeAttributes(thisServicesAttrs,startupLogger,Level.CONFIG);
        }//endif
    }//end logInfoStartup

    /* Logs shutdown/destroy data to file or standard output */
    private void logInfoShutdown() {
        if (startupLogger.isLoggable(Level.INFO)) {
            startupLogger.log
                   (Level.INFO, "Fiddler destroyed: {0}, {1}, {2}",
                    new Object[]
                        { (FiddlerImpl.this.serviceID).toString(),
                          writeGroupArrayToString(thisServicesGroups),
                          writeArrayElementsToString(thisServicesLocators) } );
        }//endif
    }//end logInfoShutdown

    /* Logs information related to the run method of a particular task */
    private void logInfoTasks(String str) {
        if( tasksLogger.isLoggable(Level.FINEST) ) {
            tasksLogger.log(Level.FINEST, str);
        }//endif
    }//end logInfoTasks

    /* Logs information about events that are sent */
    private void logInfoEvents(String str) {
        if( eventsLogger.isLoggable(Level.FINE) ) {
            eventsLogger.log(Level.FINE, str);
        }//endif
    }//end logInfoEvents

    /* Logs information about events that are sent */
    private void logInfoEvents(Map  groupsMap,
                               long eventID,
                               long seqNum,
                               MarshalledObject handback,
                               boolean discarded,
                               Logger logger,
                               Level  level)
    {
        if( (logger == null) || !(logger.isLoggable(level)) ) {
            return;
        }
        String discardedStr = (discarded == true ? "DISCARDED":"DISCOVERED");
        Object hb = null;
        if(handback != null) {
            try {
                hb = handback.get();
            } catch (ClassNotFoundException e) {
                problemLogger.log(Levels.HANDLED,
                                  "ClassNotFoundException when "
                                  +"unmarshalling handback",e);
            } catch (IOException e) {
                problemLogger.log(Levels.HANDLED,
                                  "IOException when unmarshalling "
                                  +"handback",e);
            }
        }//endif
        logger.log(level, "\n"+discardedStr+" Event:");
        logger.log(level, "  EventID  = "+eventID);
        logger.log(level, "  SeqNum   = "+seqNum);
        logger.log(level, "  handback = "+hb);
        ServiceRegistrar[] regs =
                          (ServiceRegistrar[])(groupsMap.keySet()).toArray
                                     (new ServiceRegistrar[groupsMap.size()]);
        logger.log(level, "  Registrars = ");
        writeRegistrarsArray(regs,logger,level);
        for(int i=0;i<regs.length;i++){
            String[] curGroups = (String[])groupsMap.get(regs[i]);
            logger.log(level, "  member groups ["+i+"] = ");
            writeGroupArray(curGroups,logger,level);
        }//end loop
        logger.log(level, "");
    }//end logInfoEvents

    /* Logs group state information over all active registrations */
    private void logInfoGroups() {
        String[] allGroups = discoveryMgr.getGroups();
        if( groupsLogger.isLoggable(Level.FINER) ) {
            groupsLogger.log(Level.FINER, 
                             "Group(s) over all registrations: ");
            writeGroupArray(allGroups,groupsLogger,Level.FINER);
        }//endif
    }//end logInfoGroups

    /* Logs group state information over all active registrations */
    private void logInfoGroups(String headerStr) {
        if( (headerStr != null) && (groupsLogger.isLoggable(Level.FINER)) ) {
            groupsLogger.log(Level.FINER, headerStr);
        }//endif
        logInfoGroups();
    }//end logInfoGroups

    /* Logs locator state information over all active registrations */
    private void logInfoLocators() {
        LookupLocator[] allLocators = discoveryMgr.getLocators();
        if( locatorsLogger.isLoggable(Level.FINER) ) {
            locatorsLogger.log(Level.FINER, 
                               "Locator(s) over all registrations: ");
            writeArrayElements(allLocators,locatorsLogger,Level.FINER);
        }//endif
    }//end logInfoLocators

    /* Logs information useful to debugging the discard process */
    private void logInfoDiscard(String str, Uuid regID) {
        if(    (str != null) && (regID != null)
            && (discardLogger.isLoggable(Level.FINE)) )
        {
            discardLogger.log(Level.FINE, str+" registrationID = "+regID);
        }//endif
    }//end logInfoDiscard

    /* Logs information useful to debugging the discard process */
    private void logInfoDiscard(String str) {
        if( discardLogger.isLoggable(Level.FINE) ) {
            discardLogger.log(Level.FINE, str);
        }//endif
    }//end logInfoDiscard

    /* Logs information useful to debugging the leasing mechanism */
    private void logInfoLease(String str, Uuid regID, Uuid leaseID) {
        if(    (str != null) && (regID != null)
            && (leaseLogger.isLoggable(Level.FINER)) )
        {
            leaseLogger.log(Level.FINER, str+" (registrationID,leaseID) = ("
                                         +regID+", "+leaseID+")");
        }//endif
    }//end logInfoLease

    /* Logs information useful to debugging the registration mechanism */
    private void logInfoRegistration(String str, Object regInfo) {
        if(    (str != null) && (regInfo != null)
            && (registrationLogger.isLoggable(Level.FINER)) )
        {
            registrationLogger.log(Level.FINER, str+" {0}", regInfo);
        }//endif
    }//end logInfoRegistration

    /* Logs information useful to debugging the logging mechanism */
    private static void logInfoPersist(String str) {
        if( persistLogger.isLoggable(Level.FINEST) ) {
            persistLogger.log(Level.FINEST, str);
        }//endif
    }//end logInfoPersist

    /* Logs information useful to debugging the addLogRecord method */
    private void logInfoAddLogRecord(LogRecord rec) {
        if( !(persistLogger.isLoggable(Level.FINEST)) )  return;
        String logStr = "Logging a state change: Unknown log record instance";
        /* JoinAdmin */
        if(rec instanceof LookupAttrsAddedLogObj) {
            logStr = "Logging state change: lookup attributes added";
        } else if (rec instanceof LookupAttrsModifiedLogObj) {
            logStr = "Logging state change: lookup attributes modified";
        } else if (rec instanceof LookupGroupsChangedLogObj) {
            logStr = "Logging state change: groups to join changed to "
                     +writeGroupArrayToString(thisServicesGroups);
        } else if (rec instanceof LookupLocatorsChangedLogObj) {
            logStr = "Logging state change: locators to join changed to "
                     +writeArrayElementsToString(thisServicesLocators);
        /* FiddlerAdmin */
        } else if (rec instanceof LeaseBoundSetLogObj) {
            logStr = "Logging state change: lease duration bound changed";
        } else if (rec instanceof SnapshotWeightSetLogObj) {
            logStr = "Logging state change: snapshot weight factor changed";
        } else if (rec instanceof SnapshotThresholdSetLogObj) {
            logStr = "Logging state change: log-to-snapshot threshold changed";
        /* LookupDiscoveryService */
        } else if (rec instanceof RegistrationGrantedLogObj) {
            logStr = "Logging state change: new registration granted";
        /* LookupDiscoveryRegistration */
        } else if (rec instanceof GroupsAddedToRegistrationLogObj) {
            logStr = "Logging state change: added new groups to "
                     +"registration's set of groups to discover";
        } else if (rec instanceof GroupsSetInRegistrationLogObj) {
            logStr = "Logging state change: replaced registration's set of "
                     +"groups to discover";
        } else if (rec instanceof GroupsRemovedFromRegistrationLogObj) {
            logStr = "Logging state change: removed groups from "
                     +"registration's set of groups to discover";
        } else if (rec instanceof LocsAddedToRegistrationLogObj) {
            logStr = "Logging state change: added new locators to "
                     +"registration's set of locators to discover";
        } else if (rec instanceof LocsSetInRegistrationLogObj) {
            logStr = "Logging state change: replaced registration's set of "
                     +"locators to discover";
        } else if (rec instanceof LocsRemovedFromRegistrationLogObj) {
            logStr = "Logging state change: removed locators from "
                     +"registration's set of locators to discover";
        } else if (rec instanceof LeaseRenewedLogObj) {
            logStr = "Logging state change: registration's lease renewed";
        } else if (rec instanceof LeasesRenewedLogObj) {
            logStr = "Logging state change: set of leases renewed for a "
                     +"set of registrations";
        } else if (rec instanceof LeaseCancelledLogObj) {
            logStr = "Logging state change: registration's lease cancelled";
        } else if (rec instanceof LeasesCancelledLogObj) {
            logStr = "Logging state change: set of leases cancelled for a "
                     +"set of registrations";
        }//endif
        logInfoPersist(logStr);
    }//end logInfoAddLogRecord
    /* END Private Logging Facility Methods -------------------------------- */
    /* *************** END Private NON-Static Utility Methods ************** */

}/*end class FiddlerImpl */
