diff options
Diffstat (limited to 'gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java')
-rw-r--r-- | gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java | 673 |
1 files changed, 0 insertions, 673 deletions
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java deleted file mode 100644 index f501dd3a7b..0000000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java +++ /dev/null @@ -1,673 +0,0 @@ -// Copyright (C) 2012 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.server.project; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; - -import com.google.gerrit.common.Nullable; -import com.google.gerrit.common.data.SubmitRecord; -import com.google.gerrit.common.data.SubmitTypeRecord; -import com.google.gerrit.extensions.client.SubmitType; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gerrit.rules.PrologEnvironment; -import com.google.gerrit.rules.StoredValues; -import com.google.gerrit.server.CurrentUser; -import com.google.gerrit.server.account.AccountCache; -import com.google.gerrit.server.account.Accounts; -import com.google.gerrit.server.account.Emails; -import com.google.gerrit.server.query.change.ChangeData; -import com.google.gwtorm.server.OrmException; -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; -import com.googlecode.prolog_cafe.exceptions.CompileException; -import com.googlecode.prolog_cafe.exceptions.ReductionLimitException; -import com.googlecode.prolog_cafe.lang.IntegerTerm; -import com.googlecode.prolog_cafe.lang.ListTerm; -import com.googlecode.prolog_cafe.lang.Prolog; -import com.googlecode.prolog_cafe.lang.StructureTerm; -import com.googlecode.prolog_cafe.lang.SymbolTerm; -import com.googlecode.prolog_cafe.lang.Term; -import com.googlecode.prolog_cafe.lang.VariableTerm; -import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Evaluates a submit-like Prolog rule found in the rules.pl file of the current project and filters - * the results through rules found in the parent projects, all the way up to All-Projects. - */ -public class SubmitRuleEvaluator { - private static final Logger log = LoggerFactory.getLogger(SubmitRuleEvaluator.class); - - private static final String DEFAULT_MSG = "Error evaluating project rules, check server log"; - - public static List<SubmitRecord> defaultRuleError() { - return createRuleError(DEFAULT_MSG); - } - - public static List<SubmitRecord> createRuleError(String err) { - SubmitRecord rec = new SubmitRecord(); - rec.status = SubmitRecord.Status.RULE_ERROR; - rec.errorMessage = err; - return Collections.singletonList(rec); - } - - public static SubmitTypeRecord defaultTypeError() { - return SubmitTypeRecord.error(DEFAULT_MSG); - } - - /** - * Exception thrown when the label term of a submit record unexpectedly didn't contain a user - * term. - */ - private static class UserTermExpected extends Exception { - private static final long serialVersionUID = 1L; - - UserTermExpected(SubmitRecord.Label label) { - super(String.format("A label with the status %s must contain a user.", label.toString())); - } - } - - public interface Factory { - SubmitRuleEvaluator create(CurrentUser user, ChangeData cd); - } - - private final AccountCache accountCache; - private final Accounts accounts; - private final Emails emails; - private final ProjectCache projectCache; - private final ChangeData cd; - - private SubmitRuleOptions.Builder optsBuilder = SubmitRuleOptions.defaults(); - private SubmitRuleOptions opts; - private Change change; - private CurrentUser user; - private PatchSet patchSet; - private boolean logErrors = true; - private long reductionsConsumed; - private ProjectState projectState; - - private Term submitRule; - - @Inject - SubmitRuleEvaluator( - AccountCache accountCache, - Accounts accounts, - Emails emails, - ProjectCache projectCache, - @Assisted CurrentUser user, - @Assisted ChangeData cd) { - this.accountCache = accountCache; - this.accounts = accounts; - this.emails = emails; - this.projectCache = projectCache; - this.user = user; - this.cd = cd; - } - - /** - * @return immutable snapshot of options configured so far. If neither {@link #getSubmitRule()} - * nor {@link #getSubmitType()} have been called yet, state within this instance is still - * mutable, so may change before evaluation. The instance's options are frozen at evaluation - * time. - */ - public SubmitRuleOptions getOptions() { - if (opts != null) { - return opts; - } - return optsBuilder.build(); - } - - public SubmitRuleEvaluator setOptions(SubmitRuleOptions opts) { - checkNotStarted(); - if (opts != null) { - optsBuilder = opts.toBuilder(); - } else { - optsBuilder = SubmitRuleOptions.defaults(); - } - return this; - } - - /** - * @param ps patch set of the change to evaluate. If not set, the current patch set will be loaded - * from {@link #evaluate()} or {@link #getSubmitType}. - * @return this - */ - public SubmitRuleEvaluator setPatchSet(PatchSet ps) { - checkArgument( - ps.getId().getParentKey().equals(cd.getId()), - "Patch set %s does not match change %s", - ps.getId(), - cd.getId()); - patchSet = ps; - return this; - } - - /** - * @param fast if true assume reviewers are permitted to use label values currently stored on the - * change. Fast mode bypasses some reviewer permission checks. - * @return this - */ - public SubmitRuleEvaluator setFastEvalLabels(boolean fast) { - checkNotStarted(); - optsBuilder.fastEvalLabels(fast); - return this; - } - - /** - * @param allow whether to allow {@link #evaluate()} on closed changes. - * @return this - */ - public SubmitRuleEvaluator setAllowClosed(boolean allow) { - checkNotStarted(); - optsBuilder.allowClosed(allow); - return this; - } - - /** - * @param skip if true, submit filter will not be applied. - * @return this - */ - public SubmitRuleEvaluator setSkipSubmitFilters(boolean skip) { - checkNotStarted(); - optsBuilder.skipFilters(skip); - return this; - } - - /** - * @param rule custom rule to use, or null to use refs/meta/config:rules.pl. - * @return this - */ - public SubmitRuleEvaluator setRule(@Nullable String rule) { - checkNotStarted(); - optsBuilder.rule(rule); - return this; - } - - /** - * @param log whether to log error messages in addition to returning error records. If true, error - * record messages will be less descriptive. - */ - public SubmitRuleEvaluator setLogErrors(boolean log) { - logErrors = log; - return this; - } - - /** @return Prolog reductions consumed during evaluation. */ - public long getReductionsConsumed() { - return reductionsConsumed; - } - - /** - * Evaluate the submit rules. - * - * @return List of {@link SubmitRecord} objects returned from the evaluated rules, including any - * errors. - */ - public List<SubmitRecord> evaluate() { - initOptions(); - try { - init(); - } catch (OrmException e) { - return ruleError("Error looking up change " + cd.getId(), e); - } - - if (!opts.allowClosed() && change.getStatus().isClosed()) { - SubmitRecord rec = new SubmitRecord(); - rec.status = SubmitRecord.Status.CLOSED; - return Collections.singletonList(rec); - } - - List<Term> results; - try { - results = - evaluateImpl( - "locate_submit_rule", - "can_submit", - "locate_submit_filter", - "filter_submit_results", - user); - } catch (RuleEvalException e) { - return ruleError(e.getMessage(), e); - } - - if (results.isEmpty()) { - // This should never occur. A well written submit rule will always produce - // at least one result informing the caller of the labels that are - // required for this change to be submittable. Each label will indicate - // whether or not that is actually possible given the permissions. - return ruleError( - String.format( - "Submit rule '%s' for change %s of %s has no solution.", - getSubmitRuleName(), cd.getId(), getProjectName())); - } - - return resultsToSubmitRecord(getSubmitRule(), results); - } - - /** - * Convert the results from Prolog Cafe's format to Gerrit's common format. - * - * <p>can_submit/1 terminates when an ok(P) record is found. Therefore walk the results backwards, - * using only that ok(P) record if it exists. This skips partial results that occur early in the - * output. Later after the loop the out collection is reversed to restore it to the original - * ordering. - */ - private List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) { - List<SubmitRecord> out = new ArrayList<>(results.size()); - for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) { - Term submitRecord = results.get(resultIdx); - SubmitRecord rec = new SubmitRecord(); - out.add(rec); - - if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) { - return invalidResult(submitRule, submitRecord); - } - - if ("ok".equals(submitRecord.name())) { - rec.status = SubmitRecord.Status.OK; - - } else if ("not_ready".equals(submitRecord.name())) { - rec.status = SubmitRecord.Status.NOT_READY; - - } else { - return invalidResult(submitRule, submitRecord); - } - - // Unpack the one argument. This should also be a structure with one - // argument per label that needs to be reported on to the caller. - // - submitRecord = submitRecord.arg(0); - - if (!(submitRecord instanceof StructureTerm)) { - return invalidResult(submitRule, submitRecord); - } - - rec.labels = new ArrayList<>(submitRecord.arity()); - - for (Term state : ((StructureTerm) submitRecord).args()) { - if (!(state instanceof StructureTerm) - || 2 != state.arity() - || !"label".equals(state.name())) { - return invalidResult(submitRule, submitRecord); - } - - SubmitRecord.Label lbl = new SubmitRecord.Label(); - rec.labels.add(lbl); - - lbl.label = state.arg(0).name(); - Term status = state.arg(1); - - try { - if ("ok".equals(status.name())) { - lbl.status = SubmitRecord.Label.Status.OK; - appliedBy(lbl, status); - - } else if ("reject".equals(status.name())) { - lbl.status = SubmitRecord.Label.Status.REJECT; - appliedBy(lbl, status); - - } else if ("need".equals(status.name())) { - lbl.status = SubmitRecord.Label.Status.NEED; - - } else if ("may".equals(status.name())) { - lbl.status = SubmitRecord.Label.Status.MAY; - - } else if ("impossible".equals(status.name())) { - lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE; - - } else { - return invalidResult(submitRule, submitRecord); - } - } catch (UserTermExpected e) { - return invalidResult(submitRule, submitRecord, e.getMessage()); - } - } - - if (rec.status == SubmitRecord.Status.OK) { - break; - } - } - Collections.reverse(out); - - return out; - } - - private List<SubmitRecord> invalidResult(Term rule, Term record, String reason) { - return ruleError( - String.format( - "Submit rule %s for change %s of %s output invalid result: %s%s", - rule, - cd.getId(), - getProjectName(), - record, - (reason == null ? "" : ". Reason: " + reason))); - } - - private List<SubmitRecord> invalidResult(Term rule, Term record) { - return invalidResult(rule, record, null); - } - - private List<SubmitRecord> ruleError(String err) { - return ruleError(err, null); - } - - private List<SubmitRecord> ruleError(String err, Exception e) { - if (logErrors) { - if (e == null) { - log.error(err); - } else { - log.error(err, e); - } - return defaultRuleError(); - } - return createRuleError(err); - } - - /** - * Evaluate the submit type rules to get the submit type. - * - * @return record from the evaluated rules. - */ - public SubmitTypeRecord getSubmitType() { - initOptions(); - try { - init(); - } catch (OrmException e) { - return typeError("Error looking up change " + cd.getId(), e); - } - - List<Term> results; - try { - results = - evaluateImpl( - "locate_submit_type", - "get_submit_type", - "locate_submit_type_filter", - "filter_submit_type_results", - // Do not include current user in submit type evaluation. This is used - // for mergeability checks, which are stored persistently and so must - // have a consistent view of the submit type. - null); - } catch (RuleEvalException e) { - return typeError(e.getMessage(), e); - } - - if (results.isEmpty()) { - // Should never occur for a well written rule - return typeError( - "Submit rule '" - + getSubmitRuleName() - + "' for change " - + cd.getId() - + " of " - + getProjectName() - + " has no solution."); - } - - Term typeTerm = results.get(0); - if (!(typeTerm instanceof SymbolTerm)) { - return typeError( - "Submit rule '" - + getSubmitRuleName() - + "' for change " - + cd.getId() - + " of " - + getProjectName() - + " did not return a symbol."); - } - - String typeName = ((SymbolTerm) typeTerm).name(); - try { - return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase())); - } catch (IllegalArgumentException e) { - return typeError( - "Submit type rule " - + getSubmitRule() - + " for change " - + cd.getId() - + " of " - + getProjectName() - + " output invalid result: " - + typeName); - } - } - - private SubmitTypeRecord typeError(String err) { - return typeError(err, null); - } - - private SubmitTypeRecord typeError(String err, Exception e) { - if (logErrors) { - if (e == null) { - log.error(err); - } else { - log.error(err, e); - } - return defaultTypeError(); - } - return SubmitTypeRecord.error(err); - } - - private List<Term> evaluateImpl( - String userRuleLocatorName, - String userRuleWrapperName, - String filterRuleLocatorName, - String filterRuleWrapperName, - CurrentUser user) - throws RuleEvalException { - PrologEnvironment env = getPrologEnvironment(user); - try { - Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm()); - if (opts.fastEvalLabels()) { - env.once("gerrit", "assume_range_from_label"); - } - - List<Term> results = new ArrayList<>(); - try { - for (Term[] template : env.all("gerrit", userRuleWrapperName, sr, new VariableTerm())) { - results.add(template[1]); - } - } catch (ReductionLimitException err) { - throw new RuleEvalException( - String.format( - "%s on change %d of %s", err.getMessage(), cd.getId().get(), getProjectName())); - } catch (RuntimeException err) { - throw new RuleEvalException( - String.format( - "Exception calling %s on change %d of %s", sr, cd.getId().get(), getProjectName()), - err); - } finally { - reductionsConsumed = env.getReductions(); - } - - Term resultsTerm = toListTerm(results); - if (!opts.skipFilters()) { - resultsTerm = - runSubmitFilters(resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName); - } - List<Term> r; - if (resultsTerm instanceof ListTerm) { - r = new ArrayList<>(); - for (Term t = resultsTerm; t instanceof ListTerm; ) { - ListTerm l = (ListTerm) t; - r.add(l.car().dereference()); - t = l.cdr().dereference(); - } - } else { - r = Collections.emptyList(); - } - submitRule = sr; - return r; - } finally { - env.close(); - } - } - - private PrologEnvironment getPrologEnvironment(CurrentUser user) throws RuleEvalException { - PrologEnvironment env; - try { - if (opts.rule() == null) { - env = projectState.newPrologEnvironment(); - } else { - env = projectState.newPrologEnvironment("stdin", new StringReader(opts.rule())); - } - } catch (CompileException err) { - String msg; - if (opts.rule() == null) { - msg = String.format("Cannot load rules.pl for %s: %s", getProjectName(), err.getMessage()); - } else { - msg = err.getMessage(); - } - throw new RuleEvalException(msg, err); - } - env.set(StoredValues.ACCOUNTS, accounts); - env.set(StoredValues.ACCOUNT_CACHE, accountCache); - env.set(StoredValues.EMAILS, emails); - env.set(StoredValues.REVIEW_DB, cd.db()); - env.set(StoredValues.CHANGE_DATA, cd); - if (user != null) { - env.set(StoredValues.CURRENT_USER, user); - } - env.set(StoredValues.PROJECT_STATE, projectState); - return env; - } - - private Term runSubmitFilters( - Term results, - PrologEnvironment env, - String filterRuleLocatorName, - String filterRuleWrapperName) - throws RuleEvalException { - PrologEnvironment childEnv = env; - for (ProjectState parentState : projectState.parents()) { - PrologEnvironment parentEnv; - try { - parentEnv = parentState.newPrologEnvironment(); - } catch (CompileException err) { - throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err); - } - - parentEnv.copyStoredValues(childEnv); - Term filterRule = parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm()); - try { - if (opts.fastEvalLabels()) { - env.once("gerrit", "assume_range_from_label"); - } - - Term[] template = - parentEnv.once( - "gerrit", filterRuleWrapperName, filterRule, results, new VariableTerm()); - results = template[2]; - } catch (ReductionLimitException err) { - throw new RuleEvalException( - String.format( - "%s on change %d of %s", - err.getMessage(), cd.getId().get(), parentState.getName())); - } catch (RuntimeException err) { - throw new RuleEvalException( - String.format( - "Exception calling %s on change %d of %s", - filterRule, cd.getId().get(), parentState.getName()), - err); - } finally { - reductionsConsumed += env.getReductions(); - } - childEnv = parentEnv; - } - return results; - } - - private static Term toListTerm(List<Term> terms) { - Term list = Prolog.Nil; - for (int i = terms.size() - 1; i >= 0; i--) { - list = new ListTerm(terms.get(i), list); - } - return list; - } - - private void appliedBy(SubmitRecord.Label label, Term status) throws UserTermExpected { - if (status instanceof StructureTerm && status.arity() == 1) { - Term who = status.arg(0); - if (isUser(who)) { - label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue()); - } else { - throw new UserTermExpected(label); - } - } - } - - private static boolean isUser(Term who) { - return who instanceof StructureTerm - && who.arity() == 1 - && who.name().equals("user") - && who.arg(0) instanceof IntegerTerm; - } - - public Term getSubmitRule() { - checkState(submitRule != null, "getSubmitRule() invalid before evaluation"); - return submitRule; - } - - public String getSubmitRuleName() { - return submitRule != null ? submitRule.toString() : "<unknown rule>"; - } - - private void checkNotStarted() { - checkState(opts == null, "cannot set options after starting evaluation"); - } - - private void initOptions() { - if (opts == null) { - opts = optsBuilder.build(); - optsBuilder = null; - } - } - - private void init() throws OrmException { - if (change == null) { - change = cd.change(); - if (change == null) { - throw new OrmException("No change found"); - } - } - - if (projectState == null) { - try { - projectState = projectCache.checkedGet(change.getProject()); - } catch (IOException e) { - throw new OrmException("Can't load project state", e); - } - } - - if (patchSet == null) { - patchSet = cd.currentPatchSet(); - if (patchSet == null) { - throw new OrmException("No patch set found"); - } - } - } - - private String getProjectName() { - return projectState.getName(); - } -} |