Add @RequiresOptions annotation for dynamic options

Add @RequiresOptions annotation for dynamic options to note one or more
option dependencies so that the dynamic options will not be present
unless all the @Option's on which they depend are present.

Change-Id: Iadfb061a4b288bfebee34e1806d97c13e421fdb6
This commit is contained in:
Zac Livingston
2017-11-21 12:13:29 -07:00
committed by Martin Fick
parent 57b553ad94
commit 99a1ad102c
4 changed files with 131 additions and 4 deletions

View File

@@ -814,8 +814,18 @@ parameters to be parsed. OptionHandlers may optionally pass this class to
other methods which may then both parse/consume more parameters and call
additional parameters.
When calling command options not provided by your plugin, there is always
a risk that the options may not exist, perhaps because the options being
called are to be provided by another plugin, and said plugin is not
currently installed. To protect againt this situation, it is possible to
define an option as being dependent on other options using the
@RequiresOptions() annotation. If the required options are not all not
currently present, then the dependent option will not be available or
visible in the help.
The example below shows a plugin that adds a "--special" option (perhaps
for use with the Query command) that calls the "--format json" option.
for use with the Query command) that calls (and requires) the
"--format json" option.
[source, java]
----
@@ -840,6 +850,7 @@ public class JsonOutputOptionHandler<T> extends OptionHandler<T> {
}
}
@RequiresOptions("--format")
@Option(
name = "--special",
usage = "ouptut results using json",

View File

@@ -50,8 +50,18 @@ public class DynamicOptions {
* }
* </pre>
*
* The option will be prefixed by the plugin name. In the example above, if the plugin name was
* <p>The option will be prefixed by the plugin name. In the example above, if the plugin name was
* my-plugin, then the --verbose option as used by the caller would be --my-plugin--verbose.
*
* <p>Additional options can be annotated with @RequiresOption which will cause them to be ignored
* unless the required option is present. For example:
*
* <pre>
* {@literal @}RequiresOptions("--help")
* {@literal @}Option(name = "--help-as-json",
* usage = "display help text in json format")
* public boolean displayHelpAsJson;
* </pre>
*/
public interface DynamicBean {}
@@ -261,6 +271,7 @@ public class DynamicOptions {
for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) {
clp.parseWithPrefix("--" + e.getKey(), e.getValue());
}
clp.drainOptionQueue();
}
public void setDynamicBeans() {

View File

@@ -53,6 +53,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
@@ -355,6 +357,10 @@ public class CmdLineParser {
parser.parseWithPrefix(prefix, bean);
}
public void drainOptionQueue() {
parser.addOptionsWithMetRequirements();
}
private String makeOption(String name) {
if (!name.startsWith("-")) {
if (name.length() == 1) {
@@ -493,14 +499,54 @@ public class CmdLineParser {
@SuppressWarnings("rawtypes")
private List<OptionHandler> optionsList;
private Map<String, QueuedOption> queuedOptionsByName = new LinkedHashMap<>();
private HelpOption help;
private class QueuedOption {
public final Option option;
public final Setter setter;
public final String[] requiredOptions;
private QueuedOption(Option option, Setter setter, RequiresOptions requiresOptions) {
this.option = option;
this.setter = setter;
this.requiredOptions = requiresOptions != null ? requiresOptions.value() : new String[0];
}
}
MyParser(Object bean) {
super(bean, ParserProperties.defaults().withAtSyntax(false));
parseAdditionalOptions(bean, new HashSet<>());
addOptionsWithMetRequirements();
ensureOptionsInitialized();
}
public int addOptionsWithMetRequirements() {
int count = 0;
for (Iterator<Map.Entry<String, QueuedOption>> it = queuedOptionsByName.entrySet().iterator();
it.hasNext(); ) {
QueuedOption queuedOption = it.next().getValue();
if (hasAllRequiredOptions(queuedOption)) {
addOption(queuedOption.setter, queuedOption.option);
it.remove();
count++;
}
}
if (count > 0) {
count += addOptionsWithMetRequirements();
}
return count;
}
private boolean hasAllRequiredOptions(QueuedOption queuedOption) {
for (String name : queuedOption.requiredOptions) {
if (findOptionByName(name) == null) {
return false;
}
}
return true;
}
// NOTE: Argument annotations on bean are ignored.
public void parseWithPrefix(String prefix, Object bean) {
parseWithPrefix(prefix, bean, new HashSet<>());
@@ -515,13 +561,19 @@ public class CmdLineParser {
for (Method m : c.getDeclaredMethods()) {
Option o = m.getAnnotation(Option.class);
if (o != null) {
addOption(new MethodSetter(this, bean, m), new PrefixedOption(prefix, o));
queueOption(
new PrefixedOption(prefix, o),
new MethodSetter(this, bean, m),
m.getAnnotation(RequiresOptions.class));
}
}
for (Field f : c.getDeclaredFields()) {
Option o = f.getAnnotation(Option.class);
if (o != null) {
addOption(Setters.create(f, bean), new PrefixedOption(prefix, o));
queueOption(
new PrefixedOption(prefix, o),
Setters.create(f, bean),
f.getAnnotation(RequiresOptions.class));
}
if (f.isAnnotationPresent(Options.class)) {
try {
@@ -588,6 +640,14 @@ public class CmdLineParser {
return null;
}
private void queueOption(Option option, Setter setter, RequiresOptions requiresOptions) {
if (queuedOptionsByName.put(option.name(), new QueuedOption(option, setter, requiresOptions))
!= null) {
throw new IllegalAnnotationError(
"Option name " + option.name() + " is used more than once");
}
}
@SuppressWarnings("rawtypes")
private OptionHandler add(OptionHandler handler) {
ensureOptionsInitialized();

View File

@@ -0,0 +1,45 @@
// Copyright (C) 2017 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 static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* Marks a field/setter annotated with {@literal @}Option as having a dependency on multiple other
* command line option.
*
* <p>If any of the required command line options are not present, the {@literal @}Option will be
* ignored.
*
* <p>For example:
*
* <pre>
* {@literal @}RequiresOptions({"--help", "--usage"})
* {@literal @}Option(name = "--help-as-json",
* usage = "display help text in json format")
* public boolean displayHelpAsJson;
* </pre>
*/
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER})
public @interface RequiresOptions {
String[] value();
}