com.google.gitiles.GitwebRedirectFilter.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gitiles.GitwebRedirectFilter.java

Source

// 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.net.HttpHeaders.LOCATION;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_GONE;
import static javax.servlet.http.HttpServletResponse.SC_MOVED_PERMANENTLY;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.gitiles.GitilesView.InvalidViewException;

import org.eclipse.jgit.lib.ObjectId;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.List;
import java.util.regex.Pattern;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/** Filter to redirect Gitweb-style URLs to Gitiles-style URLs. */
public class GitwebRedirectFilter extends AbstractHttpFilter {
    public static class TooManyUriParametersException extends IllegalArgumentException {
        private static final long serialVersionUID = 1L;
    }

    private static final String ARG_SEP = "&|;|%3[Bb]";
    private static final Pattern IS_GITWEB_PATTERN = Pattern.compile("(^|" + ARG_SEP + ")[pa]=");
    private static final Splitter ARG_SPLIT = Splitter.on(Pattern.compile(ARG_SEP));
    private static final Splitter VAR_SPLIT = Splitter.on(Pattern.compile("=|%3[Dd]")).limit(2);
    private static final int MAX_ARGS = 512;

    private final boolean trimDotGit;

    public GitwebRedirectFilter() {
        this(false);
    }

    public GitwebRedirectFilter(boolean trimDotGit) {
        this.trimDotGit = trimDotGit;
    }

    @Override
    public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        GitilesView gitwebView = ViewFilter.getView(req);
        if (!isGitwebStyleQuery(req)) {
            chain.doFilter(req, res);
            return;
        }

        ListMultimap<String, String> params = parse(req.getQueryString());
        String action = getFirst(params, "a");
        String project = getFirst(params, "p");
        String path = Strings.nullToEmpty(getFirst(params, "f"));

        // According to gitweb's perl source code, the primary parameters are these
        // short abbreviated names. When pointing to blob or subtree hash,hashParent
        // are the blob or subtree SHA-1s and hashBase,hashParentBase are commits.
        // When pointing to commits or tags, hash is the commit/tag. Its messy.
        Revision hash = toRevision(getFirst(params, "h"));
        Revision hashBase = toRevision(getFirst(params, "hb"));
        Revision hashParent = toRevision(getFirst(params, "hp"));
        Revision hashParentBase = toRevision(getFirst(params, "hpb"));

        GitilesView.Builder view;
        if ("project_index".equals(action)) {
            view = GitilesView.hostIndex();
            project = null;
        } else if ("summary".equals(action) || "tags".equals(action)) {
            view = GitilesView.repositoryIndex();
        } else if (("commit".equals(action) || "tag".equals(action)) && hash != null) {
            view = GitilesView.revision().setRevision(hash);
        } else if ("log".equals(action) || "shortlog".equals(action)) {
            view = GitilesView.log().setRevision(firstNonNull(hash, Revision.HEAD));
        } else if ("tree".equals(action)) {
            view = GitilesView.path().setRevision(firstNonNull(hashBase, Revision.HEAD)).setPathPart(path);
        } else if (("blob".equals(action) || "blob_plain".equals(action)) && hashBase != null && !path.isEmpty()) {
            view = GitilesView.path().setRevision(hashBase).setPathPart(path);
        } else if ("commitdiff".equals(action) && hash != null) {
            view = GitilesView.diff().setOldRevision(firstNonNull(hashParent, Revision.NULL)).setRevision(hash)
                    .setPathPart("");
        } else if ("blobdiff".equals(action) && !path.isEmpty() && hashParentBase != null && hashBase != null) {
            view = GitilesView.diff().setOldRevision(hashParentBase).setRevision(hashBase).setPathPart(path);
        } else if ("history".equals(action) && !path.isEmpty()) {
            view = GitilesView.log().setRevision(firstNonNull(hashBase, Revision.HEAD)).setPathPart(path);
        } else {
            // Gitiles does not provide an RSS feed (a=rss,atom,opml)
            // Any other URL is out of date and not valid anymore.
            res.sendError(SC_GONE);
            return;
        }

        if (!Strings.isNullOrEmpty(project)) {
            view.setRepositoryName(cleanProjectName(project));
        }

        String url;
        try {
            url = view.setHostName(gitwebView.getHostName()).setServletPath(gitwebView.getServletPath()).toUrl();
        } catch (InvalidViewException e) {
            res.setStatus(SC_GONE);
            return;
        }
        res.setStatus(SC_MOVED_PERMANENTLY);
        res.setHeader(LOCATION, url);
    }

    private static boolean isGitwebStyleQuery(HttpServletRequest req) {
        String qs = req.getQueryString();
        return qs != null && IS_GITWEB_PATTERN.matcher(qs).find();
    }

    private static String getFirst(ListMultimap<String, String> params, String name) {
        return Iterables.getFirst(params.get(checkNotNull(name)), null);
    }

    private static Revision toRevision(String rev) {
        if (Strings.isNullOrEmpty(rev)) {
            return null;
        } else if ("HEAD".equals(rev) || rev.startsWith("refs/")) {
            return Revision.named(rev);
        } else if (ObjectId.isId(rev)) {
            return Revision.unpeeled(rev, ObjectId.fromString(rev));
        } else {
            return Revision.named(rev);
        }
    }

    private String cleanProjectName(String p) {
        if (p.startsWith("/")) {
            p = p.substring(1);
        }
        if (p.endsWith("/")) {
            p = p.substring(0, p.length() - 1);
        }
        if (trimDotGit && p.endsWith(".git")) {
            p = p.substring(0, p.length() - ".git".length());
        }
        if (p.endsWith("/")) {
            p = p.substring(0, p.length() - 1);
        }
        return p;
    }

    private static ListMultimap<String, String> parse(String query) {
        // Parse a gitweb style query string which uses ";" rather than "&" between
        // key=value pairs. Some user agents encode ";" as "%3B" and/or "=" as
        // "%3D", making a real mess of the query string. Parsing here is
        // approximate because ; shouldn't be the pair separator and %3B might have
        // been a ; within a value.
        // This is why people shouldn't use gitweb.
        ListMultimap<String, String> map = LinkedListMultimap.create();
        for (String piece : ARG_SPLIT.split(query)) {
            if (map.size() > MAX_ARGS) {
                throw new TooManyUriParametersException();
            }

            List<String> pair = VAR_SPLIT.splitToList(piece);
            if (pair.size() == 2) {
                map.put(decode(pair.get(0)), decode(pair.get(1)));
            } else { // no equals sign
                map.put(piece, "");
            }
        }
        return map;
    }

    private static String decode(String str) {
        try {
            return URLDecoder.decode(str, UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            return str;
        }
    }
}