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

import java.io.InvalidObjectException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.rmi.MarshalledObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import net.jini.core.event.RemoteEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.io.MarshalledInstance;

import com.sun.jini.proxy.MarshalledWrapper;

/**
 * Whenever the lookup discovery service discovers or discards a lookup
 * service matching the discovery/discard criteria of one or more of its 
 * registrations, the lookup discovery service sends an instance of this
 * class to the listener corresponding to each such registration.
 * <p>
 * For each registration created by the lookup discovery service, an event
 * identifier is generated. That event identifier uniquely maps the
 * registration to the listener (submitted by the client to the lookup
 * discovery service during the registration process) and to the set of
 * groups and locators the client is interested in discovering. The event
 * identifier is unique across all other active registrations with the
 * lookup discovery service, and is sent to the listener as part of the
 * event.
 * <p>
 * Because clients of the lookup discovery service need to know not only 
 * when a targeted lookup service has been discovered, but also when it 
 * has been discarded, the lookup discovery service uses an instance of
 * this class to notify a client's registration(s) when either of these
 * events occurs.
 * <p>
 * This class extends RemoteEvent, adding the following additional items
 * of abstract state: a boolean indicating whether the lookup services
 * referenced by the event have been discovered or discarded; and a set
 * consisting of proxy objects where each proxy is a marshalled instance
 * of the ServiceRegistrar interface, and each is a proxy of one of the
 * recently discovered or discarded lookup service(s). Methods are defined
 * through which this additional state may be retrieved upon receipt of an
 * instance of this class.
 * <p>
 * The sequence numbers for a given event identifier are "strictly
 * increasing". This means that when any two such successive events
 * have sequence numbers differing by only a value of 1, then it is
 * guaranteed that no events have been missed. On the other hand, when
 * viewing the set of received events in order, if the difference 
 * between the sequence numbers of two successive events is greater
 * than 1, then one or more events may or may not have been missed.
 * For example, a difference greater than 1 could occur if the lookup
 * discovery service crashes, even if no events are lost because of
 * the crash. When two successive events have sequence numbers whose
 * difference is greater than 1, there is said to be a "gap" between
 * the events.
 * <p>
 * When a gap occurs between events, the state of the locally managed
 * set of lookup services may or may not fall "out of sync" with the
 * corresponding remote state. For example, if the gap corresponds to
 * a missed event representing the (initial) discovery of a targeted
 * lookup service, the remote state will reflect this discovery whereas
 * the local state will not. When such a situation occurs, clients may
 * wish to employ the methods of the corresponding registration object
 * to query the current remote state in order to update the current
 * local state. 
 * <p>
 * Thus, clients typically use this class to determine if conditions
 * are right for a loss of synchronization (by verifying the existence
 * of a gap in the event sequence). Clients then typically use the
 * methods provided by the registration object to both determine if a
 * loss of synchronization has actually occurred, and to correct
 * such a situation when it does occur.
 *
 * @author Sun Microsystems, Inc.
 *
 * @see net.jini.core.event.RemoteEvent
 * @see net.jini.core.lookup.ServiceRegistrar
 */
public class RemoteDiscoveryEvent extends RemoteEvent {

    private static final long serialVersionUID = -9171289945014585248L;

    /**
     * Flag indicating whether the event is a discovery event or a discard
     * event. If this variable is <code>false</code>, then the lookup services
     * referenced by this event were just discovered; if <code>true</code>,
     * then those lookup services were just discarded.
     *
     * @serial
     */
    protected boolean discarded;

    /**
     * List consisting of marshalled proxy objects where each proxy implements
     * the <code>ServiceRegistrar</code> interface, and each is a proxy of
     * one of the recently discovered or discarded lookup service(s); the
     * lookup service(s) with which this event is associated. 
     * <p>
     * Each proxy in this list is individually marshalled in order to add
     * an additional 'layer' of serialization. Placing this serialization
     * "wrapper" around each element prevents the deserialization mechanism
     * from attempting to deserialize the individual elements in the list.
     * That is, the deserialization mechanism will only deserialize the list
     * itself. After the list itself is successfully deserialized, the client
     * (or a third party, if the client requested that events be sent to a
     * third party such as a mailbox), can then attempt to unmarshal each
     * element separately. This allows each success to be captured, and each
     * failure to be noted.
     * <p>
     * If the elements of this list were not each marshalled separately,
     * then upon encountering failure while attempting to deserialize any
     * one element of the list, the deserialization mechanism's
     * <code>readObject</code> method will throw an <code>IOException</code>;
     * resulting in the loss of all of the elements of the list, even those
     * that could be successfully deserialized.
     *
     * @serial
     */
    protected ArrayList marshalledRegs;

    /**
     * Array containing a subset of the set of proxies to the lookup
     * service(s) with which this event is associated. The elements of this
     * array correspond to those elements of the <code>marshalledRegs<code>
     * array that were successfully unmarshalled (at least once) as a result
     * of one or more invocations of the <code>getRegistrars</code> method
     * of this event. Upon deserializing this event, this array is empty,
     * but of the same size as <code>marshalledRegs<code>; and will be
     * populated when the recipient of this event retrieves the registrars
     * corresponding to the elements of <code>marshalledRegs<code>.
     *
     * @serial
     */
    protected ServiceRegistrar[] regs;

    /**
     * <code>Map</code> from the service IDs of the registrars of this event
     * to the groups in which each registrar is a member.
     *
     * @serial
     */
    protected Map groups;

    /**
     * Flag related to the verification of codebase integrity. A value of
     * <code>true</code> in this field indicates that the last time this
     * event was unmarshalled, the enforcement of codebase integrity was
     * in effect.
     */
    private transient boolean integrity;

    /**
     * Constructs a new instance of <code>RemoteDiscoveryEvent</code>.
     *
     * @param source     reference to the lookup discovery service that
     *                   generated the event
     * @param eventID    the event identifier that maps a particular
     *                   registration to its listener and targeted groups
     *                   and locators
     * @param seqNum     the sequence number of this event
     * @param handback   the client handback (null may be input)
     * @param discarded  flag indicating whether the event being constructed
     *                   is a discovery event or a discard event
     * @param groups     mapping from the registrars of this event to the
     *                   groups in which each registrar is a member
     *
     * @throws java.io.IOException when serialization failure occurs on 
     *         every registrar of this event. That is, if at least one
     *         registrar is successfully serialized, then this exception
     *         will not be thrown.
     *
     * @throws java.lang.NullPointerException this exception occurs when
     *         either <code>null</code> is input for the map parameter, or
     *         at least one element of that map is <code>null</code>.
     *
     * @throws java.lang.IllegalArgumentException this exception occurs
     *         when an empty set of registrars is input.
     */
    public RemoteDiscoveryEvent(Object source,
                                long eventID,
                                long seqNum,
                                MarshalledObject handback,
                                boolean discarded,
                                Map groups)    throws IOException
    {
	super(source, eventID, seqNum, handback);
	this.discarded = discarded;
        if(groups != null) {
            /* If the set of registrars is empty, throw exception */
            if(groups.size() == 0) {
                throw new IllegalArgumentException("empty input map");
            }
            ServiceRegistrar[] registrars =
                          (ServiceRegistrar[])(groups.keySet()).toArray
                                        (new ServiceRegistrar[groups.size()]);
            /* If any elements of the array are null, throw exception */
            for(int i=0;i<registrars.length;i++) {
                if(registrars[i] == null) {
                    throw new NullPointerException("null element ("+i
                                                   +") in input map");
                }
            }
            /* Create a new marshalled instance of each element of the
             * registrars array, and place each in the marshalledRegs
             * ArrayList of this class. Also, construct the groups map that
             * contains the mappings from the service ID of each registrar
             * to the registrar's corresponding member groups.
             *
             * Drop any element that can't be serialized.
             */
            this.groups = new HashMap(groups.size());
            this.marshalledRegs = new ArrayList(groups.size());
            for(int i=0;i<registrars.length;i++) {
                try {
                    marshalledRegs.add(new MarshalledObject(registrars[i]));
                    (this.groups).put((registrars[i]).getServiceID(),
                                       groups.get(registrars[i]) );
		} catch(IOException e) { /* drop if can't serialize */ }
            }
            if( !(marshalledRegs.isEmpty()) ) {
                regs = new ServiceRegistrar[marshalledRegs.size()];
            } else {
                throw new IOException("failed to serialize any of the "
                                      +registrars.length+" elements");
            }
	} else {
            throw new NullPointerException("null input map");
        }
    }//end constructor

    /**
     * Returns the value of the boolean flag that indicates whether this
     * event is a discovery event or a discard event.
     * 
     * @return <code>true</code> if this is a discard event, <code>false</code>
     *         if it is a discovery event.
     */
    public boolean isDiscarded() {
	return discarded;
    }//end isDiscarded

    /**
     * Returns an array consisting of instances of the ServiceRegistrar
     * interface. Each element in the returned set is a proxy to one of
     * the newly discovered or discarded lookup service(s) that caused
     * the current instance of this event class to be sent to the listener
     * of the client's registration. Note that a new array is returned
     * on every call.
     * <p>
     * When the lookup discovery service sends an instance of this event
     * class to the listener of a client's registration, the set of lookup
     * service proxies contained in the event is sent as a set of marshalled
     * instances of the ServiceRegistrar interface. Thus, in order to 
     * construct the return set, this method attempts to unmarshal each
     * element of that set of proxies. Should a failure occur while
     * attempting to unmarshal any of the elements of the set of marshalled
     * proxy objects contained in the current instance of this class, this
     * method will throw an exception of type LookupUnmarshalException. 
     * <p>
     * When a LookupUnmarshalException is thrown by this method, the
     * contents of the exception provides the client with the following
     * useful information: (1) the knowledge that a problem has occurred
     * while unmarshalling at least one of the as yet unmarshalled proxy
     * objects, (2) the set consisting of the proxy objects that were
     * successfully unmarshalled (either on the current invocation of
     * this method or on some previous invocation), (3) the set consisting
     * of the marshalled proxy objects that could not be unmarshalled
     * during the current or any previous invocation of this method, and
     * (4) the set of exceptions corresponding to each failed attempt at
     * unmarshalling during the current invocation of this method.
     * <p>
     * Typically, the type of exception that occurs when attempting to
     * unmarshal an element of the set of marshalled proxies is either an
     * IOException or a ClassNotFoundException. A ClassNotFoundException 
     * occurs whenever a remote field of the marshalled proxy cannot be
     * retrieved (usually because the codebase of one of the field's classes
     * or interfaces is currently 'down'). To address this situation, the
     * client may wish to invoke this method at some later time when the
     * 'down' codebase(s) may be accessible. Thus, the client can invoke
     * this method multiple times until all of the elements of the set of
     * marshalled proxies can be successfully unmarshalled.
     * <p>
     * Note that once an element of the set of marshalled proxy objects has
     * been successfully unmarshalled on a particular invocation of this
     * method, the resulting unmarshalled proxy is stored for return on
     * all future invocations of this method. That is, once successfully
     * unmarshalled, no attempt will be made to unmarshal that element on
     * any future invocations of this method. Thus, if this method returns
     * successfully without throwing a LookupUnmarshalException, the client
     * is guaranteed that all marshalled proxies have been successfully
     * unmarshalled; and any future invocations of this method will return
     * successfully.
     *
     * @return an array consisting of references to the discovered or discarded
     *         lookup service(s) corresponding to this event.
     * 
     * @throws net.jini.discovery.LookupUnmarshalException this exception
     *         occurs when at least one of the set of lookup service
     *         references cannot be deserialized (unmarshalled).
     *
     * @see net.jini.discovery.LookupUnmarshalException
     */
    public ServiceRegistrar[] getRegistrars() throws LookupUnmarshalException {
	synchronized (marshalledRegs) {
            if( marshalledRegs.size() > 0 ) {
                ArrayList unmarshalledRegs = new ArrayList();
                ArrayList exceptions = unmarshalRegistrars(marshalledRegs,
                                                           unmarshalledRegs);
                /* Add the un-marshalled elements to the end of regs */
                insertRegistrars(regs,unmarshalledRegs);
                if( exceptions.size() > 0 ) {
                    throw(new LookupUnmarshalException
                      ( clipNullsFromEnd(regs),
                        (MarshalledObject[])(marshalledRegs.toArray
                               (new MarshalledObject[marshalledRegs.size()])),
                        (Throwable[])(exceptions.toArray
                               (new Throwable[exceptions.size()])),
                        "failed to unmarshal at least one ServiceRegistrar") );
                }//endif
            }//endif
            return clipNullsFromEnd(regs);
        }//end sync(marshalledRegs)
    }//end getRegistrars

    /**
     * Returns a set that maps to the service ID of each registrar referenced
     * by this event, the current set of groups in which each registrar is a
     * member.
     * <p>
     * To retrieve the set of member groups corresponding to any element
     * of the array returned by the <code>getRegistrars</code> method,
     * simply use the service ID of the desired element from that array as
     * the key to the <code>get</code> method of the <code>Map</code> object
     * returned by this method and cast to <code>String</code>[].
     * <p>
     * Note that the same <code>Map</code> object is returned on every
     * call to this method; that is, a copy is not made.
     *
     *  @return <code>Map</code> whose key set consists of the service IDs
     *          of each lookup service with which this event is associated,
     *          and whose values are <code>String</code>[] arrays containing
     *          the names of the groups in which the lookup service having
     *          the corresponding service ID is a member.
     */
    public Map getGroups() {
        return groups;
    }//end getGroups

    /**
     * Attempts to unmarshal each element of the first input argument. When
     * an element of that argument is successfully unmarshalled, that element
     * is removed from the first set and the resulting unmarshalled proxy
     * is placed in the set referenced by the second input argument. 
     * Whenever failure occurs as a result of an attempt to unmarshal one
     * of the elements of the first set, the exception that is thrown as
     * as a result of that failure is placed in the returned set of
     * exceptions, and the element from the first set that caused the
     * failure is left in the first set to facilitate the creation of the
     * <code>LookupUnmarshalException</code> that will ultimately be thrown
     * from <code>getRegistrars</code>.
     * <p>
     * Note that there is a one-to-one correspondence between the exceptions
     * contained in the return set and the remaining elements in the first
     * set after all unmarshalling attempts have completed.
     * 
     * @param marshalledRegs   an ArrayList object consisting of marshalled
     *                         instances of ServiceRegistrar, each 
     *                         corresponding to a proxy to a lookup service.
     *
     * @param unmarshalledRegs an ArrayList object consisting of all
     *                         successfully unmarshalled proxies from
     *                         the first argument.
     *
     * @return an ArrayList consisting of the exceptions that occur as a
     *         result of attempts to unmarshal each element of the first
     *         argument to this method.
     */
    private ArrayList unmarshalRegistrars(ArrayList marshalledRegs,
                                          ArrayList unmarshalledRegs)
    {
        ArrayList exceptions = new ArrayList();
       /* Try to un-marshal each element in marshalledRegs; verify codebase
        * when appropriate.
        * 
        * If current element is successfully un-marshalled: 
        *    -- record the un-marshalled element
        *    -- delete the corresponding marshalled element from its set
        * 
        * If current element cannot be un-marshalled:
        *    -- place the exception in the return object
        *    -- leave the corresponding marshalled element in its set
        *    -- increment the index to the next marshalled element
        *
        * Note that index 'n' used in the loop is only a counter. That is,
        * it is intentional that the element at index 'i' is the element
        * that is unmarshalled, not the element at index 'n'. This is because
        * whenever the element is successfully unmarshalled, the element is
        * removed from the set of marshalled registrars, decreasing that set
        * by 1 element. Thus, the 'next' element to unmarshal is actually at
        * the same index as the last element that was unmarshalled.
        */
        int i = 0;
        int nMarshalledRegs = marshalledRegs.size();
        for(int n=0;n<nMarshalledRegs;n++) {
            MarshalledObject marshalledObj
                                  = (MarshalledObject)(marshalledRegs.get(i));
            MarshalledInstance marshalledInstance
                                  =  new MarshalledInstance(marshalledObj);
            try {
                ServiceRegistrar reg =
                         (ServiceRegistrar)(marshalledInstance.get(integrity));
                /* Success: record the un-marshalled element
                 *          delete the corresponding un-marshalled element
                 */
                unmarshalledRegs.add( reg );
                marshalledRegs.remove(i);
            } catch(Throwable e) {
                exceptions.add(e);
                i=i+1;
            }
        }//end loop
        return exceptions;
    }//end unmarshalRegistrars

    /**
     * Places the lookup service reference(s), contained in the input
     * ArrayList, into the 'empty' slots occurring at the end (indicated
     * by the first null element) of the input array.
     * 
     * @param regsArray array that will receive the new references.
     * 
     * @param regsList ArrayList containing the ServiceRegistrar references
     *        to place in regsArray input argument.
     */
    private static void insertRegistrars(ServiceRegistrar[] regsArray,
                                         ArrayList regsList)
    {
        if((regsArray != null) && (regsList != null)) {
            int lenA = regsArray.length;
            int lenB = regsList.size();
            if((lenA == 0) || (lenB == 0)) return;
            int beg = indexFirstNull(regsArray);
            int end = ( (beg+lenB) <= lenA ? (beg+lenB) : (lenA) );
            for(int i=beg, j=0; i<end; i++,j++) {
                regsArray[i] = (ServiceRegistrar)(regsList.get(j));
            }
        }
    }//end insertRegistrars

    /**
     * Convenience method that copies elements from the given array into
     * a new array, and returns the new array. This method begins with the
     * first element in the given array, and stops when it encounters the
     * first <code>null</code> element.
     * 
     * @param regsArray array from which to copy elements
     * 
     * @return array of <code>ServiceRegistrar</code> containing each element
     *         of the given array from its first element up to, but not 
     *         including, the <code>null</code> element; and all subsequent
     *         elements. If the first element of the given array is 
     *         <code>null</code>, then this method will return an empty array.
     */
    private static ServiceRegistrar[] clipNullsFromEnd
                                               (ServiceRegistrar[] regsArray)
    {
        if( regsArray == null) return new ServiceRegistrar[0];
        int clippedLen = indexFirstNull(regsArray);
        ServiceRegistrar[] clippedArray = new ServiceRegistrar[clippedLen];
        for(int i=0; i<clippedLen; i++) {
            clippedArray[i] = regsArray[i];
        }//end loop
        return clippedArray;
    }//end clipNullsFromEnd

    /**
     * Finds the index of the first element in the input array that contains
     * null.
     * <p>
     * If the array is null (or has zero length), -1 will be returned. If
     * every element of the array is non-null, this method will return
     * the length of the array. Thus, after invoking this method, it is
     * important to test for these conditions to avoid the occurrence of an 
     * IndexOutOfBoundsException when using the value returned by this
     * method.
     * 
     * @param arr Object array to examine for the first occurrence of null
     *
     * @return the index of the first element in the input array that contains
     *         null. A value of -1 is returned if the input array is null;
     *         the length of the array is returned if no element in the
     *         array is null.
     */
    private static int indexFirstNull(Object[] arr) {
        int i = -1;
        if( (arr == null) || (arr.length == 0) ) return i;
        for(i=0;i<arr.length;i++) {
            if(arr[i] == null) return i;
        }
        return i;
    }//end indexFirstNull

    /** 
     * When an instance of this class is deserialized, this method is
     * automatically invoked. This implementation of this method validates
     * the state of the deserialized instance, and additionally determines
     * whether or not codebase integrity verification was performed when
     * unmarshalling occurred.
     *
     * @throws <code>InvalidObjectException</code> if the state of the
     *         deserialized instance of this class is found to be invalid.
     */
    private void readObject(ObjectInputStream s)  
                               throws IOException, ClassNotFoundException
    {
        s.defaultReadObject();
        /* Verify source */
        if(source == null) {
            throw new InvalidObjectException("RemoteDiscoveryEvent.readObject "
                                            +"failure - source field is null");
        }//endif
        /* Retrieve the value of the integrity flag */
        integrity = MarshalledWrapper.integrityEnforced(s);
    }//end readObject

}//end class RemoteDiscoveryEvent

