/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.sun.jini.start;

import net.jini.config.Configuration;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InvalidObjectException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.rmi.activation.ActivationException;
import java.rmi.activation.ActivationGroupDesc;
import java.rmi.activation.ActivationGroupDesc.CommandEnvironment;
import java.rmi.activation.ActivationGroupID;
import java.rmi.activation.ActivationSystem;
import java.rmi.MarshalledObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.logging.Logger;
import java.util.Properties;

/**
 * Class used to create a shared activation group. 
 * Clients construct this object with the details
 * of the activation group to be launched, then call <code>create</code>
 * to register the activation system group with the activation system
 * <P>
 * This class, in conjunction with the {@link ActivateWrapper} class,
 * creates an activation group suitable for hosting
 * multiple service objects, with each object 
 * maintaining a distinct codebase and policy. 
 *
 * 
 *
 * @since 2.0
 */

public class SharedActivationGroupDescriptor 
    implements ServiceDescriptor, Serializable
{
    private static final long serialVersionUID = 1L;

    // Required Args
    /**
     * @serial <code>String</code> representing VM policy filename or URL
     */
    private final String policy;
    
    /**
     * @serial <code>String</code> representing the class path of the shared VM
     *     classes
     */
    private final String classpath;
    
    /**
     * @serial <code>String</code> representing the location where group identifier 
     *     information will be persisted
     */
    private final String log;
    
    // Optional Args
    /**
     * @serial <code>String</code> representing the VM command to use
     */
    private final String serverCommand;
    
    /**
     * @serial <code>String[]</code> representing array of command line 
     *     options to pass to the VM's command line 
     */
    private final String[] serverOptions;
    
    /**
     * @serial <code>Properties</code> representing propperties to pass
     *     to the VM's command line
     */
    private final Properties serverProperties;
    
    /**
     * @serial <code>String</code> representing host name of the desired 
     *     activation system
     */
    private final String host;
    
    /**
     * @serial <code>int</code> representing port of the desired activation 
     *     system
     */
    private final int port;
    
    private static final String GROUP_COOKIE_FILE = "cookie";
    
    private static final Logger logger = ServiceStarter.logger;
    

    /**
     * Trivial constructor. Simply calls the other overloaded constructor
     * with the <code>host</code> and <code>port</code> parameters set to 
     * <code>null</code> and 0, respectively.
     * 
     */
    public SharedActivationGroupDescriptor(
	//Required Args
	String policy, String classpath, String log,
	//Optional Args
	String serverCommand, String[] serverOptions,
	String[] serverProperties)
    {
	this(policy, classpath, log, serverCommand, serverOptions,
	     serverProperties, null, 
	     ServiceStarter.getActivationSystemPort());
    }

    /**
     * Trivial constructor. Simply assigns given parameters to 
     * their associated, internal fields.
     * 
     * @param policy location of VM policy filename or URL
     * @param classpath location where shared VM
     *     classes can be found. Classpath components must be separated 
     *     by path separators.
     * @param log location where group identifier information will be persisted
     * @param serverCommand VM command to use
     * @param serverOptions array of command line options to pass on the VM's
     *     command line
     * @param serverProperties array of property/value string pairs to 
     *     pass on the VMs command line (as in -D&lt;property&gt;=value). This
     *     array must have an even number of elements.
     * @param host hostname of desired activation system. If <code>null</code>,
     *     defaults to the localhost.  
     * @param port port of desired activation system. If value is <= 0, then
     *     defaults to  
     *     {@link java.rmi.activation.ActivationSystem#SYSTEM_PORT 
     *     ActivationSystem.SYSTEM_PORT}.
     */
    public SharedActivationGroupDescriptor(
	//Required Args
	String policy, String classpath, String log,
	//Optional Args
	String serverCommand, String[] serverOptions, 
	String[] serverProperties, String host, int port)
    {
	if (policy == null || classpath == null || log == null) {
            throw new NullPointerException(
		"Policy, classpath, or log cannot be null");
	}
	this.policy = policy;
	this.classpath = classpath;
	this.log = log;
	this.serverCommand = serverCommand;
	this.serverOptions = 
	    customizeSharedGroupOptions(classpath, serverOptions);
	Properties props = 
	    convertToProperties(serverProperties);
	this.serverProperties = 
	    customizeSharedGroupProperties(policy, props);
	this.host = (host == null) ? "" : host;
	if (port <= 0) {
	    this.port = ServiceStarter.getActivationSystemPort(); 
	} else {
	    this.port = port;
	}
    }
	    
    /**
     * Policy accessor method.
     *
     * @return the policy location associated with this service descriptor.
     */
    final public String getPolicy() { return policy; }
    
    /**
     * Classpath accessor method.
     *
     * @return classpath associated with this service descriptor.
     */
    final public String getClasspath() { return classpath; }
    
    /**
     * Shared group log accessor method.
     *
     * @return the shared group log associated with this service descriptor.
     */
    final public String getLog() { return log; }
    
    /**
     * Command accessor method.
     *
     * @return the path-qualified java command name associated with this 
     *     service descriptor.
     */
    final public String getServerCommand() { return serverCommand; }
    
    /**
     * Command options accessor method.
     *
     * @return the command options associated with this service descriptor.
     */
    final public String[] getServerOptions() { 
	return (String[])serverOptions.clone(); 
    }
    
    /**
     * Properties accessor method.
     *
     * @return the VM properties associated with this service descriptor.
     */
    final public Properties getServerProperties() { 
	return (Properties)serverProperties.clone(); 
    }
    
    /**
     * Activation system host accessor method.
     *
     * @return the activation system host associated with this service descriptor.
     */
    final public String getActivationSystemHost() { return host; }
    
    /**
     * Activation system port accessor method.
     *
     * @return the activation system port associated with this service descriptor.
     */
    final public int getActivationSystemPort() { return port; } 
    
    private static String[] customizeSharedGroupOptions(
        String classpath, String[] userOptions)
    {
        String[] customOpts = new String[] {"-cp", classpath};
	//Prepend classpath so user options can override later on
	if (userOptions != null) {
            String[] tmp = new String[customOpts.length + userOptions.length];
            System.arraycopy(customOpts, 0, tmp, 0, customOpts.length);
            System.arraycopy(userOptions, 0, tmp, customOpts.length,
                             userOptions.length);
            customOpts = tmp;
        }
        return customOpts;
    }
    
    private static Properties convertToProperties(String[] propertyValues) 
    {
        Properties properties = new Properties();
    
	if (propertyValues == null || propertyValues.length == 0)
	    return properties;

        if (propertyValues.length % 2 != 0) {
            throw new IllegalArgumentException(
                "The service properties entry has an odd number of elements");
        }
        for (int i = 0; i < propertyValues.length; i += 2) {
            properties.setProperty(propertyValues[i], propertyValues[i + 1]);
        }
	return properties;
    }


    private static Properties customizeSharedGroupProperties(
        String policy, Properties userProperties)
    {
        // Avoid passing null properties
        if (userProperties == null) {
            userProperties = new Properties();
        }
        userProperties.put("java.security.policy", policy);

	return userProperties;
    }
    
    /**
     * Method that attempts to create a shared activation system group from the 
     * description information provided via constructor parameters.
     * <P>
     * This method:
     * <UL>
     * <LI> creates a 
     *      {@link java.rmi.activation.ActivationGroupDesc} with
     *      the provided constructor parameter information
     * <LI> calls 
     *      {@link java.rmi.activation.ActivationSystem#registerGroup(java.rmi.activation.ActivationGroupDesc)
     *      ActivationSystem.registerGroup()} with the constructed 
     *      <code>ActivationGroupDesc</code>
     * <LI> persists the returned
     *      {@link java.rmi.activation.ActivationGroupID activation group identifier}
     *      to the shared group log.
     * <LI> calls 
     *      {@link java.rmi.activation.ActivationSystem#unregisterGroup(java.rmi.activation.ActivationGroupID) 
     *      ActivationSystem.unregisterGroup()}
     *      if an exception occurs while persisting the 
     *      <code>ActivationGroupID</code>.
     * </UL>
     * <EM>Notes:</EM>
     * <OL>
     * <LI>Prepends invoking VM's classpath to the server command options. 
     *     This allows
     *     subsequent classpath settings to override.
     * <LI>Adds a <code>"java.security.policy"</code> property with the provided
     *     policy setting to server properties.
     * </OL>
     * @return the 
     *      {@link java.rmi.activation.ActivationGroupID} for the newly 
     *      created activation system group instance.
     *
     */
    public Object create(Configuration config) throws Exception {
        ServiceStarter.ensureSecurityManager();
        logger.entering(SharedActivationGroupDescriptor.class.getName(),
	    "create", new Object[] {config});

	if (config == null) {
	   throw new NullPointerException(
	       "Configuration argument cannot be null");
	}

//TODO - expand and/or canonicalize classpath components

//TODO - check for shared log existence prior to group creation
    
	ActivationSystem sys = 
	    ServiceStarter.getActivationSystem(
	        getActivationSystemHost(),
		getActivationSystemPort(),
		config);
		
	CommandEnvironment cmdToExecute
	    = new CommandEnvironment(getServerCommand(), 
	                             getServerOptions());
	ActivationGroupID gid = null;
        try {
	    gid = sys.registerGroup(
                new ActivationGroupDesc(getServerProperties(), 
		                        cmdToExecute));
 	    storeGroupID(getLog(), gid);
	} catch (Exception e) {
            try {
                if (gid != null) sys.unregisterGroup(gid);
            } catch (Exception ee) {
                // ignore - did the best we could
            }
            if (e instanceof IOException) 
	        throw (IOException)e;
	    else if (e instanceof ActivationException)
	        throw (ActivationException)e;
	    else if (e instanceof ClassNotFoundException)
	        throw (ClassNotFoundException)e;
	    else 
	        throw new RuntimeException("Unexpected Exception", e);
        }

        logger.exiting(SharedActivationGroupDescriptor.class.getName(), 
	    "create", gid);
	return gid;
    }

    /**
     * Stores the <code>created</code> object to a well known file
     * under the provided <code>dir</code> path.
     */
    private static void storeGroupID(final String dir, 
        final ActivationGroupID obj)
        throws IOException
    {
//TODO - create log dir as a separate step
        File log = new File(dir);
        String absDir = log.getAbsolutePath();
        if (log.exists()) {
            throw new IOException("Log " + absDir + " exists."
                + " Please delete or select another path");
        }
        if (!log.mkdir()) {
            throw new IOException("Could not create directory: " + absDir);
// TODO - implement a lock out strategy
        }

        File cookieFile = new File(log, GROUP_COOKIE_FILE);
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(
                new BufferedOutputStream(
                    new FileOutputStream(cookieFile)));
            oos.writeObject(new MarshalledObject(obj));
            oos.flush();
//TODO - file sync?
	} catch (IOException e) {
            cookieFile.delete();
            throw (IOException)e.fillInStackTrace();
        } finally {
            if (oos != null) oos.close();
        }
    }
    
    /**
     * Utility method that restores the object stored in a well known file
     * under the provided <code>dir</code> path.
     */
    static ActivationGroupID restoreGroupID(final String dir)
        throws IOException, ClassNotFoundException
    {
        File log = new File(dir);
        String absDir = log.getAbsolutePath();
        if (!log.exists() || !log.isDirectory()) {
            throw new IOException("Log directory [" 
	    + absDir + "] does not exist.");
        }

        File cookieFile = new File(log, GROUP_COOKIE_FILE);
        ObjectInputStream ois = null;
        ActivationGroupID obj = null;
        try {
//TODO - lock out strategy for concurrent r/w file access
            ois = new ObjectInputStream(
                      new BufferedInputStream(
                         new FileInputStream(cookieFile)));
            MarshalledObject mo = (MarshalledObject)ois.readObject();
	    obj = (ActivationGroupID)mo.get();
        } finally {
            if (ois != null) ois.close();
        }
        return obj;
    }

    public String toString() {
        ArrayList fields = new ArrayList(8);
        fields.add(policy);
        fields.add(classpath);
        fields.add(log);
        fields.add(serverCommand);
        fields.add(Arrays.asList(serverOptions));
        fields.add(serverProperties);
        fields.add(host);
        fields.add(Integer.valueOf(port));
        return fields.toString();
    }
    
    /**
     * Reads the default serializable field values for this object.  
     * Also, verifies that the deserialized values are legal.
     */
    private void readObject(ObjectInputStream in) 
        throws IOException, ClassNotFoundException 
    {
        in.defaultReadObject();
	// Verify that serialized fields
	if (policy == null) {
	    throw new InvalidObjectException("null policy");
	}
	if (classpath == null) {
	    throw new InvalidObjectException("null class path");
	}
	if (log == null) {
	    throw new InvalidObjectException("null log");
	}
	if (serverOptions == null) {
	    throw new InvalidObjectException("null server options");
	}
	if (serverProperties == null) {
	    throw new InvalidObjectException("null server properties");
	}
	if (host == null) {
	    throw new InvalidObjectException("null activation host name");
	}
	if (port <= 0) {
	    throw new InvalidObjectException("invalid activation port: " + port);
	}    
    }
    
    /**
     * Throws InvalidObjectException, since data for this class is required.
     */
    private void readObjectNoData() throws ObjectStreamException {
	throw new InvalidObjectException("no data");
    }

}


