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

Java tutorial

Introduction

Here is the source code for com.googlesource.gerrit.plugins.xdocs.XDocLoader.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 com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.Weigher;
import com.google.common.collect.Maps;
import com.google.gerrit.extensions.annotations.PluginName;
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.server.cache.CacheModule;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

import com.googlesource.gerrit.plugins.xdocs.formatter.Formatter;
import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters;
import com.googlesource.gerrit.plugins.xdocs.formatter.Formatters.FormatterProvider;
import com.googlesource.gerrit.plugins.xdocs.formatter.StreamFormatter;
import com.googlesource.gerrit.plugins.xdocs.formatter.StringFormatter;

import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
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 org.outerj.daisy.diff.HtmlCleaner;
import org.outerj.daisy.diff.XslFilter;
import org.outerj.daisy.diff.html.HTMLDiffer;
import org.outerj.daisy.diff.html.HtmlSaxDiffOutput;
import org.outerj.daisy.diff.html.TextNodeComparator;
import org.outerj.daisy.diff.html.dom.DomTreeBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;

@Singleton
public class XDocLoader extends CacheLoader<String, Resource> {
    private static final Logger log = LoggerFactory.getLogger(XDocLoader.class);

    private static final String DEFAULT_HOST = "review.example.com";

    private final GitRepositoryManager repoManager;
    private final Provider<String> webUrl;
    private final String pluginName;
    private final PluginConfigFactory cfgFactory;
    private final Formatters formatters;

    @Inject
    XDocLoader(GitRepositoryManager repoManager, @CanonicalWebUrl Provider<String> webUrl,
            @PluginName String pluginName, PluginConfigFactory cfgFactory, Formatters formatters) {
        this.repoManager = repoManager;
        this.webUrl = webUrl;
        this.pluginName = pluginName;
        this.cfgFactory = cfgFactory;
        this.formatters = formatters;
    }

    @Override
    public Resource load(String strKey) throws Exception {
        XDocResourceKey key = XDocResourceKey.fromString(strKey);
        try (Repository repo = repoManager.openRepository(key.getProject())) {
            FormatterProvider formatter = getFormatter(key.getFormatter());
            try (RevWalk rw = new RevWalk(repo)) {
                String html = null;
                if (key.getRevId() != null) {
                    html = loadHtml(formatter, repo, rw, key, key.getRevId());
                }

                if (key.getDiffMode() != DiffMode.NO_DIFF) {
                    String htmlB = loadHtml(formatter, repo, rw, key, checkRevId(key.getRevIdB()));
                    if (html == null && htmlB == null) {
                        throw new ResourceNotFoundException();
                    }
                    html = diffHtml(html, htmlB, key.getDiffMode());
                } else {
                    if (html == null) {
                        throw new ResourceNotFoundException();
                    }
                }

                RevCommit commit = rw.parseCommit(MoreObjects.firstNonNull(key.getRevIdB(), key.getRevId()));
                return getAsHtmlResource(html, commit.getCommitTime());
            }
        } catch (ResourceNotFoundException e) {
            return Resource.NOT_FOUND;
        } catch (MethodNotAllowedException e) {
            return Resources.METHOD_NOT_ALLOWED;
        }
    }

    private FormatterProvider getFormatter(String formatterName) throws ResourceNotFoundException {
        FormatterProvider formatter = formatters.getByName(formatterName);
        if (formatter == null) {
            throw new ResourceNotFoundException();
        }
        return formatter;
    }

    private static ObjectId checkRevId(ObjectId revId) throws ResourceNotFoundException {
        if (revId == null) {
            throw new ResourceNotFoundException();
        }
        return revId;
    }

    private String loadHtml(FormatterProvider formatter, Repository repo, RevWalk rw, XDocResourceKey key,
            ObjectId revId)
            throws IOException, ResourceNotFoundException, MethodNotAllowedException, GitAPIException {
        RevCommit commit = rw.parseCommit(revId);
        RevTree tree = commit.getTree();
        try (TreeWalk tw = new TreeWalk(repo)) {
            tw.addTree(tree);
            tw.setRecursive(true);
            tw.setFilter(PathFilter.create(key.getResource()));
            if (!tw.next()) {
                return null;
            }
            ObjectId objectId = tw.getObjectId(0);
            ObjectLoader loader = repo.open(objectId);
            return getHtml(formatter, repo, loader, key.getProject(), key.getResource(), revId);
        }
    }

    private String getHtml(FormatterProvider formatter, Repository repo, ObjectLoader loader,
            Project.NameKey project, String path, ObjectId revId)
            throws MethodNotAllowedException, IOException, GitAPIException, ResourceNotFoundException {
        Formatter f = formatter.get();
        if (f instanceof StringFormatter) {
            return getHtml(formatter.getName(), (StringFormatter) f, repo, loader, project, path, revId);
        } else if (f instanceof StreamFormatter) {
            return getHtml(formatter.getName(), (StreamFormatter) f, repo, loader, project, path, revId);
        } else {
            log.error(String.format("Unsupported formatter: %s", formatter.getName()));
            throw new ResourceNotFoundException();
        }
    }

    private String getHtml(String formatterName, StringFormatter f, Repository repo, ObjectLoader loader,
            Project.NameKey project, String path, ObjectId revId)
            throws MethodNotAllowedException, IOException, GitAPIException {
        byte[] bytes = loader.getBytes(Integer.MAX_VALUE);
        boolean isBinary = RawText.isBinary(bytes);
        if (formatterName.equals(Formatters.RAW_FORMATTER) && isBinary) {
            throw new MethodNotAllowedException();
        }
        String raw = new String(bytes, UTF_8);
        String abbrRevId = getAbbrRevId(repo, revId);
        if (!isBinary) {
            raw = replaceMacros(repo, project, revId, abbrRevId, raw);
        }
        return f.format(project.get(), path, revId.getName(), abbrRevId, getFormatterConfig(formatterName), raw);
    }

    private String getHtml(String formatterName, StreamFormatter f, Repository repo, ObjectLoader loader,
            Project.NameKey project, String path, ObjectId revId) throws IOException {
        try (InputStream raw = loader.openStream()) {
            return f.format(project.get(), path, revId.getName(), getAbbrRevId(repo, revId),
                    getFormatterConfig(formatterName), raw);
        }
    }

    private String diffHtml(String htmlA, String htmlB, DiffMode diffMode)
            throws IOException, TransformerConfigurationException, SAXException, ResourceNotFoundException {
        ByteArrayOutputStream htmlDiff = new ByteArrayOutputStream();

        SAXTransformerFactory tf = (SAXTransformerFactory) TransformerFactory.newInstance();
        TransformerHandler result = tf.newTransformerHandler();
        result.setResult(new StreamResult(htmlDiff));

        String htmlHeader = "com/googlesource/gerrit/plugins/xdocs/diff/htmlheader-";
        switch (diffMode) {
        case SIDEBYSIDE_A:
            htmlHeader += "sidebyside-a.xsl";
            break;
        case SIDEBYSIDE_B:
            htmlHeader += "sidebyside-b.xsl";
            break;
        case UNIFIED:
            htmlHeader += "unified.xsl";
            break;
        default:
            log.error(String.format("Unsupported diff mode: %s", diffMode.name()));
            throw new ResourceNotFoundException();
        }

        ContentHandler postProcess = new XslFilter().xsl(result, htmlHeader);
        postProcess.startDocument();
        postProcess.startElement("", "diffreport", "diffreport", new AttributesImpl());
        postProcess.startElement("", "diff", "diff", new AttributesImpl());

        HtmlSaxDiffOutput output = new HtmlSaxDiffOutput(postProcess, "diff");
        HTMLDiffer differ = new HTMLDiffer(output);
        differ.diff(getComparator(htmlA), getComparator(htmlB));

        postProcess.endElement("", "diff", "diff");
        postProcess.endElement("", "diffreport", "diffreport");
        postProcess.endDocument();

        return fixStyles(htmlDiff.toString(UTF_8.name()));
    }

    /**
     * The daisydiff formatting may make inlined styles unparsable. Fix it:
     * <ul>
     *   <li>Remove span element to highlight addition/deletion inside style elements.</li>
     *   <li>Replace '&gt;' with '>'.</li>
     * </ul>
     */
    private String fixStyles(String html) {
        Matcher m = Pattern.compile("(<style[a-zA-Z -=/\"]+>\n)<[a-zA-Z -=\"]+>(.*)</[a-z]+>(\n</style>)")
                .matcher(html);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            m.appendReplacement(sb, m.group(1) + m.group(2).replaceAll("&gt;", ">") + m.group(3));
        }
        m.appendTail(sb);
        return sb.toString();
    }

    private TextNodeComparator getComparator(String html) throws IOException, SAXException {
        InputSource source = new InputSource(new ByteArrayInputStream(Strings.nullToEmpty(html).getBytes(UTF_8)));
        DomTreeBuilder handler = new DomTreeBuilder();
        new HtmlCleaner().cleanAndParse(source, handler);
        return new TextNodeComparator(handler, Locale.US);
    }

    private ConfigSection getFormatterConfig(String formatterName) {
        XDocGlobalConfig cfg = new XDocGlobalConfig(cfgFactory.getGlobalPluginConfig(pluginName));
        return cfg.getFormatterConfig(formatterName);
    }

    private static String getAbbrRevId(Repository repo, ObjectId revId) throws IOException {
        try (ObjectReader reader = repo.newObjectReader()) {
            return reader.abbreviate(revId).name();
        }
    }

    private String replaceMacros(Repository repo, Project.NameKey project, ObjectId revId, String abbrRevId,
            String raw) throws GitAPIException, IOException {
        Map<String, String> macros = Maps.newHashMap();

        String url = webUrl.get();
        if (Strings.isNullOrEmpty(url)) {
            url = "http://" + DEFAULT_HOST + "/";
        }
        macros.put("URL", url);

        macros.put("PROJECT", project.get());
        macros.put("PROJECT_URL", url + "#/admin/projects/" + project.get());
        macros.put("REVISION", abbrRevId);
        try (Git git = new Git(repo)) {
            macros.put("GIT_DESCRIPTION",
                    MoreObjects.firstNonNull(git.describe().setTarget(revId).call(), abbrRevId));
        }

        Matcher m = Pattern.compile("(\\\\)?@([A-Z_]+)@").matcher(raw);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            String key = m.group(2);
            String val = macros.get(key);
            if (m.group(1) != null || val == null) {
                m.appendReplacement(sb, "@" + key + "@");
            } else {
                m.appendReplacement(sb, val);
            }
        }
        m.appendTail(sb);
        return sb.toString();
    }

    private Resource getAsHtmlResource(String html, int lastModified) {
        return new SmallResource(html.getBytes(UTF_8)).setContentType("text/html")
                .setCharacterEncoding(UTF_8.name()).setLastModified(lastModified);
    }

    public static class Module extends CacheModule {
        static final String X_DOC_RESOURCES = "x_doc_resources";

        @Override
        protected void configure() {
            install(new CacheModule() {
                @Override
                protected void configure() {
                    persist(X_DOC_RESOURCES, String.class, Resource.class).maximumWeight(2 << 20)
                            .weigher(XDocResourceWeigher.class).loader(XDocLoader.class);
                }
            });
        }
    }

    private static class XDocResourceWeigher implements Weigher<String, Resource> {
        @Override
        public int weigh(String key, Resource value) {
            return key.length() * 2 + value.weigh();
        }
    }
}