All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.httpd.plugins.HttpPluginServlet Maven / Gradle / Ivy

// 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.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;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.net.HttpHeaders;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.httpd.restapi.RestApiServlet;
import com.google.gerrit.server.MimeUtilFileTypeRegistry;
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.StartPluginListener;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gwtexpui.server.CacheHeaders;
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 org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
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.util.Collections;
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.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;

@Singleton
class HttpPluginServlet extends HttpServlet
    implements StartPluginListener, ReloadPluginListener {
  private static final int SMALL_RESOURCE = 128 * 1024;
  private static final long serialVersionUID = 1L;
  private static final Logger log
      = LoggerFactory.getLogger(HttpPluginServlet.class);

  private final MimeUtilFileTypeRegistry mimeUtil;
  private final Provider webUrl;
  private final Cache resourceCache;
  private final String sshHost;
  private final int sshPort;
  private final RestApiServlet managerApi;

  private List pending = Lists.newArrayList();
  private ContextMapper wrapper;
  private final ConcurrentMap plugins
      = Maps.newConcurrentMap();

  @Inject
  HttpPluginServlet(
      MimeUtilFileTypeRegistry mimeUtil,
      @CanonicalWebUrl Provider webUrl,
      @Named(HttpPluginModule.PLUGIN_RESOURCES) Cache cache,
      @GerritServerConfig Config cfg,
      SshInfo sshInfo,
      RestApiServlet.Globals globals,
      PluginsCollection plugins) {
    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;
  }

  @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) {
        log.warn(String.format("Plugin %s cannot load GuiceFilter", name), e);
        return null;
      }

      try {
        ServletContext ctx =
            PluginServletContext.create(plugin, wrapper.getFullPath(name));
        filter.init(new WrappedFilterConfig(ctx));
      } catch (ServletException e) {
        log.warn(String.format("Plugin %s failed to initialize HTTP", name), e);
        return null;
      }

      plugin.add(new RegistrationHandle() {
        @Override
        public void remove() {
          filter.destroy();
        }
      });
      return filter;
    }
    return null;
  }

  @Override
  public void service(HttpServletRequest req, HttpServletResponse res)
      throws IOException, ServletException {
    List parts = Lists.newArrayList(
      Splitter.on('/').limit(3).omitEmptyStrings()
        .split(Strings.nullToEmpty(req.getPathInfo())));

    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 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 = req.getPathInfo();
    if (pathInfo.length() < 1) {
      Resource.NOT_FOUND.send(req, res);
      return;
    }

    String file = pathInfo.substring(1);
    ResourceKey key = new ResourceKey(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 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 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 = holder.plugin.getSrcFile().lastModified();
        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 boolean hasUpToDateCachedResource(Resource cachedResource, long lastUpdateTime) {
    return cachedResource != null
        && cachedResource.isUnchanged(lastUpdateTime);
  }

  private void appendEntriesSection(PluginContentScanner scanner, List 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('-', ' ');
          }
          rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
        } else {
          entryTitle = rsrc.substring(nameOffset).replace('-', ' ');
        }
        md.append(String.format("* [%s](%s)\n", entryTitle, rsrc));
      }
      md.append("\n");
    }
  }

  private void sendAutoIndex(PluginContentScanner scanner,
      String prefix, String pluginName,
      ResourceKey cacheKey, HttpServletResponse res,long lastModifiedTime)
      throws IOException {
    List cmds = Lists.newArrayList();
    List servlets = Lists.newArrayList();
    List restApis = Lists.newArrayList();
    List docs = Lists.newArrayList();
    PluginEntry about = null;
    Enumeration entries = scanner.entries();
    while (entries.hasMoreElements()) {
      PluginEntry entry = entries.nextElement();
      String name = entry.getName();
      Optional size = entry.getSize();
      if (name.startsWith(prefix)
          && (name.endsWith(".md")
              || name.endsWith(".html"))
              && size.isPresent()
          && 0 < size.get() && size.get() <= SMALL_RESOURCE) {
        name = name.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 {
          docs.add(entry);
        }
      }
    }

    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, scanner.getManifest().getMainAttributes());

    if (about != null) {
      InputStreamReader isr = new InputStreamReader(scanner.getInputStream(about));
      BufferedReader reader = new BufferedReader(isr);
      StringBuilder aboutContent = new StringBuilder();
      String line;
      while ((line = reader.readLine()) != null) {
        line = line.trim();
        if (line.isEmpty()) {
          aboutContent.append("\n");
        } else {
          aboutContent.append(line).append("\n");
        }
      }
      reader.close();

      // 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,
      ResourceKey cacheKey, HttpServletResponse res, long lastModifiedTime)
      throws UnsupportedEncodingException, IOException {
    Map macros = Maps.newHashMap();
    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");
    resourceCache.put(cacheKey, new SmallResource(html)
        .setContentType("text/html")
        .setCharacterEncoding("UTF-8")
        .setLastModified(lastModifiedTime));
    res.setContentType("text/html");
    res.setCharacterEncoding("UTF-8");
    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 u = main.getValue(Attributes.Name.IMPLEMENTATION_URL);
      String a = main.getValue("Gerrit-ApiVersion");

      html.append("");
      if (!Strings.isNullOrEmpty(t)) {
        html.append("\n");
      }
      if (!Strings.isNullOrEmpty(n)) {
        html.append("\n");
      }
      if (!Strings.isNullOrEmpty(v)) {
        html.append("\n");
      }
      if (!Strings.isNullOrEmpty(u)) {
        html.append("\n");
      }
      if (!Strings.isNullOrEmpty(a)) {
        html.append("\n");
      }
      html.append("
Name") .append(t) .append("
Vendor") .append(n) .append("
Version") .append(v) .append("
URL") .append(String.format("%s", u, u)) .append("
API Version") .append(a) .append("
\n"); } } private static String extractTitleFromMarkdown(PluginContentScanner scanner, PluginEntry entry) throws IOException { String charEnc = null; Map atts = entry.getAttrs(); if (atts != null) { charEnc = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING)); } if (charEnc == null) { charEnc = "UTF-8"; } return new MarkdownFormatter().extractTitleFromMarkdown( readWholeEntry(scanner, entry), charEnc); } private static Optional 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.absent(); } private void sendMarkdownAsHtml(PluginContentScanner scanner, PluginEntry entry, String pluginName, ResourceKey key, HttpServletResponse res) throws IOException { byte[] rawmd = readWholeEntry(scanner, entry); String encoding = null; Map 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"), 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, ResourceKey key, HttpServletResponse res) throws IOException { byte[] data = null; Optional size = entry.getSize(); if (size.isPresent() && size.get() <= SMALL_RESOURCE) { data = readWholeEntry(scanner, entry); } String contentType = null; String charEnc = null; Map 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"; } } 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, ResourceKey key, HttpServletRequest req, HttpServletResponse res) throws IOException { File pluginFile = plugin.getSrcFile(); if (req.getRequestURI().endsWith(getJsPluginPath(plugin)) && pluginFile.exists()) { res.setHeader("Content-Length", Long.toString(pluginFile.length())); res.setContentType("application/javascript"); writeToResponse(res, new FileInputStream(pluginFile)); } 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() .getName()); } private void writeToResponse(HttpServletResponse res, InputStream in) throws IOException { 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 byte[] readWholeEntry(PluginContentScanner scanner, PluginEntry entry) throws IOException { byte[] data = new byte[entry.getSize().get().intValue()]; InputStream in = scanner.getInputStream(entry); try { IO.readFully(in, data, 0, data.length); } finally { in.close(); } return data; } 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) { File srcFile = plugin.getSrcFile(); PluginContentScanner scanner = plugin.getContentScanner(); if (srcFile == null || scanner == PluginContentScanner.EMPTY) { return def; } try { String prefix = scanner.getManifest().getMainAttributes().getValue(attr); if (prefix != null) { return CharMatcher.is('/').trimFrom(prefix) + "/"; } else { return def; } } catch (IOException e) { log.warn(String.format("Error getting %s for plugin %s, using default", attr, plugin.getName()), e); return null; } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy