org.apache.solr.util.SolrCLI.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.solr.util.SolrCLI.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.solr.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.invoke.MethodHandles;
import java.net.ConnectException;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.time.Instant;
import java.time.Period;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.exec.DefaultExecuteResultHandler;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.Executor;
import org.apache.commons.exec.OS;
import org.apache.commons.exec.environment.EnvironmentUtils;
import org.apache.commons.io.FileUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NoHttpResponseException;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.apache.lucene.util.Version;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.impl.HttpSolrClient.Builder;
import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder;
import org.apache.solr.client.solrj.impl.ZkClientClusterStateProvider;
import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkConfigManager;
import org.apache.solr.common.cloud.ZkCoreNodeProps;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.ContentStreamBase;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.StrUtils;
import org.noggit.CharArr;
import org.noggit.JSONParser;
import org.noggit.JSONWriter;
import org.noggit.ObjectBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.solr.common.SolrException.ErrorCode.FORBIDDEN;
import static org.apache.solr.common.SolrException.ErrorCode.UNAUTHORIZED;
import static org.apache.solr.common.params.CommonParams.NAME;

/**
 * Command-line utility for working with Solr.
 */
public class SolrCLI {
    /**
     * Defines the interface to a Solr tool that can be run from this command-line app.
     */
    public interface Tool {
        String getName();

        Option[] getOptions();

        int runTool(CommandLine cli) throws Exception;
    }

    public static abstract class ToolBase implements Tool {
        protected PrintStream stdout;
        protected boolean verbose = false;

        protected ToolBase() {
            this(System.out);
        }

        protected ToolBase(PrintStream stdout) {
            this.stdout = stdout;
        }

        protected void echo(final String msg) {
            stdout.println(msg);
        }

        public int runTool(CommandLine cli) throws Exception {
            verbose = cli.hasOption("verbose");

            int toolExitStatus = 0;
            try {
                setBasicAuth();
                runImpl(cli);
            } catch (Exception exc) {
                // since this is a CLI, spare the user the stacktrace
                String excMsg = exc.getMessage();
                if (excMsg != null) {
                    System.err.println("\nERROR: " + excMsg + "\n");
                    toolExitStatus = 1;
                } else {
                    throw exc;
                }
            }
            return toolExitStatus;
        }

        protected abstract void runImpl(CommandLine cli) throws Exception;
    }

    /**
     * Helps build SolrCloud aware tools by initializing a CloudSolrClient
     * instance before running the tool.
     */
    public static abstract class SolrCloudTool extends ToolBase {

        protected SolrCloudTool(PrintStream stdout) {
            super(stdout);
        }

        public Option[] getOptions() {
            return cloudOptions;
        }

        protected void runImpl(CommandLine cli) throws Exception {
            String zkHost = cli.getOptionValue("zkHost", ZK_HOST);

            log.debug("Connecting to Solr cluster: " + zkHost);
            try (CloudSolrClient cloudSolrClient = new CloudSolrClient.Builder().withZkHost(zkHost).build()) {

                String collection = cli.getOptionValue("collection");
                if (collection != null)
                    cloudSolrClient.setDefaultCollection(collection);

                cloudSolrClient.connect();
                runCloudTool(cloudSolrClient, cli);
            }
        }

        /**
         * Runs a SolrCloud tool with CloudSolrClient initialized
         */
        protected abstract void runCloudTool(CloudSolrClient cloudSolrClient, CommandLine cli) throws Exception;
    }

    private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
    public static final String DEFAULT_SOLR_URL = "http://localhost:8983/solr";
    public static final String ZK_HOST = "localhost:9983";

    @SuppressWarnings("static-access")
    public static Option[] cloudOptions = new Option[] {
            OptionBuilder.withArgName("HOST").hasArg().isRequired(false)
                    .withDescription("Address of the Zookeeper ensemble; defaults to: " + ZK_HOST).create("zkHost"),
            OptionBuilder.withArgName("COLLECTION").hasArg().isRequired(false)
                    .withDescription("Name of collection; no default").create("collection") };

    private static void exit(int exitStatus) {
        try {
            System.exit(exitStatus);
        } catch (java.lang.SecurityException secExc) {
            if (exitStatus != 0)
                throw new RuntimeException("SolrCLI failed to exit with status " + exitStatus);
        }
    }

    /**
     * Runs a tool.
     */
    public static void main(String[] args) throws Exception {
        if (args == null || args.length == 0 || args[0] == null || args[0].trim().length() == 0) {
            System.err.println(
                    "Invalid command-line args! Must pass the name of a tool to run.\n" + "Supported tools:\n");
            displayToolOptions();
            exit(1);
        }

        if (args.length == 1 && Arrays.asList("-v", "-version", "version").contains(args[0])) {
            // Simple version tool, no need for its own class
            System.out.println(Version.LATEST);
            exit(0);
        }

        Tool tool = findTool(args);
        CommandLine cli = parseCmdLine(args, tool.getOptions());
        System.exit(tool.runTool(cli));
    }

    public static Tool findTool(String[] args) throws Exception {
        String toolType = args[0].trim().toLowerCase(Locale.ROOT);
        return newTool(toolType);
    }

    public static CommandLine parseCmdLine(String[] args, Option[] toolOptions) throws Exception {

        String builderClassName = System.getProperty("solr.authentication.httpclient.builder");
        if (builderClassName != null) {
            try {
                Class c = Class.forName(builderClassName);
                SolrHttpClientBuilder builder = (SolrHttpClientBuilder) c.newInstance();
                HttpClientUtil.setHttpClientBuilder(builder);
                log.info("Set SolrHttpClientBuilder from: " + builderClassName);
            } catch (Exception ex) {
                log.error(ex.getMessage());
                throw new RuntimeException("Error during loading of builder '" + builderClassName + "'.", ex);
            }
        }

        // the parser doesn't like -D props
        List<String> toolArgList = new ArrayList<String>();
        List<String> dashDList = new ArrayList<String>();
        for (int a = 1; a < args.length; a++) {
            String arg = args[a];
            if (arg.startsWith("-D")) {
                dashDList.add(arg);
            } else {
                toolArgList.add(arg);
            }
        }
        String[] toolArgs = toolArgList.toArray(new String[0]);

        // process command-line args to configure this application
        CommandLine cli = processCommandLineArgs(joinCommonAndToolOptions(toolOptions), toolArgs);

        List argList = cli.getArgList();
        argList.addAll(dashDList);

        // for SSL support, try to accommodate relative paths set for SSL store props
        String solrInstallDir = System.getProperty("solr.install.dir");
        if (solrInstallDir != null) {
            checkSslStoreSysProp(solrInstallDir, "keyStore");
            checkSslStoreSysProp(solrInstallDir, "trustStore");
        }

        return cli;
    }

    protected static void checkSslStoreSysProp(String solrInstallDir, String key) {
        String sysProp = "javax.net.ssl." + key;
        String keyStore = System.getProperty(sysProp);
        if (keyStore == null)
            return;

        File keyStoreFile = new File(keyStore);
        if (keyStoreFile.isFile())
            return; // configured setting is OK

        keyStoreFile = new File(solrInstallDir, "server/" + keyStore);
        if (keyStoreFile.isFile()) {
            System.setProperty(sysProp, keyStoreFile.getAbsolutePath());
        } else {
            System.err.println("WARNING: " + sysProp + " file " + keyStore
                    + " not found! https requests to Solr will likely fail; please update your " + sysProp
                    + " setting to use an absolute path.");
        }
    }

    /**
     * Support options common to all tools.
     */
    public static Option[] getCommonToolOptions() {
        return new Option[0];
    }

    // Creates an instance of the requested tool, using classpath scanning if necessary
    private static Tool newTool(String toolType) throws Exception {
        if ("healthcheck".equals(toolType))
            return new HealthcheckTool();
        else if ("status".equals(toolType))
            return new StatusTool();
        else if ("api".equals(toolType))
            return new ApiTool();
        else if ("create_collection".equals(toolType))
            return new CreateCollectionTool();
        else if ("create_core".equals(toolType))
            return new CreateCoreTool();
        else if ("create".equals(toolType))
            return new CreateTool();
        else if ("delete".equals(toolType))
            return new DeleteTool();
        else if ("config".equals(toolType))
            return new ConfigTool();
        else if ("run_example".equals(toolType))
            return new RunExampleTool();
        else if ("upconfig".equals(toolType))
            return new ConfigSetUploadTool();
        else if ("downconfig".equals(toolType))
            return new ConfigSetDownloadTool();
        else if ("rm".equals(toolType))
            return new ZkRmTool();
        else if ("mv".equals(toolType))
            return new ZkMvTool();
        else if ("cp".equals(toolType))
            return new ZkCpTool();
        else if ("ls".equals(toolType))
            return new ZkLsTool();
        else if ("mkroot".equals(toolType))
            return new ZkMkrootTool();
        else if ("assert".equals(toolType))
            return new AssertTool();
        else if ("utils".equals(toolType))
            return new UtilsTool();

        // If you add a built-in tool to this class, add it here to avoid
        // classpath scanning

        for (Class<Tool> next : findToolClassesInPackage("org.apache.solr.util")) {
            Tool tool = next.newInstance();
            if (toolType.equals(tool.getName()))
                return tool;
        }

        throw new IllegalArgumentException(toolType + " not supported!");
    }

    private static void displayToolOptions() throws Exception {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("healthcheck", getToolOptions(new HealthcheckTool()));
        formatter.printHelp("status", getToolOptions(new StatusTool()));
        formatter.printHelp("api", getToolOptions(new ApiTool()));
        formatter.printHelp("create_collection", getToolOptions(new CreateCollectionTool()));
        formatter.printHelp("create_core", getToolOptions(new CreateCoreTool()));
        formatter.printHelp("create", getToolOptions(new CreateTool()));
        formatter.printHelp("delete", getToolOptions(new DeleteTool()));
        formatter.printHelp("config", getToolOptions(new ConfigTool()));
        formatter.printHelp("run_example", getToolOptions(new RunExampleTool()));
        formatter.printHelp("upconfig", getToolOptions(new ConfigSetUploadTool()));
        formatter.printHelp("downconfig", getToolOptions(new ConfigSetDownloadTool()));
        formatter.printHelp("rm", getToolOptions(new ZkRmTool()));
        formatter.printHelp("cp", getToolOptions(new ZkCpTool()));
        formatter.printHelp("mv", getToolOptions(new ZkMvTool()));
        formatter.printHelp("ls", getToolOptions(new ZkLsTool()));

        List<Class<Tool>> toolClasses = findToolClassesInPackage("org.apache.solr.util");
        for (Class<Tool> next : toolClasses) {
            Tool tool = next.newInstance();
            formatter.printHelp(tool.getName(), getToolOptions(tool));
        }
    }

    private static Options getToolOptions(Tool tool) {
        Options options = new Options();
        options.addOption("help", false, "Print this message");
        options.addOption("verbose", false, "Generate verbose log messages");
        Option[] toolOpts = joinCommonAndToolOptions(tool.getOptions());
        for (int i = 0; i < toolOpts.length; i++)
            options.addOption(toolOpts[i]);
        return options;
    }

    public static Option[] joinCommonAndToolOptions(Option[] toolOpts) {
        return joinOptions(getCommonToolOptions(), toolOpts);
    }

    public static Option[] joinOptions(Option[] lhs, Option[] rhs) {
        List<Option> options = new ArrayList<Option>();
        if (lhs != null && lhs.length > 0) {
            for (Option opt : lhs)
                options.add(opt);
        }

        if (rhs != null) {
            for (Option opt : rhs)
                options.add(opt);
        }

        return options.toArray(new Option[0]);
    }

    /**
     * Parses the command-line arguments passed by the user.
     */
    public static CommandLine processCommandLineArgs(Option[] customOptions, String[] args) {
        Options options = new Options();

        options.addOption("help", false, "Print this message");
        options.addOption("verbose", false, "Generate verbose log messages");

        if (customOptions != null) {
            for (int i = 0; i < customOptions.length; i++)
                options.addOption(customOptions[i]);
        }

        CommandLine cli = null;
        try {
            cli = (new GnuParser()).parse(options, args);
        } catch (ParseException exp) {
            boolean hasHelpArg = false;
            if (args != null && args.length > 0) {
                for (int z = 0; z < args.length; z++) {
                    if ("--help".equals(args[z]) || "-help".equals(args[z])) {
                        hasHelpArg = true;
                        break;
                    }
                }
            }
            if (!hasHelpArg) {
                System.err.println("Failed to parse command-line arguments due to: " + exp.getMessage());
            }
            HelpFormatter formatter = new HelpFormatter();
            formatter.printHelp(SolrCLI.class.getName(), options);
            exit(1);
        }

        if (cli.hasOption("help")) {
            HelpFormatter formatter = new HelpFormatter();
            formatter.printHelp(SolrCLI.class.getName(), options);
            exit(0);
        }

        return cli;
    }

    /**
     * Scans Jar files on the classpath for Tool implementations to activate.
     */
    @SuppressWarnings("unchecked")
    private static List<Class<Tool>> findToolClassesInPackage(String packageName) {
        List<Class<Tool>> toolClasses = new ArrayList<Class<Tool>>();
        try {
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            String path = packageName.replace('.', '/');
            Enumeration<URL> resources = classLoader.getResources(path);
            Set<String> classes = new TreeSet<String>();
            while (resources.hasMoreElements()) {
                URL resource = (URL) resources.nextElement();
                classes.addAll(findClasses(resource.getFile(), packageName));
            }

            for (String classInPackage : classes) {
                Class<?> theClass = Class.forName(classInPackage);
                if (Tool.class.isAssignableFrom(theClass))
                    toolClasses.add((Class<Tool>) theClass);
            }
        } catch (Exception e) {
            // safe to squelch this as it's just looking for tools to run
            log.debug("Failed to find Tool impl classes in " + packageName + " due to: " + e);
        }
        return toolClasses;
    }

    private static Set<String> findClasses(String path, String packageName) throws Exception {
        Set<String> classes = new TreeSet<String>();
        if (path.startsWith("file:") && path.contains("!")) {
            String[] split = path.split("!");
            URL jar = new URL(split[0]);
            ZipInputStream zip = new ZipInputStream(jar.openStream());
            ZipEntry entry;
            while ((entry = zip.getNextEntry()) != null) {
                if (entry.getName().endsWith(".class")) {
                    String className = entry.getName().replaceAll("[$].*", "").replaceAll("[.]class", "")
                            .replace('/', '.');
                    if (className.startsWith(packageName))
                        classes.add(className);
                }
            }
        }
        return classes;
    }

    /**
     * Inspects system property basicauth and enables authentication for HttpClient
     * @throws Exception if the basicauth SysProp has wrong format
     */
    protected static void setBasicAuth() throws Exception {
        String basicauth = System.getProperty("basicauth", null);
        if (basicauth != null) {
            List<String> ss = StrUtils.splitSmart(basicauth, ':');
            if (ss.size() != 2)
                throw new Exception("Please provide 'basicauth' in the 'user:password' format");

            HttpClientUtil.addRequestInterceptor((httpRequest, httpContext) -> {
                String pair = ss.get(0) + ":" + ss.get(1);
                byte[] encodedBytes = Base64.encodeBase64(pair.getBytes(UTF_8));
                httpRequest.addHeader(new BasicHeader("Authorization", "Basic " + new String(encodedBytes, UTF_8)));
            });
        }
    }

    /**
     * Determine if a request to Solr failed due to a communication error,
     * which is generally retry-able. 
     */
    public static boolean checkCommunicationError(Exception exc) {
        Throwable rootCause = SolrException.getRootCause(exc);
        boolean wasCommError = (rootCause instanceof ConnectException
                || rootCause instanceof ConnectTimeoutException || rootCause instanceof NoHttpResponseException
                || rootCause instanceof SocketException);
        return wasCommError;
    }

    /**
     * Tries a simple HEAD request and throws SolrException in case of Authorization error
     * @param url the url to do a HEAD request to
     * @param httpClient the http client to use (make sure it has authentication optinos set)
     * @return the HTTP response code
     * @throws SolrException if auth/autz problems
     * @throws IOException if connection failure
     */
    private static int attemptHttpHead(String url, HttpClient httpClient) throws SolrException, IOException {
        HttpResponse response = httpClient.execute(new HttpHead(url),
                HttpClientUtil.createNewHttpClientRequestContext());
        int code = response.getStatusLine().getStatusCode();
        if (code == UNAUTHORIZED.code || code == FORBIDDEN.code) {
            throw new SolrException(SolrException.ErrorCode.getErrorCode(code), "Solr requires authentication for "
                    + url + ". Please supply valid credentials. HTTP code=" + code);
        }
        return code;
    }

    private static boolean exceptionIsAuthRelated(Exception exc) {
        return (exc instanceof SolrException
                && Arrays.asList(UNAUTHORIZED.code, FORBIDDEN.code).contains(((SolrException) exc).code()));
    }

    public static CloseableHttpClient getHttpClient() {
        ModifiableSolrParams params = new ModifiableSolrParams();
        params.set(HttpClientUtil.PROP_MAX_CONNECTIONS, 128);
        params.set(HttpClientUtil.PROP_MAX_CONNECTIONS_PER_HOST, 32);
        params.set(HttpClientUtil.PROP_FOLLOW_REDIRECTS, false);
        return HttpClientUtil.createClient(params);
    }

    @SuppressWarnings("deprecation")
    public static void closeHttpClient(CloseableHttpClient httpClient) {
        if (httpClient != null) {
            try {
                HttpClientUtil.close(httpClient);
            } catch (Exception exc) {
                // safe to ignore, we're just shutting things down
            }
        }
    }

    public static final String JSON_CONTENT_TYPE = "application/json";

    public static NamedList<Object> postJsonToSolr(SolrClient solrClient, String updatePath, String jsonBody)
            throws Exception {
        ContentStreamBase.StringStream contentStream = new ContentStreamBase.StringStream(jsonBody);
        contentStream.setContentType(JSON_CONTENT_TYPE);
        ContentStreamUpdateRequest req = new ContentStreamUpdateRequest(updatePath);
        req.addContentStream(contentStream);
        return solrClient.request(req);
    }

    /**
     * Useful when a tool just needs to send one request to Solr. 
     */
    public static Map<String, Object> getJson(String getUrl) throws Exception {
        Map<String, Object> json = null;
        CloseableHttpClient httpClient = getHttpClient();
        try {
            json = getJson(httpClient, getUrl, 2, true);
        } finally {
            closeHttpClient(httpClient);
        }
        return json;
    }

    /**
     * Utility function for sending HTTP GET request to Solr with built-in retry support.
     */
    public static Map<String, Object> getJson(HttpClient httpClient, String getUrl, int attempts,
            boolean isFirstAttempt) throws Exception {
        Map<String, Object> json = null;
        if (attempts >= 1) {
            try {
                json = getJson(httpClient, getUrl);
            } catch (Exception exc) {
                if (exceptionIsAuthRelated(exc)) {
                    throw exc;
                }
                if (--attempts > 0 && checkCommunicationError(exc)) {
                    if (!isFirstAttempt) // only show the log warning after the second attempt fails
                        log.warn("Request to " + getUrl + " failed due to: " + exc.getMessage()
                                + ", sleeping for 5 seconds before re-trying the request ...");
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException ie) {
                        Thread.interrupted();
                    }

                    // retry using recursion with one-less attempt available
                    json = getJson(httpClient, getUrl, attempts, false);
                } else {
                    // no more attempts or error is not retry-able
                    throw exc;
                }
            }
        }

        return json;
    }

    private static class SolrResponseHandler implements ResponseHandler<Map<String, Object>> {
        public Map<String, Object> handleResponse(HttpResponse response)
                throws ClientProtocolException, IOException {
            HttpEntity entity = response.getEntity();
            if (entity != null) {

                String respBody = EntityUtils.toString(entity);
                Object resp = null;
                try {
                    resp = ObjectBuilder.getVal(new JSONParser(respBody));
                } catch (JSONParser.ParseException pe) {
                    throw new ClientProtocolException("Expected JSON response from server but received: " + respBody
                            + "\nTypically, this indicates a problem with the Solr server; check the Solr server logs for more information.");
                }

                if (resp != null && resp instanceof Map) {
                    return (Map<String, Object>) resp;
                } else {
                    throw new ClientProtocolException("Expected JSON object in response but received " + resp);
                }
            } else {
                StatusLine statusLine = response.getStatusLine();
                throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
            }
        }
    }

    /**
     * Utility function for sending HTTP GET request to Solr and then doing some
     * validation of the response.
     */
    @SuppressWarnings({ "unchecked" })
    public static Map<String, Object> getJson(HttpClient httpClient, String getUrl) throws Exception {
        try {
            // ensure we're requesting JSON back from Solr
            HttpGet httpGet = new HttpGet(
                    new URIBuilder(getUrl).setParameter(CommonParams.WT, CommonParams.JSON).build());

            // make the request and get back a parsed JSON object
            Map<String, Object> json = httpClient.execute(httpGet, new SolrResponseHandler(),
                    HttpClientUtil.createNewHttpClientRequestContext());
            // check the response JSON from Solr to see if it is an error
            Long statusCode = asLong("/responseHeader/status", json);
            if (statusCode == -1) {
                throw new SolrServerException(
                        "Unable to determine outcome of GET request to: " + getUrl + "! Response: " + json);
            } else if (statusCode != 0) {
                String errMsg = asString("/error/msg", json);
                if (errMsg == null)
                    errMsg = String.valueOf(json);
                throw new SolrServerException(errMsg);
            } else {
                // make sure no "failure" object in there either
                Object failureObj = json.get("failure");
                if (failureObj != null) {
                    if (failureObj instanceof Map) {
                        Object err = ((Map) failureObj).get("");
                        if (err != null)
                            throw new SolrServerException(err.toString());
                    }
                    throw new SolrServerException(failureObj.toString());
                }
            }
            return json;
        } catch (ClientProtocolException cpe) {
            // Currently detecting authentication by string-matching the HTTP response
            // Perhaps SolrClient should have thrown an exception itself??
            if (cpe.getMessage().contains("HTTP ERROR 401") || cpe.getMessage().contentEquals("HTTP ERROR 403")) {
                int code = cpe.getMessage().contains("HTTP ERROR 401") ? 401 : 403;
                throw new SolrException(SolrException.ErrorCode.getErrorCode(code),
                        "Solr requires authentication for " + getUrl
                                + ". Please supply valid credentials. HTTP code=" + code);
            } else {
                throw cpe;
            }
        }
    }

    /**
     * Helper function for reading a String value from a JSON Object tree. 
     */
    public static String asString(String jsonPath, Map<String, Object> json) {
        return pathAs(String.class, jsonPath, json);
    }

    /**
     * Helper function for reading a Long value from a JSON Object tree. 
     */
    public static Long asLong(String jsonPath, Map<String, Object> json) {
        return pathAs(Long.class, jsonPath, json);
    }

    /**
     * Helper function for reading a List of Strings from a JSON Object tree. 
     */
    @SuppressWarnings("unchecked")
    public static List<String> asList(String jsonPath, Map<String, Object> json) {
        return pathAs(List.class, jsonPath, json);
    }

    /**
     * Helper function for reading a Map from a JSON Object tree. 
     */
    @SuppressWarnings("unchecked")
    public static Map<String, Object> asMap(String jsonPath, Map<String, Object> json) {
        return pathAs(Map.class, jsonPath, json);
    }

    @SuppressWarnings("unchecked")
    public static <T> T pathAs(Class<T> clazz, String jsonPath, Map<String, Object> json) {
        T val = null;
        Object obj = atPath(jsonPath, json);
        if (obj != null) {
            if (clazz.isAssignableFrom(obj.getClass())) {
                val = (T) obj;
            } else {
                // no ok if it's not null and of a different type
                throw new IllegalStateException("Expected a " + clazz.getName() + " at path " + jsonPath
                        + " but found " + obj + " instead! " + json);
            }
        } // it's ok if it is null
        return val;
    }

    /**
     * Helper function for reading an Object of unknown type from a JSON Object tree. 
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static Object atPath(String jsonPath, Map<String, Object> json) {
        if ("/".equals(jsonPath))
            return json;

        if (!jsonPath.startsWith("/"))
            throw new IllegalArgumentException("Invalid JSON path: " + jsonPath + "! Must start with a /");

        Map<String, Object> parent = json;
        Object result = null;
        String[] path = jsonPath.split("/");
        for (int p = 1; p < path.length; p++) {
            Object child = parent.get(path[p]);
            if (child == null)
                break;

            if (p == path.length - 1) {
                // success - found the node at the desired path
                result = child;
            } else {
                if (child instanceof Map) {
                    // keep walking the path down to the desired node
                    parent = (Map) child;
                } else {
                    // early termination - hit a leaf before the requested node
                    break;
                }
            }
        }
        return result;
    }

    /**
     * Get the status of a Solr server.
     */
    public static class StatusTool extends ToolBase {

        public StatusTool() {
            this(System.out);
        }

        public StatusTool(PrintStream stdout) {
            super(stdout);
        }

        public String getName() {
            return "status";
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] {
                    OptionBuilder.withArgName("URL").hasArg().isRequired(false)
                            .withDescription(
                                    "Address of the Solr Web application, defaults to: " + DEFAULT_SOLR_URL)
                            .create("solr"),
                    OptionBuilder.withArgName("SECS").hasArg().isRequired(false)
                            .withDescription("Wait up to the specified number of seconds to see Solr running.")
                            .create("maxWaitSecs") };
        }

        protected void runImpl(CommandLine cli) throws Exception {
            int maxWaitSecs = Integer.parseInt(cli.getOptionValue("maxWaitSecs", "0"));
            String solrUrl = cli.getOptionValue("solr", DEFAULT_SOLR_URL);
            if (maxWaitSecs > 0) {
                int solrPort = (new URL(solrUrl)).getPort();
                echo("Waiting up to " + maxWaitSecs + " to see Solr running on port " + solrPort);
                try {
                    waitToSeeSolrUp(solrUrl, maxWaitSecs);
                    echo("Started Solr server on port " + solrPort + ". Happy searching!");
                } catch (TimeoutException timeout) {
                    throw new Exception(
                            "Solr at " + solrUrl + " did not come online within " + maxWaitSecs + " seconds!");
                }
            } else {
                try {
                    CharArr arr = new CharArr();
                    new JSONWriter(arr, 2).write(getStatus(solrUrl));
                    echo(arr.toString());
                } catch (Exception exc) {
                    if (exceptionIsAuthRelated(exc)) {
                        throw exc;
                    }
                    if (checkCommunicationError(exc)) {
                        // this is not actually an error from the tool as it's ok if Solr is not online.
                        System.err.println("Solr at " + solrUrl + " not online.");
                    } else {
                        throw new Exception("Failed to get system information from " + solrUrl + " due to: " + exc);
                    }
                }
            }
        }

        public Map<String, Object> waitToSeeSolrUp(String solrUrl, int maxWaitSecs) throws Exception {
            long timeout = System.nanoTime() + TimeUnit.NANOSECONDS.convert(maxWaitSecs, TimeUnit.SECONDS);
            while (System.nanoTime() < timeout) {
                try {
                    return getStatus(solrUrl);
                } catch (Exception exc) {
                    if (exceptionIsAuthRelated(exc)) {
                        throw exc;
                    }
                    try {
                        Thread.sleep(2000L);
                    } catch (InterruptedException interrupted) {
                        timeout = 0; // stop looping
                    }
                }
            }
            throw new TimeoutException("Did not see Solr at " + solrUrl + " come online within " + maxWaitSecs);
        }

        public Map<String, Object> getStatus(String solrUrl) throws Exception {
            Map<String, Object> status = null;

            if (!solrUrl.endsWith("/"))
                solrUrl += "/";

            String systemInfoUrl = solrUrl + "admin/info/system";
            CloseableHttpClient httpClient = getHttpClient();
            try {
                // hit Solr to get system info
                Map<String, Object> systemInfo = getJson(httpClient, systemInfoUrl, 2, true);
                // convert raw JSON into user-friendly output
                status = reportStatus(solrUrl, systemInfo, httpClient);
            } finally {
                closeHttpClient(httpClient);
            }

            return status;
        }

        public Map<String, Object> reportStatus(String solrUrl, Map<String, Object> info, HttpClient httpClient)
                throws Exception {
            Map<String, Object> status = new LinkedHashMap<String, Object>();

            String solrHome = (String) info.get("solr_home");
            status.put("solr_home", solrHome != null ? solrHome : "?");
            status.put("version", asString("/lucene/solr-impl-version", info));
            status.put("startTime", asString("/jvm/jmx/startTime", info));
            status.put("uptime", uptime(asLong("/jvm/jmx/upTimeMS", info)));

            String usedMemory = asString("/jvm/memory/used", info);
            String totalMemory = asString("/jvm/memory/total", info);
            status.put("memory", usedMemory + " of " + totalMemory);

            // if this is a Solr in solrcloud mode, gather some basic cluster info
            if ("solrcloud".equals(info.get("mode"))) {
                String zkHost = (String) info.get("zkHost");
                status.put("cloud", getCloudStatus(httpClient, solrUrl, zkHost));
            }

            return status;
        }

        /**
         * Calls the CLUSTERSTATUS endpoint in Solr to get basic status information about
         * the SolrCloud cluster. 
         */
        protected Map<String, String> getCloudStatus(HttpClient httpClient, String solrUrl, String zkHost)
                throws Exception {
            Map<String, String> cloudStatus = new LinkedHashMap<String, String>();
            cloudStatus.put("ZooKeeper", (zkHost != null) ? zkHost : "?");

            String clusterStatusUrl = solrUrl + "admin/collections?action=CLUSTERSTATUS";
            Map<String, Object> json = getJson(httpClient, clusterStatusUrl, 2, true);

            List<String> liveNodes = asList("/cluster/live_nodes", json);
            cloudStatus.put("liveNodes", String.valueOf(liveNodes.size()));

            Map<String, Object> collections = asMap("/cluster/collections", json);
            cloudStatus.put("collections", String.valueOf(collections.size()));

            return cloudStatus;
        }

    } // end StatusTool class

    /**
     * Used to send an arbitrary HTTP request to a Solr API endpoint.
     */
    public static class ApiTool extends ToolBase {

        public ApiTool() {
            this(System.out);
        }

        public ApiTool(PrintStream stdout) {
            super(stdout);
        }

        public String getName() {
            return "api";
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] { OptionBuilder.withArgName("URL").hasArg().isRequired(false)
                    .withDescription("Send a GET request to a Solr API endpoint").create("get") };
        }

        protected void runImpl(CommandLine cli) throws Exception {
            String getUrl = cli.getOptionValue("get");
            if (getUrl != null) {
                Map<String, Object> json = getJson(getUrl);

                // pretty-print the response to stdout
                CharArr arr = new CharArr();
                new JSONWriter(arr, 2).write(json);
                echo(arr.toString());
            }
        }
    } // end ApiTool class

    private static final String DEFAULT_CONFIG_SET = "data_driven_schema_configs";

    private static final long MS_IN_MIN = 60 * 1000L;
    private static final long MS_IN_HOUR = MS_IN_MIN * 60L;
    private static final long MS_IN_DAY = MS_IN_HOUR * 24L;

    private static final String uptime(long uptimeMs) {
        if (uptimeMs <= 0L)
            return "?";

        long numDays = (uptimeMs >= MS_IN_DAY) ? (long) Math.floor(uptimeMs / MS_IN_DAY) : 0L;
        long rem = uptimeMs - (numDays * MS_IN_DAY);
        long numHours = (rem >= MS_IN_HOUR) ? (long) Math.floor(rem / MS_IN_HOUR) : 0L;
        rem = rem - (numHours * MS_IN_HOUR);
        long numMinutes = (rem >= MS_IN_MIN) ? (long) Math.floor(rem / MS_IN_MIN) : 0L;
        rem = rem - (numMinutes * MS_IN_MIN);
        long numSeconds = Math.round(rem / 1000);
        return String.format(Locale.ROOT, "%d days, %d hours, %d minutes, %d seconds", numDays, numHours,
                numMinutes, numSeconds);
    }

    static class ReplicaHealth implements Comparable<ReplicaHealth> {
        String shard;
        String name;
        String url;
        String status;
        long numDocs;
        boolean isLeader;
        String uptime;
        String memory;

        ReplicaHealth(String shard, String name, String url, String status, long numDocs, boolean isLeader,
                String uptime, String memory) {
            this.shard = shard;
            this.name = name;
            this.url = url;
            this.numDocs = numDocs;
            this.status = status;
            this.isLeader = isLeader;
            this.uptime = uptime;
            this.memory = memory;
        }

        public Map<String, Object> asMap() {
            Map<String, Object> map = new LinkedHashMap<String, Object>();
            map.put(NAME, name);
            map.put("url", url);
            map.put("numDocs", numDocs);
            map.put("status", status);
            if (uptime != null)
                map.put("uptime", uptime);
            if (memory != null)
                map.put("memory", memory);
            if (isLeader)
                map.put("leader", true);
            return map;
        }

        public String toString() {
            CharArr arr = new CharArr();
            new JSONWriter(arr, 2).write(asMap());
            return arr.toString();
        }

        public int hashCode() {
            return this.shard.hashCode() + (isLeader ? 1 : 0);
        }

        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (!(obj instanceof ReplicaHealth))
                return true;
            ReplicaHealth that = (ReplicaHealth) obj;
            return this.shard.equals(that.shard) && this.isLeader == that.isLeader;
        }

        public int compareTo(ReplicaHealth other) {
            if (this == other)
                return 0;
            if (other == null)
                return 1;

            int myShardIndex = Integer.parseInt(this.shard.substring("shard".length()));

            int otherShardIndex = Integer.parseInt(other.shard.substring("shard".length()));

            if (myShardIndex == otherShardIndex) {
                // same shard index, list leaders first
                return this.isLeader ? -1 : 1;
            }

            return myShardIndex - otherShardIndex;
        }
    }

    static enum ShardState {
        healthy, degraded, down, no_leader
    }

    static class ShardHealth {
        String shard;
        List<ReplicaHealth> replicas;

        ShardHealth(String shard, List<ReplicaHealth> replicas) {
            this.shard = shard;
            this.replicas = replicas;
        }

        public ShardState getShardState() {
            boolean healthy = true;
            boolean hasLeader = false;
            boolean atLeastOneActive = false;
            for (ReplicaHealth replicaHealth : replicas) {
                if (replicaHealth.isLeader)
                    hasLeader = true;

                if (!Replica.State.ACTIVE.toString().equals(replicaHealth.status)) {
                    healthy = false;
                } else {
                    atLeastOneActive = true;
                }
            }

            if (!hasLeader)
                return ShardState.no_leader;

            return healthy ? ShardState.healthy : (atLeastOneActive ? ShardState.degraded : ShardState.down);
        }

        public Map<String, Object> asMap() {
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("shard", shard);
            map.put("status", getShardState().toString());
            List<Object> replicaList = new ArrayList<Object>();
            for (ReplicaHealth replica : replicas)
                replicaList.add(replica.asMap());
            map.put("replicas", replicaList);
            return map;
        }

        public String toString() {
            CharArr arr = new CharArr();
            new JSONWriter(arr, 2).write(asMap());
            return arr.toString();
        }
    }

    /**
     * Requests health information about a specific collection in SolrCloud.
     */
    public static class HealthcheckTool extends SolrCloudTool {

        public HealthcheckTool() {
            this(System.out);
        }

        public HealthcheckTool(PrintStream stdout) {
            super(stdout);
        }

        @Override
        public String getName() {
            return "healthcheck";
        }

        @Override
        protected void runCloudTool(CloudSolrClient cloudSolrClient, CommandLine cli) throws Exception {

            String collection = cli.getOptionValue("collection");
            if (collection == null)
                throw new IllegalArgumentException("Must provide a collection to run a healthcheck against!");

            log.debug("Running healthcheck for " + collection);

            ZkStateReader zkStateReader = cloudSolrClient.getZkStateReader();

            ClusterState clusterState = zkStateReader.getClusterState();
            Set<String> liveNodes = clusterState.getLiveNodes();
            Collection<Slice> slices = clusterState.getSlices(collection);
            if (slices == null)
                throw new IllegalArgumentException("Collection " + collection + " not found!");

            // Test http code using a HEAD request first, fail fast if authentication failure
            String urlForColl = zkStateReader.getLeaderUrl(collection, slices.stream().findFirst().get().getName(),
                    1000);
            attemptHttpHead(urlForColl, cloudSolrClient.getHttpClient());

            SolrQuery q = new SolrQuery("*:*");
            q.setRows(0);
            QueryResponse qr = cloudSolrClient.query(q);
            String collErr = null;
            long docCount = -1;
            try {
                docCount = qr.getResults().getNumFound();
            } catch (Exception exc) {
                collErr = String.valueOf(exc);
            }

            List<Object> shardList = new ArrayList<>();
            boolean collectionIsHealthy = (docCount != -1);

            for (Slice slice : slices) {
                String shardName = slice.getName();
                // since we're reporting health of this shard, there's no guarantee of a leader
                String leaderUrl = null;
                try {
                    leaderUrl = zkStateReader.getLeaderUrl(collection, shardName, 1000);
                } catch (Exception exc) {
                    log.warn("Failed to get leader for shard " + shardName + " due to: " + exc);
                }

                List<ReplicaHealth> replicaList = new ArrayList<ReplicaHealth>();
                for (Replica r : slice.getReplicas()) {

                    String uptime = null;
                    String memory = null;
                    String replicaStatus = null;
                    long numDocs = -1L;

                    ZkCoreNodeProps replicaCoreProps = new ZkCoreNodeProps(r);
                    String coreUrl = replicaCoreProps.getCoreUrl();
                    boolean isLeader = coreUrl.equals(leaderUrl);

                    // if replica's node is not live, its status is DOWN
                    String nodeName = replicaCoreProps.getNodeName();
                    if (nodeName == null || !liveNodes.contains(nodeName)) {
                        replicaStatus = Replica.State.DOWN.toString();
                    } else {
                        // query this replica directly to get doc count and assess health
                        q = new SolrQuery("*:*");
                        q.setRows(0);
                        q.set("distrib", "false");
                        try (HttpSolrClient solr = new HttpSolrClient.Builder(coreUrl).build()) {

                            String solrUrl = solr.getBaseURL();

                            qr = solr.query(q);
                            numDocs = qr.getResults().getNumFound();

                            int lastSlash = solrUrl.lastIndexOf('/');
                            String systemInfoUrl = solrUrl.substring(0, lastSlash) + "/admin/info/system";
                            Map<String, Object> info = getJson(solr.getHttpClient(), systemInfoUrl, 2, true);
                            uptime = uptime(asLong("/jvm/jmx/upTimeMS", info));
                            String usedMemory = asString("/jvm/memory/used", info);
                            String totalMemory = asString("/jvm/memory/total", info);
                            memory = usedMemory + " of " + totalMemory;

                            // if we get here, we can trust the state
                            replicaStatus = replicaCoreProps.getState();
                        } catch (Exception exc) {
                            log.error("ERROR: " + exc + " when trying to reach: " + coreUrl);

                            if (checkCommunicationError(exc)) {
                                replicaStatus = Replica.State.DOWN.toString();
                            } else {
                                replicaStatus = "error: " + exc;
                            }
                        }
                    }

                    replicaList.add(new ReplicaHealth(shardName, r.getName(), coreUrl, replicaStatus, numDocs,
                            isLeader, uptime, memory));
                }

                ShardHealth shardHealth = new ShardHealth(shardName, replicaList);
                if (ShardState.healthy != shardHealth.getShardState())
                    collectionIsHealthy = false; // at least one shard is un-healthy

                shardList.add(shardHealth.asMap());
            }

            Map<String, Object> report = new LinkedHashMap<String, Object>();
            report.put("collection", collection);
            report.put("status", collectionIsHealthy ? "healthy" : "degraded");
            if (collErr != null) {
                report.put("error", collErr);
            }
            report.put("numDocs", docCount);
            report.put("numShards", slices.size());
            report.put("shards", shardList);

            CharArr arr = new CharArr();
            new JSONWriter(arr, 2).write(report);
            echo(arr.toString());
        }
    } // end HealthcheckTool

    private static final Option[] CREATE_COLLECTION_OPTIONS = new Option[] {
            OptionBuilder.withArgName("HOST").hasArg().isRequired(false)
                    .withDescription("Address of the Zookeeper ensemble; defaults to: " + ZK_HOST).create("zkHost"),
            OptionBuilder.withArgName("HOST").hasArg().isRequired(false)
                    .withDescription("Base Solr URL, which can be used to determine the zkHost if that's not known")
                    .create("solrUrl"),
            OptionBuilder.withArgName("NAME").hasArg().isRequired(true)
                    .withDescription("Name of collection to create.").create(NAME),
            OptionBuilder.withArgName("#").hasArg().isRequired(false)
                    .withDescription("Number of shards; default is 1").create("shards"),
            OptionBuilder.withArgName("#").hasArg().isRequired(false).withDescription(
                    "Number of copies of each document across the collection (replicas per shard); default is 1")
                    .create("replicationFactor"),
            OptionBuilder.withArgName("#").hasArg().isRequired(false).withDescription(
                    "Maximum number of shards per Solr node; default is determined based on the number of shards, replication factor, and live nodes.")
                    .create("maxShardsPerNode"),
            OptionBuilder.withArgName("NAME").hasArg().isRequired(false)
                    .withDescription("Configuration directory to copy when creating the new collection; default is "
                            + DEFAULT_CONFIG_SET)
                    .create("confdir"),
            OptionBuilder.withArgName("NAME").hasArg().isRequired(false)
                    .withDescription("Configuration name; default is the collection name").create("confname"),
            OptionBuilder.withArgName("DIR").hasArg().isRequired(true)
                    .withDescription("Path to configsets directory on the local system.").create("configsetsDir") };

    /**
     * Get the base URL of a live Solr instance from either the solrUrl command-line option from ZooKeeper.
     */
    public static String resolveSolrUrl(CommandLine cli) throws Exception {
        String solrUrl = cli.getOptionValue("solrUrl");
        if (solrUrl == null) {
            String zkHost = cli.getOptionValue("zkHost");
            if (zkHost == null)
                throw new IllegalStateException("Must provide either the '-solrUrl' or '-zkHost' parameters!");

            try (CloudSolrClient cloudSolrClient = new CloudSolrClient.Builder().withZkHost(zkHost).build()) {
                cloudSolrClient.connect();
                Set<String> liveNodes = cloudSolrClient.getZkStateReader().getClusterState().getLiveNodes();
                if (liveNodes.isEmpty())
                    throw new IllegalStateException(
                            "No live nodes found! Cannot determine 'solrUrl' from ZooKeeper: " + zkHost);

                String firstLiveNode = liveNodes.iterator().next();
                solrUrl = cloudSolrClient.getZkStateReader().getBaseUrlForNodeName(firstLiveNode);
            }
        }
        return solrUrl;
    }

    /**
     * Get the ZooKeeper connection string from either the zkHost command-line option or by looking it
     * up from a running Solr instance based on the solrUrl option.
     */
    public static String getZkHost(CommandLine cli) throws Exception {
        String zkHost = cli.getOptionValue("zkHost");
        if (zkHost != null)
            return zkHost;

        // find it using the localPort
        String solrUrl = cli.getOptionValue("solrUrl");
        if (solrUrl == null)
            throw new IllegalStateException(
                    "Must provide either the -zkHost or -solrUrl parameters to use the create_collection command!");

        if (!solrUrl.endsWith("/"))
            solrUrl += "/";

        String systemInfoUrl = solrUrl + "admin/info/system";
        CloseableHttpClient httpClient = getHttpClient();
        try {
            // hit Solr to get system info
            Map<String, Object> systemInfo = getJson(httpClient, systemInfoUrl, 2, true);

            // convert raw JSON into user-friendly output
            StatusTool statusTool = new StatusTool();
            Map<String, Object> status = statusTool.reportStatus(solrUrl, systemInfo, httpClient);
            Map<String, Object> cloud = (Map<String, Object>) status.get("cloud");
            if (cloud != null) {
                String zookeeper = (String) cloud.get("ZooKeeper");
                if (zookeeper.endsWith("(embedded)")) {
                    zookeeper = zookeeper.substring(0, zookeeper.length() - "(embedded)".length());
                }
                zkHost = zookeeper;
            }
        } finally {
            HttpClientUtil.close(httpClient);
        }

        return zkHost;
    }

    public static boolean safeCheckCollectionExists(String url, String collection) {
        boolean exists = false;
        try {
            Map<String, Object> existsCheckResult = getJson(url);
            List<String> collections = (List<String>) existsCheckResult.get("collections");
            exists = collections != null && collections.contains(collection);
        } catch (Exception exc) {
            // just ignore it since we're only interested in a positive result here
        }
        return exists;
    }

    public static boolean safeCheckCoreExists(String coreStatusUrl, String coreName) {
        boolean exists = false;
        try {
            Map<String, Object> existsCheckResult = getJson(coreStatusUrl);
            Map<String, Object> status = (Map<String, Object>) existsCheckResult.get("status");
            Map<String, Object> coreStatus = (Map<String, Object>) status.get(coreName);
            Map<String, Object> failureStatus = (Map<String, Object>) existsCheckResult.get("initFailures");
            String errorMsg = (String) failureStatus.get(coreName);
            exists = coreStatus != null && coreStatus.containsKey(NAME) || errorMsg != null;
        } catch (Exception exc) {
            // just ignore it since we're only interested in a positive result here
        }
        return exists;
    }

    /**
     * Supports create_collection command in the bin/solr script.
     */
    public static class CreateCollectionTool extends ToolBase {

        public CreateCollectionTool() {
            this(System.out);
        }

        public CreateCollectionTool(PrintStream stdout) {
            super(stdout);
        }

        public String getName() {
            return "create_collection";
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return CREATE_COLLECTION_OPTIONS;
        }

        protected void runImpl(CommandLine cli) throws Exception {

            String zkHost = getZkHost(cli);
            if (zkHost == null) {
                throw new IllegalStateException("Solr at " + cli.getOptionValue("solrUrl")
                        + " is running in standalone server mode, please use the create_core command instead;\n"
                        + "create_collection can only be used when running in SolrCloud mode.\n");
            }

            try (CloudSolrClient cloudSolrClient = new CloudSolrClient.Builder().withZkHost(zkHost).build()) {
                echo("\nConnecting to ZooKeeper at " + zkHost + " ...");
                cloudSolrClient.connect();
                runCloudTool(cloudSolrClient, cli);
            }
        }

        protected void runCloudTool(CloudSolrClient cloudSolrClient, CommandLine cli) throws Exception {

            Set<String> liveNodes = cloudSolrClient.getZkStateReader().getClusterState().getLiveNodes();
            if (liveNodes.isEmpty())
                throw new IllegalStateException("No live nodes found! Cannot create a collection until "
                        + "there is at least 1 live node in the cluster.");

            String baseUrl = cli.getOptionValue("solrUrl");
            if (baseUrl == null) {
                String firstLiveNode = liveNodes.iterator().next();
                baseUrl = cloudSolrClient.getZkStateReader().getBaseUrlForNodeName(firstLiveNode);
            }

            String collectionName = cli.getOptionValue(NAME);

            // build a URL to create the collection
            int numShards = optionAsInt(cli, "shards", 1);
            int replicationFactor = optionAsInt(cli, "replicationFactor", 1);
            int maxShardsPerNode = -1;

            if (cli.hasOption("maxShardsPerNode")) {
                maxShardsPerNode = Integer.parseInt(cli.getOptionValue("maxShardsPerNode"));
            } else {
                // need number of live nodes to determine maxShardsPerNode if it is not set
                int numNodes = liveNodes.size();
                maxShardsPerNode = ((numShards * replicationFactor) + numNodes - 1) / numNodes;
            }

            String confname = cli.getOptionValue("confname", collectionName);
            boolean configExistsInZk = cloudSolrClient.getZkStateReader().getZkClient()
                    .exists("/configs/" + confname, true);

            if (".system".equals(collectionName)) {
                //do nothing
            } else if (configExistsInZk) {
                echo("Re-using existing configuration directory " + confname);
            } else {
                Path confPath = ZkConfigManager.getConfigsetPath(cli.getOptionValue("confdir", DEFAULT_CONFIG_SET),
                        cli.getOptionValue("configsetsDir"));

                echo("Uploading " + confPath.toAbsolutePath().toString() + " for config " + confname
                        + " to ZooKeeper at " + cloudSolrClient.getZkHost());
                ((ZkClientClusterStateProvider) cloudSolrClient.getClusterStateProvider()).uploadConfig(confPath,
                        confname);
            }

            // since creating a collection is a heavy-weight operation, check for existence first
            String collectionListUrl = baseUrl + "/admin/collections?action=list";
            if (safeCheckCollectionExists(collectionListUrl, collectionName)) {
                throw new IllegalStateException("\nCollection '" + collectionName
                        + "' already exists!\nChecked collection existence using Collections API command:\n"
                        + collectionListUrl);
            }

            // doesn't seem to exist ... try to create
            String createCollectionUrl = String.format(Locale.ROOT,
                    "%s/admin/collections?action=CREATE&name=%s&numShards=%d&replicationFactor=%d&maxShardsPerNode=%d&collection.configName=%s",
                    baseUrl, collectionName, numShards, replicationFactor, maxShardsPerNode, confname);

            echo("\nCreating new collection '" + collectionName + "' using command:\n" + createCollectionUrl
                    + "\n");

            Map<String, Object> json = null;
            try {
                json = getJson(createCollectionUrl);
            } catch (SolrServerException sse) {
                throw new Exception(
                        "Failed to create collection '" + collectionName + "' due to: " + sse.getMessage());
            }

            CharArr arr = new CharArr();
            new JSONWriter(arr, 2).write(json);
            echo(arr.toString());
        }

        protected int optionAsInt(CommandLine cli, String option, int defaultVal) {
            return Integer.parseInt(cli.getOptionValue(option, String.valueOf(defaultVal)));
        }
    } // end CreateCollectionTool class

    public static class CreateCoreTool extends ToolBase {

        public CreateCoreTool() {
            this(System.out);
        }

        public CreateCoreTool(PrintStream stdout) {
            super(stdout);
        }

        public String getName() {
            return "create_core";
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] {
                    OptionBuilder.withArgName("URL").hasArg().isRequired(false)
                            .withDescription("Base Solr URL, default is " + DEFAULT_SOLR_URL).create("solrUrl"),
                    OptionBuilder.withArgName("NAME").hasArg().isRequired(true)
                            .withDescription("Name of the core to create.").create(NAME),
                    OptionBuilder.withArgName("CONFIG").hasArg().isRequired(false)
                            .withDescription(
                                    "Configuration directory to copy when creating the new core; default is "
                                            + DEFAULT_CONFIG_SET)
                            .create("confdir"),
                    OptionBuilder.withArgName("DIR").hasArg().isRequired(true)
                            .withDescription("Path to configsets directory on the local system.")
                            .create("configsetsDir") };
        }

        protected void runImpl(CommandLine cli) throws Exception {

            String solrUrl = cli.getOptionValue("solrUrl", DEFAULT_SOLR_URL);
            if (!solrUrl.endsWith("/"))
                solrUrl += "/";

            File configsetsDir = new File(cli.getOptionValue("configsetsDir"));
            if (!configsetsDir.isDirectory())
                throw new FileNotFoundException(configsetsDir.getAbsolutePath() + " not found!");

            String configSet = cli.getOptionValue("confdir", DEFAULT_CONFIG_SET);
            File configSetDir = new File(configsetsDir, configSet);
            if (!configSetDir.isDirectory()) {
                // we allow them to pass a directory instead of a configset name
                File possibleConfigDir = new File(configSet);
                if (possibleConfigDir.isDirectory()) {
                    configSetDir = possibleConfigDir;
                } else {
                    throw new FileNotFoundException("Specified config directory " + configSet + " not found in "
                            + configsetsDir.getAbsolutePath());
                }
            }

            String coreName = cli.getOptionValue(NAME);

            String systemInfoUrl = solrUrl + "admin/info/system";
            CloseableHttpClient httpClient = getHttpClient();
            String solrHome = null;
            try {
                Map<String, Object> systemInfo = getJson(httpClient, systemInfoUrl, 2, true);
                if ("solrcloud".equals(systemInfo.get("mode"))) {
                    throw new IllegalStateException("Solr at " + solrUrl
                            + " is running in SolrCloud mode, please use create_collection command instead.");
                }

                // convert raw JSON into user-friendly output
                solrHome = (String) systemInfo.get("solr_home");
                if (solrHome == null)
                    solrHome = configsetsDir.getParentFile().getAbsolutePath();

            } finally {
                closeHttpClient(httpClient);
            }

            String coreStatusUrl = solrUrl + "admin/cores?action=STATUS&core=" + coreName;
            if (safeCheckCoreExists(coreStatusUrl, coreName)) {
                throw new IllegalArgumentException("\nCore '" + coreName
                        + "' already exists!\nChecked core existence using Core API command:\n" + coreStatusUrl);
            }

            File coreInstanceDir = new File(solrHome, coreName);
            File confDir = new File(configSetDir, "conf");
            if (!coreInstanceDir.isDirectory()) {
                coreInstanceDir.mkdirs();
                if (!coreInstanceDir.isDirectory())
                    throw new IOException(
                            "Failed to create new core instance directory: " + coreInstanceDir.getAbsolutePath());

                if (confDir.isDirectory()) {
                    FileUtils.copyDirectoryToDirectory(confDir, coreInstanceDir);
                } else {
                    // hmmm ... the configset we're cloning doesn't have a conf sub-directory,
                    // we'll just assume it is OK if it has solrconfig.xml
                    if ((new File(configSetDir, "solrconfig.xml")).isFile()) {
                        FileUtils.copyDirectory(configSetDir, new File(coreInstanceDir, "conf"));
                    } else {
                        throw new IllegalArgumentException("\n" + configSetDir.getAbsolutePath()
                                + " doesn't contain a conf subdirectory or solrconfig.xml\n");
                    }
                }
                echo("\nCopying configuration to new core instance directory:\n"
                        + coreInstanceDir.getAbsolutePath());
            }

            String createCoreUrl = String.format(Locale.ROOT, "%sadmin/cores?action=CREATE&name=%s&instanceDir=%s",
                    solrUrl, coreName, coreName);

            echo("\nCreating new core '" + coreName + "' using command:\n" + createCoreUrl + "\n");

            try {
                Map<String, Object> json = getJson(createCoreUrl);
                CharArr arr = new CharArr();
                new JSONWriter(arr, 2).write(json);
                echo(arr.toString());
                echo("\n");
            } catch (Exception e) {
                /* create-core failed, cleanup the copied configset before propagating the error. */
                FileUtils.deleteDirectory(coreInstanceDir);
                throw e;
            }
        }
    } // end CreateCoreTool class

    public static class CreateTool extends ToolBase {

        public CreateTool() {
            this(System.out);
        }

        public CreateTool(PrintStream stdout) {
            super(stdout);
        }

        public String getName() {
            return "create";
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return CREATE_COLLECTION_OPTIONS;
        }

        protected void runImpl(CommandLine cli) throws Exception {

            String solrUrl = cli.getOptionValue("solrUrl", DEFAULT_SOLR_URL);
            if (!solrUrl.endsWith("/"))
                solrUrl += "/";

            String systemInfoUrl = solrUrl + "admin/info/system";
            CloseableHttpClient httpClient = getHttpClient();

            ToolBase tool = null;
            try {
                Map<String, Object> systemInfo = getJson(httpClient, systemInfoUrl, 2, true);
                if ("solrcloud".equals(systemInfo.get("mode"))) {
                    tool = new CreateCollectionTool(stdout);
                } else {
                    tool = new CreateCoreTool(stdout);
                }
                tool.runImpl(cli);
            } finally {
                closeHttpClient(httpClient);
            }
        }

    } // end CreateTool class

    public static class ConfigSetUploadTool extends ToolBase {

        public ConfigSetUploadTool() {
            this(System.out);
        }

        public ConfigSetUploadTool(PrintStream stdout) {
            super(stdout);
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] { OptionBuilder.withArgName("confname") // Comes out in help message
                    .hasArg() // Has one sub-argument
                    .isRequired(true) // confname argument must be present
                    .withDescription("Configset name on Zookeeper").create("confname"), // passed as -confname value
                    OptionBuilder.withArgName("confdir").hasArg().isRequired(true)
                            .withDescription("Local directory with configs").create("confdir"),
                    OptionBuilder.withArgName("configsetsDir").hasArg().isRequired(false)
                            .withDescription("Parent directory of example configsets").create("configsetsDir"),
                    OptionBuilder.withArgName("HOST").hasArg().isRequired(true)
                            .withDescription("Address of the Zookeeper ensemble; defaults to: " + ZK_HOST)
                            .create("zkHost") };
        }

        public String getName() {
            return "upconfig";
        }

        protected void runImpl(CommandLine cli) throws Exception {
            String zkHost = getZkHost(cli);
            if (zkHost == null) {
                throw new IllegalStateException("Solr at " + cli.getOptionValue("solrUrl")
                        + " is running in standalone server mode, upconfig can only be used when running in SolrCloud mode.\n");
            }

            String confName = cli.getOptionValue("confname");
            try (SolrZkClient zkClient = new SolrZkClient(zkHost, 30000)) {
                echo("\nConnecting to ZooKeeper at " + zkHost + " ...");
                Path confPath = ZkConfigManager.getConfigsetPath(cli.getOptionValue("confdir"),
                        cli.getOptionValue("configsetsDir"));

                echo("Uploading " + confPath.toAbsolutePath().toString() + " for config "
                        + cli.getOptionValue("confname") + " to ZooKeeper at " + zkHost);

                zkClient.upConfig(confPath, confName);
            } catch (Exception e) {
                log.error("Could not complete upconfig operation for reason: " + e.getMessage());
                throw (e);
            }

        }
    }

    public static class ConfigSetDownloadTool extends ToolBase {

        public ConfigSetDownloadTool() {
            this(System.out);
        }

        public ConfigSetDownloadTool(PrintStream stdout) {
            super(stdout);
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] {
                    OptionBuilder.withArgName("confname").hasArg().isRequired(true)
                            .withDescription("Configset name on Zookeeper").create("confname"),
                    OptionBuilder.withArgName("confdir").hasArg().isRequired(true)
                            .withDescription("Local directory with configs").create("confdir"),
                    OptionBuilder.withArgName("HOST").hasArg().isRequired(true)
                            .withDescription("Address of the Zookeeper ensemble; defaults to: " + ZK_HOST)
                            .create("zkHost") };
        }

        public String getName() {
            return "downconfig";
        }

        protected void runImpl(CommandLine cli) throws Exception {

            String zkHost = getZkHost(cli);
            if (zkHost == null) {
                throw new IllegalStateException("Solr at " + cli.getOptionValue("solrUrl")
                        + " is running in standalone server mode, downconfig can only be used when running in SolrCloud mode.\n");
            }

            try (SolrZkClient zkClient = new SolrZkClient(zkHost, 30000)) {
                echo("\nConnecting to ZooKeeper at " + zkHost + " ...");
                String confName = cli.getOptionValue("confname");
                String confDir = cli.getOptionValue("confdir");
                Path configSetPath = Paths.get(confDir);
                // we try to be nice about having the "conf" in the directory, and we create it if it's not there.
                if (configSetPath.endsWith("/conf") == false) {
                    configSetPath = Paths.get(configSetPath.toString(), "conf");
                }
                if (Files.exists(configSetPath) == false) {
                    Files.createDirectories(configSetPath);
                }
                echo("Downloading configset " + confName + " from ZooKeeper at " + zkHost + " to directory "
                        + configSetPath.toAbsolutePath());

                zkClient.downConfig(confName, configSetPath);
            } catch (Exception e) {
                log.error("Could not complete downconfig operation for reason: " + e.getMessage());
                throw (e);
            }

        }

    } // End ConfigSetDownloadTool class

    public static class ZkRmTool extends ToolBase {

        public ZkRmTool() {
            this(System.out);
        }

        public ZkRmTool(PrintStream stdout) {
            super(stdout);
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] {
                    OptionBuilder.withArgName("path").hasArg().isRequired(true).withDescription("Path to remove")
                            .create("path"),
                    OptionBuilder.withArgName("recurse").hasArg().isRequired(false)
                            .withDescription("Recurse (true|false, default is false)").create("recurse"),
                    OptionBuilder.withArgName("HOST").hasArg().isRequired(true)
                            .withDescription("Address of the Zookeeper ensemble; defaults to: " + ZK_HOST)
                            .create("zkHost") };
        }

        public String getName() {
            return "rm";
        }

        protected void runImpl(CommandLine cli) throws Exception {

            String zkHost = getZkHost(cli);

            if (zkHost == null) {
                throw new IllegalStateException("Solr at " + cli.getOptionValue("zkHost")
                        + " is running in standalone server mode, 'zk rm' can only be used when running in SolrCloud mode.\n");
            }
            String target = cli.getOptionValue("path");
            Boolean recurse = Boolean.parseBoolean(cli.getOptionValue("recurse"));

            String znode = target;
            if (target.toLowerCase(Locale.ROOT).startsWith("zk:")) {
                znode = target.substring(3);
            }
            if (znode.equals("/")) {
                throw new SolrServerException("You may not remove the root ZK node ('/')!");
            }
            echo("\nConnecting to ZooKeeper at " + zkHost + " ...");
            try (SolrZkClient zkClient = new SolrZkClient(zkHost, 30000)) {
                if (recurse == false && zkClient.getChildren(znode, null, true).size() != 0) {
                    throw new SolrServerException(
                            "Zookeeper node " + znode + " has children and recurse has NOT been specified");
                }
                echo("Removing Zookeeper node " + znode + " from ZooKeeper at " + zkHost + " recurse: "
                        + Boolean.toString(recurse));
                zkClient.clean(znode);
            } catch (Exception e) {
                log.error("Could not complete rm operation for reason: " + e.getMessage());
                throw (e);
            }

        }

    } // End RmTool class

    public static class ZkLsTool extends ToolBase {

        public ZkLsTool() {
            this(System.out);
        }

        public ZkLsTool(PrintStream stdout) {
            super(stdout);
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] {
                    OptionBuilder.withArgName("path").hasArg().isRequired(true).withDescription("Path to list")
                            .create("path"),
                    OptionBuilder.withArgName("recurse").hasArg().isRequired(false)
                            .withDescription("Recurse (true|false, default is false)").create("recurse"),
                    OptionBuilder.withArgName("HOST").hasArg().isRequired(true)
                            .withDescription("Address of the Zookeeper ensemble; defaults to: " + ZK_HOST)
                            .create("zkHost") };
        }

        public String getName() {
            return "ls";
        }

        protected void runImpl(CommandLine cli) throws Exception {

            String zkHost = getZkHost(cli);

            if (zkHost == null) {
                throw new IllegalStateException("Solr at " + cli.getOptionValue("zkHost")
                        + " is running in standalone server mode, 'zk ls' can only be used when running in SolrCloud mode.\n");
            }

            try (SolrZkClient zkClient = new SolrZkClient(zkHost, 30000)) {
                echo("\nConnecting to ZooKeeper at " + zkHost + " ...");

                String znode = cli.getOptionValue("path");
                Boolean recurse = Boolean.parseBoolean(cli.getOptionValue("recurse"));
                echo("Getting listing for Zookeeper node " + znode + " from ZooKeeper at " + zkHost + " recurse: "
                        + Boolean.toString(recurse));
                stdout.print(zkClient.listZnode(znode, recurse));
            } catch (Exception e) {
                log.error("Could not complete ls operation for reason: " + e.getMessage());
                throw (e);
            }
        }
    } // End zkLsTool class

    public static class ZkMkrootTool extends ToolBase {

        public ZkMkrootTool() {
            this(System.out);
        }

        public ZkMkrootTool(PrintStream stdout) {
            super(stdout);
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] {
                    OptionBuilder.withArgName("path").hasArg().isRequired(true).withDescription("Path to create")
                            .create("path"),
                    OptionBuilder.withArgName("HOST").hasArg().isRequired(true)
                            .withDescription("Address of the Zookeeper ensemble; defaults to: " + ZK_HOST)
                            .create("zkHost") };
        }

        public String getName() {
            return "mkroot";
        }

        protected void runImpl(CommandLine cli) throws Exception {

            String zkHost = getZkHost(cli);

            if (zkHost == null) {
                throw new IllegalStateException("Solr at " + cli.getOptionValue("zkHost")
                        + " is running in standalone server mode, 'zk mkroot' can only be used when running in SolrCloud mode.\n");
            }

            try (SolrZkClient zkClient = new SolrZkClient(zkHost, 30000)) {
                echo("\nConnecting to ZooKeeper at " + zkHost + " ...");

                String znode = cli.getOptionValue("path");
                echo("Creating Zookeeper path " + znode + " on ZooKeeper at " + zkHost);
                zkClient.makePath(znode, true);
            } catch (Exception e) {
                log.error("Could not complete mkroot operation for reason: " + e.getMessage());
                throw (e);
            }
        }
    } // End zkMkrootTool class

    public static class ZkCpTool extends ToolBase {

        public ZkCpTool() {
            this(System.out);
        }

        public ZkCpTool(PrintStream stdout) {
            super(stdout);
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] {
                    OptionBuilder.withArgName("src").hasArg().isRequired(true)
                            .withDescription("Source file or directory, may be local or a Znode").create("src"),
                    OptionBuilder.withArgName("dst").hasArg().isRequired(true)
                            .withDescription("Destination of copy, may be local or a Znode.").create("dst"),
                    OptionBuilder.withArgName("recurse").hasArg().isRequired(false)
                            .withDescription("Recurse (true|false, default is false)").create("recurse"),
                    OptionBuilder.withArgName("HOST").hasArg().isRequired(true)
                            .withDescription("Address of the Zookeeper ensemble; defaults to: " + ZK_HOST)
                            .create("zkHost") };
        }

        public String getName() {
            return "cp";
        }

        protected void runImpl(CommandLine cli) throws Exception {

            String zkHost = getZkHost(cli);
            if (zkHost == null) {
                throw new IllegalStateException("Solr at " + cli.getOptionValue("solrUrl")
                        + " is running in standalone server mode, cp can only be used when running in SolrCloud mode.\n");
            }

            try (SolrZkClient zkClient = new SolrZkClient(zkHost, 30000)) {
                echo("\nConnecting to ZooKeeper at " + zkHost + " ...");
                String src = cli.getOptionValue("src");
                String dst = cli.getOptionValue("dst");
                Boolean recurse = Boolean.parseBoolean(cli.getOptionValue("recurse"));
                echo("Copying from '" + src + "' to '" + dst + "'. ZooKeeper at " + zkHost);

                boolean srcIsZk = src.toLowerCase(Locale.ROOT).startsWith("zk:");
                boolean dstIsZk = dst.toLowerCase(Locale.ROOT).startsWith("zk:");

                String srcName = src;
                if (srcIsZk) {
                    srcName = src.substring(3);
                } else if (srcName.toLowerCase(Locale.ROOT).startsWith("file:")) {
                    srcName = srcName.substring(5);
                }

                String dstName = dst;
                if (dstIsZk) {
                    dstName = dst.substring(3);
                } else {
                    if (dstName.toLowerCase(Locale.ROOT).startsWith("file:")) {
                        dstName = dstName.substring(5);
                    }
                }
                zkClient.zkTransfer(srcName, srcIsZk, dstName, dstIsZk, recurse);
            } catch (Exception e) {
                log.error("Could not complete the zk operation for reason: " + e.getMessage());
                throw (e);
            }
        }
    } // End CpTool class 

    public static class ZkMvTool extends ToolBase {

        public ZkMvTool() {
            this(System.out);
        }

        public ZkMvTool(PrintStream stdout) {
            super(stdout);
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] {
                    OptionBuilder.withArgName("src").hasArg().isRequired(true)
                            .withDescription("Source Znode to movej from.").create("src"),
                    OptionBuilder.withArgName("dst").hasArg().isRequired(true)
                            .withDescription("Destination Znode to move to.").create("dst"),
                    OptionBuilder.withArgName("HOST").hasArg().isRequired(true)
                            .withDescription("Address of the Zookeeper ensemble; defaults to: " + ZK_HOST)
                            .create("zkHost") };
        }

        public String getName() {
            return "mv";
        }

        protected void runImpl(CommandLine cli) throws Exception {

            String zkHost = getZkHost(cli);
            if (zkHost == null) {
                throw new IllegalStateException("Solr at " + cli.getOptionValue("solrUrl")
                        + " is running in standalone server mode, downconfig can only be used when running in SolrCloud mode.\n");
            }

            try (SolrZkClient zkClient = new SolrZkClient(zkHost, 30000)) {
                echo("\nConnecting to ZooKeeper at " + zkHost + " ...");
                String src = cli.getOptionValue("src");
                String dst = cli.getOptionValue("dst");

                if (src.toLowerCase(Locale.ROOT).startsWith("file:")
                        || dst.toLowerCase(Locale.ROOT).startsWith("file:")) {
                    throw new SolrServerException("mv command operates on znodes and 'file:' has been specified.");
                }
                String source = src;
                if (src.toLowerCase(Locale.ROOT).startsWith("zk")) {
                    source = src.substring(3);
                }

                String dest = dst;
                if (dst.toLowerCase(Locale.ROOT).startsWith("zk")) {
                    dest = dst.substring(3);
                }

                echo("Moving Znode " + source + " to " + dest + " on ZooKeeper at " + zkHost);
                zkClient.moveZnode(source, dest);
            } catch (Exception e) {
                log.error("Could not complete mv operation for reason: " + e.getMessage());
                throw (e);
            }

        }
    } // End MvTool class

    public static class DeleteTool extends ToolBase {

        public DeleteTool() {
            this(System.out);
        }

        public DeleteTool(PrintStream stdout) {
            super(stdout);
        }

        public String getName() {
            return "delete";
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] {
                    OptionBuilder.withArgName("URL").hasArg().isRequired(false)
                            .withDescription("Base Solr URL, default is " + DEFAULT_SOLR_URL).create("solrUrl"),
                    OptionBuilder.withArgName("NAME").hasArg().isRequired(true)
                            .withDescription("Name of the core / collection to delete.").create(NAME),
                    OptionBuilder.withArgName("true|false").hasArg().isRequired(false).withDescription(
                            "Flag to indicate if the underlying configuration directory for a collection should also be deleted; default is true")
                            .create("deleteConfig"),
                    OptionBuilder.isRequired(false).withDescription(
                            "Skip safety checks when deleting the configuration directory used by a collection")
                            .create("forceDeleteConfig"),
                    OptionBuilder.withArgName("HOST").hasArg().isRequired(false)
                            .withDescription("Address of the Zookeeper ensemble; defaults to: " + ZK_HOST)
                            .create("zkHost") };
        }

        protected void runImpl(CommandLine cli) throws Exception {

            String solrUrl = cli.getOptionValue("solrUrl", DEFAULT_SOLR_URL);
            if (!solrUrl.endsWith("/"))
                solrUrl += "/";

            String systemInfoUrl = solrUrl + "admin/info/system";
            CloseableHttpClient httpClient = getHttpClient();
            try {
                Map<String, Object> systemInfo = getJson(httpClient, systemInfoUrl, 2, true);
                if ("solrcloud".equals(systemInfo.get("mode"))) {
                    deleteCollection(cli);
                } else {
                    deleteCore(cli, httpClient, solrUrl);
                }
            } finally {
                closeHttpClient(httpClient);
            }
        }

        protected void deleteCollection(CommandLine cli) throws Exception {
            String zkHost = getZkHost(cli);
            try (CloudSolrClient cloudSolrClient = new CloudSolrClient.Builder().withZkHost(zkHost).build()) {
                echo("Connecting to ZooKeeper at " + zkHost);
                cloudSolrClient.connect();
                deleteCollection(cloudSolrClient, cli);
            }
        }

        protected void deleteCollection(CloudSolrClient cloudSolrClient, CommandLine cli) throws Exception {
            Set<String> liveNodes = cloudSolrClient.getZkStateReader().getClusterState().getLiveNodes();
            if (liveNodes.isEmpty())
                throw new IllegalStateException("No live nodes found! Cannot delete a collection until "
                        + "there is at least 1 live node in the cluster.");

            String firstLiveNode = liveNodes.iterator().next();
            ZkStateReader zkStateReader = cloudSolrClient.getZkStateReader();
            String baseUrl = zkStateReader.getBaseUrlForNodeName(firstLiveNode);
            String collectionName = cli.getOptionValue(NAME);
            if (!zkStateReader.getClusterState().hasCollection(collectionName)) {
                throw new IllegalArgumentException("Collection " + collectionName + " not found!");
            }

            String configName = zkStateReader.readConfigName(collectionName);
            boolean deleteConfig = "true".equals(cli.getOptionValue("deleteConfig", "true"));
            if (deleteConfig && configName != null) {
                if (cli.hasOption("forceDeleteConfig")) {
                    log.warn("Skipping safety checks, configuration directory " + configName
                            + " will be deleted with impunity.");
                } else {
                    // need to scan all Collections to see if any are using the config
                    Set<String> collections = zkStateReader.getClusterState().getCollectionsMap().keySet();

                    // give a little note to the user if there are many collections in case it takes a while
                    if (collections.size() > 50)
                        log.info("Scanning " + collections.size()
                                + " to ensure no other collections are using config " + configName);

                    for (String next : collections) {
                        if (collectionName.equals(next))
                            continue; // don't check the collection we're deleting

                        if (configName.equals(zkStateReader.readConfigName(next))) {
                            deleteConfig = false;
                            log.warn("Configuration directory " + configName + " is also being used by " + next
                                    + "; configuration will not be deleted from ZooKeeper. You can pass the -forceDeleteConfig flag to force delete.");
                            break;
                        }
                    }
                }
            }

            String deleteCollectionUrl = String.format(Locale.ROOT, "%s/admin/collections?action=DELETE&name=%s",
                    baseUrl, collectionName);

            echo("\nDeleting collection '" + collectionName + "' using command:\n" + deleteCollectionUrl + "\n");

            Map<String, Object> json = null;
            try {
                json = getJson(deleteCollectionUrl);
            } catch (SolrServerException sse) {
                throw new Exception(
                        "Failed to delete collection '" + collectionName + "' due to: " + sse.getMessage());
            }

            if (deleteConfig) {
                String configZnode = "/configs/" + configName;
                try {
                    zkStateReader.getZkClient().clean(configZnode);
                } catch (Exception exc) {
                    System.err.println("\nWARNING: Failed to delete configuration directory " + configZnode
                            + " in ZooKeeper due to: " + exc.getMessage()
                            + "\nYou'll need to manually delete this znode using the zkcli script.");
                }
            }

            if (json != null) {
                CharArr arr = new CharArr();
                new JSONWriter(arr, 2).write(json);
                echo(arr.toString());
                echo("\n");
            }
        }

        protected void deleteCore(CommandLine cli, CloseableHttpClient httpClient, String solrUrl)
                throws Exception {
            String coreName = cli.getOptionValue(NAME);
            String deleteCoreUrl = String.format(Locale.ROOT,
                    "%sadmin/cores?action=UNLOAD&core=%s&deleteIndex=true&deleteDataDir=true&deleteInstanceDir=true",
                    solrUrl, coreName);

            echo("\nDeleting core '" + coreName + "' using command:\n" + deleteCoreUrl + "\n");

            Map<String, Object> json = null;
            try {
                json = getJson(deleteCoreUrl);
            } catch (SolrServerException sse) {
                throw new Exception("Failed to delete core '" + coreName + "' due to: " + sse.getMessage());
            }

            if (json != null) {
                CharArr arr = new CharArr();
                new JSONWriter(arr, 2).write(json);
                echo(arr.toString());
                echo("\n");
            }
        }
    } // end DeleteTool class

    /**
     * Sends a POST to the Config API to perform a specified action.
     */
    public static class ConfigTool extends ToolBase {

        public ConfigTool() {
            this(System.out);
        }

        public ConfigTool(PrintStream stdout) {
            super(stdout);
        }

        public String getName() {
            return "config";
        }

        @SuppressWarnings("static-access")
        @Override
        public Option[] getOptions() {
            Option[] configOptions = new Option[] {
                    OptionBuilder.withArgName("ACTION").hasArg().isRequired(false).withDescription(
                            "Config API action, one of: set-property, unset-property; default is set-property")
                            .create("action"),
                    OptionBuilder.withArgName("PROP").hasArg().isRequired(true).withDescription(
                            "Name of the Config API property to apply the action to, such as: updateHandler.autoSoftCommit.maxTime")
                            .create("property"),
                    OptionBuilder.withArgName("VALUE").hasArg().isRequired(false)
                            .withDescription("Set the property to this value; accepts JSON objects and strings")
                            .create("value"),
                    OptionBuilder.withArgName("HOST").hasArg().isRequired(false)
                            .withDescription(
                                    "Base Solr URL, which can be used to determine the zkHost if that's not known")
                            .create("solrUrl") };
            return joinOptions(configOptions, cloudOptions);
        }

        protected void runImpl(CommandLine cli) throws Exception {
            String solrUrl = resolveSolrUrl(cli);
            String action = cli.getOptionValue("action", "set-property");
            String collection = cli.getOptionValue("collection", "gettingstarted");
            String property = cli.getOptionValue("property");
            String value = cli.getOptionValue("value");

            Map<String, Object> jsonObj = new HashMap<>();
            if (value != null) {
                Map<String, String> setMap = new HashMap<>();
                setMap.put(property, value);
                jsonObj.put(action, setMap);
            } else {
                jsonObj.put(action, property);
            }

            CharArr arr = new CharArr();
            (new JSONWriter(arr, 0)).write(jsonObj);
            String jsonBody = arr.toString();

            String updatePath = "/" + collection + "/config";

            echo("\nPOSTing request to Config API: " + solrUrl + updatePath);
            echo(jsonBody);

            try (SolrClient solrClient = new Builder(solrUrl).build()) {
                NamedList<Object> result = postJsonToSolr(solrClient, updatePath, jsonBody);
                Integer statusCode = (Integer) ((NamedList) result.get("responseHeader")).get("status");
                if (statusCode == 0) {
                    if (value != null) {
                        echo("Successfully " + action + " " + property + " to " + value);
                    } else {
                        echo("Successfully " + action + " " + property);
                    }
                } else {
                    throw new Exception("Failed to " + action + " property due to:\n" + result);
                }
            }
        }

    } // end ConfigTool class

    /**
     * Supports an interactive session with the user to launch (or relaunch the -e cloud example)
     */
    public static class RunExampleTool extends ToolBase {

        private static final String PROMPT_FOR_NUMBER = "Please enter %s [%d]: ";
        private static final String PROMPT_FOR_NUMBER_IN_RANGE = "Please enter %s between %d and %d [%d]: ";
        private static final String PROMPT_NUMBER_TOO_SMALL = "%d is too small! " + PROMPT_FOR_NUMBER_IN_RANGE;
        private static final String PROMPT_NUMBER_TOO_LARGE = "%d is too large! " + PROMPT_FOR_NUMBER_IN_RANGE;

        protected InputStream userInput;
        protected Executor executor;
        protected String script;
        protected File serverDir;
        protected File exampleDir;
        protected String urlScheme;

        /**
         * Default constructor used by the framework when running as a command-line application.
         */
        public RunExampleTool() {
            this(null, System.in, System.out);
        }

        public RunExampleTool(Executor executor, InputStream userInput, PrintStream stdout) {
            super(stdout);
            this.executor = (executor != null) ? executor : new DefaultExecutor();
            this.userInput = userInput;
        }

        public String getName() {
            return "run_example";
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] { OptionBuilder.isRequired(false).withDescription(
                    "Don't prompt for input; accept all defaults when running examples that accept user input")
                    .create("noprompt"),
                    OptionBuilder.withArgName("NAME").hasArg().isRequired(true)
                            .withDescription(
                                    "Name of the example to launch, one of: cloud, techproducts, dih, schemaless")
                            .withLongOpt("example").create('e'),
                    OptionBuilder.withArgName("PATH").hasArg().isRequired(false)
                            .withDescription("Path to the bin/solr script").create("script"),
                    OptionBuilder.withArgName("DIR").hasArg().isRequired(true)
                            .withDescription("Path to the Solr server directory.").withLongOpt("serverDir")
                            .create('d'),
                    OptionBuilder.withArgName("DIR").hasArg().isRequired(false).withDescription(
                            "Path to the Solr example directory; if not provided, ${serverDir}/../example is expected to exist.")
                            .create("exampleDir"),
                    OptionBuilder.withArgName("SCHEME").hasArg().isRequired(false)
                            .withDescription("Solr URL scheme: http or https, defaults to http if not specified")
                            .create("urlScheme"),
                    OptionBuilder.withArgName("PORT").hasArg().isRequired(false)
                            .withDescription("Specify the port to start the Solr HTTP listener on; default is 8983")
                            .withLongOpt("port").create('p'),
                    OptionBuilder.withArgName("HOSTNAME").hasArg().isRequired(false)
                            .withDescription("Specify the hostname for this Solr instance").withLongOpt("host")
                            .create('h'),
                    OptionBuilder.withArgName("ZKHOST").hasArg().isRequired(false).withDescription(
                            "ZooKeeper connection string; only used when running in SolrCloud mode using -c")
                            .withLongOpt("zkhost").create('z'),
                    OptionBuilder.isRequired(false).withDescription(
                            "Start Solr in SolrCloud mode; if -z not supplied, an embedded ZooKeeper instance is started on Solr port+1000, such as 9983 if Solr is bound to 8983")
                            .withLongOpt("cloud").create('c'),
                    OptionBuilder.withArgName("MEM").hasArg().isRequired(false).withDescription(
                            "Sets the min (-Xms) and max (-Xmx) heap size for the JVM, such as: -m 4g results in: -Xms4g -Xmx4g; by default, this script sets the heap size to 512m")
                            .withLongOpt("memory").create('m'),
                    OptionBuilder.withArgName("OPTS").hasArg().isRequired(false).withDescription(
                            "Additional options to be passed to the JVM when starting example Solr server(s)")
                            .withLongOpt("addlopts").create('a') };
        }

        protected void runImpl(CommandLine cli) throws Exception {
            this.urlScheme = cli.getOptionValue("urlScheme", "http");

            serverDir = new File(cli.getOptionValue("serverDir"));
            if (!serverDir.isDirectory())
                throw new IllegalArgumentException("Value of -serverDir option is invalid! "
                        + serverDir.getAbsolutePath() + " is not a directory!");

            script = cli.getOptionValue("script");
            if (script != null) {
                if (!(new File(script)).isFile())
                    throw new IllegalArgumentException(
                            "Value of -script option is invalid! " + script + " not found");
            } else {
                File scriptFile = new File(serverDir.getParentFile(), "bin/solr");
                if (scriptFile.isFile()) {
                    script = scriptFile.getAbsolutePath();
                } else {
                    scriptFile = new File(serverDir.getParentFile(), "bin/solr.cmd");
                    if (scriptFile.isFile()) {
                        script = scriptFile.getAbsolutePath();
                    } else {
                        throw new IllegalArgumentException(
                                "Cannot locate the bin/solr script! Please pass -script to this application.");
                    }
                }
            }

            exampleDir = (cli.hasOption("exampleDir")) ? new File(cli.getOptionValue("exampleDir"))
                    : new File(serverDir.getParent(), "example");
            if (!exampleDir.isDirectory())
                throw new IllegalArgumentException("Value of -exampleDir option is invalid! "
                        + exampleDir.getAbsolutePath() + " is not a directory!");

            if (verbose) {
                echo("Running with\nserverDir=" + serverDir.getAbsolutePath() + ",\nexampleDir="
                        + exampleDir.getAbsolutePath() + "\nscript=" + script);
            }

            String exampleType = cli.getOptionValue("example");
            if ("cloud".equals(exampleType)) {
                runCloudExample(cli);
            } else if ("dih".equals(exampleType)) {
                runDihExample(cli);
            } else if ("techproducts".equals(exampleType) || "schemaless".equals(exampleType)) {
                runExample(cli, exampleType);
            } else {
                throw new IllegalArgumentException("Unsupported example " + exampleType
                        + "! Please choose one of: cloud, dih, schemaless, or techproducts");
            }
        }

        protected void runDihExample(CommandLine cli) throws Exception {
            File dihSolrHome = new File(exampleDir, "example-DIH/solr");
            if (!dihSolrHome.isDirectory()) {
                dihSolrHome = new File(serverDir.getParentFile(), "example/example-DIH/solr");
                if (!dihSolrHome.isDirectory()) {
                    throw new Exception("example/example-DIH/solr directory not found");
                }
            }

            boolean isCloudMode = cli.hasOption('c');
            String zkHost = cli.getOptionValue('z');
            int port = Integer.parseInt(cli.getOptionValue('p', "8983"));

            Map<String, Object> nodeStatus = startSolr(dihSolrHome, isCloudMode, cli, port, zkHost, 30);
            String solrUrl = (String) nodeStatus.get("baseUrl");
            echo("\nSolr dih example launched successfully. Direct your Web browser to " + solrUrl
                    + " to visit the Solr Admin UI");
        }

        protected void runExample(CommandLine cli, String exampleName) throws Exception {
            File exDir = setupExampleDir(serverDir, exampleDir, exampleName);
            String collectionName = "schemaless".equals(exampleName) ? "gettingstarted" : exampleName;
            String configSet = "techproducts".equals(exampleName) ? "sample_techproducts_configs"
                    : "data_driven_schema_configs";

            boolean isCloudMode = cli.hasOption('c');
            String zkHost = cli.getOptionValue('z');
            int port = Integer.parseInt(cli.getOptionValue('p', "8983"));
            Map<String, Object> nodeStatus = startSolr(new File(exDir, "solr"), isCloudMode, cli, port, zkHost, 30);

            // invoke the CreateTool
            File configsetsDir = new File(serverDir, "solr/configsets");

            String solrUrl = (String) nodeStatus.get("baseUrl");

            // safe check if core / collection already exists
            boolean alreadyExists = false;
            if (nodeStatus.get("cloud") != null) {
                String collectionListUrl = solrUrl + "/admin/collections?action=list";
                if (safeCheckCollectionExists(collectionListUrl, collectionName)) {
                    alreadyExists = true;
                    echo("\nWARNING: Collection '" + collectionName
                            + "' already exists!\nChecked collection existence using Collections API command:\n"
                            + collectionListUrl + "\n");
                }
            } else {
                String coreName = collectionName;
                String coreStatusUrl = solrUrl + "/admin/cores?action=STATUS&core=" + coreName;
                if (safeCheckCoreExists(coreStatusUrl, coreName)) {
                    alreadyExists = true;
                    echo("\nWARNING: Core '" + coreName
                            + "' already exists!\nChecked core existence using Core API command:\n" + coreStatusUrl
                            + "\n");
                }
            }

            if (!alreadyExists) {
                String[] createArgs = new String[] { "-name", collectionName, "-shards", "1", "-replicationFactor",
                        "1", "-confname", collectionName, "-confdir", configSet, "-configsetsDir",
                        configsetsDir.getAbsolutePath(), "-solrUrl", solrUrl };
                CreateTool createTool = new CreateTool(stdout);
                int createCode = createTool.runTool(
                        processCommandLineArgs(joinCommonAndToolOptions(createTool.getOptions()), createArgs));
                if (createCode != 0)
                    throw new Exception(
                            "Failed to create " + collectionName + " using command: " + Arrays.asList(createArgs));
            }

            if ("techproducts".equals(exampleName) && !alreadyExists) {

                File exampledocsDir = new File(exampleDir, "exampledocs");
                if (!exampledocsDir.isDirectory()) {
                    File readOnlyExampleDir = new File(serverDir.getParentFile(), "example");
                    if (readOnlyExampleDir.isDirectory()) {
                        exampledocsDir = new File(readOnlyExampleDir, "exampledocs");
                    }
                }

                if (exampledocsDir.isDirectory()) {
                    String updateUrl = String.format(Locale.ROOT, "%s/%s/update", solrUrl, collectionName);
                    echo("Indexing tech product example docs from " + exampledocsDir.getAbsolutePath());

                    String currentPropVal = System.getProperty("url");
                    System.setProperty("url", updateUrl);
                    SimplePostTool.main(new String[] { exampledocsDir.getAbsolutePath() + "/*.xml" });
                    if (currentPropVal != null) {
                        System.setProperty("url", currentPropVal); // reset
                    } else {
                        System.clearProperty("url");
                    }
                } else {
                    echo("exampledocs directory not found, skipping indexing step for the techproducts example");
                }
            }

            echo("\nSolr " + exampleName + " example launched successfully. Direct your Web browser to " + solrUrl
                    + " to visit the Solr Admin UI");
        }

        protected void runCloudExample(CommandLine cli) throws Exception {

            boolean prompt = !cli.hasOption("noprompt");
            int numNodes = 2;
            int[] cloudPorts = new int[] { 8983, 7574, 8984, 7575 };
            File cloudDir = new File(exampleDir, "cloud");
            if (!cloudDir.isDirectory())
                cloudDir.mkdir();

            echo("\nWelcome to the SolrCloud example!\n");

            Scanner readInput = prompt ? new Scanner(userInput, UTF_8.name()) : null;
            if (prompt) {
                echo("This interactive session will help you launch a SolrCloud cluster on your local workstation.");

                // get the number of nodes to start
                numNodes = promptForInt(readInput,
                        "To begin, how many Solr nodes would you like to run in your local cluster? (specify 1-4 nodes) [2]: ",
                        "a number", numNodes, 1, 4);

                echo("Ok, let's start up " + numNodes + " Solr nodes for your example SolrCloud cluster.");

                // get the ports for each port
                for (int n = 0; n < numNodes; n++) {
                    String promptMsg = String.format(Locale.ROOT, "Please enter the port for node%d [%d]: ",
                            (n + 1), cloudPorts[n]);
                    int port = promptForPort(readInput, n + 1, promptMsg, cloudPorts[n]);
                    while (!isPortAvailable(port)) {
                        port = promptForPort(readInput, n + 1, "Oops! Looks like port " + port
                                + " is already being used by another process. Please choose a different port.",
                                cloudPorts[n]);
                    }

                    cloudPorts[n] = port;
                    if (verbose)
                        echo("Using port " + port + " for node " + (n + 1));
                }
            } else {
                echo("Starting up " + numNodes + " Solr nodes for your example SolrCloud cluster.\n");
            }

            // setup a unique solr.solr.home directory for each node
            File node1Dir = setupExampleDir(serverDir, cloudDir, "node1");
            for (int n = 2; n <= numNodes; n++) {
                File nodeNDir = new File(cloudDir, "node" + n);
                if (!nodeNDir.isDirectory()) {
                    echo("Cloning " + node1Dir.getAbsolutePath() + " into\n   " + nodeNDir.getAbsolutePath());
                    FileUtils.copyDirectory(node1Dir, nodeNDir);
                } else {
                    echo(nodeNDir.getAbsolutePath() + " already exists.");
                }
            }

            // deal with extra args passed to the script to run the example
            String zkHost = cli.getOptionValue('z');

            // start the first node (most likely with embedded ZK)
            Map<String, Object> nodeStatus = startSolr(new File(node1Dir, "solr"), true, cli, cloudPorts[0], zkHost,
                    30);

            if (zkHost == null) {
                Map<String, Object> cloudStatus = (Map<String, Object>) nodeStatus.get("cloud");
                if (cloudStatus != null) {
                    String zookeeper = (String) cloudStatus.get("ZooKeeper");
                    if (zookeeper != null)
                        zkHost = zookeeper;
                }
                if (zkHost == null)
                    throw new Exception("Could not get the ZooKeeper connection string for node1!");
            }

            if (numNodes > 1) {
                // start the other nodes
                for (int n = 1; n < numNodes; n++)
                    startSolr(new File(cloudDir, "node" + (n + 1) + "/solr"), true, cli, cloudPorts[n], zkHost, 30);
            }

            String solrUrl = (String) nodeStatus.get("baseUrl");
            if (solrUrl.endsWith("/"))
                solrUrl = solrUrl.substring(0, solrUrl.length() - 1);

            // wait until live nodes == numNodes
            waitToSeeLiveNodes(10 /* max wait */, zkHost, numNodes);

            // create the collection
            String collectionName = createCloudExampleCollection(numNodes, readInput, prompt, solrUrl);

            // update the config to enable soft auto-commit
            echo("\nEnabling auto soft-commits with maxTime 3 secs using the Config API");
            setCollectionConfigProperty(solrUrl, collectionName, "updateHandler.autoSoftCommit.maxTime", "3000");

            echo("\n\nSolrCloud example running, please visit: " + solrUrl + " \n");
        }

        protected void setCollectionConfigProperty(String solrUrl, String collectionName, String propName,
                String propValue) {
            ConfigTool configTool = new ConfigTool(stdout);
            String[] configArgs = new String[] { "-collection", collectionName, "-property", propName, "-value",
                    propValue, "-solrUrl", solrUrl };

            // let's not fail if we get this far ... just report error and finish up
            try {
                configTool.runTool(
                        processCommandLineArgs(joinCommonAndToolOptions(configTool.getOptions()), configArgs));
            } catch (Exception exc) {
                System.err.println("Failed to update '" + propName + "' property due to: " + exc);
            }
        }

        protected void waitToSeeLiveNodes(int maxWaitSecs, String zkHost, int numNodes) {
            CloudSolrClient cloudClient = null;
            try {
                cloudClient = new CloudSolrClient.Builder().withZkHost(zkHost).build();
                cloudClient.connect();
                Set<String> liveNodes = cloudClient.getZkStateReader().getClusterState().getLiveNodes();
                int numLiveNodes = (liveNodes != null) ? liveNodes.size() : 0;
                long timeout = System.nanoTime() + TimeUnit.NANOSECONDS.convert(maxWaitSecs, TimeUnit.SECONDS);
                while (System.nanoTime() < timeout && numLiveNodes < numNodes) {
                    echo("\nWaiting up to " + maxWaitSecs + " seconds to see " + (numNodes - numLiveNodes)
                            + " more nodes join the SolrCloud cluster ...");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException ie) {
                        Thread.interrupted();
                    }
                    liveNodes = cloudClient.getZkStateReader().getClusterState().getLiveNodes();
                    numLiveNodes = (liveNodes != null) ? liveNodes.size() : 0;
                }
                if (numLiveNodes < numNodes) {
                    echo("\nWARNING: Only " + numLiveNodes + " of " + numNodes + " are active in the cluster after "
                            + maxWaitSecs
                            + " seconds! Please check the solr.log for each node to look for errors.\n");
                }
            } catch (Exception exc) {
                System.err.println("Failed to see if " + numNodes + " joined the SolrCloud cluster due to: " + exc);
            } finally {
                if (cloudClient != null) {
                    try {
                        cloudClient.close();
                    } catch (Exception ignore) {
                    }
                }
            }
        }

        protected Map<String, Object> startSolr(File solrHomeDir, boolean cloudMode, CommandLine cli, int port,
                String zkHost, int maxWaitSecs) throws Exception {

            String extraArgs = readExtraArgs(cli.getArgs());

            String host = cli.getOptionValue('h');
            String memory = cli.getOptionValue('m');

            String hostArg = (host != null && !"localhost".equals(host)) ? " -h " + host : "";
            String zkHostArg = (zkHost != null) ? " -z " + zkHost : "";
            String memArg = (memory != null) ? " -m " + memory : "";
            String cloudModeArg = cloudMode ? "-cloud " : "";

            String addlOpts = cli.getOptionValue('a');
            String addlOptsArg = (addlOpts != null) ? " -a \"" + addlOpts + "\"" : "";

            File cwd = new File(System.getProperty("user.dir"));
            File binDir = (new File(script)).getParentFile();

            boolean isWindows = (OS.isFamilyDOS() || OS.isFamilyWin9x() || OS.isFamilyWindows());
            String callScript = (!isWindows && cwd.equals(binDir.getParentFile())) ? "bin/solr" : script;

            String cwdPath = cwd.getAbsolutePath();
            String solrHome = solrHomeDir.getAbsolutePath();

            // don't display a huge path for solr home if it is relative to the cwd
            if (!isWindows && cwdPath.length() > 1 && solrHome.startsWith(cwdPath))
                solrHome = solrHome.substring(cwdPath.length() + 1);

            String startCmd = String.format(Locale.ROOT, "%s start %s -p %d -s \"%s\" %s %s %s %s %s", callScript,
                    cloudModeArg, port, solrHome, hostArg, zkHostArg, memArg, extraArgs, addlOptsArg);
            startCmd = startCmd.replaceAll("\\s+", " ").trim(); // for pretty printing

            echo("\nStarting up Solr on port " + port + " using command:");
            echo(startCmd + "\n");

            String solrUrl = String.format(Locale.ROOT, "%s://%s:%d/solr", urlScheme,
                    (host != null ? host : "localhost"), port);

            Map<String, Object> nodeStatus = checkPortConflict(solrUrl, solrHomeDir, port, cli);
            if (nodeStatus != null)
                return nodeStatus; // the server they are trying to start is already running

            int code = 0;
            if (isWindows) {
                // On Windows, the execution doesn't return, so we have to execute async
                // and when calling the script, it seems to be inheriting the environment that launched this app
                // so we have to prune out env vars that may cause issues
                Map<String, String> startEnv = new HashMap<>();
                Map<String, String> procEnv = EnvironmentUtils.getProcEnvironment();
                if (procEnv != null) {
                    for (String envVar : procEnv.keySet()) {
                        String envVarVal = procEnv.get(envVar);
                        if (envVarVal != null && !"EXAMPLE".equals(envVar) && !envVar.startsWith("SOLR_")) {
                            startEnv.put(envVar, envVarVal);
                        }
                    }
                }
                executor.execute(org.apache.commons.exec.CommandLine.parse(startCmd), startEnv,
                        new DefaultExecuteResultHandler());

                // brief wait before proceeding on Windows
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException ie) {
                    // safe to ignore ...
                    Thread.interrupted();
                }

            } else {
                code = executor.execute(org.apache.commons.exec.CommandLine.parse(startCmd));
            }
            if (code != 0)
                throw new Exception("Failed to start Solr using command: " + startCmd);

            return getNodeStatus(solrUrl, maxWaitSecs);
        }

        protected Map<String, Object> checkPortConflict(String solrUrl, File solrHomeDir, int port,
                CommandLine cli) {
            // quickly check if the port is in use
            if (isPortAvailable(port))
                return null; // not in use ... try to start

            Map<String, Object> nodeStatus = null;
            try {
                nodeStatus = (new StatusTool()).getStatus(solrUrl);
            } catch (Exception ignore) {
                /* just trying to determine if this example is already running. */ }

            if (nodeStatus != null) {
                String solr_home = (String) nodeStatus.get("solr_home");
                if (solr_home != null) {
                    String solrHomePath = solrHomeDir.getAbsolutePath();
                    if (!solrHomePath.endsWith("/"))
                        solrHomePath += "/";
                    if (!solr_home.endsWith("/"))
                        solr_home += "/";

                    if (solrHomePath.equals(solr_home)) {
                        CharArr arr = new CharArr();
                        new JSONWriter(arr, 2).write(nodeStatus);
                        echo("Solr is already setup and running on port " + port + " with status:\n"
                                + arr.toString());
                        echo("\nIf this is not the example node you are trying to start, please choose a different port.");
                        nodeStatus.put("baseUrl", solrUrl);
                        return nodeStatus;
                    }
                }
            }

            throw new IllegalStateException("Port " + port + " is already being used by another process.");
        }

        protected String readExtraArgs(String[] extraArgsArr) {
            String extraArgs = "";
            if (extraArgsArr != null && extraArgsArr.length > 0) {
                StringBuilder sb = new StringBuilder();
                int app = 0;
                for (int e = 0; e < extraArgsArr.length; e++) {
                    String arg = extraArgsArr[e];
                    if ("e".equals(arg) || "example".equals(arg)) {
                        e++; // skip over the example arg
                        continue;
                    }

                    if (app > 0)
                        sb.append(" ");
                    sb.append(arg);
                    ++app;
                }
                extraArgs = sb.toString().trim();
            }
            return extraArgs;
        }

        protected String createCloudExampleCollection(int numNodes, Scanner readInput, boolean prompt,
                String solrUrl) throws Exception {
            // yay! numNodes SolrCloud nodes running
            int numShards = 2;
            int replicationFactor = 2;
            String cloudConfig = "data_driven_schema_configs";
            String collectionName = "gettingstarted";

            File configsetsDir = new File(serverDir, "solr/configsets");
            String collectionListUrl = solrUrl + "/admin/collections?action=list";

            if (prompt) {
                echo("\nNow let's create a new collection for indexing documents in your " + numNodes
                        + "-node cluster.");

                while (true) {
                    collectionName = prompt(readInput,
                            "Please provide a name for your new collection: [" + collectionName + "] ",
                            collectionName);

                    // Test for existence and then prompt to either create another or skip the create step
                    if (safeCheckCollectionExists(collectionListUrl, collectionName)) {
                        echo("\nCollection '" + collectionName + "' already exists!");
                        int oneOrTwo = promptForInt(readInput,
                                "Do you want to re-use the existing collection or create a new one? Enter 1 to reuse, 2 to create new [1]: ",
                                "a 1 or 2", 1, 1, 2);
                        if (oneOrTwo == 1) {
                            return collectionName;
                        } else {
                            continue;
                        }
                    } else {
                        break; // user selected a collection that doesn't exist ... proceed on
                    }
                }

                numShards = promptForInt(readInput,
                        "How many shards would you like to split " + collectionName + " into? [2]", "a shard count",
                        2, 1, 4);

                replicationFactor = promptForInt(readInput,
                        "How many replicas per shard would you like to create? [2] ", "a replication factor", 2, 1,
                        4);

                echo("Please choose a configuration for the " + collectionName
                        + " collection, available options are:");
                String validConfigs = "basic_configs, data_driven_schema_configs, or sample_techproducts_configs ["
                        + cloudConfig + "] ";
                cloudConfig = prompt(readInput, validConfigs, cloudConfig);

                // validate the cloudConfig name
                while (!isValidConfig(configsetsDir, cloudConfig)) {
                    echo(cloudConfig
                            + " is not a valid configuration directory! Please choose a configuration for the "
                            + collectionName + " collection, available options are:");
                    cloudConfig = prompt(readInput, validConfigs, cloudConfig);
                }
            } else {
                // must verify if default collection exists
                if (safeCheckCollectionExists(collectionListUrl, collectionName)) {
                    echo("\nCollection '" + collectionName
                            + "' already exists! Skipping collection creation step.");
                    return collectionName;
                }
            }

            // invoke the CreateCollectionTool
            String[] createArgs = new String[] { "-name", collectionName, "-shards", String.valueOf(numShards),
                    "-replicationFactor", String.valueOf(replicationFactor), "-confname", collectionName,
                    "-confdir", cloudConfig, "-configsetsDir", configsetsDir.getAbsolutePath(), "-solrUrl",
                    solrUrl };

            CreateCollectionTool createCollectionTool = new CreateCollectionTool(stdout);
            int createCode = createCollectionTool.runTool(processCommandLineArgs(
                    joinCommonAndToolOptions(createCollectionTool.getOptions()), createArgs));

            if (createCode != 0)
                throw new Exception("Failed to create collection using command: " + Arrays.asList(createArgs));

            return collectionName;
        }

        protected boolean isValidConfig(File configsetsDir, String config) {
            File configDir = new File(configsetsDir, config);
            if (configDir.isDirectory())
                return true;

            // not a built-in configset ... maybe it's a custom directory?
            configDir = new File(config);
            if (configDir.isDirectory())
                return true;

            return false;
        }

        protected Map<String, Object> getNodeStatus(String solrUrl, int maxWaitSecs) throws Exception {
            StatusTool statusTool = new StatusTool();
            if (verbose)
                echo("\nChecking status of Solr at " + solrUrl + " ...");

            URL solrURL = new URL(solrUrl);
            Map<String, Object> nodeStatus = statusTool.waitToSeeSolrUp(solrUrl, maxWaitSecs);
            nodeStatus.put("baseUrl", solrUrl);
            CharArr arr = new CharArr();
            new JSONWriter(arr, 2).write(nodeStatus);
            String mode = (nodeStatus.get("cloud") != null) ? "cloud" : "standalone";
            if (verbose)
                echo("\nSolr is running on " + solrURL.getPort() + " in " + mode + " mode with status:\n"
                        + arr.toString());

            return nodeStatus;
        }

        protected File setupExampleDir(File serverDir, File exampleParentDir, String dirName) throws IOException {
            File solrXml = new File(serverDir, "solr/solr.xml");
            if (!solrXml.isFile())
                throw new IllegalArgumentException(
                        "Value of -serverDir option is invalid! " + solrXml.getAbsolutePath() + " not found!");

            File zooCfg = new File(serverDir, "solr/zoo.cfg");
            if (!zooCfg.isFile())
                throw new IllegalArgumentException(
                        "Value of -serverDir option is invalid! " + zooCfg.getAbsolutePath() + " not found!");

            File solrHomeDir = new File(exampleParentDir, dirName + "/solr");
            if (!solrHomeDir.isDirectory()) {
                echo("Creating Solr home directory " + solrHomeDir);
                solrHomeDir.mkdirs();
            } else {
                echo("Solr home directory " + solrHomeDir.getAbsolutePath() + " already exists.");
            }

            copyIfNeeded(solrXml, new File(solrHomeDir, "solr.xml"));
            copyIfNeeded(zooCfg, new File(solrHomeDir, "zoo.cfg"));

            return solrHomeDir.getParentFile();
        }

        protected void copyIfNeeded(File src, File dest) throws IOException {
            if (!dest.isFile())
                FileUtils.copyFile(src, dest);

            if (!dest.isFile())
                throw new IllegalStateException("Required file " + dest.getAbsolutePath() + " not found!");
        }

        protected boolean isPortAvailable(int port) {
            Socket s = null;
            try {
                s = new Socket("localhost", port);
                return false;
            } catch (IOException e) {
                return true;
            } finally {
                if (s != null) {
                    try {
                        s.close();
                    } catch (IOException ignore) {
                    }
                }
            }
        }

        protected Integer promptForPort(Scanner s, int node, String prompt, Integer defVal) {
            return promptForInt(s, prompt, "a port for node " + node, defVal, null, null);
        }

        protected Integer promptForInt(Scanner s, String prompt, String label, Integer defVal, Integer min,
                Integer max) {
            Integer inputAsInt = null;

            String value = prompt(s, prompt, null /* default is null since we handle that here */);
            if (value != null) {
                int attempts = 3;
                while (value != null && --attempts > 0) {
                    try {
                        inputAsInt = new Integer(value);

                        if (min != null) {
                            if (inputAsInt < min) {
                                value = prompt(s, String.format(Locale.ROOT, PROMPT_NUMBER_TOO_SMALL, inputAsInt,
                                        label, min, max, defVal));
                                inputAsInt = null;
                                continue;
                            }
                        }

                        if (max != null) {
                            if (inputAsInt > max) {
                                value = prompt(s, String.format(Locale.ROOT, PROMPT_NUMBER_TOO_LARGE, inputAsInt,
                                        label, min, max, defVal));
                                inputAsInt = null;
                                continue;
                            }
                        }

                    } catch (NumberFormatException nfe) {
                        if (verbose)
                            echo(value + " is not a number!");

                        if (min != null && max != null) {
                            value = prompt(s, String.format(Locale.ROOT, PROMPT_FOR_NUMBER_IN_RANGE, label, min,
                                    max, defVal));
                        } else {
                            value = prompt(s, String.format(Locale.ROOT, PROMPT_FOR_NUMBER, label, defVal));
                        }
                    }
                }
                if (attempts == 0 && value != null && inputAsInt == null)
                    echo("Too many failed attempts! Going with default value " + defVal);
            }

            return (inputAsInt != null) ? inputAsInt : defVal;
        }

        protected String prompt(Scanner s, String prompt) {
            return prompt(s, prompt, null);
        }

        protected String prompt(Scanner s, String prompt, String defaultValue) {
            echo(prompt);
            String nextInput = s.nextLine();
            if (nextInput != null) {
                nextInput = nextInput.trim();
                if (nextInput.isEmpty())
                    nextInput = null;
            }
            return (nextInput != null) ? nextInput : defaultValue;
        }

    } // end RunExampleTool class

    /**
     * Asserts various conditions and exists with error code if fails, else continues with no output
     */
    public static class AssertTool extends ToolBase {

        private static String message = null;
        private static boolean useExitCode = false;
        private static Optional<Long> timeoutMs = Optional.empty();

        public AssertTool() {
            this(System.out);
        }

        public AssertTool(PrintStream stdout) {
            super(stdout);
        }

        public String getName() {
            return "assert";
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] {
                    OptionBuilder.withDescription("Asserts that we are NOT the root user").withLongOpt("not-root")
                            .create("R"),
                    OptionBuilder.withDescription("Asserts that we are the root user").withLongOpt("root")
                            .create("r"),
                    OptionBuilder
                            .withDescription(
                                    "Asserts that Solr is NOT running on a certain URL. Default timeout is 1000ms")
                            .withLongOpt("not-started").hasArg(true).withArgName("url").create("S"),
                    OptionBuilder
                            .withDescription(
                                    "Asserts that Solr is running on a certain URL. Default timeout is 1000ms")
                            .withLongOpt("started").hasArg(true).withArgName("url").create("s"),
                    OptionBuilder.withDescription("Asserts that we run as same user that owns <directory>")
                            .withLongOpt("same-user").hasArg(true).withArgName("directory").create("u"),
                    OptionBuilder.withDescription("Asserts that directory <directory> exists").withLongOpt("exists")
                            .hasArg(true).withArgName("directory").create("x"),
                    OptionBuilder.withDescription("Asserts that directory <directory> does NOT exist")
                            .withLongOpt("not-exists").hasArg(true).withArgName("directory").create("X"),
                    OptionBuilder
                            .withDescription("Exception message to be used in place of the default error message")
                            .withLongOpt("message").hasArg(true).withArgName("message").create("m"),
                    OptionBuilder.withDescription("Timeout in ms for commands supporting a timeout")
                            .withLongOpt("timeout").hasArg(true).withType(Long.class).withArgName("ms").create("t"),
                    OptionBuilder
                            .withDescription(
                                    "Return an exit code instead of printing error message on assert fail.")
                            .withLongOpt("exitcode").create("e") };
        }

        public int runTool(CommandLine cli) throws Exception {
            verbose = cli.hasOption("verbose");

            int toolExitStatus = 0;
            try {
                setBasicAuth();
                toolExitStatus = runAssert(cli);
            } catch (Exception exc) {
                // since this is a CLI, spare the user the stacktrace
                String excMsg = exc.getMessage();
                if (excMsg != null) {
                    System.err.println("\nERROR: " + excMsg + "\n");
                    toolExitStatus = 100; // Exit >= 100 means error, else means number of tests that failed
                } else {
                    throw exc;
                }
            }
            return toolExitStatus;
        }

        @Override
        protected void runImpl(CommandLine cli) throws Exception {
            runAssert(cli);
        }

        /**
         * Custom run method which may return exit code
         * @param cli the command line object
         * @return 0 on success, or a number corresponding to number of tests that failed
         * @throws Exception if a tool failed, e.g. authentication failure
         */
        protected int runAssert(CommandLine cli) throws Exception {
            if (cli.getOptions().length == 0 || cli.getArgs().length > 0 || cli.hasOption("h")) {
                new HelpFormatter().printHelp(
                        "bin/solr assert [-m <message>] [-e] [-rR] [-s <url>] [-S <url>] [-u <dir>] [-x <dir>] [-X <dir>]",
                        getToolOptions(this));
                return 1;
            }
            if (cli.hasOption("m")) {
                message = cli.getOptionValue("m");
            }
            if (cli.hasOption("t")) {
                timeoutMs = Optional.of(Long.parseLong(cli.getOptionValue("t")));
            }
            if (cli.hasOption("e")) {
                useExitCode = true;
            }

            int ret = 0;
            if (cli.hasOption("r")) {
                ret += assertRootUser();
            }
            if (cli.hasOption("R")) {
                ret += assertNotRootUser();
            }
            if (cli.hasOption("x")) {
                ret += assertFileExists(cli.getOptionValue("x"));
            }
            if (cli.hasOption("X")) {
                ret += assertFileNotExists(cli.getOptionValue("X"));
            }
            if (cli.hasOption("u")) {
                ret += sameUser(cli.getOptionValue("u"));
            }
            if (cli.hasOption("s")) {
                ret += assertSolrRunning(cli.getOptionValue("s"));
            }
            if (cli.hasOption("S")) {
                ret += assertSolrNotRunning(cli.getOptionValue("S"));
            }
            return ret;
        }

        public static int assertSolrRunning(String url) throws Exception {
            StatusTool status = new StatusTool();
            try {
                status.waitToSeeSolrUp(url, timeoutMs.orElse(1000L).intValue() / 1000);
            } catch (Exception se) {
                if (exceptionIsAuthRelated(se)) {
                    throw se;
                }
                return exitOrException(
                        "Solr is not running on url " + url + " after " + timeoutMs.orElse(1000L) / 1000 + "s");
            }
            return 0;
        }

        public static int assertSolrNotRunning(String url) throws Exception {
            StatusTool status = new StatusTool();
            long timeout = System.nanoTime()
                    + TimeUnit.NANOSECONDS.convert(timeoutMs.orElse(1000L), TimeUnit.MILLISECONDS);
            try {
                attemptHttpHead(url, getHttpClient());
            } catch (SolrException se) {
                throw se; // Auth error
            } catch (IOException e) {
                log.debug("Opening connection to " + url + " failed, Solr does not seem to be running", e);
                return 0;
            }
            while (System.nanoTime() < timeout) {
                try {
                    status.waitToSeeSolrUp(url, 1);
                    try {
                        log.debug("Solr still up. Waiting before trying again to see if it was stopped");
                        Thread.sleep(1000L);
                    } catch (InterruptedException interrupted) {
                        timeout = 0; // stop looping
                    }
                } catch (Exception se) {
                    if (exceptionIsAuthRelated(se)) {
                        throw se;
                    }
                    return exitOrException(se.getMessage());
                }
            }
            return exitOrException(
                    "Solr is still running at " + url + " after " + timeoutMs.orElse(1000L) / 1000 + "s");
        }

        public static int sameUser(String directory) throws Exception {
            if (Files.exists(Paths.get(directory))) {
                String userForDir = userForDir(Paths.get(directory));
                if (!currentUser().equals(userForDir)) {
                    return exitOrException("Must run as user " + userForDir + ". We are " + currentUser());
                }
            } else {
                return exitOrException("Directory " + directory + " does not exist.");
            }
            return 0;
        }

        public static int assertFileExists(String directory) throws Exception {
            if (!Files.exists(Paths.get(directory))) {
                return exitOrException("Directory " + directory + " does not exist.");
            }
            return 0;
        }

        public static int assertFileNotExists(String directory) throws Exception {
            if (Files.exists(Paths.get(directory))) {
                return exitOrException("Directory " + directory + " should not exist.");
            }
            return 0;
        }

        public static int assertRootUser() throws Exception {
            if (!currentUser().equals("root")) {
                return exitOrException("Must run as root user");
            }
            return 0;
        }

        public static int assertNotRootUser() throws Exception {
            if (currentUser().equals("root")) {
                return exitOrException("Not allowed to run as root user");
            }
            return 0;
        }

        public static String currentUser() {
            return System.getProperty("user.name");
        }

        public static String userForDir(Path pathToDir) {
            try {
                FileOwnerAttributeView ownerAttributeView = Files.getFileAttributeView(pathToDir,
                        FileOwnerAttributeView.class);
                return ownerAttributeView.getOwner().getName();
            } catch (IOException e) {
                return "N/A";
            }
        }

        private static int exitOrException(String msg) throws Exception {
            if (useExitCode) {
                return 1;
            } else {
                throw new Exception(message != null ? message : msg);
            }
        }
    } // end AssertTool class

    public static class UtilsTool extends ToolBase {
        private Path serverPath;
        private Path logsPath;
        private boolean beQuiet;

        public UtilsTool() {
            this(System.out);
        }

        public UtilsTool(PrintStream stdout) {
            super(stdout);
        }

        public String getName() {
            return "utils";
        }

        @SuppressWarnings("static-access")
        public Option[] getOptions() {
            return new Option[] {
                    OptionBuilder.withArgName("path").hasArg()
                            .withDescription("Path to server dir. Required if logs path is relative").create("s"),
                    OptionBuilder.withArgName("path").hasArg()
                            .withDescription("Path to logs dir. If relative, also provide server dir with -s")
                            .create("l"),
                    OptionBuilder.withDescription("Be quiet, don't print to stdout, only return exit codes")
                            .create("q"),
                    OptionBuilder.withArgName("daysToKeep").hasArg().withType(Integer.class)
                            .withDescription("Path to logs directory").create("remove_old_solr_logs"),
                    OptionBuilder.withArgName("generations").hasArg().withType(Integer.class)
                            .withDescription("Rotate solr.log to solr.log.1 etc").create("rotate_solr_logs"),
                    OptionBuilder.withDescription("Archive old garbage collection logs into archive/")
                            .create("archive_gc_logs"),
                    OptionBuilder.withDescription("Archive old console logs into archive/")
                            .create("archive_console_logs") };
        }

        @Override
        public int runTool(CommandLine cli) throws Exception {
            if (cli.getOptions().length == 0 || cli.getArgs().length > 0 || cli.hasOption("h")) {
                new HelpFormatter().printHelp("bin/solr utils [OPTIONS]", getToolOptions(this));
                return 1;
            }
            if (cli.hasOption("s")) {
                serverPath = Paths.get(cli.getOptionValue("s"));
            }
            if (cli.hasOption("l")) {
                logsPath = Paths.get(cli.getOptionValue("l"));
            }
            if (cli.hasOption("q")) {
                beQuiet = cli.hasOption("q");
            }
            if (cli.hasOption("remove_old_solr_logs")) {
                if (removeOldSolrLogs(Integer.parseInt(cli.getOptionValue("remove_old_solr_logs"))) > 0)
                    return 1;
            }
            if (cli.hasOption("rotate_solr_logs")) {
                if (rotateSolrLogs(Integer.parseInt(cli.getOptionValue("rotate_solr_logs"))) > 0)
                    return 1;
            }
            if (cli.hasOption("archive_gc_logs")) {
                if (archiveGcLogs() > 0)
                    return 1;
            }
            if (cli.hasOption("archive_console_logs")) {
                if (archiveConsoleLogs() > 0)
                    return 1;
            }
            return 0;
        }

        /**
         * Moves gc logs into archived/
         * @return 0 on success
         * @throws Exception on failure
         */
        public int archiveGcLogs() throws Exception {
            prepareLogsPath();
            Path archivePath = logsPath.resolve("archived");
            if (!archivePath.toFile().exists()) {
                Files.createDirectories(archivePath);
            }
            List<Path> archived = Files
                    .find(archivePath, 1,
                            (f, a) -> a.isRegularFile()
                                    && String.valueOf(f.getFileName()).matches("^solr_gc[_.].+"))
                    .collect(Collectors.toList());
            for (Path p : archived) {
                Files.delete(p);
            }
            List<Path> files = Files
                    .find(logsPath, 1,
                            (f, a) -> a.isRegularFile()
                                    && String.valueOf(f.getFileName()).matches("^solr_gc[_.].+"))
                    .collect(Collectors.toList());
            if (files.size() > 0) {
                out("Archiving " + files.size() + " old GC log files to " + archivePath);
                for (Path p : files) {
                    Files.move(p, archivePath.resolve(p.getFileName()), StandardCopyOption.REPLACE_EXISTING);
                }
            }
            return 0;
        }

        /**
         * Moves console log(s) into archiced/
         * @return 0 on success
         * @throws Exception on failure
         */
        public int archiveConsoleLogs() throws Exception {
            prepareLogsPath();
            Path archivePath = logsPath.resolve("archived");
            if (!archivePath.toFile().exists()) {
                Files.createDirectories(archivePath);
            }
            List<Path> archived = Files
                    .find(archivePath, 1,
                            (f, a) -> a.isRegularFile() && String.valueOf(f.getFileName()).endsWith("-console.log"))
                    .collect(Collectors.toList());
            for (Path p : archived) {
                Files.delete(p);
            }
            List<Path> files = Files
                    .find(logsPath, 1,
                            (f, a) -> a.isRegularFile() && String.valueOf(f.getFileName()).endsWith("-console.log"))
                    .collect(Collectors.toList());
            if (files.size() > 0) {
                out("Archiving " + files.size() + " console log files to " + archivePath);
                for (Path p : files) {
                    Files.move(p, archivePath.resolve(p.getFileName()), StandardCopyOption.REPLACE_EXISTING);
                }
            }
            return 0;
        }

        /**
         * Rotates solr.log before starting Solr. Mimics log4j2 behavior, i.e. with generations=9:
         * <pre>
         *   solr.log.9 (and higher) are deleted
         *   solr.log.8 -&gt; solr.log.9
         *   solr.log.7 -&gt; solr.log.8
         *   ...
         *   solr.log   -&gt; solr.log.1
         * </pre>
         * @param generations number of generations to keep. Should agree with setting in log4j.properties
         * @return 0 if success
         * @throws Exception if problems
         */
        public int rotateSolrLogs(int generations) throws Exception {
            prepareLogsPath();
            if (logsPath.toFile().exists() && logsPath.resolve("solr.log").toFile().exists()) {
                out("Rotating solr logs, keeping a max of " + generations + " generations");
                try (Stream<Path> files = Files
                        .find(logsPath, 1,
                                (f, a) -> a.isRegularFile()
                                        && String.valueOf(f.getFileName()).startsWith("solr.log."))
                        .sorted((b, a) -> new Integer(a.getFileName().toString().substring(9))
                                .compareTo(new Integer(b.getFileName().toString().substring(9))))) {
                    files.forEach(p -> {
                        try {
                            int number = Integer.parseInt(p.getFileName().toString().substring(9));
                            if (number >= generations) {
                                Files.delete(p);
                            } else {
                                Path renamed = p.getParent().resolve("solr.log." + (number + 1));
                                Files.move(p, renamed);
                            }
                        } catch (IOException e) {
                            out("Problem during rotation of log files: " + e.getMessage());
                        }
                    });
                } catch (NumberFormatException nfe) {
                    throw new Exception(
                            "Do not know how to rotate solr.log.<ext> with non-numeric extension. Rotate aborted.",
                            nfe);
                }
                Files.move(logsPath.resolve("solr.log"), logsPath.resolve("solr.log.1"));
            }

            return 0;
        }

        /**
         * Deletes time-stamped old solr logs, if older than n days 
         * @param daysToKeep number of days logs to keep before deleting
         * @return 0 on success
         * @throws Exception on failure
         */
        public int removeOldSolrLogs(int daysToKeep) throws Exception {
            prepareLogsPath();
            if (logsPath.toFile().exists()) {
                try (Stream<Path> stream = Files.find(logsPath, 2,
                        (f, a) -> a.isRegularFile()
                                && Instant.now().minus(Period.ofDays(daysToKeep))
                                        .isAfter(a.lastModifiedTime().toInstant())
                                && String.valueOf(f.getFileName()).startsWith("solr_log_"))) {
                    List<Path> files = stream.collect(Collectors.toList());
                    if (files.size() > 0) {
                        out("Deleting " + files.size() + " solr_log_* files older than " + daysToKeep + " days.");
                        for (Path p : files) {
                            Files.delete(p);
                        }
                    }
                }
            }
            return 0;
        }

        // Private methods to follow

        private void out(String message) {
            if (!beQuiet) {
                stdout.print(message + "\n");
            }
        }

        private void prepareLogsPath() throws Exception {
            if (logsPath == null) {
                throw new Exception("Command requires the -l <log-directory> option");
            }
            if (!logsPath.isAbsolute()) {
                if (serverPath != null && serverPath.isAbsolute() && Files.exists(serverPath)) {
                    logsPath = serverPath.resolve(logsPath);
                } else {
                    throw new Exception("Logs directory must be an absolute path, or -s must be supplied");
                }
            }
        }

        @Override
        protected void runImpl(CommandLine cli) throws Exception {
        }

        public void setLogPath(Path logsPath) {
            this.logsPath = logsPath;
        }

        public void setServerPath(Path serverPath) {
            this.serverPath = serverPath;
        }

        public void setQuiet(boolean shouldPrintStdout) {
            this.beQuiet = shouldPrintStdout;
        }
    } // end UtilsTool class  
}