/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, 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/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.enterprise.deployapi;

import java.util.Collection;
import java.util.Vector;
import java.util.Iterator;
import javax.enterprise.deploy.spi.status.ProgressListener;
import javax.enterprise.deploy.spi.status.ProgressEvent;
import javax.enterprise.deploy.spi.status.ProgressObject;
import javax.enterprise.deploy.spi.status.DeploymentStatus;
import javax.enterprise.deploy.spi.TargetModuleID;
import javax.enterprise.deploy.shared.StateType;
import javax.enterprise.deploy.spi.exceptions.OperationUnsupportedException;
import com.sun.enterprise.util.LocalStringManagerImpl;
import com.sun.enterprise.deployment.client.JESProgressObject;
import com.sun.enterprise.deployment.client.DeploymentClientUtils;

/**
 * This class acts as a sink for ProgressObject. It registers itself
 * as ProgressObject listener for multiple deployment actions and 
 * tunnel all events to registered ProgressObject listener.
 *<p>
 *Whenever this class receives a progress event from one of its sources (one of the deploymentFacility
 *actions) it forwards that event on to the sink's listeners, changing the state of the event to 
 *"running."  Then, after the sink receives the completion or failure event from the last source,
 *it forwards that event as running (as it had all earlier events) and then sends one final
 *aggregate completion or failure event.
 *<p>
 *The sink always follows this pattern, even if it encapsulates only a single source.  JSR88 clients should
 *be aware of this behavior.  
 *
 * @author Jerome Dochez
 */
public class ProgressObjectSink implements JESProgressObject, ProgressListener {
    
    private static String LINE_SEPARATOR = System.getProperty("line.separator");
    private Vector registeredPL = new Vector();
    private Vector deliveredEvents = new Vector();
    
    /* aggregate state starts as successful and is changed only if at least one source operation fails */
    private StateType finalStateType = StateType.COMPLETED;
    private String finalMessage;
    
    private Vector targetModuleIDs = new Vector();
    private Vector sources = new Vector();
        
    private static LocalStringManagerImpl localStrings =
	    new LocalStringManagerImpl(ProgressObjectSink.class);
    
    private com.sun.enterprise.deployment.backend.DeploymentStatus completedStatus = new com.sun.enterprise.deployment.backend.DeploymentStatus();
    private boolean completedStatusReady = false;
    
    /**
     * register to a new ProgressObject for ProgressEvent notifications
     */
    public void sinkProgressObject(ProgressObject source) {
        /*
         *The following two statements must appear in the order shown.  Otherwise, a race condition can exist. 
         */
        sources.add(source);
        source.addProgressListener(this);
    }
    
    /** 
     * receives notification of a progress event from one of our 
     * registered interface.
     */
    public void handleProgressEvent(ProgressEvent progressEvent) {
        
        ProgressEvent forwardedEvent;
        DeploymentStatus forwardedDS = progressEvent.getDeploymentStatus();
        
        // we intercept all events...
        if (!forwardedDS.isRunning()) {
            // this mean we are either completed or failed...
            if (forwardedDS.isFailed()) {
                /*
                 *Once at least one operation fails, we know that the aggregate state will have
                 *to be failed.
                 */
                finalStateType = StateType.FAILED;
            }
            
            // since this is the completion event 
            // we are done with that progress listener;
            Object source = progressEvent.getSource();
            if (source instanceof ProgressObject) {
                ProgressObject po = (ProgressObject) source;
                po.removeProgressListener(this);
                
                sources.remove(source);
                
                if (forwardedDS.isCompleted()) {
                
                    TargetModuleID[] ids = po.getResultTargetModuleIDs();
                    for (int i=0;i<ids.length;i++) {
                        targetModuleIDs.add(ids[i]);
                    }
                }
            } else {
                throw new RuntimeException(localStrings.getLocalString(
                    "enterprise.deployment.client.noprogressobject",
                    "Progress event does not contain a ProgressObject source"
                    ));
            }
            
            /*
             *Update the completionStatus by adding a stage to it and recording the completion
             *of this event as the newest stage.
             */
            updateCompletedStatus(forwardedDS);
            
            // now we change our event state to running.  We always forward every event from a
            // source to the listeners with "running" status because the sink is not yet completely
            // finished.  We will also send a final aggregate completion event
            // if this is a completion event from our last source (see below).
            DeploymentStatusImpl forwardedStatus = new DeploymentStatusImpl();
            forwardedStatus.setState(StateType.RUNNING);
            forwardedStatus.setMessage(forwardedDS.getMessage());
            forwardedStatus.setCommand(forwardedDS.getCommand());
            forwardedEvent = new ProgressEvent(this, progressEvent.getTargetModuleID(), forwardedStatus);
        } else {
            // This is a "running" event from one of our sources, so we just need to swap the source...
            forwardedEvent = new ProgressEvent(this, progressEvent.getTargetModuleID(), 
                forwardedDS);
        }
        
        // we need to fire the received event to our listeners
        Collection clone;
        ProgressEvent finalEvent = null;
        
        synchronized(registeredPL) {
            clone = (Collection) registeredPL.clone();
            deliveredEvents.add(forwardedEvent);
            /*
             *If we are done with all of our sources, let's wrap up by creating a final event that will
             *be broadcast to the listeners along with the forwarded event.  Also create the completed status
             *that meets the requirements of the JESProgressObject interface.
             */
            if (sources.isEmpty()) {
                prepareCompletedStatus();
                DeploymentStatusImpl status = new DeploymentStatusImpl();
                status.setState(finalStateType);
                if (finalStateType.equals(StateType.FAILED)) {
                    status.setMessage(localStrings.getLocalString(
                        "enterprise.deployment.client.aggregatefailure",
                        "At least one operation failed"
                        ));
                } else {
                    status.setMessage(localStrings.getLocalString(
                        "enterprise.deployment.client.aggregatesuccess",
                        "All operations completed successfully"
                        ));
                }
                finalEvent = new ProgressEvent(this, null, status);
                deliveredEvents.add(finalEvent);
            }
        }        
        
        for (Iterator itr=clone.iterator();itr.hasNext();) {
            ProgressListener pl = (ProgressListener) itr.next();
            pl.handleProgressEvent(forwardedEvent);
        }

        /*
         *Send the final event if there is one.
         */
        if (finalEvent != null) {
            for (Iterator itr=clone.iterator();itr.hasNext();) {
                ProgressListener pl = (ProgressListener) itr.next();
                pl.handleProgressEvent(finalEvent);
            }
        }
    }    
    
    
    /**
     * Register a new ProgressListener
     * @param the new listener instance
     */
    public void addProgressListener(ProgressListener progressListener) {
        
	Collection clone;
        synchronized(registeredPL) {
            registeredPL.add(progressListener);
        
            // now let's deliver all the events we already received.
            clone = (Collection) deliveredEvents.clone();
        }
        
        for (Iterator itr=clone.iterator();itr.hasNext();) {
            ProgressEvent pe = (ProgressEvent) itr.next();
            progressListener.handleProgressEvent(pe);
        }
    }
    
    /**
     * removes a ProgressListener from our list of listeners
     * @param the ProgressListener to remove
     */
    public void removeProgressListener(ProgressListener progressListener) {
        registeredPL.remove(progressListener);
    }
    
    
    public javax.enterprise.deploy.spi.status.ClientConfiguration getClientConfiguration(TargetModuleID targetModuleID) {
        // since we are never called upon deploying, I don't 
        // have to deal with this at this time.
        return null;
    }
    
    public DeploymentStatus getDeploymentStatus() {
        DeploymentStatusImpl status = new DeploymentStatusImpl();
        if (sources.isEmpty()) {
            status.setState(finalStateType);
            status.setMessage(finalMessage);
        } else {
            status.setState(StateType.RUNNING);
        }
        return status;        
    }
    
    public TargetModuleID[] getResultTargetModuleIDs() {
        
        TargetModuleID[] ids = new TargetModuleID[targetModuleIDs.size()];
        targetModuleIDs.copyInto(ids);
        return ids;
    }
    
    public boolean isCancelSupported() {
        
        // if only one of our sources does not support cancel, we don't
        for (Iterator itr=getSources().iterator();itr.hasNext();) {
            ProgressObject source = (ProgressObject) itr.next();
            if (!source.isCancelSupported()) {
                return false;
            }
        }
        return true;
    }
    
    public boolean isStopSupported() {
        
        // if only one of our sources does not support stop, we don't
        for (Iterator itr=getSources().iterator();itr.hasNext();) {
            ProgressObject source = (ProgressObject) itr.next();
            if (!source.isStopSupported()) {
                return false;
            }
        }
        return true;
    }
    
    public void cancel() throws OperationUnsupportedException {
        if (!isCancelSupported()) {
            throw new OperationUnsupportedException("cancel");
        }
        for (Iterator itr=getSources().iterator();itr.hasNext();) {
            ProgressObject source = (ProgressObject) itr.next();
            source.cancel();
        }        
    }
    
    public void stop() throws OperationUnsupportedException {
        if (!isStopSupported()) {
            throw new OperationUnsupportedException("stop");
        }
        for (Iterator itr=getSources().iterator();itr.hasNext();) {
            ProgressObject source = (ProgressObject) itr.next();
            source.stop();
        }
        
    }
    
    private Collection getSources() {
        return (Collection) sources.clone();
    }
    
    private void prepareCompletedStatus() {
        /*
         *The substages may have status values of success when in fact a warning is present
         *in a substage.  Traverse all the substages, composing the true aggregate state and
         *message based on the most severe state that is present in the entire stage tree.
         */
        int worstStatus = com.sun.enterprise.deployment.backend.DeploymentStatus.NOTINITIALIZED;
        StringBuffer msgs = new StringBuffer();

        int newWorstStatus = aggregateStages(worstStatus, msgs, completedStatus);
        completedStatus.setStageStatus(newWorstStatus);
        completedStatus.setStageStatusMessage(msgs.toString());
        
        completedStatusReady = true;
    }
    
    private int aggregateStages(int worstStatusSoFar, StringBuffer msgs, com.sun.enterprise.deployment.backend.DeploymentStatus stage) {
        /*
         *Starting with the stage passed in, see if its severity is more urgent than that seen so far.
         *If so, then discard the messages accumulated so far for the less urgent severity and save
         *this stage's message and severity as the worst seen so far.
         */
        int stageStatus = stage.getStageStatus();
        if (stageStatus < worstStatusSoFar) {
            worstStatusSoFar = stageStatus;
            msgs.delete(0,msgs.length());
        }
        
        /*
         *If the stage's severity is the same as the currently worst seen, then add this stage's message
         *to the aggregate message.
         */
        if (stageStatus == worstStatusSoFar) {
            msgs.append(stage.getStageStatusMessage()).append(LINE_SEPARATOR);
        }
        
        /*
         *Now, do the same for each substage.
         */
        for (Iterator it = stage.getSubStages(); it.hasNext(); ) {
            com.sun.enterprise.deployment.backend.DeploymentStatus substage = (com.sun.enterprise.deployment.backend.DeploymentStatus) it.next();
            worstStatusSoFar = aggregateStages(worstStatusSoFar, msgs, substage);
        }
        
        return worstStatusSoFar;
    }
    
    /**
     *Report completed status for deploytool.
     *@return null if not completed, or the backend.DeploymentStatus set to reflect the completion
     */
    public com.sun.enterprise.deployment.backend.DeploymentStatus getCompletedStatus() {
	com.sun.enterprise.deployment.backend.DeploymentStatus answer = null;
        if (completedStatusReady) {
            answer = completedStatus;
        }
        return answer;
    }

    private void updateCompletedStatus(DeploymentStatus ds) {
        /*
         *If the status passed in is already a backend.DeploymentStatus then add it as a new stage to the 
         *completed status.  Otherwise, create a new backend.DeploymentStatus, fill it in as much as 
         *possible, and add it as the next stage.
         */
        
        com.sun.enterprise.deployment.backend.DeploymentStatus newStageStatus = null;
        if (ds instanceof DeploymentStatusImpl) {
            DeploymentStatusImpl dsi = (DeploymentStatusImpl) ds;
            newStageStatus = dsi.progressObject.getCompletedStatus();
        } else {
            /*
             *Create a new status stage and add it to the completed status.
             */
            newStageStatus = new com.sun.enterprise.deployment.backend.DeploymentStatus(completedStatus);
            /*
             *The new state status depends on the DeploymentStatus outcome.
             */
            String msgKey = null;
            int stageStatus = -1;
            Throwable exc;

            reviseStatusAndMessage(ds, newStageStatus);
        }
        if (newStageStatus != null) {
            /*
             *Update the final status state if this new stage's state is worse than the final status's
             *current state.
             */
            completedStatus.addSubStage(newStageStatus);
            /*
             *The status being reported may say it is successful but there could be warnings in substages 
             *(or substages of substages...).  So the truly final state and message for the completed 
             *status is determined once, after the last source of events is removed.
             */
        } else {
            System.err.println("A newStageStatus was null");
        }
    }
    
    private void reviseStatusAndMessage(DeploymentStatus ds, com.sun.enterprise.deployment.backend.DeploymentStatus newStageStatus) {
        String msgKey = null;
        int stageStatus = -1;
        
        if (ds.isCompleted()) {
            /*
             *The deployment status for this source was successful. 
             */
            msgKey = "enterprise.deployment.client.action_completed";
            stageStatus = com.sun.enterprise.deployment.backend.DeploymentStatus.SUCCESS;
        } else {
            /*
             *The deployment status for this source failed.
             */
            msgKey = "enterprise.deployment.client.action_failed";
            stageStatus = com.sun.enterprise.deployment.backend.DeploymentStatus.FAILURE;
        }

        String i18msg = localStrings.getLocalString(msgKey, ds.getMessage());
        newStageStatus.setStageStatus(stageStatus);
        newStageStatus.setStageStatusMessage(i18msg);
    }
}
