/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

package com.sun.enterprise.deployapi;

import com.sun.appserv.management.client.ConnectionSource;
import com.sun.enterprise.admin.util.HostAndPort;
import com.sun.enterprise.deployapi.SunDeploymentManager;
import com.sun.enterprise.deployapi.SunTarget;
import com.sun.enterprise.deployapi.SunTargetModuleID;
import com.sun.enterprise.deployment.client.DeploymentClientUtils;
import com.sun.enterprise.deployment.client.JESProgressObject;
import com.sun.enterprise.deployment.client.RollBackAction;
import com.sun.enterprise.util.i18n.StringManager;
import com.sun.enterprise.util.io.FileUtils;
import com.sun.enterprise.util.Print;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.URL;
import java.rmi.RemoteException;
import java.util.Iterator;
import java.util.Vector;

import javax.enterprise.deploy.shared.CommandType;
import javax.enterprise.deploy.shared.ModuleType;
import javax.enterprise.deploy.shared.StateType;
import javax.enterprise.deploy.spi.exceptions.OperationUnsupportedException;
import javax.enterprise.deploy.spi.status.ClientConfiguration;
import javax.enterprise.deploy.spi.status.DeploymentStatus;
import javax.enterprise.deploy.spi.status.ProgressEvent;
import javax.enterprise.deploy.spi.status.ProgressListener;
import javax.enterprise.deploy.spi.TargetModuleID;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;

/**
 * Implementation of the Progress Object 
 * @author  dochez
 */
public abstract class ProgressObjectImpl implements Runnable, JESProgressObject {
    
    protected CommandType commandType;
    protected Object[] args;
    private Vector listeners = new Vector(); // <-- needs to be synchronized
    protected SunTarget target;
    protected SunTarget[] targetsList;
    protected String moduleID;
    protected ModuleType moduleType;
    protected DeploymentStatusImpl deploymentStatus =null;
    protected TargetModuleID[] targetModuleIDs = null;
    protected Vector deliveredEvents = new Vector();
    protected com.sun.enterprise.deployment.backend.DeploymentStatus finalDeploymentStatus = null;
    protected boolean deployActionCompleted;
    protected String warningMessages;
    private static StringManager localStrings = StringManager.getManager(ProgressObjectImpl.class);
    
    private final static String MODULE_ID = 
        com.sun.enterprise.deployment.backend.DeploymentStatus.MODULE_ID;
    private final static String MODULE_TYPE = 
        com.sun.enterprise.deployment.backend.DeploymentStatus.MODULE_TYPE;
    private final static String KEY_SEPARATOR = 
        com.sun.enterprise.deployment.backend.DeploymentStatus.KEY_SEPARATOR;
    private final static String SUBMODULE_COUNT = 
        com.sun.enterprise.deployment.backend.DeploymentStatus.SUBMODULE_COUNT;
    private final static String CONTEXT_ROOT = 
        com.sun.enterprise.deployment.backend.DeploymentStatus.CONTEXT_ROOT;
    
    /** Creates a new instance of ProgressObjectImpl */
    public ProgressObjectImpl(SunTarget target) {
        this.target = target;
        deploymentStatus = new DeploymentStatusImpl(this);
        deploymentStatus.setState(StateType.RELEASED);
        finalDeploymentStatus = new com.sun.enterprise.deployment.backend.DeploymentStatus();
        deployActionCompleted = false;
    }

    public ProgressObjectImpl(SunTarget[] targets) {
        this.targetsList = targets;
        deploymentStatus = new DeploymentStatusImpl(this);
        deploymentStatus.setState(StateType.RELEASED);
        finalDeploymentStatus = new com.sun.enterprise.deployment.backend.DeploymentStatus();
        deployActionCompleted = false;
    }
    
    /** Add a listener to receive Progress events on deployment
     * actions.
     *
     * @param The listener to receive events
     * @see ProgressEvent
     */
    public void addProgressListener(ProgressListener pol) {
	synchronized (listeners) {
            listeners.add(pol);
	    if (deliveredEvents.size() > 0) {
		Print.dprintln("Delivering undelivered messages...");
	        for (Iterator i = deliveredEvents.iterator(); i.hasNext();) {
		    pol.handleProgressEvent((ProgressEvent)i.next());
	        }
	    }
	}
    }
    
    /** (optional)
     * A cancel request on an in-process operation
     * stops all further processing of the operation and returns
     * the environment to it original state before the operation
     * was executed.  An operation that has run to completion
     * cannot be cancelled.
     *
     * @throws OperationUnsupportedException this optional command
     *         is not supported by this implementation.
     */
    public void cancel() throws OperationUnsupportedException {
        throw new OperationUnsupportedException("cancel not supported");
    }
    
    /** Return the ClientConfiguration object associated with the
     * TargetModuleID.
     *
     * @return ClientConfiguration for a given TargetModuleID or
     *         null if none exists.
     */
    public ClientConfiguration getClientConfiguration(TargetModuleID id) {
        return null;
    }
    
    /** Retrieve the status of this activity.
     *
     * @return An object containing the status
     *          information.
     */
    public DeploymentStatus getDeploymentStatus() {
        DeploymentStatusImpl result = new DeploymentStatusImpl(this);
        result.setState(deploymentStatus.getState());
        result.setMessage(deploymentStatus.getMessage());
        
        return result;
    }

    /**
     * Retrieve the final deployment status which has complete details for each stage
     */
    public com.sun.enterprise.deployment.backend.DeploymentStatus getCompletedStatus() {
        if(deployActionCompleted) {
            return finalDeploymentStatus;
        }
        return null;
    }
    
    /** Retrieve the list of TargetModuleIDs successfully
     * processed or created by the associated DeploymentManager
     * operation.
     *
     * @return a list of TargetModuleIDs.
     */
    public TargetModuleID[] getResultTargetModuleIDs() {

        /**
         * this should go once CTS has fixed their bugs...
         */
        if (targetModuleIDs==null) {
            if(target != null) {
                initializeTargetModuleIDs(moduleID);
            } else if(targetsList != null) {
                initializeTargetModuleIDForAllServers(null, null);
            }
        }
        // will return null until the operation is completed
        return targetModuleIDs;
    }
    
    /**
     * initialize the target module IDs with the passed application moduleID
     * and the descriptors
     */
    protected void initializeTargetModuleIDs(String moduleID) {
        SunTargetModuleID parentTargetModuleID = new SunTargetModuleID(moduleID, target);        
        parentTargetModuleID.setModuleType(getModuleType());
        
        targetModuleIDs = new SunTargetModuleID[1];        
        targetModuleIDs[0] = parentTargetModuleID;
    }

    /**
     * Initialize the target module IDs with the application information stored
     * in the DeploymentStatus for all the server in the target list.
     */
    protected void initializeTargetModuleIDForAllServers(
        com.sun.enterprise.deployment.backend.DeploymentStatus status, 
        MBeanServerConnection mbsc) {

        if(targetsList == null) {
            return;
        }

        targetModuleIDs = new SunTargetModuleID[targetsList.length];
        String moduleID = status == null 
                        ? this.moduleID : status.getProperty(MODULE_ID);
        String key = moduleID + KEY_SEPARATOR + MODULE_TYPE;
        ModuleType type = status == null
                        ? getModuleType()
                        : ModuleType.getModuleType((new Integer(status.getProperty(key))).intValue());

        for(int i=0; i<targetsList.length; i++) {
            SunTargetModuleID parentTargetModuleID = new SunTargetModuleID(moduleID, targetsList[i]);
            parentTargetModuleID.setModuleType(type);
            targetModuleIDs[i] = parentTargetModuleID;

            if (status != null) {
                // let's get the host name and port where the application was deployed 
                HostAndPort webHost=null;
                try {
                    Object[] params = new Object[]{ moduleID, Boolean.FALSE };
                    String[] signature = new String[]{ "java.lang.String", "boolean"};
                    ObjectName applicationsMBean = new ObjectName(APPS_CONFIGMBEAN_OBJNAME);                
                    webHost = (HostAndPort) mbsc.invoke(applicationsMBean, "getHostAndPort", params, signature);                        
                } catch(Exception e) {
                    Print.dprintStackTrace(e.getLocalizedMessage(), e);
                }

                key = moduleID + KEY_SEPARATOR + SUBMODULE_COUNT;
                if (status.getProperty(key) == null) { //standalone module
                    if (ModuleType.WAR.equals(type)) {
                        key = moduleID + KEY_SEPARATOR + CONTEXT_ROOT;
                        String contextRoot = status.getProperty(key);
                        initTargetModuleIDWebURL(parentTargetModuleID, webHost, contextRoot);
                     }
                } else {
                    int counter = (Integer.valueOf(status.getProperty(key))).intValue();
                    // now for each sub module            
                    for (int j = 0; j < counter; j++) {
                        //subModuleID
                        key = moduleID + KEY_SEPARATOR + MODULE_ID + KEY_SEPARATOR + String.valueOf(j);
                        String subModuleID = status.getProperty(key);
                        SunTargetModuleID subModule = new SunTargetModuleID(subModuleID, targetsList[i]);

                        //subModuleType 
                        key = subModuleID + KEY_SEPARATOR + MODULE_TYPE;
                        type = ModuleType.getModuleType((new Integer(status.getProperty(key))).intValue());
                        subModule.setModuleType(type);
                        if (ModuleType.WAR.equals(type) && webHost!=null) {
                            key = subModuleID + KEY_SEPARATOR + CONTEXT_ROOT;
                            String contextRoot = status.getProperty(key);
                            initTargetModuleIDWebURL(subModule, webHost, contextRoot);
                        }
                        parentTargetModuleID.addChildTargetModuleID(subModule);
                    }
                } 
            }
        }
    }

    /**
     * private method to initialize the web url for the associated deployed web
     * module
     */
    private void initTargetModuleIDWebURL(
        SunTargetModuleID tm, HostAndPort webHost, String contextRoot) {
        
        if (webHost==null)
            return;
        
        try {
            // Patchup code for fixing netbeans issue 6221411; Need to find a 
            // good solution for this and WSDL publishing
            String host;
            SunDeploymentManager sdm = new SunDeploymentManager(tm.getConnectionInfo());
            if(sdm.isPE()) {
                host = tm.getConnectionInfo().getHostName();
            } else {
                host = webHost.getHost();
            }
            
            URL webURL = new URL("http", host, webHost.getPort(), contextRoot);
            tm.setWebURL(webURL.toExternalForm());
        } catch(Exception e) {
            Print.dprintStackTrace(e.getLocalizedMessage(),e);
        }
    }
    
    /** Tests whether the vendor supports a cancel
     * opertation for deployment activities.
     *
     * @return <code>true</code> if canceling an
     *         activity is supported by this platform.
     */
    public boolean isCancelSupported() {
        return false;
    }
    
    /** Tests whether the vendor supports a stop
     * opertation for deployment activities.
     *
     * @return <code>true</code> if canceling an
     *         activity is supported by this platform.
     */
    public boolean isStopSupported() {
        return false;
    }
    
    /** Remove a ProgressObject listener.
     *
     * @param The listener being removed
     * @see ProgressEvent
     */
    public void removeProgressListener(ProgressListener pol) {
	synchronized (listeners) {
            listeners.remove(pol);
	}
    }
    
    /** (optional)
     * A stop request on an in-process operation allows the
     * operation on the current TargetModuleID to run to completion but
     * does not process any of the remaining unprocessed TargetModuleID
     * objects.  The processed TargetModuleIDs must be returned by the
     * method getResultTargetModuleIDs.
     *
     * @throws OperationUnsupportedException this optional command
     *         is not supported by this implementation.
     */
    public void stop() throws OperationUnsupportedException {
        throw new OperationUnsupportedException("stop not supported");
    }

    
    public void setCommand(CommandType commandType, Object[] args) {
        this.commandType = commandType;
        this.args = args;
    }
    

    /**
     * implement this method to do the actual processing
     */
    public abstract void run();


    /**
     * Notifies all listeners that have registered interest for ProgressEvent notification. 
     */
    protected void fireProgressEvent(ProgressEvent progressEvent) {
        /*
         *Bug 4977764
         *Iteration failed due to concurrent modification of the vector.  Even though the add, remove, and fire 
         *methods synchronize on the listeners vector, a listener could conceivably invoke add or remove 
         *recursively, thereby triggering the concurrent modification exception.
         *
         *Fix: clone the listeners vector and iterate through the clone.  
         */
	Vector currentListeners = null;
        synchronized (listeners) {
            currentListeners = (Vector) listeners.clone();
            /*
             *The following add must remain inside the synchronized block.  Otherwise, there will be a small window
             *in which a new listener's registration could interleave with fireProgressEvent, registering itself 
             *after the listeners vector had been cloned (thus excluding the new listener from the iteration a
             *few lines below) but before the list of previously-delivered events had been updated.  
             *This would cause the new listener to miss the event that was firing.  
             *Keeping the following add inside the synchronized block ensures that updates to the listeners 
             *vector by addProgressListener and to deliveredEvents by fireProgressEvent do not interleave and therefore
             *all listeners will receive all events.
             */
            
            deliveredEvents.add(progressEvent);
        }

        for (Iterator listenersItr = currentListeners.iterator(); listenersItr.hasNext();) {
            ((ProgressListener)listenersItr.next()).handleProgressEvent(progressEvent);
        }
        currentListeners = null;
    }


    /**
     * Notifies all listeners that have registered interest for ProgressEvent notification. 
     */
    protected void fireProgressEvent(StateType state, String message) {
        fireProgressEvent(state, message, target);
    }

    /**
     * Notifies all listeners that have registered interest for ProgressEvent notification. 
     */
    protected void fireProgressEvent(StateType state, String message, SunTarget aTarget) {
        
        StateType stateToBroadcast = (state != null) ? state : deploymentStatus.getState();

        /* new copy of DeploymentStatus */
	DeploymentStatusImpl depStatus = new DeploymentStatusImpl(this);
	depStatus.setState(stateToBroadcast);
        depStatus.setMessage(message);

	/* send notification */
	SunTargetModuleID tmi = new SunTargetModuleID(moduleID, aTarget);
        tmi.setModuleType(getModuleType());
	fireProgressEvent(new ProgressEvent(this, tmi, depStatus));
	
        /*
         *Update this progress object's status after notifying listeners.
         */
        if (state != null) {
            deploymentStatus.setMessage(message);
            deploymentStatus.setState(state); // retain current state
	}
    }

    CommandType getCommandType() {
        return commandType;
    }
    
    /**
     * Sets the module type for this deployed module
     * @param the module type
     */
    public void setModuleType(ModuleType moduleType) {
        this.moduleType = moduleType;
    }
    
    /**
     * @return the module type of this deployed module
     */ 
    public ModuleType getModuleType() {
        return moduleType;
    }    

    /**
     * Since DeploymentStatus only provides the ability to pass a String to the
     * ProgressListener, the following is a convenience method for allowing the
     * stack-trace from a Throwable to be converted to a String to send to the
     * ProgressListeners.
     */
    protected String getThrowableString(Throwable t) {
	ByteArrayOutputStream bos = new ByteArrayOutputStream();
	PrintStream ps = new PrintStream(bos);
	t.printStackTrace(ps);
	ps.close(); // may not be necessary
	return bos.toString();
    }
    
    protected static final String APPS_CONFIGMBEAN_OBJNAME =
                                  "com.sun.appserv:type=applications,category=config";    

    protected String getDeploymentStatusMessage(com.sun.enterprise.deployment.backend.DeploymentStatus status) {
        return getDeploymentStatusMessage(status, false);
    }
    /**
     * Parse the DeploymentStatus to get the status message within
     */
    protected String getDeploymentStatusMessage(com.sun.enterprise.deployment.backend.DeploymentStatus status, boolean isStartPhase) {
        if(status == null) {
            return null;
        }
        // if stage status is success, return as it is
        if (status!=null && status.getStatus() >= com.sun.enterprise.deployment.backend.DeploymentStatus.SUCCESS) {
            return null;
        }
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        PrintWriter pw = new PrintWriter(bos);
        com.sun.enterprise.deployment.backend.DeploymentStatus.parseDeploymentStatus(status, pw);
        byte[] statusBytes = bos.toByteArray();
        String statusString = new String(statusBytes);
        // if stage status is WARNING, collect the warning messages
        if(status.getStatus() == com.sun.enterprise.deployment.backend.DeploymentStatus.WARNING) {
            if(warningMessages==null) {
                warningMessages = statusString;
            } else {
                warningMessages += statusString;
            }

            // add additional messages if it failed at loading.
            if (isStartPhase) { 
                warningMessages = localStrings.getString("enterprise.deployment.client.start_failed_msg") + warningMessages;
            }

            return null;
        }
        // Failed stage; return the failure message
        return statusString;
    }

    protected void setupForNormalExit(String message, SunTarget aTarget) {
        String i18nmsg;
        // If we ever got some warning during any of the stages, the the final status is warning; else status=success
        if(warningMessages == null) {
            i18nmsg = localStrings.getString("enterprise.deployment.client.action_completed", message);
            finalDeploymentStatus.setStageStatus(com.sun.enterprise.deployment.backend.DeploymentStatus.SUCCESS);
        } else {
            i18nmsg = localStrings.getString("enterprise.deployment.client.action_completed_with_warning", warningMessages);
            finalDeploymentStatus.setStageStatus(com.sun.enterprise.deployment.backend.DeploymentStatus.WARNING);            
        }
        finalDeploymentStatus.setStageStatusMessage(i18nmsg);
        deployActionCompleted = true;
        fireProgressEvent(StateType.COMPLETED, i18nmsg, aTarget);
        return;
    }
    
    protected void setupForAbnormalExit(String errorMsg, SunTarget aTarget) {
        String i18nmsg = localStrings.getString("enterprise.deployment.client.action_failed", errorMsg);
        finalDeploymentStatus.setStageStatus(com.sun.enterprise.deployment.backend.DeploymentStatus.FAILURE);
        finalDeploymentStatus.setStageStatusMessage(i18nmsg);
        deployActionCompleted = true;
        fireProgressEvent(StateType.FAILED, i18nmsg, aTarget);
        return;
    }

    protected boolean checkStatusAndAddStage(SunTarget aTarget, RollBackAction rollback, String action, ConnectionSource dasConnection, com.sun.enterprise.deployment.backend.DeploymentStatus currentStatus) {
        return checkStatusAndAddStage(aTarget, rollback, action, dasConnection, 
            currentStatus, false);
    }

    /**
     * Given a Deployment status, this checks if the status is success
     * If the status is failed, it tries roll back operations (if rollback is specified) and then sets up
     * for an abnormal exit
     */
    protected boolean checkStatusAndAddStage(SunTarget aTarget, RollBackAction rollback, String action, ConnectionSource dasConnection, com.sun.enterprise.deployment.backend.DeploymentStatus currentStatus, boolean isStartPhase) {
        String statusMsg = getDeploymentStatusMessage(currentStatus, 
            isStartPhase);
        finalDeploymentStatus.addSubStage(currentStatus);
        if(statusMsg == null) {
            fireProgressEvent(StateType.RUNNING,
                                localStrings.getString("enterprise.deployment.client.action_completed", action),
                                aTarget);
            return true;
        }
        if(rollback != null) {
            com.sun.enterprise.deployment.backend.DeploymentStatus tmp = new
                        com.sun.enterprise.deployment.backend.DeploymentStatus();
            if(!rollback.rollback(dasConnection, tmp)) {
                fireProgressEvent(StateType.RUNNING, 
                                localStrings.getString("enterprise.deployment.client.action_failed", "Rollback failed"),
                                        aTarget);
                tmp.setStageStatus(com.sun.enterprise.deployment.backend.DeploymentStatus.FAILURE);
                tmp.setStageStatusMessage(localStrings.getString("enterprise.deployment.client.action_failed", "Rollback failed"));
            } else {
                fireProgressEvent(StateType.RUNNING, 
                                localStrings.getString("enterprise.deployment.client.action_completed", "Rollback"),
                                        aTarget);
                tmp.setStageStatus(com.sun.enterprise.deployment.backend.DeploymentStatus.SUCCESS);
                tmp.setStageStatusMessage(localStrings.getString("enterprise.deployment.client.action_completed", "Rollback"));
            }
            finalDeploymentStatus.addSubStage(tmp);
        }
        setupForAbnormalExit(localStrings.getString("enterprise.deployment.client.action_failed_with_message", action, statusMsg),  aTarget);
        return false;
    }
}
