org.apache.geode.distributed.AbstractLauncher.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.geode.distributed.AbstractLauncher.java

Source

/*
 * 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.lang.StringUtils.isNotBlank;
import static org.apache.commons.lang.StringUtils.join;
import static org.apache.commons.lang.StringUtils.lowerCase;
import static org.apache.geode.distributed.ConfigurationProperties.NAME;
import static org.apache.geode.internal.lang.ClassUtils.forName;
import static org.apache.geode.internal.lang.ObjectUtils.defaultIfNull;
import static org.apache.geode.internal.lang.StringUtils.defaultString;
import static org.apache.geode.internal.lang.SystemUtils.CURRENT_DIRECTORY;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.UncheckedIOException;
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 org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.unsafe.RegisterSignalHandlerSupport;
import org.apache.geode.internal.AvailablePort;
import org.apache.geode.internal.GemFireVersion;
import org.apache.geode.internal.OSProcess;
import org.apache.geode.internal.i18n.LocalizedStrings;
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.management.internal.cli.json.GfJsonObject;

/**
 * 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 {

    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 = DistributionConfig.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);

    // TODO: use log4j logger instead of JUL
    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 org.apache.geode.internal.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 {
            properties.load(new FileReader(new File(url.toURI())));
        } 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 this.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 this.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 err, replacing any placeholder values with the
     * specified arguments on output, if debugging has been enabled.
     *
     * @param message the String value written to standard err.
     * @param args an Object array containing arguments to replace the placeholder values in the
     *        message.
     * @see java.lang.System#err
     * @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.err.printf(message, args);
            } else {
                System.err.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 err, replacing any placeholder values
     * with the specified arguments on output.
     *
     * @param message the String value written to standard err.
     * @param args an Object array containing arguments to replace the placeholder values in the
     *        message.
     * @see java.lang.System#err
     * @see #debug(String, Object...)
     */
    protected void info(final Object message, final Object... args) {
        if (args != null && args.length > 0) {
            System.err.printf(message.toString(), args);
        } else {
            System.err.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 static abstract 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";

        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();
        }

        @SuppressWarnings("unchecked")
        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 = defaultIfNull(Collections.unmodifiableList(jvmArguments),
                    Collections.<String>emptyList());
            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());
            return new GfJsonObject(map).toString();
        }

        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(this.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 this.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 this.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 defaultIfNull(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 this.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 this.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 this.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() {
            switch (getStatus()) {
            case STARTING:
                return LocalizedStrings.Launcher_ServiceStatus_STARTING_MESSAGE.toLocalizedString(getServiceName(),
                        getWorkingDirectory(), getServiceLocation(), getMemberName(), toString(getTimestamp()),
                        toString(getPid()), toString(getGemFireVersion()), toString(getJavaVersion()), getLogFile(),
                        ArgumentRedactor.redact(getJvmArguments()), toString(getClasspath()));
            case ONLINE:
                return LocalizedStrings.Launcher_ServiceStatus_RUNNING_MESSAGE.toLocalizedString(getServiceName(),
                        getWorkingDirectory(), getServiceLocation(), getMemberName(), getStatus(),
                        toString(getPid()), toDaysHoursMinutesSeconds(getUptime()), toString(getGemFireVersion()),
                        toString(getJavaVersion()), getLogFile(), ArgumentRedactor.redact(getJvmArguments()),
                        toString(getClasspath()));
            case STOPPED:
                return LocalizedStrings.Launcher_ServiceStatus_STOPPED_MESSAGE.toLocalizedString(getServiceName(),
                        getWorkingDirectory(), getServiceLocation());
            default: // NOT_RESPONDING
                return LocalizedStrings.Launcher_ServiceStatus_MESSAGE.toLocalizedString(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 defaultIfNull(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(LocalizedStrings.Launcher_Status_NOT_RESPONDING.toLocalizedString()), ONLINE(
                LocalizedStrings.Launcher_Status_ONLINE.toLocalizedString()), STARTING(
                        LocalizedStrings.Launcher_Status_STARTING.toLocalizedString()), STOPPED(
                                LocalizedStrings.Launcher_Status_STOPPED.toLocalizedString());

        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;
                }
            }

            return null;
        }

        /**
         * Gets the description of the Status enum type.
         *
         * @return a String describing the Status enum type.
         */
        public String getDescription() {
            return description;
        }

        /**
         * Gets a String representation of the Status enum type.
         *
         * @return a String representing the Status enum type.
         * @see #getDescription()
         */
        @Override
        public String toString() {
            return getDescription();
        }
    }

}