com.googlesource.gerrit.plugins.xdocs.XDocServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.googlesource.gerrit.plugins.xdocs.XDocServlet.java

Source

// Copyright (C) 2014 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.googlesource.gerrit.plugins.xdocs;

import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;

import com.google.common.base.CharMatcher;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.net.HttpHeaders;
import com.google.gerrit.common.data.PatchScript.FileMode;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.httpd.resources.Resource;
import com.google.gerrit.httpd.resources.SmallResource;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.mime.FileTypeRegistry;
import com.google.gerrit.server.change.FileContentUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.GetHead;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters;
import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters.FormatterProvider;

import eu.medsea.mimeutil.MimeType;

import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Singleton
public class XDocServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    public static final String PATH_PREFIX = "/project/";

    private final String pluginName;
    private final Provider<ReviewDb> db;
    private final ProjectControl.Factory projectControlFactory;
    private final ProjectCache projectCache;
    private final Provider<GetHead> getHead;
    private final GitRepositoryManager repoManager;
    private final XDocCache docCache;
    private final FileTypeRegistry fileTypeRegistry;
    private final XDocProjectConfig.Factory cfgFactory;
    private final Formatters formatters;

    @Inject
    XDocServlet(@PluginName String pluginName, Provider<ReviewDb> db, ProjectControl.Factory projectControlFactory,
            ProjectCache projectCache, Provider<GetHead> getHead, GitRepositoryManager repoManager, XDocCache cache,
            FileTypeRegistry fileTypeRegistry, XDocProjectConfig.Factory cfgFactory, Formatters formatters) {
        this.pluginName = pluginName;
        this.db = db;
        this.projectControlFactory = projectControlFactory;
        this.projectCache = projectCache;
        this.getHead = getHead;
        this.repoManager = repoManager;
        this.docCache = cache;
        this.fileTypeRegistry = fileTypeRegistry;
        this.cfgFactory = cfgFactory;
        this.formatters = formatters;
    }

    @Override
    public void service(HttpServletRequest req, HttpServletResponse res) throws IOException {
        try {
            validateRequestMethod(req);

            ResourceKey key = ResourceKey.fromPath(getEncodedPath(req));
            ProjectState state = getProject(key);
            XDocProjectConfig cfg = cfgFactory.create(state);

            if (key.file == null) {
                res.sendRedirect(getRedirectUrl(req, key, cfg));
                return;
            }

            MimeType mimeType = fileTypeRegistry.getMimeType(key.file, null);
            mimeType = new MimeType(
                    FileContentUtil.resolveContentType(state, key.file, FileMode.FILE, mimeType.toString()));
            FormatterProvider formatter = getFormatter(req, key, mimeType);
            validateDiffMode(key);

            ProjectControl projectControl = projectControlFactory.validateFor(key.project);
            String rev = getRevision(
                    key.diffMode == DiffMode.NO_DIFF ? MoreObjects.firstNonNull(key.revision, cfg.getIndexRef())
                            : key.revision,
                    projectControl);
            String revB = getRevision(key.revisionB, projectControl);

            try (Repository repo = repoManager.openRepository(key.project)) {
                ObjectId revId = resolveRevision(repo,
                        key.diffMode == DiffMode.NO_DIFF ? MoreObjects.firstNonNull(rev, Constants.HEAD) : rev);
                if (revId != null && ObjectId.isId(rev)) {
                    validateCanReadCommit(repo, projectControl, revId);
                }

                ObjectId revIdB = resolveRevision(repo, revB);
                if (revIdB != null && ObjectId.isId(revB)) {
                    validateCanReadCommit(repo, projectControl, revIdB);
                }

                if (isResourceNotModified(req, key, revId, revIdB)) {
                    res.sendError(SC_NOT_MODIFIED);
                    return;
                }

                Resource rsc;
                if (formatter != null) {
                    rsc = docCache.get(formatter, key.project, key.file, revId, revIdB, key.diffMode);
                } else if (isImage(mimeType)) {
                    rsc = getImageResource(repo, key.diffMode, revId, revIdB, key.file);
                } else {
                    rsc = Resource.NOT_FOUND;
                }

                if (rsc != Resource.NOT_FOUND) {
                    res.setHeader(HttpHeaders.ETAG,
                            computeETag(key.project, revId, key.file, revIdB, key.diffMode));
                }
                if (key.diffMode == DiffMode.NO_DIFF && rev == null) {
                    // file was loaded from HEAD, since HEAD is modifiable the document
                    // should only be cached for a short period
                    CacheHeaders.setCacheablePrivate(res, 15, TimeUnit.MINUTES, false);
                } else {
                    CacheHeaders.setCacheablePrivate(res, 7, TimeUnit.DAYS, false);
                }
                rsc.send(req, res);
                return;
            }
        } catch (RepositoryNotFoundException | NoSuchProjectException | ResourceNotFoundException | AuthException
                | RevisionSyntaxException e) {
            Resource.NOT_FOUND.send(req, res);
        } catch (MethodNotAllowedException e) {
            CacheHeaders.setNotCacheable(res);
            res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
        }
    }

    private String getEncodedPath(HttpServletRequest req) {
        String path = req.getRequestURI();
        String prefix = "/plugins/" + pluginName;
        if (path.startsWith(prefix)) {
            path = path.substring(prefix.length());
        }
        return path;
    }

    private Resource getImageResource(Repository repo, DiffMode diffMode, ObjectId revId, ObjectId revIdB,
            String file) {
        ObjectId id = diffMode == DiffMode.NO_DIFF || diffMode == DiffMode.SIDEBYSIDE_A ? revId : revIdB;
        try (RevWalk rw = new RevWalk(repo)) {
            RevCommit commit = rw.parseCommit(id);
            RevTree tree = commit.getTree();
            try (TreeWalk tw = new TreeWalk(repo)) {
                tw.addTree(tree);
                tw.setRecursive(true);
                tw.setFilter(PathFilter.create(file));
                if (!tw.next()) {
                    return Resource.NOT_FOUND;
                }
                ObjectId objectId = tw.getObjectId(0);
                ObjectLoader loader = repo.open(objectId);
                byte[] content = loader.getBytes(Integer.MAX_VALUE);

                MimeType mimeType = fileTypeRegistry.getMimeType(file, content);
                if (!isSafeImage(mimeType)) {
                    return Resource.NOT_FOUND;
                }
                return new SmallResource(content).setContentType(mimeType.toString())
                        .setCharacterEncoding(UTF_8.name()).setLastModified(commit.getCommitTime());
            }
        } catch (IOException e) {
            return Resource.NOT_FOUND;
        }
    }

    private static void validateRequestMethod(HttpServletRequest req) throws MethodNotAllowedException {
        if (!("GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod()))) {
            throw new MethodNotAllowedException();
        }
    }

    private static void validateDiffMode(ResourceKey key) throws ResourceNotFoundException {
        if (key.diffMode != DiffMode.NO_DIFF && (key.revisionB == null)) {
            throw new ResourceNotFoundException();
        }
    }

    private ProjectState getProject(ResourceKey key) throws ResourceNotFoundException {
        ProjectState state = projectCache.get(key.project);
        if (state == null) {
            throw new ResourceNotFoundException();
        }
        return state;
    }

    private FormatterProvider getFormatter(HttpServletRequest req, ResourceKey key, MimeType mimeType)
            throws ResourceNotFoundException {
        FormatterProvider formatter;
        if (req.getParameter("raw") != null) {
            formatter = formatters.getRawFormatter();
        } else {
            formatter = formatters.get(getProject(key), key.file);
        }
        if (isSafeImage(mimeType)) {
            if (req.getParameter("formatImage") == null) {
                // image formatting is not requested, return the plain image
                formatter = null;
            }
        } else {
            if (formatter == null) {
                throw new ResourceNotFoundException();
            }
        }
        if (formatter == null && !isSafeImage(mimeType)) {
            throw new ResourceNotFoundException();
        }
        return formatter;
    }

    private boolean isSafeImage(MimeType mimeType) {
        return isImage(mimeType) && fileTypeRegistry.isSafeInline(mimeType);
    }

    private static boolean isImage(MimeType mimeType) {
        return "image".equals(mimeType.getMediaType());
    }

    private String getRevision(String revision, ProjectControl projectControl)
            throws ResourceNotFoundException, AuthException, IOException {
        if (revision == null) {
            return null;
        }

        if (ObjectId.isId(revision)) {
            return revision;
        }

        if (Constants.HEAD.equals(revision)) {
            return getHead.get().apply(new ProjectResource(projectControl));
        } else {
            String rev = revision;
            if (!rev.startsWith(Constants.R_REFS)) {
                rev = Constants.R_HEADS + rev;
            }
            if (!projectControl.controlForRef(rev).isVisible()) {
                throw new ResourceNotFoundException();
            }
            return rev;
        }
    }

    private static ObjectId resolveRevision(Repository repo, String revision)
            throws ResourceNotFoundException, IOException {
        if (revision == null) {
            return null;
        }

        ObjectId revId = repo.resolve(revision);
        if (revId == null) {
            throw new ResourceNotFoundException();
        }
        return revId;
    }

    private void validateCanReadCommit(Repository repo, ProjectControl projectControl, ObjectId revId)
            throws ResourceNotFoundException, IOException {
        try (RevWalk rw = new RevWalk(repo)) {
            RevCommit commit = rw.parseCommit(revId);
            if (!projectControl.canReadCommit(db.get(), repo, commit)) {
                throw new ResourceNotFoundException();
            }
        }
    }

    private static boolean isResourceNotModified(HttpServletRequest req, ResourceKey key, ObjectId revId,
            ObjectId revIdB) {
        String receivedETag = req.getHeader(HttpHeaders.IF_NONE_MATCH);
        if (receivedETag != null) {
            return receivedETag.equals(computeETag(key.project, revId, key.file, revIdB, key.diffMode));
        }
        return false;
    }

    private static String computeETag(Project.NameKey project, ObjectId revId, String file, ObjectId revIdB,
            DiffMode diffMode) {
        Hasher hasher = Hashing.md5().newHasher();
        hasher.putUnencodedChars(project.get());
        if (revId != null) {
            hasher.putUnencodedChars(revId.getName());
        }
        hasher.putUnencodedChars(file);
        if (diffMode != DiffMode.NO_DIFF) {
            hasher.putUnencodedChars(revIdB.getName()).putUnencodedChars(diffMode.name());
        }
        return hasher.hash().toString();
    }

    private String getRedirectUrl(HttpServletRequest req, ResourceKey key, XDocProjectConfig cfg) {
        StringBuilder redirectUrl = new StringBuilder();
        redirectUrl.append(
                req.getRequestURL().substring(0, req.getRequestURL().length() - req.getRequestURI().length()));
        redirectUrl.append(req.getContextPath());
        redirectUrl.append(PATH_PREFIX);
        redirectUrl.append(IdString.fromDecoded(key.project.get()).encoded());
        redirectUrl.append("/");
        if (key.revision != null) {
            redirectUrl.append("rev/");
            redirectUrl.append(key.revision);
            redirectUrl.append("/");
        }
        redirectUrl.append(cfg.getIndexFile());
        return redirectUrl.toString();
    }

    private static class ResourceKey {
        final Project.NameKey project;
        final String file;
        final String revision;
        final String revisionB;
        final DiffMode diffMode;

        static ResourceKey fromPath(String path) {
            String project;
            String file = null;
            String revision = null;
            String revisionB = null;
            DiffMode diffMode = DiffMode.NO_DIFF;

            if (!path.startsWith(PATH_PREFIX)) {
                // should not happen since this servlet is only registered to handle
                // paths that start with this prefix
                throw new IllegalStateException("path must start with '" + PATH_PREFIX + "'");
            }
            path = path.substring(PATH_PREFIX.length());

            int i = path.indexOf('/');
            if (i != -1 && i != path.length() - 1) {
                project = IdString.fromUrl(path.substring(0, i)).get();
                String rest = path.substring(i + 1);

                if (rest.startsWith("rev/")) {
                    if (rest.length() > 4) {
                        rest = rest.substring(4);
                        i = rest.indexOf('/');
                        if (i != -1 && i != path.length() - 1) {
                            revision = IdString.fromUrl(rest.substring(0, i)).get();
                            file = rest.substring(i + 1);
                        } else {
                            revision = IdString.fromUrl(rest).get();
                        }
                    }
                } else {
                    file = rest;
                }

            } else {
                project = IdString.fromUrl(CharMatcher.is('/').trimTrailingFrom(path)).get();
            }

            if (revision != null) {
                if (revision.contains("<->")) {
                    diffMode = DiffMode.UNIFIED;
                    int p = revision.indexOf("<->");
                    revisionB = revision.substring(p + 3);
                    revision = Strings.emptyToNull(revision.substring(0, p));
                } else if (revision.contains("<-")) {
                    diffMode = DiffMode.SIDEBYSIDE_A;
                    int p = revision.indexOf("<-");
                    revisionB = revision.substring(p + 2);
                    revision = revision.substring(0, p);
                } else if (revision.contains("->")) {
                    diffMode = DiffMode.SIDEBYSIDE_B;
                    int p = revision.indexOf("->");
                    revisionB = revision.substring(p + 2);
                    revision = Strings.emptyToNull(revision.substring(0, p));
                }
            }

            return new ResourceKey(project, file, revision, revisionB, diffMode);
        }

        private ResourceKey(String p, String f, String r, String r2, DiffMode dm) {
            project = new Project.NameKey(p);
            file = f;
            revision = r;
            revisionB = r2;
            diffMode = dm;
        }
    }
}