| // 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.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.lang3.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 appendPageAsSection( |
| PluginContentScanner scanner, PluginEntry pluginEntry, String sectionTitle, StringBuilder md) |
| throws IOException { |
| InputStreamReader isr = new InputStreamReader(scanner.getInputStream(pluginEntry), UTF_8); |
| StringBuilder content = new StringBuilder(); |
| try (BufferedReader reader = new BufferedReader(isr)) { |
| String line; |
| while ((line = reader.readLine()) != null) { |
| line = StringUtils.stripEnd(line, null); |
| if (line.isEmpty()) { |
| content.append("\n"); |
| } else { |
| content.append(line).append("\n"); |
| } |
| } |
| } |
| |
| // Only append the section if there was anything in it |
| if (content.toString().trim().length() > 0) { |
| md.append("## "); |
| md.append(sectionTitle); |
| md.append(" ##\n"); |
| md.append("\n").append(content); |
| md.append("\n"); |
| } |
| } |
| |
| 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; |
| PluginEntry toc = 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 = scanner.entries().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 if (name.startsWith("toc.")) { |
| if (toc == null) { |
| toc = entry; |
| } else { |
| logger.atWarning().log( |
| "Plugin %s: Multiple 'toc' documents found; using %s", |
| pluginName, toc.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) { |
| appendPageAsSection(scanner, about, "About", md); |
| } |
| |
| if (toc != null) { |
| appendPageAsSection(scanner, toc, "Documentation", md); |
| } else { |
| 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); |
| StringBuilder sb = new StringBuilder(); |
| 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; |
| } |
| } |
| } |
| } |