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
This commit is contained in:
@@ -17,19 +17,29 @@ package com.google.gerrit.httpd.plugins;
|
||||
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 @@ class HttpPluginServlet extends HttpServlet
|
||||
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 @@ class HttpPluginServlet extends HttpServlet
|
||||
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 @@ class HttpPluginServlet extends HttpServlet
|
||||
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 @@ class HttpPluginServlet extends HttpServlet
|
||||
plugin.add(new RegistrationHandle() {
|
||||
@Override
|
||||
public void remove() {
|
||||
try {
|
||||
filter.destroy();
|
||||
} finally {
|
||||
plugins.remove(name, filter);
|
||||
}
|
||||
filter.destroy();
|
||||
}
|
||||
});
|
||||
return filter;
|
||||
@@ -126,23 +142,95 @@ class HttpPluginServlet extends HttpServlet
|
||||
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 @@ class HttpPluginServlet extends HttpServlet
|
||||
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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.eclipse.jgit.storage.file.FileSnapshot;
|
||||
|
||||
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 @@ public class Plugin {
|
||||
}
|
||||
|
||||
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 @@ public class Plugin {
|
||||
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 class Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
public JarFile getJarFile() {
|
||||
return jarFile;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Injector getSshInjector() {
|
||||
return sshInjector;
|
||||
|
||||
@@ -169,7 +169,7 @@ public class PluginLoader implements LifecycleListener {
|
||||
|
||||
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 @@ public class PluginLoader implements LifecycleListener {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user