summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/pgm/rules/PrologCompiler.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/pgm/rules/PrologCompiler.java')
-rw-r--r--java/com/google/gerrit/pgm/rules/PrologCompiler.java306
1 files changed, 306 insertions, 0 deletions
diff --git a/java/com/google/gerrit/pgm/rules/PrologCompiler.java b/java/com/google/gerrit/pgm/rules/PrologCompiler.java
new file mode 100644
index 0000000000..2663f42f51
--- /dev/null
+++ b/java/com/google/gerrit/pgm/rules/PrologCompiler.java
@@ -0,0 +1,306 @@
+// Copyright (C) 2011 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.google.gerrit.pgm.rules;
+
+import com.google.gerrit.common.Version;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlecode.prolog_cafe.compiler.Compiler;
+import com.googlecode.prolog_cafe.exceptions.CompileException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Callable;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ToolProvider;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Helper class for Rulec: does the actual prolog -> java src -> class -> jar work Finds rules.pl in
+ * refs/meta/config branch Creates rules-(sha1 of rules.pl).jar in (site-path)/cache/rules
+ */
+public class PrologCompiler implements Callable<PrologCompiler.Status> {
+ public interface Factory {
+ PrologCompiler create(Repository git);
+ }
+
+ public enum Status {
+ NO_RULES,
+ COMPILED
+ }
+
+ private final Path ruleDir;
+ private final Repository git;
+
+ @Inject
+ PrologCompiler(
+ @GerritServerConfig Config config, SitePaths site, @Assisted Repository gitRepository) {
+ Path cacheDir = site.resolve(config.getString("cache", null, "directory"));
+ ruleDir = cacheDir != null ? cacheDir.resolve("rules") : null;
+ git = gitRepository;
+ }
+
+ @Override
+ public Status call() throws IOException, CompileException {
+ ObjectId metaConfig = git.resolve(RefNames.REFS_CONFIG);
+ if (metaConfig == null) {
+ return Status.NO_RULES;
+ }
+
+ ObjectId rulesId = git.resolve(metaConfig.name() + ":rules.pl");
+ if (rulesId == null) {
+ return Status.NO_RULES;
+ }
+
+ if (ruleDir == null) {
+ throw new CompileException("Caching not enabled");
+ }
+ Files.createDirectories(ruleDir);
+
+ File tempDir = File.createTempFile("GerritCodeReview_", ".rulec");
+ if (!tempDir.delete() || !tempDir.mkdir()) {
+ throw new IOException("Cannot create " + tempDir);
+ }
+ try {
+ // Try to make the directory accessible only by this process.
+ // This may help to prevent leaking rule data to outsiders.
+ tempDir.setReadable(true, true);
+ tempDir.setWritable(true, true);
+ tempDir.setExecutable(true, true);
+
+ compileProlog(rulesId, tempDir);
+ compileJava(tempDir);
+
+ Path jarPath = ruleDir.resolve("rules-" + rulesId.getName() + ".jar");
+ List<String> classFiles = getRelativePaths(tempDir, ".class");
+ createJar(jarPath, classFiles, tempDir, metaConfig, rulesId);
+
+ return Status.COMPILED;
+ } finally {
+ deleteAllFiles(tempDir);
+ }
+ }
+
+ /** Creates a copy of rules.pl and compiles it into Java sources. */
+ private void compileProlog(ObjectId prolog, File tempDir) throws IOException, CompileException {
+ File tempRules = copyToTempFile(prolog, tempDir);
+ try {
+ Compiler comp = new Compiler();
+ comp.prologToJavaSource(tempRules.getPath(), tempDir.getPath());
+ } finally {
+ tempRules.delete();
+ }
+ }
+
+ private File copyToTempFile(ObjectId blobId, File tempDir)
+ throws IOException, FileNotFoundException, MissingObjectException {
+ // Any leak of tmp caused by this method failing will be cleaned
+ // up by our caller when tempDir is recursively deleted.
+ File tmp = File.createTempFile("rules", ".pl", tempDir);
+ try (OutputStream out = Files.newOutputStream(tmp.toPath())) {
+ git.open(blobId).copyTo(out);
+ }
+ return tmp;
+ }
+
+ /** Compile java src into java .class files */
+ private void compileJava(File tempDir) throws IOException, CompileException {
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ if (compiler == null) {
+ throw new CompileException("JDK required (running inside of JRE)");
+ }
+
+ DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
+ try (StandardJavaFileManager fileManager =
+ compiler.getStandardFileManager(diagnostics, null, null)) {
+ Iterable<? extends JavaFileObject> compilationUnits =
+ fileManager.getJavaFileObjectsFromFiles(getAllFiles(tempDir, ".java"));
+ ArrayList<String> options = new ArrayList<>();
+ String classpath = getMyClasspath();
+ if (classpath != null) {
+ options.add("-classpath");
+ options.add(classpath);
+ }
+ options.add("-d");
+ options.add(tempDir.getPath());
+ JavaCompiler.CompilationTask task =
+ compiler.getTask(null, fileManager, diagnostics, options, null, compilationUnits);
+ if (!task.call()) {
+ Locale myLocale = Locale.getDefault();
+ StringBuilder msg = new StringBuilder();
+ msg.append("Cannot compile to Java bytecode:");
+ for (Diagnostic<? extends JavaFileObject> err : diagnostics.getDiagnostics()) {
+ msg.append('\n');
+ msg.append(err.getKind());
+ msg.append(": ");
+ if (err.getSource() != null) {
+ msg.append(err.getSource().getName());
+ }
+ msg.append(':');
+ msg.append(err.getLineNumber());
+ msg.append(": ");
+ msg.append(err.getMessage(myLocale));
+ }
+ throw new CompileException(msg.toString());
+ }
+ }
+ }
+
+ private String getMyClasspath() {
+ StringBuilder cp = new StringBuilder();
+ appendClasspath(cp, getClass().getClassLoader());
+ return 0 < cp.length() ? cp.toString() : null;
+ }
+
+ private void appendClasspath(StringBuilder cp, ClassLoader classLoader) {
+ if (classLoader.getParent() != null) {
+ appendClasspath(cp, classLoader.getParent());
+ }
+ if (classLoader instanceof URLClassLoader) {
+ for (URL url : ((URLClassLoader) classLoader).getURLs()) {
+ if ("file".equals(url.getProtocol())) {
+ if (0 < cp.length()) {
+ cp.append(File.pathSeparatorChar);
+ }
+ cp.append(url.getPath());
+ }
+ }
+ }
+ }
+
+ /** Takes compiled prolog .class files, puts them into the jar file. */
+ private void createJar(
+ Path archiveFile, List<String> toBeJared, File tempDir, ObjectId metaConfig, ObjectId rulesId)
+ throws IOException {
+ long now = TimeUtil.nowMs();
+ Manifest mf = new Manifest();
+ mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ mf.getMainAttributes().putValue("Built-by", "Gerrit Code Review " + Version.getVersion());
+ if (git.getDirectory() != null) {
+ mf.getMainAttributes().putValue("Source-Repository", git.getDirectory().getPath());
+ }
+ mf.getMainAttributes().putValue("Source-Commit", metaConfig.name());
+ mf.getMainAttributes().putValue("Source-Blob", rulesId.name());
+
+ Path tmpjar = Files.createTempFile(archiveFile.getParent(), ".rulec_", ".jar");
+ try (OutputStream stream = Files.newOutputStream(tmpjar);
+ JarOutputStream out = new JarOutputStream(stream, mf)) {
+ byte[] buffer = new byte[10240];
+ // TODO: fixify this loop
+ for (String path : toBeJared) {
+ JarEntry jarAdd = new JarEntry(path);
+ File f = new File(tempDir, path);
+ jarAdd.setTime(now);
+ out.putNextEntry(jarAdd);
+ if (f.isFile()) {
+ try (InputStream in = Files.newInputStream(f.toPath())) {
+ while (true) {
+ int nRead = in.read(buffer, 0, buffer.length);
+ if (nRead <= 0) {
+ break;
+ }
+ out.write(buffer, 0, nRead);
+ }
+ }
+ }
+ out.closeEntry();
+ }
+ }
+
+ try {
+ Files.move(tmpjar, archiveFile);
+ } catch (IOException e) {
+ throw new IOException("Cannot replace " + archiveFile, e);
+ }
+ }
+
+ private List<File> getAllFiles(File dir, String extension) throws IOException {
+ ArrayList<File> fileList = new ArrayList<>();
+ getAllFiles(dir, extension, fileList);
+ return fileList;
+ }
+
+ private void getAllFiles(File dir, String extension, List<File> fileList) throws IOException {
+ for (File f : listFiles(dir)) {
+ if (f.getName().endsWith(extension)) {
+ fileList.add(f);
+ }
+ if (f.isDirectory()) {
+ getAllFiles(f, extension, fileList);
+ }
+ }
+ }
+
+ private List<String> getRelativePaths(File dir, String extension) throws IOException {
+ ArrayList<String> pathList = new ArrayList<>();
+ getRelativePaths(dir, extension, "", pathList);
+ return pathList;
+ }
+
+ private static void getRelativePaths(
+ File dir, String extension, String path, List<String> pathList) throws IOException {
+ for (File f : listFiles(dir)) {
+ if (f.getName().endsWith(extension)) {
+ pathList.add(path + f.getName());
+ }
+ if (f.isDirectory()) {
+ getRelativePaths(f, extension, path + f.getName() + "/", pathList);
+ }
+ }
+ }
+
+ private static void deleteAllFiles(File dir) throws IOException {
+ for (File f : listFiles(dir)) {
+ if (f.isDirectory()) {
+ deleteAllFiles(f);
+ } else {
+ f.delete();
+ }
+ }
+ dir.delete();
+ }
+
+ private static File[] listFiles(File dir) throws IOException {
+ File[] files = dir.listFiles();
+ if (files == null) {
+ throw new IOException("Failed to list directory: " + dir);
+ }
+ return files;
+ }
+}