Support serving static/ and Documentation/ from plugins
The static/ and Documentation/ resource directories of a plugin can be
served over HTTP for any loaded and running plugin, even if it has no
other HTTP handlers. This permits a plugin to supply icons or other
graphics for the web UI, or documentation content to help users learn
how to use the plugin.
Change-Id: I267176cc76e161617d780438f88531fc50c1c2b8
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index c3f78e6..86f886c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -17,19 +17,29 @@
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import com.google.gerrit.server.MimeUtilFileTypeRegistry;
import com.google.gerrit.server.plugins.Plugin;
import com.google.gerrit.server.plugins.RegistrationHandle;
import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.servlet.GuiceFilter;
+import eu.medsea.mimeutil.MimeType;
+
+import org.eclipse.jgit.util.IO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
import javax.servlet.FilterChain;
import javax.servlet.ServletConfig;
@@ -48,11 +58,17 @@
private static final Logger log
= LoggerFactory.getLogger(HttpPluginServlet.class);
+ private final MimeUtilFileTypeRegistry mimeUtil;
private List<Plugin> pending = Lists.newArrayList();
private String base;
- private final ConcurrentMap<String, GuiceFilter> plugins
+ private final ConcurrentMap<String, PluginHolder> plugins
= Maps.newConcurrentMap();
+ @Inject
+ HttpPluginServlet(MimeUtilFileTypeRegistry mimeUtil) {
+ this.mimeUtil = mimeUtil;
+ }
+
@Override
public synchronized void init(ServletConfig config) throws ServletException {
super.init(config);
@@ -60,10 +76,7 @@
String path = config.getServletContext().getContextPath();
base = Strings.nullToEmpty(path) + "/plugins/";
for (Plugin plugin : pending) {
- GuiceFilter filter = load(plugin);
- if (filter != null) {
- plugins.put(plugin.getName(), filter);
- }
+ install(plugin);
}
pending = null;
}
@@ -73,19 +86,26 @@
if (pending != null) {
pending.add(plugin);
} else {
- GuiceFilter filter = load(plugin);
- if (filter != null) {
- plugins.put(plugin.getName(), filter);
- }
+ install(plugin);
}
}
@Override
public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
- GuiceFilter filter = load(newPlugin);
- if (filter != null) {
- plugins.put(newPlugin.getName(), filter);
- }
+ install(newPlugin);
+ }
+
+ private void install(Plugin plugin) {
+ GuiceFilter filter = load(plugin);
+ final String name = plugin.getName();
+ final PluginHolder holder = new PluginHolder(plugin, filter);
+ plugin.add(new RegistrationHandle() {
+ @Override
+ public void remove() {
+ plugins.remove(name, holder);
+ }
+ });
+ plugins.put(name, holder);
}
private GuiceFilter load(Plugin plugin) {
@@ -110,11 +130,7 @@
plugin.add(new RegistrationHandle() {
@Override
public void remove() {
- try {
- filter.destroy();
- } finally {
- plugins.remove(name, filter);
- }
+ filter.destroy();
}
});
return filter;
@@ -126,23 +142,95 @@
public void service(HttpServletRequest req, HttpServletResponse res)
throws IOException, ServletException {
String name = extractName(req);
- GuiceFilter filter = plugins.get(name);
- if (filter == null) {
+ final PluginHolder holder = plugins.get(name);
+ if (holder == null) {
noCache(res);
res.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
- filter.doFilter(new WrappedRequest(req, base + name), res,
- new FilterChain() {
- @Override
- public void doFilter(ServletRequest req, ServletResponse response)
- throws IOException, ServletException {
- HttpServletResponse res = (HttpServletResponse) response;
- noCache(res);
- res.sendError(HttpServletResponse.SC_NOT_FOUND);
+ WrappedRequest wr = new WrappedRequest(req, base + name);
+ FilterChain chain = new FilterChain() {
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res)
+ throws IOException {
+ onDefault(holder, (HttpServletRequest) req, (HttpServletResponse) res);
+ }
+ };
+ if (holder.filter != null) {
+ holder.filter.doFilter(wr, res, chain);
+ } else {
+ chain.doFilter(wr, res);
+ }
+ }
+
+ private void onDefault(PluginHolder holder,
+ HttpServletRequest req,
+ HttpServletResponse res) throws IOException {
+ String uri = req.getRequestURI();
+ String ctx = req.getContextPath();
+ String file = uri.substring(ctx.length() + 1);
+ if (file.startsWith("Documentation/") || file.startsWith("static/")) {
+ JarFile jar = holder.plugin.getJarFile();
+ JarEntry entry = jar.getJarEntry(file);
+ if (entry != null && entry.getSize() > 0) {
+ sendResource(jar, entry, res);
+ return;
+ }
+ }
+
+ noCache(res);
+ res.sendError(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ private void sendResource(JarFile jar, JarEntry entry, HttpServletResponse res)
+ throws IOException {
+ byte[] data = null;
+ if (entry.getSize() <= 128 * 1024) {
+ data = new byte[(int) entry.getSize()];
+ InputStream in = jar.getInputStream(entry);
+ try {
+ IO.readFully(in, data, 0, data.length);
+ } finally {
+ in.close();
+ }
+ }
+
+ String contentType = null;
+ Attributes atts = entry.getAttributes();
+ if (atts != null) {
+ contentType = Strings.emptyToNull(atts.getValue("Content-Type"));
+ }
+ if (contentType == null) {
+ MimeType type = mimeUtil.getMimeType(entry.getName(), data);
+ contentType = type.toString();
+ }
+
+ long time = entry.getTime();
+ if (0 < time) {
+ res.setDateHeader("Last-Modified", time);
+ }
+ res.setContentType(contentType);
+ res.setHeader("Content-Length", Long.toString(entry.getSize()));
+ if (data != null) {
+ res.getOutputStream().write(data);
+ } else {
+ InputStream in = jar.getInputStream(entry);
+ try {
+ OutputStream out = res.getOutputStream();
+ try {
+ byte[] tmp = new byte[1024];
+ int n;
+ while ((n = in.read(tmp)) > 0) {
+ out.write(tmp, 0, n);
}
- });
+ } finally {
+ out.close();
+ }
+ } finally {
+ in.close();
+ }
+ }
}
private static String extractName(HttpServletRequest req) {
@@ -161,6 +249,16 @@
res.setHeader("Content-Disposition", "attachment");
}
+ private static class PluginHolder {
+ final Plugin plugin;
+ final GuiceFilter filter;
+
+ PluginHolder(Plugin plugin, GuiceFilter filter) {
+ this.plugin = plugin;
+ this.filter = filter;
+ }
+ }
+
private static class WrappedRequest extends HttpServletRequestWrapper {
private final String contextPath;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
index 418dee9..e18d840 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -15,20 +15,31 @@
package com.google.gerrit.server.plugins;
import java.io.File;
+import java.io.IOException;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
+import java.util.jar.JarFile;
class CleanupHandle extends WeakReference<ClassLoader> {
private final File tmpFile;
+ private final JarFile jarFile;
- CleanupHandle(File jarFile,
+ CleanupHandle(File tmpFile,
+ JarFile jarFile,
ClassLoader ref,
ReferenceQueue<ClassLoader> queue) {
super(ref, queue);
- this.tmpFile = jarFile;
+ this.tmpFile = tmpFile;
+ this.jarFile = jarFile;
}
void cleanup() {
- tmpFile.delete();
+ try {
+ jarFile.close();
+ } catch (IOException err) {
+ }
+ if (!tmpFile.delete() && tmpFile.exists()) {
+ PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath());
+ }
}
}
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 497b0f5..9e8da32 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
@@ -27,6 +27,7 @@
import java.io.File;
import java.util.jar.Attributes;
+import java.util.jar.JarFile;
import java.util.jar.Manifest;
import javax.annotation.Nullable;
@@ -40,9 +41,10 @@
}
private final String name;
- private final File jar;
- private final Manifest manifest;
+ private final File srcJar;
private final FileSnapshot snapshot;
+ private final JarFile jarFile;
+ private final Manifest manifest;
private Class<? extends Module> sysModule;
private Class<? extends Module> sshModule;
private Class<? extends Module> httpModule;
@@ -53,23 +55,25 @@
private LifecycleManager manager;
public Plugin(String name,
- File jar,
- Manifest manifest,
+ File srcJar,
FileSnapshot snapshot,
+ JarFile jarFile,
+ Manifest manifest,
@Nullable Class<? extends Module> sysModule,
@Nullable Class<? extends Module> sshModule,
@Nullable Class<? extends Module> httpModule) {
this.name = name;
- this.jar = jar;
- this.manifest = manifest;
+ this.srcJar = srcJar;
this.snapshot = snapshot;
+ this.jarFile = jarFile;
+ this.manifest = manifest;
this.sysModule = sysModule;
this.sshModule = sshModule;
this.httpModule = httpModule;
}
- File getJar() {
- return jar;
+ File getSrcJar() {
+ return srcJar;
}
public String getName() {
@@ -151,6 +155,10 @@
}
}
+ public JarFile getJarFile() {
+ return jarFile;
+ }
+
@Nullable
public Injector getSshInjector() {
return sshInjector;
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 b0b0667..330dc46 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
@@ -169,7 +169,7 @@
log.info(String.format("Disabling plugin %s", name));
File off = new File(pluginsDir, active.getName() + ".jar.disabled");
- active.getJar().renameTo(off);
+ active.getSrcJar().renameTo(off);
active.stop();
running.remove(name);
@@ -304,34 +304,45 @@
return 0 < ext ? name.substring(0, ext) : name;
}
- private Plugin loadPlugin(String name, File jarFile, FileSnapshot snapshot)
+ private Plugin loadPlugin(String name, File srcJar, FileSnapshot snapshot)
throws IOException, ClassNotFoundException {
File tmp;
- FileInputStream in = new FileInputStream(jarFile);
+ FileInputStream in = new FileInputStream(srcJar);
try {
tmp = asTemp(in, tempNameFor(name), ".jar", tmpDir);
} finally {
in.close();
}
- Manifest manifest = new JarFile(tmp).getManifest();
- Attributes main = manifest.getMainAttributes();
- String sysName = main.getValue("Gerrit-Module");
- String sshName = main.getValue("Gerrit-SshModule");
- String httpName = main.getValue("Gerrit-HttpModule");
+ JarFile jarFile = new JarFile(tmp);
+ boolean keep = false;
+ try {
+ Manifest manifest = jarFile.getManifest();
+ Attributes main = manifest.getMainAttributes();
+ String sysName = main.getValue("Gerrit-Module");
+ String sshName = main.getValue("Gerrit-SshModule");
+ String httpName = main.getValue("Gerrit-HttpModule");
- URL[] urls = {tmp.toURI().toURL()};
- ClassLoader parentLoader = PluginLoader.class.getClassLoader();
- ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
- cleanupHandles.put(
- new CleanupHandle(tmp, pluginLoader, cleanupQueue),
- Boolean.TRUE);
+ URL[] urls = {tmp.toURI().toURL()};
+ ClassLoader parentLoader = PluginLoader.class.getClassLoader();
+ ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
+ cleanupHandles.put(
+ new CleanupHandle(tmp, jarFile, pluginLoader, cleanupQueue),
+ Boolean.TRUE);
- Class<? extends Module> sysModule = load(sysName, pluginLoader);
- Class<? extends Module> sshModule = load(sshName, pluginLoader);
- Class<? extends Module> httpModule = load(httpName, pluginLoader);
- return new Plugin(name, jarFile, manifest, snapshot,
- sysModule, sshModule, httpModule);
+ Class<? extends Module> sysModule = load(sysName, pluginLoader);
+ Class<? extends Module> sshModule = load(sshName, pluginLoader);
+ Class<? extends Module> httpModule = load(httpName, pluginLoader);
+ keep = true;
+ return new Plugin(name,
+ srcJar, snapshot,
+ jarFile, manifest,
+ sysModule, sshModule, httpModule);
+ } finally {
+ if (!keep) {
+ jarFile.close();
+ }
+ }
}
private static String tempNameFor(String name) {