// 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.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.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(() -> 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 =
        (sreq, sres) -> onDefault(holder, (HttpServletRequest) sreq, (HttpServletResponse) sres);
    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;
      }
    }
  }
}
