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

View File

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

View File

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