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

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.rmi.MarshalledObject;
import java.rmi.RemoteException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import net.jini.core.discovery.LookupLocator;
import net.jini.core.entry.Entry;
import net.jini.core.lookup.ServiceID;
import net.jini.discovery.DiscoveryManagement;
import net.jini.discovery.DiscoveryLocatorManagement;
import net.jini.discovery.DiscoveryGroupManagement;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.lookup.JoinManager;

import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.security.ProxyPreparer;

import com.sun.jini.config.Config;
import com.sun.jini.logging.Levels;

/**
 * <code>JoinStateManager</code> provides a utility that manages
 * a service's join state (optionally persisting that state) and
 * manages the join protocol protocol on behalf of the service.
 *
 * @author Sun Microsystems, Inc.
 *
 * @see com.sun.jini.lookup.JoinManager
 * @see JoinAdminState
 */
class JoinStateManager implements StorableObject {
    /** <code>ProxyPreparer</code> for <code>LookupLocators</code> */
    private ProxyPreparer lookupLocatorPreparer;

    /**
     * Object used to find lookups. Has to implement DiscoveryManagement
     * and DiscoveryLocatorManagement as well as DiscoveryGroupManagement.
     */
    private DiscoveryGroupManagement dgm;

    /**
     * <code>JoinManager</code> that is handling the details of binding
     * into Jini(TM) lookup services.
     */
    private JoinManager  mgr;

    /**
     * The object that is coordinating our persistent state.
     */
    private LogOps log;

    /**
     * The list of attributes to start with. This field is only used
     * to carry data from the <code>restore</code> method to the
     * <code>startManager</code> method. The current set of attributes
     * is kept by <code>mgr</code>. This field is nulled out by
     * <code>startManager</code>.
     */
    private Entry[]		attributes;

    /**
     * The list of <code>LookupLocator</code>s to start with. This
     * field is only used to carry data from the <code>restore</code>
     * method to the <code>startManager</code> method. The current set
     * of attributes is kept by <code>mgr</code>. This field is nulled
     * out by <code>startManager</code>.  
     */
    private LookupLocator	locators[];


    /**
     * The list of group names to start with. This field is only used
     * to carry data from the <code>restore</code> method to the
     * <code>startManager</code> method. The current set of attributes
     * is kept by <code>mgr</code>. This field is nulled out by
     * <code>startManager</code>.
     */
    private String		groups[];

    /**
     * Conceptually, true if this is the first time this
     * service has come up, implemented as if there was
     * no previous state then this is the first time.
     */
    private boolean initial = true;

    /** Logger for logging join related information */
    private static final Logger logger = 
	Logger.getLogger(OutriggerServerImpl.joinLoggerName);
    
    /**
     * Simple constructor.
     */
    JoinStateManager() { }

    /**
     * Start the manager. Start looking for lookup and registering
     * with them.
     * @param config object to use to obtain
     *               <code>DiscoveryManagement</code> object, and if
     *               this is the initial incarnation of this service, 
     *               the object used to get the initial set of groups,
     *               locators, and deployer defined attributes.
     * @param log object used to persist the manager's state, may be
     *            <code>null</code>.
     * @param serviceID The <code>ServiceID</code> to register
     *                  under.
     * @param service The proxy object to register with lookups.
     * @param baseAttributes Any attributes the implementation wants
     *                       attached, only used if this is the
     *                       initial incarnation.
     * @throws IOException if the is problem persisting the 
     *         initial state or in starting discovery.
     * @throws ConfigurationException if the configuration
     *         is invalid.
     * @throws NullPointerException if <code>config</code>, 
     *         or <code>serviceID</code> is  <code>null</code>.
     */
    void startManager(Configuration config, LogOps log, Object service,
		      ServiceID serviceID, Entry[] baseAttributes) 
	throws IOException, ConfigurationException
    {
	// Default do nothing preparer
	final ProxyPreparer defaultPreparer = 
	    new net.jini.security.BasicProxyPreparer();

	if (serviceID == null)
	    throw new NullPointerException("serviceID can't be null");

	this.log = log;

	lookupLocatorPreparer = 
	    (ProxyPreparer)Config.getNonNullEntry(config, 
		OutriggerServerImpl.COMPONENT_NAME, "lookupLocatorPreparer",
		ProxyPreparer.class, defaultPreparer);

	dgm = (DiscoveryGroupManagement)
	    Config.getNonNullEntry(config, 
		OutriggerServerImpl.COMPONENT_NAME, "discoveryManager",
		DiscoveryGroupManagement.class, 
		new LookupDiscoveryManager(
                    DiscoveryGroupManagement.NO_GROUPS, null, null,
		    config));

	if (!(dgm instanceof DiscoveryManagement))
	    throw throwNewConfigurationException("Entry for component " +
		OutriggerServerImpl.COMPONENT_NAME + ", name " +
		"discoveryManager must implement " +
	        "net.jini.discovery.DiscoveryGroupManagement");

	if (!(dgm instanceof DiscoveryLocatorManagement))
	    throw throwNewConfigurationException("Entry for component " +
		OutriggerServerImpl.COMPONENT_NAME + ", name " +
		"discoveryManager must implement " +
		"net.jini.discovery.DiscoveryLocatorManagement");

	final String[] toCheck = dgm.getGroups();
	if (toCheck == null || toCheck.length != 0)
	    throw throwNewConfigurationException("Entry for component " +
		OutriggerServerImpl.COMPONENT_NAME + ", name " +
		"discoveryManager must be initially configured with no " +
                "groups");

	if (((DiscoveryLocatorManagement)dgm).getLocators().length != 0)
	    throw throwNewConfigurationException("Entry for component " +
                OutriggerServerImpl.COMPONENT_NAME + ", name " +
		"discoveryManager must be initially configured with no " +
		"locators");

	// if this is the first incarnation, consult config for groups,
	// locators and attributes.
	if (initial) {
	    groups = (String[])
		config.getEntry(OutriggerServerImpl.COMPONENT_NAME,
		    "initialLookupGroups", String[].class,
		    new String[]{""});

	    locators = (LookupLocator[])
		Config.getNonNullEntry(config, 
		    OutriggerServerImpl.COMPONENT_NAME,
                    "initialLookupLocators", LookupLocator[].class, 
                    new LookupLocator[0]);

	    final Entry[] cAttrs = (Entry[])
		Config.getNonNullEntry(config, 
                    OutriggerServerImpl.COMPONENT_NAME,
		    "initialLookupAttributes", Entry[].class, new Entry[0]);

	    if (cAttrs.length == 0) {
		attributes = baseAttributes;
	    } else {
		attributes = 
		    new Entry[cAttrs.length + baseAttributes.length];
		System.arraycopy(baseAttributes, 0, attributes, 
				 0, baseAttributes.length);
		System.arraycopy(cAttrs, 0, attributes, 
				 baseAttributes.length, cAttrs.length);
	    }
	} else {
	    /* recovery : if there are any locators get and
	     * use recoveredLookupLocatorPreparer
	     */
	    if (locators.length > 0) {
		final ProxyPreparer recoveredLookupLocatorPreparer = 
		    (ProxyPreparer)Config.getNonNullEntry(config, 
		        OutriggerServerImpl.COMPONENT_NAME,
			"recoveredLookupLocatorPreparer", ProxyPreparer.class,
			 defaultPreparer);

		final List prepared = new java.util.LinkedList();
		for (int i=0; i<locators.length; i++) {
		    final LookupLocator locator = locators[i];
		    try {
			prepared.add(recoveredLookupLocatorPreparer.
				     prepareProxy(locator));
		    } catch (Throwable t) {
			logger.log(Level.INFO,
			    "Encountered exception preparing lookup locator " +
			    "for " + locator + ", dropping locator", t);
		    }
		}

		locators = 
		    (LookupLocator[])prepared.toArray(new LookupLocator[0]);
	    }
	}

	// Now that we have groups & locators (either from 
	// a previous incarnation or from the config) start discovery.
	if (logger.isLoggable(Level.CONFIG)) {
	    if (groups == null) {
		logger.log(Level.CONFIG, "joining all groups");
	    } else if (groups.length == 0) {
		logger.log(Level.CONFIG, "joining no groups");	
	    } else {
		final StringBuffer buf = new StringBuffer();
		buf.append("joining groups:");
		for (int i=0; i<groups.length; i++) {
		    if (i != 0)
			buf.append(",");

		    buf.append("\"");
		    buf.append(groups[i]);
		    buf.append("\"");
		}
		logger.log(Level.CONFIG, buf.toString());
	    }

	    if (locators.length == 0) {
		logger.log(Level.CONFIG, "joining no specific registrars");
	    } else {
		final StringBuffer buf = new StringBuffer();
		buf.append("joining the specific registrars:");
		for (int i=0; i<locators.length; i++) {
		    if (i != 0)
			buf.append(", ");

		    buf.append(locators[i]);
		}
		logger.log(Level.CONFIG, buf.toString());
	    }

	    if (attributes.length == 0) {
		logger.log(Level.CONFIG, "registering no attributes");
	    } else {
		final StringBuffer buf = new StringBuffer();
		buf.append("registering the attributes:");
		for (int i=0; i<attributes.length; i++) {
		    if (i != 0)
			buf.append(", ");

		    buf.append(attributes[i]);
		}
		logger.log(Level.CONFIG, buf.toString());		
	    }
	}

	dgm.setGroups(groups);
	((DiscoveryLocatorManagement)dgm).setLocators(locators);

	mgr = new JoinManager(service, attributes, serviceID, 
			      (DiscoveryManagement)dgm, null, config);

	// Once we are running we don't need the attributes,
	// locators, and groups fields, null them out (the
	// state is in the mgr and dgm.
	attributes = null;
	groups = null;
	locators = null;

	// Now that we have state, make sure it is written to disk.
	update();
    }

    /** 
     * Make a good faith attempt to terminate
     * discovery, and cancel any lookup registrations.  */
    public void destroy() {
	// Unregister with lookup

	// Terminate the JoinManager first so it will not call
	// into the dgm after it has been terminated.
	if (mgr != null)
	    mgr.terminate();

	if (dgm != null) 
	    ((DiscoveryManagement)dgm).terminate();
    }

    /* Basically we are implementing JoinAdmin, for get methods we just
     * delegate to JoinManager, for the set methods we call
     * JoinManager to and then persist the change by calling the
     * appropriate method on our JoinAdminState.  If the call on our
     * JoinAdminState throws an IOException we throw a runtime
     * exception since JoinAdmin methods don't let us throw a
     * IOException 
     */

    /** 
     * Get the current attribute sets for the service. 
     * 
     * @return the current attribute sets for the service
     */
    public Entry[] getLookupAttributes() {
	return mgr.getAttributes();
    }

    /** 
     * Add attribute sets for the service.  The resulting set will be used
     * for all future joins.  The attribute sets are also added to all 
     * currently-joined lookup services.
     *
     * @param attrSets the attribute sets to add
     * @throws java.rmi.RuntimeException if the change can not be persisted.
     */
    public void addLookupAttributes(Entry[] attrSets) {
	mgr.addAttributes(attrSets, true);
	update();
    }

    /**  
     * Modify the current attribute sets, using the same semantics as
     * ServiceRegistration.modifyAttributes.  The resulting set will be used
     * for all future joins.  The same modifications are also made to all 
     * currently-joined lookup services.
     *
     * @param attrSetTemplates the templates for matching attribute sets
     * @param attrSets the modifications to make to matching sets
     *     
     * @throws java.rmi.RuntimeException if the change can not be persisted.
     * @see net.jini.core.lookup.ServiceRegistration#modifyAttributes
     */
    public void modifyLookupAttributes(Entry[] attrSetTemplates,
				       Entry[] attrSets) {
	mgr.modifyAttributes(attrSetTemplates, attrSets, true);
	update();
    }

   /**
     * Get the list of groups to join.  An empty array means the service
     * joins no groups (as opposed to "all" groups).
     *
     * @return an array of groups to join. An empty array means the service
     *         joins no groups (as opposed to "all" groups).
     * @see #setLookupGroups
     */
    public String[] getLookupGroups() {
	return dgm.getGroups();
    }

    /**
     * Add new groups to the set to join.  Lookup services in the new
     * groups will be discovered and joined.
     *
     * @param groups groups to join
     * @throws java.rmi.RuntimeException if the change can not be persisted.
     * @see #removeLookupGroups
     */
    public void addLookupGroups(String[] groups) {
 	try {
	    dgm.addGroups(groups);
	} catch (IOException e) {
	    throw propagateIOException("Could not change groups", e);
	}
	update();
    }

    /**
     * Remove groups from the set to join.  Leases are cancelled at lookup
     * services that are not members of any of the remaining groups.
     *
     * @param groups groups to leave
     * @throws java.rmi.RuntimeException if the change can not be persisted.
     * @see #addLookupGroups
     */
    public void removeLookupGroups(String[] groups) {
	dgm.removeGroups(groups);
	update();
    }

    /**
     * Replace the list of groups to join with a new list.  Leases are
     * cancelled at lookup services that are not members of any of the
     * new groups.  Lookup services in the new groups will be discovered
     * and joined.
     *
     * @param groups groups to join
     * @throws java.rmi.RuntimeException if the change can not be persisted.
     * @see #getLookupGroups
     */
    public void setLookupGroups(String[] groups) {
	try {
	    dgm.setGroups(groups);
	} catch (IOException e) {
	    throw propagateIOException("Could not change groups", e);
	}
	update();
    }
    
    /** 
     * Get the list of locators of specific lookup services to join. 
     *
     * @return the list of locators of specific lookup services to join
     * @see #setLookupLocators
     */
    public LookupLocator[] getLookupLocators() {
	return ((DiscoveryLocatorManagement)dgm).getLocators();
    }

    /**
     * Add locators for specific new lookup services to join.  The new
     * lookup services will be discovered and joined.
     *
     * @param locators locators of specific lookup services to join
     * @throws java.rmi.RuntimeException if the change can not be persisted.
     * @see #removeLookupLocators
     */
    public void addLookupLocators(LookupLocator[] locators) 
        throws RemoteException
    {
	prepareLocators(locators);
	((DiscoveryLocatorManagement)dgm).addLocators(locators);
	update();
    }

    /**
     * Remove locators for specific lookup services from the set to join.
     * Any leases held at the lookup services are cancelled.
     *
     * @param locators locators of specific lookup services to leave
     * @throws java.rmi.RuntimeException if the change can not be persisted.
     * @see #addLookupLocators
     */
    public void removeLookupLocators(LookupLocator[] locators) 
        throws RemoteException
    {
	prepareLocators(locators);
	((DiscoveryLocatorManagement)dgm).removeLocators(locators);
	update();
    }

    /**
     * Replace the list of locators of specific lookup services to join
     * with a new list.  Leases are cancelled at lookup services that were
     * in the old list but are not in the new list.  Any new lookup services
     * will be discovered and joined.
     *
     * @param locators locators of specific lookup services to join
     * @throws java.rmi.RuntimeException if the change can not be persisted.
     * @see #getLookupLocators
     */
    public void setLookupLocators(LookupLocator[] locators) 
        throws RemoteException
    {
	prepareLocators(locators);
	((DiscoveryLocatorManagement)dgm).setLocators(locators);
	update();
    }

    private void update() {
	if (log != null)
	    log.joinStateOp(this);
    }

    /**
     * Apply <code>lookupLocatorPreparer</code> to each locator in the
     * array, replacing the original locator with the result of the
     * <code>prepareProxy</code> call. If call fails with an exception
     * throw that exception.  
     * @param locators the <code>LookupLocator</code>s to be prepared.
     * @throws RemoteException if preparation of any of the locators
     *         does.
     * @throws SecurityException if preparation of any of the locators
     *         does.
     */
    private void prepareLocators(LookupLocator[] locators) 
        throws RemoteException 
    {
	for (int i = 0; i<locators.length; i++) 
	    locators[i] = (LookupLocator)lookupLocatorPreparer.prepareProxy(
		locators[i]);
    }

    /**
     * Utility method to write out an array of entities to an
     * <code>ObjectOutputStream</code>.  Can be recovered by a call
     * to <code>readAttributes()</code>
     * <p>
     * Packages each attribute in its own <code>MarshalledObject</code> so
     * a bad codebase on an attribute class will not corrupt the whole array.
     * @see JoinAdminActivationState#readAttributes
     */  
    static private void writeAttributes(Entry[] attributes,
                                        ObjectOutputStream out)
        throws IOException
    {
        // Need to package each attribute in its own marshaled object,
        // this makes sure that the attribute's code base is preserved
        // and when we unpack to discard attributes who's codebase
        // has been lost without throwing away those we can still deal with.
         
        out.writeInt(attributes.length);
        for (int i=0; i<attributes.length; i++) {
            out.writeObject(new MarshalledObject(attributes[i]));
	}
    }
 
    /**
     * Utility method to read in an array of entities from a
     * <code>ObjectInputStream</code>.  Array should have been written
     * by a call to <code>writeAttributes()</code>
     * <p>
     *   
     * Will try and recover as many attributes as possible.
     * Attributes which can't be recovered won't be returned but they
     * will remain in the log.
     *   
     * @see JoinAdminActivationState#writeAttributes
     */  
    static private Entry[] readAttributes(ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        final List entries = new java.util.LinkedList();
        final int objectCount = in.readInt();
        for (int i=0; i<objectCount; i++) {
            try {
                MarshalledObject mo = (MarshalledObject)in.readObject();
                entries.add(mo.get());
            } catch (IOException e) {
		logger.log(Level.INFO, "Encountered IOException recovering " +
                    "attribute, dropping attribute", e);
            } catch (ClassNotFoundException e) {
		logger.log(Level.INFO, "Encountered ClassNotFoundException " +
		    "recovering attribute, dropping attribute", e);
            }
        }
 
        return (Entry[])entries.toArray(new Entry[0]);
    }

    // -----------------------------------
    //  Methods required by StorableObject
    // -----------------------------------
 
    // inherit doc comment
    public void store(ObjectOutputStream out) throws IOException {
        writeAttributes(mgr.getAttributes(), out);
        out.writeObject(((DiscoveryLocatorManagement)dgm).getLocators());
        out.writeObject(dgm.getGroups());
    }

    // inherit doc comment
    public void restore(ObjectInputStream in) 
	throws IOException, ClassNotFoundException 
    {
	initial = false;
        attributes = readAttributes(in);
        locators   = (LookupLocator [])in.readObject();
        groups     = (String [])in.readObject();
    }

    /**
     * Construct, log, and throw a new ConfigurationException with
     * the given message.
     */
    private static ConfigurationException throwNewConfigurationException(
            String msg) 
	throws ConfigurationException
    {
	final ConfigurationException e = new ConfigurationException(msg);

	if (logger.isLoggable(Levels.FAILED)) {
	    logger.log(Levels.FAILED, msg, e);
	}

	throw e;
    }	

    /** 
     * Propagate an IOException by wrapping it in a RuntimeException.
     * Performs appropriate logging.
     */
    private static RuntimeException propagateIOException(String msg,
							 IOException nested)
    {
	final RuntimeException e = new RuntimeException(msg, nested);
	if (logger.isLoggable(Levels.FAILED)) {
	    logger.log(Levels.FAILED, msg, e);
	}

	throw e;
    }
}
