/*
 * 
 * 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.reggie;

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.discovery.ClientSubjectChecker;
import com.sun.jini.discovery.Discovery;
import com.sun.jini.discovery.DiscoveryConstraints;
import com.sun.jini.discovery.DiscoveryProtocolException;
import com.sun.jini.discovery.EncodeIterator;
import com.sun.jini.discovery.MulticastAnnouncement;
import com.sun.jini.discovery.MulticastRequest;
import com.sun.jini.discovery.UnicastResponse;
import com.sun.jini.logging.Levels;
import com.sun.jini.lookup.entry.BasicServiceType;
import com.sun.jini.proxy.MarshalledWrapper;
import com.sun.jini.reliableLog.LogException;
import com.sun.jini.reliableLog.LogHandler;
import com.sun.jini.reliableLog.ReliableLog;
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 java.io.DataInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.rmi.MarshalledObject;
import java.rmi.NoSuchObjectException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.UnmarshalException;
import java.rmi.activation.ActivationException;
import java.rmi.activation.ActivationID;
import java.rmi.activation.ActivationSystem;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.SecureRandom;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import net.jini.activation.ActivationExporter;
import net.jini.activation.ActivationGroup;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.config.ConfigurationProvider;
import net.jini.config.NoSuchEntryException;
import net.jini.constraint.BasicMethodConstraints;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.MethodConstraints;
import net.jini.core.constraint.RemoteMethodControl;
import net.jini.core.discovery.LookupLocator;
import net.jini.core.entry.Entry;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEvent;
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.ServiceItem;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceRegistration;
import net.jini.discovery.Constants;
import net.jini.discovery.ConstrainableLookupLocator;
import net.jini.discovery.DiscoveryGroupManagement;
import net.jini.discovery.DiscoveryLocatorManagement;
import net.jini.discovery.DiscoveryManagement;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.export.Exporter;
import net.jini.export.ProxyAccessor;
import net.jini.id.ReferentUuid;
import net.jini.id.Uuid;
import net.jini.id.UuidFactory;
import net.jini.io.MarshalledInstance;
import net.jini.io.UnsupportedConstraintException;
import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.tcp.TcpServerEndpoint;
import net.jini.lookup.JoinManager;
import net.jini.lookup.entry.ServiceInfo;
import net.jini.security.BasicProxyPreparer;
import net.jini.security.ProxyPreparer;
import net.jini.security.TrustVerifier;
import net.jini.security.proxytrust.ServerProxyTrust;

/**
 * Base server-side implementation of a lookup service, subclassed by
 * TransientRegistrarImpl and PersistentRegistrarImpl.  Multiple client-side
 * proxy classes are used, for the ServiceRegistrar interface as well as for
 * leases and administration; their methods transform the parameters and then
 * make corresponding calls on the Registrar interface implemented on the
 * server side.
 *
 * @author Sun Microsystems, Inc.
 *
 */
class RegistrarImpl implements Registrar, ProxyAccessor, ServerProxyTrust {

    /** Maximum minMax lease duration for both services and events */
    private static final long MAX_LEASE = 1000L * 60 * 60 * 24 * 365 * 1000;
    /** Maximum minimum renewal interval */
    private static final long MAX_RENEW = 1000L * 60 * 60 * 24 * 365;
    /** Default maximum size of multicast packets to send and receive */
    private static final int DEFAULT_MAX_PACKET_SIZE = 512;
    /** Default time to live value to use for sending multicast packets */
    private static final int DEFAULT_MULTICAST_TTL = 15;
    /** Default timeout to set on sockets used for unicast discovery */
    private static final int DEFAULT_SOCKET_TIMEOUT = 1*60*1000;
    /** Log format version */
    private static final int LOG_VERSION = 2;
    /** Logger and configuration component name */
    private static final String COMPONENT = "com.sun.jini.reggie";
    /** Lease ID always assigned to self */
    private static final Uuid myLeaseID = UuidFactory.create(0L, 0L);
    /** Logger used by this service */
    private static final Logger logger = Logger.getLogger(COMPONENT);

    /** Base set of initial attributes for self */
    private static final Entry[] baseAttrs = {
	new ServiceInfo(
	    "Lookup", "Sun Microsystems, Inc.", "Sun Microsystems, Inc.",
	    VersionConstants.SERVER_VERSION, "", ""),
	new BasicServiceType("Lookup")
    };
    /** Empty attribute set */
    private static final EntryRep[] emptyAttrs = {};

    /** Proxy for myself */
    private RegistrarProxy proxy;
    /** Exporter for myself */
    private Exporter serverExporter;
    /** Remote reference for myself */
    private Registrar myRef;
    /** Our service ID */
    private ServiceID myServiceID;
    /** Our activation id, or null if not activatable */
    private ActivationID activationID;
    /** Associated activation system, or null if not activatable */
    private ActivationSystem activationSystem;
    /** Our LookupLocator */
    private LookupLocator myLocator;
    /** Our login context, for logging out */
    private LoginContext loginContext;
    /** Shutdown callback object, or null if no callback needed */
    private LifeCycle lifeCycle;

    /**
     * Map from ServiceID to SvcReg.  Every service is in this map under
     * its serviceID.
     */
    private final HashMap serviceByID = new HashMap();
    /**
     * Identity map from SvcReg to SvcReg, ordered by lease expiration.
     * Every service is in this map.
     */
    private final TreeMap serviceByTime = new TreeMap();
    /**
     * Map from ServiceType to ArrayList(SvcReg).  Every service is in
     * this map under its exact serviceType.
     */
    private final HashMap serviceByType = new HashMap();
    /**
     * Map from EntryClass to HashMap[] where each HashMap is a map from
     * Object (field value) to ArrayList(SvcReg).  The HashMap array has as
     * many elements as the EntryClass has fields (including fields defined
     * by superclasses).  Services are in this map multiple times, once
     * for each field of each entry it has.  The outer map is indexed by the
     * first (highest) superclass that defines the field.  This means that a
     * HashMap[] has null elements for fields defined by superclasses, but
     * this is a small memory hit and is simpler than subtracting off base
     * index values when accessing the arrays.
     */
    private final HashMap serviceByAttr = new HashMap(23);
    /**
     * Map from EntryClass to ArrayList(SvcReg).  Services are in this map
     * multiple times, once for each no-fields entry it has (no fields meaning
     * none of the superclasses have fields either).  The map is indexed by
     * the exact type of the entry.
     */
    private final HashMap serviceByEmptyAttr = new HashMap(11);
    /** All EntryClasses with non-zero numInstances */
    private final ArrayList entryClasses = new ArrayList();
    /**
     * Map from Long(eventID) to EventReg.  Every event registration is in
     * this map under its eventID.
     */
    private final HashMap eventByID = new HashMap(11);
    /**
     * Identity map from EventReg to EventReg, ordered by lease expiration.
     * Every event registration is in this map.
     */
    private final TreeMap eventByTime = new TreeMap();
    /**
     * Map from ServiceID to EventReg or EventReg[].  An event
     * registration is in this map if its template matches on (at least)
     * a specific serviceID.
     */
    private final HashMap subEventByService = new HashMap(11);
    /**
     * Map from Long(eventID) to EventReg.  An event registration is in
     * this map if its template matches on ANY_SERVICE_ID.
     */
    private final HashMap subEventByID = new HashMap(11);

    /** Generator for resource (e.g., registration, lease) Uuids */
    private UuidGenerator resourceIdGenerator = new UuidGenerator();
    /** Generator for service IDs */
    private UuidGenerator serviceIdGenerator = resourceIdGenerator;
    /** Event id generator */
    private long eventID = 0;
    /** Random number generator for use in lookup */
    private final Random random = new Random();

    /** Preparer for received remote event listeners */
    private ProxyPreparer listenerPreparer = new BasicProxyPreparer();
    /** Preparer for remote event listeners recovered from state log */
    private ProxyPreparer recoveredListenerPreparer = listenerPreparer;
    /** Preparer for received lookup locators */
    private ProxyPreparer locatorPreparer = listenerPreparer;
    /** Preparer for lookup locators recovered from state log */
    private ProxyPreparer recoveredLocatorPreparer = listenerPreparer;

    /** ArrayList of pending EventTasks */
    private final ArrayList newNotifies = new ArrayList();

    /** Current maximum service lease duration granted, in milliseconds. */
    private long maxServiceLease;
    /** Current maximum event lease duration granted, in milliseconds. */
    private long maxEventLease;
    /** Earliest expiration time of a SvcReg */
    private long minSvcExpiration = Long.MAX_VALUE;
    /** Earliest expiration time of an EventReg */
    private long minEventExpiration = Long.MAX_VALUE;

    /** Manager for discovering other lookup services */
    private DiscoveryManagement discoer;
    /** Manager for joining other lookup services */
    private JoinManager joiner;
    /** Task manager for sending events and discovery responses */
    private TaskManager tasker;
    /** Service lease expiration thread */
    private Thread serviceExpirer;
    /** Event lease expiration thread */
    private Thread eventExpirer;
    /** Unicast discovery request packet receiving thread */
    private UnicastThread unicaster;
    /** Multicast discovery request packet receiving thread */
    private Thread multicaster;
    /** Multicast discovery announcement sending thread */
    private Thread announcer;
    /** Snapshot-taking thread */
    private Thread snapshotter;

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

    /** Class resolver */
    private final ClassResolver resolver = new ClassResolver();
    /** Canonical ServiceType for java.lang.Object */
    private ServiceType objectServiceType;

    /** Log for recovering/storing state, or null if running as transient */
    private ReliableLog log;
    /** Flag indicating whether system is in a state of recovery */
    private boolean inRecovery;
    /** Flag indicating whether system state was recovered from a snapshot */
    private boolean recoveredSnapshot = false;
    /** Current number of records in the Log File since the last snapshot */
    private int logFileSize = 0;

    /** Log file must contain this many records before snapshot allowed */
    private int persistenceSnapshotThreshold = 200;
    /** Weight factor applied to snapshotSize when deciding to take snapshot */
    private float persistenceSnapshotWeight = 10;
    /** Minimum value for maxServiceLease. */
    private long minMaxServiceLease = 1000 * 60 * 5;
    /** Minimum value for maxEventLease. */
    private long minMaxEventLease = 1000 * 60 * 30;
    /** Minimum average time between lease renewals, in milliseconds. */
    private long minRenewalInterval = 100;
    /** Port for unicast discovery */
    private int unicastPort = 0;
    /** The groups we are a member of */
    private String[] memberGroups = { "" };
    /** The groups we should join */
    private String[] lookupGroups = DiscoveryGroupManagement.NO_GROUPS;
    /** The locators of other lookups we should join */
    private LookupLocator[] lookupLocators = {};
    /** The attributes to use when joining (including with myself) */
    private Entry[] lookupAttrs;
    /** Interval to wait in between sending multicast announcements */
    private long multicastAnnouncementInterval = 1000 * 60 * 2;

    /** Network interfaces to use for multicast discovery */
    private NetworkInterface[] multicastInterfaces;
    /** Flag indicating whether network interfaces were explicitly specified */
    private boolean multicastInterfacesSpecified;
    /** Interval to wait in between retrying failed interfaces */
    private int multicastInterfaceRetryInterval = 1000 * 60 * 5;
    /** Utility for participating in version 2 of discovery protocols */
    private Discovery protocol2;
    /** Constraints specified for incoming multicast requests */
    private DiscoveryConstraints multicastRequestConstraints;
    /** Constraints specified for outgoing multicast announcements */
    private DiscoveryConstraints multicastAnnouncementConstraints;
    /** Constraints specified for handling unicast discovery */
    private DiscoveryConstraints unicastDiscoveryConstraints;
    /** Client subject checker to apply to incoming multicast requests */
    private ClientSubjectChecker multicastRequestSubjectChecker;
    /** Client subject checker to apply to unicast discovery attempts */
    private ClientSubjectChecker unicastDiscoverySubjectChecker;

    /**
     * Constructs RegistrarImpl based on a configuration obtained using the
     * provided string arguments.  If activationID is non-null, the created
     * RegistrarImpl runs as activatable; if persistent is true, it
     * persists/recovers its state to/from disk.  A RegistrarImpl instance
     * cannot be constructed as both activatable and non-persistent.  If
     * lifeCycle is non-null, its unregister method is invoked during shutdown.
     */
    RegistrarImpl(String[] configArgs,
		  final ActivationID activationID,
		  final boolean persistent,
		  final LifeCycle lifeCycle)
	throws Exception
    {
	if (activationID != null && !persistent) {
	    throw new IllegalArgumentException();
	}
	try {
	    final Configuration config = ConfigurationProvider.getInstance(
		configArgs, getClass().getClassLoader());
	    try {
		loginContext = (LoginContext) Config.getNonNullEntry(
		    config, COMPONENT, "loginContext", LoginContext.class);
	    } catch (NoSuchEntryException e) {
		// leave null
	    }
	    PrivilegedExceptionAction init = new PrivilegedExceptionAction() {
		public Object run() throws Exception {
		    init(config, activationID, persistent, lifeCycle);
		    return null;
		}
	    };
	    if (loginContext != null) {
		loginContext.login();
		try {
		    Subject.doAsPrivileged(
			loginContext.getSubject(), init, null);
		} catch (PrivilegedActionException e) {
		    throw e.getCause();
		}
	    } else {
		init.run();
	    }
	} catch (Throwable t) {
	    logger.log(Level.SEVERE, "Reggie initialization failed", t);
	    if (t instanceof Exception) {
		throw (Exception) t;
	    } else {
		throw (Error) t;
	    }
	}
    }

    /** A service item registration record. */
    private final static class SvcReg implements Comparable, Serializable {

	private static final long serialVersionUID = 2L;

	/**
	 * The service item.
	 *
	 * @serial
	 */
	public final Item item;
	/**
	 * The lease id.
	 *
	 * @serial
	 */
	public final Uuid leaseID;
	/**
	 * The lease expiration time.
	 *
	 * @serial
	 */
	public long leaseExpiration;

	/** Simple constructor */
	public SvcReg(Item item, Uuid leaseID, long leaseExpiration) {
	    this.item = item;
	    this.leaseID = leaseID;
	    this.leaseExpiration = leaseExpiration;
	}

	/**
	 * Primary sort by leaseExpiration, secondary by leaseID.  The
	 * secondary sort is immaterial, except to ensure a total order
	 * (required by TreeMap).
	 */
	public int compareTo(Object obj) {
	    SvcReg reg = (SvcReg)obj;
	    if (this == reg)
		return 0;
	    int i = compare(leaseExpiration, reg.leaseExpiration);
	    if (i != 0) {
		return i;
	    }
	    i = compare(leaseID.getMostSignificantBits(),
			reg.leaseID.getMostSignificantBits());
	    if (i != 0) {
		return i;
	    }
	    return compare(leaseID.getLeastSignificantBits(),
			   reg.leaseID.getLeastSignificantBits());
	}

	/**
	 * Compares long values, returning -1, 0, or 1 if l1 is less than,
	 * equal to or greater than l2, respectively.
	 */
	private static int compare(long l1, long l2) {
	    return (l1 < l2) ? -1 : ((l1 > l2) ? 1 : 0);
	}
    }

    /** An event registration record. */
    private final static class EventReg implements Comparable, Serializable {

	private static final long serialVersionUID = 2L;

	/**
	 * The event id.
	 * @serial
	 */
	public final long eventID;
	/**
	 * The lease id.
	 * @serial
	 */
	public final Uuid leaseID;
	/**
	 * The template to match.
	 * @serial
	 */
	public final Template tmpl;
	/**
	 * The transitions.
	 *
	 * @serial
	 */
	public final int transitions;
	/**
	 * The current sequence number.
	 *
	 * @serial
	 */
	public long seqNo;
	/**
	 * The event listener.
	 */
	public transient RemoteEventListener listener;
	/**
	 * The handback object.
	 *
	 * @serial
	 */
	public final MarshalledObject handback;
	/**
	 * The lease expiration time.
	 *
	 * @serial
	 */
	public long leaseExpiration;

	/** Simple constructor */
	public EventReg(long eventID, Uuid leaseID, Template tmpl,
			int transitions, RemoteEventListener listener,
			MarshalledObject handback, long leaseExpiration) {
	    this.eventID = eventID;
	    this.leaseID = leaseID;
	    this.tmpl = tmpl;
	    this.transitions = transitions;
	    this.seqNo = 0;
	    this.listener = listener;
	    this.handback = handback;
	    this.leaseExpiration = leaseExpiration;
	}

	/**
	 * Primary sort by leaseExpiration, secondary by eventID.  The
	 * secondary sort is immaterial, except to ensure a total order
	 * (required by TreeMap).
	 */
	public int compareTo(Object obj) {
	    EventReg reg = (EventReg)obj;
	    if (this == reg)
		return 0;
	    if (leaseExpiration < reg.leaseExpiration ||
		(leaseExpiration == reg.leaseExpiration &&
		 eventID < reg.eventID))
		return -1;
	    return 1;
	}

	/**
	 * Prepares listener (if non-null) using the given proxy preparer.  If
	 * preparation fails, the listener field is set to null.
	 */
	public void prepareListener(ProxyPreparer preparer) {
	    if (listener != null) {
		try {
		    listener =
			(RemoteEventListener) preparer.prepareProxy(listener);
		} catch (Exception e) {
		    if (logger.isLoggable(Level.WARNING)) {
			logThrow(
			    Level.WARNING,
			    getClass().getName(),
			    "prepareListener",
			    "failed to prepare event listener {0}",
			    new Object[]{ listener },
			    e);
		    }
		    listener = null;
		}
	    }
	}

	/**
	 * @serialData RemoteEventListener as a MarshalledInstance
	 */
	private void writeObject(ObjectOutputStream stream)
	    throws IOException
	{
	    stream.defaultWriteObject();
	    stream.writeObject(new MarshalledInstance(listener));
	}

	/**
	 * Unmarshals the event listener.
	 */
	private void readObject(ObjectInputStream stream)
	    throws IOException, ClassNotFoundException
	{
	    stream.defaultReadObject();
	    MarshalledInstance mi = (MarshalledInstance) stream.readObject();
	    try {
		listener = (RemoteEventListener) mi.get(false);
	    } catch (Throwable e) {
		if (e instanceof Error &&
		    ThrowableConstants.retryable(e) ==
			ThrowableConstants.BAD_OBJECT)
		{
		    throw (Error) e;
		}
		logger.log(Level.WARNING,
			   "failed to recover event listener", e);
	    }
	}
    }

    /**
     * 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(RegistrarImpl regImpl);
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a new service is registered.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class SvcRegisteredLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The service registration.
	 *
	 * @serial
	 */
	private SvcReg reg;

	/** Simple constructor */
	public SvcRegisteredLogObj(SvcReg reg) {
	    this.reg = reg;
	}
	
	/**
	 * Modifies the state of the Registrar by registering the service
         * stored in the reg object. Also needs to delete any existing
	 * service with the same serviceID; this can happen if a service
	 * re-registers while an existing registration is in effect, because
	 * we don't log a separate lease cancellation record for the existing
	 * registration in that case.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    regImpl.resolver.resolve(reg.item);
	    SvcReg oldReg =
		(SvcReg)regImpl.serviceByID.get(reg.item.serviceID);
	    if (oldReg != null)
		regImpl.deleteService(oldReg, 0);
	    regImpl.addService(reg);
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * new attributes are added to an existing service in the Registrar.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class AttrsAddedLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The service id.
	 *
	 * @serial
	 */
	private ServiceID serviceID;
	/**
	 * The lease id.
	 *
	 * @serial
	 */
	private Uuid leaseID;
	/**
	 * The attributes added.
	 *
	 * @serial
	 */
	private EntryRep[] attrSets;

	/** Simple constructor */
	public AttrsAddedLogObj(ServiceID serviceID,
				Uuid leaseID,
				EntryRep[] attrSets)
	{
	    this.serviceID = serviceID;
	    this.leaseID = leaseID;
	    this.attrSets = attrSets;
	}
	
	/**
	 * Modifies the state of the Registrar by adding to all of the
         * services matching the template, the attributes stored in
         * attributeSets.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    try {
		regImpl.addAttributesDo(serviceID, leaseID, attrSets);
	    } catch (UnknownLeaseException e) {
		/* this exception should never occur when recovering  */
	    }
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * existing attributes of an existing service in the Registrar are
     * modified.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class AttrsModifiedLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The service id.
	 *
	 * @serial
	 */
	private ServiceID serviceID;
	/**
	 * The lease id.
	 *
	 * @serial
	 */
	private Uuid leaseID;
	/**
	 * The templates to match.
	 * @serial
	 */
	private EntryRep[] attrSetTmpls;
	/**
	 * The new attributes.
	 *
	 * @serial
	 */
	private EntryRep[] attrSets;

	/** Simple constructor */
	public AttrsModifiedLogObj(ServiceID serviceID,
				   Uuid leaseID,
				   EntryRep[] attrSetTmpls,
				   EntryRep[] attrSets)
	{
	    this.serviceID = serviceID;
	    this.leaseID = leaseID;
	    this.attrSetTmpls = attrSetTmpls;
	    this.attrSets = attrSets;
	}

	/**
	 * Modifies the state of the Registrar by modifying the attributes
         * of the services that match the template with the attributes 
         * stored in attributeSets.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    try {
		regImpl.modifyAttributesDo(serviceID, leaseID,
					   attrSetTmpls, attrSets);
	    } catch (UnknownLeaseException e) {
		/* this exception should never occur when recovering  */
	    }
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * new attributes are set on an existing service in the Registrar.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class AttrsSetLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The service id.
	 *
	 * @serial
	 */
	private ServiceID serviceID;
	/**
	 * The lease id.
	 *
	 * @serial
	 */
	private Uuid leaseID;
	/**
	 * The new attributes.
	 *
	 * @serial
	 */
	private EntryRep[] attrSets;

	/** Simple constructor */
	public AttrsSetLogObj(ServiceID serviceID,
			      Uuid leaseID,
			      EntryRep[] attrSets)
	{
	    this.serviceID = serviceID;
	    this.leaseID = leaseID;
	    this.attrSets = attrSets;
	}
	
	/**
	 * Modifies the state of the Registrar by replacing the attributes
         * of the services matching the template with the attributes stored
         * in attributeSets.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    try {
		regImpl.setAttributesDo(serviceID, leaseID, attrSets);
	    } catch (UnknownLeaseException e) {
		/* this exception should never occur when recovering  */
	    }
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a new event is registered.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class EventRegisteredLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The event registration.
	 *
	 * @serial
	 */
	private EventReg eventReg;

	/** Simple constructor */
	public EventRegisteredLogObj(EventReg eventReg) {
	    this.eventReg = eventReg;
	}
	
	/**
	 * Modifies the state of the Registrar by registering the event
         * stored in the eventReg object; and by updating both the event
         * sequence number and the event ID.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    regImpl.resolver.resolve(eventReg.tmpl);
	    eventReg.prepareListener(regImpl.recoveredListenerPreparer);
	    eventReg.seqNo += Integer.MAX_VALUE;
	    regImpl.addEvent(eventReg);
	    regImpl.eventID++;
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a lease on an existing service in the Registrar is cancelled.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class ServiceLeaseCancelledLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The service id.
	 *
	 * @serial
	 */
	private ServiceID serviceID;
	/**
	 * The lease id.
	 *
	 * @serial
	 */
	private Uuid leaseID;

	/** Simple constructor */
	public ServiceLeaseCancelledLogObj(ServiceID serviceID, Uuid leaseID) {
	    this.serviceID = serviceID;
	    this.leaseID = leaseID;
	}
	
	/**
	 * Modifies the state of the Registrar by cancelling the lease
         * having ID equal to the contents of the leaseID field; and
         * corresponding to the service with ID equal to the contents of
         * the serviceID field.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    try {
		regImpl.cancelServiceLeaseDo(serviceID, leaseID);
	    } catch (UnknownLeaseException e) {
		/* this exception should never occur when recovering  */
	    }
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a lease on an existing service in the Registrar is renewed.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class ServiceLeaseRenewedLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The service id.
	 *
	 * @serial
	 */
	private ServiceID serviceID;
	/**
	 * The lease id.
	 *
	 * @serial
	 */
	private Uuid leaseID;
	/**
	 * The new lease expiration time.
	 *
	 * @serial
	 */
	private long leaseExpTime;

	/** Simple constructor */
	public ServiceLeaseRenewedLogObj(ServiceID serviceID,
					 Uuid leaseID,
					 long leaseExpTime)
	{
	    this.serviceID = serviceID;
	    this.leaseID = leaseID;
	    this.leaseExpTime = leaseExpTime;
	}
	
	/**
	 * Modifies the state of the Registrar by renewing the lease
         * having ID equal to the contents of the leaseID field; and
         * corresponding to the service with ID equal to the contents
         * of the serviceID field.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    regImpl.renewServiceLeaseAbs(serviceID, leaseID, leaseExpTime);
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a lease on a registered event is cancelled.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class EventLeaseCancelledLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The event id.
	 *
	 * @serial
	 */
	private long eventID;
	/**
	 * The lease id.
	 *
	 * @serial
	 */
	private Uuid leaseID;

	/** Simple constructor */
	public EventLeaseCancelledLogObj(long eventID, Uuid leaseID) {
	    this.eventID = eventID;
	    this.leaseID = leaseID;
	}
	
	/**
	 * Modifies the state of the Registrar by cancelling the lease
         * corresponding to the event with ID equal to the contents of
         * the eventID field.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    try {
		regImpl.cancelEventLeaseDo(eventID, leaseID);
	    } catch (UnknownLeaseException e) {
		/* this exception should never occur when recovering */
	    }
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a lease on a registered event is renewed.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class EventLeaseRenewedLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The event id.
	 *
	 * @serial
	 */
	private long eventID;
	/**
	 * The lease id.
	 *
	 * @serial
	 */
	private Uuid leaseID;
	/**
	 * The new lease expiration time.
	 *
	 * @serial
	 */
	private long leaseExpTime;

	/** Simple constructor */
	public EventLeaseRenewedLogObj(long eventID,
				       Uuid leaseID,
				       long leaseExpTime)
	{
	    this.eventID = eventID;
	    this.leaseID = leaseID;
	    this.leaseExpTime = leaseExpTime;
	}

	/**
	 * Modifies the state of the Registrar by renewing the lease
         * corresponding to the event with ID equal to the contents of
         * the eventID field.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    regImpl.renewEventLeaseAbs(eventID, leaseID, leaseExpTime);
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * a leases in the Registrar is renewed via a LeaseMap.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class LeasesRenewedLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The service and event ids.
	 *
	 * @serial
	 */
	private Object[] regIDs;
	/**
	 * The lease ids.
	 *
	 * @serial
	 */
	private Uuid[] leaseIDs;
	/**
	 * The new lease expiration times.
	 *
	 * @serial
	 */
	private long[] leaseExpTimes;

	/** Simple constructor */
	public LeasesRenewedLogObj(Object[] regIDs,
				   Uuid[] leaseIDs,
				   long[] leaseExpTimes)
	{
	    this.regIDs = regIDs;
	    this.leaseIDs = leaseIDs;
	    this.leaseExpTimes = leaseExpTimes;
	}
	
	/**
	 * Modifies the state of the Registrar by renewing the specified
         * leases.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    regImpl.renewLeasesAbs(regIDs, leaseIDs, leaseExpTimes);
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * lease are cancelled via a LeaseMap.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class LeasesCancelledLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The service and event ids.
	 *
	 * @serial
	 */
	private Object[] regIDs;
	/**
	 * The lease ids.
	 *
	 * @serial
	 */
	private Uuid[] leaseIDs;

	/** Simple constructor */
	public LeasesCancelledLogObj(Object[] regIDs, Uuid[] leaseIDs) {
	    this.regIDs = regIDs;
	    this.leaseIDs = leaseIDs;
	}
	
	/**
	 * Modifies the state of the Registrar by cancelling the specified
         * leases.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    /* Exceptions can be returned, since we didn't weed out unknown
	     * leases before logging, but we can just ignore them anyway.
	     */
	    regImpl.cancelLeasesDo(regIDs, leaseIDs);
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * the Unicast Port Number is set to a new value.
     * <p>
     * Note: the apply() method of this class merely sets the private field
     *       unicastPort. This means that during a recovery, the unicaster
     *       thread will be created with this new port number ONLY IF that
     *       thread is created AFTER recovery is complete. Thus, it is 
     *       important that at re-initialization during a re-activation
     *       of the Registrar, the recovery() method is invoked before
     *       the unicaster thread is created.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class UnicastPortSetLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The new port number.
	 *
	 * @serial
	 */
	private int newPort;

	/** Simple constructor */
	public UnicastPortSetLogObj(int newPort) {
	    this.newPort = newPort;
	}

	/**
	 * Modifies the state of the Registrar by setting the value of the
         * private unicastPort field to the value of the newPort field.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    regImpl.unicastPort = newPort;
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * the set of groups to join is changed.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class LookupGroupsChangedLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The new groups to join.
	 *
	 * @serial
	 */
	private String[] groups;

	/** Simple constructor */
	public LookupGroupsChangedLogObj(String[] groups) {
	    this.groups = groups;
	}

	/**
	 * Modifies the state of the Registrar by setting the private
         * field lookupGroups to the reference to the groups field.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    regImpl.lookupGroups = groups;
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * the set of locators of lookup services to join is changed.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class LookupLocatorsChangedLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The new locators to join.
	 */
	private transient LookupLocator[] locators;

	/** Simple constructor */
	public LookupLocatorsChangedLogObj(LookupLocator[] locators) {
	    this.locators = locators;
	}

	/**
	 * Modifies the state of the Registrar by setting the private
	 * field lookupLocators to the reference to the locators field.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    try {
		regImpl.lookupLocators = prepareLocators(
		    locators, regImpl.recoveredLocatorPreparer, true);
	    } catch (RemoteException e) {
		throw new AssertionError(e);
	    }
	}

	/**
	 * Writes locators as a null-terminated list of MarshalledInstances.
	 */
	private void writeObject(ObjectOutputStream stream)
	    throws IOException
	{
	    stream.defaultWriteObject();
	    marshalLocators(locators, stream);
	}

	/**
	 * Reads in null-terminated list of MarshalledInstances, from which
	 * locators are unmarshalled.
	 */
	private void readObject(ObjectInputStream stream)
	    throws IOException, ClassNotFoundException
	{
	    stream.defaultReadObject();
	    locators = unmarshalLocators(stream);
	}
    }

    /**
     * LogObj class whose instances are recorded to the log file whenever
     * the memberGroups array is set to reference a new array of strings.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private static class MemberGroupsChangedLogObj implements LogRecord {

	private static final long serialVersionUID = 2L;

	/**
	 * The new groups to be a member of.
	 *
	 * @serial
	 */
	private String[] groups;

	/** Simple constructor */
	public MemberGroupsChangedLogObj(String[] groups) {
	    this.groups = groups;
	}

	/**
	 * Modifies the state of the Registrar by setting the private
         * memberGroups field to the reference to the groups field.
         * 
         * @see RegistrarImpl.LocalLogHandler#applyUpdate
	 */
	public void apply(RegistrarImpl regImpl) {
	    regImpl.memberGroups = groups;
	}
    }

    /**
     * Handler class for the persistent storage facility.
     * <p>
     * At any point during processing in a persistent Registrar instance, there
     * will exist both a 'snapshot' of the Registrar'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 Registrar when it is restarted or
     * reactivated (for example, after a crash or network outage).
     * <p>
     * This class contains the methods that are used to record and recover
     * the snapshot of the Registrar'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 persistent Registrar'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 Registrar. That data
     * represents many changes -- over time -- to the Registrar'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 Registrar's state at a particular point in time.
     * <p>
     * The data written to the snapshot file is shown below:
     * <ul>
     * <li> our service ID
     * <li> current event ID
     * <li> current values of administrable parameters
     * <li> contents of the container holding the current registered services
     * <li> null (termination 'marker' for the set of registered services)
     * <li> contents of the container holding the current registered events
     * <li> null (termination 'marker' for the set of registered events)
     * </ul>
     * The type of state changes that will generate a new record in the log
     * file are:
     * <ul>
     * <li> a new service was registered
     * <li> a new event was registered
     * <li> new attributes were added to an existing service
     * <li> existing attributes of a service were modified
     * <li> a service lease was cancelled
     * <li> a service lease was renewed
     * <li> an event lease was cancelled
     * <li> an event lease was renewed
     * <li> an administrable parameter was changed
     * </ul>
     * During recovery, the state of the Registrar 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 Registrar'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 {
	
	/** Simple constructor */
	public LocalLogHandler() { }

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

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

	/**
	 * Required method implementing 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 Registrar in a way dictated
	 * by the type of record that was retrieved.
	 */
	public void applyUpdate(Object logRecObj) {
	    ((LogRecord)logRecObj).apply(RegistrarImpl.this);
	}
    }

    /** Base class for iterating over all Items that match a Template. */
    private abstract class ItemIter {
	/** Current time */
	public final long now = System.currentTimeMillis();
	/** True means duplicate items are possible */
	public boolean dupsPossible = false;
	/** Template to match */
	protected final Template tmpl;
	/** Next item to return */
	protected SvcReg reg;

	/** Subclass constructors must initialize reg */
	protected ItemIter(Template tmpl) {
	    this.tmpl = tmpl;
	}

	/** Returns true if the iteration has more elements. */
	public boolean hasNext() {
	    return reg != null;
	}

	/** Returns the next element in the iteration as an Item. */
	public Item next() {
	    if (reg == null)
		throw new NoSuchElementException();
	    Item item = reg.item;
	    step();
	    return item;
	}

	/** Returns the next element in the iteration as a SvcReg. */
	public SvcReg nextReg() {
	    if (reg == null)
		throw new NoSuchElementException();
	    SvcReg cur = reg;
	    step();
	    return cur;
	}

	/** Set reg to the next matching element, or null if none */
	protected abstract void step();
    }

    /** Iterate over all Items. */
    private class AllItemIter extends ItemIter {
	/** Iterator over serviceByID */
	private final Iterator iter;

	/** Assumes the empty template */
	public AllItemIter() {
	    super(null);
	    iter = serviceByID.values().iterator();
	    step();
	}

	/** Set reg to the next matching element, or null if none */
	protected void step() {
	    while (iter.hasNext()) {
		reg = (SvcReg)iter.next();
		if (reg.leaseExpiration > now)
		    return;
	    }
	    reg = null;
	}
    }

    /** Iterate over all matching Items by ServiceType. */
    private class TypeItemIter extends ItemIter {
	/** Concrete classes matching tmpl.serviceTypes */
	private final ServiceType[] types;
	/** Current index into types */
	private int typeidx;
	/** SvcRegs obtained from serviceByType for current service type */
	private ArrayList svcs;
	/** Current index into svcs */
	private int svcidx = 0;

	/**
	 * tmp.serviceID == null and
	 * tmpl.serviceTypes is non-empty
	 */
	public TypeItemIter(Template tmpl) {
	    super(tmpl);
	    types = matchingConcreteClasses(tmpl.serviceTypes);
	    typeidx = types.length;
	    step();
	}

	/** Set reg to the next matching element, or null if none */
	protected void step() {
	    do {
		while (--svcidx >= 0) {
		    reg = (SvcReg)svcs.get(svcidx);
		    if (reg.leaseExpiration > now &&
			matchAttributes(tmpl, reg.item))
			return;
		}
	    } while (stepType());
	    reg = null;
	}

	/**
	 * Step to the next type in types, if any, reset svcs and svcidx,
	 * and return false if types exhausted.
	 */
	private boolean stepType() {
	    if (--typeidx < 0)
		return false;
	    svcs = (ArrayList)serviceByType.get(types[typeidx]);
	    svcidx = svcs.size();
	    return true;
	}
    }

    /** Iterate over all matching Items by attribute value. */
    private class AttrItemIter extends ItemIter {
	/** SvcRegs obtained from serviceByAttr for chosen attr */
	protected ArrayList svcs;
	/** Current index into svcs */
	protected int svcidx;

	/**
	 * tmpl.serviceID == null and
	 * tmpl.serviceTypes is empty and
	 * tmpl.attributeSetTemplates[setidx].fields[fldidx] != null
	 */
	public AttrItemIter(Template tmpl, int setidx, int fldidx) {
	    super(tmpl);
	    EntryRep set = tmpl.attributeSetTemplates[setidx];
	    HashMap[] attrMaps =
		(HashMap[])serviceByAttr.get(getDefiningClass(set.eclass,
							      fldidx));
	    if (attrMaps != null && attrMaps[fldidx] != null) {
		svcs = (ArrayList)attrMaps[fldidx].get(set.fields[fldidx]);
		if (svcs != null) {
		    svcidx = svcs.size();
		    step();
		}
	    }
	}

	/** Simple constructor */
	protected AttrItemIter(Template tmpl) {
	    super(tmpl);
	}

	/** Set reg to the next matching element, or null if none. */
	protected void step() {
	    while (--svcidx >= 0) {
		reg = (SvcReg)svcs.get(svcidx);
		if (reg.leaseExpiration > now &&
		    matchAttributes(tmpl, reg.item))
		    return;
	    }
	    reg = null;
	}
    }

    /** Iterate over all matching Items by no-fields entry class. */
    private class EmptyAttrItemIter extends AttrItemIter {

	/**
	 * tmpl.serviceID == null and
	 * tmpl.serviceTypes is empty and
	 * eclass has no fields
	 */
	public EmptyAttrItemIter(Template tmpl, EntryClass eclass) {
	    super(tmpl);
	    svcs = (ArrayList)serviceByEmptyAttr.get(eclass);
	    if (svcs != null) {
		svcidx = svcs.size();
		step();
	    }
	}
    }

    /** Iterate over all matching Items by entry class, dups possible. */
    private class ClassItemIter extends ItemIter {
	/** Entry class to match on */
	private final EntryClass eclass;
	/** Current index into entryClasses */
	private int classidx;
	/** Values iterator for current HashMap */
	private Iterator iter;
	/** SvcRegs obtained from iter or serviceByEmptyAttr */
	private ArrayList svcs;
	/** Current index into svcs */
	private int svcidx = 0;

	/**
	 * tmpl.serviceID == null and
	 * tmpl.serviceTypes is empty and
	 * tmpl.attributeSetTemplates is non-empty
	 */
	public ClassItemIter(Template tmpl) {
	    super(tmpl);
	    dupsPossible = true;
	    eclass = tmpl.attributeSetTemplates[0].eclass;
	    classidx = entryClasses.size();
	    step();
	}

	/** Set reg to the next matching element, or null if none */
	protected void step() {
	    do {
		while (--svcidx >= 0) {
		    reg = (SvcReg)svcs.get(svcidx);
		    if (reg.leaseExpiration > now &&
			matchAttributes(tmpl, reg.item))
			return;
		}
	    } while (stepValue());
	    reg = null;
	}

	/**
	 * Step to the next HashMap value, if any, reset svcs and svcidx,
	 * and return false if everything exhausted.
	 */
	private boolean stepValue() {
	    while (true) {
		if (iter != null && iter.hasNext()) {
		    svcs = (ArrayList)iter.next();
		    svcidx = svcs.size();
		    return true;
		}
		if (!stepClass())
		    return false;
		if (iter == null)
		    return true;
	    }
	}

	/**
	 * Step to the next matching entry class, if any, reset iter
	 * using the HashMap for the last field of the class (and reset
	 * (svcs and svcidx if the entry class has no fields), and
	 * return false if everything exhausted.
	 */
	private boolean stepClass() {
	    while (--classidx >= 0) {
		EntryClass cand = (EntryClass)entryClasses.get(classidx);
		if (!eclass.isAssignableFrom(cand))
		    continue;
		if (cand.getNumFields() > 0) {
		    cand = getDefiningClass(cand, cand.getNumFields() - 1);
		    HashMap[] attrMaps = (HashMap[])serviceByAttr.get(cand);
		    iter = attrMaps[attrMaps.length - 1].values().iterator();
		} else {
		    iter = null;
		    svcs = (ArrayList)serviceByEmptyAttr.get(cand);
		    svcidx = svcs.size();
		}
		return true;
	    }
	    return false;
	}
    }

    /** Iterate over a singleton matching Item by serviceID. */
    private class IDItemIter extends ItemIter {

	/** tmpl.serviceID != null */
	public IDItemIter(Template tmpl) {
	    super(tmpl);
	    reg = (SvcReg)serviceByID.get(tmpl.serviceID);
	    if (reg != null &&
		(reg.leaseExpiration <= now || !matchItem(tmpl, reg.item)))
		reg = null;
	}

	/** Set reg to null */
	protected void step() {
	    reg = null;
	}
    }

    /** An event to be sent, and the listener to send it to. */
    private final class EventTask implements TaskManager.Task {

	/** The event registration */
	public final EventReg reg;
	/** The sequence number of this event */
	public final long seqNo;
	/** The service id */
	public final ServiceID sid;
	/** The new state of the item, or null if deleted */
	public final Item item;
	/** The transition that fired */
	public final int transition;

	/** Simple constructor, except increments reg.seqNo. */
	public EventTask(EventReg reg,
			 ServiceID sid,
			 Item item,
			 int transition)
	{
	    this.reg = reg;
	    seqNo = ++reg.seqNo;
	    this.sid = sid;
	    this.item = item;
	    this.transition = transition;
	}

	/** Send the event */
	public void run() {
	    if (logger.isLoggable(Level.FINE)) {
		logger.log(
		    Level.FINE,
		    "notifying listener {0} of event {1}",
		    new Object[]{ reg.listener, new Long(reg.eventID) });
	    }
	    try {
		reg.listener.notify(new RegistrarEvent(proxy, reg.eventID,
						       seqNo, reg.handback,
						       sid, transition, item));
	    } catch (Throwable e) {
		switch (ThrowableConstants.retryable(e)) {
		case ThrowableConstants.BAD_OBJECT:
		    if (e instanceof Error) {
			logger.log(
			    Levels.HANDLED, "exception sending event", e);
			throw (Error) e;
		    }
		case ThrowableConstants.BAD_INVOCATION:
		case ThrowableConstants.UNCATEGORIZED:
		    /* If the listener throws UnknownEvent or some other
		     * definite exception, we can cancel the lease.
		     */
		    logger.log(Level.INFO, "exception sending event", e);
		    try {
			cancelEventLease(reg.eventID, reg.leaseID);
		    } catch (UnknownLeaseException ee) {
			logger.log(
			    Levels.HANDLED,
			    "exception canceling event lease",
			    e);
		    }
		}
	    }
	}

	/** Keep events going to the same listener ordered. */
	public boolean runAfter(List tasks, int size) {
	    for (int i = size; --i >= 0; ) {
		Object obj = tasks.get(i);
		if (obj instanceof EventTask &&
		    reg.listener.equals(((EventTask)obj).reg.listener))
		    return true;
	    }
	    return false;
	}
    }

    /** Task for decoding multicast request packets. */
    private final class DecodeRequestTask implements TaskManager.Task {

	/** The multicast packet to decode */
	private final DatagramPacket datagram;
	/** The decoder for parsing the packet */
	private final Discovery decoder;

	public DecodeRequestTask(DatagramPacket datagram, Discovery decoder) {
	    this.datagram = datagram;
	    this.decoder = decoder;
	}

	/**
	 * Decodes this task's multicast request packet, spawning an
	 * AddressTask if the packet satisfies the configured constraints,
	 * matches this registrar's groups, and does not already contain this
	 * registrar's service ID in its list of known registrars.  This method
	 * assumes that the protocol version of the request has already been
	 * checked.
	 */
	public void run() {
	    MulticastRequest req;
	    try {
		req = decoder.decodeMulticastRequest(
		    datagram,
		    multicastRequestConstraints.getUnfulfilledConstraints(),
		    multicastRequestSubjectChecker);
	    } catch (Exception e) {
		if (!(e instanceof InterruptedIOException) &&
		    logger.isLoggable(Levels.HANDLED))
		{
		    logThrow(
			Levels.HANDLED,
			getClass().getName(),
			"run",
			"exception decoding multicast request from {0}:{1}",
			new Object[]{
			    datagram.getAddress(),
			    new Integer(datagram.getPort()) },
			e);
		}
		return;
	    }
	    String[] groups = req.getGroups();
	    if ((groups.length == 0 || overlap(memberGroups, groups)) &&
		indexOf(req.getServiceIDs(), myServiceID) < 0)
	    {
		try {
		    tasker.addIfNew(new AddressTask(
			InetAddress.getByName(req.getHost()), req.getPort()));
		} catch (UnknownHostException e) {
		    if (logger.isLoggable(Level.INFO)) {
			logThrow(
			    Level.INFO,
			    getClass().getName(),
			    "run",
			    "failed to resolve host {0}",
			    new Object[]{ req.getHost() },
			    e);
		    }
		}
	    }
	}

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

    /** Address for unicast discovery response. */
    private final class AddressTask implements TaskManager.Task {

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

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

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

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

	/** Connect and then process a unicast discovery request */
	public void run() {
	    try {
		respond(new Socket(addr, port));
	    } catch (Exception e) {
		if (logger.isLoggable(Level.INFO)) {
		    logThrow(
			Level.INFO,
			getClass().getName(),
			"run",
			"failed to connect to {0}:{1}",
			new Object[]{ addr, new Integer(port) },
			e);
		}
	    }
	}

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

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

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

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

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

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

    /** Service lease expiration thread code */
    private class ServiceExpireThread extends Thread {
	/** True if thread has been interrupted */
	private boolean interrupted = false;

	/** Create a daemon thread */
	public ServiceExpireThread() {
	    super("service expire");
	    setDaemon(true);
	}

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

	public synchronized boolean isInterrupted() {
	    return interrupted;
	}

	public void run() {
	    try {
		concurrentObj.writeLock();
	    } catch (ConcurrentLockException e) {
		return;
	    }
	    try {
		while (!isInterrupted()) {
		    long now = System.currentTimeMillis();
		    while (true) {
			SvcReg reg = (SvcReg)serviceByTime.firstKey();
			minSvcExpiration = reg.leaseExpiration;
			if (minSvcExpiration > now)
			    break;
			deleteService(reg, now);
			addLogRecord(new ServiceLeaseCancelledLogObj(
					    reg.item.serviceID, reg.leaseID));
			if (logger.isLoggable(Level.FINE)) {
			    logger.log(
				Level.FINE,
				"expired service registration {0}",
				new Object[]{ reg.item.serviceID });
			}
		    }
		    queueEvents();
		    try {
			concurrentObj.writerWait(serviceNotifier,
						 minSvcExpiration - now);
		    } catch (ConcurrentLockException e) {
			return;
		    }
		}
	     } finally {
		 concurrentObj.writeUnlock();
	     }
	}
    }

    /** Event lease expiration thread code */
    private class EventExpireThread extends Thread {

	/** Create a daemon thread */
	public EventExpireThread() {
	    super("event expire");
	    setDaemon(true);
	}

	public void run() {
	    try {
		concurrentObj.writeLock();
	    } catch (ConcurrentLockException e) {
		return;
	    }
	    try {
		while (!isInterrupted()) {
		    long now = System.currentTimeMillis();
		    minEventExpiration = Long.MAX_VALUE;
		    while (!eventByTime.isEmpty()) {
			EventReg reg = (EventReg)eventByTime.firstKey();
			if (reg.leaseExpiration > now) {
			    minEventExpiration = reg.leaseExpiration;
			    break;
			}
			deleteEvent(reg);
			if (logger.isLoggable(Level.FINE)) {
			    logger.log(
				Level.FINE,
				"expired event registration {0} for {1}",
				new Object[]{ reg.leaseID, reg.listener });
			}
		    }
		    try {
			concurrentObj.writerWait(eventNotifier,
						 minEventExpiration - now);
		    } catch (ConcurrentLockException e) {
			return;
		    }
		}
	     } finally {
		 concurrentObj.writeUnlock();
	     }
	}
    }

    /**
     * Termination thread code.  We do this in a separate thread to
     * avoid deadlock, because ActivationGroup.inactive will block until
     * in-progress RMI calls are finished.
     */
    private class DestroyThread extends Thread {
        /** Maximum delay for unexport attempts */
        private static final long MAX_UNEXPORT_DELAY = 2*TimeConstants.MINUTES;

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

	public void run() {

	    /* must unregister before unexport */
	    if (activationID != null) {
		try {
		    activationSystem.unregisterObject(activationID);
		} catch (RemoteException e) {
		    /* give up until we can at least unregister */
		    logger.log(Level.WARNING, "aborting shutdown", e);
		    return;
		} catch (ActivationException e) {
		    logger.log(
			Level.INFO,
			"exception unregistering activation ID",
			e);
		}
	    }

            long endTime = System.currentTimeMillis() + MAX_UNEXPORT_DELAY;
            boolean unexported = false;
	    /* first try unexporting politely */
	    while(!unexported && (System.currentTimeMillis() < endTime)) {
		unexported = serverExporter.unexport(false);
		if (!unexported) {
		    Thread.yield();
		}
	    }
	    /* if still not unexported, forcibly unexport */
            if(!unexported) {
		serverExporter.unexport(true);
            }

	    /* all daemons must terminate before deleting persistent store */
	    serviceExpirer.interrupt();
	    eventExpirer.interrupt();
	    unicaster.interrupt();
	    multicaster.interrupt();
	    announcer.interrupt();
	    snapshotter.interrupt();
	    tasker.terminate();
	    joiner.terminate();
	    discoer.terminate();
	    try {
		serviceExpirer.join();
		eventExpirer.join();
		unicaster.join();
		multicaster.join();
		announcer.join();
		snapshotter.join();
	    } catch (InterruptedException e) {
	    }
	    closeRequestSockets(tasker.getPending());
	    if (log != null) {
		log.deletePersistentStore();
		logger.finer("deleted persistence directory");
	    }
	    if (activationID != null) {
		try {
		    ActivationGroup.inactive(activationID, serverExporter);
		} catch (Exception e) {
		    logger.log(Level.INFO, "exception going inactive", e);
		}
	    }
	    if (lifeCycle != null) {
		lifeCycle.unregister(RegistrarImpl.this);
	    }
	    if (loginContext != null) {
		try {
		    loginContext.logout();
		} catch (LoginException e) {
		    logger.log(Level.INFO, "logout failed", e);
		}
	    }
	    logger.info("Reggie shutdown completed");
	}
    }

    /** Multicast discovery request thread code. */
    private class MulticastThread extends Thread {

	/** Multicast group address used by multicast requests */
	private final InetAddress requestAddr;
	/** Multicast socket to receive packets */
	private final MulticastSocket socket;
	/** Interfaces for which configuration failed */
	private final List failedInterfaces = new ArrayList();

	/**
	 * Create a high priority daemon thread.  Set up the socket now
	 * rather than in run, so that we get any exception up front.
	 */
	public MulticastThread() throws IOException {
	    super("multicast request");
	    setDaemon(true);
	    if (multicastInterfaces != null && multicastInterfaces.length == 0)
	    {
		requestAddr = null;
		socket = null;
		return;
	    }
	    requestAddr = Constants.getRequestAddress();
	    socket = new MulticastSocket(Constants.discoveryPort);
	    if (multicastInterfaces != null) {
		Level failureLogLevel = multicastInterfacesSpecified ?
		    Level.WARNING : Levels.HANDLED;
		for (int i = 0; i < multicastInterfaces.length; i++) {
		    NetworkInterface nic = multicastInterfaces[i];
		    try {
			socket.setNetworkInterface(nic);
			socket.joinGroup(requestAddr);
		    } catch (IOException e) {
			failedInterfaces.add(nic);
			if (logger.isLoggable(failureLogLevel)) {
			    logThrow(
				failureLogLevel,
				getClass().getName(),
				"<init>",
				"exception enabling {0}",
				new Object[]{ nic },
				e);
			}
		    }
		}
	    } else {
		try {
		    socket.joinGroup(requestAddr);
		} catch (IOException e) {
		    failedInterfaces.add(null);
		    logger.log(
			Level.WARNING,
			"exception enabling default interface", e);
		}
	    }
	}

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

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

	public synchronized boolean isInterrupted() {
	    return interrupted;
	}

	public void run() {
	    if (multicastInterfaces != null && multicastInterfaces.length == 0)
	    {
		return;
	    }
	    byte[] buf = new byte[
		multicastRequestConstraints.getMulticastMaxPacketSize(
		    DEFAULT_MAX_PACKET_SIZE)];
	    DatagramPacket dgram = new DatagramPacket(buf, buf.length);
	    long retryTime =
		System.currentTimeMillis() + multicastInterfaceRetryInterval;
	    while (!isInterrupted()) {
		try {
		    int timeout = 0;
		    if (!failedInterfaces.isEmpty()) {
			timeout =
			    (int) (retryTime - System.currentTimeMillis());
			if (timeout <= 0) {
			    retryFailedInterfaces();
			    if (failedInterfaces.isEmpty()) {
				timeout = 0;
			    } else {
				timeout = multicastInterfaceRetryInterval;
				retryTime =
				    System.currentTimeMillis() + timeout;
			    }
			}
		    }
		    socket.setSoTimeout(timeout);
		    dgram.setLength(buf.length);
		    try {
			socket.receive(dgram);
		    } catch (NullPointerException e) {
			break; // workaround for bug 4190513
		    }

		    int pv;
		    try {
			pv = ByteBuffer.wrap(dgram.getData(),
					     dgram.getOffset(),
					     dgram.getLength()).getInt();
		    } catch (BufferUnderflowException e) {
			throw new DiscoveryProtocolException(null, e);
		    }
		    multicastRequestConstraints.checkProtocolVersion(pv);
		    tasker.add(new DecodeRequestTask(dgram, getDiscovery(pv)));

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

		} catch (SocketTimeoutException e) {
		    // retry failed network interfaces in next iteration
		} catch (InterruptedIOException e) {
		    break;
		} catch (Exception e) {
		    if (isInterrupted()) {
			break;
		    }
		    logger.log(Levels.HANDLED,
			       "exception receiving multicast request", e);
		}
	    }
	    socket.close();
	}

	/**
	 * Attempts to configure each interface contained in the
	 * failedInterfaces list, removing it from the list if configuration
	 * succeeds.  The null value is used to indicate the default network
	 * interface.
	 */
	private void retryFailedInterfaces() {
	    for (Iterator i = failedInterfaces.iterator(); i.hasNext(); ) {
		NetworkInterface nic = (NetworkInterface) i.next();
		try {
		    if (nic != null) {
			socket.setNetworkInterface(nic);
		    }
		    socket.joinGroup(requestAddr);
		    i.remove();

		    Level l = multicastInterfacesSpecified ?
			Level.INFO : Level.FINE;
		    if (logger.isLoggable(l)) {
			if (nic != null) {
			    logger.log(l, "enabled {0}", new Object[]{ nic });
			} else {
			    logger.log(l, "enabled default interface");
			}
		    }
		} catch (IOException e) {
		    // keep nic in failedInterfaces
		}
	    }
	}
    }

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

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

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

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

	public synchronized boolean isInterrupted() {
	    return interrupted;
	}

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

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

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

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

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

	/**
	 * Attempts to multicast the given packets on each of the configured
	 * network interfaces.
	 */
	private void send(DatagramPacket[] packets)
	    throws InterruptedIOException
	{
	    if (multicastInterfaces != null) {
		Level failureLogLevel = multicastInterfacesSpecified ?
		    Level.WARNING : Levels.HANDLED;
		for (int i = 0; i < multicastInterfaces.length; i++) {
		    send(packets, multicastInterfaces[i], failureLogLevel);
		}
	    } else {
		send(packets, null, Level.WARNING);
	    }
	}

	/**
	 * Attempts to multicast the given packets on the specified network
	 * interface, logging failures at the given logging level.  If the
	 * specified network interface is null, then the default interface is
	 * used.
	 */
	private void send(DatagramPacket[] packets,
			  NetworkInterface nic,
			  Level failureLogLevel)
	    throws InterruptedIOException
	{
	    if (nic != null) {
		try {
		    socket.setNetworkInterface(nic);
		} catch (SocketException e) {
		    if (logger.isLoggable(failureLogLevel)) {
			logThrow(
			    failureLogLevel,
			    getClass().getName(),
			    "send",
			    "exception setting {0}",
			    new Object[]{ nic },
			    e);
		    }
		    return;
		}
	    }
	    for (int i = 0; i < packets.length; i++) {
		try {
		    socket.send(packets[i]);
		} catch (InterruptedIOException e) {
		    throw e;
		} catch (IOException e) {
		    if (nic != null) {
			if (logger.isLoggable(failureLogLevel)) {
			    logThrow(
				failureLogLevel,
				getClass().getName(),
				"send",
				"exception sending packet on {0}",
				new Object[]{ nic },
				e);
			}
		    } else {
			logger.log(
			    failureLogLevel,
			    "exception sending packet on default interface",
			    e);
		    }
		}
	    }
	}
    }

    /**
     * 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 the Registrar 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 there will be far more readers (e.g. lookups) in use 
     * than writers (e.g. add/mod/del Attributes). 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 requests 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 snapshotNotifier instance until it is notified
     * (or "signalled") that a snapshot must be taken. The notification
     * is sent by another thread, created by the Registrar, 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 lookups can be
     * performed; and the reader mutex prevents changes to the data 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();
	}

	public synchronized boolean isInterrupted() {
	    return interrupted;
	}

	public void run() {
	    if (log == null) {
		return;
	    }
	    try {
		concurrentObj.readLock();
	    } catch (ConcurrentLockException e) {
		return;
	    }
	    try {
		while (!isInterrupted()) {
		    try {
			concurrentObj.readerWait(snapshotNotifier,
						 Long.MAX_VALUE);
                        try {
                            log.snapshot();
		            logFileSize = 0;
	                } catch (Exception e) {
			    if (isInterrupted())
				return;
			    logger.log(Level.WARNING, "snapshot failed", e);
		        }
		    } catch (ConcurrentLockException e) {
			return;
		    }
		}
	    } finally {
		concurrentObj.readUnlock();
	    }
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public Object getServiceProxy() throws RemoteException {
	return proxy;
    }

    // This method's javadoc is inherited from an interface of this class
    public Object getProxy() {
	return myRef;
    }

    // This method's javadoc is inherited from an interface of this class
    public TrustVerifier getProxyVerifier() {
	return new ProxyVerifier(myRef, myServiceID);
    }

    // This method's javadoc is inherited from an interface of this class
    public ServiceRegistration register(Item nitem, long leaseDuration)
    {
	concurrentObj.writeLock();
	try {
	    ServiceRegistration reg = registerDo(nitem, leaseDuration);
	    if (logger.isLoggable(Level.FINE)) {
		logger.log(
		    Level.FINE,
		    "registered instance of {0} as {1}",
		    new Object[]{
			nitem.serviceType.name, reg.getServiceID() });
	    }
	    return reg;
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public MarshalledWrapper lookup(Template tmpl)
    {
	concurrentObj.readLock();
	try {
	    return lookupDo(tmpl);
	} finally {
	    concurrentObj.readUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public Matches lookup(Template tmpl, int maxMatches)
    {
	concurrentObj.readLock();
	try {
	    return lookupDo(tmpl, maxMatches);
	} finally {
	    concurrentObj.readUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public EventRegistration notify(Template tmpl,
				    int transitions,
				    RemoteEventListener listener,
				    MarshalledObject handback,
				    long leaseDuration)
	throws RemoteException
    {
	concurrentObj.writeLock();
	try {
	    EventRegistration reg = notifyDo(
		tmpl, transitions, listener, handback, leaseDuration);
	    if (logger.isLoggable(Level.FINE)) {
		logger.log(
		    Level.FINE,
		    "registered event listener {0} as {1}",
		    new Object[]{
			listener,
			((ReferentUuid) reg.getLease()).getReferentUuid()
		    });
	    }
	    return reg;
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public EntryClassBase[] getEntryClasses(Template tmpl)
    {
	concurrentObj.readLock();
	try {
	    return getEntryClassesDo(tmpl);
	} finally {
	    concurrentObj.readUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public Object[] getFieldValues(Template tmpl, int setIndex, int field)
    {
	concurrentObj.readLock();
	try {
	    return getFieldValuesDo(tmpl, setIndex, field);
	} finally {
	    concurrentObj.readUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public ServiceTypeBase[] getServiceTypes(Template tmpl, String prefix)
    {
	concurrentObj.readLock();
	try {
	    return getServiceTypesDo(tmpl, prefix);
	} finally {
	    concurrentObj.readUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public LookupLocator getLocator() {
	return myLocator;
    }

    // This method's javadoc is inherited from an interface of this class
    public Object getAdmin() throws RemoteException {
	return AdminProxy.getInstance(myRef, myServiceID);
    }

    // This method's javadoc is inherited from an interface of this class
    public void addAttributes(ServiceID serviceID,
			      Uuid leaseID,
			      EntryRep[] attrSets)
	throws UnknownLeaseException
    {
	if (serviceID.equals(myServiceID))
	    throw new SecurityException("privileged service id");
	concurrentObj.writeLock();
	try {
	    addAttributesDo(serviceID, leaseID, attrSets);
	    addLogRecord(new AttrsAddedLogObj(serviceID, leaseID, attrSets));
	    queueEvents();
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void modifyAttributes(ServiceID serviceID,
				 Uuid leaseID,
				 EntryRep[] attrSetTmpls,
				 EntryRep[] attrSets)
	throws UnknownLeaseException
    {
	if (serviceID.equals(myServiceID))
	    throw new SecurityException("privileged service id");
	concurrentObj.writeLock();
	try {
	    modifyAttributesDo(serviceID, leaseID, attrSetTmpls, attrSets);
	    addLogRecord(new AttrsModifiedLogObj(serviceID, leaseID,
						 attrSetTmpls, attrSets));
	    queueEvents();
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void setAttributes(ServiceID serviceID,
			      Uuid leaseID,
			      EntryRep[] attrSets)
	throws UnknownLeaseException
    {
	if (serviceID.equals(myServiceID))
	    throw new SecurityException("privileged service id");
	concurrentObj.writeLock();
	try {
	    setAttributesDo(serviceID, leaseID, attrSets);
	    addLogRecord(new AttrsSetLogObj(serviceID, leaseID, attrSets));
	    queueEvents();
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void cancelServiceLease(ServiceID serviceID, Uuid leaseID)
	throws UnknownLeaseException
    {
	concurrentObj.writeLock();
	try {
	    cancelServiceLeaseDo(serviceID, leaseID);
	    addLogRecord(new ServiceLeaseCancelledLogObj(serviceID, leaseID));
	    queueEvents();
	    if (logger.isLoggable(Level.FINE)) {
		logger.log(
		    Level.FINE,
		    "cancelled service registration {0}",
		    new Object[]{ serviceID });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public long renewServiceLease(ServiceID serviceID,
				  Uuid leaseID,
				  long renewDuration)
	throws UnknownLeaseException
    {
	concurrentObj.priorityWriteLock();
	try {
	    return renewServiceLeaseDo(serviceID, leaseID, renewDuration);
	    /* addLogRecord is in renewServiceLeaseDo */
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void cancelEventLease(long eventID, Uuid leaseID)
	throws UnknownLeaseException
    {
	concurrentObj.writeLock();
	try {
	    cancelEventLeaseDo(eventID, leaseID);
	    addLogRecord(new EventLeaseCancelledLogObj(eventID, leaseID));
	    if (logger.isLoggable(Level.FINE)) {
		logger.log(
		    Level.FINE,
		    "cancelled event registration {0}",
		    new Object[]{ leaseID });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public long renewEventLease(long eventID, Uuid leaseID, long renewDuration)
	throws UnknownLeaseException
    {
	concurrentObj.priorityWriteLock();
	try {
	    return renewEventLeaseDo(eventID, leaseID, renewDuration);
	    /* addLogRecord is in renewEventLeaseDo */
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public RenewResults renewLeases(Object[] regIDs,
				    Uuid[] leaseIDs,
				    long[] renewDurations)
    {
	concurrentObj.priorityWriteLock();
	try {
	    return renewLeasesDo(regIDs, leaseIDs, renewDurations);
	    /* addLogRecord is in renewLeasesDo */
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public Exception[] cancelLeases(Object[] regIDs, Uuid[] leaseIDs)
    {
	concurrentObj.writeLock();
	try {
	    Exception[] exceptions = cancelLeasesDo(regIDs, leaseIDs);
	    addLogRecord(new LeasesCancelledLogObj(regIDs, leaseIDs));
	    queueEvents();
	    if (logger.isLoggable(Level.FINE)) {
		for (int i = 0; i < regIDs.length; i++) {
		    if (exceptions != null && exceptions[i] != null) {
			continue;
		    }
		    if (regIDs[i] instanceof ServiceID) {
			logger.log(
			    Level.FINE,
			    "cancelled service registration {0}",
			    new Object[]{ regIDs[i] });
		    } else {
			logger.log(
			    Level.FINE,
			    "cancelled event registration {0}",
			    new Object[]{ leaseIDs[i] });
		    }
		}
	    }
	    return exceptions;
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public Entry[] getLookupAttributes() throws RemoteException {
	concurrentObj.readLock();
	try {
	    /* no need to clone, never modified once created */
	    return lookupAttrs;
	} finally {
	    concurrentObj.readUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void addLookupAttributes(Entry[] attrSets) throws RemoteException {
	concurrentObj.writeLock();
	try {
	    joiner.addAttributes(attrSets, true);
	    lookupAttrs = joiner.getAttributes();
	    EntryRep[] attrs = EntryRep.toEntryRep(attrSets, true);
	    try {
		addAttributesDo(myServiceID, myLeaseID, attrs);
	    } catch (UnknownLeaseException e) {
	    }
	    addLogRecord(new AttrsAddedLogObj(myServiceID, myLeaseID, attrs));
	    queueEvents();
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void modifyLookupAttributes(Entry[] attrSetTemplates,
				       Entry[] attrSets)
	throws RemoteException
    {
	concurrentObj.writeLock();
	try {
	    joiner.modifyAttributes(attrSetTemplates, attrSets, true);
	    lookupAttrs = joiner.getAttributes();
	    EntryRep[] tmpls = EntryRep.toEntryRep(attrSetTemplates, false);
	    EntryRep[] attrs = EntryRep.toEntryRep(attrSets, false);
	    try {
		modifyAttributesDo(myServiceID, myLeaseID, tmpls, attrs);
	    } catch (UnknownLeaseException e) {
	    }
	    addLogRecord(new AttrsModifiedLogObj(myServiceID, myLeaseID,
						 tmpls, attrs));
	    queueEvents();
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public String[] getLookupGroups() throws RemoteException {
	concurrentObj.readLock();
	try {
	    /* no need to clone, never modified once created */
	    return lookupGroups;
	} finally {
	    concurrentObj.readUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void addLookupGroups(String[] groups) throws RemoteException {
	concurrentObj.writeLock();
	try {
	    DiscoveryGroupManagement dgm = (DiscoveryGroupManagement) discoer;
	    try {
		dgm.addGroups(groups);
	    } catch (IOException e) {
		throw new RuntimeException(e.toString());
	    }
	    lookupGroups = dgm.getGroups();
	    addLogRecord(new LookupGroupsChangedLogObj(lookupGroups));
	    if (logger.isLoggable(Level.CONFIG)) {
		logger.log(
		    Level.CONFIG,
		    "added lookup groups {0}",
		    new Object[]{ Arrays.asList(groups) });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void removeLookupGroups(String[] groups) throws RemoteException {
	concurrentObj.writeLock();
	try {
	    DiscoveryGroupManagement dgm = (DiscoveryGroupManagement) discoer;
	    dgm.removeGroups(groups);
	    lookupGroups = dgm.getGroups();
	    addLogRecord(new LookupGroupsChangedLogObj(lookupGroups));
	    if (logger.isLoggable(Level.CONFIG)) {
		logger.log(
		    Level.CONFIG,
		    "removed lookup groups {0}",
		    new Object[]{ Arrays.asList(groups) });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void setLookupGroups(String[] groups) throws RemoteException {
	concurrentObj.writeLock();
	try {
	    DiscoveryGroupManagement dgm = (DiscoveryGroupManagement) discoer;
	    try {
		dgm.setGroups(groups);
	    } catch (IOException e) {
		throw new RuntimeException(e.toString());
	    }
	    lookupGroups = dgm.getGroups();
	    addLogRecord(new LookupGroupsChangedLogObj(lookupGroups));
	    if (logger.isLoggable(Level.CONFIG)) {
		logger.log(
		    Level.CONFIG,
		    "set lookup groups {0}",
		    new Object[]{ 
			(groups != null) ? Arrays.asList(groups) : null });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public LookupLocator[] getLookupLocators() throws RemoteException {
	concurrentObj.readLock();
	try {
	    /* no need to clone, never modified once created */
	    return lookupLocators;
	} finally {
	    concurrentObj.readUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void addLookupLocators(LookupLocator[] locators)
	throws RemoteException
    {
	locators = prepareLocators(locators, locatorPreparer, false);
	concurrentObj.writeLock();
	try {
	    DiscoveryLocatorManagement dlm = 
		(DiscoveryLocatorManagement) discoer;
	    dlm.addLocators(locators);
	    lookupLocators = dlm.getLocators();
	    addLogRecord(new LookupLocatorsChangedLogObj(lookupLocators));
	    if (logger.isLoggable(Level.CONFIG)) {
		logger.log(
		    Level.CONFIG,
		    "added lookup locators {0}",
		    new Object[]{ Arrays.asList(locators) });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void removeLookupLocators(LookupLocator[] locators)
	throws RemoteException
    {
	locators = prepareLocators(locators, locatorPreparer, false);
	concurrentObj.writeLock();
	try {
	    DiscoveryLocatorManagement dlm = 
		(DiscoveryLocatorManagement) discoer;
	    dlm.removeLocators(locators);
	    lookupLocators = dlm.getLocators();
	    addLogRecord(new LookupLocatorsChangedLogObj(lookupLocators));
	    if (logger.isLoggable(Level.CONFIG)) {
		logger.log(
		    Level.CONFIG,
		    "removed lookup locators {0}",
		    new Object[]{ Arrays.asList(locators) });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void setLookupLocators(LookupLocator[] locators)
	throws RemoteException
    {
	locators = prepareLocators(locators, locatorPreparer, false);
	concurrentObj.writeLock();
	try {
	    DiscoveryLocatorManagement dlm = 
		(DiscoveryLocatorManagement) discoer;
	    dlm.setLocators(locators);
	    lookupLocators = dlm.getLocators();
	    addLogRecord(new LookupLocatorsChangedLogObj(lookupLocators));
	    if (logger.isLoggable(Level.CONFIG)) {
		logger.log(
		    Level.CONFIG,
		    "set lookup locators {0}",
		    new Object[]{ Arrays.asList(locators) });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void addMemberGroups(String[] groups) throws RemoteException {
	concurrentObj.writeLock();
	try {
	    for (int i = 0; i < groups.length; i++) {
	        if (indexOf(memberGroups, groups[i]) < 0)
		    memberGroups = (String[])arrayAdd(memberGroups, groups[i]);
	    }
	    synchronized (announcer) {
		announcer.notify();
	    }
	    addLogRecord(new MemberGroupsChangedLogObj(memberGroups));
	    if (logger.isLoggable(Level.CONFIG)) {
		logger.log(
		    Level.CONFIG,
		    "added member groups {0}",
		    new Object[]{ Arrays.asList(groups) });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void removeMemberGroups(String[] groups) throws RemoteException {
	concurrentObj.writeLock();
	try {
	    for (int i = 0; i < groups.length; i++) {
                int j = indexOf(memberGroups, groups[i]);
	        if (j >= 0)
		    memberGroups = (String[])arrayDel(memberGroups, j);
	    }
	    synchronized (announcer) {
		announcer.notify();
	    }
	    addLogRecord(new MemberGroupsChangedLogObj(memberGroups));
	    if (logger.isLoggable(Level.CONFIG)) {
		logger.log(
		    Level.CONFIG,
		    "removed member groups {0}",
		    new Object[]{ Arrays.asList(groups) });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public String[] getMemberGroups() {
	concurrentObj.readLock();
	try {
	    /* no need to clone, never modified once created */
	    return memberGroups;
	} finally {
	    concurrentObj.readUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void setMemberGroups(String[] groups) throws RemoteException {
	concurrentObj.writeLock();
	try {
	    memberGroups = (String[])removeDups(groups);
	    addLogRecord(new MemberGroupsChangedLogObj(memberGroups));
	    synchronized (announcer) {
		announcer.notify();
	    }
	    if (logger.isLoggable(Level.CONFIG)) {
		logger.log(
		    Level.CONFIG,
		    "set member groups {0}",
		    new Object[]{ Arrays.asList(groups) });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public int getUnicastPort() throws RemoteException {
	concurrentObj.readLock();
	try {
	    return unicastPort;
	} finally {
	    concurrentObj.readUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void setUnicastPort(int port) throws IOException,RemoteException {
	if (port == unicastPort)
	    return;
	concurrentObj.writeLock();
	try {
	    if ((port == 0 && unicaster.port == Constants.discoveryPort) ||
		port == unicaster.port)
	    {
		unicastPort = port;
		addLogRecord(new UnicastPortSetLogObj(port));
		return;
	    }
	    /* create a UnicastThread that listens on the new port */
	    UnicastThread newUnicaster = new UnicastThread(port);
	    /* terminate the current UnicastThread listening on the old port */
	    unicaster.interrupt();
	    try {
		unicaster.join();
	    } catch (InterruptedException e) { }
	    /* start the UnicastThread listening on the new port */
	    unicaster = newUnicaster;
	    unicaster.start();
	    unicastPort = port;
	    myLocator = (proxy instanceof RemoteMethodControl) ?
		new ConstrainableLookupLocator(
		    myLocator.getHost(), unicaster.port, null) :
		new LookupLocator(myLocator.getHost(), unicaster.port);
	    synchronized (announcer) {
		announcer.notify();
	    }
	    addLogRecord(new UnicastPortSetLogObj(port));
	    if (logger.isLoggable(Level.CONFIG)) {
		logger.log(
		    Level.CONFIG,
		    "changed unicast discovery port to {0}",
		    new Object[]{ new Integer(unicaster.port) });
	    }
	} finally {
	    concurrentObj.writeUnlock();
	}
    }

    // This method's javadoc is inherited from an interface of this class
    public void destroy() {
	logger.info("starting Reggie shutdown");
	new DestroyThread().start();
    }

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

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

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

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

    /** Return true if the array is null or zero length */
    private static boolean isEmpty(Object[] array) {
	return (array == null || array.length == 0);
    }

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

    /** Test if all elements of the array are null. */
    private static boolean allNull(Object[] array) {
	for (int i = array.length; --i >= 0; ) {
	    if (array[i] != null)
		return false;
	}
	return true;
    }

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

    /** Delete item.attributeSets[i] and return the new array. */
    private static EntryRep[] deleteSet(Item item, int i) {
	item.attributeSets = (EntryRep[])arrayDel(item.attributeSets, i);
	return item.attributeSets;
    }

    /**
     * Do a deep copy of the item, and substitute replacements for all
     * embedded EntryClass instances and null for the ServiceType and
     * codebase (since they aren't needed on the client side).
     */
    private static Item copyItem(Item item) {
	item = (Item)item.clone();
	item.serviceType = null;
	item.codebase = null;
	EntryRep[] attrSets = item.attributeSets;
	for (int i = attrSets.length; --i >= 0; ) {
	    attrSets[i].eclass = attrSets[i].eclass.getReplacement();
	}
	return item;
    }

    /**
     * Return the first (highest) class that defines the given field.  This
     * would be a method on EntryClass, but we want to minimize code
     * downloaded into the client.
     */
    private static EntryClass getDefiningClass(EntryClass eclass, int fldidx) {
	while (true) {
	    EntryClass sup = eclass.getSuperclass();
	    if (sup.getNumFields() <= fldidx)
		return eclass;
	    eclass = sup;
	}
    }

    /**
     * Add conc as a concrete class of the given type and all supertypes,
     * if it isn't already there.  (It might already be there due to
     * diamond merges in the interface hierarchy.)  This would be a method on
     * ServiceType, but we want to minimize code downloaded into the client.
     */
    private static void addConcreteClass(ServiceType type, ServiceType conc) {
	ServiceType[] types = type.getConcreteClasses();
	if (indexOf(types, conc) >= 0)
	    return;
	type.setConcreteClasses((ServiceType[])arrayAdd(types, conc));
	ServiceType[] ifaces = type.getInterfaces();
	for (int i = ifaces.length; --i >= 0; ) {
	    addConcreteClass(ifaces[i], conc);
	}
	ServiceType sup = type.getSuperclass();
	if (sup != null)
	    addConcreteClass(sup, conc);
    }

    /**
     * Delete conc as a concrete class of the given type and all supertypes,
     * if it's there.  (It might already be delete due to diamond merges
     * in the interface hierarchy.)  This would be a method on ServiceType,
     * but we want to minimize code downloaded into the client.
     */
    private static void deleteConcreteClass(ServiceType type, ServiceType conc)
    {
	ServiceType[] types = type.getConcreteClasses();
	int i = indexOf(types, conc);
	if (i >= 0) {
	    type.setConcreteClasses((ServiceType[])arrayDel(types, i));
	    ServiceType[] ifaces = type.getInterfaces();
	    for (int j = ifaces.length; --j >= 0; ) {
		deleteConcreteClass(ifaces[j], conc);
	    }
	    ServiceType sup = type.getSuperclass();
	    if (sup != null)
		deleteConcreteClass(sup, conc);
	}
    }

    /**
     * Test if an item matches a template.  This would be a method on
     * Template, but we want to minimize code downloaded into the client.
     */
    private static boolean matchItem(Template tmpl, Item item) {
	return ((tmpl.serviceID == null ||
		 tmpl.serviceID.equals(item.serviceID)) &&
		matchType(tmpl.serviceTypes, item.serviceType) &&
		matchAttributes(tmpl, item));
    }

    /** Test if a type is equal to or a subtype of every type in an array. */
    private static boolean matchType(ServiceType[] types, ServiceType type) {
	if (types != null) {
	    for (int i = types.length; --i >= 0; ) {
		if (!types[i].isAssignableFrom(type))
		    return false;
	    }
	}
	return true;
    }

    /**
     * Test if an entry matches a template.  This would be a method on
     * EntryRep, but we want to minimize code downloaded into the client.
     */
    private static boolean matchEntry(EntryRep tmpl, EntryRep entry) {
	if (!tmpl.eclass.isAssignableFrom(entry.eclass) ||
	    tmpl.fields.length > entry.fields.length)
	    return false;
	for (int i = tmpl.fields.length; --i >= 0; ) {
	    if (tmpl.fields[i] != null &&
		!tmpl.fields[i].equals(entry.fields[i]))
		return false;
	}
	return true;
    }

    /**
     * Test if there is at least one matching entry in the Item for
     * each entry template in the Template.
     */
    private static boolean matchAttributes(Template tmpl, Item item) {
	EntryRep[] tmpls = tmpl.attributeSetTemplates;
	if (tmpls != null) {
	    EntryRep[] entries = item.attributeSets;
	outer:
	    for (int i = tmpls.length; --i >= 0; ) {
		EntryRep etmpl = tmpls[i];
		for (int j = entries.length; --j >= 0; ) {
		    if (matchEntry(etmpl, entries[j]))
			continue outer;
		}
		return false;
	    }
	}
	return true;
    }

    /**
     * Test if an entry either doesn't match any template in an array,
     * or matches a template but is a subclass of the template type.
     */
    private static boolean attrMatch(EntryRep[] tmpls, EntryRep attrSet) {
	boolean good = true;
	if (tmpls != null) {
	    for (int i = tmpls.length; --i >= 0; ) {
		EntryRep tmpl = tmpls[i];
		if (matchEntry(tmpl, attrSet)) {
		    if (tmpl.eclass.isAssignableFrom(attrSet.eclass) &&
			!tmpl.eclass.equals(attrSet.eclass))
			return true;
		    good = false;
		}
	    }
	}
	return good;
    }

    /**
     * Test if the service has an entry of the given class or subclass
     * with a field of the given value.
     */
    private static boolean hasAttr(SvcReg reg,
				   EntryClass eclass,
				   int fldidx,
				   Object value)
    {
	EntryRep[] sets = reg.item.attributeSets;
	for (int i = sets.length; --i >= 0; ) {
	    EntryRep set = sets[i];
	    if (eclass.isAssignableFrom(set.eclass) &&
		((value == null && set.fields[fldidx] == null) ||
		 (value != null && value.equals(set.fields[fldidx]))))
		return true;
	}
	return false;
    }

    /**
     * Test if the service has an entry of the exact given class (assumed
     * to have no fields).
     */
    private static boolean hasEmptyAttr(SvcReg reg, EntryClass eclass) {
	EntryRep[] sets = reg.item.attributeSets;
	for (int i = sets.length; --i >= 0; ) {
	    if (eclass.equals(sets[i].eclass))
		return true;
	}
	return false;
    }

    /**
     * Find the most specific types (of type) that don't match prefix and
     * aren't equal to or a supertype of any types in bases, and add them
     * to types.
     */
    private static void addTypes(ArrayList types,
				 ArrayList codebases,
				 ServiceType[] bases,
				 String prefix,
				 ServiceType type,
				 String codebase)
    {
	if (types.contains(type))
	    return;
	if (bases != null) {
	    for (int i = bases.length; --i >= 0; ) {
		if (type.isAssignableFrom(bases[i]))
		    return;
	    }
	}
	if (prefix == null || type.getName().startsWith(prefix)) {
	    types.add(type);
	    codebases.add(codebase);
	    return;
	}
	ServiceType[] ifs = type.getInterfaces();
	for (int i = ifs.length; --i >= 0; ) {
	    addTypes(types, codebases, bases, prefix, ifs[i], codebase);
	}
	ServiceType sup = type.getSuperclass();
	if (sup != null)
	    addTypes(types, codebases, bases, prefix, sup, codebase);
    }

    /** Limit leaseDuration by limit, and check for negative value. */
    private static long limitDuration(long leaseDuration, long limit) {
	if (leaseDuration == Lease.ANY || leaseDuration > limit)
	    leaseDuration = limit;
	else if (leaseDuration < 0)
	    throw new IllegalArgumentException("negative lease duration");
	return leaseDuration;
    }

    /**
     * Writes locators to the given stream as a null-terminated list of
     * MarshalledInstances.
     */
    private static void marshalLocators(LookupLocator[] locators,
					ObjectOutputStream out)
	throws IOException
    {
	for (int i = 0; i < locators.length; i++) {
	    out.writeObject(new MarshalledInstance(locators[i]));
	}
	out.writeObject(null);
    }

    /**
     * Returns locators unmarshalled from a null-terminated list of
     * MarshalledInstances read from the given stream, logging (but tolerating)
     * unmarshalling failures.
     */
    private static LookupLocator[] unmarshalLocators(ObjectInputStream in)
	throws IOException, ClassNotFoundException
    {
	List l = new ArrayList();
	MarshalledInstance mi;
	while ((mi = (MarshalledInstance) in.readObject()) != null) {
	    try {
		l.add((LookupLocator) mi.get(false));
	    } catch (Throwable e) {
		if (e instanceof Error &&
		    ThrowableConstants.retryable(e) ==
			ThrowableConstants.BAD_OBJECT)
		{
		    throw (Error) e;
		}
		logger.log(
		    Level.WARNING, "failed to recover lookup locator", e);
	    }
	}
	return (LookupLocator[]) l.toArray(new LookupLocator[l.size()]);
    }
				
    /**
     * Returns new array containing locators from the given non-null array
     * prepared using the given proxy preparer.  If tolerateFailures is false,
     * then any proxy preparation exception is propagated to the caller.
     * Otherwise, such exceptions are logged, and only successfully prepared
     * locators are included in the returned array.
     */
    private static LookupLocator[] prepareLocators(LookupLocator[] locators,
						   ProxyPreparer preparer,
						   boolean tolerateFailures)
	throws RemoteException
    {
	List l = new ArrayList();
	for (int i = 0; i < locators.length; i++) {
	    try {
		l.add((LookupLocator) preparer.prepareProxy(locators[i]));
	    } catch (Exception e) {
		if (!tolerateFailures) {
		    if (e instanceof RemoteException) {
			throw (RemoteException) e;
		    } else {
			throw (RuntimeException) e;
		    }
		}
		if (logger.isLoggable(Level.WARNING)) {
		    logThrow(
			Level.WARNING,
			RegistrarImpl.class.getName(),
			"prepareLocators",
			"failed to prepare lookup locator {0}",
			new Object[]{ locators[i] },
			e);
		}
	    }
	}
	return (LookupLocator[]) l.toArray(new LookupLocator[l.size()]);
    }

    /**
     * Logs a thrown exception.
     */
    private static void logThrow(Level level,
				 String className,
				 String methodName,
				 String message,
				 Object[] args,
				 Throwable thrown)
    {
	java.util.logging.LogRecord lr =
	    new java.util.logging.LogRecord(level, message);
	lr.setLoggerName(logger.getName());
	lr.setSourceClassName(className);
	lr.setSourceMethodName(methodName);
	lr.setParameters(args);
	lr.setThrown(thrown);
	logger.log(lr);
    }

    /**
     * Add a service to our state.  This includes putting it in the
     * serviceByID map under the serviceID, in the serviceByTime map,
     * in the serviceByType map under the service's most-specific
     * ServiceType, and in the serviceByAttr map under all of its
     * attribute values, incrementing the number of instances of each
     * EntryClass, and updating entryClasses as necessary.  If this is
     * the first instance of that ServiceType registered, then we need
     * to add concrete class information to the type and all supertypes.
     */
    private void addService(SvcReg reg) {
	serviceByID.put(reg.item.serviceID, reg);
	serviceByTime.put(reg, reg);
	ServiceType type = reg.item.serviceType;
	ArrayList regs = (ArrayList)serviceByType.get(type);
	if (regs == null) {
	    regs = new ArrayList(3);
	    serviceByType.put(type, regs);
	    addConcreteClass(type, type);
	}
	regs.add(reg);
	EntryRep[] entries = reg.item.attributeSets;
	for (int i = entries.length; --i >= 0; ) {
	    addAttrs(reg, entries[i]);
	}
	computeMaxLeases();
    }

    /**
     * Delete given service, generating events as necessary.  This includes
     * deleting from the serviceByID, serviceByTime, serviceByType, and
     * serviceByAttr maps, decrementing the number of instances of each
     * EntryClass, and updating entryClasses as necessary.  If this is the
     * last registered instance of the service type, then we delete the
     * concrete class information from the type and all supertypes.
     */
    private void deleteService(SvcReg reg, long now) {
	Item item = reg.item;
	generateEvents(item, null, now);
	serviceByID.remove(item.serviceID);
	serviceByTime.remove(reg);
	ServiceType type = item.serviceType;
	ArrayList regs = (ArrayList)serviceByType.get(type);
	regs.remove(regs.indexOf(reg));
	if (regs.isEmpty()) {
	    serviceByType.remove(type);
	    deleteConcreteClass(type, type);
	}
	EntryRep[] entries = item.attributeSets;
	for (int i = entries.length; --i >= 0; ) {
	    deleteAttrs(reg, entries[i], false);
	}
	computeMaxLeases();
    }

    /**
     * Add an event registration to our state.  This includes adding a
     * template of each EntryClass, putting the registration in the
     * eventByID map, in the eventByTime map, and in either
     * subEventByService (if the template is for a specific service id)
     * or subEventByID.  Since we expect in most cases there will only
     * ever be a single event registration for a given service id, we
     * avoid creating a singleton array in that case.
     */
    private void addEvent(EventReg reg) {
	if (reg.listener == null)
	    return; /* failed to recover from log */
	EntryRep[] tmpls = reg.tmpl.attributeSetTemplates;
	if (tmpls != null) {
	    for (int i = tmpls.length; --i >= 0; ) {
		EntryClass eclass = tmpls[i].eclass;
		eclass.setNumTemplates(eclass.getNumTemplates() + 1);
	    }
	}
	Long id = new Long(reg.eventID);
	eventByID.put(id, reg);
	eventByTime.put(reg, reg);
	if (reg.tmpl.serviceID != null) {
	    Object val = subEventByService.get(reg.tmpl.serviceID);
	    if (val == null)
		val = reg;
	    else if (val instanceof EventReg)
		val = new EventReg[]{(EventReg)val, reg};
	    else
		val = arrayAdd((EventReg[])val, reg);
	    subEventByService.put(reg.tmpl.serviceID, val);
	} else {
	    subEventByID.put(id, reg);
	}
	computeMaxLeases();
    }

    /**
     * Remove an event registration from our state.  This includes deleting
     * a template of each EntryClass, deleting the registration from the
     * eventByID map, the eventByTime map, and either the subEventByService
     * or subEventByID map.
     */
    private void deleteEvent(EventReg reg) {
	EntryRep[] tmpls = reg.tmpl.attributeSetTemplates;
	if (tmpls != null) {
	    for (int i = tmpls.length; --i >= 0; ) {
		EntryClass eclass = tmpls[i].eclass;
		eclass.setNumTemplates(eclass.getNumTemplates() - 1);
	    }
	}
	Long id = new Long(reg.eventID);
	eventByID.remove(id);
	eventByTime.remove(reg);
	if (reg.tmpl.serviceID != null) {
	    Object val = subEventByService.get(reg.tmpl.serviceID);
	    if (val == reg) {
		subEventByService.remove(reg.tmpl.serviceID);
	    } else {
		Object[] array = (EventReg[])val;
		array = arrayDel(array, indexOf(array, reg));
		if (array.length == 1)
		    val = array[0];
		else
		    val = array;
		subEventByService.put(reg.tmpl.serviceID, val);
	    }
	} else {
	    subEventByID.remove(id);
	}
	computeMaxLeases();
    }

    /**
     * Put the service in the serviceByAttr map under all attribute values
     * in the given entry, or in the serviceByEmptyAttr map if the entry
     * has no attributes, add a new instance of the EntryClass, and update
     * entryClasses as necessary.
     */
    private void addAttrs(SvcReg reg, EntryRep entry) {
	EntryClass eclass = entry.eclass;
	addInstance(eclass);
	Object[] fields = entry.fields;
	if (fields.length > 0) {
	    /* walk backwards to make getDefiningClass more efficient */
	    for (int i = fields.length; --i >= 0; ) {
		eclass = getDefiningClass(eclass, i);
		addAttr(reg, eclass, i, fields[i]);
	    }
	    return;
	}
	ArrayList regs = (ArrayList)serviceByEmptyAttr.get(eclass);
	if (regs == null) {
	    regs = new ArrayList(2);
	    regs.add(reg);
	    serviceByEmptyAttr.put(eclass, regs);
	} else if (!regs.contains(reg)) {
	    regs.add(reg);
	}
    }

    /**
     * If checkDups is false, delete the service (if present) from
     * serviceByAttr under all attribute values of the given entry or
     * from serviceByEmptyAttr if the entry has no attributes.
     * If checkDups is true, only delete for a given attribute value if the
     * service has no other entries of similar type that still have the
     * same value.  Either way, delete an instance of the EntryClass,
     * and update entryClasses as necessary.
     */
    private void deleteAttrs(SvcReg reg, EntryRep entry, boolean checkDups) {
	EntryClass eclass = entry.eclass;
	deleteInstance(eclass);
	Object[] fields = entry.fields;
	if (fields.length == 0) {
	    ArrayList regs = (ArrayList)serviceByEmptyAttr.get(eclass);
	    if (regs == null || (checkDups && hasEmptyAttr(reg, eclass)))
		return;
	    int idx = regs.indexOf(reg);
	    if (idx >= 0) {
		regs.remove(idx);
		if (regs.isEmpty())
		    serviceByEmptyAttr.remove(eclass);
	    }
	    return;
	}
	/* walk backwards to make getDefiningClass more efficient */
	for (int fldidx = fields.length; --fldidx >= 0; ) {
	    eclass = getDefiningClass(eclass, fldidx);
	    HashMap[] attrMaps = (HashMap[])serviceByAttr.get(eclass);
	    if (attrMaps == null ||
		attrMaps[fldidx] == null ||
		(checkDups && hasAttr(reg, eclass, fldidx, fields[fldidx])))
		continue;
	    HashMap map = attrMaps[fldidx];
	    Object value = fields[fldidx];
	    ArrayList regs = (ArrayList)map.get(value);
	    if (regs == null)
		continue;
	    int idx = regs.indexOf(reg);
	    if (idx < 0)
		continue;
	    regs.remove(idx);
	    if (!regs.isEmpty())
		continue;
	    map.remove(value);
	    if (!map.isEmpty())
		continue;
	    attrMaps[fldidx] = null;
	    if (allNull(attrMaps))
		serviceByAttr.remove(eclass);
	}
    }

    /**
     * Store all non-null elements of values into the given entry,
     * and update serviceByAttr to match.
     */
    private void updateAttrs(SvcReg reg, EntryRep entry, Object[] values)
    {
	EntryClass eclass = entry.eclass;
	/* walk backwards to make getDefiningClass more efficient */
	for (int fldidx = values.length; --fldidx >= 0; ) {
	    Object oval = entry.fields[fldidx];
	    Object nval = values[fldidx];
	    if (nval != null && !nval.equals(oval)) {
		eclass = getDefiningClass(eclass, fldidx);
		HashMap map = addAttr(reg, eclass, fldidx, nval);
		entry.fields[fldidx] = nval;
		if (hasAttr(reg, eclass, fldidx, oval))
		    continue;
		ArrayList regs = (ArrayList)map.get(oval);
		regs.remove(regs.indexOf(reg));
		if (regs.isEmpty())
		    map.remove(oval); /* map cannot become empty */
	    }
	}
    }

    /**
     * Put the service under the given attribute value for the given
     * defining class and field, if it isn't already there.  Return
     * the HashMap for the given class and field.
     */
    private HashMap addAttr(SvcReg reg,
			    EntryClass eclass,
			    int fldidx,
			    Object value)
    {
	HashMap[] attrMaps = (HashMap[])serviceByAttr.get(eclass);
	if (attrMaps == null) {
	    attrMaps = new HashMap[eclass.getNumFields()];
	    serviceByAttr.put(eclass, attrMaps);
	}
	HashMap map = attrMaps[fldidx];
	if (map == null) {
	    map = new HashMap(11);
	    attrMaps[fldidx] = map;
	}
	ArrayList regs = (ArrayList)map.get(value);
	if (regs == null) {
	    regs = new ArrayList(3);
	    map.put(value, regs);
	} else if (regs.contains(reg))
	    return map;
	regs.add(reg);
	return map;
    }

    /**
     * Add an instance of the EntryClass, and add the class to entryClasses
     * if this is the first such instance.
     */
    private void addInstance(EntryClass eclass) {
	int num = eclass.getNumInstances();
	if (num == 0)
	    entryClasses.add(eclass);
	eclass.setNumInstances(num + 1);
    }

    /**
     * Delete an instance of the EntryClass, and remove the class from
     * entryClasses if this is the last such instance.
     */
    private void deleteInstance(EntryClass eclass) {
	int num = eclass.getNumInstances() - 1;
	if (num == 0)
	    entryClasses.remove(entryClasses.indexOf(eclass));
	eclass.setNumInstances(num);
    }

    /** Return an appropriate iterator for Items matching the Template. */
    private ItemIter matchingItems(Template tmpl) {
	if (tmpl.serviceID != null)
	    return new IDItemIter(tmpl);
	if (!isEmpty(tmpl.serviceTypes))
	    return new TypeItemIter(tmpl);
	EntryRep[] sets = tmpl.attributeSetTemplates;
	if (isEmpty(sets))
	    return new AllItemIter();
	for (int i = sets.length; --i >= 0; ) {
	    Object[] fields = sets[i].fields;
	    if (fields.length == 0) {
		EntryClass eclass = getEmptyEntryClass(sets[i].eclass);
		if (eclass != null)
		    return new EmptyAttrItemIter(tmpl, eclass);
	    } else {
		/* try subclass fields before superclass fields */
		for (int j = fields.length; --j >= 0; ) {
		    if (fields[j] != null)
			return new AttrItemIter(tmpl, i, j);
		}
	    }
	}
	return new ClassItemIter(tmpl);
    }

    /**
     * Return member of entryClasses that is equal to or a subclass of
     * the specified class, provided there is exactly one such member
     * and it has no fields.
     */
    private EntryClass getEmptyEntryClass(EntryClass eclass) {
	EntryClass match = null;
	for (int i = entryClasses.size(); --i >= 0; ) {
	    EntryClass cand = (EntryClass)entryClasses.get(i);
	    if (eclass.isAssignableFrom(cand)) {
		if (cand.getNumFields() != 0 || match != null)
		    return null;
		match = cand;
	    }
	}
	return match;
    }

    /**
     * Returns all concrete classes that are equal to or subclasses of
     * all of the types in an array.
     */
    private ServiceType[] matchingConcreteClasses(ServiceType[] types) {
	if (isEmpty(types))
	    return objectServiceType.getConcreteClasses();
	ServiceType[] stypes = types[0].getConcreteClasses();
	if (types.length > 1) {
	    for (int i = stypes.length; --i >= 0; ) {
		if (!matchType(types, stypes[i]))
		    stypes = (ServiceType[])arrayDel(stypes, i);
	    }
	}
	return stypes;
    }

    /** Return any valid codebase for an entry class that has instances. */
    private String pickCodebase(EntryClass eclass, long now)
	throws ClassNotFoundException
    {
	if (eclass.getNumFields() == 0)
	    return pickCodebase(eclass,
				(ArrayList)serviceByEmptyAttr.get(eclass),
				now);
	int fldidx = eclass.getNumFields() - 1;
	HashMap[] attrMaps =
	    (HashMap[])serviceByAttr.get(getDefiningClass(eclass, fldidx));
	for (Iterator iter = attrMaps[fldidx].values().iterator();
	     iter.hasNext(); )
	{
	    try {
		return pickCodebase(eclass, (ArrayList)iter.next(), now);
	    } catch (ClassNotFoundException e) {
	    }
	}
	throw new ClassNotFoundException();
    }

    /** Return any valid codebase for an entry of the exact given class. */
    private String pickCodebase(EntryClass eclass, ArrayList svcs, long now)
	throws ClassNotFoundException
    {
	for (int i = svcs.size(); --i >= 0; ) {
	    SvcReg reg = (SvcReg)svcs.get(i);
	    if (reg.leaseExpiration <= now)
		continue;
	    EntryRep[] sets = reg.item.attributeSets;
	    for (int j = sets.length; --j >= 0; ) {
		if (eclass.equals(sets[j].eclass))
		    return sets[j].codebase;
	    }
	}
	throw new ClassNotFoundException();
    }

    /** Return any valid codebase for a concrete service type. */
    private String pickCodebase(ServiceType stype, long now)
	throws ClassNotFoundException
    {
	ArrayList svcs = (ArrayList)serviceByType.get(stype);
	for (int i = svcs.size(); --i >= 0; ) {
	    SvcReg reg = (SvcReg)svcs.get(i);
	    if (reg.leaseExpiration > now)
		return reg.item.codebase;
	}
	throw new ClassNotFoundException();
    }

    /**
     * Compute new maxServiceLease and maxEventLease values.  This needs to
     * be called whenever the number of services (#S) or number of events
     * (#E) changes, or whenever any of the configuration parameters change.
     * The two base equations driving the computation are:
     *     #S/maxServiceLease + #E/maxEventLease <= 1/minRenewalInterval
     *     maxServiceLease/maxEventLease = minMaxServiceLease/minMaxEventLease
     */
    private void computeMaxLeases() {
	if (inRecovery)
	    return;
	maxServiceLease =
	    Math.max(minMaxServiceLease,
		     minRenewalInterval *
		     (serviceByID.size() +
		      ((eventByID.size() * minMaxServiceLease) /
		       minMaxEventLease)));
	maxEventLease = Math.max(minMaxEventLease,
				 ((maxServiceLease * minMaxEventLease) /
				  minMaxServiceLease));
    }

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

	    int pv = new DataInputStream(socket.getInputStream()).readInt();
	    unicastDiscoveryConstraints.checkProtocolVersion(pv);
	    getDiscovery(pv).handleUnicastDiscovery(
		new UnicastResponse(myLocator.getHost(),
				    myLocator.getPort(),
				    memberGroups,
				    proxy),
		socket,
		unicastDiscoveryConstraints.getUnfulfilledConstraints(),
		unicastDiscoverySubjectChecker,
		Collections.EMPTY_LIST);

	} catch (Exception e) {
	    if (logger.isLoggable(Levels.HANDLED)) {
		logThrow(
		    Levels.HANDLED,
		    getClass().getName(),
		    "respond",
		    "exception handling unicast discovery from {0}:{1}",
		    new Object[]{
			socket.getInetAddress(),
			new Integer(socket.getPort()) },
		    e);
	    }
	} finally {
	    try {
		socket.close();
	    } catch (IOException e) {
		logger.log(Levels.HANDLED, "exception closing socket", e);
	    }
	}
    }

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

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

    /** Post-login (if login configured) initialization. */
    private void init(Configuration config,
		      ActivationID activationID,
		      boolean persistent,
		      LifeCycle lifeCycle)
	throws IOException, ConfigurationException, ActivationException
    {
	this.lifeCycle = lifeCycle;

	/* persistence-specific initialization */
	if (persistent) {
	    persistenceSnapshotThreshold = Config.getIntEntry(
		config, COMPONENT, "persistenceSnapshotThreshold",
		persistenceSnapshotThreshold, 0, Integer.MAX_VALUE);
	    String persistenceDirectory = (String) config.getEntry(
		COMPONENT, "persistenceDirectory", String.class);
	    recoveredListenerPreparer = (ProxyPreparer) Config.getNonNullEntry(
		config, COMPONENT, "recoveredListenerPreparer",
		ProxyPreparer.class, recoveredListenerPreparer);
	    recoveredLocatorPreparer = (ProxyPreparer) Config.getNonNullEntry(
		config, COMPONENT, "recoveredLocatorPreparer",
		ProxyPreparer.class, recoveredLocatorPreparer);
	    persistenceSnapshotWeight = Config.getFloatEntry(
		config, COMPONENT, "persistenceSnapshotWeight",
		persistenceSnapshotWeight, 0F, Float.MAX_VALUE);

	    log = new ReliableLog(persistenceDirectory, new LocalLogHandler());
	    if (logger.isLoggable(Level.CONFIG)) {
		logger.log(Level.CONFIG, "using persistence directory {0}",
			   new Object[]{ persistenceDirectory });
	    }
	    inRecovery = true;
	    resolver.setInRecovery(true);
	    log.recover();
	    resolver.setInRecovery(false);
	    inRecovery = false;
	} else {
	    log = null;
	}

	/* activation-specific initialization */
	if (activationID != null) {
	    ProxyPreparer activationIdPreparer = (ProxyPreparer)
		Config.getNonNullEntry(
		    config, COMPONENT, "activationIdPreparer",
		    ProxyPreparer.class, new BasicProxyPreparer());
	    ProxyPreparer activationSystemPreparer = (ProxyPreparer)
		Config.getNonNullEntry(
		    config, COMPONENT, "activationSystemPreparer",
		    ProxyPreparer.class, new BasicProxyPreparer());

	    this.activationID = (ActivationID)
		activationIdPreparer.prepareProxy(activationID);
	    activationSystem = (ActivationSystem)
		activationSystemPreparer.prepareProxy(
		    ActivationGroup.getSystem());

	    serverExporter = (Exporter) Config.getNonNullEntry(
		config, COMPONENT, "serverExporter", Exporter.class,
		new ActivationExporter(
		    this.activationID,
		    new BasicJeriExporter(
			TcpServerEndpoint.getInstance(0),
			new BasicILFactory())),
		this.activationID);
	} else {
	    this.activationID = null;
	    activationSystem = null;

	    serverExporter = (Exporter) Config.getNonNullEntry(
		config, COMPONENT, "serverExporter", Exporter.class,
		new BasicJeriExporter(
		    TcpServerEndpoint.getInstance(0),
		    new BasicILFactory()));
	}

	/* fetch "initial*" config entries, if first time starting up */
	if (!recoveredSnapshot) {
	    Entry[] initialLookupAttributes = (Entry[]) config.getEntry(
		COMPONENT, "initialLookupAttributes", Entry[].class,
		new Entry[0]);
	    lookupGroups = (String[]) config.getEntry(
		COMPONENT, "initialLookupGroups", String[].class,
		lookupGroups);
	    lookupLocators = (LookupLocator[]) config.getEntry(
		COMPONENT, "initialLookupLocators", LookupLocator[].class,
		lookupLocators);
	    memberGroups = (String[]) config.getEntry(
		COMPONENT, "initialMemberGroups", String[].class,
		memberGroups);
	    if (memberGroups == null) {
		throw new ConfigurationException(
		    "member groups cannot be ALL_GROUPS (null)");
	    }
	    memberGroups = (String[]) removeDups(memberGroups);
	    unicastPort = Config.getIntEntry(
		config, COMPONENT, "initialUnicastDiscoveryPort",
		unicastPort, 0, 0xFFFF);

	    if (initialLookupAttributes != null && 
		initialLookupAttributes.length > 0)
	    {
		List l = new ArrayList(Arrays.asList(baseAttrs));
		l.addAll(Arrays.asList(initialLookupAttributes));
		lookupAttrs = (Entry[]) l.toArray(new Entry[l.size()]);
	    } else {
		lookupAttrs = baseAttrs;
	    }
	}

	/* fetch remaining config entries */
	MethodConstraints discoveryConstraints = 
	    (MethodConstraints) config.getEntry(
		COMPONENT, "discoveryConstraints",
		MethodConstraints.class, null);
	if (discoveryConstraints == null) {
	    discoveryConstraints = 
		new BasicMethodConstraints(InvocationConstraints.EMPTY);
	}
	try {
	    discoer = (DiscoveryManagement) config.getEntry(
		COMPONENT, "discoveryManager", DiscoveryManagement.class);
	} catch (NoSuchEntryException e) {
	    discoer = new LookupDiscoveryManager(
		DiscoveryGroupManagement.NO_GROUPS, null, null, config);
	}
	listenerPreparer = (ProxyPreparer) Config.getNonNullEntry(
	    config, COMPONENT, "listenerPreparer", ProxyPreparer.class,
	    listenerPreparer);
	locatorPreparer = (ProxyPreparer) Config.getNonNullEntry(
	    config, COMPONENT, "locatorPreparer", ProxyPreparer.class,
	    locatorPreparer);
	minMaxEventLease = Config.getLongEntry(
	    config, COMPONENT, "minMaxEventLease",
	    minMaxEventLease, 1, MAX_LEASE);
	minMaxServiceLease = Config.getLongEntry(
	    config, COMPONENT, "minMaxServiceLease",
	    minMaxServiceLease, 1, MAX_LEASE);
	minRenewalInterval = Config.getLongEntry(
	    config, COMPONENT, "minRenewalInterval",
	    minRenewalInterval, 0, MAX_RENEW);
	multicastAnnouncementInterval = Config.getLongEntry(
	    config, COMPONENT, "multicastAnnouncementInterval",
	    multicastAnnouncementInterval, 1, Long.MAX_VALUE);

	multicastInterfaceRetryInterval = Config.getIntEntry(
	    config, COMPONENT, "multicastInterfaceRetryInterval",
	    multicastInterfaceRetryInterval, 1, Integer.MAX_VALUE);
	try {
	    multicastInterfaces = (NetworkInterface[]) config.getEntry(
		COMPONENT, "multicastInterfaces", NetworkInterface[].class);
	    multicastInterfacesSpecified = true;
	} catch (NoSuchEntryException e) {
	    Enumeration en = NetworkInterface.getNetworkInterfaces();
	    List l = (en != null) ?
		Collections.list(en) : Collections.EMPTY_LIST;
	    multicastInterfaces = (NetworkInterface[])
		l.toArray(new NetworkInterface[l.size()]);
	    multicastInterfacesSpecified = false;
	}
	if (multicastInterfaces == null) {
	    logger.config("using system default interface for multicast");
	} else if (multicastInterfaces.length == 0) {
	    if (multicastInterfacesSpecified) {
		logger.config("multicast disabled");
	    } else {
		logger.severe("no network interfaces detected");
	    }
	} else if (logger.isLoggable(Level.CONFIG)) {
	    logger.log(Level.CONFIG, "multicasting on interfaces {0}",
		       new Object[]{ Arrays.asList(multicastInterfaces) });
	}

	try {
	    multicastRequestSubjectChecker =
		(ClientSubjectChecker) Config.getNonNullEntry(
		    config, COMPONENT, "multicastRequestSubjectChecker",
		    ClientSubjectChecker.class);
	} catch (NoSuchEntryException e) {
	    // leave null
	}
	resourceIdGenerator = (UuidGenerator) Config.getNonNullEntry(
	    config, COMPONENT, "resourceIdGenerator", UuidGenerator.class,
	    resourceIdGenerator);
	serviceIdGenerator = (UuidGenerator) Config.getNonNullEntry(
	    config, COMPONENT, "serviceIdGenerator", UuidGenerator.class,
	    serviceIdGenerator);
	tasker = (TaskManager) Config.getNonNullEntry(
	    config, COMPONENT, "taskManager", TaskManager.class,
	    new TaskManager(10, 1000 * 15, 1.0F));
	String unicastDiscoveryHost;
	try {
	    unicastDiscoveryHost = (String) Config.getNonNullEntry(
		config, COMPONENT, "unicastDiscoveryHost", String.class);
	} catch (NoSuchEntryException e) {
	    // fix for 4906732: only invoke getCanonicalHostName if needed
	    unicastDiscoveryHost =
		InetAddress.getLocalHost().getCanonicalHostName();
	}
	try {
	    unicastDiscoverySubjectChecker =
		(ClientSubjectChecker) Config.getNonNullEntry(
		    config, COMPONENT, "unicastDiscoverySubjectChecker",
		    ClientSubjectChecker.class);
	} catch (NoSuchEntryException e) {
	    // leave null
	}

	/* initialize state based on recovered/configured values */
	objectServiceType = resolver.resolve(new ServiceType(Object.class,
							     null, null));
	computeMaxLeases();
	protocol2 = Discovery.getProtocol2(null);
	multicastRequestConstraints = DiscoveryConstraints.process(
	    discoveryConstraints.getConstraints(
		DiscoveryConstraints.multicastRequestMethod));
	multicastAnnouncementConstraints = DiscoveryConstraints.process(
	    discoveryConstraints.getConstraints(
		DiscoveryConstraints.multicastAnnouncementMethod));
	unicastDiscoveryConstraints = DiscoveryConstraints.process(
	    discoveryConstraints.getConstraints(
		DiscoveryConstraints.unicastDiscoveryMethod));
	serviceExpirer = new ServiceExpireThread();
	eventExpirer = new EventExpireThread();
	unicaster = new UnicastThread(unicastPort);
	multicaster = new MulticastThread();
	announcer = new AnnounceThread();
	snapshotter = new SnapshotThread();
	if (myServiceID == null) {
	    myServiceID = newServiceID();
	}
	myRef = (Registrar) serverExporter.export(this);
	proxy = RegistrarProxy.getInstance(myRef, myServiceID);
	myLocator = (proxy instanceof RemoteMethodControl) ?
	    new ConstrainableLookupLocator(
		unicastDiscoveryHost, unicaster.port, null) :
	    new LookupLocator(unicastDiscoveryHost, unicaster.port);
	if (serviceByID.isEmpty()) {
	    /* register myself */
	    Item item = new Item(new ServiceItem(myServiceID,
						 proxy,
						 lookupAttrs));
	    resolver.resolve(item);
	    SvcReg reg = new SvcReg(item, myLeaseID, Long.MAX_VALUE);
	    addService(reg);
	} else {
	    SvcReg reg = (SvcReg)serviceByID.get(myServiceID);
	    lookupAttrs = EntryRep.toEntry(reg.item.attributeSets);
	    /* delete whatever we can't reconstitute */
	    for (int i = lookupAttrs.length; --i >= 0; ) {
		if (lookupAttrs[i] == null) {
		    lookupAttrs = (Entry[])arrayDel(lookupAttrs, i);
		    EntryRep set = reg.item.attributeSets[i];
		    deleteSet(reg.item, i);
		    deleteAttrs(reg, set, true);
		}
	    }
	}
	if (log != null) {
	    log.snapshot();
	}

	try {
	    DiscoveryGroupManagement dgm = (DiscoveryGroupManagement) discoer;
	    String[] groups = dgm.getGroups();
	    if (groups == null || groups.length > 0) {
		throw new ConfigurationException(
		    "discoveryManager must be initially configured with " +
		    "no groups");
	    }
	    DiscoveryLocatorManagement dlm =
		(DiscoveryLocatorManagement) discoer;
	    if (dlm.getLocators().length > 0) {
		throw new ConfigurationException(
		    "discoveryManager must be initially configured with " +
		    "no locators");
	    }
	    dgm.setGroups(lookupGroups);
	    dlm.setLocators(lookupLocators);
	} catch (ClassCastException e) {
	    throw new ConfigurationException(null, e);
	}
	joiner = new JoinManager(proxy, lookupAttrs, myServiceID,
				 discoer, null, config);

	/* start up all the daemon threads */
	serviceExpirer.start();
	eventExpirer.start();
	unicaster.start();
	multicaster.start();
	announcer.start();
        snapshotter.start();

	if (logger.isLoggable(Level.INFO)) {
	    logger.log(Level.INFO, "started Reggie: {0}, {1}, {2}",
		       new Object[]{ myServiceID,
				     Arrays.asList(memberGroups),
				     myLocator });
	}
    }

    /** The code that does the real work of register. */
    private ServiceRegistration registerDo(Item nitem, long leaseDuration)
    {
	resolver.resolve(nitem);
	if (nitem.service == null)
	    throw new NullPointerException("null service");
	if (myServiceID.equals(nitem.serviceID))
	    throw new IllegalArgumentException("reserved service id");
	if (nitem.attributeSets == null)
	    nitem.attributeSets = emptyAttrs;
	else
	    nitem.attributeSets = (EntryRep[])removeDups(nitem.attributeSets);
	leaseDuration = limitDuration(leaseDuration, maxServiceLease);
	long now = System.currentTimeMillis();
	if (nitem.serviceID == null) {
	    /* new service, match on service object */
	    ArrayList svcs = (ArrayList)serviceByType.get(nitem.serviceType);
	    if (svcs != null) {
		for (int i = svcs.size(); --i >= 0; ) {
		    SvcReg reg = (SvcReg)svcs.get(i);
		    if (nitem.service.equals(reg.item.service)) {
			nitem.serviceID = reg.item.serviceID;
			deleteService(reg, now);
			break;
		    }
		}
	    }
	    if (nitem.serviceID == null)
		nitem.serviceID = newServiceID();
	} else {
	    /* existing service, match on service id */
	    SvcReg reg = (SvcReg)serviceByID.get(nitem.serviceID);
	    if (reg != null)
		deleteService(reg, now);
	}
	Util.checkRegistrantServiceID(nitem.serviceID, logger, Level.FINE);
	SvcReg reg = new SvcReg(nitem, newLeaseID(), now + leaseDuration);
	addService(reg);
	generateEvents(null, nitem, now);
	addLogRecord(new SvcRegisteredLogObj(reg));
	queueEvents();
	/* see if the expire thread needs to wake up earlier */
	if (reg.leaseExpiration < minSvcExpiration) {
	    minSvcExpiration = reg.leaseExpiration;
	    concurrentObj.waiterNotify(serviceNotifier);
	}
	return Registration.getInstance(
	    myRef,
	    ServiceLease.getInstance(
		myRef,
		myServiceID,
		nitem.serviceID,
		reg.leaseID,
		reg.leaseExpiration));
    }

   /**
    * The code that does the real work of lookup.  As a special case,
    * if the template specifies at least one service type to match,
    * and there are multiple items that match the template, then we
    * make a random pick among them, in an attempt to load balance
    * use of "equivalent" services and avoid starving any of them.
    */
    private MarshalledWrapper lookupDo(Template tmpl)
    {
	resolver.resolve(tmpl);
	if (isEmpty(tmpl.serviceTypes) || tmpl.serviceID != null)
	{
	    ItemIter iter = matchingItems(tmpl);
	    if (iter.hasNext())
		return iter.next().service;
	    return null;
	}
	ServiceType[] types = matchingConcreteClasses(tmpl.serviceTypes);
	long now = System.currentTimeMillis();
	int tlen = types.length;
	if (tlen == 0)
	    return null;
	int trand = Math.abs(random.nextInt()) % tlen;
	for (int i = 0; i < tlen; i++) {
	    ArrayList svcs = (ArrayList)serviceByType.get(
						  types[(i + trand) % tlen]);
	    int slen = svcs.size();
	    int srand = Math.abs(random.nextInt()) % slen;
	    for (int j = 0; j < slen; j++) {
		SvcReg reg = (SvcReg)svcs.get((j + srand) % slen);
		if (reg.leaseExpiration > now &&
		    matchAttributes(tmpl, reg.item))
		    return reg.item.service;
	    }
	}
	return null;
    }

    /**
     * The code that does the real work of lookup.  We do a deep copy of the
     * items being returned, both to avoid having them modified while being
     * marshalled (by a concurrent update method), and to substitute
     * replacements for embedded EntryClass and ServiceType instances, to
     * minimize data sent back to the client.  If duplicates are possible
     * from the iterator, we save all matches, weeding out duplicates as we
     * go, then trim to maxMatches and deep copy.
     */
    private Matches lookupDo(Template tmpl, int maxMatches)
    {
	if (maxMatches < 0)
	    throw new IllegalArgumentException("negative maxMatches");
	resolver.resolve(tmpl);
	int totalMatches = 0;
	ArrayList matches = null;
	ItemIter iter = matchingItems(tmpl);
	if (maxMatches > 0 || iter.dupsPossible)
	    matches = new ArrayList();
	if (iter.dupsPossible) {
	    while (iter.hasNext()) {
		Item item = iter.next();
		if (!matches.contains(item))
		    matches.add(item);
	    }
	    totalMatches = matches.size();
	    if (maxMatches > 0) {
		for (int i = matches.size(); --i >= maxMatches; )
		    matches.remove(i);
		for (int i = matches.size(); --i >= 0; ) {
		    matches.set(i, copyItem((Item)matches.get(i)));
		}
	    } else {
		matches = null;
	    }
	} else {
	    while (iter.hasNext()) {
		Item item = iter.next();
		totalMatches++;
		if (--maxMatches >= 0)
		    matches.add(copyItem(item));
	    }
	}
	return new Matches(matches, totalMatches);
    }

    /**
     * The code that does the real work of notify.
     * Every registration is given a unique event id.  The event id
     * can thus also serve as a lease id.
     *
     */
    private EventRegistration notifyDo(Template tmpl,
				       int transitions,
				       RemoteEventListener listener,
				       MarshalledObject handback,
				       long leaseDuration)
	throws RemoteException
    {
	resolver.resolve(tmpl);
	if (transitions == 0 ||
	    transitions !=
	    (transitions & (ServiceRegistrar.TRANSITION_MATCH_NOMATCH |
			    ServiceRegistrar.TRANSITION_NOMATCH_MATCH |
			    ServiceRegistrar.TRANSITION_MATCH_MATCH)))
	    throw new IllegalArgumentException("invalid transitions");
	if (listener == null)
	    throw new NullPointerException("listener");
	listener =
	    (RemoteEventListener) listenerPreparer.prepareProxy(listener);
	leaseDuration = limitDuration(leaseDuration, maxEventLease);
	long now = System.currentTimeMillis();
	EventReg reg = new EventReg(eventID, newLeaseID(), tmpl, transitions,
				    listener, handback, now + leaseDuration);
	eventID++;
	addEvent(reg);
	addLogRecord(new EventRegisteredLogObj(reg));
	/* see if the expire thread needs to wake up earlier */
	if (reg.leaseExpiration < minEventExpiration) {
	    minEventExpiration = reg.leaseExpiration;
	    concurrentObj.waiterNotify(eventNotifier);
	}
	return new EventRegistration(
	    reg.eventID,
	    proxy,
	    EventLease.getInstance(
		myRef,
		myServiceID,
		reg.eventID,
		reg.leaseID,
		reg.leaseExpiration),
	    reg.seqNo);
    }

    /**
     * The code that does the real work of getEntryClasses. If the
     * template is empty, then we can just use entryClasses, without
     * having to iterate over items, but we have to work harder to
     * get codebases.
     */
    private EntryClassBase[] getEntryClassesDo(Template tmpl)
    {
	resolver.resolve(tmpl);
	ArrayList classes = new ArrayList();
	ArrayList codebases = new ArrayList();
	if (tmpl.serviceID == null &&
	    isEmpty(tmpl.serviceTypes) &&
	    isEmpty(tmpl.attributeSetTemplates)) {
	    long now = System.currentTimeMillis();
	    for (int i = entryClasses.size(); --i >= 0; ) {
		EntryClass eclass = (EntryClass)entryClasses.get(i);
		try {
		    codebases.add(pickCodebase(eclass, now));
		    classes.add(eclass);
		} catch (ClassNotFoundException e) {
		}
	    }
	} else {
	    for (ItemIter iter = matchingItems(tmpl); iter.hasNext(); ) {
		Item item = iter.next();
		for (int i = item.attributeSets.length; --i >= 0; ) {
		    EntryRep attrSet = item.attributeSets[i];
		    if (attrMatch(tmpl.attributeSetTemplates, attrSet) &&
			!classes.contains(attrSet.eclass)) {
			classes.add(attrSet.eclass);
			codebases.add(attrSet.codebase);
		    }
		}
	    }
	}
	if (classes.isEmpty())
	    return null; /* spec says null */
	EntryClassBase[] vals = new EntryClassBase[classes.size()];
	for (int i = vals.length; --i >= 0; ) {
	    vals[i] = new EntryClassBase(
				((EntryClass)classes.get(i)).getReplacement(),
				(String)codebases.get(i));
	}
	return vals;
    }

    /**
     * The code that does the real work of getFieldValues.  If the
     * template is just a singleton entry with all null fields, then
     * we can do a faster computation by iterating over keys in the
     * given attribute's serviceByAttr map, rather than iterating
     * over items.
     */
    private Object[] getFieldValuesDo(Template tmpl, int setidx, int fldidx)
    {
	resolver.resolve(tmpl);
	ArrayList values = new ArrayList();
	EntryRep etmpl = tmpl.attributeSetTemplates[setidx];
	if (tmpl.serviceID == null &&
	    isEmpty(tmpl.serviceTypes) &&
	    tmpl.attributeSetTemplates.length == 1 &&
	    allNull(etmpl.fields))
	{
	    long now = System.currentTimeMillis();
	    EntryClass eclass = getDefiningClass(etmpl.eclass, fldidx);
	    boolean checkAttr = !eclass.equals(etmpl.eclass);
	    HashMap[] attrMaps = (HashMap[])serviceByAttr.get(eclass);
	    if (attrMaps != null && attrMaps[fldidx] != null) {
		for (Iterator iter = attrMaps[fldidx].entrySet().iterator();
		     iter.hasNext(); )
		{
		    Map.Entry ent = (Map.Entry)iter.next();
		    ArrayList regs = (ArrayList)ent.getValue();
		    Object value = ent.getKey();
		    for (int i = regs.size(); --i >= 0; ) {
			SvcReg reg = (SvcReg)regs.get(i);
			if (reg.leaseExpiration > now &&
			    (!checkAttr ||
			     hasAttr(reg, etmpl.eclass, fldidx, value))) {
			    values.add(value);
			    break;
			}
		    }
		}
	    }
	} else {
	    for (ItemIter iter = matchingItems(tmpl); iter.hasNext(); ) {
		Item item = iter.next();
		for (int j = item.attributeSets.length; --j >= 0; ) {
		    if (matchEntry(etmpl, item.attributeSets[j])) {
			Object value = item.attributeSets[j].fields[fldidx];
			if (!values.contains(value))
			    values.add(value);
		    }
		}
	    }
	}
	if (values.isEmpty())
	    return null;
	return values.toArray();
    }

    /**
     * The code that does the real work of getServiceTypes.  If the
     * template has at most service types, then we can do a fast
     * computation based solely on concrete classes, without having
     * to iterate over items, but we have to work a bit harder to
     * get codebases.
     */
    private ServiceTypeBase[] getServiceTypesDo(Template tmpl, String prefix)
    {
	resolver.resolve(tmpl);
	ArrayList classes = new ArrayList();
	ArrayList codebases = new ArrayList();
	if (tmpl.serviceID == null && isEmpty(tmpl.attributeSetTemplates)) {
	    long now = System.currentTimeMillis();
	    ServiceType[] types = matchingConcreteClasses(tmpl.serviceTypes);
	    for (int i = types.length; --i >= 0; ) {
		try {
		    addTypes(classes, codebases, tmpl.serviceTypes, prefix,
			     types[i], pickCodebase(types[i], now));
		} catch (ClassNotFoundException e) {
		}
	    }
	} else {
	    for (ItemIter iter = matchingItems(tmpl); iter.hasNext(); ) {
		Item item = iter.next();
		addTypes(classes, codebases, tmpl.serviceTypes, prefix,
			 item.serviceType, item.codebase);
	    }
	}
	if (classes.isEmpty())
	    return null; /* spec says null */
	ServiceTypeBase[] vals = new ServiceTypeBase[classes.size()];
	for (int i = vals.length; --i >= 0; ) {
	    vals[i] = new ServiceTypeBase(
			       ((ServiceType)classes.get(i)).getReplacement(),
			       (String)codebases.get(i));
	}
	return vals;
    }

    /**
     * The code that does the real work of addAttributes.
     * Add every element of attrSets to item, updating serviceByAttr as
     * necessary, incrementing the number of EntryClass instances, and
     * updating entryClasses as necessary.
     */
    private void addAttributesDo(ServiceID serviceID,
				 Uuid leaseID,
				 EntryRep[] attrSets)
	throws UnknownLeaseException
    {
	resolver.resolve(attrSets);
	long now = System.currentTimeMillis();
	SvcReg reg = (SvcReg)serviceByID.get(serviceID);
	if (reg == null ||
	    !reg.leaseID.equals(leaseID) ||
	    reg.leaseExpiration <= now)
	    throw new UnknownLeaseException();
	Item pre = (Item)reg.item.clone();
	EntryRep[] sets = reg.item.attributeSets;
	int i = 0;
	/* don't add if it's a duplicate */
	for (int j = 0; j < attrSets.length; j++) {
	    EntryRep set = attrSets[j];
	    if (indexOf(sets, set) < 0 && indexOf(attrSets, j, set) < 0) {
		attrSets[i++] = set;
		addAttrs(reg, set);
	    }
	}
	if (i > 0) {
	    int len = sets.length;
	    EntryRep[] nsets = new EntryRep[len + i];
	    System.arraycopy(sets, 0, nsets, 0, len);
	    System.arraycopy(attrSets, 0, nsets, len, i);
	    reg.item.attributeSets = nsets;
	}
	generateEvents(pre, reg.item, now);
    }

    /**
     * The code that does the real work of modifyAttributes.
     * Modify the attribute sets that match attrSetTmpls, updating
     * or deleting based on attrSets, updating serviceByAttr as necessary,
     * decrementing the number of EntryClass instances, and updating
     * entryClasses as necessary.
     */
    private void modifyAttributesDo(ServiceID serviceID,
				    Uuid leaseID,
				    EntryRep[] attrSetTmpls,
				    EntryRep[] attrSets)
	throws UnknownLeaseException
    {
	resolver.resolve(attrSetTmpls);
	resolver.resolveWithNulls(attrSets);
	if (attrSetTmpls.length != attrSets.length)
	    throw new IllegalArgumentException(
				       "attribute set length mismatch");
	for (int i = attrSets.length; --i >= 0; ) {
	    if (attrSets[i] != null &&
		!attrSets[i].eclass.isAssignableFrom(attrSetTmpls[i].eclass))
		throw new IllegalArgumentException(
					   "attribute set type mismatch");
	}
	long now = System.currentTimeMillis();
	SvcReg reg = (SvcReg)serviceByID.get(serviceID);
	if (reg == null ||
	    !reg.leaseID.equals(leaseID) ||
	    reg.leaseExpiration <= now)
	    throw new UnknownLeaseException();
	Item pre = (Item)reg.item.clone();
	EntryRep[] preSets = pre.attributeSets;
	EntryRep[] sets = reg.item.attributeSets;
	for (int i = preSets.length; --i >= 0; ) {
	    EntryRep preSet = preSets[i];
	    EntryRep set = sets[i];
	    for (int j = attrSetTmpls.length; --j >= 0; ) {
		if (matchEntry(attrSetTmpls[j], preSet)) {
		    EntryRep attrs = attrSets[j];
		    if (attrs == null) {
			sets = deleteSet(reg.item, i);
			deleteAttrs(reg, set, true);
			break;
		    } else {
			updateAttrs(reg, set, attrs.fields);
		    }
		}
	    }
	}
	for (int i = sets.length; --i >= 0; ) {
	    EntryRep set = sets[i];
	    if (indexOf(sets, i, set) >= 0) {
		sets = deleteSet(reg.item, i);
		deleteInstance(set.eclass);
	    }
	}
	reg.item.attributeSets = sets;
	generateEvents(pre, reg.item, now);
    }

    /**
     * The code that does the real work of setAttributes.
     * Replace all attributes of item with attrSets, updating serviceByAttr
     * as necessary, incrementing the number of EntryClass instances, and
     * updating entryClasses as necessary.
     */
    private void setAttributesDo(ServiceID serviceID,
				 Uuid leaseID,
				 EntryRep[] attrSets)
	throws UnknownLeaseException
    {
	resolver.resolve(attrSets);
	if (attrSets == null)
	    attrSets = emptyAttrs;
	else
	    attrSets = (EntryRep[])removeDups(attrSets);
	long now = System.currentTimeMillis();
	SvcReg reg = (SvcReg)serviceByID.get(serviceID);
	if (reg == null ||
	    !reg.leaseID.equals(leaseID) ||
	    reg.leaseExpiration <= now)
	    throw new UnknownLeaseException();
	Item pre = (Item)reg.item.clone();
	EntryRep[] entries = reg.item.attributeSets;
	for (int i = entries.length; --i >= 0; ) {
	    deleteAttrs(reg, entries[i], false);
	}
	reg.item.attributeSets = attrSets;
	for (int i = attrSets.length; --i >= 0; ) {
	    addAttrs(reg, attrSets[i]);
	}
	generateEvents(pre, reg.item, now);
    }

    /** The code that does the real work of cancelServiceLease. */
    private void cancelServiceLeaseDo(ServiceID serviceID, Uuid leaseID)
	throws UnknownLeaseException
    {
	if (serviceID.equals(myServiceID))
	    throw new SecurityException("privileged service id");
	long now = System.currentTimeMillis();
	SvcReg reg = (SvcReg)serviceByID.get(serviceID);
	if (reg == null ||
	    !reg.leaseID.equals(leaseID) ||
	    reg.leaseExpiration <= now)
	    throw new UnknownLeaseException();
	deleteService(reg, now);
	/* wake up thread if this might be the (only) earliest time */
	if (reg.leaseExpiration == minSvcExpiration)
	    concurrentObj.waiterNotify(serviceNotifier);
    }

    /** The code that does the real work of renewServiceLease. */
    private long renewServiceLeaseDo(ServiceID serviceID,
				     Uuid leaseID,
				     long renewDuration)
	throws UnknownLeaseException
    {
	long now = System.currentTimeMillis();
	long renewExpiration = renewServiceLeaseInt(serviceID, leaseID,
						    renewDuration, now);
	addLogRecord(new ServiceLeaseRenewedLogObj(serviceID, leaseID,
						   renewExpiration));
	return renewExpiration - now;
    }

    /** Renew a service lease for a relative duration from now. */
    private long renewServiceLeaseInt(ServiceID serviceID,
				      Uuid leaseID,
				      long renewDuration,
				      long now)
	throws UnknownLeaseException
    {
	if (serviceID.equals(myServiceID))
	    throw new SecurityException("privileged service id");
	if (renewDuration == Lease.ANY)
	    renewDuration = maxServiceLease;
	else if (renewDuration < 0)
	    throw new IllegalArgumentException("negative lease duration");
	SvcReg reg = (SvcReg)serviceByID.get(serviceID);
	if (reg == null ||
	    !reg.leaseID.equals(leaseID) ||
	    reg.leaseExpiration <= now)
	    throw new UnknownLeaseException();
	if (renewDuration > maxServiceLease &&
	    renewDuration > reg.leaseExpiration - now)
	    renewDuration = Math.max(reg.leaseExpiration - now,
				     maxServiceLease);
	long renewExpiration = now + renewDuration;
	/* force a re-sort: must remove before changing, then reinsert */
	serviceByTime.remove(reg);
	reg.leaseExpiration = renewExpiration;
	serviceByTime.put(reg, reg);
	/* see if the expire thread needs to wake up earlier */
	if (renewExpiration < minSvcExpiration) {
	    minSvcExpiration = renewExpiration;
	    concurrentObj.waiterNotify(serviceNotifier);
	}
	return renewExpiration;
    }

    /** Renew the service lease for an absolute expiration time. */
    private void renewServiceLeaseAbs(ServiceID serviceID,
				      Uuid leaseID,
				      long renewExpiration)
    {
	SvcReg reg = (SvcReg)serviceByID.get(serviceID);
	if (reg == null || !reg.leaseID.equals(leaseID))
	    return;
	/* force a re-sort: must remove before changing, then reinsert */
	serviceByTime.remove(reg);
	reg.leaseExpiration = renewExpiration;
	serviceByTime.put(reg, reg);
    }

    /** The code that does the real work of cancelEventLease. */
    private void cancelEventLeaseDo(long eventID, Uuid leaseID)
	throws UnknownLeaseException
    {
	long now = System.currentTimeMillis();
	EventReg reg = (EventReg)eventByID.get(new Long(eventID));
	if (reg == null || reg.leaseExpiration <= now)
	    throw new UnknownLeaseException();
	deleteEvent(reg);
	/* wake up thread if this might be the (only) earliest time */
	if (reg.leaseExpiration == minEventExpiration)
	    concurrentObj.waiterNotify(eventNotifier);
    }

    /** The code that does the real work of renewEventLease. */
    private long renewEventLeaseDo(long eventID,
				   Uuid leaseID,
				   long renewDuration)
	throws UnknownLeaseException
    {
	long now = System.currentTimeMillis();
	long renewExpiration = renewEventLeaseInt(eventID, leaseID,
						  renewDuration, now);
	addLogRecord(new EventLeaseRenewedLogObj(eventID, leaseID,
						 renewExpiration));
	return renewExpiration - now;
    }

    private long renewEventLeaseInt(long eventID,
				    Uuid leaseID,
				    long renewDuration,
				    long now)
	throws UnknownLeaseException
    {
	if (renewDuration == Lease.ANY)
	    renewDuration = maxEventLease;
	else if (renewDuration < 0)
	    throw new IllegalArgumentException("negative lease duration");
	EventReg reg = (EventReg)eventByID.get(new Long(eventID));
	if (reg == null ||
	    !reg.leaseID.equals(leaseID) ||
	    reg.leaseExpiration <= now)
	    throw new UnknownLeaseException();
	if (renewDuration > maxEventLease &&
	    renewDuration > reg.leaseExpiration - now)
	    renewDuration = Math.max(reg.leaseExpiration - now, maxEventLease);
	long renewExpiration = now + renewDuration;
	/* force a re-sort: must remove before changing, then reinsert */
	eventByTime.remove(reg);
	reg.leaseExpiration = renewExpiration;
	eventByTime.put(reg, reg);
	/* see if the expire thread needs to wake up earlier */
	if (renewExpiration < minEventExpiration) {
	    minEventExpiration = renewExpiration;
	    concurrentObj.waiterNotify(eventNotifier);
	}
	return renewExpiration;
    }

    /** Renew the event lease for an absolute expiration time. */
    private void renewEventLeaseAbs(long eventID,
				    Uuid leaseID,
				    long renewExpiration)
    {
	EventReg reg = (EventReg)eventByID.get(new Long(eventID));
	if (reg == null || !reg.leaseID.equals(leaseID))
	    return;
	/* force a re-sort: must remove before changing, then reinsert */
	eventByTime.remove(reg);
	reg.leaseExpiration = renewExpiration;
	eventByTime.put(reg, reg);
    }

    /**
     * The code that does the real work of renewLeases.  Each element of
     * regIDs must either be a ServiceID (for a service lease) or a Long
     * (for an event lease).  Renewals contains durations.  All three
     * arrays must be the same length.
     */
    private RenewResults renewLeasesDo(Object[] regIDs,
				       Uuid[] leaseIDs,
				       long[] renewals)
    {
	long now = System.currentTimeMillis();
	Exception[] exceptions = null;
	for (int i = 0; i < regIDs.length; i++) {
	    Object id = regIDs[i];
	    try {
		if (id instanceof ServiceID)
		    renewals[i] = renewServiceLeaseInt((ServiceID)id,
						       leaseIDs[i],
						       renewals[i], now);
		else
		    renewals[i] = renewEventLeaseInt(((Long)id).longValue(),
						     leaseIDs[i], renewals[i],
						     now);
	    } catch (Exception e) {
		renewals[i] = -1;
		if (exceptions == null)
		    exceptions = new Exception[]{e};
		else
		    exceptions = (Exception[])arrayAdd(exceptions, e);
	    }
	}
	/* don't bother to weed out problem leases */
	addLogRecord(new LeasesRenewedLogObj(regIDs, leaseIDs, renewals));
	for (int i = regIDs.length; --i >= 0; ) {
	    if (renewals[i] >= 0)
		renewals[i] -= now;
	}
	return new RenewResults(renewals, exceptions);
    }

    /**
     * Renew the leases for absolute expiration times.  Skip any leases
     * with negative expiration times.
     */
    private void renewLeasesAbs(Object[] regIDs,
				Uuid[] leaseIDs,
				long[] renewExpirations)
    {
	for (int i = regIDs.length; --i >= 0; ) {
	    long expiration = renewExpirations[i];
	    if (expiration < 0)
		continue;
	    Object id = regIDs[i];
	    if (id instanceof ServiceID)
		renewServiceLeaseAbs((ServiceID)id, leaseIDs[i], expiration);
	    else
		renewEventLeaseAbs(((Long)id).longValue(), leaseIDs[i],
				   expiration);
	}
    }

    /**
     * The code that does the real work of cancelLeases.  Each element of
     * regIDs must either be a ServiceID (for a service lease) or a Long
     * (for an event lease).  The two 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 regIDs, and has nulls for
     * leases that successfully renewed.
     */
    private Exception[] cancelLeasesDo(Object[] regIDs, Uuid[] leaseIDs) {
	Exception[] exceptions = null;
	for (int i = regIDs.length; --i >= 0; ) {
	    Object id = regIDs[i];
	    try {
		if (id instanceof ServiceID)
		    cancelServiceLeaseDo((ServiceID)id, leaseIDs[i]);
		else
		    cancelEventLeaseDo(((Long)id).longValue(), leaseIDs[i]);
	    } catch (Exception e) {
		if (exceptions == null)
		    exceptions = new Exception[regIDs.length];
		exceptions[i] = e;
	    }
	}
	return exceptions;
    }

    /**
     * Generate events for all matching event registrations.  A null pre
     * represents creation of a new item, a null post represents deletion
     * of an item.
     */
    private void generateEvents(Item pre, Item post, long now) {
	if (inRecovery)
	    return;
	ServiceID sid = (pre != null) ? pre.serviceID : post.serviceID;
	Object val = subEventByService.get(sid);
	if (val instanceof EventReg) {
	    generateEvent((EventReg)val, pre, post, sid, now);
	} else if (val != null) {
	    EventReg[] regs = (EventReg[])val;
	    for (int i = regs.length; --i >= 0; ) {
		generateEvent(regs[i], pre, post, sid, now);
	    }
	}
	for (Iterator iter = subEventByID.values().iterator();
	     iter.hasNext(); )
	{
	    generateEvent((EventReg)iter.next(), pre, post, sid, now);
	}
    }

    /**
     * Generate an event if the event registration matches.  A null pre
     * represents creation of a new item, a null post represents deletion
     * of an item.  sid is the serviceID of the item.
     */
    private void generateEvent(EventReg reg,
			       Item pre,
			       Item post,
			       ServiceID sid,
			       long now)
    {
	if (reg.leaseExpiration <= now)
	    return;
	if ((reg.transitions &
		  ServiceRegistrar.TRANSITION_NOMATCH_MATCH) != 0 &&
		 (pre == null || !matchItem(reg.tmpl, pre)) &&
		 (post != null && matchItem(reg.tmpl, post)))
	    pendingEvent(reg, sid, post,
			 ServiceRegistrar.TRANSITION_NOMATCH_MATCH);
	else if ((reg.transitions &
		  ServiceRegistrar.TRANSITION_MATCH_NOMATCH) != 0 &&
		 (pre != null && matchItem(reg.tmpl, pre)) &&
		 (post == null || !matchItem(reg.tmpl, post)))
	    pendingEvent(reg, sid, post,
			 ServiceRegistrar.TRANSITION_MATCH_NOMATCH);
	else if ((reg.transitions &
		  ServiceRegistrar.TRANSITION_MATCH_MATCH) != 0 &&
		 (pre != null && matchItem(reg.tmpl, pre)) &&
		 (post != null && matchItem(reg.tmpl, post)))
	    pendingEvent(reg, sid, post,
			 ServiceRegistrar.TRANSITION_MATCH_MATCH);
    }

    /** Add a pending EventTask for this event registration. */
    private void pendingEvent(EventReg reg,
			      ServiceID sid,
			      Item item,
			      int transition)
    {
	if (item != null)
	    item = copyItem(item);
	newNotifies.add(new EventTask(reg, sid, item, transition));
    }

    /** Queue all pending EventTasks for processing by the task manager. */
    private void queueEvents() {
	if (!newNotifies.isEmpty()) {
	    tasker.addAll(newNotifies);
	    newNotifies.clear();
	}
    }

    /** Generate a new service ID */
    private ServiceID newServiceID() {
	Uuid uuid = serviceIdGenerator.generate();
	return new ServiceID(
	    uuid.getMostSignificantBits(), uuid.getLeastSignificantBits());
    }

    /** Generate a new lease ID */
    private Uuid newLeaseID() {
	return resourceIdGenerator.generate();
    }

    /**
     * Write the current state of the Registrar to persistent storage.
     * <p>
     * A 'snapshot' of the Registrar's current state is represented by
     * the data contained in certain fields of the Registrar. That data
     * represents many changes -- over time -- to the Registrar'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> our class name
     * <li> log format version number
     * <li> our service ID
     * <li> current event ID
     * <li> current values of administrable parameters
     * <li> contents of the container holding the current registered services
     * <li> null (termination 'marker' for the set of registered services)
     * <li> contents of the container holding the current registered events
     * <li> null (termination 'marker' for the set of registered events)
     * </ul>
     * Each data item is written to the snapshot file in serialized form.
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private void takeSnapshot(OutputStream  out) throws IOException {
	ObjectOutputStream stream = new ObjectOutputStream(out);

	stream.writeUTF(getClass().getName());
	stream.writeInt(LOG_VERSION);
	stream.writeObject(myServiceID);
	stream.writeLong(eventID);
	stream.writeInt(unicastPort);
	stream.writeObject(memberGroups);
	stream.writeObject(lookupGroups);
	marshalLocators(lookupLocators, stream);
	for (Iterator iter = serviceByID.values().iterator(); iter.hasNext(); )
	{
	    stream.writeObject(iter.next());
	}
	stream.writeObject(null);
	for (Iterator iter = eventByID.values().iterator(); iter.hasNext(); )
	{
	    stream.writeObject(iter.next());
	}
	stream.writeObject(null);
	stream.flush();
	logger.finer("wrote state snapshot");
    }

    /**
     * Retrieve the contents of the snapshot file and reconstitute the 'base'
     * state of the Registrar from the retrieved data.
     * <p>
     * The data retrieved by this method from the snapshot file is shown
     * below:
     * <ul>
     * <li> our class name
     * <li> log format version number
     * <li> our service ID
     * <li> current event ID
     * <li> current values of administrable parameters
     * <li> contents of the container holding the current registered services
     * <li> contents of the container holding the current registered events
     * </ul>
     * During recovery, the state of the Registrar 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 resolved and then 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 set of event sequence numbers
     * 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 RegistrarImpl.LocalLogHandler
     */
    private void recoverSnapshot(InputStream in)
	throws IOException, ClassNotFoundException
    {
	ObjectInputStream stream = new ObjectInputStream(in);
	if (!getClass().getName().equals(stream.readUTF()))
	    throw new IOException("log from wrong implementation");
	if (stream.readInt() != LOG_VERSION)
	    throw new IOException("wrong log format version");
	myServiceID = (ServiceID)stream.readObject();
	eventID = stream.readLong();
	unicastPort = stream.readInt();
	memberGroups = (String[])stream.readObject();
	lookupGroups = (String[])stream.readObject();
	lookupLocators = prepareLocators(
	    unmarshalLocators(stream), recoveredLocatorPreparer, true);
	SvcReg sReg;
	while ((sReg = (SvcReg)stream.readObject()) != null) {
	    resolver.resolve(sReg.item);
	    addService(sReg);
	}

	EventReg eReg;
	while ((eReg = (EventReg)stream.readObject()) != null) {
	    resolver.resolve(eReg.tmpl);
	    eReg.prepareListener(recoveredListenerPreparer);
	    eReg.seqNo += Integer.MAX_VALUE;
	    addEvent(eReg);
	}
	recoveredSnapshot = true;
	logger.finer("recovered state from snapshot");
    }

    /**
     * Add a state-change record to persistent storage.
     * <p>
     * Whenever a significant change occurs to the Registrar'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 the
     * Registrar's 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 the current state of the Registrar 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> a new service was registered
     * <li> a new event was registered
     * <li> new attributes were added to an existing service
     * <li> existing attributes of a service were modified
     * <li> new attributes were set in an existing service
     * <li> a single service lease was renewed
     * <li> a single service lease was cancelled
     * <li> a single event lease was renewed
     * <li> a single event lease was cancelled
     * <li> a set of service leases were renewed
     * <li> a set of service leases were cancelled
     * <li> a set of event leases were renewed
     * <li> a set of event leases were cancelled
     * <li> the unicast port number was set
     * <li> the set of Lookup Groups were changed
     * <li> the set of Lookup Locators were changed
     * <li> the set of Member Groups were changed
     * </ul>
     * 
     * @see RegistrarImpl.LocalLogHandler
     */
    private void addLogRecord(LogRecord rec) {
	if (log == null) {
	    return;
	}
	try {
	    log.update(rec, true);
	    if (logger.isLoggable(Level.FINER)) {
		logger.log(Level.FINER, "wrote log record {0}",
			   new Object[]{ rec });
	    }
	    if (++logFileSize >= persistenceSnapshotThreshold) {
		int snapshotSize = serviceByID.size() + eventByID.size();
		if (logFileSize >= persistenceSnapshotWeight * snapshotSize) {
                    concurrentObj.waiterNotify(snapshotNotifier);
		}
	    }
	} catch (Exception e) {
	    if (!Thread.currentThread().isInterrupted()) {
		logger.log(Level.WARNING, "log update failed", e);
	    }
	}
    }
}
