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

import org.netbeans.modules.glassfish.tooling.admin.ResultProcess;
import org.netbeans.modules.glassfish.tooling.admin.CommandException;
import org.netbeans.modules.glassfish.tooling.admin.CommandStartInstance;
import org.netbeans.modules.glassfish.tooling.admin.CommandVersion;
import org.netbeans.modules.glassfish.tooling.admin.CommandStartCluster;
import org.netbeans.modules.glassfish.tooling.admin.ResultString;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
import org.netbeans.modules.glassfish.tooling.GlassFishIdeException;
import org.netbeans.modules.glassfish.tooling.GlassFishStatus;
import static org.netbeans.modules.glassfish.tooling.GlassFishStatus.OFFLINE;
import static org.netbeans.modules.glassfish.tooling.GlassFishStatus.ONLINE;
import static org.netbeans.modules.glassfish.tooling.GlassFishStatus.SHUTDOWN;
import static org.netbeans.modules.glassfish.tooling.GlassFishStatus.STARTUP;
import org.netbeans.modules.glassfish.tooling.TaskEvent;
import org.netbeans.modules.glassfish.tooling.TaskState;
import org.netbeans.modules.glassfish.tooling.TaskStateListener;
import org.netbeans.modules.glassfish.tooling.data.GlassFishServerStatus;
import org.netbeans.modules.glassfish.tooling.data.StartupArgs;
import org.netbeans.modules.glassfish.tooling.data.StartupArgsEntity;
import org.netbeans.modules.glassfish.tooling.server.FetchLogSimple;
import org.netbeans.modules.glassfish.tooling.server.ServerTasks;
import org.netbeans.modules.glassfish.tooling.utils.NetUtils;
import org.netbeans.modules.glassfish.tooling.utils.ServerUtils;
import org.netbeans.api.extexecution.startup.StartupExtender;
import static org.netbeans.modules.glassfish.common.BasicTask.START_TIMEOUT;
import org.netbeans.modules.glassfish.common.ui.JavaSEPlatformPanel;
import org.netbeans.modules.glassfish.common.utils.AdminKeyFile;
import org.netbeans.modules.glassfish.common.utils.JavaUtils;
import org.netbeans.modules.glassfish.common.utils.Util;
import org.netbeans.modules.glassfish.spi.*;
import org.netbeans.modules.glassfish.spi.GlassfishModule.ServerState;
import org.openide.execution.NbProcessDescriptor;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
import org.openide.util.lookup.Lookups;

/**
 * Asynchronous GlassFish server startup command execution.
 * <p/>
 * @author Ludovic Chamenois, Peter Williams, Tomas Kraus
 */
public class StartTask extends BasicTask<TaskState> {

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

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

    private static RequestProcessor NODE_REFRESHER
            = new RequestProcessor("nodes to refresh");

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

    private static String[] removeEscapes(String[] args) {
        for (int i = 0; i < args.length; i++) {
            args[i] = args[i].replace("\\\"", ""); // NOI18N
        }
        return args;
    }

    private static StartupExtender.StartMode getMode(String gfMode) {
        if (GlassfishModule.PROFILE_MODE.equals(gfMode)) {
            return StartupExtender.StartMode.PROFILE;
        } else if (GlassfishModule.DEBUG_MODE.equals(gfMode)) {
            return StartupExtender.StartMode.DEBUG;
        } else {
            return StartupExtender.StartMode.NORMAL;
        }
    }

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

    private final CommonServerSupport support;
    private List<Recognizer> recognizers;
    private List<String> jvmArgs = null;
    private final VMIntrospector vmi;

    /** internal Java SE platform home cache. */
    private FileObject jdkHome;

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

    /**
     * Constructs an instance of asynchronous GlassFish server startup command
     * execution support object.
     * <p/>
     * @param support       Common support object for the server instance being
     *                      started.
     * @param recognizers   Output recognizers to pass to log processors, if any.
     * @param stateListener State monitor to track start progress.
     */
    public StartTask(CommonServerSupport support, List<Recognizer> recognizers,
            VMIntrospector vmi,
            TaskStateListener... stateListener) {
        this(support, recognizers, vmi, null, stateListener);
    }

    /**
     * Constructs an instance of asynchronous GlassFish server startup command
     * execution support object.
     * <p/>
     * @param support       Common support object for the server instance being
     *                      started.
     * @param recognizers   Output recognizers to pass to log processors, if any.
     * @param jdkRoot       Java SE Development Kit to start server in profiling
     *                      mode.
     * @param jvmArgs       JVM arguments used to start server in profiling
     *                      mode.
     * @param stateListener State monitor to track start progress.
     */
    public StartTask(final CommonServerSupport support,
            List<Recognizer> recognizers, VMIntrospector vmi, String[] jvmArgs,
            TaskStateListener... stateListener) {
        super(support.getInstance(), stateListener);
        List<TaskStateListener> listeners = new ArrayList<>();
        listeners.addAll(Arrays.asList(stateListener));
        listeners.add(new TaskStateListener() {

            @Override
            public void operationStateChanged(TaskState newState,
                    TaskEvent event, String... args) {
                if (TaskState.COMPLETED.equals(newState)) {
                    // attempt to sync the comet support
                    RequestProcessor.getDefault().post(
                            new EnableComet(support.getInstance()));
                }
            }
        });
        this.stateListener = listeners.toArray(new TaskStateListener[listeners.size()]);
        this.support = support;
        this.recognizers = recognizers;
        this.jvmArgs = (jvmArgs != null) ? Arrays.asList(removeEscapes(jvmArgs)) : null;
        this.vmi = vmi;
        this.jdkHome = null;
        LOGGER.log(Level.FINE, "VMI == {0}", vmi);
    }

    ////////////////////////////////////////////////////////////////////////////
    // ExecutorService call() Method                                          //
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Asynchronous task method started by {@link Executors}.
     * <p/>
     * @return Task execution result.
     */
    @Override
    public TaskState call() {
        // Save the current time so that we can deduct that the startup
        // Failed due to timeout
        LOGGER.log(Level.FINEST, "StartTask.call() called on thread \"{0}\"",
                Thread.currentThread().getName());
        final long start = System.currentTimeMillis();

        final String host = instance.getHost();
        final int adminPort = instance.getAdminPort();
        StateChange change;
        if ((change = validateAdminHostAndPort(host, adminPort)) != null) {
                return change.fireOperationStateChanged();
        }
        // Remote server.
        if (support.isRemote()) {
            if (GlassFishState.isOnline(instance)) {
                if (Util.isDefaultOrServerTarget(instance.getProperties())) {
                    return restartDAS(host, adminPort, start);
                } else {
                    return startClusterOrInstance(host, adminPort);
                }
            } else {
                return fireOperationStateChanged(TaskState.FAILED,
                        TaskEvent.CMD_FAILED,
                        "MSG_START_SERVER_FAILED_DASDOWN", instanceName);
            }
        // Local server.
        } else
        // Our server is offline.
        if (GlassFishState.isOffline(instance)) {
            // But administrator port is occupied.
            if (ServerUtils.isAdminPortListening(
                    instance, NetUtils.PORT_CHECK_TIMEOUT)) {
                ResultString version;
                try {
                    version = CommandVersion.getVersion(instance);
                } catch (CommandException ce) {
                    version = null;
                }
                // Got version response from DAS.
                if (version != null) {
                    // There is server with matching version.
                    if (CommandVersion.verifyResult(version, instance)) {
                        fireOperationStateChanged(TaskState.RUNNING,
                                TaskEvent.CMD_COMPLETED,
                                "StartTask.call.matchVersion",
                                version.getValue());
                        return startClusterOrInstance(host, adminPort);
                    // There is server with non matching version.
                    } else {
                        if (!version.isAuth()) {
                            return fireOperationStateChanged(TaskState.FAILED,
                                    TaskEvent.CMD_FAILED,
                                    "StartTask.call.authFailed",
                                    instanceName, version.getValue());
                        } else {
                            return fireOperationStateChanged(TaskState.FAILED,
                                    TaskEvent.CMD_FAILED,
                                    "StartTask.call.anotherVersion",
                                    instanceName, version.getValue());
                        }
                    }
                // Got no version response from DAS.
                } else {
                    return fireOperationStateChanged(TaskState.FAILED,
                            TaskEvent.CMD_FAILED,
                            "StartTask.call.unknownVersion", instanceName);
                }
            } else {
                return startDAS(host, adminPort);
            }
            // Our server is online.
        } else {
            return startClusterOrInstance(host, adminPort);
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    // Methods                                                                //
    ////////////////////////////////////////////////////////////////////////////

    /**
     * Validate <code>host</code> and <code>port</code> values
     * for DAS listener.
     * <p/>
     * @return State change request data when server shall not be started
     *          and listeners should be notified about it or <code>null</code>
     *          otherwise.
     */
    private StateChange validateAdminHostAndPort(
            final String host, final int adminPort) {
        if (host == null || host.length() == 0) {
            return new StateChange(this, TaskState.FAILED, TaskEvent.CMD_FAILED,
                    "MSG_START_SERVER_FAILED_NOHOST", instanceName);
        }
        if (adminPort < 0 || adminPort > 65535) {
            return new StateChange(this, TaskState.FAILED, TaskEvent.CMD_FAILED,
                    "MSG_START_SERVER_FAILED_BADPORT", instanceName);
        }
        return null;
    }

    private TaskState restartDAS(String adminHost, int adminPort, final long start) {
        // deal with the remote case here...
        TaskStateListener[] listeners = {
                new TaskStateListener() {
                    // if the http command is successful, we are not done yet...
                    // The server still has to stop. If we signal success to the 'stateListener'
                    // for the task, it may be premature.
                    @SuppressWarnings("SleepWhileInLoop")
                    @Override
                    public void operationStateChanged(TaskState newState,
                            TaskEvent event, String... args) {
                        if (newState == TaskState.RUNNING) {
                            support.setServerState(ServerState.STARTING);
                        }
                        if (newState == TaskState.FAILED) {
                            fireOperationStateChanged(newState, event,
                                    instanceName, args);
                            support.setServerState(ServerState.STOPPED);
                            //support.refresh();
                        } else if (args != null && newState == TaskState.COMPLETED) {
                            for (String message : args) {
                                if (message.matches("[sg]et\\?.*\\=configs\\..*")) {
                                    return;
                                }
                            }
                            long startTime = System.currentTimeMillis();
                            TaskState state = TaskState.RUNNING;
                            try {
                                Thread.sleep(2000);
                            } catch (InterruptedException e) {
                                // no op
                            }
                            while (TaskState.RUNNING == state && System.currentTimeMillis() - start < START_TIMEOUT) {
                                // Send the 'completed' event and return when the server is running
                                boolean httpLive = GlassFishState.isOnline(instance); //CommonServerSupport.isRunning(host, port,instance.getProperty(GlassfishModule.DISPLAY_NAME_ATTR));

                                // Sleep for a little so that we do not make our checks too often
                                //
                                // Doing this before we check httpAlive also prevents us from
                                // pinging the server too quickly after the ports go live.
                                //
                                try {
                                    Thread.sleep(1000);
                                } catch (InterruptedException e) {
                                    // no op
                                }

                                if (httpLive) {
                                    try {
                                        Thread.sleep(2000);
                                    } catch (InterruptedException e) {
                                        // no op
                                    }
                                    state = TaskState.COMPLETED;
                                }
                            }
                            if (state == TaskState.COMPLETED) { //support.isReady(false, 120, TimeUnit.SECONDS)) {
                                support.setServerState(ServerState.RUNNING);
                            } else {
                                support.setServerState(ServerState.STOPPED);
                            }
                        }
                    }
                }};
        int debugPort = -1;
        if (GlassfishModule.DEBUG_MODE.equals(instance.getProperty(GlassfishModule.JVM_MODE))) {
            debugPort = instance.getDebugPort();
        }
        support.restartServer(debugPort,
                support.supportsRestartInDebug() && debugPort >= 0, listeners);
        return fireOperationStateChanged(
                TaskState.RUNNING, TaskEvent.CMD_FAILED,
                "StartTask.restartDAS.inProgress", instanceName);

    }

    /**
     * Reset administrator password when needed.
     */
    private void resetPassword() {
        AdminKeyFile keyFile = new AdminKeyFile(instance);
        keyFile.read();
        if (keyFile.isReset()) {
            String password = AdminKeyFile.randomPassword(
                    AdminKeyFile.RANDOM_PASSWORD_LENGTH);            
            instance.setAdminPassword(password);
            keyFile.setPassword(password);
            try {
                GlassfishInstance.writeInstanceToFile(instance);
            } catch(IOException ex) {
                LOGGER.log(Level.INFO,
                        "Could not store GlassFish server attributes", ex);
            }
            keyFile.write();
        }
    }

    /**
     * Initialize JDK used to start GlassFish server.
     * <p/>
     * @return State change request data when JDK could not be initialized
     *         or <code>null</code> otherwise.
     */
    private StateChange initJDK() {
        try {
            if (null == jdkHome) {
                jdkHome = getJavaPlatformRoot();
                File jdkHomeFile = FileUtil.toFile(jdkHome);
                if (!JavaUtils.isJavaPlatformSupported(instance, jdkHomeFile)) {
                    jdkHome = JavaSEPlatformPanel.selectServerSEPlatform(
                            instance, jdkHomeFile);
                }
            }
            if (jdkHome == null) {
                return new StateChange(this, TaskState.FAILED,
                        TaskEvent.CMD_FAILED, "StartTask.initJDK.null",
                        instanceName);
            }
        } catch (IOException ex) {
            LOGGER.log(Level.INFO, null, ex); // NOI18N
            return new StateChange(this, TaskState.FAILED,
                    TaskEvent.CMD_FAILED, "StartTask.initJDK.exception",
                    new String[] {instanceName, ex.getLocalizedMessage()});
        }
        return null;
    }

    /**
     * Verify GlassFish server status before starting it.
     * <p/>
     * @return State change request data when server shall not be started
     *         and listeners should be notified about it or <code>null</code>
     *         otherwise.
     */
    private StateChange checkBeforeStart() {
        GlassFishServerStatus status = GlassFishState.getStatus(instance);
        String msgKey = null;
        switch (status.getStatus()) {
            case ONLINE:
                TaskState result;
                TaskEvent event;
                if (GlassfishModule.PROFILE_MODE.equals(
                        instance.getProperty(GlassfishModule.JVM_MODE))) {
                    result = TaskState.FAILED;
                    event = TaskEvent.CMD_FAILED;
                } else {
                    result = TaskState.COMPLETED;
                    event = TaskEvent.CMD_COMPLETED;
                }
                return new StateChange(this, result, event,
                        "StartTask.startDAS.alreadyRunning");
            case OFFLINE:
                if (ServerUtils.isAdminPortListening(
                        instance, NetUtils.PORT_CHECK_TIMEOUT)) {
                    msgKey = "StartTask.startDAS.adminPortOccupied";
                } else {
                    final int httpPort = instance.getPort();
                    if (httpPort >= 0 && httpPort <= 65535
                            && NetUtils.isPortListeningLocal(
                            instance.getHost(), httpPort)) {
                        msgKey = "StartTask.startDAS.httpPortOccupied";
                    }
                }
                break;
            case SHUTDOWN:
                msgKey = "StartTask.startDAS.shutdown";
                break;
            case STARTUP:
                msgKey = "StartTask.startDAS.startup";
        }
        return msgKey != null
                ? new StateChange(this, TaskState.FAILED,
                TaskEvent.CMD_FAILED, msgKey, this.instance.getDisplayName())
                : null;
    }

    /**
     * Check for server status in profiling mode during startup.
     * <p/>
     * @return State change request about server startup success.
     */
    @SuppressWarnings("SleepWhileInLoop")
    private StateChange profilingServerStatus() {
        LOGGER.log(Level.FINE,
                "Profiling mode status hack for {0}",
                new Object[]{instance.getName()});
        // Save process to be able to stop process waiting for profiler
        // to attach.
        support.setLocalStartProcess(instance.getProcess());
        // Try to sync the states after the profiler attaches.
        NODE_REFRESHER.post(new Runnable() {
            @Override
            public void run() {
                while (!GlassFishState.isOnline(instance)) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException ex) {
                    }
                }
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        support.refresh();
                    }
                });
            }
        });
        return new StateChange(this, TaskState.COMPLETED,
                TaskEvent.CMD_COMPLETED,
                "MSG_SERVER_STARTED", instanceName);
    }

    private TaskState startDAS(String adminHost, int adminPort) {
        StateChange change;
        resetPassword();
        if ((change = initJDK()) != null) {
            return change.fireOperationStateChanged();
        }
        // Time must be measured after Java SE platform selection is done.
        long start = System.currentTimeMillis();
        StartStateListener listener;
        try {
            // lookup the javadb start service and use it here.
            RegisteredDerbyServer db
                    = Lookup.getDefault().lookup(RegisteredDerbyServer.class);
            if (null != db && "true".equals(
                    instance.getProperty(GlassfishModule.START_DERBY_FLAG))) { // NOI18N
                db.start();
            }
            // This may be an autheticated server... so we will say it is
            // started. Other operations will fail if the process on the port
            // is not a GF v3 server.
            LOGGER.log(Level.FINEST,
                    "Checking if GlassFish {0} is running.",
                    instance.getName());
            if ((change = checkBeforeStart()) != null) {
                return change.fireOperationStateChanged();
            } else if (upgradeFailed()) {
                return fireOperationStateChanged(TaskState.FAILED,
                        TaskEvent.CMD_FAILED,
                        "StartTask.startDAS.domainUpgrade", instanceName);
            }
            // We should be listening for reaching ONLINE state before process
            // is started.
            listener = prepareStartMonitoring(
                    instance.getJvmMode() == GlassFishJvmMode.PROFILE);
            if (listener == null) {
                return fireOperationStateChanged(TaskState.FAILED,
                        TaskEvent.CMD_FAILED,
                        "StartTask.startDAS.startupMonitoring", instanceName);                
            }
            Process process = createProcess();
            instance.setProcess(process);
            listener.setProcess(process);
        } catch (ProcessCreationException ex) {
            Logger.getLogger("glassfish").log(Level.INFO,
                    "Could not start process for " + instanceName, ex);
            return fireOperationStateChanged(TaskState.FAILED,
                    TaskEvent.CMD_FAILED, "MSG_PASS_THROUGH",
                    ex.getLocalizedMessage());
        }

        fireOperationStateChanged(TaskState.RUNNING, TaskEvent.CMD_RUNNING,
                "StartTask.startDAS.inProgress", instanceName);
        // create a logger to the server's output stream so that a user
        // can observe the progress
        LogViewMgr logger = LogViewMgr.getInstance(instance.getProperty(
                GlassfishModule.URL_ATTR));
        logger.readInputStreams(recognizers, false, null,
                new FetchLogSimple(instance.getProcess().getInputStream()),
                new FetchLogSimple(instance.getProcess().getErrorStream()));

        // Waiting for server to start
        LOGGER.log(Level.FINER, "Waiting for server to start for {0} ms",
                new Object[] {Integer.toString(START_TIMEOUT)});
        try {
            synchronized(listener) {
                while (!listener.isWakeUp()
                        && (System.currentTimeMillis()
                        - start < START_TIMEOUT)) {
                    listener.wait(System.currentTimeMillis() - start);
                }
            }
        } catch (InterruptedException ie) {
            LOGGER.log(Level.INFO,
                    "Caught InterruptedException while waiting for {0} to start: {1}",
                    new Object[] {instance.getName(), ie.getLocalizedMessage()});
        } finally {
            GlassFishStatus.removeListener(instance, listener);
        }
        // We need to lie about server status in profiling mode.
        if (null != jvmArgs && (change = profilingServerStatus()) != null) {
            return change.fireOperationStateChanged();
        }
        if (!GlassFishState.isOnline(instance)) {
              return fireOperationStateChanged(
                      TaskState.FAILED, TaskEvent.CMD_FAILED,
                      "StartTask.startDAS.startFailed", instanceName);
        } else {
            return startClusterOrInstance(adminHost, adminPort);
        }
    }

    private TaskState startClusterOrInstance(String adminHost, int adminPort) {
        String target = Util.computeTarget(instance.getProperties());
        if (Util.isDefaultOrServerTarget(instance.getProperties())) {
            return fireOperationStateChanged(TaskState.COMPLETED,
                    TaskEvent.CMD_COMPLETED,
                    "MSG_SERVER_STARTED", instanceName);
        } else {
            TaskState state;
            try {
                ResultString result
                        = CommandStartCluster.startCluster(instance, target);
                state = result.getState();
            } catch (GlassFishIdeException gfie) {
                state = TaskState.FAILED;
                LOGGER.log(Level.INFO, gfie.getMessage(), gfie);
            }
            if (state == TaskState.FAILED) {
                try {
                    ResultString result
                            = CommandStartInstance.startInstance(instance, target);
                    state = result.getState();
                } catch (GlassFishIdeException gfie) {
                    state = TaskState.FAILED;
                    LOGGER.log(Level.INFO, gfie.getMessage(), gfie);
                }
                if (state == TaskState.FAILED) {
                    // if start instance not suscessful fail
                    return fireOperationStateChanged(TaskState.FAILED,
                            TaskEvent.CMD_FAILED,
                            "MSG_START_TARGET_FAILED", instanceName, target);
                }
            }
            support.updateHttpPort();
            return fireOperationStateChanged(TaskState.COMPLETED,
                    TaskEvent.CMD_COMPLETED,
                    "MSG_SERVER_STARTED", instanceName);
        }
    }

    /**
     * Search for Java SE platform to be used for running GlassFish server.
     * <p/>
     * GlassFish instance Java SE platform property is checked first
     * and Java SE platform used to run NetBeans as a fallback option.
     * <p/>
     * @return Java SE platform to be used for running GlassFish server.
     * @throws IOException when GlassFish instance Java SE platform property
     *         does not point to existing directory.
     */
    private FileObject getJavaPlatformRoot() throws IOException {
        FileObject retVal;
        String javaHome = instance.getJavaHome();
        if (null == javaHome || javaHome.trim().length() < 1) {
            File dir = new File(getJdkHome());
            retVal = FileUtil.createFolder(FileUtil.normalizeFile(dir));
        } else {
            File f = new File(javaHome);
            if (f.exists()) {
                retVal = FileUtil.createFolder(FileUtil.normalizeFile(f));
            } else {
                throw new FileNotFoundException(
                        NbBundle.getMessage(StartTask.class,
                        "MSG_INVALID_JAVA", instanceName, javaHome));
            }
        }
        return retVal;
    }

    /**
     * Get Java SE platform used to run NetBeans.
     * <p/>
     * @return Java SE platform used to run NetBeans.
     */
    private String getJdkHome() {
        String result;
        result = System.getProperty("java.home");
        if (result.endsWith(File.separatorChar + "jre")) {
            result = result.substring(0, result.length() - 4);
        }
        return result;
    }

    private StartupArgs createProcessDescriptor() throws ProcessCreationException {
        List<String> glassfishArgs = new ArrayList<>(2);
        String domainDir = Util.quote(getDomainFolder().getAbsolutePath());
        glassfishArgs.add(ServerUtils.cmdLineArgument(
                ServerUtils.GF_DOMAIN_ARG, getDomainName()));
        glassfishArgs.add(ServerUtils.cmdLineArgument(
                ServerUtils.GF_DOMAIN_DIR_ARG, domainDir));

        ArrayList<String> optList = new ArrayList<>();
        // append debug options
        if (GlassfishModule.DEBUG_MODE.equals(instance.getProperty(GlassfishModule.JVM_MODE))) {
            appendDebugOptions(optList);
        }
        
        // append other options from startup extenders, e.g. for profiling
        appendStartupExtenderParams(optList);

        return new StartupArgsEntity(
                glassfishArgs,
                optList,
                (Map<String, String>) null,
                FileUtil.toFile(jdkHome).getAbsolutePath());
    }

    /**
     * Appends debug options for server start.
     * If the port read from instance properties is not valid (null or out of the range),
     * it offers to the user a different free port.
     * 
     * @param optList
     * @throws ProcessCreationException 
     */
    private void appendDebugOptions(List<String> optList) throws ProcessCreationException {
        String debugPortString = instance.getProperty(GlassfishModule.DEBUG_PORT);
        String debugTransport = "dt_socket"; // NOI18N
        if ("true".equals(instance.getProperty(GlassfishModule.USE_SHARED_MEM_ATTR))) { // NOI18N
            debugTransport = "dt_shmem";  // NOI18N
        } else {
            if (null != debugPortString && debugPortString.trim().length() > 0) {
                int t = Integer.parseInt(debugPortString);
                if (t != 0 && (t < GlassfishInstance.LOWEST_USER_PORT
                        || t > 65535)) {
                    throw new NumberFormatException();
                }
            }
        }
        if (null == debugPortString
                || "0".equals(debugPortString) || "".equals(debugPortString)) {
            if ("true".equals(instance.getProperty(GlassfishModule.USE_SHARED_MEM_ATTR))) { // NOI18N
                debugPortString = Integer.toString(
                        Math.abs((instance.getProperty(GlassfishModule.GLASSFISH_FOLDER_ATTR)
                        + instance.getDomainsRoot()
                        + instance.getProperty(GlassfishModule.DOMAIN_NAME_ATTR)).hashCode() + 1));
            } else {
                try {
                    debugPortString = selectDebugPort();
                } catch (IOException ioe) {
                    throw new ProcessCreationException(ioe,
                            "MSG_START_SERVER_FAILED_INVALIDPORT", instanceName, debugPortString); //NOI18N
                }
            }
        }
        support.setEnvironmentProperty(GlassfishModule.DEBUG_PORT, debugPortString, true);
        StringBuilder opt = new StringBuilder();
        opt.append("-agentlib:jdwp=transport="); // NOI18N
        opt.append(debugTransport);
        opt.append(",address="); // NOI18N
        opt.append(debugPortString);
        opt.append(",server=y,suspend=n"); // NOI18N
        optList.add(opt.toString());
    }
    
    private void appendStartupExtenderParams(List<String> optList) {
        for (StartupExtender args : StartupExtender.getExtenders(
                Lookups.singleton(support.getInstanceProvider().getInstance(instance.getProperty("url"))), 
                getMode(instance.getProperty(GlassfishModule.JVM_MODE)))) {
            for (String arg : args.getArguments()) {
                String[] argSplitted = arg.trim().split("\\s+(?=-)");
                optList.addAll(Arrays.asList(argSplitted));
            }
        }
    }

    private String selectDebugPort() throws IOException {
        int debugPort = 9009;
        ServerSocket t = null;
        try {
            // try to use the 'standard port'
            t = new ServerSocket(debugPort);
            return Integer.toString(debugPort);
        } catch (IOException ex) {
            // log this... but don't panic
            Logger.getLogger("glassfish").fine("9009 is in use... going random");
        } finally {
            if (null != t) {
                try {
                    t.close();
                } catch (IOException ioe) {
                }
            }
        }
        try {
            // try to find a different port... if this fails,
            //    it is a great time to panic.
            t = new ServerSocket(0);
            debugPort = t.getLocalPort();
            return Integer.toString(debugPort);
        } finally {
            if (null != t) {
                try {
                    t.close();
                } catch (IOException ioe) {
                }
            }
        }
    }

    private Process createProcess() throws ProcessCreationException {
        StartupArgs args = createProcessDescriptor();
        // JDK checks and Java VM process startup were moved to GF Tooling SDK.
        ResultProcess process = ServerTasks.startServer(instance, args);
        if (process.getState() != TaskState.COMPLETED) {
            throw new ProcessCreationException(null, "MSG_START_SERVER_FAILED_PD", instanceName);
        }
        return process.getValue().getProcess();
    }

    private File getDomainFolder() {
        return new File(instance.getDomainsRoot() + File.separatorChar + getDomainName());
    }

    private String getDomainName() {
        return instance.getProperty(GlassfishModule.DOMAIN_NAME_ATTR);
    }

    private boolean upgradeFailed() {
        // get server install version
        File glassfishDir = new File(instance.getGlassfishRoot());
        int installVersion = ServerDetails.getVersionFromInstallDirectory(glassfishDir);

        if (installVersion < 0) {
            return false;  // no upgrade attempted, so it DID NOT fail.
        }
        // get domain.xml 'version'
        File domainXmlFile = new File(getDomainFolder(), "config" + File.separator + "domain.xml"); // NOI18N
        int domainVersion = ServerDetails.getVersionFromDomainXml(domainXmlFile);

        if (domainVersion < 0) {
            return false;  // no upgrade attempted, so it DID NOT fail.
        }
        if (domainVersion / 10 < installVersion / 10 && domainVersion < 310) {
            return executeUpgradeProcess() != 0;
        }
        return false;
    }

    private int executeUpgradeProcess() {
        int retVal = -1;
        File asadmin = findFirstExecutableFile(new File(instance.getGlassfishRoot()), "asadmin", "bin");
        if (null == asadmin) {
            return retVal;
        }
        NbProcessDescriptor upgrader = new NbProcessDescriptor(asadmin.getAbsolutePath(),
                "start-domain --upgrade --domaindir " + Util.quote(instance.getDomainsRoot()) + " " + // NOI18N
                instance.getDomainName());
        try {
            Process p = upgrader.exec();
            p.waitFor();
            retVal = p.exitValue();
        } catch (InterruptedException ex) {
            LOGGER.log(Level.INFO, upgrader.toString(), ex); // NOI18N
        } catch (IOException ex) {
            LOGGER.log(Level.INFO, upgrader.toString(), ex); // NOI18N
        }
        return retVal;
    }

    // TODO : refactor and remove 'similar' methods post 7.0
    private File findFirstExecutableFile(File installRoot, String executableName, String... directories) {
        File result = null;
        if (installRoot != null && installRoot.exists()) {
            for (String dir : directories) {
                File updateCenterBin = new File(installRoot, dir); // NOI18N
                if (updateCenterBin.exists()) {
                    if (Utilities.isWindows()) {
                        File launcherPath = new File(updateCenterBin, executableName + ".exe"); // NOI18N
                        if (launcherPath.exists()) {
                            result = launcherPath;
                        } else {
                            launcherPath = new File(updateCenterBin, executableName + ".bat"); // NOI18N
                            result = (launcherPath.exists()) ? launcherPath : null;
                        }
                    } else {
                        File launcherPath = new File(updateCenterBin, executableName); // NOI18N
                        result = (launcherPath.exists()) ? launcherPath : null;
                    }
                    if (null != result) {
                        break;
                    }
                }
            }
        }
        return result;
    }
    
}
