Serve static resources for non-jar Server plugins

Abstract from the Server plugins external packaging
and allows to serve static resources from any
form of Server plugin that exposes a PluginContentScanner.

This allows potentially other forms of plugins
(e.g. Scripting, directory-based or any other) to
provide their on-line documentation and serve their
static resources.

Change-Id: I80b8159cf87255e3132298f5863b374b5bd44a6c
This commit is contained in:
Luca Milanesio
2014-04-28 23:54:23 +01:00
parent aff2e43291
commit dacbec6f59
3 changed files with 100 additions and 95 deletions

View File

@@ -14,7 +14,11 @@
package com.google.gerrit.httpd.plugins; package com.google.gerrit.httpd.plugins;
import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CHARACTER_ENCODING;
import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CONTENT_TYPE;
import com.google.common.base.CharMatcher; import com.google.common.base.CharMatcher;
import com.google.common.base.Optional;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
@@ -27,9 +31,11 @@ import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.documentation.MarkdownFormatter; import com.google.gerrit.server.documentation.MarkdownFormatter;
import com.google.gerrit.server.plugins.Plugin; import com.google.gerrit.server.plugins.Plugin;
import com.google.gerrit.server.plugins.Plugin.ApiType;
import com.google.gerrit.server.plugins.PluginContentScanner;
import com.google.gerrit.server.plugins.PluginEntry;
import com.google.gerrit.server.plugins.PluginsCollection; import com.google.gerrit.server.plugins.PluginsCollection;
import com.google.gerrit.server.plugins.ReloadPluginListener; import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.plugins.ServerPlugin;
import com.google.gerrit.server.plugins.StartPluginListener; import com.google.gerrit.server.plugins.StartPluginListener;
import com.google.gerrit.server.ssh.SshInfo; import com.google.gerrit.server.ssh.SshInfo;
import com.google.gwtexpui.server.CacheHeaders; import com.google.gwtexpui.server.CacheHeaders;
@@ -55,14 +61,11 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.jar.Attributes; import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -83,13 +86,6 @@ class HttpPluginServlet extends HttpServlet
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final Logger log private static final Logger log
= LoggerFactory.getLogger(HttpPluginServlet.class); = LoggerFactory.getLogger(HttpPluginServlet.class);
private static final Comparator<JarEntry> JAR_ENTRY_COMPARATOR_BY_NAME =
new Comparator<JarEntry>() {
@Override
public int compare(JarEntry a, JarEntry b) {
return a.getName().compareTo(b.getName());
}
};
private final MimeUtilFileTypeRegistry mimeUtil; private final MimeUtilFileTypeRegistry mimeUtil;
private final Provider<String> webUrl; private final Provider<String> webUrl;
@@ -276,17 +272,17 @@ class HttpPluginServlet extends HttpServlet
} }
if (file.startsWith(holder.staticPrefix)) { if (file.startsWith(holder.staticPrefix)) {
JarFile jar = jarFileOf(holder.plugin); if (holder.plugin.getApiType() == ApiType.JS) {
if (jar != null) { sendJsPlugin(holder.plugin, key, req, res);
JarEntry entry = jar.getJarEntry(file); } else {
if (exists(entry)) { PluginContentScanner scanner = holder.plugin.getContentScanner();
sendResource(jar, entry, key, res); Optional<PluginEntry> entry = scanner.getEntry(file);
if (entry.isPresent()) {
sendResource(scanner, entry.get(), key, res);
} else { } else {
resourceCache.put(key, Resource.NOT_FOUND); resourceCache.put(key, Resource.NOT_FOUND);
Resource.NOT_FOUND.send(req, res); Resource.NOT_FOUND.send(req, res);
} }
} else {
sendJsPlugin(holder.plugin, key, req, res);
} }
} else if (file.equals( } else if (file.equals(
holder.docPrefix.substring(0, holder.docPrefix.length() - 1))) { holder.docPrefix.substring(0, holder.docPrefix.length() - 1))) {
@@ -294,19 +290,19 @@ class HttpPluginServlet extends HttpServlet
} else if (file.startsWith(holder.docPrefix) && file.endsWith("/")) { } else if (file.startsWith(holder.docPrefix) && file.endsWith("/")) {
res.sendRedirect(uri + "index.html"); res.sendRedirect(uri + "index.html");
} else if (file.startsWith(holder.docPrefix)) { } else if (file.startsWith(holder.docPrefix)) {
JarFile jar = jarFileOf(holder.plugin); PluginContentScanner scanner = holder.plugin.getContentScanner();
JarEntry entry = jar.getJarEntry(file); Optional<PluginEntry> entry = scanner.getEntry(file);
if (!exists(entry)) { if (!entry.isPresent()) {
entry = findSource(jar, file); entry = findSource(scanner, file);
} }
if (!exists(entry) && file.endsWith("/index.html")) { if (!entry.isPresent() && file.endsWith("/index.html")) {
String pfx = file.substring(0, file.length() - "index.html".length()); String pfx = file.substring(0, file.length() - "index.html".length());
sendAutoIndex(jar, pfx, holder.plugin.getName(), key, res, sendAutoIndex(scanner, pfx, holder.plugin.getName(), key, res,
holder.plugin.getSrcFile().lastModified()); holder.plugin.getSrcFile().lastModified());
} else if (exists(entry) && entry.getName().endsWith(".md")) { } else if (entry.isPresent() && entry.get().getName().endsWith(".md")) {
sendMarkdownAsHtml(jar, entry, holder.plugin.getName(), key, res); sendMarkdownAsHtml(scanner, entry.get(), holder.plugin.getName(), key, res);
} else if (exists(entry)) { } else if (entry.isPresent()) {
sendResource(jar, entry, key, res); sendResource(scanner, entry.get(), key, res);
} else { } else {
resourceCache.put(key, Resource.NOT_FOUND); resourceCache.put(key, Resource.NOT_FOUND);
Resource.NOT_FOUND.send(req, res); Resource.NOT_FOUND.send(req, res);
@@ -317,18 +313,18 @@ class HttpPluginServlet extends HttpServlet
} }
} }
private void appendEntriesSection(JarFile jar, List<JarEntry> entries, private void appendEntriesSection(PluginContentScanner scanner, List<PluginEntry> entries,
String sectionTitle, StringBuilder md, String prefix, String sectionTitle, StringBuilder md, String prefix,
int nameOffset) throws IOException { int nameOffset) throws IOException {
if (!entries.isEmpty()) { if (!entries.isEmpty()) {
md.append("## ").append(sectionTitle).append(" ##\n"); md.append("## ").append(sectionTitle).append(" ##\n");
for(JarEntry entry : entries) { for(PluginEntry entry : entries) {
String rsrc = entry.getName().substring(prefix.length()); String rsrc = entry.getName().substring(prefix.length());
String entryTitle; String entryTitle;
if (rsrc.endsWith(".html")) { if (rsrc.endsWith(".html")) {
entryTitle = rsrc.substring(nameOffset, rsrc.length() - 5).replace('-', ' '); entryTitle = rsrc.substring(nameOffset, rsrc.length() - 5).replace('-', ' ');
} else if (rsrc.endsWith(".md")) { } else if (rsrc.endsWith(".md")) {
entryTitle = extractTitleFromMarkdown(jar, entry); entryTitle = extractTitleFromMarkdown(scanner, entry);
if (Strings.isNullOrEmpty(entryTitle)) { if (Strings.isNullOrEmpty(entryTitle)) {
entryTitle = rsrc.substring(nameOffset, rsrc.length() - 3).replace('-', ' '); entryTitle = rsrc.substring(nameOffset, rsrc.length() - 3).replace('-', ' ');
} }
@@ -342,24 +338,25 @@ class HttpPluginServlet extends HttpServlet
} }
} }
private void sendAutoIndex(JarFile jar, private void sendAutoIndex(PluginContentScanner scanner,
String prefix, String pluginName, String prefix, String pluginName,
ResourceKey cacheKey, HttpServletResponse res, long lastModifiedTime) ResourceKey cacheKey, HttpServletResponse res,long lastModifiedTime)
throws IOException { throws IOException {
List<JarEntry> cmds = Lists.newArrayList(); List<PluginEntry> cmds = Lists.newArrayList();
List<JarEntry> servlets = Lists.newArrayList(); List<PluginEntry> servlets = Lists.newArrayList();
List<JarEntry> restApis = Lists.newArrayList(); List<PluginEntry> restApis = Lists.newArrayList();
List<JarEntry> docs = Lists.newArrayList(); List<PluginEntry> docs = Lists.newArrayList();
JarEntry about = null; PluginEntry about = null;
Enumeration<JarEntry> entries = jar.entries(); Enumeration<PluginEntry> entries = scanner.entries();
while (entries.hasMoreElements()) { while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement(); PluginEntry entry = entries.nextElement();
String name = entry.getName(); String name = entry.getName();
long size = entry.getSize(); Optional<Long> size = entry.getSize();
if (name.startsWith(prefix) if (name.startsWith(prefix)
&& (name.endsWith(".md") && (name.endsWith(".md")
|| name.endsWith(".html")) || name.endsWith(".html"))
&& 0 < size && size <= SMALL_RESOURCE) { && size.isPresent()
&& 0 < size.get() && size.get() <= SMALL_RESOURCE) {
name = name.substring(prefix.length()); name = name.substring(prefix.length());
if (name.startsWith("cmd-")) { if (name.startsWith("cmd-")) {
cmds.add(entry); cmds.add(entry);
@@ -377,16 +374,16 @@ class HttpPluginServlet extends HttpServlet
} }
} }
Collections.sort(cmds, JAR_ENTRY_COMPARATOR_BY_NAME); Collections.sort(cmds, PluginEntry.COMPARATOR_BY_NAME);
Collections.sort(docs, JAR_ENTRY_COMPARATOR_BY_NAME); Collections.sort(docs, PluginEntry.COMPARATOR_BY_NAME);
StringBuilder md = new StringBuilder(); StringBuilder md = new StringBuilder();
md.append(String.format("# Plugin %s #\n", pluginName)); md.append(String.format("# Plugin %s #\n", pluginName));
md.append("\n"); md.append("\n");
appendPluginInfoTable(md, jar.getManifest().getMainAttributes()); appendPluginInfoTable(md, scanner.getManifest().getMainAttributes());
if (about != null) { if (about != null) {
InputStreamReader isr = new InputStreamReader(jar.getInputStream(about)); InputStreamReader isr = new InputStreamReader(scanner.getInputStream(about));
BufferedReader reader = new BufferedReader(isr); BufferedReader reader = new BufferedReader(isr);
StringBuilder aboutContent = new StringBuilder(); StringBuilder aboutContent = new StringBuilder();
String line; String line;
@@ -407,10 +404,10 @@ class HttpPluginServlet extends HttpServlet
} }
} }
appendEntriesSection(jar, docs, "Documentation", md, prefix, 0); appendEntriesSection(scanner, docs, "Documentation", md, prefix, 0);
appendEntriesSection(jar, servlets, "Servlets", md, prefix, "servlet-".length()); appendEntriesSection(scanner, servlets, "Servlets", md, prefix, "servlet-".length());
appendEntriesSection(jar, restApis, "REST APIs", md, prefix, "rest-api-".length()); appendEntriesSection(scanner, restApis, "REST APIs", md, prefix, "rest-api-".length());
appendEntriesSection(jar, cmds, "Commands", md, prefix, "cmd-".length()); appendEntriesSection(scanner, cmds, "Commands", md, prefix, "cmd-".length());
sendMarkdownAsHtml(md.toString(), pluginName, cacheKey, res, lastModifiedTime); sendMarkdownAsHtml(md.toString(), pluginName, cacheKey, res, lastModifiedTime);
} }
@@ -494,41 +491,38 @@ class HttpPluginServlet extends HttpServlet
} }
} }
private static String extractTitleFromMarkdown(JarFile jar, JarEntry entry) private static String extractTitleFromMarkdown(PluginContentScanner scanner, PluginEntry entry)
throws IOException { throws IOException {
String charEnc = null; String charEnc = null;
Attributes atts = entry.getAttributes(); Map<Object, String> atts = entry.getAttrs();
if (atts != null) { if (atts != null) {
charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding")); charEnc = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING));
} }
if (charEnc == null) { if (charEnc == null) {
charEnc = "UTF-8"; charEnc = "UTF-8";
} }
return new MarkdownFormatter().extractTitleFromMarkdown( return new MarkdownFormatter().extractTitleFromMarkdown(
readWholeEntry(jar, entry), readWholeEntry(scanner, entry),
charEnc); charEnc);
} }
private static JarEntry findSource(JarFile jar, String file) { private static Optional<PluginEntry> findSource(
PluginContentScanner scanner, String file) throws IOException {
if (file.endsWith(".html")) { if (file.endsWith(".html")) {
int d = file.lastIndexOf('.'); int d = file.lastIndexOf('.');
return jar.getJarEntry(file.substring(0, d) + ".md"); return scanner.getEntry(file.substring(0, d) + ".md");
} }
return null; return Optional.absent();
} }
private static boolean exists(JarEntry entry) { private void sendMarkdownAsHtml(PluginContentScanner scanner, PluginEntry entry,
return entry != null && entry.getSize() > 0;
}
private void sendMarkdownAsHtml(JarFile jar, JarEntry entry,
String pluginName, ResourceKey key, HttpServletResponse res) String pluginName, ResourceKey key, HttpServletResponse res)
throws IOException { throws IOException {
byte[] rawmd = readWholeEntry(jar, entry); byte[] rawmd = readWholeEntry(scanner, entry);
String encoding = null; String encoding = null;
Attributes atts = entry.getAttributes(); Map<Object, String> atts = entry.getAttrs();
if (atts != null) { if (atts != null) {
encoding = Strings.emptyToNull(atts.getValue("Character-Encoding")); encoding = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING));
} }
String txtmd = RawParseUtils.decode( String txtmd = RawParseUtils.decode(
@@ -541,20 +535,21 @@ class HttpPluginServlet extends HttpServlet
sendMarkdownAsHtml(txtmd, pluginName, key, res, time); sendMarkdownAsHtml(txtmd, pluginName, key, res, time);
} }
private void sendResource(JarFile jar, JarEntry entry, private void sendResource(PluginContentScanner scanner, PluginEntry entry,
ResourceKey key, HttpServletResponse res) ResourceKey key, HttpServletResponse res)
throws IOException { throws IOException {
byte[] data = null; byte[] data = null;
if (entry.getSize() <= SMALL_RESOURCE) { Optional<Long> size = entry.getSize();
data = readWholeEntry(jar, entry); if (size.isPresent() && size.get() <= SMALL_RESOURCE) {
data = readWholeEntry(scanner, entry);
} }
String contentType = null; String contentType = null;
String charEnc = null; String charEnc = null;
Attributes atts = entry.getAttributes(); Map<Object, String> atts = entry.getAttrs();
if (atts != null) { if (atts != null) {
contentType = Strings.emptyToNull(atts.getValue("Content-Type")); contentType = Strings.emptyToNull(atts.get(ATTR_CONTENT_TYPE));
charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding")); charEnc = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING));
} }
if (contentType == null) { if (contentType == null) {
contentType = mimeUtil.getMimeType(entry.getName(), data).toString(); contentType = mimeUtil.getMimeType(entry.getName(), data).toString();
@@ -568,7 +563,9 @@ class HttpPluginServlet extends HttpServlet
if (0 < time) { if (0 < time) {
res.setDateHeader("Last-Modified", time); res.setDateHeader("Last-Modified", time);
} }
res.setHeader("Content-Length", Long.toString(entry.getSize())); if (size.isPresent()) {
res.setHeader("Content-Length", size.get().toString());
}
res.setContentType(contentType); res.setContentType(contentType);
if (charEnc != null) { if (charEnc != null) {
res.setCharacterEncoding(charEnc); res.setCharacterEncoding(charEnc);
@@ -580,7 +577,7 @@ class HttpPluginServlet extends HttpServlet
.setLastModified(time)); .setLastModified(time));
res.getOutputStream().write(data); res.getOutputStream().write(data);
} else { } else {
writeToResponse(res, jar.getInputStream(entry)); writeToResponse(res, scanner.getInputStream(entry));
} }
} }
@@ -620,10 +617,10 @@ class HttpPluginServlet extends HttpServlet
} }
} }
private static byte[] readWholeEntry(JarFile jar, JarEntry entry) private static byte[] readWholeEntry(PluginContentScanner scanner, PluginEntry entry)
throws IOException { throws IOException {
byte[] data = new byte[(int) entry.getSize()]; byte[] data = new byte[entry.getSize().get().intValue()];
InputStream in = jar.getInputStream(entry); InputStream in = scanner.getInputStream(entry);
try { try {
IO.readFully(in, data, 0, data.length); IO.readFully(in, data, 0, data.length);
} finally { } finally {
@@ -632,14 +629,6 @@ class HttpPluginServlet extends HttpServlet
return data; return data;
} }
private static JarFile jarFileOf(Plugin plugin) {
if(plugin instanceof ServerPlugin) {
return ((ServerPlugin) plugin).getJarFile();
} else {
return null;
}
}
private static class PluginHolder { private static class PluginHolder {
final Plugin plugin; final Plugin plugin;
final GuiceFilter filter; final GuiceFilter filter;
@@ -656,13 +645,14 @@ class HttpPluginServlet extends HttpServlet
} }
private static String getPrefix(Plugin plugin, String attr, String def) { private static String getPrefix(Plugin plugin, String attr, String def) {
JarFile jarFile = jarFileOf(plugin); File srcFile = plugin.getSrcFile();
if (jarFile == null) { PluginContentScanner scanner = plugin.getContentScanner();
if (srcFile == null || scanner == PluginContentScanner.EMPTY) {
return def; return def;
} }
try { try {
String prefix = jarFile.getManifest().getMainAttributes() String prefix =
.getValue(attr); scanner.getManifest().getMainAttributes().getValue(attr);
if (prefix != null) { if (prefix != null) {
return CharMatcher.is('/').trimFrom(prefix) + "/"; return CharMatcher.is('/').trimFrom(prefix) + "/";
} else { } else {

View File

@@ -299,7 +299,7 @@ public class JarScanner implements PluginContentScanner {
private PluginEntry resourceOf(JarEntry jarEntry) throws IOException { private PluginEntry resourceOf(JarEntry jarEntry) throws IOException {
return new PluginEntry(jarEntry.getName(), jarEntry.getTime(), return new PluginEntry(jarEntry.getName(), jarEntry.getTime(),
jarEntry.getSize(), attributesOf(jarEntry)); Optional.of(jarEntry.getSize()), attributesOf(jarEntry));
} }
private Map<Object, String> attributesOf(JarEntry jarEntry) private Map<Object, String> attributesOf(JarEntry jarEntry)

View File

@@ -13,7 +13,10 @@
// limitations under the License. // limitations under the License.
package com.google.gerrit.server.plugins; package com.google.gerrit.server.plugins;
import com.google.common.base.Optional;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.Map; import java.util.Map;
/** /**
@@ -26,15 +29,23 @@ import java.util.Map;
public class PluginEntry { public class PluginEntry {
public static final String ATTR_CHARACTER_ENCODING = "Character-Encoding"; public static final String ATTR_CHARACTER_ENCODING = "Character-Encoding";
public static final String ATTR_CONTENT_TYPE = "Content-Type"; public static final String ATTR_CONTENT_TYPE = "Content-Type";
public static final Comparator<PluginEntry> COMPARATOR_BY_NAME =
new Comparator<PluginEntry>() {
@Override
public int compare(PluginEntry a, PluginEntry b) {
return a.getName().compareTo(b.getName());
}
};
private static final Map<Object,String> EMPTY_ATTRS = Collections.emptyMap(); private static final Map<Object, String> EMPTY_ATTRS = Collections.emptyMap();
private static final Optional<Long> NO_SIZE = Optional.absent();
private final String name; private final String name;
private final long time; private final long time;
private final long size; private final Optional<Long> size;
private final Map<Object, String> attrs; private final Map<Object, String> attrs;
public PluginEntry(String name, long time, long size, public PluginEntry(String name, long time, Optional<Long> size,
Map<Object, String> attrs) { Map<Object, String> attrs) {
this.name = name; this.name = name;
this.time = time; this.time = time;
@@ -42,10 +53,14 @@ public class PluginEntry {
this.attrs = attrs; this.attrs = attrs;
} }
public PluginEntry(String name, long time, long size) { public PluginEntry(String name, long time, Optional<Long> size) {
this(name, time, size, EMPTY_ATTRS); this(name, time, size, EMPTY_ATTRS);
} }
public PluginEntry(String name, long time) {
this(name, time, NO_SIZE, EMPTY_ATTRS);
}
public String getName() { public String getName() {
return name; return name;
} }
@@ -54,11 +69,11 @@ public class PluginEntry {
return time; return time;
} }
public long getSize() { public Optional<Long> getSize() {
return size; return size;
} }
public Map<Object, String> getAttrs() { public Map<Object, String> getAttrs() {
return attrs; return attrs;
} }
} }