/*
 * 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.apache.geode.distributed;

import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.join;
import static org.apache.commons.lang3.StringUtils.lowerCase;
import static org.apache.geode.distributed.ConfigurationProperties.NAME;
import static org.apache.geode.internal.lang.StringUtils.defaultString;
import static org.apache.geode.internal.lang.SystemUtils.CURRENT_DIRECTORY;
import static org.apache.geode.internal.lang.utils.ClassUtils.forName;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.URISyntaxException;
import java.net.URL;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.apache.geode.annotations.Immutable;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.unsafe.RegisterSignalHandlerSupport;
import org.apache.geode.internal.GemFireVersion;
import org.apache.geode.internal.membership.utils.AvailablePort;
import org.apache.geode.internal.process.PidUnavailableException;
import org.apache.geode.internal.process.ProcessUtils;
import org.apache.geode.internal.util.ArgumentRedactor;
import org.apache.geode.internal.util.SunAPINotFoundException;
import org.apache.geode.logging.internal.OSProcess;
import org.apache.geode.util.internal.GeodeGlossary;

/**
 * The AbstractLauncher class is a base class for implementing various launchers to construct and
 * run different GemFire processes, like Cache Servers, Locators, Managers, HTTP servers and so on.
 *
 * @see java.lang.Comparable
 * @see java.lang.Runnable
 * @see org.apache.geode.lang.Identifiable
 * @since GemFire 7.0
 */
public abstract class AbstractLauncher<T extends Comparable<T>> implements Runnable {

  public static final String MEMBER_NAME_ERROR_MESSAGE =
      "The member name of the %s must be provided as an argument to the launcher, or a path to gemfire.properties must be specified, which assumes the %s member name will be set using the name property.";
  public static final String WORKING_DIRECTORY_OPTION_NOT_VALID_ERROR_MESSAGE =
      "Specifying the --dir option is not valid when starting a %s with the %sLauncher.";
  public static final String WORKING_DIRECTORY_NOT_FOUND_ERROR_MESSAGE =
      "The working directory for the %s could not be found.";

  @Immutable
  protected static final Boolean DEFAULT_FORCE = Boolean.FALSE;

  /**
   * @deprecated This timeout is no longer needed.
   */
  @Deprecated
  protected static final long READ_PID_FILE_TIMEOUT_MILLIS = 2 * 1000;

  public static final String DEFAULT_WORKING_DIRECTORY = CURRENT_DIRECTORY;

  public static final String SIGNAL_HANDLER_REGISTRATION_SYSTEM_PROPERTY =
      GeodeGlossary.GEMFIRE_PREFIX + "launcher.registerSignalHandlers";

  protected static final String OPTION_PREFIX = "-";

  private static final String SUN_SIGNAL_API_CLASS_NAME = "sun.misc.Signal";

  private volatile boolean debug;

  protected final transient AtomicBoolean running = new AtomicBoolean(false);

  /**
   * @deprecated Please use Log4J 2 instead.
   */
  @Deprecated
  protected Logger logger = Logger.getLogger(getClass().getName());

  public AbstractLauncher() {
    try {
      if (Boolean.getBoolean(SIGNAL_HANDLER_REGISTRATION_SYSTEM_PROPERTY)) {
        forName(SUN_SIGNAL_API_CLASS_NAME, new SunAPINotFoundException(
            "WARNING!!! Not running a Sun JVM.  Could not find the sun.misc.Signal class; Signal handling disabled."));
        RegisterSignalHandlerSupport.registerSignalHandlers();
      }
    } catch (SunAPINotFoundException handled) {
      info(handled.getMessage());
    }
  }

  /**
   * Asserts that the specified port is available on all network interfaces on this local system.
   *
   * @param port an integer indicating the network port to listen for client network requests.
   * @throws BindException if the network port is not available.
   */
  protected static void assertPortAvailable(final int port) throws BindException {
    assertPortAvailable(null, port);
  }

  /**
   * Asserts that the specified port is available on the specified network interface, indicated by
   * it's assigned IP address, on this local system.
   *
   * @param bindAddress an InetAddress indicating the bounded network interface to determine whether
   *        the service port is available or not.
   * @param port an integer indicating the network port to listen for client network requests.
   * @throws BindException if the network address and port are not available. Address defaults to
   *         localhost (or all network interfaces on the local system) if null.
   * @see AvailablePort
   */
  protected static void assertPortAvailable(final InetAddress bindAddress, final int port)
      throws BindException {
    if (!AvailablePort.isPortAvailable(port, AvailablePort.SOCKET, bindAddress)) {
      throw new BindException(
          String.format("Network is unreachable; port (%1$d) is not available on %2$s.", port,
              bindAddress != null ? bindAddress.getCanonicalHostName() : "localhost"));
    }
  }

  /**
   * Determines whether the specified property with name is set to a value in the referenced
   * Properties. The property is considered "set" if the String value of the property is not
   * non-null, non-empty and non-blank. Therefore, the Properties may "have" a property with name,
   * but having no value as determined by this method.
   *
   * @param properties the Properties used in determining whether the given property is set.
   * @param propertyName a String indicating the name of the property to check if set.
   * @return a boolean indicating whether the specified property with name has been given a value in
   *         the referenced Properties.
   * @see java.util.Properties
   */
  protected static boolean isSet(final Properties properties, final String propertyName) {
    return isNotBlank(properties.getProperty(propertyName));
  }

  /**
   * Loads the GemFire properties at the specified URL.
   *
   * @param url the URL to the gemfire.properties to load.
   * @return a Properties instance populated with the gemfire.properties.
   * @see java.net.URL
   */
  protected static Properties loadGemFireProperties(final URL url) {
    if (url == null) {
      return new Properties();
    }
    Properties properties = new Properties();

    try (FileReader reader = new FileReader(new File(url.toURI()))) {
      properties.load(reader);
    } catch (IOException | URISyntaxException handled) {
      // not in the file system, try the classpath
      loadGemFirePropertiesFromClassPath(properties);
    }

    return properties;
  }

  private static void loadGemFirePropertiesFromClassPath(Properties properties) {
    try {
      properties
          .load(AbstractLauncher.class.getResourceAsStream(DistributedSystem.getPropertiesFile()));
    } catch (IOException | NullPointerException handled) {
      // leave the properties empty
    }
  }

  /**
   * Determines whether the Locator launcher is in debug mode.
   *
   * @return a boolean to indicate whether the Locator launcher is in debug mode.
   * @see #setDebug(boolean)
   */
  public boolean isDebugging() {
    return debug;
  }

  /**
   * Sets the debug mode of the GemFire launcher class. This mutable property of the launcher
   * enables the user to turn the debug mode on and off programmatically.
   *
   * @param debug a boolean used to enable or disable debug mode.
   * @see #isDebugging()
   */
  public void setDebug(final boolean debug) {
    this.debug = debug;
  }

  /**
   * Determines whether the Locator referenced by this launcher is running.
   *
   * @return a boolean valued indicating if the referenced Locator is running.
   */
  public boolean isRunning() {
    return running.get();
  }

  /**
   * Creates a Properties object with configuration settings that the launcher has that should take
   * precedence over anything the user has defined in their gemfire properties file.
   *
   * @return a Properties object with GemFire properties that the launcher has defined.
   * @see #getDistributedSystemProperties(java.util.Properties)
   * @see java.util.Properties
   */
  protected Properties getDistributedSystemProperties() {
    return getDistributedSystemProperties(null);
  }

  /**
   * Creates a Properties object with configuration settings that the launcher has that should take
   * precedence over anything the user has defined in their gemfire properties file.
   *
   * @param defaults default GemFire Distributed System properties as configured in the Builder.
   * @return a Properties object with GemFire properties that the launcher has defined.
   * @see java.util.Properties
   */
  protected Properties getDistributedSystemProperties(final Properties defaults) {
    final Properties distributedSystemProperties = new Properties();

    if (defaults != null) {
      distributedSystemProperties.putAll(defaults);
    }

    if (isNotBlank(getMemberName())) {
      distributedSystemProperties.setProperty(NAME, getMemberName());
    }

    return distributedSystemProperties;
  }

  /**
   * Gets a File reference with the path to the log file for the process.
   *
   * @return a File reference to the path of the log file for the process.
   */
  protected File getLogFile() {
    return new File(getWorkingDirectory(), getLogFileName());
  }

  /**
   * Gets the fully qualified canonical path of the log file for the process.
   *
   * @return a String value indicating the canonical path of the log file for the process.
   */
  protected String getLogFileCanonicalPath() {
    try {
      return getLogFile().getCanonicalPath();
    } catch (IOException handled) {
      return getLogFileName();
    }
  }

  /**
   * Gets the name of the log file used to log information about this GemFire service.
   *
   * @return a String value indicating the name of this GemFire service's log file.
   */
  public abstract String getLogFileName();

  /**
   * Gets the name or ID of the member in the GemFire distributed system. This method prefers name
   * if specified, otherwise the ID is returned. If name was not specified to the Builder that
   * created this Launcher and this call is not in-process, then null is returned.
   *
   * @return a String value indicating the member's name if specified, otherwise the member's ID is
   *         returned if this call is made in-process, or finally, null is returned if neither name
   *         name was specified or the call is out-of-process.
   * @see #getMemberName()
   * @see #getMemberId()
   */
  public String getMember() {
    if (isNotBlank(getMemberName())) {
      return getMemberName();
    }
    if (isNotBlank(getMemberId())) {
      return getMemberId();
    }
    return null;
  }

  /**
   * Gets the ID of the member in the GemFire distributed system as determined and assigned by
   * GemFire when the member process joins the distributed system. Note, this call only works if the
   * API is used in-process.
   *
   * @return a String value indicating the ID of the member in the GemFire distributed system.
   */
  public String getMemberId() {
    final InternalDistributedSystem distributedSystem =
        InternalDistributedSystem.getConnectedInstance();
    return distributedSystem != null ? distributedSystem.getMemberId() : null;
  }

  /**
   * Gets the name of the member in the GemFire distributed system as determined by the 'name'
   * GemFire property. Note, this call only works if the API is used in-process.
   *
   * @return a String value indicating the name of the member in the GemFire distributed system.
   */
  public String getMemberName() {
    final InternalDistributedSystem distributedSystem =
        InternalDistributedSystem.getConnectedInstance();
    return distributedSystem != null ? distributedSystem.getConfig().getName() : null;
  }

  /**
   * Gets the user-specified process ID (PID) of the running GemFire service that AbstractLauncher
   * implementations can use to determine status, or stop the service.
   *
   * @return an Integer value indicating the process ID (PID) of the running GemFire service.
   */
  public abstract Integer getPid();

  /**
   * Gets the name of the GemFire service.
   *
   * @return a String indicating the name of the GemFire service.
   */
  public abstract String getServiceName();

  /**
   * Gets the working directory pathname in which the process will be run.
   *
   * @return a String value indicating the pathname of the Server's working directory.
   */
  public String getWorkingDirectory() {
    return DEFAULT_WORKING_DIRECTORY;
  }

  /**
   * Prints the specified debug message to standard output, replacing any placeholder values with
   * the
   * specified arguments on output, if debugging has been enabled.
   *
   * @param message the String value written to standard output.
   * @param args an Object array containing arguments to replace the placeholder values in the
   *        message.
   * @see java.lang.System#out
   * @see #isDebugging()
   * @see #debug(Throwable)
   * @see #info(Object, Object...)
   */
  protected void debug(final String message, final Object... args) {
    if (isDebugging()) {
      if (args != null && args.length > 0) {
        System.out.printf(message, args);
      } else {
        System.out.print(message);
      }
    }
  }

  /**
   * Prints the stack trace of the given Throwable to standard err if debugging has been enabled.
   *
   * @param t the Throwable who's stack trace is printed to standard err.
   * @see java.lang.System#err
   * @see #isDebugging()
   * @see #debug(String, Object...)
   */
  protected void debug(final Throwable t) {
    if (isDebugging()) {
      t.printStackTrace(System.err);
    }
  }

  /**
   * Prints the specified informational message to standard out, replacing any placeholder values
   * with the specified arguments on output.
   *
   * @param message the String value written to standard out.
   * @param args an Object array containing arguments to replace the placeholder values in the
   *        message.
   * @see java.lang.System#out
   * @see #debug(String, Object...)
   */
  protected void info(final Object message, final Object... args) {
    if (args != null && args.length > 0) {
      System.out.printf("%s %s", message, args);
    } else {
      System.out.print(message);
    }
  }

  /**
   * Redirects the standard out and standard err to the configured log file as specified in the
   * GemFire distributed system properties.
   *
   * @param distributedSystem the GemFire model for a distributed system.
   * @throws IOException if the standard out and err redirection was unsuccessful.
   */
  protected void redirectOutput(final DistributedSystem distributedSystem) throws IOException {
    if (distributedSystem instanceof InternalDistributedSystem) {
      OSProcess
          .redirectOutput(((InternalDistributedSystem) distributedSystem).getConfig().getLogFile());
    }
  }

  /**
   * Gets the version of GemFire currently running.
   *
   * @return a String representation of GemFire's version.
   */
  public String version() {
    return GemFireVersion.getGemFireVersion();
  }

  int identifyPid() throws PidUnavailableException {
    return ProcessUtils.identifyPid();
  }

  int identifyPidOrNot() {
    try {
      return identifyPid();
    } catch (PidUnavailableException handled) {
      return -1;
    }
  }

  boolean isPidInProcess() {
    Integer pid = getPid();
    return pid != null && pid == identifyPidOrNot();
  }

  /**
   * The ServiceState is an immutable type representing the state of the specified Locator at any
   * given moment in time. The ServiceState associates the Locator with it's state at the exact
   * moment an instance of this class is constructed.
   */
  public abstract static class ServiceState<T extends Comparable<T>> {

    protected static final String JSON_CLASSPATH = "classpath";
    protected static final String JSON_GEMFIREVERSION = "gemFireVersion";
    protected static final String JSON_HOST = "bindAddress";
    protected static final String JSON_JAVAVERSION = "javaVersion";
    protected static final String JSON_JVMARGUMENTS = "jvmArguments";
    protected static final String JSON_LOCATION = "location";
    protected static final String JSON_LOGFILE = "logFileName";
    protected static final String JSON_MEMBERNAME = "memberName";
    protected static final String JSON_PID = "pid";
    protected static final String JSON_PORT = "port";
    protected static final String JSON_STATUS = "status";
    protected static final String JSON_STATUSMESSAGE = "statusMessage";
    protected static final String JSON_TIMESTAMP = "timestamp";
    protected static final String JSON_UPTIME = "uptime";
    protected static final String JSON_WORKINGDIRECTORY = "workingDirectory";

    static final String TO_STRING_PROCESS_ID = "Process ID: ";
    static final String TO_STRING_JAVA_VERSION = "Java Version: ";
    static final String TO_STRING_LOG_FILE = "Log File: ";
    static final String TO_STRING_JVM_ARGUMENTS = "JVM Arguments: ";
    static final String TO_STRING_CLASS_PATH = "Class-Path: ";
    static final String TO_STRING_UPTIME = "Uptime: ";
    static final String TO_STRING_GEODE_VERSION = "Geode Version: ";
    static final String TO_STRING_STATUS_MESSAGE = "Status Message: ";

    private static final String DATE_TIME_FORMAT_PATTERN = "MM/dd/yyyy hh:mm a";

    private final Integer pid;

    // NOTE the mutable non-Thread safe List is guarded by a call to Collections.unmodifiableList on
    // initialization
    private final List<String> jvmArguments;

    private final Long uptime;

    private final Status status;

    private final String classpath;
    private final String gemfireVersion;
    private final String host;
    private final String javaVersion;
    private final String logFile;
    private final String memberName;
    private final String port;
    private final String serviceLocation;
    private final String statusMessage;
    private final String workingDirectory;

    private final Timestamp timestamp;

    protected static String format(final Date timestamp) {
      return timestamp == null ? ""
          : new SimpleDateFormat(DATE_TIME_FORMAT_PATTERN).format(timestamp);
    }

    protected static Integer identifyPid() {
      try {
        return ProcessUtils.identifyPid();
      } catch (PidUnavailableException handled) {
        return null;
      }
    }

    protected static String toDaysHoursMinutesSeconds(final Long milliseconds) {
      final StringBuilder buffer = new StringBuilder();

      if (milliseconds != null) {
        long millisecondsRemaining = milliseconds;

        final long days = TimeUnit.MILLISECONDS.toDays(millisecondsRemaining);

        millisecondsRemaining -= TimeUnit.DAYS.toMillis(days);

        final long hours = TimeUnit.MILLISECONDS.toHours(millisecondsRemaining);

        millisecondsRemaining -= TimeUnit.HOURS.toMillis(hours);

        final long minutes = TimeUnit.MILLISECONDS.toMinutes(millisecondsRemaining);

        millisecondsRemaining -= TimeUnit.MINUTES.toMillis(minutes);

        final long seconds = TimeUnit.MILLISECONDS.toSeconds(millisecondsRemaining);

        if (days > 0) {
          buffer.append(days).append(days > 1 ? " days " : " day ");
        }

        if (hours > 0) {
          buffer.append(hours).append(hours > 1 ? " hours " : " hour ");
        }

        if (minutes > 0) {
          buffer.append(minutes).append(minutes > 1 ? " minutes " : " minute ");
        }

        buffer.append(seconds).append(seconds == 0 || seconds > 1 ? " seconds" : " second");
      }

      return buffer.toString();
    }

    protected ServiceState(final Status status, final String statusMessage, final long timestamp,
        final String serviceLocation, final Integer pid, final Long uptime,
        final String workingDirectory, final List<String> jvmArguments, final String classpath,
        final String gemfireVersion, final String javaVersion, final String logFile,
        final String host, final String port, final String memberName) {
      assert status != null : "The status of the GemFire service cannot be null!";
      this.status = status;
      this.statusMessage = statusMessage;
      this.timestamp = new Timestamp(timestamp);
      this.serviceLocation = serviceLocation;
      this.pid = pid;
      this.uptime = uptime;
      this.workingDirectory = workingDirectory;
      this.jvmArguments = Collections.unmodifiableList(jvmArguments);
      this.classpath = classpath;
      this.gemfireVersion = gemfireVersion;
      this.javaVersion = javaVersion;
      this.logFile = logFile;
      this.host = host;
      this.port = port;
      this.memberName = memberName;
    }

    /**
     * Marshals this state object into a JSON String.
     *
     * @return a String value containing the JSON representation of this state object.
     */
    public String toJson() {
      final Map<String, Object> map = new HashMap<>();
      map.put(JSON_CLASSPATH, getClasspath());
      map.put(JSON_GEMFIREVERSION, getGemFireVersion());
      map.put(JSON_HOST, getHost());
      map.put(JSON_JAVAVERSION, getJavaVersion());
      map.put(JSON_JVMARGUMENTS, getJvmArguments());
      map.put(JSON_LOCATION, getServiceLocation());
      map.put(JSON_LOGFILE, getLogFile());
      map.put(JSON_MEMBERNAME, getMemberName());
      map.put(JSON_PID, getPid());
      map.put(JSON_PORT, getPort());
      map.put(JSON_STATUS, getStatus().getDescription());
      map.put(JSON_STATUSMESSAGE, getStatusMessage());
      map.put(JSON_TIMESTAMP, getTimestamp().getTime());
      map.put(JSON_UPTIME, getUptime());
      map.put(JSON_WORKINGDIRECTORY, getWorkingDirectory());

      String jsonStatus = null;
      try {
        jsonStatus = new ObjectMapper().writeValueAsString(map);
      } catch (JsonProcessingException e) {
        // Ignored
      }

      return jsonStatus;
    }

    public static boolean isStartingNotRespondingOrNull(final ServiceState serviceState) {
      return serviceState == null || serviceState.isStartingOrNotResponding();
    }

    public boolean isStartingOrNotResponding() {
      return Status.NOT_RESPONDING == getStatus() || Status.STARTING == getStatus();
    }

    public boolean isVmWithProcessIdRunning() {
      // note: this will use JNA if available or return false
      return ProcessUtils.isProcessAlive(getPid());
    }

    /**
     * Gets the Java classpath used when launching the GemFire service.
     *
     * @return a String value indicating the Java classpath used when launching the GemFire service.
     * @see java.lang.System#getProperty(String) with 'java.class.path'
     */
    public String getClasspath() {
      return classpath;
    }

    /**
     * Gets the version of GemFire used to launch and run the GemFire service.
     *
     * @return a String indicating the version of GemFire used in the running GemFire service.
     */
    public String getGemFireVersion() {
      return gemfireVersion;
    }

    /**
     * Gets the version of Java used to launch and run the GemFire service.
     *
     * @return a String indicating the version of the Java runtime used in the running GemFire
     *         service.
     * @see java.lang.System#getProperty(String) with 'java.verson'
     */
    public String getJavaVersion() {
      return javaVersion;
    }

    /**
     * Gets the arguments passed to the JVM process that is running the GemFire service.
     *
     * @return a List of String value each representing an argument passed to the JVM of the GemFire
     *         service.
     * @see java.lang.management.RuntimeMXBean#getInputArguments()
     */
    public List<String> getJvmArguments() {
      return jvmArguments;
    }

    /**
     * Gets GemFire member's name for the process.
     *
     * @return a String indicating the GemFire member's name for the process.
     */
    public String getMemberName() {
      return memberName;
    }

    /**
     * Gets the process ID of the running GemFire service if known, otherwise returns null.
     *
     * @return a integer value indicating the process ID (PID) of the running GemFire service, or
     *         null if the PID cannot be determined.
     */
    public Integer getPid() {
      return pid;
    }

    /**
     * Gets the location of the GemFire service (usually the host in combination with the port).
     *
     * @return a String indication the location (such as host/port) of the GemFire service.
     */
    public String getServiceLocation() {
      return serviceLocation;
    }

    /**
     * Gets the name of the GemFire service.
     *
     * @return a String indicating the name of the GemFire service.
     */
    protected abstract String getServiceName();

    /**
     * Gets the state of the GemFire service.
     *
     * @return a Status enumerated type representing the state of the GemFire service.
     * @see org.apache.geode.distributed.AbstractLauncher.Status
     */
    public Status getStatus() {
      return status;
    }

    /**
     * Gets description of the the service's current state.
     *
     * @return a String describing the service's current state.
     */
    public String getStatusMessage() {
      return statusMessage;
    }

    /**
     * The date and time the GemFire service was last in this state.
     *
     * @return a Timestamp signifying the last date and time the GemFire service was in this state.
     * @see java.sql.Timestamp
     */
    public Timestamp getTimestamp() {
      return (Timestamp) timestamp.clone();
    }

    /**
     * Gets the amount of time in milliseconds that the JVM process with the GemFire service has
     * been running.
     *
     * @return a long value indicating the number of milliseconds that the GemFire service JVM has
     *         been running.
     * @see java.lang.management.RuntimeMXBean#getUptime()
     */
    public Long getUptime() {
      return uptime;
    }

    /**
     * Gets the directory in which the GemFire service is running. This is also the location where
     * all GemFire service files (log files, the PID file, and so on) are written.
     *
     * @return a String value indicating the GemFire service's working (running) directory.
     */
    public String getWorkingDirectory() {
      return workingDirectory != null ? workingDirectory : DEFAULT_WORKING_DIRECTORY;
    }

    /**
     * Gets the path of the log file for the process.
     *
     * @return a String value indicating the path of the log file for the process.
     */
    public String getLogFile() {
      return logFile;
    }

    /**
     * Gets the host or IP address for the process and its service.
     *
     * @return a String value representing the host or IP address for the process and its service.
     */
    public String getHost() {
      return host;
    }

    /**
     * Gets the port for the process and its service.
     *
     * @return an Integer value indicating the port for the process and its service.
     */
    public String getPort() {
      return port;
    }

    /**
     * Gets a String describing the state of the GemFire service.
     *
     * @return a String describing the state of the GemFire service.
     */
    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder();
      switch (getStatus()) {
        case STARTING:
          sb.append("Starting %s in %s on %s as %s at %s").append(System.lineSeparator());
          sb.append(TO_STRING_PROCESS_ID).append("%s").append(System.lineSeparator());
          sb.append(TO_STRING_JAVA_VERSION).append("%s").append(System.lineSeparator());
          sb.append(TO_STRING_LOG_FILE).append("%s").append(System.lineSeparator());
          sb.append(TO_STRING_JVM_ARGUMENTS).append("%s").append(System.lineSeparator());
          sb.append(TO_STRING_CLASS_PATH).append("%s");
          return String.format(sb.toString(),
              getServiceName(), getWorkingDirectory(), getServiceLocation(), getMemberName(),
              toString(getTimestamp()), toString(getPid()), toString(getJavaVersion()),
              getLogFile(), ArgumentRedactor.redact(getJvmArguments()),
              toString(getClasspath()));

        case ONLINE:
          sb.append("%s in %s on %s as %s is currently %s.").append(System.lineSeparator());
          sb.append(TO_STRING_PROCESS_ID).append("%s").append(System.lineSeparator());
          sb.append(TO_STRING_UPTIME).append("%s").append(System.lineSeparator());
          sb.append(TO_STRING_GEODE_VERSION).append("%s").append(System.lineSeparator());
          sb.append(TO_STRING_JAVA_VERSION).append("%s").append(System.lineSeparator());
          sb.append(TO_STRING_LOG_FILE).append("%s").append(System.lineSeparator());
          sb.append(TO_STRING_JVM_ARGUMENTS).append("%s").append(System.lineSeparator());
          sb.append(TO_STRING_CLASS_PATH).append("%s");
          return String.format(sb.toString(),
              getServiceName(), getWorkingDirectory(), getServiceLocation(), getMemberName(),
              getStatus(), toString(getPid()), toDaysHoursMinutesSeconds(getUptime()),
              toString(getGemFireVersion()), toString(getJavaVersion()), getLogFile(),
              ArgumentRedactor.redact(getJvmArguments()), toString(getClasspath()));

        case STOPPED:
          sb.append("%s in %s on %s has been requested to stop.");
          return String.format(sb.toString(),
              getServiceName(), getWorkingDirectory(), getServiceLocation());

        default: // NOT_RESPONDING
          sb.append("%s in %s on %s is currently %s.");
          return String.format(sb.toString(), getServiceName(),
              getWorkingDirectory(), getServiceLocation(), getStatus());
      }
    }

    // a timestamp (date/time) formatted as MM/dd/yyyy hh:mm a
    protected String toString(final Date dateTime) {
      return format(dateTime);
    }

    // the value of a Number as a String, or "" if null
    protected String toString(final Number value) {
      return defaultString(value);
    }

    // a String concatenation of all values separated by " "
    protected String toString(final Object... values) {
      return values == null ? "" : join(values, " ");
    }

    // the value of the String, or "" if value is null
    protected String toString(final String value) {
      return value != null ? value : "";
    }
  }

  /**
   * The Status enumerated type represents the various lifecycle states of a GemFire service (such
   * as a Cache Server, a Locator or a Manager).
   */
  public enum Status {
    NOT_RESPONDING("not responding"),
    ONLINE("online"),
    STARTING("starting"),
    STOPPED("stopped");

    private final String description;

    Status(final String description) {
      assert isNotBlank(description) : "The Status description must be specified!";
      this.description = lowerCase(description);
    }

    /**
     * Looks up the Status enum type by description. The lookup operation is case-insensitive.
     *
     * @param description a String value describing the Locator's status.
     * @return a Status enumerated type matching the description.
     */
    public static Status valueOfDescription(final String description) {
      for (Status status : values()) {
        if (status.getDescription().equalsIgnoreCase(description)) {
          return status;
        }
      }

