diff options
Diffstat (limited to 'java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java')
-rw-r--r-- | java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java | 736 |
1 files changed, 736 insertions, 0 deletions
diff --git a/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java new file mode 100644 index 0000000000..74cadd3b9a --- /dev/null +++ b/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java @@ -0,0 +1,736 @@ +// Copyright (C) 2012 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.httpd.plugins; + +import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS; +import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS; +import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; +import static com.google.common.net.HttpHeaders.ORIGIN; +import static com.google.common.net.HttpHeaders.VARY; +import static com.google.gerrit.common.FileUtil.lastModified; +import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CHARACTER_ENCODING; +import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CONTENT_TYPE; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toList; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.cache.Cache; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.flogger.FluentLogger; +import com.google.common.io.ByteStreams; +import com.google.common.net.HttpHeaders; +import com.google.gerrit.extensions.registration.RegistrationHandle; +import com.google.gerrit.httpd.resources.Resource; +import com.google.gerrit.httpd.resources.ResourceKey; +import com.google.gerrit.httpd.resources.SmallResource; +import com.google.gerrit.httpd.restapi.RestApiServlet; +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.mime.MimeUtilFileTypeRegistry; +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.StartPluginListener; +import com.google.gerrit.server.ssh.SshInfo; +import com.google.gerrit.util.http.CacheHeaders; +import com.google.gerrit.util.http.RequestUtil; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import com.google.inject.servlet.GuiceFilter; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Predicate; +import java.util.jar.Attributes; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.servlet.FilterChain; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang.StringUtils; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.RawParseUtils; + +@Singleton +class HttpPluginServlet extends HttpServlet implements StartPluginListener, ReloadPluginListener { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private static final int SMALL_RESOURCE = 128 * 1024; + private static final long serialVersionUID = 1L; + + private final MimeUtilFileTypeRegistry mimeUtil; + private final Provider<String> webUrl; + private final Cache<ResourceKey, Resource> resourceCache; + private final String sshHost; + private final int sshPort; + private final RestApiServlet managerApi; + + private List<Plugin> pending = new ArrayList<>(); + private ContextMapper wrapper; + private final ConcurrentMap<String, PluginHolder> plugins = Maps.newConcurrentMap(); + private final Pattern allowOrigin; + + @Inject + HttpPluginServlet( + MimeUtilFileTypeRegistry mimeUtil, + @CanonicalWebUrl Provider<String> webUrl, + @Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache, + SshInfo sshInfo, + RestApiServlet.Globals globals, + PluginsCollection plugins, + @GerritServerConfig Config cfg) { + this.mimeUtil = mimeUtil; + this.webUrl = webUrl; + this.resourceCache = cache; + this.managerApi = new RestApiServlet(globals, plugins); + + String sshHost = "review.example.com"; + int sshPort = 29418; + if (!sshInfo.getHostKeys().isEmpty()) { + String host = sshInfo.getHostKeys().get(0).getHost(); + int c = host.lastIndexOf(':'); + if (0 <= c) { + sshHost = host.substring(0, c); + sshPort = Integer.parseInt(host.substring(c + 1)); + } else { + sshHost = host; + sshPort = 22; + } + } + this.sshHost = sshHost; + this.sshPort = sshPort; + this.allowOrigin = makeAllowOrigin(cfg); + } + + @Override + public synchronized void init(ServletConfig config) throws ServletException { + super.init(config); + + wrapper = new ContextMapper(config.getServletContext().getContextPath()); + for (Plugin plugin : pending) { + install(plugin); + } + pending = null; + } + + @Override + public synchronized void onStartPlugin(Plugin plugin) { + if (pending != null) { + pending.add(plugin); + } else { + install(plugin); + } + } + + @Override + public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) { + 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) { + if (plugin.getHttpInjector() != null) { + final String name = plugin.getName(); + final GuiceFilter filter; + try { + filter = plugin.getHttpInjector().getInstance(GuiceFilter.class); + } catch (RuntimeException e) { + logger.atWarning().withCause(e).log("Plugin %s cannot load GuiceFilter", name); + return null; + } + + try { + ServletContext ctx = PluginServletContext.create(plugin, wrapper.getFullPath(name)); + filter.init(new WrappedFilterConfig(ctx)); + } catch (ServletException e) { + logger.atWarning().withCause(e).log("Plugin %s failed to initialize HTTP", name); + return null; + } + + plugin.add(filter::destroy); + return filter; + } + return null; + } + + @Override + public void service(HttpServletRequest req, HttpServletResponse res) + throws IOException, ServletException { + List<String> parts = + Lists.newArrayList( + Splitter.on('/') + .limit(3) + .omitEmptyStrings() + .split(Strings.nullToEmpty(RequestUtil.getEncodedPathInfo(req)))); + + if (isApiCall(req, parts)) { + managerApi.service(req, res); + return; + } + + String name = parts.get(0); + final PluginHolder holder = plugins.get(name); + if (holder == null) { + CacheHeaders.setNotCacheable(res); + res.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + HttpServletRequest wr = wrapper.create(req, 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 static boolean isApiCall(HttpServletRequest req, List<String> parts) { + String method = req.getMethod(); + int cnt = parts.size(); + return cnt == 0 + || (cnt == 1 && ("PUT".equals(method) || "DELETE".equals(method))) + || (cnt == 2 && parts.get(1).startsWith("gerrit~")); + } + + private void onDefault(PluginHolder holder, HttpServletRequest req, HttpServletResponse res) + throws IOException { + if (!"GET".equals(req.getMethod()) && !"HEAD".equals(req.getMethod())) { + CacheHeaders.setNotCacheable(res); + res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + + String pathInfo = RequestUtil.getEncodedPathInfo(req); + if (pathInfo.length() < 1) { + Resource.NOT_FOUND.send(req, res); + return; + } + + checkCors(req, res); + + String file = pathInfo.substring(1); + PluginResourceKey key = PluginResourceKey.create(holder.plugin, file); + Resource rsc = resourceCache.getIfPresent(key); + if (rsc != null && req.getHeader(HttpHeaders.IF_MODIFIED_SINCE) == null) { + rsc.send(req, res); + return; + } + + String uri = req.getRequestURI(); + if ("".equals(file)) { + res.sendRedirect(uri + holder.docPrefix + "index.html"); + return; + } + + if (file.startsWith(holder.staticPrefix)) { + 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()) { + if (hasUpToDateCachedResource(rsc, entry.get().getTime())) { + rsc.send(req, res); + } else { + sendResource(scanner, entry.get(), key, res); + } + } else { + resourceCache.put(key, Resource.NOT_FOUND); + Resource.NOT_FOUND.send(req, res); + } + } + } else if (file.equals(holder.docPrefix.substring(0, holder.docPrefix.length() - 1))) { + res.sendRedirect(uri + "/index.html"); + } else if (file.startsWith(holder.docPrefix) && file.endsWith("/")) { + res.sendRedirect(uri + "index.html"); + } else if (file.startsWith(holder.docPrefix)) { + PluginContentScanner scanner = holder.plugin.getContentScanner(); + Optional<PluginEntry> entry = scanner.getEntry(file); + if (!entry.isPresent()) { + entry = findSource(scanner, file); + } + if (!entry.isPresent() && file.endsWith("/index.html")) { + String pfx = file.substring(0, file.length() - "index.html".length()); + long pluginLastModified = lastModified(holder.plugin.getSrcFile()); + if (hasUpToDateCachedResource(rsc, pluginLastModified)) { + rsc.send(req, res); + } else { + sendAutoIndex(scanner, pfx, holder.plugin.getName(), key, res, pluginLastModified); + } + } else if (entry.isPresent() && entry.get().getName().endsWith(".md")) { + if (hasUpToDateCachedResource(rsc, entry.get().getTime())) { + rsc.send(req, res); + } else { + sendMarkdownAsHtml(scanner, entry.get(), holder.plugin.getName(), key, res); + } + } else if (entry.isPresent()) { + if (hasUpToDateCachedResource(rsc, entry.get().getTime())) { + rsc.send(req, res); + } else { + sendResource(scanner, entry.get(), key, res); + } + } else { + resourceCache.put(key, Resource.NOT_FOUND); + Resource.NOT_FOUND.send(req, res); + } + } else { + resourceCache.put(key, Resource.NOT_FOUND); + Resource.NOT_FOUND.send(req, res); + } + } + + private static Pattern makeAllowOrigin(Config cfg) { + String[] allow = cfg.getStringList("site", null, "allowOriginRegex"); + if (allow.length > 0) { + return Pattern.compile(Joiner.on('|').join(allow)); + } + return null; + } + + private void checkCors(HttpServletRequest req, HttpServletResponse res) { + String origin = req.getHeader(ORIGIN); + if (!Strings.isNullOrEmpty(origin) && isOriginAllowed(origin)) { + res.addHeader(VARY, ORIGIN); + setCorsHeaders(res, origin); + } + } + + private void setCorsHeaders(HttpServletResponse res, String origin) { + res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + res.setHeader(ACCESS_CONTROL_ALLOW_METHODS, "GET, HEAD"); + } + + private boolean isOriginAllowed(String origin) { + return allowOrigin == null || allowOrigin.matcher(origin).matches(); + } + + private boolean hasUpToDateCachedResource(Resource cachedResource, long lastUpdateTime) { + return cachedResource != null && cachedResource.isUnchanged(lastUpdateTime); + } + + 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 (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(scanner, entry); + if (Strings.isNullOrEmpty(entryTitle)) { + entryTitle = rsrc.substring(nameOffset, rsrc.length() - 3).replace('-', ' '); + } + } else { + entryTitle = rsrc.substring(nameOffset).replace('-', ' '); + } + md.append(String.format("* [%s](%s)\n", entryTitle, rsrc)); + } + md.append("\n"); + } + } + + private void sendAutoIndex( + PluginContentScanner scanner, + final String prefix, + final String pluginName, + PluginResourceKey cacheKey, + HttpServletResponse res, + long lastModifiedTime) + throws IOException { + List<PluginEntry> cmds = new ArrayList<>(); + List<PluginEntry> servlets = new ArrayList<>(); + List<PluginEntry> restApis = new ArrayList<>(); + List<PluginEntry> docs = new ArrayList<>(); + PluginEntry about = null; + + Predicate<PluginEntry> filter = + entry -> { + String name = entry.getName(); + Optional<Long> size = entry.getSize(); + if (name.startsWith(prefix) + && (name.endsWith(".md") || name.endsWith(".html")) + && size.isPresent()) { + if (size.get() <= 0 || size.get() > SMALL_RESOURCE) { + logger.atWarning().log( + "Plugin %s: %s omitted from document index. " + "Size %d out of range (0,%d).", + pluginName, name.substring(prefix.length()), size.get(), SMALL_RESOURCE); + return false; + } + return true; + } + return false; + }; + + List<PluginEntry> entries = + Collections.list(scanner.entries()).stream().filter(filter).collect(toList()); + for (PluginEntry entry : entries) { + String name = entry.getName().substring(prefix.length()); + if (name.startsWith("cmd-")) { + cmds.add(entry); + } else if (name.startsWith("servlet-")) { + servlets.add(entry); + } else if (name.startsWith("rest-api-")) { + restApis.add(entry); + } else if (name.startsWith("about.")) { + if (about == null) { + about = entry; + } else { + logger.atWarning().log( + "Plugin %s: Multiple 'about' documents found; using %s", + pluginName, about.getName().substring(prefix.length())); + } + } else { + docs.add(entry); + } + } + + cmds.sort(PluginEntry.COMPARATOR_BY_NAME); + docs.sort(PluginEntry.COMPARATOR_BY_NAME); + + StringBuilder md = new StringBuilder(); + md.append(String.format("# Plugin %s #\n", pluginName)); + md.append("\n"); + appendPluginInfoTable(md, scanner.getManifest().getMainAttributes()); + + if (about != null) { + InputStreamReader isr = new InputStreamReader(scanner.getInputStream(about), UTF_8); + StringBuilder aboutContent = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(isr)) { + String line; + while ((line = reader.readLine()) != null) { + line = StringUtils.stripEnd(line, null); + if (line.isEmpty()) { + aboutContent.append("\n"); + } else { + aboutContent.append(line).append("\n"); + } + } + } + + // Only append the About section if there was anything in it + if (aboutContent.toString().trim().length() > 0) { + md.append("## About ##\n"); + md.append("\n").append(aboutContent); + } + } + + 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); + } + + private void sendMarkdownAsHtml( + String md, + String pluginName, + PluginResourceKey cacheKey, + HttpServletResponse res, + long lastModifiedTime) + throws UnsupportedEncodingException, IOException { + Map<String, String> macros = new HashMap<>(); + macros.put("PLUGIN", pluginName); + macros.put("SSH_HOST", sshHost); + macros.put("SSH_PORT", "" + sshPort); + String url = webUrl.get(); + if (Strings.isNullOrEmpty(url)) { + url = "http://review.example.com/"; + } + macros.put("URL", url); + + Matcher m = Pattern.compile("(\\\\)?@([A-Z_]+)@").matcher(md); + StringBuffer sb = new StringBuffer(); + while (m.find()) { + String key = m.group(2); + String val = macros.get(key); + if (m.group(1) != null) { + m.appendReplacement(sb, "@" + key + "@"); + } else if (val != null) { + m.appendReplacement(sb, val); + } else { + m.appendReplacement(sb, "@" + key + "@"); + } + } + m.appendTail(sb); + + byte[] html = new MarkdownFormatter().markdownToDocHtml(sb.toString(), UTF_8.name()); + resourceCache.put( + cacheKey, + new SmallResource(html) + .setContentType("text/html") + .setCharacterEncoding(UTF_8.name()) + .setLastModified(lastModifiedTime)); + res.setContentType("text/html"); + res.setCharacterEncoding(UTF_8.name()); + res.setContentLength(html.length); + res.setDateHeader("Last-Modified", lastModifiedTime); + res.getOutputStream().write(html); + } + + private static void appendPluginInfoTable(StringBuilder html, Attributes main) { + if (main != null) { + String t = main.getValue(Attributes.Name.IMPLEMENTATION_TITLE); + String n = main.getValue(Attributes.Name.IMPLEMENTATION_VENDOR); + String v = main.getValue(Attributes.Name.IMPLEMENTATION_VERSION); + String a = main.getValue("Gerrit-ApiVersion"); + + html.append("<table class=\"plugin_info\">"); + if (!Strings.isNullOrEmpty(t)) { + html.append("<tr><th>Name</th><td>").append(t).append("</td></tr>\n"); + } + if (!Strings.isNullOrEmpty(n)) { + html.append("<tr><th>Vendor</th><td>").append(n).append("</td></tr>\n"); + } + if (!Strings.isNullOrEmpty(v)) { + html.append("<tr><th>Version</th><td>").append(v).append("</td></tr>\n"); + } + if (!Strings.isNullOrEmpty(a)) { + html.append("<tr><th>API Version</th><td>").append(a).append("</td></tr>\n"); + } + html.append("</table>\n"); + } + } + + private static String extractTitleFromMarkdown(PluginContentScanner scanner, PluginEntry entry) + throws IOException { + String charEnc = null; + Map<Object, String> atts = entry.getAttrs(); + if (atts != null) { + charEnc = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING)); + } + if (charEnc == null) { + charEnc = UTF_8.name(); + } + return new MarkdownFormatter() + .extractTitleFromMarkdown(readWholeEntry(scanner, entry), charEnc); + } + + private static Optional<PluginEntry> findSource(PluginContentScanner scanner, String file) + throws IOException { + if (file.endsWith(".html")) { + int d = file.lastIndexOf('.'); + return scanner.getEntry(file.substring(0, d) + ".md"); + } + return Optional.empty(); + } + + private void sendMarkdownAsHtml( + PluginContentScanner scanner, + PluginEntry entry, + String pluginName, + PluginResourceKey key, + HttpServletResponse res) + throws IOException { + byte[] rawmd = readWholeEntry(scanner, entry); + String encoding = null; + Map<Object, String> atts = entry.getAttrs(); + if (atts != null) { + encoding = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING)); + } + + String txtmd = + RawParseUtils.decode(Charset.forName(encoding != null ? encoding : UTF_8.name()), rawmd); + long time = entry.getTime(); + if (0 < time) { + res.setDateHeader("Last-Modified", time); + } + sendMarkdownAsHtml(txtmd, pluginName, key, res, time); + } + + private void sendResource( + PluginContentScanner scanner, + PluginEntry entry, + PluginResourceKey key, + HttpServletResponse res) + throws IOException { + byte[] data = null; + Optional<Long> size = entry.getSize(); + if (size.isPresent() && size.get() <= SMALL_RESOURCE) { + data = readWholeEntry(scanner, entry); + } + + String contentType = null; + String charEnc = null; + Map<Object, String> atts = entry.getAttrs(); + if (atts != null) { + 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(); + if ("application/octet-stream".equals(contentType) && entry.getName().endsWith(".js")) { + contentType = "application/javascript"; + } else if ("application/x-pointplus".equals(contentType) + && entry.getName().endsWith(".css")) { + contentType = "text/css"; + } + } + + long time = entry.getTime(); + if (0 < time) { + res.setDateHeader("Last-Modified", time); + } + if (size.isPresent()) { + res.setHeader("Content-Length", size.get().toString()); + } + res.setContentType(contentType); + if (charEnc != null) { + res.setCharacterEncoding(charEnc); + } + if (data != null) { + resourceCache.put( + key, + new SmallResource(data) + .setContentType(contentType) + .setCharacterEncoding(charEnc) + .setLastModified(time)); + res.getOutputStream().write(data); + } else { + writeToResponse(res, scanner.getInputStream(entry)); + } + } + + private void sendJsPlugin( + Plugin plugin, PluginResourceKey key, HttpServletRequest req, HttpServletResponse res) + throws IOException { + Path path = plugin.getSrcFile(); + if (req.getRequestURI().endsWith(getJsPluginPath(plugin)) && Files.exists(path)) { + res.setHeader("Content-Length", Long.toString(Files.size(path))); + if (path.toString().toLowerCase(Locale.US).endsWith(".html")) { + res.setContentType("text/html"); + } else { + res.setContentType("application/javascript"); + } + writeToResponse(res, Files.newInputStream(path)); + } else { + resourceCache.put(key, Resource.NOT_FOUND); + Resource.NOT_FOUND.send(req, res); + } + } + + private static String getJsPluginPath(Plugin plugin) { + return String.format( + "/plugins/%s/static/%s", plugin.getName(), plugin.getSrcFile().getFileName()); + } + + private void writeToResponse(HttpServletResponse res, InputStream inputStream) + throws IOException { + try (InputStream in = inputStream; + OutputStream out = res.getOutputStream()) { + ByteStreams.copy(in, out); + } + } + + private static byte[] readWholeEntry(PluginContentScanner scanner, PluginEntry entry) + throws IOException { + try (InputStream in = scanner.getInputStream(entry)) { + return IO.readWholeStream(in, entry.getSize().get().intValue()).array(); + } + } + + private static class PluginHolder { + final Plugin plugin; + final GuiceFilter filter; + final String staticPrefix; + final String docPrefix; + + PluginHolder(Plugin plugin, GuiceFilter filter) { + this.plugin = plugin; + this.filter = filter; + this.staticPrefix = getPrefix(plugin, "Gerrit-HttpStaticPrefix", "static/"); + this.docPrefix = getPrefix(plugin, "Gerrit-HttpDocumentationPrefix", "Documentation/"); + } + + private static String getPrefix(Plugin plugin, String attr, String def) { + Path path = plugin.getSrcFile(); + PluginContentScanner scanner = plugin.getContentScanner(); + if (path == null || scanner == PluginContentScanner.EMPTY) { + return def; + } + try { + String prefix = scanner.getManifest().getMainAttributes().getValue(attr); + if (prefix != null) { + return CharMatcher.is('/').trimFrom(prefix) + "/"; + } + return def; + } catch (IOException e) { + logger.atWarning().withCause(e).log( + "Error getting %s for plugin %s, using default", attr, plugin.getName()); + return null; + } + } + } +} |