blob: 93dbf514bf511c66c6f8935c864fbba1fc6a3cd2 [file] [log] [blame]
// 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.formatter;
import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_CSS_THEME;
import static com.googlesource.gerrit.plugins.xdocs.XDocGlobalConfig.KEY_INHERIT_CSS;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.annotations.PluginData;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.xdocs.ConfigSection;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
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 org.eclipse.jgit.util.TemporaryBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class FormatterUtil {
private static final Logger log = LoggerFactory.getLogger(FormatterUtil.class);
private final String pluginName;
private final File baseDir;
private final GitRepositoryManager repoManager;
private final ProjectCache projectCache;
private final Formatters formatters;
private final Map<String, String> defaultCss;
@Inject
FormatterUtil(
@PluginName String pluginName,
@PluginData File baseDir,
GitRepositoryManager repoManager,
ProjectCache projectCache,
Formatters formatters) {
this.pluginName = pluginName;
this.baseDir = baseDir;
this.repoManager = repoManager;
this.projectCache = projectCache;
this.formatters = formatters;
this.defaultCss = new HashMap<>();
}
/**
* Returns the CSS from the file "<plugin-name>/<name>-<theme>.css" in the refs/meta/config branch
* of the project.
*
* <p>If theme is <code>null</code> or empty, the CSS from the file "<plugin-name>/<name>.css" is
* returned.
*
* @param name the name of the file in the "<plugin-name>/" folder without theme and without the
* ".css" file extension
* @param theme the name of the CSS theme, may be <code>null</code>, if given it is included into
* the CSS file name: '<name>-<theme>.css'
* @return the CSS from the file; HTML characters are escaped; <code>null</code> if the file
* doesn't exist
*/
public String getCss(String projectName, String name, String theme) {
return Strings.isNullOrEmpty(theme)
? getCss(projectName, name)
: getCss(projectName, name + "-" + theme);
}
/**
* Returns the CSS from the file "<plugin-name>/<name>.css" in the refs/meta/config branch of the
* project.
*
* @param name the name of the file in the "<plugin-name>/" folder without the ".css" file
* extension
* @return the CSS from the file; HTML characters are escaped; <code>null</code> if the file
* doesn't exist
*/
public String getCss(String projectName, String name) {
return escapeHtml(getMetaConfigFile(projectName, name + ".css"));
}
/**
* Returns the inherited CSS.
*
* <p>If the project has a parent project the CSS of the parent project is returned; if there is
* no parent project the global CSS is returned.
*
* @param projectName the name of the project
* @param formatterName the name of the formatter for which the CSS should be returned
* @param name the name of the CSS file without theme and without the ".css" file extension
* @param theme the name of the CSS theme, may be <code>null</code>, if given it is included into
* the CSS file name: '<name>-<theme>.css'
* @return the inherited CSS; HTML characters are escaped; <code>null</code> if there is no
* inherited CSS
* @throws IOException thrown in case of an I/O Error while reading the global CSS file
*/
public String getInheritedCss(String projectName, String formatterName, String name, String theme)
throws IOException {
return getInheritedCss(
projectCache.get(new Project.NameKey(projectName)), formatterName, name, theme);
}
private String getInheritedCss(
ProjectState project, String formatterName, String name, String theme) throws IOException {
for (ProjectState parent : project.parents()) {
String css = getCss(parent.getProject().getName(), name, theme);
ConfigSection cfg = formatters.getFormatterConfig(formatterName, parent);
if (cfg.getBoolean(KEY_INHERIT_CSS, true)) {
return joinCss(getInheritedCss(parent, formatterName, name, theme), css);
} else {
return css;
}
}
return getGlobalCss(name, theme);
}
private String joinCss(String css1, String css2) {
if (css1 == null) {
return css2;
}
if (css2 == null) {
return css1;
}
return Joiner.on('\n').join(css1, css2);
}
/**
* Returns the CSS from the file "<review-site>/data/<plugin-name>/css/<name>-<theme>.css".
*
* <p>If theme is <code>null</code> or empty, the CSS from the file
* "<review-site>/data/<plugin-name>/css/<name>.css" is returned.
*
* @param name the name of the CSS file without theme and without the ".css" file extension
* @param theme the name of the CSS theme, may be <code>null</code>, if given it is included into
* the CSS file name: '<name>-<theme>.css'
* @return the CSS from the file; HTML characters are escaped; <code>null</code> if the file
* doesn't exist
* @throws IOException thrown in case of an I/O Error while reading the CSS file
*/
public String getGlobalCss(String name, String theme) throws IOException {
return Strings.isNullOrEmpty(theme) ? getGlobalCss(name) : getGlobalCss(name + "-" + theme);
}
/**
* Returns the CSS from the file "<review-site>/data/<plugin-name>/css/<name>.css".
*
* @param name the name of the CSS file without the ".css" file extension
* @return the CSS from the file; HTML characters are escaped; <code>null</code> if the file
* doesn't exist
* @throws IOException thrown in case of an I/O Error while reading the CSS file
*/
public String getGlobalCss(String name) throws IOException {
Path p = Paths.get(baseDir.getAbsolutePath(), "css", name + ".css");
if (Files.exists(p)) {
byte[] css = Files.readAllBytes(p);
return escapeHtml(new String(css, UTF_8));
}
return null;
}
public String applyCss(String html, String formatterName, String projectName) throws IOException {
ConfigSection projectCfg = formatters.getFormatterConfig(formatterName, projectName);
String cssName = formatterName.toLowerCase(Locale.US);
String cssTheme = projectCfg.getString(KEY_CSS_THEME);
String defaultCss = getDefaultCss(formatterName);
String inheritedCss = getInheritedCss(projectName, formatterName, cssName, cssTheme);
String projectCss = getCss(projectName, cssName, cssTheme);
if (projectCfg.getBoolean(KEY_INHERIT_CSS, true)) {
return insertCss(html, MoreObjects.firstNonNull(inheritedCss, defaultCss), projectCss);
} else {
return insertCss(
html,
MoreObjects.firstNonNull(projectCss, MoreObjects.firstNonNull(inheritedCss, defaultCss)));
}
}
private String getDefaultCss(String formatterName) throws IOException {
String css = defaultCss.get(formatterName);
if (css == null) {
URL url = FormatterUtil.class.getResource(formatterName.toLowerCase(Locale.US) + ".css");
if (url != null) {
try (InputStream in = url.openStream();
TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024)) {
tmp.copy(in);
css = new String(tmp.toByteArray(), UTF_8);
}
} else {
log.info(String.format("No default CSS for formatter '%s' found.", formatterName));
css = "";
}
defaultCss.put(formatterName, css);
}
return css;
}
/**
* Inserts the given CSS into the given HTML.
*
* @param html the HTML
* @param css the CSS, may be <code>null</code>
* @return the HTML that includes the CSS
*/
public String insertCss(String html, String css) {
if (html == null || css == null) {
return html;
}
return insertCss(html, css, null);
}
/**
* Inserts the given CSS's into the given HTML.
*
* @param html the HTML
* @param css1 first CSS, may be <code>null</code>
* @param css2 second CSS, may be <code>null</code>
* @return the HTML that includes the CSS
*/
public String insertCss(String html, String css1, String css2) {
if (html == null || (css1 == null && css2 == null)) {
return html;
}
int p = html.lastIndexOf("</body>");
if (p > 0) {
StringBuilder b = new StringBuilder();
b.append(html.substring(0, p));
if (css1 != null) {
b.append("<style type=\"text/css\">\n");
b.append(css1);
b.append("</style>\n");
}
if (css2 != null) {
b.append("<style type=\"text/css\">\n");
b.append(css2);
b.append("</style>\n");
}
b.append(html.substring(p));
return b.toString();
} else {
return html;
}
}
/**
* Returns the content of the specified file from the "<plugin-name>/" folder of the
* ref/meta/config branch.
*
* @param projectName the name of the project
* @param fileName the name of the file in the "<plugin-name>/" folder
* @return the file content, <code>null</code> if the file doesn't exist
*/
public String getMetaConfigFile(String projectName, String fileName) {
try (Repository repo = repoManager.openRepository(new Project.NameKey(projectName))) {
try (RevWalk rw = new RevWalk(repo)) {
ObjectId id = repo.resolve(RefNames.REFS_CONFIG);
if (id == null) {
return null;
}
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(pluginName + "/" + fileName));
if (!tw.next()) {
return null;
}
ObjectId objectId = tw.getObjectId(0);
ObjectLoader loader = repo.open(objectId);
byte[] raw = loader.getBytes(Integer.MAX_VALUE);
return new String(raw, UTF_8);
}
}
} catch (IOException e) {
return null;
}
}
}