Add support for HTTP plugins

Plugins may contribute to the /plugins/NAME/ URL space by providing
a ServletModule in the manifest using Gerrit-HttpModule and binding
servlets and filters using Guice bindings.

All names are relative to the plugin's directory, so

  serve("/").with(IndexServlet.class);

will handle /plugins/NAME/ and not "/" on the server. This makes a
plugin automatically relocatable to match its SSH command name or
the name in $site_dir/plugins.

Change-Id: I17e3007f0310d2bf4989d652f18864a77c5d5f2e
This commit is contained in:
Shawn O. Pearce
2012-05-09 12:02:50 -07:00
committed by gerrit code review
parent 9c11044bf7
commit 2aefecf604
11 changed files with 554 additions and 6 deletions

View File

@@ -20,6 +20,7 @@ import com.google.inject.AbstractModule;
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;
@@ -30,15 +31,24 @@ import java.util.jar.Manifest;
import javax.annotation.Nullable;
public class Plugin {
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())
.setLevel(java.util.logging.Level.OFF);
}
private final String name;
private final File jar;
private final Manifest manifest;
private final FileSnapshot snapshot;
private Class<? extends Module> sysModule;
private Class<? extends Module> sshModule;
private Class<? extends Module> httpModule;
private Injector sysInjector;
private Injector sshInjector;
private Injector httpInjector;
private LifecycleManager manager;
public Plugin(String name,
@@ -46,13 +56,15 @@ public class Plugin {
Manifest manifest,
FileSnapshot snapshot,
@Nullable Class<? extends Module> sysModule,
@Nullable Class<? extends Module> sshModule) {
@Nullable Class<? extends Module> sshModule,
@Nullable Class<? extends Module> httpModule) {
this.name = name;
this.jar = jar;
this.manifest = manifest;
this.snapshot = snapshot;
this.sysModule = sysModule;
this.sshModule = sshModule;
this.httpModule = httpModule;
}
File getJar() {
@@ -90,6 +102,13 @@ public class Plugin {
manager.add(sshInjector);
}
if (httpModule != null && env.hasHttpModule()) {
httpInjector = sysInjector.createChildInjector(
env.getHttpModule(),
sysInjector.getInstance(httpModule));
manager.add(httpInjector);
}
manager.start();
env.onStartPlugin(this);
}
@@ -113,6 +132,7 @@ public class Plugin {
manager = null;
sysInjector = null;
sshInjector = null;
httpInjector = null;
}
}
@@ -121,6 +141,11 @@ public class Plugin {
return sshInjector;
}
@Nullable
public Injector getHttpInjector() {
return httpInjector;
}
public void add(final RegistrationHandle handle) {
add(new LifecycleListener() {
@Override

View File

@@ -45,6 +45,7 @@ public class PluginGuiceEnvironment {
private final List<StartPluginListener> listeners;
private Module sysModule;
private Module sshModule;
private Module httpModule;
@Inject
PluginGuiceEnvironment(Injector sysInjector, CopyConfigModule ccm) {
@@ -84,6 +85,19 @@ public class PluginGuiceEnvironment {
return sshModule;
}
public void setHttpInjector(Injector httpInjector) {
httpModule = copy(httpInjector);
listeners.addAll(getListeners(httpInjector));
}
boolean hasHttpModule() {
return httpModule != null;
}
Module getHttpModule() {
return httpModule;
}
void onStartPlugin(Plugin plugin) {
for (StartPluginListener l : listeners) {
l.onStartPlugin(plugin);
@@ -126,15 +140,74 @@ public class PluginGuiceEnvironment {
private static boolean shouldCopy(Key<?> key) {
Class<?> type = key.getTypeLiteral().getRawType();
if (type == LifecycleListener.class) {
if (LifecycleListener.class.isAssignableFrom(type)) {
return false;
}
if (type == StartPluginListener.class) {
if (StartPluginListener.class.isAssignableFrom(type)) {
return false;
}
if ("org.apache.sshd.server.Command".equals(type.getName())) {
if (type.getName().startsWith("com.google.inject.")) {
return false;
}
if (is("org.apache.sshd.server.Command", type)) {
return false;
}
if (is("javax.servlet.Filter", type)) {
return false;
}
if (is("javax.servlet.ServletContext", type)) {
return false;
}
if (is("javax.servlet.ServletRequest", type)) {
return false;
}
if (is("javax.servlet.ServletResponse", type)) {
return false;
}
if (is("javax.servlet.http.HttpServlet", type)) {
return false;
}
if (is("javax.servlet.http.HttpServletRequest", type)) {
return false;
}
if (is("javax.servlet.http.HttpServletResponse", type)) {
return false;
}
if (is("javax.servlet.http.HttpSession", type)) {
return false;
}
if (Map.class.isAssignableFrom(type)
&& key.getAnnotationType() != null
&& "com.google.inject.servlet.RequestParameters"
.equals(key.getAnnotationType().getName())) {
return false;
}
if (type.getName().startsWith("com.google.gerrit.httpd.GitOverHttpServlet$")) {
return false;
}
return true;
}
private static boolean is(String name, Class<?> type) {
Class<?> p = type;
while (p != null) {
if (name.equals(p.getName())) {
return true;
}
p = p.getSuperclass();
}
Class<?>[] interfaces = type.getInterfaces();
if (interfaces != null) {
for (Class<?> i : interfaces) {
if (is(name, i)) {
return true;
}
}
}
return false;
}
}

View File

@@ -245,6 +245,7 @@ public class PluginLoader implements LifecycleListener {
Attributes main = manifest.getMainAttributes();
String sysName = main.getValue("Gerrit-Module");
String sshName = main.getValue("Gerrit-SshModule");
String httpName = main.getValue("Gerrit-HttpModule");
URL[] urls = {jarFile.toURI().toURL()};
ClassLoader parentLoader = PluginLoader.class.getClassLoader();
@@ -252,7 +253,9 @@ public class PluginLoader implements LifecycleListener {
Class<? extends Module> sysModule = load(sysName, pluginLoader);
Class<? extends Module> sshModule = load(sshName, pluginLoader);
return new Plugin(name, jarFile, manifest, snapshot, sysModule, sshModule);
Class<? extends Module> httpModule = load(httpName, pluginLoader);
return new Plugin(name, jarFile, manifest, snapshot,
sysModule, sshModule, httpModule);
}
private Class<? extends Module> load(String name, ClassLoader pluginLoader)