Automatically register plugin bindings
If a plugin has no modules declared in the manifest, automatically
generate the modules for the plugin based on the class files that
appear in the plugin and the @Export annotations that appear on these
concrete classes.
For any non-abstract command that extends SshCommand (or really the
internal MINA SSHD Command interface that Gerrit uses), plugins may
declare the command with @Export("name") to bind the implementation
as that SSH command, e.g.:
@Export("print-hello")
class Hello extend SshCommand {
Likewise HTTP servlets can also be bound to URLs, but this only works
for standard servlet mappings like "/foo" or "/foo/*". Regex style
bindings must use the Guice ServletModule declared in the manifest:
@Export("/print-hello")
class Hello extends HttpServlet {
Change-Id: Iae4cffcd62d7d2911d3f2705e226fbe21434be68
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
new file mode 100644
index 0000000..2d957f2
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2012 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.httpd.plugins;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.google.inject.servlet.ServletModule;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServlet;
+
+class HttpAutoRegisterModuleGenerator extends ServletModule
+ implements ModuleGenerator {
+ private final Map<String, Class<HttpServlet>> serve = Maps.newHashMap();
+
+ @Override
+ protected void configureServlets() {
+ for (Map.Entry<String, Class<HttpServlet>> e : serve.entrySet()) {
+ bind(e.getValue()).in(Scopes.SINGLETON);
+ serve(e.getKey()).with(e.getValue());
+ }
+ }
+
+ @Override
+ public void setPluginName(String name) {
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void export(Export export, Class<?> type)
+ throws InvalidPluginException {
+ if (HttpServlet.class.isAssignableFrom(type)) {
+ Class<HttpServlet> old = serve.get(export.value());
+ if (old != null) {
+ throw new InvalidPluginException(String.format(
+ "@Export(\"%s\") has duplicate bindings:\n %s\n %s",
+ export.value(), old.getName(), type.getName()));
+ }
+ serve.put(export.value(), (Class<HttpServlet>) type);
+ } else {
+ throw new InvalidPluginException(String.format(
+ "Class %s with @Export(\"%s\") must extend %s",
+ type.getName(), export.value(),
+ HttpServlet.class.getName()));
+ }
+ }
+
+ @Override
+ public Module create() throws InvalidPluginException {
+ return this;
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
index 0ad90c2..2e5001b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -14,6 +14,7 @@
package com.google.gerrit.httpd.plugins;
+import com.google.gerrit.server.plugins.ModuleGenerator;
import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.plugins.StartPluginListener;
import com.google.inject.internal.UniqueAnnotations;
@@ -32,5 +33,8 @@
bind(ReloadPluginListener.class)
.annotatedWith(UniqueAnnotations.create())
.to(HttpPluginServlet.class);
+
+ bind(ModuleGenerator.class)
+ .to(HttpAutoRegisterModuleGenerator.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
new file mode 100644
index 0000000..5c8885b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -0,0 +1,254 @@
+// Copyright (C) 2012 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.plugins;
+
+import static com.google.gerrit.server.plugins.PluginGuiceEnvironment.is;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Module;
+
+import org.eclipse.jgit.util.IO;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+class AutoRegisterModules {
+ private static final int SKIP_ALL = ClassReader.SKIP_CODE
+ | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
+ private final String pluginName;
+ private final JarFile jarFile;
+ private final ClassLoader classLoader;
+ private final ModuleGenerator sshGen;
+ private final ModuleGenerator httpGen;
+
+ Module sysModule;
+ Module sshModule;
+ Module httpModule;
+
+ AutoRegisterModules(String pluginName,
+ PluginGuiceEnvironment env,
+ JarFile jarFile,
+ ClassLoader classLoader) {
+ this.pluginName = pluginName;
+ this.jarFile = jarFile;
+ this.classLoader = classLoader;
+ this.sshGen = env.hasSshModule() ? env.newSshModuleGenerator() : null;
+ this.httpGen = env.hasHttpModule() ? env.newHttpModuleGenerator() : null;
+ }
+
+ AutoRegisterModules discover() throws InvalidPluginException {
+ if (sshGen != null) {
+ sshGen.setPluginName(pluginName);
+ }
+ if (httpGen != null) {
+ httpGen.setPluginName(pluginName);
+ }
+
+ scan();
+
+ if (sshGen != null) {
+ sshModule = sshGen.create();
+ }
+ if (httpGen != null) {
+ httpModule = httpGen.create();
+ }
+ return this;
+ }
+
+ private void scan() throws InvalidPluginException {
+ Enumeration<JarEntry> e = jarFile.entries();
+ while (e.hasMoreElements()) {
+ JarEntry entry = e.nextElement();
+ if (skip(entry)) {
+ continue;
+ }
+
+ ClassData def = new ClassData();
+ try {
+ new ClassReader(read(entry)).accept(def, SKIP_ALL);
+ } catch (IOException err) {
+ throw new InvalidPluginException("Cannot auto-register", err);
+ } catch (RuntimeException err) {
+ PluginLoader.log.warn(String.format(
+ "Plugin %s has invaild class file %s inside of %s",
+ pluginName, entry.getName(), jarFile.getName()), err);
+ continue;
+ }
+
+ if (def.exportedAsName != null) {
+ if (def.isConcrete()) {
+ export(def);
+ } else {
+ PluginLoader.log.warn(String.format(
+ "Plugin %s tries to export abstract class %s",
+ pluginName, def.className));
+ }
+ }
+ }
+ }
+
+ private void export(ClassData def) throws InvalidPluginException {
+ Class<?> clazz;
+ try {
+ clazz = Class.forName(def.className, false, classLoader);
+ } catch (ClassNotFoundException err) {
+ throw new InvalidPluginException(String.format(
+ "Cannot load %s with @Export(\"%s\")",
+ def.className, def.exportedAsName), err);
+ }
+
+ Export export = clazz.getAnnotation(Export.class);
+ if (export == null) {
+ PluginLoader.log.warn(String.format(
+ "In plugin %s asm incorrectly parsed %s with @Export(\"%s\")",
+ pluginName, clazz.getName(), def.exportedAsName));
+ return;
+ }
+
+ if (is("org.apache.sshd.server.Command", clazz)) {
+ if (sshGen != null) {
+ sshGen.export(export, clazz);
+ }
+ } else if (is("javax.servlet.http.HttpServlet", clazz)) {
+ if (httpGen != null) {
+ httpGen.export(export, clazz);
+ }
+ } else {
+ throw new InvalidPluginException(String.format(
+ "Class %s with @Export(\"%s\") not supported",
+ clazz.getName(), export.value()));
+ }
+ }
+
+ private static boolean skip(JarEntry entry) {
+ if (!entry.getName().endsWith(".class")) {
+ return true; // Avoid non-class resources.
+ }
+ if (entry.getSize() <= 0) {
+ return true; // Directories have 0 size.
+ }
+ if (entry.getSize() >= 1024 * 1024) {
+ return true; // Do not scan huge class files.
+ }
+ return false;
+ }
+
+ private byte[] read(JarEntry entry) throws IOException {
+ byte[] data = new byte[(int) entry.getSize()];
+ InputStream in = jarFile.getInputStream(entry);
+ try {
+ IO.readFully(in, data, 0, data.length);
+ } finally {
+ in.close();
+ }
+ return data;
+ }
+
+ private static class ClassData implements ClassVisitor {
+ private static final String EXPORT = Type.getType(Export.class).getDescriptor();
+ String className;
+ int access;
+ String exportedAsName;
+
+ boolean isConcrete() {
+ return (access & Opcodes.ACC_ABSTRACT) == 0
+ && (access & Opcodes.ACC_INTERFACE) == 0;
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature,
+ String superName, String[] interfaces) {
+ this.className = Type.getObjectType(name).getClassName();
+ this.access = access;
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ if (visible && EXPORT.equals(desc)) {
+ return new AbstractAnnotationVisitor() {
+ @Override
+ public void visit(String name, Object value) {
+ exportedAsName = (String) value;
+ }
+ };
+ }
+ return null;
+ }
+
+ @Override
+ public void visitSource(String arg0, String arg1) {
+ }
+
+ @Override
+ public void visitOuterClass(String arg0, String arg1, String arg2) {
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int arg0, String arg1, String arg2,
+ String arg3, String[] arg4) {
+ return null;
+ }
+
+ @Override
+ public void visitInnerClass(String arg0, String arg1, String arg2, int arg3) {
+ }
+
+ @Override
+ public FieldVisitor visitField(int arg0, String arg1, String arg2,
+ String arg3, Object arg4) {
+ return null;
+ }
+
+ @Override
+ public void visitEnd() {
+ }
+
+ @Override
+ public void visitAttribute(Attribute arg0) {
+ }
+ }
+
+ private static abstract class AbstractAnnotationVisitor implements
+ AnnotationVisitor {
+ @Override
+ public AnnotationVisitor visitAnnotation(String arg0, String arg1) {
+ return null;
+ }
+
+ @Override
+ public AnnotationVisitor visitArray(String arg0) {
+ return null;
+ }
+
+ @Override
+ public void visitEnum(String arg0, String arg1, String arg2) {
+ }
+
+ @Override
+ public void visitEnd() {
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
new file mode 100644
index 0000000..31be10c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2012 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.plugins;
+
+public class InvalidPluginException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidPluginException(String message) {
+ super(message);
+ }
+
+ public InvalidPluginException(String message, Throwable why) {
+ super(message, why);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
new file mode 100644
index 0000000..92e3b1d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2012 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.plugins;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Module;
+
+public interface ModuleGenerator {
+ void setPluginName(String name);
+
+ void export(Export export, Class<?> type) throws InvalidPluginException;
+
+ Module create() throws InvalidPluginException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index b4bee88..a97eb1d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -23,7 +23,6 @@
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
-import com.google.inject.servlet.GuiceFilter;
import org.eclipse.jgit.storage.file.FileSnapshot;
@@ -38,7 +37,7 @@
static {
// Guice logs warnings about multiple injectors being created.
// Silence this in case HTTP plugins are used.
- java.util.logging.Logger.getLogger(GuiceFilter.class.getName())
+ java.util.logging.Logger.getLogger("com.google.inject.servlet.GuiceFilter")
.setLevel(java.util.logging.Level.OFF);
}
@@ -47,6 +46,7 @@
private final FileSnapshot snapshot;
private final JarFile jarFile;
private final Manifest manifest;
+ private final ClassLoader classLoader;
private Class<? extends Module> sysModule;
private Class<? extends Module> sshModule;
private Class<? extends Module> httpModule;
@@ -61,6 +61,7 @@
FileSnapshot snapshot,
JarFile jarFile,
Manifest manifest,
+ ClassLoader classLoader,
@Nullable Class<? extends Module> sysModule,
@Nullable Class<? extends Module> sshModule,
@Nullable Class<? extends Module> httpModule) {
@@ -69,6 +70,7 @@
this.snapshot = snapshot;
this.jarFile = jarFile;
this.manifest = manifest;
+ this.classLoader = classLoader;
this.sysModule = sysModule;
this.sshModule = sshModule;
this.httpModule = httpModule;
@@ -110,25 +112,48 @@
Injector root = newRootInjector(env);
manager = new LifecycleManager();
+ AutoRegisterModules auto = null;
+ if (sysModule == null && sshModule == null && httpModule == null) {
+ auto = new AutoRegisterModules(name, env, jarFile, classLoader);
+ auto.discover();
+ }
+
if (sysModule != null) {
sysInjector = root.createChildInjector(root.getInstance(sysModule));
manager.add(sysInjector);
+ } else if (auto != null && auto.sysModule != null) {
+ sysInjector = root.createChildInjector(auto.sysModule);
+ manager.add(sysInjector);
} else {
sysInjector = root;
}
- if (sshModule != null && env.hasSshModule()) {
- sshInjector = sysInjector.createChildInjector(
- env.getSshModule(),
- sysInjector.getInstance(sshModule));
- manager.add(sshInjector);
+ if (env.hasSshModule()) {
+ if (sshModule != null) {
+ sshInjector = sysInjector.createChildInjector(
+ env.getSshModule(),
+ sysInjector.getInstance(sshModule));
+ manager.add(sshInjector);
+ } else if (auto != null && auto.sshModule != null) {
+ sshInjector = sysInjector.createChildInjector(
+ env.getSshModule(),
+ auto.sshModule);
+ manager.add(sshInjector);
+ }
}
- if (httpModule != null && env.hasHttpModule()) {
- httpInjector = sysInjector.createChildInjector(
- env.getHttpModule(),
- sysInjector.getInstance(httpModule));
- manager.add(httpInjector);
+ if (env.hasHttpModule()) {
+ if (httpModule != null) {
+ httpInjector = sysInjector.createChildInjector(
+ env.getHttpModule(),
+ sysInjector.getInstance(httpModule));
+ manager.add(httpInjector);
+ } else if (auto != null && auto.httpModule != null) {
+ httpInjector = sysInjector.createChildInjector(
+ env.getHttpModule(),
+ auto.httpModule);
+ manager.add(httpInjector);
+ }
}
manager.start();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 0e8a95d..d259cba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -22,6 +22,7 @@
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
@@ -48,6 +49,9 @@
private Module sshModule;
private Module httpModule;
+ private Provider<ModuleGenerator> sshGen;
+ private Provider<ModuleGenerator> httpGen;
+
@Inject
PluginGuiceEnvironment(Injector sysInjector, CopyConfigModule ccm) {
this.sysInjector = sysInjector;
@@ -79,6 +83,7 @@
public void setSshInjector(Injector injector) {
sshModule = copy(injector);
+ sshGen = injector.getProvider(ModuleGenerator.class);
onStart.addAll(listeners(injector, StartPluginListener.class));
onReload.addAll(listeners(injector, ReloadPluginListener.class));
}
@@ -91,8 +96,13 @@
return sshModule;
}
+ ModuleGenerator newSshModuleGenerator() {
+ return sshGen.get();
+ }
+
public void setHttpInjector(Injector injector) {
httpModule = copy(injector);
+ httpGen = injector.getProvider(ModuleGenerator.class);
onStart.addAll(listeners(injector, StartPluginListener.class));
onReload.addAll(listeners(injector, ReloadPluginListener.class));
}
@@ -105,6 +115,10 @@
return httpModule;
}
+ ModuleGenerator newHttpModuleGenerator() {
+ return httpGen.get();
+ }
+
void onStartPlugin(Plugin plugin) {
for (StartPluginListener l : onStart) {
l.onStartPlugin(plugin);
@@ -202,22 +216,22 @@
return true;
}
- private static boolean is(String name, Class<?> type) {
- Class<?> p = type;
- while (p != null) {
- if (name.equals(p.getName())) {
+ static boolean is(String name, Class<?> type) {
+ while (type != null) {
+ if (name.equals(type.getName())) {
return true;
}
- p = p.getSuperclass();
- }
- Class<?>[] interfaces = type.getInterfaces();
- if (interfaces != null) {
- for (Class<?> i : interfaces) {
- if (is(name, i)) {
- return true;
+ Class<?>[] interfaces = type.getInterfaces();
+ if (interfaces != null) {
+ for (Class<?> i : interfaces) {
+ if (is(name, i)) {
+ return true;
+ }
}
}
+
+ type = type.getSuperclass();
}
return false;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 330dc46..16cd78c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -337,6 +337,7 @@
return new Plugin(name,
srcJar, snapshot,
jarFile, manifest,
+ pluginLoader,
sysModule, sshModule, httpModule);
} finally {
if (!keep) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
new file mode 100644
index 0000000..b843893
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2012 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.sshd;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+
+import org.apache.sshd.server.Command;
+
+import java.util.Map;
+
+class SshAutoRegisterModuleGenerator
+ extends AbstractModule
+ implements ModuleGenerator {
+ private final Map<String, Class<Command>> commands = Maps.newHashMap();
+ private CommandName command;
+
+ @Override
+ protected void configure() {
+ bind(Commands.key(command))
+ .toProvider(new DispatchCommandProvider(command));
+ for (Map.Entry<String, Class<Command>> e : commands.entrySet()) {
+ bind(Commands.key(command, e.getKey())).to(e.getValue());
+ }
+ }
+
+ public void setPluginName(String name) {
+ command = Commands.named(name);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void export(Export export, Class<?> type)
+ throws InvalidPluginException {
+ Preconditions.checkState(command != null, "pluginName must be provided");
+ if (Command.class.isAssignableFrom(type)) {
+ Class<Command> old = commands.get(export.value());
+ if (old != null) {
+ throw new InvalidPluginException(String.format(
+ "@Export(\"%s\") has duplicate bindings:\n %s\n %s",
+ export.value(), old.getName(), type.getName()));
+ }
+ commands.put(export.value(), (Class<Command>) type);
+ } else {
+ throw new InvalidPluginException(String.format(
+ "Class %s with @Export(\"%s\") must extend %s or implement %s",
+ type.getName(), export.value(),
+ SshCommand.class.getName(), Command.class.getName()));
+ }
+ }
+
+ @Override
+ public Module create() throws InvalidPluginException {
+ Preconditions.checkState(command != null, "pluginName must be provided");
+ return this;
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 284f231..a814111 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -30,6 +30,7 @@
import com.google.gerrit.server.config.GerritRequestModule;
import com.google.gerrit.server.git.QueueProvider;
import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.plugins.ModuleGenerator;
import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.plugins.StartPluginListener;
import com.google.gerrit.server.project.ProjectControl;
@@ -92,6 +93,7 @@
install(new LifecycleModule() {
@Override
protected void configure() {
+ bind(ModuleGenerator.class).to(SshAutoRegisterModuleGenerator.class);
bind(SshPluginStarterCallback.class);
bind(StartPluginListener.class)
.annotatedWith(UniqueAnnotations.create())