Java tutorial
// 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 '>' 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(">", ">") + 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(); } } }