/*
 * 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 org.netbeans.modules.payara.common;

import org.netbeans.modules.payara.tooling.admin.CommandRedeploy;
import org.netbeans.modules.payara.tooling.admin.CommandRestartDAS;
import org.netbeans.modules.payara.tooling.admin.CommandUndeploy;
import org.netbeans.modules.payara.tooling.admin.CommandDeploy;
import org.netbeans.modules.payara.tooling.admin.CommandDisable;
import org.netbeans.modules.payara.tooling.admin.CommandSetProperty;
import org.netbeans.modules.payara.tooling.admin.ResultMap;
import org.netbeans.modules.payara.tooling.admin.CommandGetProperty;
import org.netbeans.modules.payara.tooling.admin.Command;
import org.netbeans.modules.payara.tooling.admin.CommandEnable;
import org.netbeans.modules.payara.tooling.admin.ServerAdmin;
import org.netbeans.modules.payara.tooling.admin.ResultString;
import org.netbeans.modules.payara.tooling.admin.CommandListComponents;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.ChangeListener;
import org.netbeans.modules.payara.tooling.PayaraIdeException;
import org.netbeans.modules.payara.tooling.PayaraStatus;
import static org.netbeans.modules.payara.tooling.PayaraStatus.OFFLINE;
import static org.netbeans.modules.payara.tooling.PayaraStatus.SHUTDOWN;
import static org.netbeans.modules.payara.tooling.PayaraStatus.STARTUP;
import org.netbeans.modules.payara.tooling.TaskEvent;
import org.netbeans.modules.payara.tooling.TaskState;
import org.netbeans.modules.payara.tooling.TaskStateListener;
import org.netbeans.modules.payara.tooling.utils.Utils;
import org.netbeans.modules.payara.common.nodes.actions.RefreshModulesCookie;
import org.netbeans.modules.payara.common.utils.Util;
import org.netbeans.modules.payara.spi.AppDesc;
import org.netbeans.modules.payara.spi.CommandFactory;
import static org.netbeans.modules.payara.spi.PayaraModule.HOSTNAME_ATTR;
import static org.netbeans.modules.payara.spi.PayaraModule.PROPERTIES_FETCH_TIMEOUT;
import org.netbeans.modules.payara.spi.PayaraModule.ServerState;
import org.netbeans.modules.payara.spi.Recognizer;
import org.netbeans.modules.payara.spi.RecognizerCookie;
import org.netbeans.modules.payara.spi.ResourceDesc;
import org.netbeans.modules.payara.spi.VMIntrospector;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.ChangeSupport;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.lookup.Lookups;
import org.netbeans.modules.payara.spi.PayaraModule;
import org.netbeans.modules.payara.spi.PayaraModule3;
import org.netbeans.modules.payara.tooling.data.PayaraServerStatus;
import org.netbeans.modules.payara.tooling.data.PayaraPlatformVersionAPI;

/**
 * Payara server support API.
 * <p/>
 * @author Peter Williams, Tomas Kraus
 */
public class CommonServerSupport
        implements PayaraModule3, RefreshModulesCookie {

    ////////////////////////////////////////////////////////////////////////////
    // Inner classes                                                         //
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Task state listener watching __locations command execution.
     */
    private static class LocationsTaskStateListener
            implements TaskStateListener {

        /** Payara server support object instance. */
        final CommonServerSupport css;

        /**
         * Creates an instance of task state listener watching __locations
         * command execution.
         * <p/>
         * @param css Payara server support object instance.
         */
        LocationsTaskStateListener(CommonServerSupport css) {
            this.css = css;
        }

        private String adminCommandFailedMsg(String resName, String[] args) {
            String serverName = args[0];
            String command = args[1];
            String exMessage = args.length > 2 ? args[2] : null;
            return args.length > 2
                    ? NbBundle.getMessage(CommonServerSupport.class, resName,
                    args[0], args[1], args[2])
                    : NbBundle.getMessage(CommonServerSupport.class, resName,
                    args[0], args[1]);
        }

        /**
         * Callback to notify about Payara __locations command execution
         * state change.
         * <p/>
         * <code>String</codce> arguments passed to state listener
         * from runner:<ul>
         *   <li><code>args[0]</code> server name</li>
         *   <li><code>args[1]</code> administration command</li>
         *   <li><code>args[2]</code> exception message</li>
         *   <li><code>args[3]</code> display message in GUI</li></ul>
         * <p/>
         * @param newState New command execution state.
         * @param event    Event related to execution state change.
         * @param args     <code>String</codce> arguments passed to state
         *                 listener.
         */
        @Override
        public void operationStateChanged(
                TaskState newState, TaskEvent event,
                String[] args) {
            // Server name and command are mandatory.
            if (args.length > 1) {
                String exMessage = args.length > 2 ? args[2] : null;
                boolean display = args.length > 3
                        ? Boolean.parseBoolean(args[3]) : false;
                if (display) {
                    long lastDisplayed = css.getLatestWarningDisplayTime();
                    long currentTime = System.currentTimeMillis();
                    if (TaskState.FAILED == newState
                            && currentTime - lastDisplayed > 5000) {
                        String message;

                        switch (event) {
                            case EXCEPTION:
                                if (exMessage != null
                                        && exMessage.length() > 0) {
                                    message = adminCommandFailedMsg(
                                            "MSG_ADMIN_EXCEPTION", args);
                                } else {
                                    message = adminCommandFailedMsg(
                                            "MSG_ADMIN_FAILED", args);
                                }
                                break;
                            case AUTH_FAILED: case AUTH_FAILED_HTTP:
                                message = adminCommandFailedMsg(
                                        "MSG_ADMIN_AUTH_FAILED", args);
                                break;
                            default:
                                message = adminCommandFailedMsg(
                                        "MSG_ADMIN_FAILED", args);
                        }
                        displayPopUpMessage(css, message);
                    }
                }
            }
        }
    }

    class StartOperationStateListener implements TaskStateListener {
        private ServerState endState;

        StartOperationStateListener(ServerState endState) {
            this.endState = endState;
        }

        @Override
        public void operationStateChanged(TaskState newState, TaskEvent event,
                String... args) {
            if(newState == TaskState.RUNNING) {
                setServerState(ServerState.STARTING);
            } else if(newState == TaskState.COMPLETED) {
                startedByIde = isRemote
                        ? false : PayaraState.isOnline(instance);
                setServerState(endState);
            } else if(newState == TaskState.FAILED) {
                setServerState(ServerState.STOPPED);
                // Open a warning dialog here...
                NotifyDescriptor nd = new NotifyDescriptor.Message(Utils.concatenate(args));
                DialogDisplayer.getDefault().notifyLater(nd);
            }
        }
    }
    class StopOperationStateListener implements TaskStateListener {

        @Override
        public void operationStateChanged(
                TaskState newState, TaskEvent event, String... args) {
            if (newState == TaskState.RUNNING) {
                setServerState(ServerState.STOPPING);
            } else if (newState == TaskState.COMPLETED) {
                setServerState(ServerState.STOPPED);
            } else if (newState == TaskState.FAILED) {
                // Possible bug: What if server was started in other mode
                // than RUNNING.
                setServerState(ServerState.RUNNING);
            }
        }
    };

    class KillOperationStateListener implements TaskStateListener {

        @Override
        public void operationStateChanged(final TaskState newState,
                final TaskEvent event, final String... args) {
            if (newState == TaskState.RUNNING) {
                setServerState(ServerState.STOPPING);
            } else if (newState == TaskState.COMPLETED) {
                setServerState(ServerState.STOPPED);
            } else if (newState == TaskState.FAILED) {
                // Registered process has already finished.
                if (event == TaskEvent.PROCESS_NOT_RUNNING) {
                    setServerState(ServerState.STOPPED);
                }
                // Otherwise do nothing for TaskEvent.PROCESS_NOT_EXISTS.
            }
        }
    };

    ////////////////////////////////////////////////////////////////////////////
    // Class attributes                                                       //
    ////////////////////////////////////////////////////////////////////////////

    /** Local logger. */
    private static final Logger LOGGER
            = PayaraLogger.get(CommonServerSupport.class);

    /** Local host name (DNS). */
    private static final String LOCALHOST = "localhost";

    /** String to return for failed {@see getHttpHostFromServer()} search. */
    private static final String FAILED_HTTP_HOST = LOCALHOST + "FAIL";

    ////////////////////////////////////////////////////////////////////////////
    // Static methods                                                         //
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Display pop up window with given message.
     * <p/>
     * Method is thread safe.
     * <p/>
     * @param css     Payara server support object.
     * @param message Message to be displayed.
     */
    public static void displayPopUpMessage(final CommonServerSupport css,
            final String message) {
        synchronized (css) {
            NotifyDescriptor nd = new NotifyDescriptor.Message(message);
            DialogDisplayer.getDefault().notifyLater(nd);
            css.setLatestWarningDisplayTime(System.currentTimeMillis());
            LOGGER.log(Level.INFO, message);
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    // Instance attributes                                                    //
    ////////////////////////////////////////////////////////////////////////////

    /** Managed Payara instance. */
    private final PayaraInstance instance;

    private volatile ServerState serverState = ServerState.UNKNOWN;
    private final Object stateMonitor = new Object();

    private ChangeSupport changeSupport = new ChangeSupport(this);

    private FileObject instanceFO;

    private volatile boolean startedByIde = false;

    /** Cache local/remote test for instance. */
    private transient boolean isRemote = false;

    // prevent j2eeserver from stopping an authenticated domain that
    // the IDE did not start.
    private boolean stopDisabled = false;

    private Process localStartProcess;

    /** Last executed start task. */
    private volatile FutureTask<TaskState> startTask;

    ////////////////////////////////////////////////////////////////////////////
    // Constructors                                                           //
    ////////////////////////////////////////////////////////////////////////////

    CommonServerSupport(PayaraInstance instance) {
        this.instance = instance;
        this.isRemote = instance.isRemote();
        // !PW FIXME temporary patch for JavaONE 2008 to make it easier
        // to persist per-instance property changes made by the user.
        instanceFO = getInstanceFileObject();
        startTask = null;
    }

    /**
     * Get <code>PayaraInstance</code> object associated with this object.
     * <p/>
     * @return <code>PayaraInstance</code> object associated with this object.
     */
    @Override
    public PayaraInstance getInstance() {
        return this.instance;
    }

    private FileObject getInstanceFileObject() {
        FileObject dir = FileUtil.getConfigFile(
                instance.getInstanceProvider().getInstancesDirFirstName());
        if(dir != null) {
            String instanceFN = instance
                    .getProperty(PayaraInstance.INSTANCE_FO_ATTR);
            if(instanceFN != null) {
                return dir.getFileObject(instanceFN);
            }
        }
        return null;
    }

    @Override
    public String getPassword() {
        return instance.getPassword();
    }

    /** @deprecated Use in <code>PayaraInstance</code> context. */
    @Deprecated
    public String getInstallRoot() {
        return instance.getInstallRoot();
    }

    /** @deprecated Use in <code>PayaraInstance</code> context. */
    @Deprecated
    public String getPayaraRoot() {
        return instance.getPayaraRoot();
    }

    /** @deprecated Use in <code>PayaraInstance</code> context. */
    @Deprecated
    public String getDisplayName() {
        return instance.getDisplayName();
    }

    /** @deprecated Use in <code>PayaraInstance</code> context. */
    @Deprecated
    public String getDeployerUri() {
        return instance.getDeployerUri();
    }

    /** @deprecated Use in <code>PayaraInstance</code> context. */
    @Deprecated
    public String getUserName() {
        return instance.getUserName();
    }

    /** @deprecated Use in <code>PayaraInstance</code> context. */
    @Deprecated
    public String getAdminPort() {
        return instance.getHttpAdminPort();
    }

    /** @deprecated Use in <code>PayaraInstance</code> context. */
    @Deprecated
    public String getHttpPort() {
        return instance.getHttpPort();
    }

    /** @deprecated Use in <code>PayaraInstance</code> context. */
    @Deprecated
    public int getHttpPortNumber() {
        return instance.getPort();
    }

    /** @deprecated Use in <code>PayaraInstance</code> context. */
    @Deprecated
    public int getAdminPortNumber() {
        return instance.getAdminPort();
    }

    /** @deprecated Use in <code>PayaraInstance</code> context. */
    @Deprecated
    public String getHostName() {
        return instance.getProperty(HOSTNAME_ATTR);
    }

   /** @deprecated Use in <code>PayaraInstance</code> context. */
   @Deprecated
   public String getDomainsRoot() {
        return instance.getDomainsRoot();
    }

    /** @deprecated Use in <code>PayaraInstance</code> context. */
    @Deprecated
    public String getDomainName() {
        return instance.getDomainName();
    }

    public void setServerState(final ServerState newState) {
        // Synchronized on private monitor to serialize changes in state.
        // Storage of serverState is volatile to facilitate readability of
        // current state regardless of lock status.
        boolean fireChange = false;

        synchronized (stateMonitor) {
            if(serverState != newState) {
                serverState = newState;
                fireChange = true;
            }
        }

        if(fireChange) {
            changeSupport.fireChange();
        }
    }

    boolean isStartedByIde() {
        return startedByIde;
    }

    // ------------------------------------------------------------------------
    // PayaraModule interface implementation
    // ------------------------------------------------------------------------
    @Override
    public Map<String, String> getInstanceProperties() {
        // force the domains conversion
        getDomainsRoot();
        return Collections.unmodifiableMap(instance.getProperties());
    }

    @Override
    public PayaraInstanceProvider getInstanceProvider() {
        return instance.getInstanceProvider();
    }

    @Override
    public boolean isRemote() {
        return isRemote;
    }

    private static final RequestProcessor RP = new RequestProcessor("CommonServerSupport - start/stop/refresh",5); // NOI18N

    @Override
    public Future<TaskState> startServer(
            final TaskStateListener stateListener, ServerState endState) {
        LOGGER.log(Level.FINEST,
                "CSS.startServer called on thread \"{0}\"",
                Thread.currentThread().getName());
        TaskStateListener startServerListener = new StartOperationStateListener(endState);
        VMIntrospector vmi = Lookups.forPath(Util.PF_LOOKUP_PATH).lookup(VMIntrospector.class);
        FutureTask<TaskState> task = new FutureTask<TaskState>(
                new StartTask(this, getRecognizers(), vmi,
                endState == ServerState.STOPPED_JVM_PROFILER ? new String[]{""} : null,
                startServerListener, stateListener));
        startTask = task;
        RP.post(task);
        return task;
    }

    private List<Recognizer> getRecognizers() {
        List<Recognizer> recognizers;
        Collection<? extends RecognizerCookie> cookies = 
                instance.localLookup().lookupAll(RecognizerCookie.class);
        if(!cookies.isEmpty()) {
            recognizers = new LinkedList<Recognizer>();
            for(RecognizerCookie cookie: cookies) {
                recognizers.addAll(cookie.getRecognizers());
            }
            recognizers = Collections.unmodifiableList(recognizers);
        } else {
            recognizers = Collections.emptyList();
        }
        return recognizers;
    }


    @Override
    public Future<TaskState> stopServer(final TaskStateListener stateListener) {
        LOGGER.log(Level.FINEST, "CSS.stopServer called on thread \"{0}\"", Thread.currentThread().getName()); // NOI18N
        TaskStateListener stopServerListener = new StopOperationStateListener();
        FutureTask<TaskState> task;
        if (!isRemote() || !Util.isDefaultOrServerTarget(instance.getProperties())) {
            if (getServerState() == ServerState.STOPPED_JVM_PROFILER) {
                task = new FutureTask<TaskState>(
                        new StopProfilingTask(this, stateListener));
            } else {
                task = new FutureTask<TaskState>(
                        new StopTask(this, stopServerListener, stateListener));
            }
        // prevent j2eeserver from stopping a server it did not start.
        } else {
            task = new FutureTask<TaskState>(
                    new NoopTask(this,stopServerListener,stateListener));
        }
        if (stopDisabled) {
            stopServerListener.operationStateChanged(
                    TaskState.COMPLETED, TaskEvent.CMD_COMPLETED, "");
            if (null != stateListener) {
                stateListener.operationStateChanged(
                        TaskState.COMPLETED, TaskEvent.CMD_COMPLETED, "");
            }
            return task;
        }
        RP.post(task);
        return task;
    }

    /** Delay between checking task state while waiting it
     *  to die [ms]. */
    private static final int WAIT_TASK_TO_DIE_SLEEP = 300;

    /** Maximum time to wait for task to die [ms]. */
    private static final int WAIT_TASK_TO_DIE_MAX = 3000;

     /**
     * Terminates local Payara server process when started from UI.
     * <p/>
     * @param stateListener External state listener to register.
     * @return Asynchronous Payara server termination task when 
     */
    @Override
    @SuppressWarnings("SleepWhileInLoop")
    public Future<TaskState> killServer(final TaskStateListener stateListener) {
        TaskStateListener killServerListener = new KillOperationStateListener();
        FutureTask<TaskState> task;
        boolean isStateListener = stateListener != null;
        if (!isRemote() && instance.getProcess() != null) {
            FutureTask<TaskState> stTask = this.startTask;
            // Stop running StartTask and wait for it to die.
            if (stTask != null && !stTask.isDone()) {
                stTask.cancel(true);
                long startTime = System.currentTimeMillis();
                while(!stTask.isDone()
                        && System.currentTimeMillis() - startTime
                        < WAIT_TASK_TO_DIE_MAX) {
                    try {
                        Thread.sleep(WAIT_TASK_TO_DIE_SLEEP);
                    } catch (InterruptedException ex) {
                        LOGGER.log(Level.INFO,
                                "Caught InterruptedException while waiting for "
                                + "{0} start task to die.",
                                instance.getName());
                    }
                }
                if (!stTask.isDone()) {
                        LOGGER.log(Level.INFO,
                                "Start task for {0} did not finish within {1} ms.",
                                new String[] {instance.getName(),
                                    Integer.toString(WAIT_TASK_TO_DIE_MAX)});
                    
                }
            }
            TaskStateListener[] listeners
                    = new TaskStateListener[isStateListener ? 2 : 1];
            listeners[0] = killServerListener;
            if (isStateListener) {
                listeners[1] = stateListener;
            }
            task = new FutureTask<TaskState>(new KillTask(instance, listeners));
        } else {
            task = new FutureTask<TaskState>(
                    new NoopTask(this, null, stateListener));
        }
        RP.post(task);
        return task;
    }

    @Override
    public Future<TaskState> restartServer(TaskStateListener stateListener) {
        LOGGER.log(Level.FINEST,
                "CSS.restartServer called on thread \"{0}\"",
                Thread.currentThread().getName());
        FutureTask<TaskState> task = new FutureTask<TaskState>(
                new RestartTask(this, stateListener));
        RP.post(task);
        return task;
    }

    /**
     * Sends restart-domain command to server (asynchronous)
     *
     */
    public Future<ResultString> restartServer(final int debugPort,
            boolean debug, TaskStateListener[] listeners) {
        if (-1 == debugPort) {
            Command command = new CommandRestartDAS(false);
            return ServerAdmin.<ResultString>exec(
                    instance, command, null, listeners);
        }
        TaskState state = null;
        try {
            ResultMap<String, String> result
                    = CommandGetProperty.getProperties(instance,
                    "configs.config.server-config.java-config.debug-options");
            if (result.getState() == TaskState.COMPLETED) {
                Map<String, String> values = result.getValue();
                if (values != null && !values.isEmpty()) {
                    String oldValue = values.get(
                            "configs.config.server-config.java-config.debug-options");
                    CommandSetProperty setCmd =
                            getCommandFactory().getSetPropertyCommand(
                            "configs.config.server-config.java-config.debug-options",
                            oldValue.replace("transport=dt_shmem", "transport=dt_socket").
                            replace("address=[^,]+", "address=" + debugPort));
                    try {
                        CommandSetProperty.setProperty(instance, setCmd);
                        debug = true;
                    } catch (PayaraIdeException gfie) {
                        debug = false;
                        LOGGER.log(Level.INFO, debugPort + "", gfie);
                    }
                }
            }
        } catch (PayaraIdeException gfie) {
            LOGGER.log(Level.INFO,
                    "Could not retrieve property from server.", gfie);
        }
        Command command = new CommandRestartDAS(debug);
        return ServerAdmin.<ResultString>exec(
                instance, command, null, listeners);
    }

    @Override
    public Future<ResultString> deploy(final TaskStateListener stateListener,
            final File application, final String name) {
        return deploy(stateListener, application, name, null);
    }

    @Override
    public Future<ResultString> deploy(final TaskStateListener stateListener,
            final File application, final String name,
            final String contextRoot) {
        return deploy(stateListener, application, name, contextRoot, null);
    }

    @Override
    public Future<ResultString> deploy(final TaskStateListener stateListener,
            final File application, final String name, final String contextRoot,
            final Map<String,String> properties) {
        return deploy(stateListener, application, name, contextRoot, null, new File[0]);
    }

    @Override
    public Future<ResultString> deploy(final TaskStateListener stateListener,
            final File application, final String name, final String contextRoot,
            final Map<String, String> properties, final File[] libraries) {
        try {
            return ServerAdmin.<ResultString>exec(instance, new CommandDeploy(
                    name, Util.computeTarget(instance.getProperties()),
                    application, contextRoot, properties, libraries,
                    instance.isHotDeployEnabled()
            ), null, new TaskStateListener[]{stateListener});
        } finally {
            refreshChildren();
        }
    }

    @Override
    public Future<ResultString> redeploy(
            final TaskStateListener stateListener,
            final String name, boolean resourcesChanged,
            boolean metadataChanged, List<String> sourcesChanged) {
        return redeploy(stateListener, name, null,
                resourcesChanged, metadataChanged, sourcesChanged);
    }

    @Override
    public Future<ResultString> redeploy(
            final TaskStateListener stateListener,
            final String name, final String contextRoot,
            boolean resourcesChanged, boolean metadataChanged, List<String> sourcesChanged) {
        return redeploy(stateListener, name, contextRoot, new File[0],
                resourcesChanged, metadataChanged, sourcesChanged);
    }

    @Override
    public Future<ResultString> redeploy(TaskStateListener stateListener,
    String name, String contextRoot, File[] libraries,
    boolean resourcesChanged, boolean metadataChanged, List<String> sourcesChanged) {

        Map<String, String> properties = new HashMap<>();
        String url = instance.getProperty(PayaraModule.URL_ATTR);

        String sessionPreservationFlag = instance.getProperty(PayaraModule.SESSION_PRESERVATION_FLAG);
        if (sessionPreservationFlag == null) {
            // If there isn't a value stored for the instance, use the value of
            // the command-line flag.
            sessionPreservationFlag = System.getProperty(
                    "glassfish.session.preservation.enabled", "false");
        }
        if (Boolean.parseBoolean(sessionPreservationFlag)) {
            properties.put("keepSessions", "true");
        }
        if (resourcesChanged) {
            properties.put("preserveAppScopedResources", "true");
        }
        try {
            return ServerAdmin.<ResultString>exec(instance, new CommandRedeploy(
                    name, Util.computeTarget(instance.getProperties()),
                    contextRoot, properties, libraries,
                    url != null && url.contains("deployer:pfv"),
                    instance.isHotDeployEnabled(),
                    metadataChanged, sourcesChanged
            ), stateListener);
        } finally {
            refreshChildren();
        }
    }

    @Override
    public Future<ResultString> undeploy(
            final TaskStateListener stateListener, final String name) {
        return ServerAdmin.<ResultString>exec(
                instance, new CommandUndeploy(name, Util.computeTarget(
                instance.getProperties())), null,
                new TaskStateListener[]{stateListener});
    }

    @Override
    public Future<ResultString> enable(
            final TaskStateListener stateListener, final String name) {
        return ServerAdmin.<ResultString>exec(instance, new CommandEnable(
                name,  Util.computeTarget(instance.getProperties())), null,
                new TaskStateListener[] {stateListener});
    }

    @Override
    public Future<ResultString> disable(
            final TaskStateListener stateListener, final String name) {
        return ServerAdmin.<ResultString>exec(instance, new CommandDisable(
                name,  Util.computeTarget(instance.getProperties())), null,
                new TaskStateListener[] {stateListener});
    }

    @Override
    public AppDesc [] getModuleList(String container) {
        int total = 0;
        Map<String, List<AppDesc>> appMap = getApplications(container);
        Collection<List<AppDesc>> appLists = appMap.values();
        for(List<AppDesc> appList: appLists) {
            total += appList.size();
        }
        AppDesc [] result = new AppDesc[total];
        int index = 0;
        for(List<AppDesc> appList: appLists) {
            for(AppDesc app: appList) {
                result[index++] = app;
            }
        }
        return result;
    }

    @Override
    public Map<String, ResourceDesc> getResourcesMap(String type) {
        Map<String, ResourceDesc> resourcesMap
                = new HashMap<String, ResourceDesc>();
        List<ResourceDesc> resourcesList
                = ResourceDesc.getResources(instance, type);
        for (ResourceDesc resource : resourcesList) {
            resourcesMap.put(resource.getName(), resource);
        }
        return resourcesMap;
    }

    @Override
    public ServerState getServerState() {
        if (serverState == ServerState.UNKNOWN) {
            RequestProcessor.Task task = refresh();
            if (task != null) {
                task.waitFinished();
            }
        }
        return serverState;
    }

    @Override
    public void addChangeListener(final ChangeListener listener) {
        changeSupport.addChangeListener(listener);
    }

    @Override
    public void removeChangeListener(final ChangeListener listener) {
        changeSupport.removeChangeListener(listener);
    }

    @Override
    public String setEnvironmentProperty(final String name, final String value,
            final boolean overwrite) {
        String result;

        synchronized (instance.getProperties()) {
            result = instance.getProperty(name);
            if(result == null || overwrite == true) {
                instance.putProperty(name, value);
                setInstanceAttr(name, value);
                result = value;
            }
        }

        return result;
    }

    // ------------------------------------------------------------------------
    // bookkeeping & impl managment, not exposed via interface.
    // ------------------------------------------------------------------------
    void setProperty(final String key, final String value) {
        instance.putProperty(key, value);
    }

    void getProperty(String key) {
        instance.getProperty(key);
    }

    boolean setInstanceAttr(String name, String value) {
        boolean retVal = false;
        if(instanceFO == null || !instanceFO.isValid()) {
            instanceFO = getInstanceFileObject();
        }
        if(instanceFO != null && instanceFO.canWrite()) {
            try {
                Object currentValue = instanceFO.getAttribute(name);
                if (null != currentValue && currentValue.equals(value)) {
                    // do nothing
                } else {
                    instanceFO.setAttribute(name, value);
                }
                retVal = true;
            } catch(IOException ex) {
                LOGGER.log(Level.WARNING,
                        "Unable to save attribute " + name + " in " + instanceFO.getPath() + " for " + getDeployerUri(), ex); // NOI18N
            }
        } else {
            if (null == instanceFO)
                LOGGER.log(Level.WARNING,
                        "Unable to save attribute {0} for {1} in {3}. Instance file is writable? {2}",
                        new Object[]{name, getDeployerUri(), false, "null"}); // NOI18N
            else
                LOGGER.log(Level.WARNING,
                        "Unable to save attribute {0} for {1} in {3}. Instance file is writable? {2}",
                        new Object[]{name, getDeployerUri(), instanceFO.canWrite(), instanceFO.getPath()}); // NOI18N
        }
        return retVal;
    }

    void setFileObject(FileObject fo) {
        instanceFO = fo;
    }

   
    public static boolean isRunning(final String host, final int port,
            final String name, final int timeout) {
        if(null == host)
            return false;

        try {
            InetSocketAddress isa = new InetSocketAddress(host, port);
            Socket socket = new Socket();
            Logger.getLogger("payara-socket-connect-diagnostic").log(
                    Level.FINE, "Using socket.connect", new Exception());
            socket.connect(isa, timeout);
            socket.setSoTimeout(timeout);
            try {
                socket.close();
            } catch (IOException ioe) {
                LOGGER.log(
                        Level.INFO, "Socket closing failed: {0}",
                        ioe.getMessage());
            }
            return true;
        } catch (java.net.ConnectException | java.net.SocketTimeoutException ex) {
            return false;
        } catch (IOException ioe) {
            String message = NbBundle.getMessage(CommonServerSupport.class,
                    name == null || "".equals(name.trim())
                    ? "MSG_FLAKEY_NETWORK" : "MSG_FLAKEY_NETWORK2",
                    host, Integer.toString(port), ioe.getLocalizedMessage());
            NotifyDescriptor nd = new NotifyDescriptor.Message(message);
            DialogDisplayer.getDefault().notifyLater(nd);
            LOGGER.log(Level.INFO,
                    "Evidence of network flakiness: {0}", ioe.getMessage());
            return false;
        }
    }

    public static boolean isRunning(final String host, final int port,
            final String name) {
        return isRunning(host, port, name, 2000);
    }

    // ------------------------------------------------------------------------
    //  RefreshModulesCookie implementation (for refreshing server state)
    // ------------------------------------------------------------------------
    private final AtomicBoolean refreshRunning = new AtomicBoolean(false);

    @Override
    public final RequestProcessor.Task refresh() {
        return refresh(null,null);
    }

    @Override
    public RequestProcessor.Task refresh(String expected, String unexpected) {
        
        if(refreshRunning.compareAndSet(false, true)) {
            return RP.post(new Runnable() {
                @Override
                public void run() {
                    // Get current server status.
                    PayaraServerStatus status
                            = PayaraStatus.get(instance);
                    // Start monitoring of this Payara server instance
                    // when status is null and refresh status.
                    if (status == null) {
                        PayaraState.monitor(instance);
                        status = PayaraStatus.get(instance);
                    }
                    ServerState currentState = serverState;
                    switch(currentState) {
                        case UNKNOWN:
                            switch(status.getStatus()) {
                                case ONLINE:
                                    setServerState(ServerState.RUNNING);
                                    break;
                                case STARTUP:
                                    setServerState(ServerState.STARTING);
                                    break;
                                case SHUTDOWN:
                                    setServerState(ServerState.STOPPING);
                                    break;
                                case OFFLINE:
                                    setServerState(ServerState.STOPPED);
                            }
                            break;
                        case STOPPED:
                            if (status.getStatus() == PayaraStatus.ONLINE) {
                                setServerState(ServerState.RUNNING);
                            }
                            break;
                        case RUNNING:
                            switch(status.getStatus()) {
                                case STARTUP:
                                    setServerState(ServerState.STARTING);
                                    break;
                                case SHUTDOWN:
                                    setServerState(ServerState.STOPPING);
                                    break;
                                case OFFLINE:
                                    setServerState(ServerState.STOPPED);
                            }
                            break;
                        case STOPPED_JVM_PROFILER:
                            if (status.getStatus() == PayaraStatus.ONLINE) {
                                setServerState(ServerState.RUNNING);
                            }
                    }
                    refreshRunning.set(false);
                }
            });
        } else {
            return null;
        }
    }

    void disableStop() {
        stopDisabled = true;
    }
    
    void setLocalStartProcess(Process process) {
        this.localStartProcess = process;
    }
    
    Process getLocalStartProcess() {
        return localStartProcess;
    }
    
    void stopLocalStartProcess() {
        localStartProcess.destroy();
        localStartProcess = null;
    }

    @Override
    public CommandFactory getCommandFactory() {
        return instance.getInstanceProvider().getCommandFactory();
    }

    @Override
    public String getResourcesXmlName() {
        return org.netbeans.modules.payara.spi.Utils
                .useGlassFishPrefix(getDeployerUri()) ?
                "glassfish-resources" : "sun-resources"; // NOI18N
    }

    @Override
    public boolean supportsRestartInDebug() {
        return getDeployerUri().contains(PayaraInstanceProvider.EE6WC_DEPLOYER_FRAGMENT);
    }

    @Override
    public boolean isRestfulLogAccessSupported() {
        return getDeployerUri().contains(PayaraInstanceProvider.EE6WC_DEPLOYER_FRAGMENT);
    }

    @Override
    public boolean isWritable() {
        return (null == instanceFO) ? false : instanceFO.canWrite();
    }

    private long latestWarningDisplayTime = System.currentTimeMillis();
    
    private long getLatestWarningDisplayTime() {
        return latestWarningDisplayTime;
    }

    private void setLatestWarningDisplayTime(long currentTime) {
        latestWarningDisplayTime = currentTime;
    }

    /**
     * Update HTTP port value from server properties.
     */
    void updateHttpPort() {
        String target = Util.computeTarget(instance.getProperties());
        String gpc;
        if (Util.isDefaultOrServerTarget(instance.getProperties())) {
            gpc = "*.server-config.*.http-listener-1.port";
            setEnvironmentProperty(PayaraModule.HTTPHOST_ATTR, 
                    instance.getProperty(PayaraModule.HOSTNAME_ATTR), true); // NOI18N
        } else {
            String server = getServerFromTarget(target);
            String adminHost = instance.getProperty(PayaraModule.HOSTNAME_ATTR);
            setEnvironmentProperty(PayaraModule.HTTPHOST_ATTR,
                    getHttpHostFromServer(server,adminHost), true);
            gpc = "servers.server."+server+".system-property.HTTP_LISTENER_PORT.value";
        }
        try {
            ResultMap<String, String> result = CommandGetProperty.getProperties(
                    instance, gpc, PayaraModule.PROPERTIES_FETCH_TIMEOUT);
            boolean didSet = false;
            if (result.getState() == TaskState.COMPLETED) {
                Map<String, String> values = result.getValue();
                for (Entry<String, String> entry : values.entrySet()) {
                    String val = entry.getValue();
                    try {
                        if (null != val && val.trim().length() > 0) {
                            Integer.parseInt(val);
                            setEnvironmentProperty(PayaraModule.HTTPPORT_ATTR, val, true);
                            didSet = true;
                        }
                    } catch (NumberFormatException nfe) {
                        LOGGER.log(Level.FINEST,
                                "Property value {0} was not a number", val);
                    }
                }
            }
            if (!didSet && !Util.isDefaultOrServerTarget(instance.getProperties())) {
                setEnvironmentProperty(PayaraModule.HTTPPORT_ATTR, "28080", true); // NOI18N
            }
        } catch (PayaraIdeException gfie) {
            LOGGER.log(Level.INFO, "Could not get http port value.", gfie);
        }
    }

    /**
     * Sends list-applications command to server (synchronous)
     *
     * @param container
     * @return String array of names of deployed applications.
     */
    public Map<String, List<AppDesc>> getApplications(String container) {
        Map<String, List<AppDesc>> result = Collections.emptyMap();
            Map<String, List<String>> apps = Collections.emptyMap();
        try {
            ResultMap<String, List<String>> resultMap
                    = CommandListComponents.listComponents(instance,
                    Util.computeTarget(instance.getProperties()));
            if (resultMap.getState() == TaskState.COMPLETED) {
                apps = resultMap.getValue();
            }
        } catch (PayaraIdeException gfie) {
            LOGGER.log(Level.INFO,
                    "Could not retrieve components server.", gfie);
        }
        if (null == apps || apps.isEmpty()) {
            return result;
        }
        try {
            ResultMap<String, String> appPropsResult = CommandGetProperty
                    .getProperties(instance, "applications.application.*");
            if (appPropsResult.getState() == TaskState.COMPLETED) {
                ResultMap<String, String> appRefResult
                        = CommandGetProperty.getProperties(
                        instance, "servers.server.*.application-ref.*");
                if (appRefResult.getState() == TaskState.COMPLETED) {
                    result = processApplications(apps,
                            appPropsResult.getValue(),
                            appRefResult.getValue());
                }
            }
        } catch (PayaraIdeException gfie) {
            LOGGER.log(Level.INFO,
                    "Could not retrieve property from server.", gfie);
        }
        return result;
    }

    private Map<String, List<AppDesc>> processApplications(Map<String,
            List<String>> appsList, Map<String, String> properties,
            Map<String, String> refProperties){
        Map<String, List<AppDesc>> result = new HashMap<String, List<AppDesc>>();
        Iterator<String> appsItr = appsList.keySet().iterator();
        while (appsItr.hasNext()) {
            String engine = appsItr.next();
            List<String> apps = appsList.get(engine);
            for (int i = 0; i < apps.size(); i++) {
                String name = apps.get(i).trim();
                String appname = "applications.application." + name; // NOI18N
                String contextKey = appname + ".context-root"; // NOI18N
                String pathKey = appname + ".location"; // NOI18N

                String contextRoot = properties.get(contextKey);
                if (contextRoot == null) {
                    contextRoot = name;
                }
                if (contextRoot.startsWith("/")) {  // NOI18N
                    contextRoot = contextRoot.substring(1);
                }

                String path = properties.get(pathKey);
                if (path == null) {
                    path = "unknown"; //NOI18N
                }
                if (path.startsWith("file:")) {  // NOI18N
                    path = path.substring(5);
                    if (getInstance().isDocker()
                            && getInstance().getHostPath() != null
                            && !getInstance().getHostPath().isEmpty()
                            && getInstance().getContainerPath() != null
                            && !getInstance().getContainerPath().isEmpty()) {
                        Path relativePath = Paths.get(getInstance().getContainerPath()).relativize(Paths.get(path));
                        path = Paths.get(getInstance().getHostPath(), relativePath.toString()).toString();
                    } else {
                        path = (new File(path)).getAbsolutePath();
                    }
                }

                String enabledKey = "servers.server.server.application-ref."
                        +name+ ".enabled";  //NOI18N
                // This needs to be more focused. Does it need to list
                // of servers that are associated with the target?
                for (String possibleKey : refProperties.keySet()) {
                    if (possibleKey.endsWith(".application-ref."
                            + name + ".enabled")) { // NOI18N
                        enabledKey = possibleKey;
                    }
                }
                String enabledValue = refProperties.get(enabledKey);
                if (null != enabledValue) {
                    boolean enabled = Boolean.parseBoolean(enabledValue);

                    List<AppDesc> appList = result.get(engine);
                    if(appList == null) {
                        appList = new ArrayList<AppDesc>();
                        result.put(engine, appList);
                    }
                    appList.add(new AppDesc(name, path, contextRoot, enabled));
                }
            }
        }
        return result;
    }

    /**
     * Retrieve server name using target name from server properties.
     * <p/>
     * @param target Server target name.
     * @return Name of server having this target.
     */
    private String getServerFromTarget(String target) {
        String retVal = target; // NOI18N
        String gpc = "clusters.cluster."+target+".server-ref.*.ref";
        try {
            ResultMap<String, String> result = CommandGetProperty.getProperties(
                    instance, gpc, PROPERTIES_FETCH_TIMEOUT);
            if (result.getState() == TaskState.COMPLETED) {
                Map<String, String> values = result.getValue();
                for (Entry<String, String> entry : values.entrySet()) {
                    String val = entry.getValue();
                        if (null != val && val.trim().length() > 0) {
                            retVal = val;
                            break;
                        }
                }
            }
        } catch (PayaraIdeException gfie) {
            LOGGER.log(Level.INFO, "Could not get server value from target.", gfie);
        }

        return retVal;
    }

    /**
     * Retrieve HTTP host name for server from server properties.
     * <p/>
     * @param server          Server name.
     * @param nameOfLocalhost Local host DNS name.
     * @return HTTP host name for server.
     */
    private String getHttpHostFromServer(
            String server, String nameOfLocalhost) {
        String retVal = FAILED_HTTP_HOST;
        String refVal = null;
        String gpc = "servers.server."+server+".node-ref";
        try {
            ResultMap<String, String> result = CommandGetProperty.getProperties(
                    instance, gpc, PROPERTIES_FETCH_TIMEOUT);
            if (result.getState() == TaskState.COMPLETED) {
                for (Entry<String, String> entry 
                        : result.getValue().entrySet()) {
                    String val = entry.getValue();
                    if (null != val && val.trim().length() > 0) {
                        refVal = val;
                        break;
                    }
                }
                if (refVal != null) {
                    gpc = "nodes.node." + refVal + ".node-host";
                    result = CommandGetProperty.getProperties(
                            instance, gpc, PROPERTIES_FETCH_TIMEOUT);
                    if (result.getState() == TaskState.COMPLETED) {
                        for (Entry<String, String> entry
                                : result.getValue().entrySet()) {
                            String val = entry.getValue();
                            if (null != val && val.trim().length() > 0) {
                                retVal = val;
                                break;
                            }
                        }
                    }
                }
            }
        } catch (PayaraIdeException gfie) {
            LOGGER.log(Level.INFO, "Could not get http host value.", gfie);
        }
        return LOCALHOST.equals(retVal) ? nameOfLocalhost : retVal; // NOI18N
    }

    private void refreshChildren() {
        PayaraInstance instance = getInstance();
        RefreshModulesCookie cookie = instance.getFullNode().getLookup().lookup(RefreshModulesCookie.class);
        if (cookie != null) {
            cookie.refresh(null, null);
        }
    }

}
