Allow plugins to register options for both ssh and restapi commands
Change-Id: I6976dffa98cb822a207a4de4c549486591e849e8
This commit is contained in:
@@ -704,6 +704,37 @@ new `has:sample_pluginName` operand is shown below:
|
||||
}
|
||||
====
|
||||
|
||||
[[command_options]]
|
||||
=== Command Options ===
|
||||
|
||||
Plugins can provide additional options for each of the gerrit ssh and the
|
||||
REST API commands by implementing the DynamicBean interface and registering
|
||||
it to a command class name in the plugin module's `configure()` method. The
|
||||
plugin's name will be prepended to the name of each @Option annotation found
|
||||
on the DynamicBean object provided by the plugin. The example below shows a
|
||||
plugin that adds an option to log a value from the gerrit 'ban-commits'
|
||||
ssh command.
|
||||
|
||||
[source, java]
|
||||
----
|
||||
public class SshModule extends AbstractModule {
|
||||
private static final Logger log = LoggerFactory.getLogger(SshModule.class);
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(DynamicOptions.DynamicBean.class)
|
||||
.annotatedWith(Exports.named(
|
||||
com.google.gerrit.sshd.commands.BanCommitCommand.class))
|
||||
.to(BanOptions.class);
|
||||
}
|
||||
|
||||
public static class BanOptions implements DynamicOptions.DynamicBean {
|
||||
@Option(name = "--log", aliases = { "-l" }, usage = "Say Hello in the Log")
|
||||
private void parse(String arg) {
|
||||
log.error("Say Hello in the Log " + arg);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[simple-configuration]]
|
||||
== Simple Configuration in `gerrit.config`
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
|
||||
package com.google.gerrit.httpd.plugins;
|
||||
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.httpd.resources.Resource;
|
||||
import com.google.gerrit.httpd.resources.ResourceKey;
|
||||
import com.google.gerrit.httpd.resources.ResourceWeigher;
|
||||
import com.google.gerrit.server.DynamicOptions;
|
||||
import com.google.gerrit.server.cache.CacheModule;
|
||||
import com.google.gerrit.server.plugins.ModuleGenerator;
|
||||
import com.google.gerrit.server.plugins.ReloadPluginListener;
|
||||
@@ -62,5 +64,7 @@ public class HttpPluginModule extends ServletModule {
|
||||
.weigher(ResourceWeigher.class);
|
||||
}
|
||||
});
|
||||
|
||||
DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.extensions.restapi.BadRequestException;
|
||||
import com.google.gerrit.extensions.restapi.BinaryResult;
|
||||
import com.google.gerrit.extensions.restapi.Url;
|
||||
import com.google.gerrit.server.DynamicOptions;
|
||||
import com.google.gerrit.util.cli.CmdLineParser;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
@@ -49,16 +51,19 @@ class ParameterParser {
|
||||
ImmutableSet.of("pp", "prettyPrint", "strict", "callback", "alt", "fields");
|
||||
|
||||
private final CmdLineParser.Factory parserFactory;
|
||||
private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
|
||||
|
||||
@Inject
|
||||
ParameterParser(CmdLineParser.Factory pf) {
|
||||
ParameterParser(CmdLineParser.Factory pf, DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
|
||||
this.parserFactory = pf;
|
||||
this.dynamicBeans = dynamicBeans;
|
||||
}
|
||||
|
||||
<T> boolean parse(
|
||||
T param, ListMultimap<String, String> in, HttpServletRequest req, HttpServletResponse res)
|
||||
throws IOException {
|
||||
CmdLineParser clp = parserFactory.create(param);
|
||||
DynamicOptions.parse(dynamicBeans, clp, param);
|
||||
try {
|
||||
clp.parseOptionMap(in);
|
||||
} catch (CmdLineException | NumberFormatException e) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (C) 2016 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.server;
|
||||
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.util.cli.CmdLineParser;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
public class DynamicOptions {
|
||||
/**
|
||||
* To provide additional options, bind a DynamicBean. For example:
|
||||
*
|
||||
* <pre>
|
||||
* bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class)
|
||||
* .annotatedWith(Exports.named(com.google.gerrit.sshd.commands.Query.class))
|
||||
* .to(MyOptions.class);
|
||||
* </pre>
|
||||
*
|
||||
* To define the additional options, implement this interface. For example:
|
||||
*
|
||||
* <pre>
|
||||
* public class MyOptions implements DynamicOptions.DynamicBean {
|
||||
* {@literal @}Option(name = "--verbose", aliases = {"-v"}
|
||||
* usage = "Make the operation more talkative")
|
||||
* public boolean verbose;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
public interface DynamicBean {}
|
||||
|
||||
/**
|
||||
* The entity which provided additional options may need a way to receive a reference to the
|
||||
* DynamicBean it provided. To do so, the existing class should implement BeanReceiver (a setter)
|
||||
* and then provide some way for the plugin to request its DynamicBean (a getter.) For example:
|
||||
*
|
||||
* <pre>
|
||||
* public class Query extends SshCommand implements DynamicOptions.BeanReceiver {
|
||||
* public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
|
||||
* dynamicBeans.put(plugin, dynamicBean);
|
||||
* }
|
||||
*
|
||||
* public DynamicOptions.DynamicBean getDynamicBean(String plugin) {
|
||||
* return dynamicBeans.get(plugin);
|
||||
* }
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public interface BeanReceiver {
|
||||
void setDynamicBean(String plugin, DynamicBean dynamicBean);
|
||||
}
|
||||
|
||||
/**
|
||||
* To include options from DynamicBeans, setup a DynamicMap and call this parse method. For
|
||||
* example:
|
||||
*
|
||||
* <pre>
|
||||
* DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* protected void parseCommandLine(Object options) throws UnloggedFailure {
|
||||
* final CmdLineParser clp = newCmdLineParser(options);
|
||||
* DynamicOptions.parse(dynamicBeans, clp, options);
|
||||
* ...
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public static void parse(DynamicMap<DynamicBean> dynamicBeans, CmdLineParser clp, Object bean) {
|
||||
for (String plugin : dynamicBeans.plugins()) {
|
||||
Provider<DynamicBean> provider =
|
||||
dynamicBeans.byPlugin(plugin).get(bean.getClass().getCanonicalName());
|
||||
if (provider != null) {
|
||||
DynamicBean dynamicBean = provider.get();
|
||||
clp.parseWithPrefix(plugin, dynamicBean);
|
||||
if (bean instanceof BeanReceiver) {
|
||||
((BeanReceiver) bean).setDynamicBean(plugin, dynamicBean);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,10 @@ import com.google.common.util.concurrent.Atomics;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.common.TimeUtil;
|
||||
import com.google.gerrit.extensions.annotations.PluginName;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.reviewdb.client.Project;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.DynamicOptions;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.RequestCleanup;
|
||||
import com.google.gerrit.server.git.ProjectRunnable;
|
||||
@@ -84,6 +86,8 @@ public abstract class BaseCommand implements Command {
|
||||
|
||||
@Inject private SshScope.Context context;
|
||||
|
||||
@Inject private DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
|
||||
|
||||
/** Commands declared by a plugin can be scoped by the plugin name. */
|
||||
@Inject(optional = true)
|
||||
@PluginName
|
||||
@@ -193,6 +197,7 @@ public abstract class BaseCommand implements Command {
|
||||
*/
|
||||
protected void parseCommandLine(Object options) throws UnloggedFailure {
|
||||
final CmdLineParser clp = newCmdLineParser(options);
|
||||
DynamicOptions.parse(dynamicBeans, clp, options);
|
||||
try {
|
||||
clp.parseArgument(argv);
|
||||
} catch (IllegalArgumentException | CmdLineException err) {
|
||||
|
||||
@@ -17,7 +17,9 @@ package com.google.gerrit.sshd;
|
||||
import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
|
||||
import static com.google.inject.Scopes.SINGLETON;
|
||||
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.lifecycle.LifecycleModule;
|
||||
import com.google.gerrit.server.DynamicOptions;
|
||||
import com.google.gerrit.server.PeerDaemonUser;
|
||||
import com.google.gerrit.server.RemotePeer;
|
||||
import com.google.gerrit.server.config.GerritRequestModule;
|
||||
@@ -94,6 +96,8 @@ public class SshModule extends LifecycleModule {
|
||||
.annotatedWith(UniqueAnnotations.create())
|
||||
.to(SshPluginStarterCallback.class);
|
||||
|
||||
DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
|
||||
|
||||
listener().toInstance(registerInParentInjectors());
|
||||
listener().to(SshLog.class);
|
||||
listener().to(SshDaemon.class);
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
package com.google.gerrit.sshd;
|
||||
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.server.DynamicOptions;
|
||||
import com.google.gerrit.server.plugins.Plugin;
|
||||
import com.google.gerrit.server.plugins.ReloadPluginListener;
|
||||
import com.google.gerrit.server.plugins.StartPluginListener;
|
||||
@@ -30,10 +32,14 @@ class SshPluginStarterCallback implements StartPluginListener, ReloadPluginListe
|
||||
private static final Logger log = LoggerFactory.getLogger(SshPluginStarterCallback.class);
|
||||
|
||||
private final DispatchCommandProvider root;
|
||||
private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
|
||||
|
||||
@Inject
|
||||
SshPluginStarterCallback(@CommandName(Commands.ROOT) DispatchCommandProvider root) {
|
||||
SshPluginStarterCallback(
|
||||
@CommandName(Commands.ROOT) DispatchCommandProvider root,
|
||||
DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
|
||||
this.root = root;
|
||||
this.dynamicBeans = dynamicBeans;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,10 +64,19 @@ class SshPluginStarterCallback implements StartPluginListener, ReloadPluginListe
|
||||
try {
|
||||
return plugin.getSshInjector().getProvider(key);
|
||||
} catch (RuntimeException err) {
|
||||
if (!providesDynamicOptions(plugin)) {
|
||||
log.warn(
|
||||
String.format("Plugin %s did not define its top-level command", plugin.getName()), err);
|
||||
String.format(
|
||||
"Plugin %s did not define its top-level command nor any DynamicOptions",
|
||||
plugin.getName()),
|
||||
err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean providesDynamicOptions(Plugin plugin) {
|
||||
return dynamicBeans.plugins().contains(plugin.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -58,8 +60,10 @@ import org.kohsuke.args4j.OptionDef;
|
||||
import org.kohsuke.args4j.spi.BooleanOptionHandler;
|
||||
import org.kohsuke.args4j.spi.EnumOptionHandler;
|
||||
import org.kohsuke.args4j.spi.FieldSetter;
|
||||
import org.kohsuke.args4j.spi.MethodSetter;
|
||||
import org.kohsuke.args4j.spi.OptionHandler;
|
||||
import org.kohsuke.args4j.spi.Setter;
|
||||
import org.kohsuke.args4j.spi.Setters;
|
||||
|
||||
/**
|
||||
* Extended command line parser which handles --foo=value arguments.
|
||||
@@ -253,6 +257,10 @@ public class CmdLineParser {
|
||||
return findHandler(makeOption(name)) instanceof BooleanOptionHandler;
|
||||
}
|
||||
|
||||
public void parseWithPrefix(String prefix, Object bean) {
|
||||
parser.parseWithPrefix(prefix, bean);
|
||||
}
|
||||
|
||||
private String makeOption(String name) {
|
||||
if (!name.startsWith("-")) {
|
||||
if (name.length() == 1) {
|
||||
@@ -313,6 +321,60 @@ public class CmdLineParser {
|
||||
throw new CmdLineException(parser, String.format("invalid boolean \"%s=%s\"", name, value));
|
||||
}
|
||||
|
||||
private static class PrefixedOption implements Option {
|
||||
String prefix;
|
||||
Option o;
|
||||
|
||||
PrefixedOption(String prefix, Option o) {
|
||||
this.prefix = prefix;
|
||||
this.o = o;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return getPrefixedName(prefix, o.name());
|
||||
}
|
||||
|
||||
public String[] aliases() {
|
||||
String[] prefixedAliases = new String[o.aliases().length];
|
||||
for (int i = 0; i < prefixedAliases.length; i++) {
|
||||
prefixedAliases[i] = getPrefixedName(prefix, o.aliases()[i]);
|
||||
}
|
||||
return prefixedAliases;
|
||||
}
|
||||
|
||||
public String usage() {
|
||||
return o.usage();
|
||||
}
|
||||
|
||||
public String metaVar() {
|
||||
return o.metaVar();
|
||||
}
|
||||
|
||||
public boolean required() {
|
||||
return o.required();
|
||||
}
|
||||
|
||||
public boolean hidden() {
|
||||
return o.hidden();
|
||||
}
|
||||
|
||||
public Class<? extends OptionHandler> handler() {
|
||||
return o.handler();
|
||||
}
|
||||
|
||||
public String[] depends() {
|
||||
return o.depends();
|
||||
}
|
||||
|
||||
public Class<? extends Annotation> annotationType() {
|
||||
return o.annotationType();
|
||||
}
|
||||
|
||||
private static String getPrefixedName(String prefix, String name) {
|
||||
return "--" + prefix + name;
|
||||
}
|
||||
}
|
||||
|
||||
private class MyParser extends org.kohsuke.args4j.CmdLineParser {
|
||||
@SuppressWarnings("rawtypes")
|
||||
private List<OptionHandler> optionsList;
|
||||
@@ -324,6 +386,25 @@ public class CmdLineParser {
|
||||
ensureOptionsInitialized();
|
||||
}
|
||||
|
||||
// NOTE: Argument annotations on bean are ignored.
|
||||
public void parseWithPrefix(String prefix, Object bean) {
|
||||
// recursively process all the methods/fields.
|
||||
for (Class c = bean.getClass(); c != null; c = c.getSuperclass()) {
|
||||
for (Method m : c.getDeclaredMethods()) {
|
||||
Option o = m.getAnnotation(Option.class);
|
||||
if (o != null) {
|
||||
addOption(new MethodSetter(this, bean, m), new PrefixedOption(prefix, o));
|
||||
}
|
||||
}
|
||||
for (Field f : c.getDeclaredFields()) {
|
||||
Option o = f.getAnnotation(Option.class);
|
||||
if (o != null) {
|
||||
addOption(Setters.create(f, bean), new PrefixedOption(prefix, o));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@Override
|
||||
protected OptionHandler createOptionHandler(final OptionDef option, final Setter setter) {
|
||||
|
||||
Reference in New Issue
Block a user