| // Copyright 2012 Google Inc. All Rights Reserved. |
| // |
| // 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.gitiles; |
| |
| import static com.google.common.base.MoreObjects.firstNonNull; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.gitiles.GitilesServlet.STATIC_PREFIX; |
| import static com.google.gitiles.Renderer.fileUrlMapper; |
| import static java.util.stream.Collectors.toList; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.LinkedListMultimap; |
| import com.google.common.collect.ListMultimap; |
| import com.google.common.collect.Maps; |
| import com.google.gitiles.blame.BlameServlet; |
| import com.google.gitiles.blame.cache.BlameCache; |
| import com.google.gitiles.blame.cache.BlameCacheImpl; |
| import com.google.gitiles.doc.DocServlet; |
| import java.io.File; |
| import java.io.IOException; |
| import java.net.UnknownHostException; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| import javax.servlet.Filter; |
| import javax.servlet.FilterChain; |
| import javax.servlet.FilterConfig; |
| 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.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.http.server.glue.MetaFilter; |
| import org.eclipse.jgit.http.server.glue.ServletBinder; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.transport.resolver.FileResolver; |
| import org.eclipse.jgit.transport.resolver.RepositoryResolver; |
| |
| /** |
| * MetaFilter to serve Gitiles. |
| * |
| * <p>Do not use directly; use {@link GitilesServlet}. |
| */ |
| class GitilesFilter extends MetaFilter { |
| |
| // The following regexes have the following capture groups: |
| // 1. The whole string, which causes RegexPipeline to set REGEX_GROUPS but |
| // not otherwise modify the original request. |
| // 2. The repository name part, before /<CMD>. |
| // 3. The command, <CMD>, with no slashes and beginning with +. Commands have |
| // names analogous to (but not exactly the same as) git command names, such |
| // as "+log" and "+show". The bare command "+" maps to one of the other |
| // commands based on the revision/path, and may change over time. |
| // 4. The revision/path part, after /<CMD> (called just "path" below). This is |
| // split into a revision and a path by RevisionParser. |
| |
| private static final String CMD = "\\+[a-z0-9-]*"; |
| |
| @VisibleForTesting |
| static final Pattern ROOT_REGEX = |
| Pattern.compile( |
| "" |
| + "^( " // 1. Everything |
| + " /* " // Excess slashes |
| + " (/) " // 2. Repo name (just slash) |
| + " () " // 3. Command |
| + " () " // 4. Path |
| + ")$ ", |
| Pattern.COMMENTS); |
| |
| @VisibleForTesting |
| static final Pattern REPO_REGEX = |
| Pattern.compile( |
| "" |
| + "^( " // 1. Everything |
| + " /* " // Excess slashes |
| + " ( " // 2. Repo name |
| + " / " // Leading slash |
| + " (?:.(?! " // Anything, as long as it's not followed by... |
| + " /" |
| + CMD |
| + "/ " // the special "/<CMD>/" separator, |
| + " |/" |
| + CMD |
| + "$ " // or "/<CMD>" at the end of the string |
| + " ))*? " |
| + " ) " |
| + " /* " // Trailing slashes |
| + " () " // 3. Command |
| + " () " // 4. Path |
| + ")$ ", |
| Pattern.COMMENTS); |
| |
| @VisibleForTesting |
| static final Pattern REPO_PATH_REGEX = |
| Pattern.compile( |
| "" |
| + "^( " // 1. Everything |
| + " /* " // Excess slashes |
| + " ( " // 2. Repo name |
| + " / " // Leading slash |
| + " .*? " // Anything, non-greedy |
| + " ) " |
| + " /(" |
| + CMD |
| + ")" // 3. Command |
| + " ( " // 4. Path |
| + " (?:/.*)? " // Slash path, or nothing. |
| + " ) " |
| + ")$ ", |
| Pattern.COMMENTS); |
| |
| private static class DispatchFilter extends AbstractHttpFilter { |
| private final ListMultimap<GitilesView.Type, Filter> filters; |
| private final Map<GitilesView.Type, HttpServlet> servlets; |
| |
| private DispatchFilter( |
| ListMultimap<GitilesView.Type, Filter> filters, |
| Map<GitilesView.Type, HttpServlet> servlets) { |
| this.filters = LinkedListMultimap.create(filters); |
| this.servlets = ImmutableMap.copyOf(servlets); |
| for (GitilesView.Type type : GitilesView.Type.values()) { |
| checkState(servlets.containsKey(type), "Missing handler for view %s", type); |
| } |
| } |
| |
| @Override |
| public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) |
| throws IOException, ServletException { |
| GitilesView view = checkNotNull(ViewFilter.getView(req)); |
| final Iterator<Filter> itr = filters.get(view.getType()).iterator(); |
| final HttpServlet servlet = servlets.get(view.getType()); |
| new FilterChain() { |
| @Override |
| public void doFilter(ServletRequest req, ServletResponse res) |
| throws IOException, ServletException { |
| if (itr.hasNext()) { |
| itr.next().doFilter(req, res, this); |
| } else { |
| servlet.service(req, res); |
| } |
| } |
| }.doFilter(req, res); |
| } |
| } |
| |
| private final ListMultimap<GitilesView.Type, Filter> filters = LinkedListMultimap.create(); |
| private final Map<GitilesView.Type, HttpServlet> servlets = Maps.newHashMap(); |
| |
| private Config config; |
| private Renderer renderer; |
| private GitilesUrls urls; |
| private Linkifier linkifier; |
| private GitilesAccess.Factory accessFactory; |
| private RepositoryResolver<HttpServletRequest> resolver; |
| private VisibilityCache visibilityCache; |
| private TimeCache timeCache; |
| private BlameCache blameCache; |
| private GitwebRedirectFilter gitwebRedirect; |
| private boolean initialized; |
| |
| GitilesFilter() {} |
| |
| GitilesFilter( |
| Config config, |
| Renderer renderer, |
| GitilesUrls urls, |
| GitilesAccess.Factory accessFactory, |
| final RepositoryResolver<HttpServletRequest> resolver, |
| VisibilityCache visibilityCache, |
| TimeCache timeCache, |
| BlameCache blameCache, |
| GitwebRedirectFilter gitwebRedirect) { |
| this.config = checkNotNull(config, "config"); |
| this.renderer = renderer; |
| this.urls = urls; |
| this.accessFactory = accessFactory; |
| this.visibilityCache = visibilityCache; |
| this.timeCache = timeCache; |
| this.blameCache = blameCache; |
| this.gitwebRedirect = gitwebRedirect; |
| if (resolver != null) { |
| this.resolver = resolver; |
| } |
| } |
| |
| @Override |
| public synchronized void init(FilterConfig config) throws ServletException { |
| super.init(config); |
| setDefaultFields(config); |
| |
| for (GitilesView.Type type : GitilesView.Type.values()) { |
| if (!servlets.containsKey(type)) { |
| servlets.put(type, getDefaultHandler(type)); |
| } |
| } |
| |
| Filter repositoryFilter = new RepositoryFilter(resolver); |
| Filter viewFilter = new ViewFilter(accessFactory, urls, visibilityCache); |
| Filter dispatchFilter = new DispatchFilter(filters, servlets); |
| |
| ServletBinder root = serveRegex(ROOT_REGEX).through(viewFilter); |
| if (gitwebRedirect != null) { |
| root.through(gitwebRedirect); |
| } |
| root.through(dispatchFilter); |
| |
| serveRegex(REPO_REGEX).through(repositoryFilter).through(viewFilter).through(dispatchFilter); |
| |
| serveRegex(REPO_PATH_REGEX) |
| .through(repositoryFilter) |
| .through(viewFilter) |
| .through(dispatchFilter); |
| |
| initialized = true; |
| } |
| |
| public synchronized BaseServlet getDefaultHandler(GitilesView.Type view) { |
| checkNotInitialized(); |
| switch (view) { |
| case HOST_INDEX: |
| return new HostIndexServlet(accessFactory, renderer, urls); |
| case REPOSITORY_INDEX: |
| return new RepositoryIndexServlet(accessFactory, renderer, timeCache); |
| case REFS: |
| return new RefServlet(accessFactory, renderer, timeCache); |
| case REVISION: |
| return new RevisionServlet(accessFactory, renderer, linkifier()); |
| case SHOW: |
| case PATH: |
| return new PathServlet(accessFactory, renderer, urls); |
| case DIFF: |
| return new DiffServlet(accessFactory, renderer, linkifier()); |
| case LOG: |
| return new LogServlet(accessFactory, renderer, linkifier()); |
| case DESCRIBE: |
| return new DescribeServlet(accessFactory); |
| case ARCHIVE: |
| return new ArchiveServlet(accessFactory); |
| case BLAME: |
| return new BlameServlet(accessFactory, renderer, blameCache); |
| case DOC: |
| case ROOTED_DOC: |
| return new DocServlet(accessFactory, renderer); |
| default: |
| throw new IllegalArgumentException("Invalid view type: " + view); |
| } |
| } |
| |
| public synchronized void setRenderer(Renderer renderer) { |
| checkNotInitialized(); |
| this.renderer = checkNotNull(renderer, "renderer"); |
| } |
| |
| synchronized void addFilter(GitilesView.Type view, Filter filter) { |
| checkNotInitialized(); |
| filters.put(checkNotNull(view, "view"), checkNotNull(filter, "filter for %s", view)); |
| } |
| |
| synchronized void setHandler(GitilesView.Type view, HttpServlet handler) { |
| checkNotInitialized(); |
| servlets.put(checkNotNull(view, "view"), checkNotNull(handler, "handler for %s", view)); |
| } |
| |
| private synchronized void checkNotInitialized() { |
| checkState(!initialized, "Gitiles already initialized"); |
| } |
| |
| private synchronized Linkifier linkifier() { |
| if (linkifier == null) { |
| checkState(urls != null, "GitilesUrls not yet set"); |
| linkifier = new Linkifier(urls, config); |
| } |
| return linkifier; |
| } |
| |
| private void setDefaultFields(FilterConfig filterConfig) throws ServletException { |
| setDefaultConfig(filterConfig); |
| setDefaultRenderer(filterConfig); |
| setDefaultUrls(); |
| setDefaultAccess(); |
| setDefaultVisibilityCache(); |
| setDefaultTimeCache(); |
| setDefaultBlameCache(); |
| setDefaultGitwebRedirect(); |
| } |
| |
| private void setDefaultConfig(FilterConfig filterConfig) throws ServletException { |
| if (config == null) { |
| try { |
| config = GitilesConfig.loadDefault(filterConfig); |
| } catch (IOException | ConfigInvalidException e) { |
| throw new ServletException(e); |
| } |
| } |
| } |
| |
| private void setDefaultRenderer(FilterConfig filterConfig) { |
| if (renderer == null) { |
| renderer = |
| new DefaultRenderer( |
| filterConfig.getServletContext().getContextPath() + STATIC_PREFIX, |
| Arrays.stream(config.getStringList("gitiles", null, "customTemplates")) |
| .map(fileUrlMapper()) |
| .collect(toList()), |
| firstNonNull(config.getString("gitiles", null, "siteTitle"), "Gitiles")); |
| } |
| } |
| |
| private void setDefaultUrls() throws ServletException { |
| if (urls == null) { |
| try { |
| urls = |
| new DefaultUrls( |
| config.getString("gitiles", null, "canonicalHostName"), |
| getBaseGitUrl(config), |
| config.getString("gitiles", null, "gerritUrl")); |
| } catch (UnknownHostException e) { |
| throw new ServletException(e); |
| } |
| } |
| } |
| |
| private void setDefaultAccess() throws ServletException { |
| if (accessFactory == null || resolver == null) { |
| String basePath = config.getString("gitiles", null, "basePath"); |
| if (basePath == null) { |
| throw new ServletException("gitiles.basePath not set"); |
| } |
| boolean exportAll = config.getBoolean("gitiles", null, "exportAll", false); |
| |
| FileResolver<HttpServletRequest> fileResolver; |
| if (resolver == null) { |
| fileResolver = new FileResolver<>(new File(basePath), exportAll); |
| resolver = fileResolver; |
| } else if (resolver instanceof FileResolver) { |
| fileResolver = (FileResolver<HttpServletRequest>) resolver; |
| } else { |
| fileResolver = null; |
| } |
| if (accessFactory == null) { |
| checkState(fileResolver != null, "need a FileResolver when GitilesAccess.Factory not set"); |
| try { |
| accessFactory = |
| new DefaultAccess.Factory( |
| new File(basePath), getBaseGitUrl(config), config, fileResolver); |
| } catch (IOException e) { |
| throw new ServletException(e); |
| } |
| } |
| } |
| } |
| |
| private void setDefaultVisibilityCache() { |
| if (visibilityCache == null) { |
| if (config.getSubsections("cache").contains("visibility")) { |
| visibilityCache = |
| new VisibilityCache(false, ConfigUtil.getCacheBuilder(config, "visibility")); |
| } else { |
| visibilityCache = new VisibilityCache(false); |
| } |
| } |
| } |
| |
| private void setDefaultTimeCache() { |
| if (timeCache == null) { |
| if (config.getSubsections("cache").contains("tagTime")) { |
| timeCache = new TimeCache(ConfigUtil.getCacheBuilder(config, "tagTime")); |
| } else { |
| timeCache = new TimeCache(); |
| } |
| } |
| } |
| |
| private void setDefaultBlameCache() { |
| if (blameCache == null) { |
| if (config.getSubsections("cache").contains("blame")) { |
| blameCache = new BlameCacheImpl(ConfigUtil.getCacheBuilder(config, "blame")); |
| } else { |
| blameCache = new BlameCacheImpl(); |
| } |
| } |
| } |
| |
| private void setDefaultGitwebRedirect() { |
| if (gitwebRedirect == null) { |
| if (config.getBoolean("gitiles", null, "redirectGitweb", true)) { |
| gitwebRedirect = new GitwebRedirectFilter(); |
| } |
| } |
| } |
| |
| private static String getBaseGitUrl(Config config) throws ServletException { |
| String baseGitUrl = config.getString("gitiles", null, "baseGitUrl"); |
| if (baseGitUrl == null) { |
| throw new ServletException("gitiles.baseGitUrl not set"); |
| } |
| return baseGitUrl; |
| } |
| } |