/*
 * 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.brooklyn.cli;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.Console;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import org.apache.brooklyn.api.catalog.BrooklynCatalog;
import org.apache.brooklyn.api.entity.Application;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
import org.apache.brooklyn.cli.CloudExplorer.BlobstoreGetBlobCommand;
import org.apache.brooklyn.cli.CloudExplorer.BlobstoreListContainerCommand;
import org.apache.brooklyn.cli.CloudExplorer.BlobstoreListContainersCommand;
import org.apache.brooklyn.cli.CloudExplorer.ComputeDefaultTemplateCommand;
import org.apache.brooklyn.cli.CloudExplorer.ComputeGetImageCommand;
import org.apache.brooklyn.cli.CloudExplorer.ComputeListHardwareProfilesCommand;
import org.apache.brooklyn.cli.CloudExplorer.ComputeListImagesCommand;
import org.apache.brooklyn.cli.CloudExplorer.ComputeListInstancesCommand;
import org.apache.brooklyn.cli.CloudExplorer.ComputeTerminateInstancesCommand;
import org.apache.brooklyn.cli.ItemLister.ListAllCommand;
import org.apache.brooklyn.core.BrooklynVersion;
import org.apache.brooklyn.core.catalog.internal.CatalogInitialization;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.core.mgmt.ShutdownHandler;
import org.apache.brooklyn.core.mgmt.ha.OsgiManager;
import org.apache.brooklyn.core.mgmt.persist.BrooklynPersistenceUtils;
import org.apache.brooklyn.core.mgmt.persist.PersistMode;
import org.apache.brooklyn.core.mgmt.rebind.transformer.CompoundTransformer;
import org.apache.brooklyn.entity.stock.BasicApplication;
import org.apache.brooklyn.launcher.BrooklynLauncher;
import org.apache.brooklyn.launcher.BrooklynServerDetails;
import org.apache.brooklyn.launcher.config.StopWhichAppsOnShutdown;
import org.apache.brooklyn.rest.security.PasswordHasher;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.FatalConfigurationRuntimeException;
import org.apache.brooklyn.util.exceptions.FatalRuntimeException;
import org.apache.brooklyn.util.exceptions.UserFacingException;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.javalang.Enums;
import org.apache.brooklyn.util.javalang.Reflections;
import org.apache.brooklyn.util.net.Networking;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects.ToStringHelper;
import com.google.common.collect.ImmutableList;

import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyShell;
import io.airlift.airline.Cli;
import io.airlift.airline.Cli.CliBuilder;
import io.airlift.airline.Command;
import io.airlift.airline.Option;

/**
 * This class is the primary CLI for brooklyn.
 * Run with the `help` argument for help.
 * <p>
 * This class is designed for subclassing, with subclasses typically:
 * <li> providing their own static {@link #main(String...)} (of course) which need simply invoke 
 *      {@link #execCli(String[])} with the arguments 
 * <li> returning their CLI name (e.g. "start.sh") in an overridden {@link #cliScriptName()}
 * <li> providing an overridden {@link LaunchCommand} via {@link #cliLaunchCommand()} if desired
 * <li> providing any other CLI customisations by overriding {@link #cliBuilder()}
 *      (typically calling the parent and then customizing the builder)
 */
public class Main extends AbstractMain {

    private static final Logger log = LoggerFactory.getLogger(Main.class);

    public static void main(String... args) {
        log.debug("Launching Brooklyn via CLI, with "+Arrays.toString(args));
        BrooklynVersion.INSTANCE.logSummary();
        new Main().execCli(args);
    }

    @Command(name = "generate-password", description = "Generates a hashed web-console password")
    public static class GeneratePasswordCommand extends BrooklynCommandCollectingArgs {

        @Option(name = { "--user" }, title = "username", required = true)
        public String user;

        @Option(name = { "--stdin" }, title = "read password from stdin, instead of console", 
                description = "Before using stdin, read http://stackoverflow.com/a/715681/1393883 for discussion of security!")
        public boolean useStdin;

        @Override
        public Void call() throws Exception {
            checkCanReadPassword();
            
            System.out.print("Enter password: ");
            System.out.flush();
            String password = readPassword();
            if (Strings.isBlank(password)) {
                throw new UserFacingException("Password must not be blank; aborting");
            }
            
            System.out.print("Re-enter password: ");
            System.out.flush();
            String password2 = readPassword();
            if (!password.equals(password2)) {
                throw new UserFacingException("Passwords did not match; aborting");
            }

            String salt = Identifiers.makeRandomId(4);
            String sha256password = PasswordHasher.sha256(salt, new String(password));
            
            System.out.println();
            System.out.println("Please add the following to your brooklyn.properties:");
            System.out.println();
            System.out.println("brooklyn.webconsole.security.users="+user);
            System.out.println("brooklyn.webconsole.security.user."+user+".salt="+salt);
            System.out.println("brooklyn.webconsole.security.user."+user+".sha256="+sha256password);

            return null;
        }
        
        private void checkCanReadPassword() {
            if (useStdin) {
                // yes; always
            } else {
                Console console = System.console();
                if (console == null) {
                    throw new FatalConfigurationRuntimeException("No console; cannot get password securely; aborting");
                }
            }
        }
        
        private String readPassword() throws IOException {
            if (useStdin) {
                return readLine(System.in);
            } else {
                return new String(System.console().readPassword());
            }
        }
        
        private String readLine(InputStream in) throws IOException {
            StringBuilder result = new StringBuilder();
            char c;
            while ((c = (char)in.read()) != '\n') {
                result.append(c);
            }
            return result.toString();
        }
    }
    
    @Command(name = "launch", description = "Starts a server, optionally with applications")
    public static class LaunchCommand extends BrooklynCommandWithSystemDefines {

        @Option(name = { "--localBrooklynProperties" }, title = "local brooklyn.properties file",
                description = "Load the given properties file, specific to this launch (appending to and overriding global properties)")
        public String localBrooklynProperties;

        @Option(name = { "--noGlobalBrooklynProperties" }, title = "do not use any global brooklyn.properties file found",
            description = "Do not use the default global brooklyn.properties file found")
        public boolean noGlobalBrooklynProperties;

        @Option(name = { "-a", "--app" }, title = "application class or file",
                description = "The Application to start. " +
                        "For example, my.AppName or file://my/app.yaml" 
                        + " -- note that a BROOKLYN_CLASSPATH "
                        + "environment variable may be required to load classes from other locations")
        public String app;

        @Beta
        @Option(name = { "-s", "--script" }, title = "script URI",
                description = "DEPRECATED. URI for a Groovy script to parse and load." +
                        " This script will run before starting the app.")
        public String script = null;

        @Option(name = { "-l", "--location", "--locations" }, title = "location list",
                description = "Specifies the locations where the application will be launched. " +
                        "You can specify more than one location as a comma-separated list of values " +
                        "(or as a JSON array, if the values are complex)")
        public String locations;

        @Option(name = { "--catalogInitial" }, title = "catalog initial bom URI",
            description = "Specifies a catalog.bom URI to be used to populate the initial catalog, "
                + "loaded on first run, or when persistence is off/empty or the catalog is reset")
        public String catalogInitial;

        @Option(name = { "-p", "--port" }, title = "port number",
                description = "Use this port for the brooklyn management web console and REST API; "
                    + "default is 8081+ for http, 8443+ for https.")
        public String port;

        @Option(name = { "--https" },
            description = "Launch the web console on https")
        public boolean useHttps;
        
        @Option(name = { "-nc", "--noConsole" },
                description = "Do not start the web console or REST API")
        public boolean noConsole;

        @Option(name = { "-b", "--bindAddress" },
                description = "Specifies the IP address of the NIC to bind the Brooklyn Management Console to")
        public String bindAddress = null;

        @Option(name = { "-pa", "--publicAddress" },
                description = "Specifies the IP address or hostname that the Brooklyn Management Console will be available on")
        public String publicAddress = null;

        @Option(name = { "--noConsoleSecurity" },
                description = "Whether to disable authentication and security filters for the web console (for use when debugging on a secure network or bound to localhost)")
        public boolean noConsoleSecurity;

        @Option(name = { "--startupContinueOnWebErrors" },
            description = "Continue on web subsystem failures during startup "
                + "(default is to abort if the web API fails to start, as management access is not normally possible)")
        public boolean startupContinueOnWebErrors;

        @Option(name = { "--startupFailOnPersistenceErrors" },
            description = "Fail on persistence/HA subsystem failures during startup "
                + "(default is to continue, so errors can be viewed via the API)")
        public boolean startupFailOnPersistenceErrors;

        @Option(name = { "--startupFailOnCatalogErrors" },
            description = "Fail on catalog subsystem failures during startup "
                + "(default is to continue, so errors can be viewed via the API)")
        public boolean startupFailOnCatalogErrors;

        @Option(name = { "--startupFailOnManagedAppsErrors" },
            description = "Fail startup on errors deploying of managed apps specified via the command line "
                + "(default is to continue, so errors can be viewed via the API)")
        public boolean startupFailOnManagedAppsErrors;

        @Beta
        @Option(name = { "--startBrooklynNode" },
                description = "Start a BrooklynNode entity representing this Brooklyn instance")
        public boolean startBrooklynNode;

        // Note in some cases, you can get java.util.concurrent.RejectedExecutionException
        // if shutdown is not co-ordinated, looks like: {@linktourl https://gist.github.com/47066f72d6f6f79b953e}
        @Beta
        @Option(name = { "-sk", "--stopOnKeyPress" },
                description = "Shutdown immediately on user text entry after startup (useful for debugging and demos)")
        public boolean stopOnKeyPress;

        final static String STOP_WHICH_APPS_ON_SHUTDOWN = "--stopOnShutdown";
        protected final static String STOP_ALL = "all";
        protected final static String STOP_ALL_IF_NOT_PERSISTED = "allIfNotPersisted";
        protected final static String STOP_NONE = "none";
        protected final static String STOP_THESE = "these";        
        protected final static String STOP_THESE_IF_NOT_PERSISTED = "theseIfNotPersisted";
        static { Enums.checkAllEnumeratedIgnoreCase(StopWhichAppsOnShutdown.class, STOP_ALL, STOP_ALL_IF_NOT_PERSISTED, STOP_NONE, STOP_THESE, STOP_THESE_IF_NOT_PERSISTED); }
        
        @Option(name = { STOP_WHICH_APPS_ON_SHUTDOWN },
            allowedValues = { STOP_ALL, STOP_ALL_IF_NOT_PERSISTED, STOP_NONE, STOP_THESE, STOP_THESE_IF_NOT_PERSISTED },
            description = "Which managed applications to stop on shutdown. Possible values are:\n"+
                "all: stop all apps\n"+
                "none: leave all apps running\n"+
                "these: stop the apps explicitly started on this command line, but leave others started subsequently running\n"+
                "theseIfNotPersisted: stop the apps started on this command line IF persistence is not enabled, otherwise leave all running\n"+
                "allIfNotPersisted: stop all apps IF persistence is not enabled, otherwise leave all running")
        public String stopWhichAppsOnShutdown = STOP_THESE_IF_NOT_PERSISTED;

        @Option(name = { "--exitAndLeaveAppsRunningAfterStarting" },
                description = "Once the application to start (from --app) is running exit the process, leaving any entities running. "
                    + "Can be used in combination with --persist auto --persistenceDir <custom folder location> to attach to the running app at a later time.")
        public boolean exitAndLeaveAppsRunningAfterStarting;

        final static String PERSIST_OPTION = "--persist";
        protected final static String PERSIST_OPTION_DISABLED = "disabled";
        protected final static String PERSIST_OPTION_AUTO = "auto";
        protected final static String PERSIST_OPTION_REBIND = "rebind";
        protected final static String PERSIST_OPTION_CLEAN = "clean";
        static { Enums.checkAllEnumeratedIgnoreCase(PersistMode.class, PERSIST_OPTION_DISABLED, PERSIST_OPTION_AUTO, PERSIST_OPTION_REBIND, PERSIST_OPTION_CLEAN); }
        
        // TODO currently defaults to disabled; want it to default to on, when we're ready
        // TODO how to force a line-split per option?!
        //      Looks like java.io.airlift.airline.UsagePrinter is splitting the description by word, and
        //      wrapping it automatically.
        //      See https://github.com/airlift/airline/issues/30
        @Option(name = { PERSIST_OPTION }, 
                allowedValues = { PERSIST_OPTION_DISABLED, PERSIST_OPTION_AUTO, PERSIST_OPTION_REBIND, PERSIST_OPTION_CLEAN },
                title = "persistence mode",
                description =
                        "The persistence mode. Possible values are: \n"+
                        "disabled: will not read or persist any state; \n"+
                        "auto: will rebind to any existing state, or start up fresh if no state; \n"+
                        "rebind: will rebind to the existing state, or fail if no state available; \n"+
                        "clean: will start up fresh (removing any existing state)")
        public String persist = PERSIST_OPTION_DISABLED;

        @Option(name = { "--persistenceDir" }, title = "persistence dir",
                description = "The directory to read/write persisted state (or container name if using an object store)")
        public String persistenceDir;

        @Option(name = { "--persistenceLocation" }, title = "persistence location",
            description = "The location spec for an object store to read/write persisted state")
        public String persistenceLocation;

        final static String HA_OPTION = "--highAvailability";
        protected final static String HA_OPTION_DISABLED = "disabled";
        protected final static String HA_OPTION_AUTO = "auto";
        protected final static String HA_OPTION_MASTER = "master";
        protected final static String HA_OPTION_STANDBY = "standby";
        protected final static String HA_OPTION_HOT_STANDBY = "hot_standby";
        protected final static String HA_OPTION_HOT_BACKUP = "hot_backup";
        static { Enums.checkAllEnumeratedIgnoreCase(HighAvailabilityMode.class, HA_OPTION_AUTO, HA_OPTION_DISABLED, HA_OPTION_MASTER, HA_OPTION_STANDBY, HA_OPTION_HOT_STANDBY, HA_OPTION_HOT_BACKUP); }
        
        @Option(name = { HA_OPTION }, allowedValues = { HA_OPTION_DISABLED, HA_OPTION_AUTO, HA_OPTION_MASTER, HA_OPTION_STANDBY, HA_OPTION_HOT_STANDBY, HA_OPTION_HOT_BACKUP },
                title = "high availability mode",
                description =
                        "The high availability mode. Possible values are: \n"+
                        "disabled: management node works in isolation - will not cooperate with any other standby/master nodes in management plane; \n"+
                        "auto: will look for other management nodes, and will allocate itself as standby or master based on other nodes' states; \n"+
                        "master: will startup as master - if there is already a master then fails immediately; \n"+
                        "standby: will start up as lukewarm standby with no state - if there is not already a master then fails immediately, "
                        + "and if there is a master which subsequently fails, this node can promote itself; \n"+
                        "hot_standby: will start up as hot standby in read-only mode - if there is not already a master then fails immediately, "
                        + "and if there is a master which subseuqently fails, this node can promote itself; \n"+
                        "hot_backup: will start up as hot backup in read-only mode - no master is required, and this node will not become a master"
                        )
        public String highAvailability = HA_OPTION_AUTO;

        @VisibleForTesting
        protected ManagementContext explicitManagementContext;
        
        @Override
        public Void call() throws Exception {
            super.call();
            
            // Configure launcher
            BrooklynLauncher launcher;
            AppShutdownHandler shutdownHandler = new AppShutdownHandler();
            failIfArguments();
            try {
                if (log.isDebugEnabled()) log.debug("Invoked launch command {}", this);
                
                if (!quiet) {
                    stdout.println(banner);
                    stdout.println(productOneLineSummary);
                    stdout.println();
                }
    
                if (verbose) {
                    if (app != null) {
                        stdout.println("Launching brooklyn app: " + app + " in " + locations);
                    } else {
                        stdout.println("Launching brooklyn server (no app)");
                    }
                }
    
                PersistMode persistMode = computePersistMode();
                HighAvailabilityMode highAvailabilityMode = computeHighAvailabilityMode(persistMode);
                
                StopWhichAppsOnShutdown stopWhichAppsOnShutdownMode = computeStopWhichAppsOnShutdown();
                
                computeLocations();
                
                ResourceUtils utils = ResourceUtils.create(this);
                GroovyClassLoader loader = new GroovyClassLoader(getClass().getClassLoader());
    
                // First, run a setup script if the user has provided one
                if (script != null) {
                    log.warn("Use of --script for groovy-script execution is deprecated");
                    execGroovyScript(utils, loader, script);
                }
    
                launcher = createLauncher();

                CatalogInitialization catInit = new CatalogInitialization(catalogInitial);
                catInit.setFailOnStartupErrors(startupFailOnCatalogErrors);
                launcher.catalogInitialization(catInit);
                
                launcher.persistMode(persistMode);
                launcher.persistenceDir(persistenceDir);
                launcher.persistenceLocation(persistenceLocation);

                launcher.highAvailabilityMode(highAvailabilityMode);

                launcher.stopWhichAppsOnShutdown(stopWhichAppsOnShutdownMode);
                launcher.shutdownHandler(shutdownHandler);
                
                computeAndSetApp(launcher, utils, loader);
                
                customize(launcher);
                
            } catch (FatalConfigurationRuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new FatalConfigurationRuntimeException("Fatal error configuring Brooklyn launch: "+e.getMessage(), e);
            }
            
            // Launch server
            try {
                launcher.start();
            } catch (FatalRuntimeException e) {
                // rely on caller logging this propagated exception
                throw e;
            } catch (Exception e) {
                // for other exceptions we log it, possibly redundantly but better too much than too little
                Exceptions.propagateIfFatal(e);
                log.error("Error launching brooklyn: "+Exceptions.collapseText(e), e);
                try {
                    launcher.terminate();
                } catch (Exception e2) {
                    log.warn("Subsequent error during termination: "+e2);
                    log.debug("Details of subsequent error during termination: "+e2, e2);
                }
                Exceptions.propagate(e);
            }
            
            BrooklynServerDetails server = launcher.getServerDetails();
            ManagementContext mgmt = server.getManagementContext();
            
            if (verbose) {
                Entities.dumpInfo(launcher.getApplications());
            }
            
            if (!exitAndLeaveAppsRunningAfterStarting) {
                waitAfterLaunch(mgmt, shutdownHandler);
            }

            // do not shutdown servers here here -- 
            // the BrooklynShutdownHookJob will invoke that and others on System.exit()
            // which happens immediately after.
            // might be nice to do it explicitly here, 
            // but the server shutdown process has some special "shutdown apps" options
            // so we'd want to refactor BrooklynShutdownHookJob to share code
            
            return null;
        }

        /** can be overridden by subclasses which need to customize the launcher and/or management */
        protected void customize(BrooklynLauncher launcher) {
        }
        
        protected void computeLocations() {
            boolean hasLocations = !Strings.isBlank(locations);
            if (app != null) {
                if (hasLocations && isYamlApp()) {
                    log.info("YAML app combined with command line locations; YAML locations will take precedence; this behaviour may change in subsequent versions");
                } else if (!hasLocations && isYamlApp()) {
                    log.info("No locations supplied; defaulting to locations defined in YAML (if any)");
                } else if (!hasLocations) {
                    log.info("No locations supplied; starting with no locations");
                }
            } else if (hasLocations) {
                log.error("Locations specified without any applications; ignoring locations");
            }
        }

        protected boolean isYamlApp() {
            return app != null && app.endsWith(".yaml");
        }

        protected PersistMode computePersistMode() {
            Maybe<PersistMode> persistMode = Enums.valueOfIgnoreCase(PersistMode.class, persist);
            if (!persistMode.isPresent()) {
                if (Strings.isBlank(persist)) {
                    throw new FatalConfigurationRuntimeException("Persist mode must not be blank");
                } else {
                    throw new FatalConfigurationRuntimeException("Illegal persist setting: "+persist);
                }
            }
   
            if (persistMode.get() == PersistMode.DISABLED) {
                if (Strings.isNonBlank(persistenceDir))
                    throw new FatalConfigurationRuntimeException("Cannot specify persistenceDir when persist is disabled");
                if (Strings.isNonBlank(persistenceLocation))
                    throw new FatalConfigurationRuntimeException("Cannot specify persistenceLocation when persist is disabled");
            }
            return persistMode.get();
        }

        protected HighAvailabilityMode computeHighAvailabilityMode(PersistMode persistMode) {
            Maybe<HighAvailabilityMode> highAvailabilityMode = Enums.valueOfIgnoreCase(HighAvailabilityMode.class, highAvailability);
            if (!highAvailabilityMode.isPresent()) {
                if (Strings.isBlank(highAvailability)) {
                    throw new FatalConfigurationRuntimeException("High availability mode must not be blank");
                } else {
                    throw new FatalConfigurationRuntimeException("Illegal highAvailability setting: "+highAvailability);
                }
            }
   
            if (highAvailabilityMode.get() != HighAvailabilityMode.DISABLED) {
                if (persistMode == PersistMode.DISABLED) {
                    if (highAvailabilityMode.get() == HighAvailabilityMode.AUTO)
                        return HighAvailabilityMode.DISABLED;
                    throw new FatalConfigurationRuntimeException("Cannot specify highAvailability when persistence is disabled");
                } else if (persistMode == PersistMode.CLEAN && 
                        (highAvailabilityMode.get() == HighAvailabilityMode.STANDBY 
                        || highAvailabilityMode.get() == HighAvailabilityMode.HOT_STANDBY
                        || highAvailabilityMode.get() == HighAvailabilityMode.HOT_BACKUP)) {
                    throw new FatalConfigurationRuntimeException("Cannot specify highAvailability "+highAvailabilityMode.get()+" when persistence is CLEAN");
                }
            }
            return highAvailabilityMode.get();
        }
        
        protected StopWhichAppsOnShutdown computeStopWhichAppsOnShutdown() {
            boolean isDefault = STOP_THESE_IF_NOT_PERSISTED.equals(stopWhichAppsOnShutdown);
            if (exitAndLeaveAppsRunningAfterStarting && isDefault) {
                return StopWhichAppsOnShutdown.NONE;
            } else {
                return Enums.valueOfIgnoreCase(StopWhichAppsOnShutdown.class, stopWhichAppsOnShutdown).get();
            }
        }
        
        @VisibleForTesting
        /** forces the launcher to use the given management context, when programmatically invoked;
         * mainly used when testing to inject a safe (and fast) mgmt context */
        public void useManagementContext(ManagementContext mgmt) {
            explicitManagementContext = mgmt;
        }

        protected BrooklynLauncher createLauncher() {
            BrooklynLauncher launcher;
            launcher = BrooklynLauncher.newInstance();
            launcher.localBrooklynPropertiesFile(localBrooklynProperties)
                    .ignorePersistenceErrors(!startupFailOnPersistenceErrors)
                    .ignoreCatalogErrors(!startupFailOnCatalogErrors)
                    .ignoreWebErrors(startupContinueOnWebErrors)
                    .ignoreAppErrors(!startupFailOnManagedAppsErrors)
                    .locations(Strings.isBlank(locations) ? ImmutableList.<String>of() : JavaStringEscapes.unwrapJsonishListStringIfPossible(locations));
            
            launcher.restServer(!noConsole);
            if (useHttps) {
                // true sets it; false (not set) leaves it blank and falls back to config key
                // (no way currently to override config key, but that could be added)
                launcher.restServerHttps(useHttps);
            }
            launcher.restServerPort(port);
            
            if (noGlobalBrooklynProperties) {
                log.debug("Configuring to disable global brooklyn.properties");
                launcher.globalBrooklynPropertiesFile(null);
            }
            if (noConsoleSecurity) {
                log.info("Configuring to disable console security");
                launcher.installSecurityFilter(false);
            }
            if (startBrooklynNode) {
                log.info("Configuring BrooklynNode entity startup");
                launcher.startBrooklynNode(true);
            }
            if (Strings.isNonEmpty(bindAddress)) {
                log.debug("Configuring bind address as "+bindAddress);
                launcher.bindAddress(Networking.getInetAddressWithFixedName(bindAddress));
            }
            if (Strings.isNonEmpty(publicAddress)) {
                log.debug("Configuring public address as "+publicAddress);
                launcher.publicAddress(Networking.getInetAddressWithFixedName(publicAddress));
            }
            if (explicitManagementContext!=null) {
                log.debug("Configuring explicit management context "+explicitManagementContext);
                launcher.managementContext(explicitManagementContext);
            }
            return launcher;
        }

        /**
         * method intended for subclassing, to add custom items to the catalog.
         * 
         * @deprecated since 1.0.0; no longer supported; does nothing - subclasses should not try to extend it!
         */
        protected final void populateCatalog(BrooklynCatalog catalog) {
            // nothing else added here
        }

        /** convenience for subclasses to specify that an app should run,
         * throwing the right (caught) error if another app has already been specified */
        protected void setAppToLaunch(String className) {
            if (app!=null) {
                if (app.equals(className)) return;
                throw new FatalConfigurationRuntimeException("Cannot specify app '"+className+"' when '"+app+"' is already specified; "
                    + "remove one or more conflicting CLI arguments.");
            }
            app = className;
        }
        
        protected void computeAndSetApp(BrooklynLauncher launcher, ResourceUtils utils, GroovyClassLoader loader)
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
            if (app != null) {
                // Create the instance of the brooklyn app
                log.debug("Loading the user's application: {}", app);
   
                if (isYamlApp()) {
                    log.debug("Loading application as YAML spec: {}", app);
                    String content = utils.getResourceAsString(app);
                    launcher.application(content);
                } else {
                    EntitySpec<? extends Application> appSpec = loadApplicationFromClasspathOrParse(utils, loader, app);
                    launcher.application(appSpec);
                }
            }
        }
        
        protected void waitAfterLaunch(ManagementContext ctx, AppShutdownHandler shutdownHandler) throws IOException {
            if (stopOnKeyPress) {
                // Wait for the user to type a key
                log.info("Server started. Press return to stop.");
                // Read in another thread so we can use timeout on the wait.
                Task<Void> readTask = ctx.getExecutionManager().submit(new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        stdin.read();
                        return null;
                    }
                });
                while (!shutdownHandler.isRequested()) {
                    try {
                        readTask.get(Duration.ONE_SECOND);
                        break;
                    } catch (TimeoutException e) {
                        //check if there's a shutdown request
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        throw Exceptions.propagate(e);
                    } catch (ExecutionException e) {
                        throw Exceptions.propagate(e);
                    }
                }
                log.info("Shutting down applications.");
                stopAllApps(ctx.getApplications());
            } else {
                // Block forever so that Brooklyn doesn't exit (until someone does cntrl-c or kill)
                log.info("Launched Brooklyn; will now block until shutdown command received via GUI/API (recommended) or process interrupt.");
                shutdownHandler.waitOnShutdownRequest();
            }
        }

        protected void execGroovyScript(ResourceUtils utils, GroovyClassLoader loader, String script) {
            log.debug("Running the user provided script: {}", script);
            String content = utils.getResourceAsString(script);
            GroovyShell shell = new GroovyShell(loader);
            shell.evaluate(content);
        }

        /**
         * Helper method that gets an instance of a brooklyn {@link EntitySpec}.
         * Guaranteed to be non-null result (throwing exception if app not appropriate).
         * 
         * @throws FatalConfigurationRuntimeException if class is not an {@link Application} or {@link Entity}
         */
        @SuppressWarnings("unchecked")
        protected EntitySpec<? extends Application> loadApplicationFromClasspathOrParse(ResourceUtils utils, GroovyClassLoader loader, String app)
                throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
            
            Class<?> clazz;
            log.debug("Loading application as class on classpath: {}", app);
            try {
                clazz = loader.loadClass(app, true, false);
            } catch (ClassNotFoundException cnfe) { // Not a class on the classpath
                throw new IllegalStateException("Unable to load app class '"+app+"'", cnfe);
            }
            
            if (Application.class.isAssignableFrom(clazz)) {
                if (clazz.isInterface()) {
                    return EntitySpec.create((Class<? extends Application>)clazz);
                } else {
                    return EntitySpec.create(Application.class)
                            .impl((Class<? extends Application>) clazz)
                            .additionalInterfaces(Reflections.getAllInterfaces(clazz));
                }
                
            } else if (Entity.class.isAssignableFrom(clazz)) {
                // TODO Should we really accept any entity type, and just wrap it in an app? That's not documented!
                EntitySpec<?> childSpec;
                if (clazz.isInterface()) {
                    childSpec = EntitySpec.create((Class<? extends Entity>)clazz);
                } else {
                    childSpec = EntitySpec.create(Entity.class)
                            .impl((Class<? extends Application>) clazz)
                            .additionalInterfaces(Reflections.getAllInterfaces(clazz));
                }
                return EntitySpec.create(BasicApplication.class).child(childSpec);
                
            } else {
                throw new FatalConfigurationRuntimeException("Application class "+clazz+" must be an Application or Entity");
            }
        }

        @VisibleForTesting
        protected void stopAllApps(Collection<? extends Application> applications) {
            for (Application application : applications) {
                try {
                    if (application instanceof Startable) {
                        ((Startable)application).stop();
                    }
                } catch (Exception e) {
                    log.error("Error stopping "+application+": "+e, e);
                }
            }
        }
        
        @Override
        public ToStringHelper string() {
            return super.string()
                    .add("app", app)
                    .add("script", script)
                    .add("location", locations)
                    .add("port", port)
                    .add("bindAddress", bindAddress)
                    .add("noConsole", noConsole)
                    .add("noConsoleSecurity", noConsoleSecurity)
                    .add("startupFailOnPersistenceErrors", startupFailOnPersistenceErrors)
                    .add("startupFailsOnCatalogErrors", startupFailOnCatalogErrors)
                    .add("startupContinueOnWebErrors", startupContinueOnWebErrors)
                    .add("startupFailOnManagedAppsErrors", startupFailOnManagedAppsErrors)
                    .add("catalogInitial", catalogInitial)
                    .add("stopWhichAppsOnShutdown", stopWhichAppsOnShutdown)
                    .add("stopOnKeyPress", stopOnKeyPress)
                    .add("localBrooklynProperties", localBrooklynProperties)
                    .add("persist", persist)
                    .add("persistenceLocation", persistenceLocation)
                    .add("persistenceDir", persistenceDir)
                    .add("highAvailability", highAvailability)
                    .add("exitAndLeaveAppsRunningAfterStarting", exitAndLeaveAppsRunningAfterStarting);
        }
    }

    @Command(name = "clean-orphaned-state", description = "Removes existing orphaned persisted state (e.g. locations, policies, enrichers and feeds)")
    public static class CleanOrphanedStateCommand extends BrooklynCommandCollectingArgs {

        @Option(name = { "--localBrooklynProperties" }, title = "local brooklyn.properties file",
                description = "local brooklyn.properties file, specific to this launch (appending to and overriding global properties)")
        public String localBrooklynProperties;

        @Option(name = { "--persistenceDir" }, title = "persistence dir",
                description = "The directory to read persisted state (or container name if using an object store)")
        public String persistenceDir;

        @Option(name = { "--persistenceLocation" }, title = "persistence location",
                description = "The location spec for an object store to read persisted state")
        public String persistenceLocation;

        @Option(name = { "--destinationDir" }, required = true, title = "destination dir",
                description = "The directory to copy persistence data to, with orphaned state removed")
        public String destinationDir;

        @Option(name = { "--destinationLocation" }, title = "persistence location",
                description = "The location spec for an object store to copy data to")
        public String destinationLocation;

        @Override
        public Void call() throws Exception {
            checkNotNull(destinationDir, "destinationDir");

            BrooklynLauncher launcher;
            failIfArguments();

            try {
                log.info("Retrieving and copying persisted state to " + destinationDir + " with orphaned state deleted");

                PersistMode persistMode = PersistMode.REBIND;
                HighAvailabilityMode highAvailabilityMode = HighAvailabilityMode.DISABLED;

                launcher = BrooklynLauncher.newInstance()
                        .localBrooklynPropertiesFile(localBrooklynProperties)
                        .brooklynProperties(OsgiManager.USE_OSGI, false)
                        .persistMode(persistMode)
                        .persistenceDir(persistenceDir)
                        .persistenceLocation(persistenceLocation)
                        .highAvailabilityMode(highAvailabilityMode);

            } catch (FatalConfigurationRuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new FatalConfigurationRuntimeException("Fatal error configuring Brooklyn launch: "+e.getMessage(), e);
            }

            try {
                launcher.cleanOrphanedState(destinationDir, destinationLocation);
            } catch (FatalRuntimeException e) {
                throw e;
            } catch (Exception e) {
                Exceptions.propagateIfFatal(e);
                log.error("Error retrieving persisted state: "+Exceptions.collapseText(e), e);
                Exceptions.propagate(e);
            } finally {
                try {
                    launcher.terminate();
                } catch (Exception e2) {
                    log.warn("Subsequent error during termination: "+e2);
                    log.debug("Details of subsequent error during termination: "+e2, e2);
                }
            }

            return null;
        }
    }

    @Command(name = "copy-state", description = "Retrieves persisted state")
    public static class CopyStateCommand extends BrooklynCommandCollectingArgs {

        @Option(name = { "--localBrooklynProperties" }, title = "local brooklyn.properties file",
                description = "local brooklyn.properties file, specific to this launch (appending to and overriding global properties)")
        public String localBrooklynProperties;

        @Option(name = { "--persistenceDir" }, title = "persistence dir",
                description = "The directory to read persisted state (or container name if using an object store)")
        public String persistenceDir;

        @Option(name = { "--persistenceLocation" }, title = "persistence location",
            description = "The location spec for an object store to read persisted state")
        public String persistenceLocation;
    
        @Option(name = { "--destinationDir" }, required = true, title = "destination dir",
                description = "The directory to copy persistence data to")
            public String destinationDir;
        
        @Option(name = { "--destinationLocation" }, title = "persistence location",
                description = "The location spec for an object store to copy data to")
            public String destinationLocation;
        
        @Option(name = { "--transformations" }, title = "transformations",
                description = "local transformations file, to be applied to the copy of the data before uploading it")
        public String transformations;
        
        @Override
        public Void call() throws Exception {
            checkNotNull(destinationDir, "destinationDir"); // presumably because required=true this will never be null!
            
            // Configure launcher
            BrooklynLauncher launcher;
            failIfArguments();
            try {
                log.info("Retrieving and copying persisted state to "+destinationDir+(Strings.isBlank(destinationLocation) ? "" : " @ "+destinationLocation));
                
                if (!quiet) {
                    stdout.println(banner);
                    stdout.println(productOneLineSummary);
                    stdout.println();
                }
    
                PersistMode persistMode = PersistMode.AUTO;
                HighAvailabilityMode highAvailabilityMode = HighAvailabilityMode.DISABLED;
                
                launcher = BrooklynLauncher.newInstance()
                        .localBrooklynPropertiesFile(localBrooklynProperties)
                        .brooklynProperties(OsgiManager.USE_OSGI, false)
                        .persistMode(persistMode)
                        .persistenceDir(persistenceDir)
                        .persistenceLocation(persistenceLocation)
                        .highAvailabilityMode(highAvailabilityMode);
                
            } catch (FatalConfigurationRuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new FatalConfigurationRuntimeException("Fatal error configuring Brooklyn launch: "+e.getMessage(), e);
            }
            
            try {
                launcher.copyPersistedState(destinationDir, destinationLocation, loadTransformer(transformations));
            } catch (FatalRuntimeException e) {
                // rely on caller logging this propagated exception
                throw e;
            } catch (Exception e) {
                // for other exceptions we log it, possibly redundantly but better too much than too little
                Exceptions.propagateIfFatal(e);
                log.error("Error retrieving persisted state: "+Exceptions.collapseText(e), e);
                Exceptions.propagate(e);
            } finally {
                try {
                    launcher.terminate();
                } catch (Exception e2) {
                    log.warn("Subsequent error during termination: "+e2);
                    log.debug("Details of subsequent error during termination: "+e2, e2);
                }
            }
            
            return null;
        }

        protected CompoundTransformer loadTransformer(String transformationsFileUrl) {
            return BrooklynPersistenceUtils.loadTransformer(ResourceUtils.create(this), transformationsFileUrl);
        }
        
        @Override
        public ToStringHelper string() {
            return super.string()
                    .add("localBrooklynProperties", localBrooklynProperties)
                    .add("persistenceLocation", persistenceLocation)
                    .add("persistenceDir", persistenceDir)
                    .add("destinationDir", destinationDir);
        }
    }

    /** method intended for overriding when a different {@link Cli} is desired,
     * or when the subclass wishes to change any of the arguments */
    @SuppressWarnings("unchecked")
    @Override
    protected CliBuilder<BrooklynCommand> cliBuilder() {
        CliBuilder<BrooklynCommand> builder = Cli.<BrooklynCommand>builder(cliScriptName())
                .withDescription("Brooklyn Management Service")
                .withDefaultCommand(cliDefaultInfoCommand())
                .withCommands(
                        HelpCommand.class,
                        cliInfoCommand(),
                        GeneratePasswordCommand.class,
                        CleanOrphanedStateCommand.class,
                        CopyStateCommand.class,
                        ListAllCommand.class,
                        cliLaunchCommand()
                );

        builder.withGroup("cloud-compute")
                .withDescription("Access compute details of a given cloud")
                .withDefaultCommand(HelpCommand.class)
                .withCommands(
                        ComputeListImagesCommand.class,
                        ComputeListHardwareProfilesCommand.class,
                        ComputeListInstancesCommand.class,
                        ComputeGetImageCommand.class,
                        ComputeDefaultTemplateCommand.class,
                        ComputeTerminateInstancesCommand.class);

        builder.withGroup("cloud-blobstore")
                .withDescription("Access blobstore details of a given cloud")
                .withDefaultCommand(HelpCommand.class)
                .withCommands(
                        BlobstoreListContainersCommand.class, 
                        BlobstoreListContainerCommand.class,
                        BlobstoreGetBlobCommand.class);

        return builder;
    }
    
    /** method intended for overriding when a custom {@link LaunchCommand} is being specified  */
    protected Class<? extends BrooklynCommand> cliLaunchCommand() {
        return LaunchCommand.class;
    }
    
    /** method intended for overriding when a custom {@link InfoCommand} is being specified  */
    protected Class<? extends BrooklynCommand> cliInfoCommand() {
        return InfoCommand.class;
    }
    
    /** method intended for overriding when a custom {@link InfoCommand} is being specified  */
    protected Class<? extends BrooklynCommand> cliDefaultInfoCommand() {
        return DefaultInfoCommand.class;
    }
    
    public static class AppShutdownHandler implements ShutdownHandler {
        private CountDownLatch lock = new CountDownLatch(1);

        @Override
        public void onShutdownRequest() {
            lock.countDown();
        }
        
        public boolean isRequested() {
            return lock.getCount() == 0;
        }

        public void waitOnShutdownRequest() {
            try {
                lock.await();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return; // exit gracefully
            }
        }
    }
}
