You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@solr.apache.org by "epugh (via GitHub)" <gi...@apache.org> on 2023/04/15 14:00:59 UTC

[GitHub] [solr] epugh opened a new pull request, #1568: SOLR-16711: Starting migration, I should stop and get Crave build verified!

epugh opened a new pull request, #1568:
URL: https://github.com/apache/solr/pull/1568

   https://issues.apache.org/jira/browse/SOLR-16711
   
   
   
   # Description
   
   Second take on this.
   
   # Solution
   
   Please provide a short description of the approach taken to implement your solution.
   
   # Tests
   
   Please describe the tests you've developed or run to confirm this patch implements the feature or solves the problem.
   
   # Checklist
   
   Please review the following and check all that apply:
   
   - [ ] I have reviewed the guidelines for [How to Contribute](https://wiki.apache.org/solr/HowToContribute) and my code conforms to the standards described there to the best of my ability.
   - [ ] I have created a Jira issue and added the issue ID to my pull request title.
   - [ ] I have given Solr maintainers [access](https://help.github.com/en/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork) to contribute to my PR branch. (optional but recommended)
   - [ ] I have developed this patch against the `main` branch.
   - [ ] I have run `./gradlew check`.
   - [ ] I have added tests for my changes.
   - [ ] I have added documentation for the [Reference Guide](https://github.com/apache/solr/tree/main/solr/solr-ref-guide)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] sonatype-lift[bot] commented on a diff in pull request #1568: SOLR-16711: Starting migration, I should stop and get Crave build verified!

Posted by "sonatype-lift[bot] (via GitHub)" <gi...@apache.org>.
sonatype-lift[bot] commented on code in PR #1568:
URL: https://github.com/apache/solr/pull/1568#discussion_r1167564372


##########
solr/core/src/java/org/apache/solr/util/cli/ToolBase.java:
##########
@@ -0,0 +1,55 @@
+package org.apache.solr.util.cli;
+
+import java.io.PrintStream;
+import org.apache.commons.cli.CommandLine;
+import org.apache.solr.util.CLIO;
+import org.apache.solr.util.SolrCLI;
+
+public abstract class ToolBase implements Tool {
+
+  protected PrintStream stdout;
+  protected boolean verbose = false;
+
+  protected ToolBase() {
+    this(CLIO.getOutStream());
+  }
+
+  protected ToolBase(PrintStream stdout) {
+    this.stdout = stdout;
+  }
+
+  protected void echoIfVerbose(final String msg, CommandLine cli) {
+    if (cli.hasOption(SolrCLI.OPTION_VERBOSE.getOpt())) {
+      echo(msg);
+    }
+  }
+
+  protected void echo(final String msg) {
+    stdout.println(msg);
+  }
+
+  @Override
+  public int runTool(CommandLine cli) throws Exception {
+    verbose = cli.hasOption(SolrCLI.OPTION_VERBOSE.getOpt());
+
+    int toolExitStatus = 0;
+    try {
+      runImpl(cli);
+    } catch (Exception exc) {
+      // since this is a CLI, spare the user the stacktrace
+      String excMsg = exc.getMessage();
+      if (excMsg != null) {
+        CLIO.err("\nERROR: " + excMsg + "\n");
+        if (verbose) {
+          exc.printStackTrace(CLIO.getErrStream());

Review Comment:
   <picture><img alt="0% of developers fix this issue" src="https://lift.sonatype.com/api/commentimage/fixrate/0/display.svg"></picture>
   
   <b>*[INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE](https://find-sec-bugs.github.io/bugs.htm#INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE):</b>*  Possible information exposure through an error message
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=492155107&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=492155107&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=492155107&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=492155107&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=492155107&lift_comment_rating=5) ]



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] sonatype-lift[bot] commented on a diff in pull request #1568: SOLR-16711: Starting migration, I should stop and get Crave build verified!

Posted by "sonatype-lift[bot] (via GitHub)" <gi...@apache.org>.
sonatype-lift[bot] commented on code in PR #1568:
URL: https://github.com/apache/solr/pull/1568#discussion_r1167634705


##########
solr/core/src/java/org/apache/solr/util/cli/AssertTool.java:
##########
@@ -0,0 +1,383 @@
+/*
+ * 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.cli;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileOwnerAttributeView;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.HealthCheckRequest;
+import org.apache.solr.client.solrj.response.CollectionAdminResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.util.CLIO;
+import org.apache.solr.util.SolrCLI;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Asserts various conditions and exists with error code if fails, else continues with no output */
+public class AssertTool extends ToolBase {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  private static String message = null;
+  private static boolean useExitCode = false;
+  private static Optional<Long> timeoutMs = Optional.empty();
+
+  public AssertTool() {
+    this(CLIO.getOutStream());
+  }
+
+  public AssertTool(PrintStream stdout) {
+    super(stdout);
+  }
+
+  @Override
+  public String getName() {
+    return "assert";
+  }
+
+  @Override
+  public List<Option> getOptions() {
+    return List.of(
+        Option.builder("R")
+            .desc("Asserts that we are NOT the root user.")
+            .longOpt("not-root")
+            .build(),
+        Option.builder("r").desc("Asserts that we are the root user.").longOpt("root").build(),
+        Option.builder("S")
+            .desc("Asserts that Solr is NOT running on a certain URL. Default timeout is 1000ms.")
+            .longOpt("not-started")
+            .hasArg(true)
+            .argName("url")
+            .build(),
+        Option.builder("s")
+            .desc("Asserts that Solr is running on a certain URL. Default timeout is 1000ms.")
+            .longOpt("started")
+            .hasArg(true)
+            .argName("url")
+            .build(),
+        Option.builder("u")
+            .desc("Asserts that we run as same user that owns <directory>.")
+            .longOpt("same-user")
+            .hasArg(true)
+            .argName("directory")
+            .build(),
+        Option.builder("x")
+            .desc("Asserts that directory <directory> exists.")
+            .longOpt("exists")
+            .hasArg(true)
+            .argName("directory")
+            .build(),
+        Option.builder("X")
+            .desc("Asserts that directory <directory> does NOT exist.")
+            .longOpt("not-exists")
+            .hasArg(true)
+            .argName("directory")
+            .build(),
+        Option.builder("c")
+            .desc(
+                "Asserts that Solr is running in cloud mode.  Also fails if Solr not running.  URL should be for root Solr path.")
+            .longOpt("cloud")
+            .hasArg(true)
+            .argName("url")
+            .build(),
+        Option.builder("C")
+            .desc(
+                "Asserts that Solr is not running in cloud mode.  Also fails if Solr not running.  URL should be for root Solr path.")
+            .longOpt("not-cloud")
+            .hasArg(true)
+            .argName("url")
+            .build(),
+        Option.builder("m")
+            .desc("Exception message to be used in place of the default error message.")
+            .longOpt("message")
+            .hasArg(true)
+            .argName("message")
+            .build(),
+        Option.builder("t")
+            .desc("Timeout in ms for commands supporting a timeout.")
+            .longOpt("timeout")
+            .hasArg(true)
+            .type(Long.class)
+            .argName("ms")
+            .build(),
+        Option.builder("e")
+            .desc("Return an exit code instead of printing error message on assert fail.")
+            .longOpt("exitcode")
+            .build());
+  }
+
+  @Override
+  public int runTool(CommandLine cli) throws Exception {
+    verbose = cli.hasOption(SolrCLI.OPTION_VERBOSE.getOpt());
+
+    int toolExitStatus = 0;
+    try {
+      toolExitStatus = runAssert(cli);
+    } catch (Exception exc) {
+      // since this is a CLI, spare the user the stacktrace
+      String excMsg = exc.getMessage();
+      if (excMsg != null) {
+        if (verbose) {
+          CLIO.err("\nERROR: " + exc + "\n");
+        } else {
+          CLIO.err("\nERROR: " + excMsg + "\n");
+        }
+        toolExitStatus = 100; // Exit >= 100 means error, else means number of tests that failed
+      } else {
+        throw exc;
+      }
+    }
+    return toolExitStatus;
+  }
+
+  @Override
+  public 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>] [-c <url>] [-C <url>] [-u <dir>] [-x <dir>] [-X <dir>]",
+              SolrCLI.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"));
+    }
+    if (cli.hasOption("c")) {
+      ret += assertSolrRunningInCloudMode(cli.getOptionValue("c"));
+    }
+    if (cli.hasOption("C")) {
+      ret += assertSolrNotRunningInCloudMode(cli.getOptionValue("C"));
+    }
+    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 (SolrCLI.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 (SolrClient solrClient = SolrCLI.getSolrClient(url)) {
+      NamedList<Object> response = solrClient.request(new HealthCheckRequest());
+      Integer statusCode = (Integer) response.findRecursive("responseHeader", "status");
+      SolrCLI.checkCodeForAuthError(statusCode);
+    } catch (SolrException se) {
+      throw se;
+    } catch (IOException | SolrServerException e) {
+      log.debug("Opening connection to {} failed, Solr does not seem to be running", url, 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 (SolrCLI.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 assertSolrRunningInCloudMode(String url) throws Exception {
+    if (!isSolrRunningOn(url)) {
+      return exitOrException(
+          "Solr is not running on url " + url + " after " + timeoutMs.orElse(1000L) / 1000 + "s");
+    }
+
+    if (!runningSolrIsCloud(url)) {
+      return exitOrException("Solr is not running in cloud mode on " + url);
+    }
+    return 0;
+  }
+
+  public static int assertSolrNotRunningInCloudMode(String url) throws Exception {
+    if (!isSolrRunningOn(url)) {
+      return exitOrException(
+          "Solr is not running on url " + url + " after " + timeoutMs.orElse(1000L) / 1000 + "s");
+    }
+
+    if (runningSolrIsCloud(url)) {
+      return exitOrException("Solr is not running in standalone mode on " + url);
+    }
+    return 0;
+  }
+
+  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")) {

Review Comment:
   <picture><img alt="14% of developers fix this issue" src="https://lift.sonatype.com/api/commentimage/fixrate/14/display.svg"></picture>
   
   <b>*NULL_DEREFERENCE:</b>*  object returned by `currentUser()` could be null and is dereferenced at line 322.
   
   ❗❗ <b>2 similar findings have been found in this PR</b>
   
   <details><summary>🔎 Expand here to view all instances of this finding</summary><br/>
     
     
   <div align=\"center\">
   
   
   | **File Path** | **Line Number** |
   | ------------- | ------------- |
   | solr/core/src/java/org/apache/solr/util/cli/AssertTool.java | [298](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/AssertTool.java#L298) |
   | solr/core/src/java/org/apache/solr/util/cli/AssertTool.java | [329](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/AssertTool.java#L329) |
   <p><a href="https://lift.sonatype.com/results/github.com/apache/solr/01GY375DKBD3QBSKFZA5SVDTE7?t=Infer|NULL_DEREFERENCE" target="_blank">Visit the Lift Web Console</a> to find more details in your report.</p></div></details>
   
   
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=492278936&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=492278936&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=492278936&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=492278936&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=492278936&lift_comment_rating=5) ]



##########
solr/core/src/java/org/apache/solr/util/cli/RunExampleTool.java:
##########
@@ -0,0 +1,1059 @@
+/*
+ * 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.cli;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+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.concurrent.TimeUnit;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.exec.DefaultExecuteResultHandler;
+import org.apache.commons.exec.DefaultExecutor;
+import org.apache.commons.exec.ExecuteException;
+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.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.util.CLIO;
+import org.apache.solr.util.SimplePostTool;
+import org.apache.solr.util.SolrCLI;
+import org.noggit.CharArr;
+import org.noggit.JSONWriter;
+
+/** Supports an interactive session with the user to launch (or relaunch the -e cloud example) */
+public 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, CLIO.getOutStream());
+  }
+
+  public RunExampleTool(Executor executor, InputStream userInput, PrintStream stdout) {
+    super(stdout);
+    this.executor = (executor != null) ? executor : new DefaultExecutor();
+    this.userInput = userInput;
+  }
+
+  @Override
+  public String getName() {
+    return "run_example";
+  }
+
+  @Override
+  public List<Option> getOptions() {
+    return List.of(
+        Option.builder("noprompt")
+            .required(false)
+            .desc(
+                "Don't prompt for input; accept all defaults when running examples that accept user input.")
+            .build(),
+        Option.builder("e")
+            .argName("NAME")
+            .hasArg()
+            .required(true)
+            .desc("Name of the example to launch, one of: cloud, techproducts, schemaless, films.")
+            .longOpt("example")
+            .build(),
+        Option.builder("script")
+            .argName("PATH")
+            .hasArg()
+            .required(false)
+            .desc("Path to the bin/solr script.")
+            .build(),
+        Option.builder("d")
+            .argName("DIR")
+            .hasArg()
+            .required(true)
+            .desc("Path to the Solr server directory.")
+            .longOpt("serverDir")
+            .build(),
+        Option.builder("force")
+            .argName("FORCE")
+            .desc("Force option in case Solr is run as root.")
+            .build(),
+        Option.builder("exampleDir")
+            .argName("DIR")
+            .hasArg()
+            .required(false)
+            .desc(
+                "Path to the Solr example directory; if not provided, ${serverDir}/../example is expected to exist.")
+            .build(),
+        Option.builder("urlScheme")
+            .argName("SCHEME")
+            .hasArg()
+            .required(false)
+            .desc("Solr URL scheme: http or https, defaults to http if not specified.")
+            .build(),
+        Option.builder("p")
+            .argName("PORT")
+            .hasArg()
+            .required(false)
+            .desc("Specify the port to start the Solr HTTP listener on; default is 8983.")
+            .longOpt("port")
+            .build(),
+        Option.builder("h")
+            .argName("HOSTNAME")
+            .hasArg()
+            .required(false)
+            .desc("Specify the hostname for this Solr instance.")
+            .longOpt("host")
+            .build(),
+        Option.builder("z")
+            .argName("ZKHOST")
+            .hasArg()
+            .required(false)
+            .desc("ZooKeeper connection string; only used when running in SolrCloud mode using -c.")
+            .longOpt("zkhost")
+            .build(),
+        Option.builder("c")
+            .required(false)
+            .desc(
+                "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.")
+            .longOpt("cloud")
+            .build(),
+        Option.builder("m")
+            .argName("MEM")
+            .hasArg()
+            .required(false)
+            .desc(
+                "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.")
+            .longOpt("memory")
+            .build(),
+        Option.builder("a")
+            .argName("OPTS")
+            .hasArg()
+            .required(false)
+            .desc(
+                "Additional options to be passed to the JVM when starting example Solr server(s).")
+            .longOpt("addlopts")
+            .build());
+  }
+
+  @Override
+  public 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!");
+
+    echoIfVerbose(
+        "Running with\nserverDir="
+            + serverDir.getAbsolutePath()
+            + ",\nexampleDir="
+            + exampleDir.getAbsolutePath()
+            + "\nscript="
+            + script,
+        cli);
+
+    String exampleType = cli.getOptionValue("example");
+    if ("cloud".equals(exampleType)) {
+      runCloudExample(cli);
+    } else if ("techproducts".equals(exampleType)
+        || "schemaless".equals(exampleType)
+        || "films".equals(exampleType)) {
+      runExample(cli, exampleType);
+    } else {
+      throw new IllegalArgumentException(
+          "Unsupported example "
+              + exampleType
+              + "! Please choose one of: cloud, schemaless, techproducts, or films");
+    }
+  }
+
+  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" : "_default";
+
+    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) {
+      if (SolrCLI.safeCheckCollectionExists(solrUrl, collectionName)) {
+        alreadyExists = true;
+        echo(
+            "\nWARNING: Collection '"
+                + collectionName
+                + "' already exists!\nChecked collection existence using Collections API");
+      }
+    } else {
+      String coreName = collectionName;
+      if (SolrCLI.safeCheckCoreExists(solrUrl, coreName)) {
+        alreadyExists = true;
+        echo(
+            "\nWARNING: Core '"
+                + coreName
+                + "' already exists!\nChecked core existence using Core API command");
+      }
+    }
+
+    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(
+              SolrCLI.processCommandLineArgs(
+                  createTool.getName(),
+                  SolrCLI.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");
+      }
+    } else if ("films".equals(exampleName) && !alreadyExists) {
+      SolrClient solrClient = new Http2SolrClient.Builder(solrUrl).build();
+
+      echo("Adding dense vector field type to films schema \"_default\"");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/schema",
+            "{\n"
+                + "        \"add-field-type\" : {\n"
+                + "          \"name\":\"knn_vector_10\",\n"
+                + "          \"class\":\"solr.DenseVectorField\",\n"
+                + "          \"vectorDimension\":10,\n"
+                + "          \"similarityFunction\":cosine\n"
+                + "          \"knnAlgorithm\":hnsw\n"
+                + "        }\n"
+                + "      }");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      echo(
+          "Adding name, initial_release_date, and film_vector fields to films schema \"_default\"");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/schema",
+            "{\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"name\",\n"
+                + "          \"type\":\"text_general\",\n"
+                + "          \"multiValued\":false,\n"
+                + "          \"stored\":true\n"
+                + "        },\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"initial_release_date\",\n"
+                + "          \"type\":\"pdate\",\n"
+                + "          \"stored\":true\n"
+                + "        },\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"film_vector\",\n"
+                + "          \"type\":\"knn_vector_10\",\n"
+                + "          \"indexed\":true\n"
+                + "          \"stored\":true\n"
+                + "        }\n"
+                + "      }");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      echo("Adding paramsets \"algo\" and \"algo_b\" to films configuration for relevancy tuning");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/config/params",
+            "{\n"
+                + "        \"set\": {\n"
+                + "        \"algo_a\":{\n"
+                + "               \"defType\":\"dismax\",\n"
+                + "               \"qf\":\"name\"\n"
+                + "             }\n"
+                + "           },\n"
+                + "           \"set\": {\n"
+                + "             \"algo_b\":{\n"
+                + "               \"defType\":\"dismax\",\n"
+                + "               \"qf\":\"name\",\n"
+                + "               \"mm\":\"100%\"\n"
+                + "             }\n"
+                + "            }\n"
+                + "        }\n");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      File filmsJsonFile = new File(exampleDir, "films/films.json");
+      String updateUrl = String.format(Locale.ROOT, "%s/%s/update/json", solrUrl, collectionName);
+      echo("Indexing films example docs from " + filmsJsonFile.getAbsolutePath());
+      String currentPropVal = System.getProperty("url");
+      System.setProperty("url", updateUrl);
+      SimplePostTool.main(new String[] {filmsJsonFile.getAbsolutePath()});
+      if (currentPropVal != null) {
+        System.setProperty("url", currentPropVal); // reset
+      } else {
+        System.clearProperty("url");
+      }
+    }
+
+    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, StandardCharsets.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;
+        echoIfVerbose("Using port " + port + " for node " + (n + 1), cli);
+      }
+    } 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) {
+      @SuppressWarnings("unchecked")
+      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(
+          SolrCLI.processCommandLineArgs(
+              configTool.getName(),
+              SolrCLI.joinCommonAndToolOptions(configTool.getOptions()),
+              configArgs));
+    } catch (Exception exc) {
+      CLIO.err("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(Collections.singletonList(zkHost), Optional.empty()).build();
+      cloudClient.connect();
+      Set<String> liveNodes = cloudClient.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.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) {
+      CLIO.err("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 forceArg = cli.hasOption("force") ? " -force" : "";
+
+    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 %s",
+            callScript,
+            cloudModeArg,
+            port,
+            solrHome,
+            hostArg,
+            zkHostArg,
+            memArg,
+            forceArg,
+            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 (Map.Entry<String, String> entry : procEnv.entrySet()) {
+          String envVar = entry.getKey();
+          String envVarVal = entry.getValue();
+          if (envVarVal != null && !"EXAMPLE".equals(envVar) && !envVar.startsWith("SOLR_")) {
+            startEnv.put(envVar, envVarVal);
+          }
+        }
+      }
+      DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
+      executor.execute(org.apache.commons.exec.CommandLine.parse(startCmd), startEnv, handler);
+
+      // wait for execution.
+      try {
+        handler.waitFor(3000);
+      } catch (InterruptedException ie) {
+        // safe to ignore ...
+        Thread.interrupted();
+      }
+      if (handler.hasResult() && handler.getExitValue() != 0) {
+        throw new Exception(
+            "Failed to start Solr using command: "
+                + startCmd
+                + " Exception : "
+                + handler.getException());
+      }
+    } else {
+      try {
+        code = executor.execute(org.apache.commons.exec.CommandLine.parse(startCmd));
+      } catch (ExecuteException e) {
+        throw new Exception(
+            "Failed to start Solr using command: " + startCmd + " Exception : " + e);
+      }
+    }
+    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 = "_default";
+    String collectionName = "gettingstarted";
+
+    File configsetsDir = new File(serverDir, "solr/configsets");
+
+    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 (SolrCLI.safeCheckCollectionExists(solrUrl, 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 = "_default 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 (SolrCLI.safeCheckCollectionExists(solrUrl, 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(
+            SolrCLI.processCommandLineArgs(
+                createCollectionTool.getName(),
+                SolrCLI.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);

Review Comment:
   <picture><img alt="8% of developers fix this issue" src="https://lift.sonatype.com/api/commentimage/fixrate/8/display.svg"></picture>
   
   <b>*[PATH_TRAVERSAL_IN](https://find-sec-bugs.github.io/bugs.htm#PATH_TRAVERSAL_IN):</b>*  This API (java/io/File.<init>(Ljava/lang/String;)V) reads a file whose location might be specified by user input
   
   ❗❗ <b>24 similar findings have been found in this PR</b>
   
   <details><summary>🔎 Expand here to view all instances of this finding</summary><br/>
     
     
   <div align=\"center\">
   
   
   | **File Path** | **Line Number** |
   | ------------- | ------------- |
   | solr/core/src/java/org/apache/solr/util/cli/CreateCoreTool.java | [141](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/CreateCoreTool.java#L141) |
   | solr/core/src/java/org/apache/solr/util/cli/RunExampleTool.java | [482](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/RunExampleTool.java#L482) |
   | solr/core/src/java/org/apache/solr/util/cli/RunExampleTool.java | [214](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/RunExampleTool.java#L214) |
   | solr/core/src/java/org/apache/solr/util/cli/RunExampleTool.java | [631](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/RunExampleTool.java#L631) |
   | solr/core/src/java/org/apache/solr/util/cli/AssertTool.java | [296](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/AssertTool.java#L296) |
   | solr/core/src/java/org/apache/solr/util/cli/AssertTool.java | [308](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/AssertTool.java#L308) |
   | solr/core/src/java/org/apache/solr/util/cli/CreateCoreTool.java | [93](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/CreateCoreTool.java#L93) |
   | solr/core/src/java/org/apache/solr/util/cli/CreateCoreTool.java | [96](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/CreateCoreTool.java#L96) |
   | solr/core/src/java/org/apache/solr/util/cli/RunExampleTool.java | [936](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/RunExampleTool.java#L936) |
   | solr/core/src/java/org/apache/solr/util/cli/ConfigSetDownloadTool.java | [92](https://github.com/apache/solr/blob/8678d92717edc42a5e2ef42cfa4ad8b87b180e3e/solr/core/src/java/org/apache/solr/util/cli/ConfigSetDownloadTool.java#L92) |
   <p> Showing <b>10</b> of <b> 24 </b> findings. <a href="https://lift.sonatype.com/results/github.com/apache/solr/01GY375DKBD3QBSKFZA5SVDTE7?t=FindSecBugs|PATH_TRAVERSAL_IN" target="_blank">Visit the Lift Web Console</a> to see all.</p></div></details>
   
   
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=492276237&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=492276237&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=492276237&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=492276237&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=492276237&lift_comment_rating=5) ]



##########
solr/core/src/java/org/apache/solr/util/cli/RunExampleTool.java:
##########
@@ -0,0 +1,1059 @@
+/*
+ * 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.cli;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+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.concurrent.TimeUnit;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.exec.DefaultExecuteResultHandler;
+import org.apache.commons.exec.DefaultExecutor;
+import org.apache.commons.exec.ExecuteException;
+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.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.util.CLIO;
+import org.apache.solr.util.SimplePostTool;
+import org.apache.solr.util.SolrCLI;
+import org.noggit.CharArr;
+import org.noggit.JSONWriter;
+
+/** Supports an interactive session with the user to launch (or relaunch the -e cloud example) */
+public 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, CLIO.getOutStream());
+  }
+
+  public RunExampleTool(Executor executor, InputStream userInput, PrintStream stdout) {
+    super(stdout);
+    this.executor = (executor != null) ? executor : new DefaultExecutor();
+    this.userInput = userInput;
+  }
+
+  @Override
+  public String getName() {
+    return "run_example";
+  }
+
+  @Override
+  public List<Option> getOptions() {
+    return List.of(
+        Option.builder("noprompt")
+            .required(false)
+            .desc(
+                "Don't prompt for input; accept all defaults when running examples that accept user input.")
+            .build(),
+        Option.builder("e")
+            .argName("NAME")
+            .hasArg()
+            .required(true)
+            .desc("Name of the example to launch, one of: cloud, techproducts, schemaless, films.")
+            .longOpt("example")
+            .build(),
+        Option.builder("script")
+            .argName("PATH")
+            .hasArg()
+            .required(false)
+            .desc("Path to the bin/solr script.")
+            .build(),
+        Option.builder("d")
+            .argName("DIR")
+            .hasArg()
+            .required(true)
+            .desc("Path to the Solr server directory.")
+            .longOpt("serverDir")
+            .build(),
+        Option.builder("force")
+            .argName("FORCE")
+            .desc("Force option in case Solr is run as root.")
+            .build(),
+        Option.builder("exampleDir")
+            .argName("DIR")
+            .hasArg()
+            .required(false)
+            .desc(
+                "Path to the Solr example directory; if not provided, ${serverDir}/../example is expected to exist.")
+            .build(),
+        Option.builder("urlScheme")
+            .argName("SCHEME")
+            .hasArg()
+            .required(false)
+            .desc("Solr URL scheme: http or https, defaults to http if not specified.")
+            .build(),
+        Option.builder("p")
+            .argName("PORT")
+            .hasArg()
+            .required(false)
+            .desc("Specify the port to start the Solr HTTP listener on; default is 8983.")
+            .longOpt("port")
+            .build(),
+        Option.builder("h")
+            .argName("HOSTNAME")
+            .hasArg()
+            .required(false)
+            .desc("Specify the hostname for this Solr instance.")
+            .longOpt("host")
+            .build(),
+        Option.builder("z")
+            .argName("ZKHOST")
+            .hasArg()
+            .required(false)
+            .desc("ZooKeeper connection string; only used when running in SolrCloud mode using -c.")
+            .longOpt("zkhost")
+            .build(),
+        Option.builder("c")
+            .required(false)
+            .desc(
+                "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.")
+            .longOpt("cloud")
+            .build(),
+        Option.builder("m")
+            .argName("MEM")
+            .hasArg()
+            .required(false)
+            .desc(
+                "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.")
+            .longOpt("memory")
+            .build(),
+        Option.builder("a")
+            .argName("OPTS")
+            .hasArg()
+            .required(false)
+            .desc(
+                "Additional options to be passed to the JVM when starting example Solr server(s).")
+            .longOpt("addlopts")
+            .build());
+  }
+
+  @Override
+  public 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!");
+
+    echoIfVerbose(
+        "Running with\nserverDir="
+            + serverDir.getAbsolutePath()
+            + ",\nexampleDir="
+            + exampleDir.getAbsolutePath()
+            + "\nscript="
+            + script,
+        cli);
+
+    String exampleType = cli.getOptionValue("example");
+    if ("cloud".equals(exampleType)) {
+      runCloudExample(cli);
+    } else if ("techproducts".equals(exampleType)
+        || "schemaless".equals(exampleType)
+        || "films".equals(exampleType)) {
+      runExample(cli, exampleType);
+    } else {
+      throw new IllegalArgumentException(
+          "Unsupported example "
+              + exampleType
+              + "! Please choose one of: cloud, schemaless, techproducts, or films");
+    }
+  }
+
+  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" : "_default";
+
+    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) {
+      if (SolrCLI.safeCheckCollectionExists(solrUrl, collectionName)) {
+        alreadyExists = true;
+        echo(
+            "\nWARNING: Collection '"
+                + collectionName
+                + "' already exists!\nChecked collection existence using Collections API");
+      }
+    } else {
+      String coreName = collectionName;
+      if (SolrCLI.safeCheckCoreExists(solrUrl, coreName)) {
+        alreadyExists = true;
+        echo(
+            "\nWARNING: Core '"
+                + coreName
+                + "' already exists!\nChecked core existence using Core API command");
+      }
+    }
+
+    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(
+              SolrCLI.processCommandLineArgs(
+                  createTool.getName(),
+                  SolrCLI.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");
+      }
+    } else if ("films".equals(exampleName) && !alreadyExists) {
+      SolrClient solrClient = new Http2SolrClient.Builder(solrUrl).build();
+
+      echo("Adding dense vector field type to films schema \"_default\"");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/schema",
+            "{\n"
+                + "        \"add-field-type\" : {\n"
+                + "          \"name\":\"knn_vector_10\",\n"
+                + "          \"class\":\"solr.DenseVectorField\",\n"
+                + "          \"vectorDimension\":10,\n"
+                + "          \"similarityFunction\":cosine\n"
+                + "          \"knnAlgorithm\":hnsw\n"
+                + "        }\n"
+                + "      }");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      echo(
+          "Adding name, initial_release_date, and film_vector fields to films schema \"_default\"");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/schema",
+            "{\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"name\",\n"
+                + "          \"type\":\"text_general\",\n"
+                + "          \"multiValued\":false,\n"
+                + "          \"stored\":true\n"
+                + "        },\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"initial_release_date\",\n"
+                + "          \"type\":\"pdate\",\n"
+                + "          \"stored\":true\n"
+                + "        },\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"film_vector\",\n"
+                + "          \"type\":\"knn_vector_10\",\n"
+                + "          \"indexed\":true\n"
+                + "          \"stored\":true\n"
+                + "        }\n"
+                + "      }");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      echo("Adding paramsets \"algo\" and \"algo_b\" to films configuration for relevancy tuning");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/config/params",
+            "{\n"
+                + "        \"set\": {\n"
+                + "        \"algo_a\":{\n"
+                + "               \"defType\":\"dismax\",\n"
+                + "               \"qf\":\"name\"\n"
+                + "             }\n"
+                + "           },\n"
+                + "           \"set\": {\n"
+                + "             \"algo_b\":{\n"
+                + "               \"defType\":\"dismax\",\n"
+                + "               \"qf\":\"name\",\n"
+                + "               \"mm\":\"100%\"\n"
+                + "             }\n"
+                + "            }\n"
+                + "        }\n");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      File filmsJsonFile = new File(exampleDir, "films/films.json");
+      String updateUrl = String.format(Locale.ROOT, "%s/%s/update/json", solrUrl, collectionName);
+      echo("Indexing films example docs from " + filmsJsonFile.getAbsolutePath());
+      String currentPropVal = System.getProperty("url");
+      System.setProperty("url", updateUrl);
+      SimplePostTool.main(new String[] {filmsJsonFile.getAbsolutePath()});
+      if (currentPropVal != null) {
+        System.setProperty("url", currentPropVal); // reset
+      } else {
+        System.clearProperty("url");
+      }
+    }
+
+    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, StandardCharsets.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;
+        echoIfVerbose("Using port " + port + " for node " + (n + 1), cli);
+      }
+    } 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) {
+      @SuppressWarnings("unchecked")
+      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(
+          SolrCLI.processCommandLineArgs(
+              configTool.getName(),
+              SolrCLI.joinCommonAndToolOptions(configTool.getOptions()),
+              configArgs));
+    } catch (Exception exc) {
+      CLIO.err("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(Collections.singletonList(zkHost), Optional.empty()).build();
+      cloudClient.connect();
+      Set<String> liveNodes = cloudClient.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.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) {
+      CLIO.err("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 forceArg = cli.hasOption("force") ? " -force" : "";
+
+    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 %s",
+            callScript,
+            cloudModeArg,
+            port,
+            solrHome,
+            hostArg,
+            zkHostArg,
+            memArg,
+            forceArg,
+            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 (Map.Entry<String, String> entry : procEnv.entrySet()) {
+          String envVar = entry.getKey();
+          String envVarVal = entry.getValue();
+          if (envVarVal != null && !"EXAMPLE".equals(envVar) && !envVar.startsWith("SOLR_")) {
+            startEnv.put(envVar, envVarVal);
+          }
+        }
+      }
+      DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
+      executor.execute(org.apache.commons.exec.CommandLine.parse(startCmd), startEnv, handler);
+
+      // wait for execution.
+      try {
+        handler.waitFor(3000);
+      } catch (InterruptedException ie) {
+        // safe to ignore ...
+        Thread.interrupted();
+      }
+      if (handler.hasResult() && handler.getExitValue() != 0) {
+        throw new Exception(
+            "Failed to start Solr using command: "
+                + startCmd
+                + " Exception : "
+                + handler.getException());
+      }
+    } else {
+      try {
+        code = executor.execute(org.apache.commons.exec.CommandLine.parse(startCmd));
+      } catch (ExecuteException e) {
+        throw new Exception(
+            "Failed to start Solr using command: " + startCmd + " Exception : " + e);
+      }
+    }
+    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 = "_default";
+    String collectionName = "gettingstarted";
+
+    File configsetsDir = new File(serverDir, "solr/configsets");
+
+    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 (SolrCLI.safeCheckCollectionExists(solrUrl, 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 = "_default 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 (SolrCLI.safeCheckCollectionExists(solrUrl, 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(
+            SolrCLI.processCommandLineArgs(
+                createCollectionTool.getName(),
+                SolrCLI.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()) Files.copy(src.toPath(), dest.toPath());
+
+    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);

Review Comment:
   <b>*[UNENCRYPTED_SOCKET](https://find-sec-bugs.github.io/bugs.htm#UNENCRYPTED_SOCKET):</b>*  Unencrypted socket to org.apache.solr.util.cli.RunExampleTool (instead of SSLSocket)
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=492276424&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=492276424&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=492276424&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=492276424&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=492276424&lift_comment_rating=5) ]



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] risdenk commented on a diff in pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "risdenk (via GitHub)" <gi...@apache.org>.
risdenk commented on code in PR #1568:
URL: https://github.com/apache/solr/pull/1568#discussion_r1169263213


##########
solr/packaging/test/test_export.bats:
##########
@@ -29,7 +29,7 @@ teardown() {
 }
 
 @test "Check export command" {
-  run solr start -c -Dsolr.modules=sql

Review Comment:
   Hmmm ok. totally unrelated to the change for this jira though...
   
   Either way looks like sql module is tested here https://github.com/apache/solr/blob/main/solr/packaging/test/test_modules.bats#L33 which is what I was worried we were losing.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] epugh commented on pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "epugh (via GitHub)" <gi...@apache.org>.
epugh commented on PR #1568:
URL: https://github.com/apache/solr/pull/1568#issuecomment-1514849013

   > Huge improvement to split this up. LGTM although have not studied every change.
   > 
   > Are there places where you did more than simply breaking classes out, where you'd like us to pay special attention?
   
   I tried to restrain myself from "fixing" too many things.  The edits were mostly just responding to some intellij warnings..   I think as long as the changes to the bash/cmd scripts look good?   The bats tests run as do the unit tests, so I think it's actually not too much risk..   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] epugh commented on pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "epugh (via GitHub)" <gi...@apache.org>.
epugh commented on PR #1568:
URL: https://github.com/apache/solr/pull/1568#issuecomment-1511257134

   Question?  Should we move `org.apache.solr.util.cli` up to `org.apache.solr.cli`?    And related...  Should we move `SolrCLI.java` into the `cli` package?   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] sonatype-lift[bot] commented on a diff in pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "sonatype-lift[bot] (via GitHub)" <gi...@apache.org>.
sonatype-lift[bot] commented on code in PR #1568:
URL: https://github.com/apache/solr/pull/1568#discussion_r1169004221


##########
solr/core/src/java/org/apache/solr/util/cli/SolrCLI.java:
##########
@@ -0,0 +1,652 @@
+/*
+ * 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.cli;
+
+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;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.File;
+import java.lang.invoke.MethodHandles;
+import java.net.ConnectException;
+import java.net.SocketException;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+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.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudHttp2SolrClient;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.request.CoreAdminRequest;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.common.SolrException;
+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.util.SolrVersion;
+import org.apache.solr.util.StartupLoggingUtils;
+import org.apache.solr.util.configuration.SSLConfigurationsFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Command-line utility for working with Solr. */
+public class SolrCLI implements CLIO {
+  private static final long MAX_WAIT_FOR_CORE_LOAD_NANOS =
+      TimeUnit.NANOSECONDS.convert(1, TimeUnit.MINUTES);
+
+  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";
+
+  public static final Option OPTION_ZKHOST =
+      Option.builder("z")
+          .argName("HOST")
+          .hasArg()
+          .required(false)
+          .desc("Address of the ZooKeeper ensemble; defaults to: " + ZK_HOST)
+          .longOpt("zkHost")
+          .build();
+  public static final Option OPTION_SOLRURL =
+      Option.builder("solrUrl")
+          .argName("HOST")
+          .hasArg()
+          .required(false)
+          .desc(
+              "Base Solr URL, which can be used to determine the zkHost if that's not known; defaults to: "
+                  + DEFAULT_SOLR_URL)
+          .build();
+  public static final Option OPTION_VERBOSE =
+      Option.builder("verbose").required(false).desc("Enable more verbose command output.").build();
+
+  public static final Option OPTION_RECURSE =
+      Option.builder("recurse")
+          .argName("recurse")
+          .hasArg()
+          .required(false)
+          .desc("Recurse (true|false), default is false.")
+          // .type(Boolean.class)
+          .build();
+
+  public static final List<Option> cloudOptions =
+      List.of(
+          OPTION_ZKHOST,
+          Option.builder("c")
+              .argName("COLLECTION")
+              .hasArg()
+              .required(false)
+              .desc("Name of collection; no default.")
+              .longOpt("collection")
+              .build(),
+          OPTION_VERBOSE);
+
+  public 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) {
+      CLIO.err(
+          "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
+      CLIO.out(SolrVersion.LATEST.toString());
+      exit(0);
+    }
+
+    SSLConfigurationsFactory.current().init();
+
+    Tool tool = null;
+    try {
+      tool = findTool(args);
+    } catch (IllegalArgumentException iae) {
+      CLIO.err(iae.getMessage());
+      System.exit(1);
+    }
+    CommandLine cli = parseCmdLine(tool.getName(), 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 toolName, String[] args, List<Option> toolOptions) {
+    // the parser doesn't like -D props
+    List<String> toolArgList = new ArrayList<>();
+    List<String> dashDList = new ArrayList<>();
+    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(toolName, toolOptions, toolArgs);
+
+    List<String> 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 {
+      CLIO.err(
+          "WARNING: "
+              + sysProp
+              + " file "
+              + keyStore
+              + " not found! https requests to Solr will likely fail; please update your "
+              + sysProp
+              + " setting to use an absolute path.");
+    }
+  }
+
+  public static void raiseLogLevelUnlessVerbose(CommandLine cli) {
+    if (!cli.hasOption(OPTION_VERBOSE.getOpt())) {
+      StartupLoggingUtils.changeLogLevel("WARN");
+    }
+  }
+
+  /** Support options common to all tools. */
+  public static List<Option> getCommonToolOptions() {
+    return List.of();
+  }
+
+  // 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 ("auth".equals(toolType)) return new AuthTool();
+    else if ("export".equals(toolType)) return new ExportTool();
+    else if ("package".equals(toolType)) return new PackageTool();
+
+    // If you add a built-in tool to this class, add it here to avoid
+    // classpath scanning
+
+    for (Class<? extends Tool> next : findToolClassesInPackage("org.apache.solr.util")) {
+      Tool tool = next.getConstructor().newInstance();
+      if (toolType.equals(tool.getName())) return tool;
+    }
+
+    throw new IllegalArgumentException(toolType + " is not a valid command!");
+  }
+
+  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()));
+    formatter.printHelp("export", getToolOptions(new ExportTool()));
+    formatter.printHelp("package", getToolOptions(new PackageTool()));
+
+    List<Class<? extends Tool>> toolClasses = findToolClassesInPackage("org.apache.solr.util");
+    for (Class<? extends Tool> next : toolClasses) {
+      Tool tool = next.getConstructor().newInstance();
+      formatter.printHelp(tool.getName(), getToolOptions(tool));
+    }
+  }
+
+  public static Options getToolOptions(Tool tool) {
+    Options options = new Options();
+    options.addOption("help", false, "Print this message");
+    options.addOption(OPTION_VERBOSE);
+    List<Option> toolOpts = tool.getOptions();
+    for (Option toolOpt : toolOpts) {
+      options.addOption(toolOpt);
+    }
+    return options;
+  }
+
+  public static List<Option> joinOptions(List<Option> lhs, List<Option> rhs) {
+    if (lhs == null) {
+      return rhs == null ? List.of() : rhs;
+    }
+
+    if (rhs == null) {
+      return lhs;
+    }
+
+    return Stream.concat(lhs.stream(), rhs.stream()).collect(Collectors.toUnmodifiableList());
+  }
+
+  /** Parses the command-line arguments passed by the user. */
+  public static CommandLine processCommandLineArgs(
+      String toolName, List<Option> customOptions, String[] args) {
+    Options options = new Options();
+
+    options.addOption("help", false, "Print this message");
+    options.addOption(OPTION_VERBOSE);
+
+    if (customOptions != null) {
+      for (Option customOption : customOptions) {
+        options.addOption(customOption);
+      }
+    }
+
+    CommandLine cli = null;
+    try {
+      cli = (new GnuParser()).parse(options, args);
+    } catch (ParseException exp) {
+      boolean hasHelpArg = false;
+      if (args != null) {
+        for (String arg : args) {
+          if ("--help".equals(arg) || "-help".equals(arg)) {
+            hasHelpArg = true;
+            break;
+          }
+        }
+      }
+      if (!hasHelpArg) {
+        CLIO.err("Failed to parse command-line arguments due to: " + exp.getMessage());
+      }
+      HelpFormatter formatter = new HelpFormatter();
+      formatter.printHelp(toolName, options);
+      exit(1);
+    }
+
+    if (cli.hasOption("help")) {
+      HelpFormatter formatter = new HelpFormatter();
+      formatter.printHelp(toolName, options);
+      exit(0);
+    }
+
+    return cli;
+  }
+
+  /** Scans Jar files on the classpath for Tool implementations to activate. */
+  private static List<Class<? extends Tool>> findToolClassesInPackage(String packageName) {
+    List<Class<? extends Tool>> toolClasses = new ArrayList<>();
+    try {
+      ClassLoader classLoader = SolrCLI.class.getClassLoader();
+      String path = packageName.replace('.', '/');
+      Enumeration<URL> resources = classLoader.getResources(path);
+      Set<String> classes = new TreeSet<>();
+      while (resources.hasMoreElements()) {
+        URL resource = resources.nextElement();
+        classes.addAll(findClasses(resource.getFile(), packageName));
+      }
+
+      for (String classInPackage : classes) {
+        Class<?> theClass = Class.forName(classInPackage);
+        if (Tool.class.isAssignableFrom(theClass)) toolClasses.add(theClass.asSubclass(Tool.class));
+      }
+    } 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 {}, due to: ", packageName, e);
+    }
+    return toolClasses;
+  }
+
+  private static Set<String> findClasses(String path, String packageName) throws Exception {
+    Set<String> classes = new TreeSet<>();
+    if (path.startsWith("file:") && path.contains("!")) {
+      String[] split = path.split("!");
+      URL jar = new URL(split[0]);
+      try (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;
+  }
+
+  /**
+   * 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 SolrServerException
+            || rootCause instanceof SocketException);
+    return wasCommError;
+  }
+
+  public static void checkCodeForAuthError(int code) {
+    if (code == UNAUTHORIZED.code || code == FORBIDDEN.code) {
+      throw new SolrException(
+          SolrException.ErrorCode.getErrorCode(code),
+          "Solr requires authentication for request. Please supply valid credentials. HTTP code="
+              + code);
+    }
+  }
+
+  public static boolean exceptionIsAuthRelated(Exception exc) {
+    return (exc instanceof SolrException
+        && Arrays.asList(UNAUTHORIZED.code, FORBIDDEN.code).contains(((SolrException) exc).code()));
+  }
+
+  public static SolrClient getSolrClient(String solrUrl) {
+    return new Http2SolrClient.Builder(solrUrl).maxConnectionsPerHost(32).build();
+  }
+
+  /**
+   * Get Solr base url with port if present and root from URI
+   *
+   * @param uri Full Solr URI (e.g. http://localhost:8983/solr/admin/info/system)
+   * @return Solr base url with port and root (from above example http://localhost:8983/solr)
+   */
+  public static String getSolrUrlFromUri(URI uri) {
+    return uri.getScheme() + "://" + uri.getAuthority() + "/" + uri.getPath().split("/")[1];
+  }
+
+  public static ModifiableSolrParams getSolrParamsFromUri(URI uri) {
+    ModifiableSolrParams paramsMap = new ModifiableSolrParams();
+    String[] params = uri.getQuery() == null ? new String[] {} : uri.getQuery().split("&");
+    for (String param : params) {
+      String[] paramSplit = param.split("=");
+      paramsMap.add(paramSplit[0], paramSplit[1]);
+    }
+    return paramsMap;
+  }
+
+  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);
+  }
+
+  public static final String DEFAULT_CONFIG_SET = "_default";
+
+  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;
+
+  @VisibleForTesting
+  public static final String uptime(long uptimeMs) {
+    if (uptimeMs <= 0L) return "?";
+
+    long numDays = (uptimeMs >= MS_IN_DAY) ? (uptimeMs / MS_IN_DAY) : 0L;
+    long rem = uptimeMs - (numDays * MS_IN_DAY);
+    long numHours = (rem >= MS_IN_HOUR) ? (rem / MS_IN_HOUR) : 0L;
+    rem = rem - (numHours * MS_IN_HOUR);
+    long numMinutes = (rem >= MS_IN_MIN) ? (rem / MS_IN_MIN) : 0L;
+    rem = rem - (numMinutes * MS_IN_MIN);
+    long numSeconds = Math.round(rem / 1000.0);
+    return String.format(
+        Locale.ROOT,
+        "%d days, %d hours, %d minutes, %d seconds",
+        numDays,
+        numHours,
+        numMinutes,
+        numSeconds);
+  }
+
+  public static final List<Option> CREATE_COLLECTION_OPTIONS =
+      List.of(
+          OPTION_ZKHOST,
+          OPTION_SOLRURL,
+          Option.builder(NAME)
+              .argName("NAME")
+              .hasArg()
+              .required(true)
+              .desc("Name of collection to create.")
+              .build(),
+          Option.builder("shards")
+              .argName("#")
+              .hasArg()
+              .required(false)
+              .desc("Number of shards; default is 1.")
+              .build(),
+          Option.builder("replicationFactor")
+              .argName("#")
+              .hasArg()
+              .required(false)
+              .desc(
+                  "Number of copies of each document across the collection (replicas per shard); default is 1.")
+              .build(),
+          Option.builder("confdir")
+              .argName("NAME")
+              .hasArg()
+              .required(false)
+              .desc(
+                  "Configuration directory to copy when creating the new collection; default is "
+                      + DEFAULT_CONFIG_SET
+                      + '.')
+              .build(),
+          Option.builder("confname")
+              .argName("NAME")
+              .hasArg()
+              .required(false)
+              .desc("Configuration name; default is the collection name.")
+              .build(),
+          Option.builder("configsetsDir")
+              .argName("DIR")
+              .hasArg()
+              .required(true)
+              .desc("Path to configsets directory on the local system.")
+              .build(),
+          OPTION_VERBOSE);
+
+  /**
+   * 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 CloudHttp2SolrClient.Builder(Collections.singletonList(zkHost), Optional.empty())
+              .build()) {
+        cloudSolrClient.connect();
+        Set<String> liveNodes = cloudSolrClient.getClusterState().getLiveNodes();
+        if (liveNodes.isEmpty())
+          throw new IllegalStateException(
+              "No live nodes found! Cannot determine 'solrUrl' from ZooKeeper: " + zkHost);
+
+        String firstLiveNode = liveNodes.iterator().next();
+        solrUrl = ZkStateReader.from(cloudSolrClient).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 += "/";
+
+    try (var solrClient = getSolrClient(solrUrl)) {
+      // hit Solr to get system info
+      NamedList<Object> systemInfo =
+          solrClient.request(
+              new GenericSolrRequest(SolrRequest.METHOD.GET, CommonParams.SYSTEM_INFO_PATH));
+
+      // convert raw JSON into user-friendly output
+      StatusTool statusTool = new StatusTool();
+      Map<String, Object> status = statusTool.reportStatus(systemInfo, solrClient);
+      @SuppressWarnings("unchecked")
+      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;
+      }
+    }
+
+    return zkHost;
+  }
+
+  public static boolean safeCheckCollectionExists(String solrUrl, String collection) {
+    boolean exists = false;
+    try (var solrClient = getSolrClient(solrUrl); ) {
+      NamedList<Object> existsCheckResult = solrClient.request(new CollectionAdminRequest.List());
+      @SuppressWarnings("unchecked")
+      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;
+  }
+
+  @SuppressWarnings("unchecked")
+  public static boolean safeCheckCoreExists(String solrUrl, String coreName) {
+    boolean exists = false;
+    try (var solrClient = getSolrClient(solrUrl)) {
+      boolean wait = false;
+      final long startWaitAt = System.nanoTime();
+      do {
+        if (wait) {
+          final int clamPeriodForStatusPollMs = 1000;
+          Thread.sleep(clamPeriodForStatusPollMs);
+        }
+        NamedList<Object> existsCheckResult =
+            CoreAdminRequest.getStatus(coreName, solrClient).getResponse();
+        NamedList<Object> status = (NamedList) existsCheckResult.get("status");
+        NamedList<Object> coreStatus = (NamedList) status.get(coreName);

Review Comment:
   <picture><img alt="16% of developers fix this issue" src="https://lift.sonatype.com/api/commentimage/fixrate/16/display.svg"></picture>
   
   <b>*NULL_DEREFERENCE:</b>*  object `status` last assigned on line 632 could be null and is dereferenced at line 633.
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=494068501&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=494068501&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494068501&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494068501&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=494068501&lift_comment_rating=5) ]



##########
solr/core/src/java/org/apache/solr/util/cli/SolrCLI.java:
##########
@@ -0,0 +1,652 @@
+/*
+ * 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.cli;
+
+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;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.File;
+import java.lang.invoke.MethodHandles;
+import java.net.ConnectException;
+import java.net.SocketException;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+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.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudHttp2SolrClient;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.request.CoreAdminRequest;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.common.SolrException;
+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.util.SolrVersion;
+import org.apache.solr.util.StartupLoggingUtils;
+import org.apache.solr.util.configuration.SSLConfigurationsFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Command-line utility for working with Solr. */
+public class SolrCLI implements CLIO {
+  private static final long MAX_WAIT_FOR_CORE_LOAD_NANOS =
+      TimeUnit.NANOSECONDS.convert(1, TimeUnit.MINUTES);
+
+  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";
+
+  public static final Option OPTION_ZKHOST =
+      Option.builder("z")
+          .argName("HOST")
+          .hasArg()
+          .required(false)
+          .desc("Address of the ZooKeeper ensemble; defaults to: " + ZK_HOST)
+          .longOpt("zkHost")
+          .build();
+  public static final Option OPTION_SOLRURL =
+      Option.builder("solrUrl")
+          .argName("HOST")
+          .hasArg()
+          .required(false)
+          .desc(
+              "Base Solr URL, which can be used to determine the zkHost if that's not known; defaults to: "
+                  + DEFAULT_SOLR_URL)
+          .build();
+  public static final Option OPTION_VERBOSE =
+      Option.builder("verbose").required(false).desc("Enable more verbose command output.").build();
+
+  public static final Option OPTION_RECURSE =
+      Option.builder("recurse")
+          .argName("recurse")
+          .hasArg()
+          .required(false)
+          .desc("Recurse (true|false), default is false.")
+          // .type(Boolean.class)
+          .build();
+
+  public static final List<Option> cloudOptions =
+      List.of(
+          OPTION_ZKHOST,
+          Option.builder("c")
+              .argName("COLLECTION")
+              .hasArg()
+              .required(false)
+              .desc("Name of collection; no default.")
+              .longOpt("collection")
+              .build(),
+          OPTION_VERBOSE);
+
+  public 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) {
+      CLIO.err(
+          "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
+      CLIO.out(SolrVersion.LATEST.toString());
+      exit(0);
+    }
+
+    SSLConfigurationsFactory.current().init();
+
+    Tool tool = null;
+    try {
+      tool = findTool(args);
+    } catch (IllegalArgumentException iae) {
+      CLIO.err(iae.getMessage());
+      System.exit(1);
+    }
+    CommandLine cli = parseCmdLine(tool.getName(), 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 toolName, String[] args, List<Option> toolOptions) {
+    // the parser doesn't like -D props
+    List<String> toolArgList = new ArrayList<>();
+    List<String> dashDList = new ArrayList<>();
+    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(toolName, toolOptions, toolArgs);
+
+    List<String> 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 {
+      CLIO.err(
+          "WARNING: "
+              + sysProp
+              + " file "
+              + keyStore
+              + " not found! https requests to Solr will likely fail; please update your "
+              + sysProp
+              + " setting to use an absolute path.");
+    }
+  }
+
+  public static void raiseLogLevelUnlessVerbose(CommandLine cli) {
+    if (!cli.hasOption(OPTION_VERBOSE.getOpt())) {
+      StartupLoggingUtils.changeLogLevel("WARN");
+    }
+  }
+
+  /** Support options common to all tools. */
+  public static List<Option> getCommonToolOptions() {
+    return List.of();
+  }
+
+  // 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 ("auth".equals(toolType)) return new AuthTool();
+    else if ("export".equals(toolType)) return new ExportTool();
+    else if ("package".equals(toolType)) return new PackageTool();
+
+    // If you add a built-in tool to this class, add it here to avoid
+    // classpath scanning
+
+    for (Class<? extends Tool> next : findToolClassesInPackage("org.apache.solr.util")) {
+      Tool tool = next.getConstructor().newInstance();
+      if (toolType.equals(tool.getName())) return tool;
+    }
+
+    throw new IllegalArgumentException(toolType + " is not a valid command!");
+  }
+
+  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()));
+    formatter.printHelp("export", getToolOptions(new ExportTool()));
+    formatter.printHelp("package", getToolOptions(new PackageTool()));
+
+    List<Class<? extends Tool>> toolClasses = findToolClassesInPackage("org.apache.solr.util");
+    for (Class<? extends Tool> next : toolClasses) {
+      Tool tool = next.getConstructor().newInstance();
+      formatter.printHelp(tool.getName(), getToolOptions(tool));
+    }
+  }
+
+  public static Options getToolOptions(Tool tool) {
+    Options options = new Options();
+    options.addOption("help", false, "Print this message");
+    options.addOption(OPTION_VERBOSE);
+    List<Option> toolOpts = tool.getOptions();
+    for (Option toolOpt : toolOpts) {
+      options.addOption(toolOpt);
+    }
+    return options;
+  }
+
+  public static List<Option> joinOptions(List<Option> lhs, List<Option> rhs) {
+    if (lhs == null) {
+      return rhs == null ? List.of() : rhs;
+    }
+
+    if (rhs == null) {
+      return lhs;
+    }
+
+    return Stream.concat(lhs.stream(), rhs.stream()).collect(Collectors.toUnmodifiableList());
+  }
+
+  /** Parses the command-line arguments passed by the user. */
+  public static CommandLine processCommandLineArgs(
+      String toolName, List<Option> customOptions, String[] args) {
+    Options options = new Options();
+
+    options.addOption("help", false, "Print this message");
+    options.addOption(OPTION_VERBOSE);
+
+    if (customOptions != null) {
+      for (Option customOption : customOptions) {
+        options.addOption(customOption);
+      }
+    }
+
+    CommandLine cli = null;
+    try {
+      cli = (new GnuParser()).parse(options, args);
+    } catch (ParseException exp) {
+      boolean hasHelpArg = false;
+      if (args != null) {
+        for (String arg : args) {
+          if ("--help".equals(arg) || "-help".equals(arg)) {
+            hasHelpArg = true;
+            break;
+          }
+        }
+      }
+      if (!hasHelpArg) {
+        CLIO.err("Failed to parse command-line arguments due to: " + exp.getMessage());
+      }
+      HelpFormatter formatter = new HelpFormatter();
+      formatter.printHelp(toolName, options);
+      exit(1);
+    }
+
+    if (cli.hasOption("help")) {
+      HelpFormatter formatter = new HelpFormatter();
+      formatter.printHelp(toolName, options);
+      exit(0);
+    }
+
+    return cli;
+  }
+
+  /** Scans Jar files on the classpath for Tool implementations to activate. */
+  private static List<Class<? extends Tool>> findToolClassesInPackage(String packageName) {
+    List<Class<? extends Tool>> toolClasses = new ArrayList<>();
+    try {
+      ClassLoader classLoader = SolrCLI.class.getClassLoader();
+      String path = packageName.replace('.', '/');
+      Enumeration<URL> resources = classLoader.getResources(path);
+      Set<String> classes = new TreeSet<>();
+      while (resources.hasMoreElements()) {
+        URL resource = resources.nextElement();
+        classes.addAll(findClasses(resource.getFile(), packageName));
+      }
+
+      for (String classInPackage : classes) {
+        Class<?> theClass = Class.forName(classInPackage);
+        if (Tool.class.isAssignableFrom(theClass)) toolClasses.add(theClass.asSubclass(Tool.class));
+      }
+    } 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 {}, due to: ", packageName, e);
+    }
+    return toolClasses;
+  }
+
+  private static Set<String> findClasses(String path, String packageName) throws Exception {
+    Set<String> classes = new TreeSet<>();
+    if (path.startsWith("file:") && path.contains("!")) {
+      String[] split = path.split("!");
+      URL jar = new URL(split[0]);
+      try (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;
+  }
+
+  /**
+   * 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 SolrServerException
+            || rootCause instanceof SocketException);
+    return wasCommError;
+  }
+
+  public static void checkCodeForAuthError(int code) {
+    if (code == UNAUTHORIZED.code || code == FORBIDDEN.code) {
+      throw new SolrException(
+          SolrException.ErrorCode.getErrorCode(code),
+          "Solr requires authentication for request. Please supply valid credentials. HTTP code="
+              + code);
+    }
+  }
+
+  public static boolean exceptionIsAuthRelated(Exception exc) {
+    return (exc instanceof SolrException
+        && Arrays.asList(UNAUTHORIZED.code, FORBIDDEN.code).contains(((SolrException) exc).code()));
+  }
+
+  public static SolrClient getSolrClient(String solrUrl) {
+    return new Http2SolrClient.Builder(solrUrl).maxConnectionsPerHost(32).build();
+  }
+
+  /**
+   * Get Solr base url with port if present and root from URI
+   *
+   * @param uri Full Solr URI (e.g. http://localhost:8983/solr/admin/info/system)
+   * @return Solr base url with port and root (from above example http://localhost:8983/solr)
+   */
+  public static String getSolrUrlFromUri(URI uri) {
+    return uri.getScheme() + "://" + uri.getAuthority() + "/" + uri.getPath().split("/")[1];
+  }
+
+  public static ModifiableSolrParams getSolrParamsFromUri(URI uri) {
+    ModifiableSolrParams paramsMap = new ModifiableSolrParams();
+    String[] params = uri.getQuery() == null ? new String[] {} : uri.getQuery().split("&");
+    for (String param : params) {
+      String[] paramSplit = param.split("=");
+      paramsMap.add(paramSplit[0], paramSplit[1]);
+    }
+    return paramsMap;
+  }
+
+  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);
+  }
+
+  public static final String DEFAULT_CONFIG_SET = "_default";
+
+  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;
+
+  @VisibleForTesting
+  public static final String uptime(long uptimeMs) {
+    if (uptimeMs <= 0L) return "?";
+
+    long numDays = (uptimeMs >= MS_IN_DAY) ? (uptimeMs / MS_IN_DAY) : 0L;
+    long rem = uptimeMs - (numDays * MS_IN_DAY);
+    long numHours = (rem >= MS_IN_HOUR) ? (rem / MS_IN_HOUR) : 0L;
+    rem = rem - (numHours * MS_IN_HOUR);
+    long numMinutes = (rem >= MS_IN_MIN) ? (rem / MS_IN_MIN) : 0L;
+    rem = rem - (numMinutes * MS_IN_MIN);
+    long numSeconds = Math.round(rem / 1000.0);
+    return String.format(
+        Locale.ROOT,
+        "%d days, %d hours, %d minutes, %d seconds",
+        numDays,
+        numHours,
+        numMinutes,
+        numSeconds);
+  }
+
+  public static final List<Option> CREATE_COLLECTION_OPTIONS =
+      List.of(
+          OPTION_ZKHOST,
+          OPTION_SOLRURL,
+          Option.builder(NAME)
+              .argName("NAME")
+              .hasArg()
+              .required(true)
+              .desc("Name of collection to create.")
+              .build(),
+          Option.builder("shards")
+              .argName("#")
+              .hasArg()
+              .required(false)
+              .desc("Number of shards; default is 1.")
+              .build(),
+          Option.builder("replicationFactor")
+              .argName("#")
+              .hasArg()
+              .required(false)
+              .desc(
+                  "Number of copies of each document across the collection (replicas per shard); default is 1.")
+              .build(),
+          Option.builder("confdir")
+              .argName("NAME")
+              .hasArg()
+              .required(false)
+              .desc(
+                  "Configuration directory to copy when creating the new collection; default is "
+                      + DEFAULT_CONFIG_SET
+                      + '.')
+              .build(),
+          Option.builder("confname")
+              .argName("NAME")
+              .hasArg()
+              .required(false)
+              .desc("Configuration name; default is the collection name.")
+              .build(),
+          Option.builder("configsetsDir")
+              .argName("DIR")
+              .hasArg()
+              .required(true)
+              .desc("Path to configsets directory on the local system.")
+              .build(),
+          OPTION_VERBOSE);
+
+  /**
+   * 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 CloudHttp2SolrClient.Builder(Collections.singletonList(zkHost), Optional.empty())
+              .build()) {
+        cloudSolrClient.connect();
+        Set<String> liveNodes = cloudSolrClient.getClusterState().getLiveNodes();
+        if (liveNodes.isEmpty())
+          throw new IllegalStateException(
+              "No live nodes found! Cannot determine 'solrUrl' from ZooKeeper: " + zkHost);
+
+        String firstLiveNode = liveNodes.iterator().next();
+        solrUrl = ZkStateReader.from(cloudSolrClient).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 += "/";
+
+    try (var solrClient = getSolrClient(solrUrl)) {
+      // hit Solr to get system info
+      NamedList<Object> systemInfo =
+          solrClient.request(
+              new GenericSolrRequest(SolrRequest.METHOD.GET, CommonParams.SYSTEM_INFO_PATH));
+
+      // convert raw JSON into user-friendly output
+      StatusTool statusTool = new StatusTool();
+      Map<String, Object> status = statusTool.reportStatus(systemInfo, solrClient);
+      @SuppressWarnings("unchecked")
+      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;
+      }
+    }
+
+    return zkHost;
+  }
+
+  public static boolean safeCheckCollectionExists(String solrUrl, String collection) {
+    boolean exists = false;
+    try (var solrClient = getSolrClient(solrUrl); ) {
+      NamedList<Object> existsCheckResult = solrClient.request(new CollectionAdminRequest.List());
+      @SuppressWarnings("unchecked")
+      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;
+  }
+
+  @SuppressWarnings("unchecked")
+  public static boolean safeCheckCoreExists(String solrUrl, String coreName) {
+    boolean exists = false;
+    try (var solrClient = getSolrClient(solrUrl)) {
+      boolean wait = false;
+      final long startWaitAt = System.nanoTime();
+      do {
+        if (wait) {
+          final int clamPeriodForStatusPollMs = 1000;
+          Thread.sleep(clamPeriodForStatusPollMs);
+        }
+        NamedList<Object> existsCheckResult =
+            CoreAdminRequest.getStatus(coreName, solrClient).getResponse();
+        NamedList<Object> status = (NamedList) existsCheckResult.get("status");
+        NamedList<Object> coreStatus = (NamedList) status.get(coreName);
+        Map<String, Object> failureStatus =
+            (Map<String, Object>) existsCheckResult.get("initFailures");
+        String errorMsg = (String) failureStatus.get(coreName);

Review Comment:
   <picture><img alt="16% of developers fix this issue" src="https://lift.sonatype.com/api/commentimage/fixrate/16/display.svg"></picture>
   
   <b>*NULL_DEREFERENCE:</b>*  object `failureStatus` last assigned on line 635 could be null and is dereferenced at line 636.
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=494068856&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=494068856&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494068856&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494068856&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=494068856&lift_comment_rating=5) ]



##########
solr/core/src/java/org/apache/solr/util/cli/SolrCLI.java:
##########
@@ -0,0 +1,652 @@
+/*
+ * 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.cli;
+
+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;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.File;
+import java.lang.invoke.MethodHandles;
+import java.net.ConnectException;
+import java.net.SocketException;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+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.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudHttp2SolrClient;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.request.CoreAdminRequest;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.common.SolrException;
+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.util.SolrVersion;
+import org.apache.solr.util.StartupLoggingUtils;
+import org.apache.solr.util.configuration.SSLConfigurationsFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Command-line utility for working with Solr. */
+public class SolrCLI implements CLIO {
+  private static final long MAX_WAIT_FOR_CORE_LOAD_NANOS =
+      TimeUnit.NANOSECONDS.convert(1, TimeUnit.MINUTES);
+
+  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";
+
+  public static final Option OPTION_ZKHOST =
+      Option.builder("z")
+          .argName("HOST")
+          .hasArg()
+          .required(false)
+          .desc("Address of the ZooKeeper ensemble; defaults to: " + ZK_HOST)
+          .longOpt("zkHost")
+          .build();
+  public static final Option OPTION_SOLRURL =
+      Option.builder("solrUrl")
+          .argName("HOST")
+          .hasArg()
+          .required(false)
+          .desc(
+              "Base Solr URL, which can be used to determine the zkHost if that's not known; defaults to: "
+                  + DEFAULT_SOLR_URL)
+          .build();
+  public static final Option OPTION_VERBOSE =
+      Option.builder("verbose").required(false).desc("Enable more verbose command output.").build();
+
+  public static final Option OPTION_RECURSE =
+      Option.builder("recurse")
+          .argName("recurse")
+          .hasArg()
+          .required(false)
+          .desc("Recurse (true|false), default is false.")
+          // .type(Boolean.class)
+          .build();
+
+  public static final List<Option> cloudOptions =
+      List.of(
+          OPTION_ZKHOST,
+          Option.builder("c")
+              .argName("COLLECTION")
+              .hasArg()
+              .required(false)
+              .desc("Name of collection; no default.")
+              .longOpt("collection")
+              .build(),
+          OPTION_VERBOSE);
+
+  public 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) {
+      CLIO.err(
+          "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
+      CLIO.out(SolrVersion.LATEST.toString());
+      exit(0);
+    }
+
+    SSLConfigurationsFactory.current().init();
+
+    Tool tool = null;
+    try {
+      tool = findTool(args);
+    } catch (IllegalArgumentException iae) {
+      CLIO.err(iae.getMessage());
+      System.exit(1);
+    }
+    CommandLine cli = parseCmdLine(tool.getName(), 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 toolName, String[] args, List<Option> toolOptions) {
+    // the parser doesn't like -D props
+    List<String> toolArgList = new ArrayList<>();
+    List<String> dashDList = new ArrayList<>();
+    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(toolName, toolOptions, toolArgs);
+
+    List<String> 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);

Review Comment:
   <picture><img alt="8% of developers fix this issue" src="https://lift.sonatype.com/api/commentimage/fixrate/8/display.svg"></picture>
   
   <b>*[PATH_TRAVERSAL_IN](https://find-sec-bugs.github.io/bugs.htm#PATH_TRAVERSAL_IN):</b>*  This API (java/io/File.<init>(Ljava/lang/String;)V) reads a file whose location might be specified by user input
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=494063413&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=494063413&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494063413&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494063413&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=494063413&lift_comment_rating=5) ]



##########
solr/core/src/java/org/apache/solr/util/cli/SolrCLI.java:
##########
@@ -0,0 +1,652 @@
+/*
+ * 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.cli;
+
+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;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.File;
+import java.lang.invoke.MethodHandles;
+import java.net.ConnectException;
+import java.net.SocketException;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+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.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudHttp2SolrClient;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.request.CoreAdminRequest;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.common.SolrException;
+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.util.SolrVersion;
+import org.apache.solr.util.StartupLoggingUtils;
+import org.apache.solr.util.configuration.SSLConfigurationsFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Command-line utility for working with Solr. */
+public class SolrCLI implements CLIO {
+  private static final long MAX_WAIT_FOR_CORE_LOAD_NANOS =
+      TimeUnit.NANOSECONDS.convert(1, TimeUnit.MINUTES);
+
+  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";
+
+  public static final Option OPTION_ZKHOST =
+      Option.builder("z")
+          .argName("HOST")
+          .hasArg()
+          .required(false)
+          .desc("Address of the ZooKeeper ensemble; defaults to: " + ZK_HOST)
+          .longOpt("zkHost")
+          .build();
+  public static final Option OPTION_SOLRURL =
+      Option.builder("solrUrl")
+          .argName("HOST")
+          .hasArg()
+          .required(false)
+          .desc(
+              "Base Solr URL, which can be used to determine the zkHost if that's not known; defaults to: "
+                  + DEFAULT_SOLR_URL)
+          .build();
+  public static final Option OPTION_VERBOSE =
+      Option.builder("verbose").required(false).desc("Enable more verbose command output.").build();
+
+  public static final Option OPTION_RECURSE =
+      Option.builder("recurse")
+          .argName("recurse")
+          .hasArg()
+          .required(false)
+          .desc("Recurse (true|false), default is false.")
+          // .type(Boolean.class)
+          .build();
+
+  public static final List<Option> cloudOptions =
+      List.of(
+          OPTION_ZKHOST,
+          Option.builder("c")
+              .argName("COLLECTION")
+              .hasArg()
+              .required(false)
+              .desc("Name of collection; no default.")
+              .longOpt("collection")
+              .build(),
+          OPTION_VERBOSE);
+
+  public 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) {
+      CLIO.err(
+          "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
+      CLIO.out(SolrVersion.LATEST.toString());
+      exit(0);
+    }
+
+    SSLConfigurationsFactory.current().init();
+
+    Tool tool = null;
+    try {
+      tool = findTool(args);
+    } catch (IllegalArgumentException iae) {
+      CLIO.err(iae.getMessage());
+      System.exit(1);
+    }
+    CommandLine cli = parseCmdLine(tool.getName(), 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 toolName, String[] args, List<Option> toolOptions) {
+    // the parser doesn't like -D props
+    List<String> toolArgList = new ArrayList<>();
+    List<String> dashDList = new ArrayList<>();
+    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(toolName, toolOptions, toolArgs);
+
+    List<String> 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);

Review Comment:
   <picture><img alt="8% of developers fix this issue" src="https://lift.sonatype.com/api/commentimage/fixrate/8/display.svg"></picture>
   
   <b>*[PATH_TRAVERSAL_IN](https://find-sec-bugs.github.io/bugs.htm#PATH_TRAVERSAL_IN):</b>*  This API (java/io/File.<init>(Ljava/lang/String;Ljava/lang/String;)V) reads a file whose location might be specified by user input
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=494063819&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=494063819&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494063819&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494063819&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=494063819&lift_comment_rating=5) ]



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] epugh commented on pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "epugh (via GitHub)" <gi...@apache.org>.
epugh commented on PR #1568:
URL: https://github.com/apache/solr/pull/1568#issuecomment-1514755448

   > I haven't had time to review all of the changes (5k LOC đŸ¤¯ ), but things LGTM from the brief skimming I was able to do.
   > 
   > I'd feel more confident in large refactors like this if the bats coverage was beefier to catch regressions in syntax/output, but that's probably an unrealistic expectation at this point and this is definitely a step in the right direction IMO.
   
   Yeah, I added two more bats tests, and actually the PDF that @bszabo97 uploaded on his work was super useful to me in thinking about these tests.     
   
   I hope to add more bats tests as I look at each Tool in seperate PR's...


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] risdenk commented on a diff in pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "risdenk (via GitHub)" <gi...@apache.org>.
risdenk commented on code in PR #1568:
URL: https://github.com/apache/solr/pull/1568#discussion_r1169241778


##########
solr/core/src/java/org/apache/solr/packagemanager/PackageManager.java:
##########
@@ -356,7 +357,7 @@ private boolean deployPackage(
     List<String> previouslyDeployedOnCollections = deployResult.second();
 
     // Verify
-    boolean verifySuccess = true;
+    boolean verifySuccess;
     // Verify that package was successfully deployed
     verifySuccess =

Review Comment:
   Combine these lines?



##########
solr/packaging/test/test_export.bats:
##########
@@ -29,7 +29,7 @@ teardown() {
 }
 
 @test "Check export command" {
-  run solr start -c -Dsolr.modules=sql

Review Comment:
   Ummm why is this removed? This is checking a query so need sql module right?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] epugh merged pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "epugh (via GitHub)" <gi...@apache.org>.
epugh merged PR #1568:
URL: https://github.com/apache/solr/pull/1568


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] epugh commented on a diff in pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "epugh (via GitHub)" <gi...@apache.org>.
epugh commented on code in PR #1568:
URL: https://github.com/apache/solr/pull/1568#discussion_r1169257870


##########
solr/packaging/test/test_export.bats:
##########
@@ -29,7 +29,7 @@ teardown() {
 }
 
 @test "Check export command" {
-  run solr start -c -Dsolr.modules=sql

Review Comment:
   I don't think it uses the sql module..  it's just a straight up solr query, not a sql one....



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] sonatype-lift[bot] commented on a diff in pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "sonatype-lift[bot] (via GitHub)" <gi...@apache.org>.
sonatype-lift[bot] commented on code in PR #1568:
URL: https://github.com/apache/solr/pull/1568#discussion_r1168680206


##########
solr/core/src/java/org/apache/solr/util/cli/RunExampleTool.java:
##########
@@ -0,0 +1,1032 @@
+/*
+ * 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.cli;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+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.concurrent.TimeUnit;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.exec.DefaultExecuteResultHandler;
+import org.apache.commons.exec.DefaultExecutor;
+import org.apache.commons.exec.ExecuteException;
+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.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.util.CLIO;
+import org.apache.solr.util.SimplePostTool;
+import org.apache.solr.util.SolrCLI;
+import org.noggit.CharArr;
+import org.noggit.JSONWriter;
+
+/** Supports an interactive session with the user to launch (or relaunch the -e cloud example) */
+public 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, CLIO.getOutStream());
+  }
+
+  public RunExampleTool(Executor executor, InputStream userInput, PrintStream stdout) {
+    super(stdout);
+    this.executor = (executor != null) ? executor : new DefaultExecutor();
+    this.userInput = userInput;
+  }
+
+  @Override
+  public String getName() {
+    return "run_example";
+  }
+
+  @Override
+  public List<Option> getOptions() {
+    return List.of(
+        Option.builder("noprompt")
+            .required(false)
+            .desc(
+                "Don't prompt for input; accept all defaults when running examples that accept user input.")
+            .build(),
+        Option.builder("e")
+            .argName("NAME")
+            .hasArg()
+            .required(true)
+            .desc("Name of the example to launch, one of: cloud, techproducts, schemaless, films.")
+            .longOpt("example")
+            .build(),
+        Option.builder("script")
+            .argName("PATH")
+            .hasArg()
+            .required(false)
+            .desc("Path to the bin/solr script.")
+            .build(),
+        Option.builder("d")
+            .argName("DIR")
+            .hasArg()
+            .required(true)
+            .desc("Path to the Solr server directory.")
+            .longOpt("serverDir")
+            .build(),
+        Option.builder("force")
+            .argName("FORCE")
+            .desc("Force option in case Solr is run as root.")
+            .build(),
+        Option.builder("exampleDir")
+            .argName("DIR")
+            .hasArg()
+            .required(false)
+            .desc(
+                "Path to the Solr example directory; if not provided, ${serverDir}/../example is expected to exist.")
+            .build(),
+        Option.builder("urlScheme")
+            .argName("SCHEME")
+            .hasArg()
+            .required(false)
+            .desc("Solr URL scheme: http or https, defaults to http if not specified.")
+            .build(),
+        Option.builder("p")
+            .argName("PORT")
+            .hasArg()
+            .required(false)
+            .desc("Specify the port to start the Solr HTTP listener on; default is 8983.")
+            .longOpt("port")
+            .build(),
+        Option.builder("h")
+            .argName("HOSTNAME")
+            .hasArg()
+            .required(false)
+            .desc("Specify the hostname for this Solr instance.")
+            .longOpt("host")
+            .build(),
+        Option.builder("z")
+            .argName("ZKHOST")
+            .hasArg()
+            .required(false)
+            .desc("ZooKeeper connection string; only used when running in SolrCloud mode using -c.")
+            .longOpt("zkhost")
+            .build(),
+        Option.builder("c")
+            .required(false)
+            .desc(
+                "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.")
+            .longOpt("cloud")
+            .build(),
+        Option.builder("m")
+            .argName("MEM")
+            .hasArg()
+            .required(false)
+            .desc(
+                "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.")
+            .longOpt("memory")
+            .build(),
+        Option.builder("a")
+            .argName("OPTS")
+            .hasArg()
+            .required(false)
+            .desc(
+                "Additional options to be passed to the JVM when starting example Solr server(s).")
+            .longOpt("addlopts")
+            .build());
+  }
+
+  @Override
+  public 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!");
+
+    echoIfVerbose(
+        "Running with\nserverDir="
+            + serverDir.getAbsolutePath()
+            + ",\nexampleDir="
+            + exampleDir.getAbsolutePath()
+            + "\nscript="
+            + script,
+        cli);
+
+    String exampleType = cli.getOptionValue("example");
+    if ("cloud".equals(exampleType)) {
+      runCloudExample(cli);
+    } else if ("techproducts".equals(exampleType)
+        || "schemaless".equals(exampleType)
+        || "films".equals(exampleType)) {
+      runExample(cli, exampleType);
+    } else {
+      throw new IllegalArgumentException(
+          "Unsupported example "
+              + exampleType
+              + "! Please choose one of: cloud, schemaless, techproducts, or films");
+    }
+  }
+
+  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" : "_default";
+
+    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) {
+      if (SolrCLI.safeCheckCollectionExists(solrUrl, collectionName)) {
+        alreadyExists = true;
+        echo(
+            "\nWARNING: Collection '"
+                + collectionName
+                + "' already exists!\nChecked collection existence using Collections API");
+      }
+    } else {
+      String coreName = collectionName;
+      if (SolrCLI.safeCheckCoreExists(solrUrl, coreName)) {
+        alreadyExists = true;
+        echo(
+            "\nWARNING: Core '"
+                + coreName
+                + "' already exists!\nChecked core existence using Core API command");
+      }
+    }
+
+    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(
+              SolrCLI.processCommandLineArgs(
+                  createTool.getName(), 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");
+      }
+    } else if ("films".equals(exampleName) && !alreadyExists) {
+      SolrClient solrClient = new Http2SolrClient.Builder(solrUrl).build();
+
+      echo("Adding dense vector field type to films schema \"_default\"");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/schema",
+            "{\n"
+                + "        \"add-field-type\" : {\n"
+                + "          \"name\":\"knn_vector_10\",\n"
+                + "          \"class\":\"solr.DenseVectorField\",\n"
+                + "          \"vectorDimension\":10,\n"
+                + "          \"similarityFunction\":cosine\n"
+                + "          \"knnAlgorithm\":hnsw\n"
+                + "        }\n"
+                + "      }");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      echo(
+          "Adding name, initial_release_date, and film_vector fields to films schema \"_default\"");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/schema",
+            "{\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"name\",\n"
+                + "          \"type\":\"text_general\",\n"
+                + "          \"multiValued\":false,\n"
+                + "          \"stored\":true\n"
+                + "        },\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"initial_release_date\",\n"
+                + "          \"type\":\"pdate\",\n"
+                + "          \"stored\":true\n"
+                + "        },\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"film_vector\",\n"
+                + "          \"type\":\"knn_vector_10\",\n"
+                + "          \"indexed\":true\n"
+                + "          \"stored\":true\n"
+                + "        }\n"
+                + "      }");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      echo("Adding paramsets \"algo\" and \"algo_b\" to films configuration for relevancy tuning");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/config/params",
+            "{\n"
+                + "        \"set\": {\n"
+                + "        \"algo_a\":{\n"
+                + "               \"defType\":\"dismax\",\n"
+                + "               \"qf\":\"name\"\n"
+                + "             }\n"
+                + "           },\n"
+                + "           \"set\": {\n"
+                + "             \"algo_b\":{\n"
+                + "               \"defType\":\"dismax\",\n"
+                + "               \"qf\":\"name\",\n"
+                + "               \"mm\":\"100%\"\n"
+                + "             }\n"
+                + "            }\n"
+                + "        }\n");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      File filmsJsonFile = new File(exampleDir, "films/films.json");
+      String updateUrl = String.format(Locale.ROOT, "%s/%s/update/json", solrUrl, collectionName);
+      echo("Indexing films example docs from " + filmsJsonFile.getAbsolutePath());
+      String currentPropVal = System.getProperty("url");
+      System.setProperty("url", updateUrl);
+      SimplePostTool.main(new String[] {filmsJsonFile.getAbsolutePath()});
+      if (currentPropVal != null) {
+        System.setProperty("url", currentPropVal); // reset
+      } else {
+        System.clearProperty("url");
+      }
+    }
+
+    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, StandardCharsets.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;
+        echoIfVerbose("Using port " + port + " for node " + (n + 1), cli);
+      }
+    } 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) {
+      @SuppressWarnings("unchecked")
+      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(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);
+
+    echo("\n\nSolrCloud example running, please visit: " + solrUrl + " \n");
+  }
+
+  protected void setCollectionConfigProperty(String solrUrl, String collectionName) {
+    ConfigTool configTool = new ConfigTool(stdout);
+    String[] configArgs =
+        new String[] {
+          "-collection",
+          collectionName,
+          "-property",
+          "updateHandler.autoSoftCommit.maxTime",
+          "-value",
+          "3000",
+          "-solrUrl",
+          solrUrl
+        };
+
+    // let's not fail if we get this far ... just report error and finish up
+    try {
+      configTool.runTool(
+          SolrCLI.processCommandLineArgs(
+              configTool.getName(), configTool.getOptions(), configArgs));
+    } catch (Exception exc) {
+      CLIO.err(
+          "Failed to update '"
+              + "updateHandler.autoSoftCommit.maxTime"
+              + "' property due to: "
+              + exc);
+    }
+  }
+
+  /** wait until the number of live nodes == numNodes. */
+  protected void waitToSeeLiveNodes(String zkHost, int numNodes) {
+    try (CloudSolrClient cloudClient =
+        new CloudSolrClient.Builder(Collections.singletonList(zkHost), Optional.empty()).build()) {
+      cloudClient.connect();
+      Set<String> liveNodes = cloudClient.getClusterState().getLiveNodes();
+      int numLiveNodes = (liveNodes != null) ? liveNodes.size() : 0;
+      long timeout = System.nanoTime() + TimeUnit.NANOSECONDS.convert(10, TimeUnit.SECONDS);
+      while (System.nanoTime() < timeout && numLiveNodes < numNodes) {
+        echo(
+            "\nWaiting up to "
+                + 10
+                + " seconds to see "
+                + (numNodes - numLiveNodes)
+                + " more nodes join the SolrCloud cluster ...");
+        try {
+          Thread.sleep(2000);
+        } catch (InterruptedException ie) {
+          Thread.interrupted();
+        }
+        liveNodes = cloudClient.getClusterState().getLiveNodes();
+        numLiveNodes = (liveNodes != null) ? liveNodes.size() : 0;
+      }
+      if (numLiveNodes < numNodes) {
+        echo(
+            "\nWARNING: Only "
+                + numLiveNodes
+                + " of "
+                + numNodes
+                + " are active in the cluster after "
+                + 10
+                + " seconds! Please check the solr.log for each node to look for errors.\n");
+      }
+    } catch (Exception exc) {
+      CLIO.err("Failed to see if " + numNodes + " joined the SolrCloud cluster due to: " + exc);
+    }
+  }
+
+  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 forceArg = cli.hasOption("force") ? " -force" : "";
+
+    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 %s",
+            callScript,
+            cloudModeArg,
+            port,
+            solrHome,
+            hostArg,
+            zkHostArg,
+            memArg,
+            forceArg,
+            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);
+    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 (Map.Entry<String, String> entry : procEnv.entrySet()) {
+          String envVar = entry.getKey();
+          String envVarVal = entry.getValue();
+          if (envVarVal != null && !"EXAMPLE".equals(envVar) && !envVar.startsWith("SOLR_")) {
+            startEnv.put(envVar, envVarVal);
+          }
+        }
+      }
+      DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
+      executor.execute(org.apache.commons.exec.CommandLine.parse(startCmd), startEnv, handler);
+
+      // wait for execution.
+      try {
+        handler.waitFor(3000);
+      } catch (InterruptedException ie) {
+        // safe to ignore ...
+        Thread.interrupted();
+      }
+      if (handler.hasResult() && handler.getExitValue() != 0) {
+        throw new Exception(
+            "Failed to start Solr using command: "
+                + startCmd
+                + " Exception : "
+                + handler.getException());
+      }
+    } else {
+      try {
+        code = executor.execute(org.apache.commons.exec.CommandLine.parse(startCmd));
+      } catch (ExecuteException e) {
+        throw new Exception(
+            "Failed to start Solr using command: " + startCmd + " Exception : " + e);
+      }
+    }
+    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) {
+    // 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);
+          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 = "_default";
+    String collectionName = "gettingstarted";
+
+    File configsetsDir = new File(serverDir, "solr/configsets");
+
+    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 collection or skip the
+        // creation step
+        if (SolrCLI.safeCheckCollectionExists(solrUrl, 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 = "_default 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 (SolrCLI.safeCheckCollectionExists(solrUrl, 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(
+            SolrCLI.processCommandLineArgs(
+                createCollectionTool.getName(), 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);
+    return configDir.isDirectory();
+  }
+
+  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, TimeUnit.SECONDS);
+    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);
+
+    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()) Files.copy(src.toPath(), dest.toPath());
+
+    if (!dest.isFile())
+      throw new IllegalStateException("Required file " + dest.getAbsolutePath() + " not found!");
+  }
+
+  protected boolean isPortAvailable(int port) {
+    try (Socket s = new Socket("localhost", port)) {

Review Comment:
   <b>*[UNENCRYPTED_SOCKET](https://find-sec-bugs.github.io/bugs.htm#UNENCRYPTED_SOCKET):</b>*  Unencrypted socket to org.apache.solr.util.cli.RunExampleTool (instead of SSLSocket)
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=493503696&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=493503696&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=493503696&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=493503696&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=493503696&lift_comment_rating=5) ]



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] HoustonPutman commented on pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "HoustonPutman (via GitHub)" <gi...@apache.org>.
HoustonPutman commented on PR #1568:
URL: https://github.com/apache/solr/pull/1568#issuecomment-1511507845

   > Question? Should we move `org.apache.solr.util.cli` up to `org.apache.solr.cli`? And related... Should we move `SolrCLI.java` into the `cli` package?
   
   Yes to both IMO


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] sonatype-lift[bot] commented on a diff in pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "sonatype-lift[bot] (via GitHub)" <gi...@apache.org>.
sonatype-lift[bot] commented on code in PR #1568:
URL: https://github.com/apache/solr/pull/1568#discussion_r1169275311


##########
solr/core/src/java/org/apache/solr/cli/SolrCLI.java:
##########
@@ -0,0 +1,652 @@
+/*
+ * 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.cli;
+
+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;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.File;
+import java.lang.invoke.MethodHandles;
+import java.net.ConnectException;
+import java.net.SocketException;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+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.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudHttp2SolrClient;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.request.CoreAdminRequest;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.common.SolrException;
+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.util.SolrVersion;
+import org.apache.solr.util.StartupLoggingUtils;
+import org.apache.solr.util.configuration.SSLConfigurationsFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Command-line utility for working with Solr. */
+public class SolrCLI implements CLIO {
+  private static final long MAX_WAIT_FOR_CORE_LOAD_NANOS =
+      TimeUnit.NANOSECONDS.convert(1, TimeUnit.MINUTES);
+
+  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";
+
+  public static final Option OPTION_ZKHOST =
+      Option.builder("z")
+          .argName("HOST")
+          .hasArg()
+          .required(false)
+          .desc("Address of the ZooKeeper ensemble; defaults to: " + ZK_HOST)
+          .longOpt("zkHost")
+          .build();
+  public static final Option OPTION_SOLRURL =
+      Option.builder("solrUrl")
+          .argName("HOST")
+          .hasArg()
+          .required(false)
+          .desc(
+              "Base Solr URL, which can be used to determine the zkHost if that's not known; defaults to: "
+                  + DEFAULT_SOLR_URL)
+          .build();
+  public static final Option OPTION_VERBOSE =
+      Option.builder("verbose").required(false).desc("Enable more verbose command output.").build();
+
+  public static final Option OPTION_RECURSE =
+      Option.builder("recurse")
+          .argName("recurse")
+          .hasArg()
+          .required(false)
+          .desc("Recurse (true|false), default is false.")
+          // .type(Boolean.class)
+          .build();
+
+  public static final List<Option> cloudOptions =
+      List.of(
+          OPTION_ZKHOST,
+          Option.builder("c")
+              .argName("COLLECTION")
+              .hasArg()
+              .required(false)
+              .desc("Name of collection; no default.")
+              .longOpt("collection")
+              .build(),
+          OPTION_VERBOSE);
+
+  public 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) {
+      CLIO.err(
+          "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
+      CLIO.out(SolrVersion.LATEST.toString());
+      exit(0);
+    }
+
+    SSLConfigurationsFactory.current().init();
+
+    Tool tool = null;
+    try {
+      tool = findTool(args);
+    } catch (IllegalArgumentException iae) {
+      CLIO.err(iae.getMessage());
+      System.exit(1);
+    }
+    CommandLine cli = parseCmdLine(tool.getName(), 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 toolName, String[] args, List<Option> toolOptions) {
+    // the parser doesn't like -D props
+    List<String> toolArgList = new ArrayList<>();
+    List<String> dashDList = new ArrayList<>();
+    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(toolName, toolOptions, toolArgs);
+
+    List<String> 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 {
+      CLIO.err(
+          "WARNING: "
+              + sysProp
+              + " file "
+              + keyStore
+              + " not found! https requests to Solr will likely fail; please update your "
+              + sysProp
+              + " setting to use an absolute path.");
+    }
+  }
+
+  public static void raiseLogLevelUnlessVerbose(CommandLine cli) {
+    if (!cli.hasOption(OPTION_VERBOSE.getOpt())) {
+      StartupLoggingUtils.changeLogLevel("WARN");
+    }
+  }
+
+  /** Support options common to all tools. */
+  public static List<Option> getCommonToolOptions() {
+    return List.of();
+  }
+
+  // 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 ("auth".equals(toolType)) return new AuthTool();
+    else if ("export".equals(toolType)) return new ExportTool();
+    else if ("package".equals(toolType)) return new PackageTool();
+
+    // If you add a built-in tool to this class, add it here to avoid
+    // classpath scanning
+
+    for (Class<? extends Tool> next : findToolClassesInPackage("org.apache.solr.util")) {
+      Tool tool = next.getConstructor().newInstance();
+      if (toolType.equals(tool.getName())) return tool;
+    }
+
+    throw new IllegalArgumentException(toolType + " is not a valid command!");
+  }
+
+  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()));
+    formatter.printHelp("export", getToolOptions(new ExportTool()));
+    formatter.printHelp("package", getToolOptions(new PackageTool()));
+
+    List<Class<? extends Tool>> toolClasses = findToolClassesInPackage("org.apache.solr.util");
+    for (Class<? extends Tool> next : toolClasses) {
+      Tool tool = next.getConstructor().newInstance();
+      formatter.printHelp(tool.getName(), getToolOptions(tool));
+    }
+  }
+
+  public static Options getToolOptions(Tool tool) {
+    Options options = new Options();
+    options.addOption("help", false, "Print this message");
+    options.addOption(OPTION_VERBOSE);
+    List<Option> toolOpts = tool.getOptions();
+    for (Option toolOpt : toolOpts) {
+      options.addOption(toolOpt);
+    }
+    return options;
+  }
+
+  public static List<Option> joinOptions(List<Option> lhs, List<Option> rhs) {
+    if (lhs == null) {
+      return rhs == null ? List.of() : rhs;
+    }
+
+    if (rhs == null) {
+      return lhs;
+    }
+
+    return Stream.concat(lhs.stream(), rhs.stream()).collect(Collectors.toUnmodifiableList());
+  }
+
+  /** Parses the command-line arguments passed by the user. */
+  public static CommandLine processCommandLineArgs(
+      String toolName, List<Option> customOptions, String[] args) {
+    Options options = new Options();
+
+    options.addOption("help", false, "Print this message");
+    options.addOption(OPTION_VERBOSE);
+
+    if (customOptions != null) {
+      for (Option customOption : customOptions) {
+        options.addOption(customOption);
+      }
+    }
+
+    CommandLine cli = null;
+    try {
+      cli = (new GnuParser()).parse(options, args);
+    } catch (ParseException exp) {
+      boolean hasHelpArg = false;
+      if (args != null) {
+        for (String arg : args) {
+          if ("--help".equals(arg) || "-help".equals(arg)) {
+            hasHelpArg = true;
+            break;
+          }
+        }
+      }
+      if (!hasHelpArg) {
+        CLIO.err("Failed to parse command-line arguments due to: " + exp.getMessage());
+      }
+      HelpFormatter formatter = new HelpFormatter();
+      formatter.printHelp(toolName, options);
+      exit(1);
+    }
+
+    if (cli.hasOption("help")) {
+      HelpFormatter formatter = new HelpFormatter();
+      formatter.printHelp(toolName, options);
+      exit(0);
+    }
+
+    return cli;
+  }
+
+  /** Scans Jar files on the classpath for Tool implementations to activate. */
+  private static List<Class<? extends Tool>> findToolClassesInPackage(String packageName) {
+    List<Class<? extends Tool>> toolClasses = new ArrayList<>();
+    try {
+      ClassLoader classLoader = SolrCLI.class.getClassLoader();
+      String path = packageName.replace('.', '/');
+      Enumeration<URL> resources = classLoader.getResources(path);
+      Set<String> classes = new TreeSet<>();
+      while (resources.hasMoreElements()) {
+        URL resource = resources.nextElement();
+        classes.addAll(findClasses(resource.getFile(), packageName));
+      }
+
+      for (String classInPackage : classes) {
+        Class<?> theClass = Class.forName(classInPackage);
+        if (Tool.class.isAssignableFrom(theClass)) toolClasses.add(theClass.asSubclass(Tool.class));
+      }
+    } 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 {}, due to: ", packageName, e);
+    }
+    return toolClasses;
+  }
+
+  private static Set<String> findClasses(String path, String packageName) throws Exception {
+    Set<String> classes = new TreeSet<>();
+    if (path.startsWith("file:") && path.contains("!")) {
+      String[] split = path.split("!");
+      URL jar = new URL(split[0]);
+      try (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;
+  }
+
+  /**
+   * 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 SolrServerException
+            || rootCause instanceof SocketException);
+    return wasCommError;
+  }
+
+  public static void checkCodeForAuthError(int code) {
+    if (code == UNAUTHORIZED.code || code == FORBIDDEN.code) {
+      throw new SolrException(
+          SolrException.ErrorCode.getErrorCode(code),
+          "Solr requires authentication for request. Please supply valid credentials. HTTP code="
+              + code);
+    }
+  }
+
+  public static boolean exceptionIsAuthRelated(Exception exc) {
+    return (exc instanceof SolrException
+        && Arrays.asList(UNAUTHORIZED.code, FORBIDDEN.code).contains(((SolrException) exc).code()));
+  }
+
+  public static SolrClient getSolrClient(String solrUrl) {
+    return new Http2SolrClient.Builder(solrUrl).maxConnectionsPerHost(32).build();
+  }
+
+  /**
+   * Get Solr base url with port if present and root from URI
+   *
+   * @param uri Full Solr URI (e.g. http://localhost:8983/solr/admin/info/system)
+   * @return Solr base url with port and root (from above example http://localhost:8983/solr)
+   */
+  public static String getSolrUrlFromUri(URI uri) {
+    return uri.getScheme() + "://" + uri.getAuthority() + "/" + uri.getPath().split("/")[1];
+  }
+
+  public static ModifiableSolrParams getSolrParamsFromUri(URI uri) {
+    ModifiableSolrParams paramsMap = new ModifiableSolrParams();
+    String[] params = uri.getQuery() == null ? new String[] {} : uri.getQuery().split("&");
+    for (String param : params) {
+      String[] paramSplit = param.split("=");
+      paramsMap.add(paramSplit[0], paramSplit[1]);
+    }
+    return paramsMap;
+  }
+
+  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);
+  }
+
+  public static final String DEFAULT_CONFIG_SET = "_default";
+
+  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;
+
+  @VisibleForTesting
+  public static final String uptime(long uptimeMs) {
+    if (uptimeMs <= 0L) return "?";
+
+    long numDays = (uptimeMs >= MS_IN_DAY) ? (uptimeMs / MS_IN_DAY) : 0L;
+    long rem = uptimeMs - (numDays * MS_IN_DAY);
+    long numHours = (rem >= MS_IN_HOUR) ? (rem / MS_IN_HOUR) : 0L;
+    rem = rem - (numHours * MS_IN_HOUR);
+    long numMinutes = (rem >= MS_IN_MIN) ? (rem / MS_IN_MIN) : 0L;
+    rem = rem - (numMinutes * MS_IN_MIN);
+    long numSeconds = Math.round(rem / 1000.0);
+    return String.format(
+        Locale.ROOT,
+        "%d days, %d hours, %d minutes, %d seconds",
+        numDays,
+        numHours,
+        numMinutes,
+        numSeconds);
+  }
+
+  public static final List<Option> CREATE_COLLECTION_OPTIONS =
+      List.of(
+          OPTION_ZKHOST,
+          OPTION_SOLRURL,
+          Option.builder(NAME)
+              .argName("NAME")
+              .hasArg()
+              .required(true)
+              .desc("Name of collection to create.")
+              .build(),
+          Option.builder("shards")
+              .argName("#")
+              .hasArg()
+              .required(false)
+              .desc("Number of shards; default is 1.")
+              .build(),
+          Option.builder("replicationFactor")
+              .argName("#")
+              .hasArg()
+              .required(false)
+              .desc(
+                  "Number of copies of each document across the collection (replicas per shard); default is 1.")
+              .build(),
+          Option.builder("confdir")
+              .argName("NAME")
+              .hasArg()
+              .required(false)
+              .desc(
+                  "Configuration directory to copy when creating the new collection; default is "
+                      + DEFAULT_CONFIG_SET
+                      + '.')
+              .build(),
+          Option.builder("confname")
+              .argName("NAME")
+              .hasArg()
+              .required(false)
+              .desc("Configuration name; default is the collection name.")
+              .build(),
+          Option.builder("configsetsDir")
+              .argName("DIR")
+              .hasArg()
+              .required(true)
+              .desc("Path to configsets directory on the local system.")
+              .build(),
+          OPTION_VERBOSE);
+
+  /**
+   * 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 CloudHttp2SolrClient.Builder(Collections.singletonList(zkHost), Optional.empty())
+              .build()) {
+        cloudSolrClient.connect();
+        Set<String> liveNodes = cloudSolrClient.getClusterState().getLiveNodes();
+        if (liveNodes.isEmpty())
+          throw new IllegalStateException(
+              "No live nodes found! Cannot determine 'solrUrl' from ZooKeeper: " + zkHost);
+
+        String firstLiveNode = liveNodes.iterator().next();
+        solrUrl = ZkStateReader.from(cloudSolrClient).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 += "/";
+
+    try (var solrClient = getSolrClient(solrUrl)) {
+      // hit Solr to get system info
+      NamedList<Object> systemInfo =
+          solrClient.request(
+              new GenericSolrRequest(SolrRequest.METHOD.GET, CommonParams.SYSTEM_INFO_PATH));
+
+      // convert raw JSON into user-friendly output
+      StatusTool statusTool = new StatusTool();
+      Map<String, Object> status = statusTool.reportStatus(systemInfo, solrClient);
+      @SuppressWarnings("unchecked")
+      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;
+      }
+    }
+
+    return zkHost;
+  }
+
+  public static boolean safeCheckCollectionExists(String solrUrl, String collection) {
+    boolean exists = false;
+    try (var solrClient = getSolrClient(solrUrl); ) {
+      NamedList<Object> existsCheckResult = solrClient.request(new CollectionAdminRequest.List());
+      @SuppressWarnings("unchecked")
+      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;
+  }
+
+  @SuppressWarnings("unchecked")
+  public static boolean safeCheckCoreExists(String solrUrl, String coreName) {
+    boolean exists = false;
+    try (var solrClient = getSolrClient(solrUrl)) {
+      boolean wait = false;
+      final long startWaitAt = System.nanoTime();
+      do {
+        if (wait) {
+          final int clamPeriodForStatusPollMs = 1000;
+          Thread.sleep(clamPeriodForStatusPollMs);
+        }
+        NamedList<Object> existsCheckResult =
+            CoreAdminRequest.getStatus(coreName, solrClient).getResponse();
+        NamedList<Object> status = (NamedList) existsCheckResult.get("status");
+        NamedList<Object> coreStatus = (NamedList) status.get(coreName);
+        Map<String, Object> failureStatus =
+            (Map<String, Object>) existsCheckResult.get("initFailures");
+        String errorMsg = (String) failureStatus.get(coreName);

Review Comment:
   <picture><img alt="16% of developers fix this issue" src="https://lift.sonatype.com/api/commentimage/fixrate/16/display.svg"></picture>
   
   <b>*NULL_DEREFERENCE:</b>*  object `failureStatus` last assigned on line 635 could be null and is dereferenced at line 636.
   
   ❗❗ <b>4 similar findings have been found in this PR</b>
   
   <details><summary>🔎 Expand here to view all instances of this finding</summary><br/>
     
     
   <div align=\"center\">
   
   
   | **File Path** | **Line Number** |
   | ------------- | ------------- |
   | solr/core/src/java/org/apache/solr/cli/SolrCLI.java | [633](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/SolrCLI.java#L633) |
   | solr/core/src/java/org/apache/solr/cli/AssertTool.java | [310](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/AssertTool.java#L310) |
   | solr/core/src/java/org/apache/solr/cli/AssertTool.java | [341](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/AssertTool.java#L341) |
   | solr/core/src/java/org/apache/solr/cli/AssertTool.java | [334](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/AssertTool.java#L334) |
   <p><a href="https://lift.sonatype.com/results/github.com/apache/solr/01GY8EKDFS5SKMPH3EB1NQ35RA?t=Infer|NULL_DEREFERENCE" target="_blank">Visit the Lift Web Console</a> to find more details in your report.</p></div></details>
   
   
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=494384305&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=494384305&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494384305&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494384305&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=494384305&lift_comment_rating=5) ]



##########
solr/core/src/java/org/apache/solr/cli/AssertTool.java:
##########
@@ -0,0 +1,395 @@
+/*
+ * 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.cli;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileOwnerAttributeView;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.HealthCheckRequest;
+import org.apache.solr.client.solrj.response.CollectionAdminResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.NamedList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Asserts various conditions and exists with error code if fails, else continues with no output */
+public class AssertTool extends ToolBase {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  private static String message = null;
+  private static boolean useExitCode = false;
+  private static Long timeoutMs = 1000L;
+
+  public AssertTool() {
+    this(CLIO.getOutStream());
+  }
+
+  public AssertTool(PrintStream stdout) {
+    super(stdout);
+  }
+
+  @Override
+  public String getName() {
+    return "assert";
+  }
+
+  @Override
+  public List<Option> getOptions() {
+    return List.of(
+        Option.builder("R")
+            .desc("Asserts that we are NOT the root user.")
+            .longOpt("not-root")
+            .build(),
+        Option.builder("r").desc("Asserts that we are the root user.").longOpt("root").build(),
+        Option.builder("S")
+            .desc("Asserts that Solr is NOT running on a certain URL. Default timeout is 1000ms.")
+            .longOpt("not-started")
+            .hasArg(true)
+            .argName("url")
+            .build(),
+        Option.builder("s")
+            .desc("Asserts that Solr is running on a certain URL. Default timeout is 1000ms.")
+            .longOpt("started")
+            .hasArg(true)
+            .argName("url")
+            .build(),
+        Option.builder("u")
+            .desc("Asserts that we run as same user that owns <directory>.")
+            .longOpt("same-user")
+            .hasArg(true)
+            .argName("directory")
+            .build(),
+        Option.builder("x")
+            .desc("Asserts that directory <directory> exists.")
+            .longOpt("exists")
+            .hasArg(true)
+            .argName("directory")
+            .build(),
+        Option.builder("X")
+            .desc("Asserts that directory <directory> does NOT exist.")
+            .longOpt("not-exists")
+            .hasArg(true)
+            .argName("directory")
+            .build(),
+        Option.builder("c")
+            .desc(
+                "Asserts that Solr is running in cloud mode.  Also fails if Solr not running.  URL should be for root Solr path.")
+            .longOpt("cloud")
+            .hasArg(true)
+            .argName("url")
+            .build(),
+        Option.builder("C")
+            .desc(
+                "Asserts that Solr is not running in cloud mode.  Also fails if Solr not running.  URL should be for root Solr path.")
+            .longOpt("not-cloud")
+            .hasArg(true)
+            .argName("url")
+            .build(),
+        Option.builder("m")
+            .desc("Exception message to be used in place of the default error message.")
+            .longOpt("message")
+            .hasArg(true)
+            .argName("message")
+            .build(),
+        Option.builder("t")
+            .desc("Timeout in ms for commands supporting a timeout.")
+            .longOpt("timeout")
+            .hasArg(true)
+            .type(Long.class)
+            .argName("ms")
+            .build(),
+        Option.builder("e")
+            .desc("Return an exit code instead of printing error message on assert fail.")
+            .longOpt("exitcode")
+            .build());
+  }
+
+  @Override
+  public int runTool(CommandLine cli) throws Exception {
+    verbose = cli.hasOption(SolrCLI.OPTION_VERBOSE.getOpt());
+
+    int toolExitStatus = 0;
+    try {
+      toolExitStatus = runAssert(cli);
+    } catch (Exception exc) {
+      // since this is a CLI, spare the user the stacktrace
+      String excMsg = exc.getMessage();
+      if (excMsg != null) {
+        if (verbose) {
+          CLIO.err("\nERROR: " + exc + "\n");
+        } else {
+          CLIO.err("\nERROR: " + excMsg + "\n");
+        }
+        toolExitStatus = 100; // Exit >= 100 means error, else means number of tests that failed
+      } else {
+        throw exc;
+      }
+    }
+    return toolExitStatus;
+  }
+
+  @Override
+  public 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>] [-c <url>] [-C <url>] [-u <dir>] [-x <dir>] [-X <dir>]",
+              SolrCLI.getToolOptions(this));
+      return 1;
+    }
+    if (cli.hasOption("m")) {
+      message = cli.getOptionValue("m");
+    }
+    if (cli.hasOption("t")) {
+      timeoutMs = 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"));
+    }
+    if (cli.hasOption("c")) {
+      ret += assertSolrRunningInCloudMode(cli.getOptionValue("c"));
+    }
+    if (cli.hasOption("C")) {
+      ret += assertSolrNotRunningInCloudMode(cli.getOptionValue("C"));
+    }
+    return ret;
+  }
+
+  public static int assertSolrRunning(String url) throws Exception {
+    StatusTool status = new StatusTool();
+    try {
+      status.waitToSeeSolrUp(url, timeoutMs, TimeUnit.MILLISECONDS);
+    } catch (Exception se) {
+      if (SolrCLI.exceptionIsAuthRelated(se)) {
+        throw se;
+      }
+      return exitOrException(
+          "Solr is not running on url "
+              + url
+              + " after "
+              + TimeUnit.SECONDS.convert(timeoutMs, TimeUnit.MILLISECONDS)
+              + " seconds");
+    }
+    return 0;
+  }
+
+  public static int assertSolrNotRunning(String url) throws Exception {
+    StatusTool status = new StatusTool();
+    long timeout =
+        System.nanoTime() + TimeUnit.NANOSECONDS.convert(timeoutMs, TimeUnit.MILLISECONDS);
+    try (SolrClient solrClient = SolrCLI.getSolrClient(url)) {
+      NamedList<Object> response = solrClient.request(new HealthCheckRequest());
+      Integer statusCode = (Integer) response.findRecursive("responseHeader", "status");
+      SolrCLI.checkCodeForAuthError(statusCode);
+    } catch (SolrException se) {
+      throw se;
+    } catch (IOException | SolrServerException e) {
+      log.debug("Opening connection to {} failed, Solr does not seem to be running", url, e);
+      return 0;
+    }
+    while (System.nanoTime() < timeout) {
+      try {
+        status.waitToSeeSolrUp(url, 1, TimeUnit.SECONDS);
+        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 (SolrCLI.exceptionIsAuthRelated(se)) {
+          throw se;
+        }
+        return exitOrException(se.getMessage());
+      }
+    }
+    return exitOrException(
+        "Solr is still running at "
+            + url
+            + " after "
+            + TimeUnit.SECONDS.convert(timeoutMs, TimeUnit.MILLISECONDS)
+            + " seconds");
+  }
+
+  public static int assertSolrRunningInCloudMode(String url) throws Exception {
+    if (!isSolrRunningOn(url)) {
+      return exitOrException(
+          "Solr is not running on url "
+              + url
+              + " after "
+              + TimeUnit.SECONDS.convert(timeoutMs, TimeUnit.MILLISECONDS)
+              + " seconds");
+    }
+
+    if (!runningSolrIsCloud(url)) {
+      return exitOrException("Solr is not running in cloud mode on " + url);
+    }
+    return 0;
+  }
+
+  public static int assertSolrNotRunningInCloudMode(String url) throws Exception {
+    if (!isSolrRunningOn(url)) {
+      return exitOrException(
+          "Solr is not running on url "
+              + url
+              + " after "
+              + TimeUnit.SECONDS.convert(timeoutMs, TimeUnit.MILLISECONDS)
+              + " seconds");
+    }
+
+    if (runningSolrIsCloud(url)) {
+      return exitOrException("Solr is not running in standalone mode on " + url);
+    }
+    return 0;
+  }
+
+  public static int sameUser(String directory) throws Exception {
+    if (Files.exists(Paths.get(directory))) {

Review Comment:
   <picture><img alt="8% of developers fix this issue" src="https://lift.sonatype.com/api/commentimage/fixrate/8/display.svg"></picture>
   
   <b>*[PATH_TRAVERSAL_IN](https://find-sec-bugs.github.io/bugs.htm#PATH_TRAVERSAL_IN):</b>*  This API (java/nio/file/Paths.get(Ljava/lang/String;[Ljava/lang/String;)Ljava/nio/file/Path;) reads a file whose location might be specified by user input
   
   ❗❗ <b>26 similar findings have been found in this PR</b>
   
   <details><summary>🔎 Expand here to view all instances of this finding</summary><br/>
     
     
   <div align=\"center\">
   
   
   | **File Path** | **Line Number** |
   | ------------- | ------------- |
   | solr/core/src/java/org/apache/solr/cli/AuthTool.java | [246](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/AuthTool.java#L246) |
   | solr/core/src/java/org/apache/solr/cli/CreateCoreTool.java | [91](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/CreateCoreTool.java#L91) |
   | solr/core/src/java/org/apache/solr/cli/AuthTool.java | [446](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/AuthTool.java#L446) |
   | solr/core/src/java/org/apache/solr/cli/RunExampleTool.java | [191](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java#L191) |
   | solr/core/src/java/org/apache/solr/cli/RunExampleTool.java | [876](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java#L876) |
   | solr/core/src/java/org/apache/solr/cli/RunExampleTool.java | [477](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java#L477) |
   | solr/core/src/java/org/apache/solr/cli/RunExampleTool.java | [507](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java#L507) |
   | solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java | [90](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java#L90) |
   | solr/core/src/java/org/apache/solr/cli/CreateCoreTool.java | [94](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/CreateCoreTool.java#L94) |
   | solr/core/src/java/org/apache/solr/cli/AuthTool.java | [409](https://github.com/apache/solr/blob/58a66dcb1519672b063c87cf2385aa993b7790e7/solr/core/src/java/org/apache/solr/cli/AuthTool.java#L409) |
   <p> Showing <b>10</b> of <b> 26 </b> findings. <a href="https://lift.sonatype.com/results/github.com/apache/solr/01GY8EKDFS5SKMPH3EB1NQ35RA?t=FindSecBugs|PATH_TRAVERSAL_IN" target="_blank">Visit the Lift Web Console</a> to see all.</p></div></details>
   
   
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=494381312&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=494381312&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494381312&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494381312&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=494381312&lift_comment_rating=5) ]



##########
solr/core/src/java/org/apache/solr/cli/ToolBase.java:
##########
@@ -0,0 +1,70 @@
+/*
+ * 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.cli;
+
+import java.io.PrintStream;
+import org.apache.commons.cli.CommandLine;
+
+public abstract class ToolBase implements Tool {
+
+  protected PrintStream stdout;
+  protected boolean verbose = false;
+
+  protected ToolBase() {
+    this(CLIO.getOutStream());
+  }
+
+  protected ToolBase(PrintStream stdout) {
+    this.stdout = stdout;
+  }
+
+  protected void echoIfVerbose(final String msg, CommandLine cli) {
+    if (cli.hasOption(SolrCLI.OPTION_VERBOSE.getOpt())) {
+      echo(msg);
+    }
+  }
+
+  protected void echo(final String msg) {
+    stdout.println(msg);
+  }
+
+  @Override
+  public int runTool(CommandLine cli) throws Exception {
+    verbose = cli.hasOption(SolrCLI.OPTION_VERBOSE.getOpt());
+
+    int toolExitStatus = 0;
+    try {
+      runImpl(cli);
+    } catch (Exception exc) {
+      // since this is a CLI, spare the user the stacktrace
+      String excMsg = exc.getMessage();
+      if (excMsg != null) {
+        CLIO.err("\nERROR: " + excMsg + "\n");
+        if (verbose) {
+          exc.printStackTrace(CLIO.getErrStream());

Review Comment:
   <picture><img alt="0% of developers fix this issue" src="https://lift.sonatype.com/api/commentimage/fixrate/0/display.svg"></picture>
   
   <b>*[INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE](https://find-sec-bugs.github.io/bugs.htm#INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE):</b>*  Possible information exposure through an error message
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=494381489&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=494381489&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494381489&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494381489&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=494381489&lift_comment_rating=5) ]



##########
solr/core/src/java/org/apache/solr/cli/RunExampleTool.java:
##########
@@ -0,0 +1,1029 @@
+/*
+ * 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.cli;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+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.concurrent.TimeUnit;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.exec.DefaultExecuteResultHandler;
+import org.apache.commons.exec.DefaultExecutor;
+import org.apache.commons.exec.ExecuteException;
+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.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.common.SolrException;
+import org.noggit.CharArr;
+import org.noggit.JSONWriter;
+
+/** Supports an interactive session with the user to launch (or relaunch the -e cloud example) */
+public 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, CLIO.getOutStream());
+  }
+
+  public RunExampleTool(Executor executor, InputStream userInput, PrintStream stdout) {
+    super(stdout);
+    this.executor = (executor != null) ? executor : new DefaultExecutor();
+    this.userInput = userInput;
+  }
+
+  @Override
+  public String getName() {
+    return "run_example";
+  }
+
+  @Override
+  public List<Option> getOptions() {
+    return List.of(
+        Option.builder("noprompt")
+            .required(false)
+            .desc(
+                "Don't prompt for input; accept all defaults when running examples that accept user input.")
+            .build(),
+        Option.builder("e")
+            .argName("NAME")
+            .hasArg()
+            .required(true)
+            .desc("Name of the example to launch, one of: cloud, techproducts, schemaless, films.")
+            .longOpt("example")
+            .build(),
+        Option.builder("script")
+            .argName("PATH")
+            .hasArg()
+            .required(false)
+            .desc("Path to the bin/solr script.")
+            .build(),
+        Option.builder("d")
+            .argName("DIR")
+            .hasArg()
+            .required(true)
+            .desc("Path to the Solr server directory.")
+            .longOpt("serverDir")
+            .build(),
+        Option.builder("force")
+            .argName("FORCE")
+            .desc("Force option in case Solr is run as root.")
+            .build(),
+        Option.builder("exampleDir")
+            .argName("DIR")
+            .hasArg()
+            .required(false)
+            .desc(
+                "Path to the Solr example directory; if not provided, ${serverDir}/../example is expected to exist.")
+            .build(),
+        Option.builder("urlScheme")
+            .argName("SCHEME")
+            .hasArg()
+            .required(false)
+            .desc("Solr URL scheme: http or https, defaults to http if not specified.")
+            .build(),
+        Option.builder("p")
+            .argName("PORT")
+            .hasArg()
+            .required(false)
+            .desc("Specify the port to start the Solr HTTP listener on; default is 8983.")
+            .longOpt("port")
+            .build(),
+        Option.builder("h")
+            .argName("HOSTNAME")
+            .hasArg()
+            .required(false)
+            .desc("Specify the hostname for this Solr instance.")
+            .longOpt("host")
+            .build(),
+        Option.builder("z")
+            .argName("ZKHOST")
+            .hasArg()
+            .required(false)
+            .desc("ZooKeeper connection string; only used when running in SolrCloud mode using -c.")
+            .longOpt("zkhost")
+            .build(),
+        Option.builder("c")
+            .required(false)
+            .desc(
+                "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.")
+            .longOpt("cloud")
+            .build(),
+        Option.builder("m")
+            .argName("MEM")
+            .hasArg()
+            .required(false)
+            .desc(
+                "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.")
+            .longOpt("memory")
+            .build(),
+        Option.builder("a")
+            .argName("OPTS")
+            .hasArg()
+            .required(false)
+            .desc(
+                "Additional options to be passed to the JVM when starting example Solr server(s).")
+            .longOpt("addlopts")
+            .build());
+  }
+
+  @Override
+  public 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!");
+
+    echoIfVerbose(
+        "Running with\nserverDir="
+            + serverDir.getAbsolutePath()
+            + ",\nexampleDir="
+            + exampleDir.getAbsolutePath()
+            + "\nscript="
+            + script,
+        cli);
+
+    String exampleType = cli.getOptionValue("example");
+    if ("cloud".equals(exampleType)) {
+      runCloudExample(cli);
+    } else if ("techproducts".equals(exampleType)
+        || "schemaless".equals(exampleType)
+        || "films".equals(exampleType)) {
+      runExample(cli, exampleType);
+    } else {
+      throw new IllegalArgumentException(
+          "Unsupported example "
+              + exampleType
+              + "! Please choose one of: cloud, schemaless, techproducts, or films");
+    }
+  }
+
+  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" : "_default";
+
+    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) {
+      if (SolrCLI.safeCheckCollectionExists(solrUrl, collectionName)) {
+        alreadyExists = true;
+        echo(
+            "\nWARNING: Collection '"
+                + collectionName
+                + "' already exists!\nChecked collection existence using Collections API");
+      }
+    } else {
+      String coreName = collectionName;
+      if (SolrCLI.safeCheckCoreExists(solrUrl, coreName)) {
+        alreadyExists = true;
+        echo(
+            "\nWARNING: Core '"
+                + coreName
+                + "' already exists!\nChecked core existence using Core API command");
+      }
+    }
+
+    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(
+              SolrCLI.processCommandLineArgs(
+                  createTool.getName(), 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");
+      }
+    } else if ("films".equals(exampleName) && !alreadyExists) {
+      SolrClient solrClient = new Http2SolrClient.Builder(solrUrl).build();
+
+      echo("Adding dense vector field type to films schema \"_default\"");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/schema",
+            "{\n"
+                + "        \"add-field-type\" : {\n"
+                + "          \"name\":\"knn_vector_10\",\n"
+                + "          \"class\":\"solr.DenseVectorField\",\n"
+                + "          \"vectorDimension\":10,\n"
+                + "          \"similarityFunction\":cosine\n"
+                + "          \"knnAlgorithm\":hnsw\n"
+                + "        }\n"
+                + "      }");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      echo(
+          "Adding name, initial_release_date, and film_vector fields to films schema \"_default\"");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/schema",
+            "{\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"name\",\n"
+                + "          \"type\":\"text_general\",\n"
+                + "          \"multiValued\":false,\n"
+                + "          \"stored\":true\n"
+                + "        },\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"initial_release_date\",\n"
+                + "          \"type\":\"pdate\",\n"
+                + "          \"stored\":true\n"
+                + "        },\n"
+                + "        \"add-field\" : {\n"
+                + "          \"name\":\"film_vector\",\n"
+                + "          \"type\":\"knn_vector_10\",\n"
+                + "          \"indexed\":true\n"
+                + "          \"stored\":true\n"
+                + "        }\n"
+                + "      }");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      echo("Adding paramsets \"algo\" and \"algo_b\" to films configuration for relevancy tuning");
+      try {
+        SolrCLI.postJsonToSolr(
+            solrClient,
+            "/" + collectionName + "/config/params",
+            "{\n"
+                + "        \"set\": {\n"
+                + "        \"algo_a\":{\n"
+                + "               \"defType\":\"dismax\",\n"
+                + "               \"qf\":\"name\"\n"
+                + "             }\n"
+                + "           },\n"
+                + "           \"set\": {\n"
+                + "             \"algo_b\":{\n"
+                + "               \"defType\":\"dismax\",\n"
+                + "               \"qf\":\"name\",\n"
+                + "               \"mm\":\"100%\"\n"
+                + "             }\n"
+                + "            }\n"
+                + "        }\n");
+      } catch (Exception ex) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, ex);
+      }
+
+      File filmsJsonFile = new File(exampleDir, "films/films.json");
+      String updateUrl = String.format(Locale.ROOT, "%s/%s/update/json", solrUrl, collectionName);
+      echo("Indexing films example docs from " + filmsJsonFile.getAbsolutePath());
+      String currentPropVal = System.getProperty("url");
+      System.setProperty("url", updateUrl);
+      SimplePostTool.main(new String[] {filmsJsonFile.getAbsolutePath()});
+      if (currentPropVal != null) {
+        System.setProperty("url", currentPropVal); // reset
+      } else {
+        System.clearProperty("url");
+      }
+    }
+
+    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, StandardCharsets.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;
+        echoIfVerbose("Using port " + port + " for node " + (n + 1), cli);
+      }
+    } 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) {
+      @SuppressWarnings("unchecked")
+      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(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);
+
+    echo("\n\nSolrCloud example running, please visit: " + solrUrl + " \n");
+  }
+
+  protected void setCollectionConfigProperty(String solrUrl, String collectionName) {
+    ConfigTool configTool = new ConfigTool(stdout);
+    String[] configArgs =
+        new String[] {
+          "-collection",
+          collectionName,
+          "-property",
+          "updateHandler.autoSoftCommit.maxTime",
+          "-value",
+          "3000",
+          "-solrUrl",
+          solrUrl
+        };
+
+    // let's not fail if we get this far ... just report error and finish up
+    try {
+      configTool.runTool(
+          SolrCLI.processCommandLineArgs(
+              configTool.getName(), configTool.getOptions(), configArgs));
+    } catch (Exception exc) {
+      CLIO.err(
+          "Failed to update '"
+              + "updateHandler.autoSoftCommit.maxTime"
+              + "' property due to: "
+              + exc);
+    }
+  }
+
+  /** wait until the number of live nodes == numNodes. */
+  protected void waitToSeeLiveNodes(String zkHost, int numNodes) {
+    try (CloudSolrClient cloudClient =
+        new CloudSolrClient.Builder(Collections.singletonList(zkHost), Optional.empty()).build()) {
+      cloudClient.connect();
+      Set<String> liveNodes = cloudClient.getClusterState().getLiveNodes();
+      int numLiveNodes = (liveNodes != null) ? liveNodes.size() : 0;
+      long timeout = System.nanoTime() + TimeUnit.NANOSECONDS.convert(10, TimeUnit.SECONDS);
+      while (System.nanoTime() < timeout && numLiveNodes < numNodes) {
+        echo(
+            "\nWaiting up to "
+                + 10
+                + " seconds to see "
+                + (numNodes - numLiveNodes)
+                + " more nodes join the SolrCloud cluster ...");
+        try {
+          Thread.sleep(2000);
+        } catch (InterruptedException ie) {
+          Thread.interrupted();
+        }
+        liveNodes = cloudClient.getClusterState().getLiveNodes();
+        numLiveNodes = (liveNodes != null) ? liveNodes.size() : 0;
+      }
+      if (numLiveNodes < numNodes) {
+        echo(
+            "\nWARNING: Only "
+                + numLiveNodes
+                + " of "
+                + numNodes
+                + " are active in the cluster after "
+                + 10
+                + " seconds! Please check the solr.log for each node to look for errors.\n");
+      }
+    } catch (Exception exc) {
+      CLIO.err("Failed to see if " + numNodes + " joined the SolrCloud cluster due to: " + exc);
+    }
+  }
+
+  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 forceArg = cli.hasOption("force") ? " -force" : "";
+
+    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 %s",
+            callScript,
+            cloudModeArg,
+            port,
+            solrHome,
+            hostArg,
+            zkHostArg,
+            memArg,
+            forceArg,
+            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);
+    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 (Map.Entry<String, String> entry : procEnv.entrySet()) {
+          String envVar = entry.getKey();
+          String envVarVal = entry.getValue();
+          if (envVarVal != null && !"EXAMPLE".equals(envVar) && !envVar.startsWith("SOLR_")) {
+            startEnv.put(envVar, envVarVal);
+          }
+        }
+      }
+      DefaultExecuteResultHandler handler = new DefaultExecuteResultHandler();
+      executor.execute(org.apache.commons.exec.CommandLine.parse(startCmd), startEnv, handler);
+
+      // wait for execution.
+      try {
+        handler.waitFor(3000);
+      } catch (InterruptedException ie) {
+        // safe to ignore ...
+        Thread.interrupted();
+      }
+      if (handler.hasResult() && handler.getExitValue() != 0) {
+        throw new Exception(
+            "Failed to start Solr using command: "
+                + startCmd
+                + " Exception : "
+                + handler.getException());
+      }
+    } else {
+      try {
+        code = executor.execute(org.apache.commons.exec.CommandLine.parse(startCmd));
+      } catch (ExecuteException e) {
+        throw new Exception(
+            "Failed to start Solr using command: " + startCmd + " Exception : " + e);
+      }
+    }
+    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) {
+    // 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);
+          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 = "_default";
+    String collectionName = "gettingstarted";
+
+    File configsetsDir = new File(serverDir, "solr/configsets");
+
+    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 collection or skip the
+        // creation step
+        if (SolrCLI.safeCheckCollectionExists(solrUrl, 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 = "_default 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 (SolrCLI.safeCheckCollectionExists(solrUrl, 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(
+            SolrCLI.processCommandLineArgs(
+                createCollectionTool.getName(), 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);
+    return configDir.isDirectory();
+  }
+
+  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, TimeUnit.SECONDS);
+    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);
+
+    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()) Files.copy(src.toPath(), dest.toPath());
+
+    if (!dest.isFile())
+      throw new IllegalStateException("Required file " + dest.getAbsolutePath() + " not found!");
+  }
+
+  protected boolean isPortAvailable(int port) {
+    try (Socket s = new Socket("localhost", port)) {

Review Comment:
   <b>*[UNENCRYPTED_SOCKET](https://find-sec-bugs.github.io/bugs.htm#UNENCRYPTED_SOCKET):</b>*  Unencrypted socket to org.apache.solr.cli.RunExampleTool (instead of SSLSocket)
   
   ---
   
   <details><summary>ℹī¸ Expand to see all <b>@sonatype-lift</b> commands</summary>
   
   You can reply with the following commands. For example, reply with ***@sonatype-lift ignoreall*** to leave out all findings.
   | **Command** | **Usage** |
   | ------------- | ------------- |
   | `@sonatype-lift ignore` | Leave out the above finding from this PR |
   | `@sonatype-lift ignoreall` | Leave out all the existing findings from this PR |
   | `@sonatype-lift exclude <file\|issue\|path\|tool>` | Exclude specified `file\|issue\|path\|tool` from Lift findings by updating your config.toml file |
   
   **Note:** When talking to LiftBot, you need to **refresh** the page to see its response.
   <sub>[Click here](https://github.com/apps/sonatype-lift/installations/new) to add LiftBot to another repo.</sub></details>
   
   
   
   ---
   
   <b>Help us improve LIFT! (<i>Sonatype LiftBot external survey</i>)</b>
   
   Was this a good recommendation for you? <sub><small>Answering this survey will not impact your Lift settings.</small></sub>
   
   [ [🙁 Not relevant](https://www.sonatype.com/lift-comment-rating?comment=494381525&lift_comment_rating=1) ] - [ [😕 Won't fix](https://www.sonatype.com/lift-comment-rating?comment=494381525&lift_comment_rating=2) ] - [ [😑 Not critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494381525&lift_comment_rating=3) ] - [ [🙂 Critical, will fix](https://www.sonatype.com/lift-comment-rating?comment=494381525&lift_comment_rating=4) ] - [ [😊 Critical, fixing now](https://www.sonatype.com/lift-comment-rating?comment=494381525&lift_comment_rating=5) ]



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org


[GitHub] [solr] janhoy commented on pull request #1568: SOLR-16711: Extract SolrCLI tool implementations into their own package and classes (Take 2)

Posted by "janhoy (via GitHub)" <gi...@apache.org>.
janhoy commented on PR #1568:
URL: https://github.com/apache/solr/pull/1568#issuecomment-1514869614

   Yea, and the script changes is basically java package name change. Looks good.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@solr.apache.org
For additional commands, e-mail: issues-help@solr.apache.org