diff --git a/java/com/google/gerrit/util/cli/CmdLineParser.java b/java/com/google/gerrit/util/cli/CmdLineParser.java index 1c16133a6d..0fff861258 100644 --- a/java/com/google/gerrit/util/cli/CmdLineParser.java +++ b/java/com/google/gerrit/util/cli/CmdLineParser.java @@ -325,26 +325,32 @@ public class CmdLineParser { public void parseOptionMap(ListMultimap params) throws CmdLineException { logger.atFinest().log("Command-line parameters: %s", params.keySet()); - List tmp = Lists.newArrayListWithCapacity(2 * params.size()); + List knownArgs = Lists.newArrayListWithCapacity(2 * params.size()); for (String key : params.keySet()) { String name = makeOption(key); - if (isBoolean(name)) { - boolean on = false; - for (String value : params.get(key)) { - on = toBoolean(key, value); - } - if (on) { - tmp.add(name); + if (isKnownOption(name)) { + if (isBoolean(name)) { + boolean on = false; + for (String value : params.get(key)) { + on = toBoolean(key, value); + } + if (on) { + knownArgs.add(name); + } + } else { + for (String value : params.get(key)) { + knownArgs.add(name); + knownArgs.add(value); + } } } else { for (String value : params.get(key)) { - tmp.add(name); - tmp.add(value); + parser.handleUnknownOption(name, value); } } } - parser.parseArgument(tmp.toArray(new String[tmp.size()])); + parser.parseArgument(knownArgs.toArray(new String[knownArgs.size()])); } public boolean isBoolean(String name) { @@ -370,6 +376,10 @@ public class CmdLineParser { return name; } + private boolean isKnownOption(String name) { + return findHandler(name) != null; + } + @SuppressWarnings("rawtypes") private OptionHandler findHandler(String name) { if (options == null) { @@ -437,6 +447,8 @@ public class CmdLineParser { } public class MyParser extends org.kohsuke.args4j.CmdLineParser { + private final Object bean; + boolean help; @SuppressWarnings("rawtypes") @@ -464,11 +476,22 @@ public class CmdLineParser { MyParser(Object bean) { super(bean, ParserProperties.defaults().withAtSyntax(false)); + this.bean = bean; parseAdditionalOptions(bean, new HashSet<>()); addOptionsWithMetRequirements(); ensureOptionsInitialized(); } + public void handleUnknownOption(String name, String value) throws CmdLineException { + if (bean instanceof UnknownOptionHandler + && ((UnknownOptionHandler) bean).accept(name, Strings.emptyToNull(value))) { + return; + } + + // Parse argument to trigger a CmdLineException for the unknown option. + parseArgument(name, value); + } + public int addOptionsWithMetRequirements() { int count = 0; for (Iterator> it = queuedOptionsByName.entrySet().iterator(); diff --git a/java/com/google/gerrit/util/cli/UnknownOptionHandler.java b/java/com/google/gerrit/util/cli/UnknownOptionHandler.java new file mode 100644 index 0000000000..af096f9500 --- /dev/null +++ b/java/com/google/gerrit/util/cli/UnknownOptionHandler.java @@ -0,0 +1,42 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed 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 com.google.gerrit.util.cli; + +import com.google.gerrit.common.Nullable; + +/** + * Classes that define command-line options by using the {@link org.kohsuke.args4j.Option} + * annotation can implement this class to accept and handle unknown options. + * + *

If a user specifies an unknown option and this unknown option doesn't get accepted, the + * parsing of the command-line options fails and the user gets an error (this is the default + * behavior if classes do not implement this interface). + */ +public interface UnknownOptionHandler { + /** + * Whether an unknown option should be accepted. + * + *

If an unknown option is not accepted, the parsing of the command-line options fails and the + * user gets an error. + * + *

This method can be used to ignore unknown options (without failure for the user) or to + * handle them. + * + * @param name the name of an unknown option that was provided by the user + * @param value the value of the unknown option that was provided by the user + * @return whether this unknown options is accepted + */ + boolean accept(String name, @Nullable String value); +} diff --git a/javatests/com/google/gerrit/acceptance/rest/BUILD b/javatests/com/google/gerrit/acceptance/rest/BUILD index 84887dadb3..c7669347df 100644 --- a/javatests/com/google/gerrit/acceptance/rest/BUILD +++ b/javatests/com/google/gerrit/acceptance/rest/BUILD @@ -6,5 +6,6 @@ acceptance_tests( labels = ["rest"], deps = [ "//java/com/google/gerrit/server/logging", + "//java/com/google/gerrit/util/cli", ], ) diff --git a/javatests/com/google/gerrit/acceptance/rest/UnknownOptionIT.java b/javatests/com/google/gerrit/acceptance/rest/UnknownOptionIT.java new file mode 100644 index 0000000000..f15334bd8c --- /dev/null +++ b/javatests/com/google/gerrit/acceptance/rest/UnknownOptionIT.java @@ -0,0 +1,75 @@ +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed 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 com.google.gerrit.acceptance.rest; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND; +import static org.apache.http.HttpStatus.SC_BAD_REQUEST; +import static org.apache.http.HttpStatus.SC_OK; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiModule; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.server.change.ChangeResource; +import com.google.gerrit.util.cli.UnknownOptionHandler; +import com.google.inject.Module; +import org.junit.Test; + +public class UnknownOptionIT extends AbstractDaemonTest { + @Override + public Module createModule() { + return new RestApiModule() { + @Override + protected void configure() { + get(CHANGE_KIND, "test").to(MyChangeView.class); + } + }; + } + + @Test + public void unknownOptionIsRejectedIfRestEndpointDoesNotHandleUnknownOptions() throws Exception { + RestResponse response = adminRestSession.get("/accounts/self/detail?foo-bar"); + assertThat(response.getStatusCode()).isEqualTo(SC_BAD_REQUEST); + } + + @Test + public void unknownOptionIsIgnoredIfRestEndpointAcceptsIt() throws Exception { + String changeId = createChange().getChangeId(); + RestResponse response = adminRestSession.get("/changes/" + changeId + "/test?ignore-foo"); + assertThat(response.getStatusCode()).isEqualTo(SC_OK); + } + + @Test + public void unknownOptionCausesFailureIfRestEndpointDoesNotAcceptIt() throws Exception { + String changeId = createChange().getChangeId(); + RestResponse response = adminRestSession.get("/changes/" + changeId + "/test?foo-bar"); + assertThat(response.getStatusCode()).isEqualTo(SC_BAD_REQUEST); + } + + private static class MyChangeView implements RestReadView, UnknownOptionHandler { + @Override + public Response apply(ChangeResource resource) { + return Response.ok("OK"); + } + + @Override + public boolean accept(String name, @Nullable String value) { + return name.startsWith("--ignore"); + } + } +}