com.google.gerrit.rules.RulesCache.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.rules.RulesCache.java

Source

// 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.rules;

import static com.googlecode.prolog_cafe.lang.PrologMachineCopy.save;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Project;
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.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import com.googlecode.prolog_cafe.exceptions.CompileException;
import com.googlecode.prolog_cafe.exceptions.SyntaxException;
import com.googlecode.prolog_cafe.exceptions.TermException;
import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
import com.googlecode.prolog_cafe.lang.ListTerm;
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.PrologClassLoader;
import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
import com.googlecode.prolog_cafe.lang.StructureTerm;
import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;

import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.lib.Config;
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.util.RawParseUtils;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.io.StringReader;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Manages a cache of compiled Prolog rules.
 * <p>
 * Rules are loaded from the {@code site_path/cache/rules/rules-SHA1.jar}, where
 * {@code SHA1} is the SHA1 of the Prolog {@code rules.pl} in a project's
 * {@link RefNames#REFS_CONFIG} branch.
 */
@Singleton
public class RulesCache {
    /** Maximum size of a dynamic Prolog script, in bytes. */
    private static final int SRC_LIMIT = 128 * 1024;

    /** Default size of the internal Prolog database within each interpreter. */
    private static final int DB_MAX = 256;

    private static final List<String> PACKAGE_LIST = ImmutableList.of(Prolog.BUILTIN, "gerrit");

    private final Map<ObjectId, MachineRef> machineCache = new HashMap<>();

    private final ReferenceQueue<PrologMachineCopy> dead = new ReferenceQueue<>();

    private static final class MachineRef extends WeakReference<PrologMachineCopy> {
        final ObjectId key;

        MachineRef(ObjectId key, PrologMachineCopy pcm, ReferenceQueue<PrologMachineCopy> queue) {
            super(pcm, queue);
            this.key = key;
        }
    }

    private final boolean enableProjectRules;
    private final Path cacheDir;
    private final Path rulesDir;
    private final GitRepositoryManager gitMgr;
    private final DynamicSet<PredicateProvider> predicateProviders;
    private final ClassLoader systemLoader;
    private final PrologMachineCopy defaultMachine;

    @Inject
    protected RulesCache(@GerritServerConfig Config config, SitePaths site, GitRepositoryManager gm,
            DynamicSet<PredicateProvider> predicateProviders) {
        enableProjectRules = config.getBoolean("rules", null, "enable", true);
        cacheDir = site.resolve(config.getString("cache", null, "directory"));
        rulesDir = cacheDir != null ? cacheDir.resolve("rules") : null;
        gitMgr = gm;
        this.predicateProviders = predicateProviders;

        systemLoader = getClass().getClassLoader();
        defaultMachine = save(newEmptyMachine(systemLoader));
    }

    public boolean isProjectRulesEnabled() {
        return enableProjectRules;
    }

    /**
     * Locate a cached Prolog machine state, or create one if not available.
     *
     * @return a Prolog machine, after loading the specified rules.
     * @throws CompileException the machine cannot be created.
     */
    public synchronized PrologMachineCopy loadMachine(Project.NameKey project, ObjectId rulesId)
            throws CompileException {
        if (!enableProjectRules || project == null || rulesId == null) {
            return defaultMachine;
        }

        Reference<? extends PrologMachineCopy> ref = machineCache.get(rulesId);
        if (ref != null) {
            PrologMachineCopy pmc = ref.get();
            if (pmc != null) {
                return pmc;
            }

            machineCache.remove(rulesId);
            ref.enqueue();
        }

        gc();

        PrologMachineCopy pcm = createMachine(project, rulesId);
        MachineRef newRef = new MachineRef(rulesId, pcm, dead);
        machineCache.put(rulesId, newRef);
        return pcm;
    }

    public PrologMachineCopy loadMachine(String name, Reader in) throws CompileException {
        PrologMachineCopy pmc = consultRules(name, in);
        if (pmc == null) {
            throw new CompileException("Cannot consult rules from the stream " + name);
        }
        return pmc;
    }

    private void gc() {
        Reference<?> ref;
        while ((ref = dead.poll()) != null) {
            ObjectId key = ((MachineRef) ref).key;
            if (machineCache.get(key) == ref) {
                machineCache.remove(key);
            }
        }
    }

    private PrologMachineCopy createMachine(Project.NameKey project, ObjectId rulesId) throws CompileException {
        // If the rules are available as a complied JAR on local disk, prefer
        // that over dynamic consult as the bytecode will be faster.
        //
        if (rulesDir != null) {
            Path jarPath = rulesDir.resolve("rules-" + rulesId.getName() + ".jar");
            if (Files.isRegularFile(jarPath)) {
                URL[] cp = new URL[] { toURL(jarPath) };
                return save(newEmptyMachine(new URLClassLoader(cp, systemLoader)));
            }
        }

        // Dynamically consult the rules into the machine's internal database.
        //
        String rules = read(project, rulesId);
        PrologMachineCopy pmc = consultRules("rules.pl", new StringReader(rules));
        if (pmc == null) {
            throw new CompileException("Cannot consult rules of " + project);
        }
        return pmc;
    }

    private PrologMachineCopy consultRules(String name, Reader rules) throws CompileException {
        BufferingPrologControl ctl = newEmptyMachine(systemLoader);
        PushbackReader in = new PushbackReader(rules, Prolog.PUSHBACK_SIZE);
        try {
            if (!ctl.execute(Prolog.BUILTIN, "consult_stream", SymbolTerm.intern(name), new JavaObjectTerm(in))) {
                return null;
            }
        } catch (SyntaxException e) {
            throw new CompileException(e.toString(), e);
        } catch (TermException e) {
            Term m = e.getMessageTerm();
            if (m instanceof StructureTerm && "syntax_error".equals(m.name()) && m.arity() >= 1) {
                StringBuilder msg = new StringBuilder();
                if (m.arg(0) instanceof ListTerm) {
                    msg.append(Joiner.on(' ').join(((ListTerm) m.arg(0)).toJava()));
                } else {
                    msg.append(m.arg(0).toString());
                }
                if (m.arity() == 2 && m.arg(1) instanceof StructureTerm && "at".equals(m.arg(1).name())) {
                    Term at = m.arg(1).arg(0).dereference();
                    if (at instanceof ListTerm) {
                        msg.append(" at: ");
                        msg.append(prettyProlog(at));
                    }
                }
                throw new CompileException(msg.toString(), e);
            }
            throw new CompileException("Error while consulting rules from " + name, e);
        } catch (RuntimeException e) {
            throw new CompileException("Error while consulting rules from " + name, e);
        }
        return save(ctl);
    }

    private static String prettyProlog(Term at) {
        StringBuilder b = new StringBuilder();
        for (Object o : ((ListTerm) at).toJava()) {
            if (o instanceof Term) {
                Term t = (Term) o;
                if (!(t instanceof StructureTerm)) {
                    b.append(t.toString()).append(' ');
                    continue;
                }
                switch (t.name()) {
                case "atom":
                    SymbolTerm atom = (SymbolTerm) t.arg(0);
                    b.append(atom.toString());
                    break;
                case "var":
                    b.append(t.arg(0).toString());
                    break;
                }
            } else {
                b.append(o);
            }
        }
        return b.toString().trim();
    }

    private String read(Project.NameKey project, ObjectId rulesId) throws CompileException {
        try (Repository git = gitMgr.openRepository(project)) {
            try {
                ObjectLoader ldr = git.open(rulesId, Constants.OBJ_BLOB);
                byte[] raw = ldr.getCachedBytes(SRC_LIMIT);
                return RawParseUtils.decode(raw);
            } catch (LargeObjectException e) {
                throw new CompileException("rules of " + project + " are too large", e);
            } catch (RuntimeException | IOException e) {
                throw new CompileException("Cannot load rules of " + project, e);
            }
        } catch (IOException e) {
            throw new CompileException("Cannot open repository " + project, e);
        }
    }

    private BufferingPrologControl newEmptyMachine(ClassLoader cl) {
        BufferingPrologControl ctl = new BufferingPrologControl();
        ctl.setMaxDatabaseSize(DB_MAX);
        ctl.setPrologClassLoader(new PrologClassLoader(new PredicateClassLoader(predicateProviders, cl)));
        ctl.setEnabled(EnumSet.allOf(Prolog.Feature.class), false);

        List<String> packages = Lists.newArrayList();
        packages.addAll(PACKAGE_LIST);
        for (PredicateProvider predicateProvider : predicateProviders) {
            packages.addAll(predicateProvider.getPackages());
        }

        // Bootstrap the interpreter and ensure there is clean state.
        ctl.initialize(packages.toArray(new String[packages.size()]));
        return ctl;
    }

    private static URL toURL(Path jarPath) throws CompileException {
        try {
            return jarPath.toUri().toURL();
        } catch (MalformedURLException e) {
            throw new CompileException("Cannot create URL for " + jarPath, e);
        }
    }
}