This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_10x in repository https://gitbox.apache.org/repos/asf/solr.git
commit 3b7767216182636ca91d08530a15bad6cc0686e2 Author: Serhiy Bzhezytskyy <[email protected]> AuthorDate: Thu Oct 8 19:36:18 2026 +0300 SOLR-18517: Port RunExampleTool to picocli (#5050) (cherry picked from commit d94c5eeb6b59d817e84b1fad300a7fb5c8b9c08a) --- .../SOLR-17697-picocli-experimental-cli.yml | 2 + .../java/org/apache/solr/cli/RunExampleTool.java | 280 ++++++++++++++++++++- .../core/src/java/org/apache/solr/cli/SolrCLI.java | 3 +- .../solr/cli/SolrCLIRunExamplePicocliTest.java | 104 ++++++++ .../org/apache/solr/cli/TestSolrCLIRunExample.java | 15 +- 5 files changed, 397 insertions(+), 7 deletions(-) diff --git a/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml b/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml index 5037bd6b2df..0603e293c11 100644 --- a/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml +++ b/changelog/unreleased/SOLR-17697-picocli-experimental-cli.yml @@ -29,3 +29,5 @@ links: url: https://issues.apache.org/jira/browse/SOLR-18519 - name: SOLR-18518 url: https://issues.apache.org/jira/browse/SOLR-18518 + - name: SOLR-18517 + url: https://issues.apache.org/jira/browse/SOLR-18517 diff --git a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java index e109022e59a..9dddc9938d3 100644 --- a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java +++ b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java @@ -27,8 +27,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Scanner; @@ -56,6 +58,21 @@ import org.noggit.JSONWriter; * <p>Enhances start command by providing an interactive session with the user to launch (or * relaunch the -e cloud example) */ +@SuppressWarnings("UnnecessarilyFullyQualified") [email protected]( + name = "run_example", + hidden = true, + description = + "Enhances the start command by providing an interactive session with the user to launch" + + " (or relaunch) one of the bundled examples.", + footerHeading = "%nExamples:%n", + footer = { + " # Launch the interactive SolrCloud example", + " bin/solr start -e cloud", + "", + " # Launch the techproducts example, accepting all prompt defaults", + " bin/solr start -e techproducts --no-prompt" + }) public class RunExampleTool extends ToolBase { private static final String PROMPT_FOR_NUMBER = "Please enter %s [%d]: "; @@ -66,6 +83,11 @@ public class RunExampleTool extends ToolBase { private static final String PROMPT_NUMBER_TOO_LARGE = "%d is too large! " + PROMPT_FOR_NUMBER_IN_RANGE; + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option NO_PROMPT_OPTION = Option.builder("y") .longOpt("no-prompt") @@ -73,6 +95,11 @@ public class RunExampleTool extends ToolBase { "Don't prompt for input; accept all defaults when running examples that accept user input.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option SCRIPT_INPUTS_OPTION = Option.builder() .longOpt("script-inputs") @@ -83,6 +110,11 @@ public class RunExampleTool extends ToolBase { + "Example: --script-inputs 3,8983,8984,8985,\"gettingstarted\",2,2,_default") .build(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option EXAMPLE_OPTION = Option.builder("e") .longOpt("example") @@ -92,6 +124,11 @@ public class RunExampleTool extends ToolBase { .desc("Name of the example to launch, one of: cloud, techproducts, schemaless, films.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option SCRIPT_OPTION = Option.builder() .longOpt("script") @@ -100,6 +137,11 @@ public class RunExampleTool extends ToolBase { .desc("Path to the bin/solr script.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option SERVER_DIR_OPTION = Option.builder("d") .longOpt("server-dir") @@ -109,6 +151,11 @@ public class RunExampleTool extends ToolBase { .desc("Path to the Solr server directory.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option FORCE_OPTION = Option.builder("f") .longOpt("force") @@ -116,6 +163,11 @@ public class RunExampleTool extends ToolBase { .desc("Force option in case Solr is run as root.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option EXAMPLE_DIR_OPTION = Option.builder() .longOpt("example-dir") @@ -125,6 +177,11 @@ public class RunExampleTool extends ToolBase { "Path to the Solr example directory; if not provided, ${serverDir}/../example is expected to exist.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option SOLR_HOME_OPTION = Option.builder() .longOpt("solr-home") @@ -135,6 +192,11 @@ public class RunExampleTool extends ToolBase { "Path to the Solr home directory; if not provided, ${serverDir}/solr is expected to exist.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option URL_SCHEME_OPTION = Option.builder() .longOpt("url-scheme") @@ -143,6 +205,11 @@ public class RunExampleTool extends ToolBase { .desc("Solr URL scheme: http or https, defaults to http if not specified.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option PORT_OPTION = Option.builder("p") .longOpt("port") @@ -151,6 +218,11 @@ public class RunExampleTool extends ToolBase { .desc("Specify the port to start the Solr HTTP listener on; default is 8983.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option HOST_OPTION = Option.builder() .longOpt("host") @@ -159,9 +231,19 @@ public class RunExampleTool extends ToolBase { .desc("Specify the hostname for this Solr instance.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option USER_MANAGED_OPTION = Option.builder().longOpt("user-managed").desc("Start Solr in User Managed mode.").get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option MEMORY_OPTION = Option.builder("m") .longOpt("memory") @@ -171,6 +253,11 @@ public class RunExampleTool extends ToolBase { "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.") .get(); + /** + * @deprecated Only used by the commons-cli parser; the picocli path declares this as an annotated + * field. + */ + @Deprecated private static final Option JVM_OPTS_OPTION = Option.builder() .longOpt("jvm-opts") @@ -222,6 +309,131 @@ public class RunExampleTool extends ToolBase { record CloudExampleParams( boolean noPrompt, String scriptInputs, String zkHost, int basePort, StartSolrParams start) {} + // --- picocli fields --- + + @picocli.CommandLine.Option( + names = {"-y", "--no-prompt"}, + description = + "Don't prompt for input; accept all defaults when running examples that accept user" + + " input.") + private boolean noPromptOpt; + + @picocli.CommandLine.Option( + names = "--script-inputs", + paramLabel = "VALUES", + description = + "Provide comma-separated values for prompts. Same as --no-prompt but uses provided" + + " values instead of defaults. Example: --script-inputs" + + " 3,8983,8984,8985,\"gettingstarted\",2,2,_default") + private String scriptInputsOpt; + + @picocli.CommandLine.Option( + names = {"-e", "--example"}, + required = true, + paramLabel = "NAME", + description = + "Name of the example to launch, one of: cloud, techproducts, schemaless, films.") + private String exampleOpt; + + @picocli.CommandLine.Option( + names = "--script", + paramLabel = "PATH", + description = "Path to the bin/solr script.") + private String scriptOpt; + + @picocli.CommandLine.Option( + names = {"-d", "--server-dir"}, + required = true, + paramLabel = "DIR", + description = "Path to the Solr server directory.") + private String serverDirOpt; + + @picocli.CommandLine.Option( + names = {"-f", "--force"}, + description = "Force option in case Solr is run as root.") + private boolean forceOpt; + + @picocli.CommandLine.Option( + names = "--example-dir", + paramLabel = "DIR", + description = + "Path to the Solr example directory; if not provided, ${serverDir}/../example is" + + " expected to exist.") + private String exampleDirOpt; + + @picocli.CommandLine.Option( + names = "--solr-home", + paramLabel = "SOLR_HOME_DIR", + description = + "Path to the Solr home directory; if not provided, ${serverDir}/solr is expected to" + + " exist.") + private String solrHomeOpt; + + @picocli.CommandLine.Option( + names = "--url-scheme", + defaultValue = "http", + paramLabel = "SCHEME", + description = "Solr URL scheme: http or https, defaults to http if not specified.") + private String urlSchemeOpt; + + // No explicit paramLabel: the default "<port>" is what CliDefaultValueProvider keys on, giving + // the solr.port.listen property / SOLR_PORT_LISTEN, else 8983, as under commons-cli. + @picocli.CommandLine.Option( + names = {"-p", "--port"}, + description = "Specify the port to start the Solr HTTP listener on; default is 8983.") + private int port; + + @picocli.CommandLine.Option( + names = "--host", + paramLabel = "HOSTNAME", + description = "Specify the hostname for this Solr instance.") + private String hostOpt; + + @picocli.CommandLine.Option( + names = "--user-managed", + description = "Start Solr in User Managed mode.") + private boolean userManagedOpt; + + @picocli.CommandLine.Option( + names = {"-m", "--memory"}, + paramLabel = "MEM", + description = + "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.") + private String memoryOpt; + + @picocli.CommandLine.Option( + names = "--jvm-opts", + paramLabel = "OPTS", + description = + "Additional options to be passed to the JVM when starting example Solr server(s).") + private String jvmOptsOpt; + + // Likewise "<zkHost>": the zkHost property / ZK_HOST, else null. + @picocli.CommandLine.Option( + names = {"-z", "--zk-host"}, + description = "Zookeeper connection string.") + private String zkHost; + + @picocli.CommandLine.Parameters( + arity = "0..*", + paramLabel = "ARG", + description = "Extra arguments passed through to the underlying bin/solr start command.") + private String[] extraArgsOpt = new String[0]; + + /** + * Options picocli does not know. {@code bin/solr start -e <example> -Dfoo=bar} forwards the + * {@code -D} system properties here; they are kept as extra arguments, as {@code + * SolrCLI.parseCmdLine} does for commons-cli, and anything else is an unknown option. + */ + @picocli.CommandLine.Unmatched private List<String> unmatchedOpts = new ArrayList<>(); + + @picocli.CommandLine.Spec private picocli.CommandLine.Model.CommandSpec spec; + + public RunExampleTool() { + this(new DefaultToolRuntime()); + } + /** Default constructor used by the framework when running as a command-line application. */ public RunExampleTool(ToolRuntime runtime) { this(null, System.in, runtime); @@ -1139,9 +1351,75 @@ public class RunExampleTool extends ToolBase { throw new IllegalStateException("Required file " + dest.toAbsolutePath() + " not found!"); } + /** The unknown options that are not {@code -D} system properties; picocli would reject these. */ + List<String> unknownOptions() { + return unmatchedOpts.stream().filter(opt -> !opt.startsWith("-D")).toList(); + } + + /** + * The positional arguments followed by the {@code -D} options, the order commons-cli gives them. + */ + String[] picocliExtraArgs() { + List<String> extra = new ArrayList<>(Arrays.asList(extraArgsOpt)); + unmatchedOpts.stream().filter(opt -> opt.startsWith("-D")).forEach(extra::add); + return extra.toArray(new String[0]); + } + @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + List<String> unknown = unknownOptions(); + if (!unknown.isEmpty()) { + // What picocli does for an unknown option it parses itself; thrown from here it would be a + // stack trace and exit code 1 instead. + java.io.PrintWriter err = spec.commandLine().getErr(); + err.println("Unknown option: '" + unknown.getFirst() + "'"); + spec.commandLine().usage(err); + err.flush(); + return picocli.CommandLine.ExitCode.USAGE; + } + String[] extraArgs = picocliExtraArgs(); + if (noPromptOpt && scriptInputsOpt != null) { + throw new IllegalArgumentException( + "Cannot use both --no-prompt and --script-inputs options together. " + + "Use --no-prompt to accept defaults, or --script-inputs to provide specific values."); + } + + this.urlScheme = urlSchemeOpt; + String exampleType = exampleOpt; + + initDirs(serverDirOpt, scriptOpt, exampleDirOpt, solrHomeOpt, exampleType); + + echoIfVerbose( + "Running with\nserverDir=" + + serverDir.toAbsolutePath() + + ",\nexampleDir=" + + exampleDir.toAbsolutePath() + + ",\nsolrHomeDir=" + + solrHomeDir.toAbsolutePath() + + "\nscript=" + + script); + + if (!"cloud".equals(exampleType) + && !"techproducts".equals(exampleType) + && !"schemaless".equals(exampleType) + && !"films".equals(exampleType)) { + throw new IllegalArgumentException( + "Unsupported example " + + exampleType + + "! Please choose one of: cloud, schemaless, techproducts, or films"); + } + + StartSolrParams startParams = + new StartSolrParams( + exampleType, hostOpt, memoryOpt, jvmOptsOpt, forceOpt, null, readExtraArgs(extraArgs)); + + if ("cloud".equals(exampleType)) { + runCloudExample( + new CloudExampleParams(noPromptOpt, scriptInputsOpt, zkHost, port, startParams)); + } else { + runExample(new RunExampleParams(!userManagedOpt, zkHost, port, startParams)); + } + return 0; } protected boolean isPortAvailable(int port) { diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index 3ebcd938f88..dc192a2e18a 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -96,7 +96,8 @@ import org.slf4j.LoggerFactory; SnapshotDeleteShim.class, SnapshotDescribeShim.class, SnapshotExportShim.class, - SnapshotListShim.class + SnapshotListShim.class, + RunExampleTool.class }) public class SolrCLI implements CLIO, java.util.concurrent.Callable<Integer> { diff --git a/solr/core/src/test/org/apache/solr/cli/SolrCLIRunExamplePicocliTest.java b/solr/core/src/test/org/apache/solr/cli/SolrCLIRunExamplePicocliTest.java new file mode 100644 index 00000000000..be338f64cba --- /dev/null +++ b/solr/core/src/test/org/apache/solr/cli/SolrCLIRunExamplePicocliTest.java @@ -0,0 +1,104 @@ +/* + * 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.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import org.junit.Test; +import picocli.CommandLine; + +/** Runs all {@link TestSolrCLIRunExample} tests through the picocli invocation path. */ +public class SolrCLIRunExamplePicocliTest extends TestSolrCLIRunExample { + + @Override + protected int runTool(RunExampleTool tool, String[] args) throws Exception { + return new CommandLine(tool) + .setDefaultValueProvider(new CliDefaultValueProvider()) + .execute(args); + } + + private static Object parsedValue(CommandLine cmd, String option) { + return cmd.getCommandSpec().findOption(option).getValue(); + } + + private CommandLine parse(String... extra) { + CommandLine cmd = + new CommandLine(new RunExampleTool(new CLITestHelper.TestingRuntime(false))) + .setDefaultValueProvider(new CliDefaultValueProvider()); + String[] base = {"-e", "techproducts", "--server-dir", "/tmp/server"}; + String[] all = new String[base.length + extra.length]; + System.arraycopy(base, 0, all, 0, base.length); + System.arraycopy(extra, 0, all, base.length, extra.length); + cmd.parseArgs(all); + return cmd; + } + + @Test + public void testPortAndZkHostDefaultsComeFromTheProperties() { + System.setProperty("solr.port.listen", "7777"); + System.setProperty("zkHost", "zk.example:2181"); + CommandLine cmd = parse(); + assertEquals(7777, parsedValue(cmd, "--port")); + assertEquals("zk.example:2181", parsedValue(cmd, "--zk-host")); + } + + @Test + public void testPortDefaultsTo8983WithoutTheProperty() { + assertEquals(8983, parsedValue(parse(), "--port")); + } + + @Test + public void testScriptInputsIsTheOptionTheScriptPasses() { + assertEquals("1,2", parsedValue(parse("--script-inputs", "1,2"), "--script-inputs")); + } + + private static RunExampleTool tool(CommandLine cmd) { + return (RunExampleTool) cmd.getCommand(); + } + + @Test + public void testDashDArgumentsAreKeptAsExtraArguments() { + // what bin/solr start -e techproducts -Dcustom.prop=1 forwards to run_example + CommandLine cmd = parse("some-arg", "-Dcustom.prop=1", "-Dother=2"); + assertArrayEquals( + new String[] {"some-arg", "-Dcustom.prop=1", "-Dother=2"}, tool(cmd).picocliExtraArgs()); + } + + @Test + public void testDashDValueOfJvmOptsIsNotAnExtraArgument() { + CommandLine cmd = parse("--jvm-opts", "-Dcustom.prop=helloworld"); + assertArrayEquals(new String[0], tool(cmd).picocliExtraArgs()); + } + + @Test + public void testOtherUnknownOptionsAreStillAUsageError() { + CommandLine cmd = parse("--no-such-option", "-Dcustom.prop=1"); + assertEquals(List.of("--no-such-option"), tool(cmd).unknownOptions()); + + // nothing runs: the message and usage go to stderr and the exit code is picocli's usage error + StringWriter err = new StringWriter(); + CommandLine run = + new CommandLine(new RunExampleTool(new CLITestHelper.TestingRuntime(false))) + .setDefaultValueProvider(new CliDefaultValueProvider()) + .setErr(new PrintWriter(err)); + assertEquals( + 2, run.execute("-e", "techproducts", "--server-dir", "/tmp/server", "--no-such-option")); + assertTrue(err.toString(), err.toString().contains("Unknown option: '--no-such-option'")); + assertTrue(err.toString(), err.toString().contains("Usage")); + } +} diff --git a/solr/core/src/test/org/apache/solr/cli/TestSolrCLIRunExample.java b/solr/core/src/test/org/apache/solr/cli/TestSolrCLIRunExample.java index f290115486c..b9957e993cc 100644 --- a/solr/core/src/test/org/apache/solr/cli/TestSolrCLIRunExample.java +++ b/solr/core/src/test/org/apache/solr/cli/TestSolrCLIRunExample.java @@ -57,6 +57,11 @@ import org.slf4j.LoggerFactory; @SolrTestCaseJ4.SuppressSSL(bugUrl = "https://issues.apache.org/jira/browse/SOLR-5776") public class TestSolrCLIRunExample extends SolrTestCaseJ4 { + /** Runs the tool. Overridden by the picocli variant of this test. */ + protected int runTool(RunExampleTool tool, String[] args) throws Exception { + return tool.runTool(SolrCLI.processCommandLineArgs(tool, args)); + } + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @BeforeClass @@ -350,7 +355,7 @@ public class TestSolrCLIRunExample extends SolrTestCaseJ4 { RunExampleTool tool = new RunExampleTool(executor, System.in, runtime); try { - int status = tool.runTool(SolrCLI.processCommandLineArgs(tool, toolArgs)); + int status = runTool(tool, toolArgs); if (status == -1) { // maybe it's the port, try again @@ -358,7 +363,7 @@ public class TestSolrCLIRunExample extends SolrTestCaseJ4 { bindPort = socket.getLocalPort(); } Thread.sleep(100); - status = tool.runTool(SolrCLI.processCommandLineArgs(tool, toolArgs)); + status = runTool(tool, toolArgs); } assertEquals("it should be ok " + tool + " " + Arrays.toString(toolArgs), 0, status); @@ -452,7 +457,7 @@ public class TestSolrCLIRunExample extends SolrTestCaseJ4 { RunExampleTool tool = new RunExampleTool(executor, userInputSim, runtime); try { - tool.runTool(SolrCLI.processCommandLineArgs(tool, toolArgs)); + runTool(tool, toolArgs); } catch (Exception e) { System.err.println( "RunExampleTool failed due to: " @@ -565,7 +570,7 @@ public class TestSolrCLIRunExample extends SolrTestCaseJ4 { RunExampleTool tool = new RunExampleTool(executor, System.in, runtime); try { - tool.runTool(SolrCLI.processCommandLineArgs(tool, toolArgs)); + runTool(tool, toolArgs); } catch (Exception e) { System.err.println( "RunExampleTool failed due to: " @@ -665,7 +670,7 @@ public class TestSolrCLIRunExample extends SolrTestCaseJ4 { ToolRuntime runtime = new CLITestHelper.TestingRuntime(false); RunExampleTool tool = new RunExampleTool(executor, System.in, runtime); - int code = tool.runTool(SolrCLI.processCommandLineArgs(tool, toolArgs)); + int code = runTool(tool, toolArgs); assertEquals("Execution should have failed with return code 1", 1, code); } }
