Move the workflow package to be strictly server side

We likely cannot invoke this on the client anymore, so move it all
to be server side.

Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2009-07-27 16:26:55 -07:00
parent 8d79fd29a5
commit f66ed7cb94
9 changed files with 13 additions and 13 deletions

View File

@@ -37,10 +37,10 @@ import com.google.gerrit.client.reviewdb.RevId;
import com.google.gerrit.client.reviewdb.ReviewDb;
import com.google.gerrit.client.rpc.Common;
import com.google.gerrit.client.rpc.NoSuchEntityException;
import com.google.gerrit.client.workflow.CategoryFunction;
import com.google.gerrit.client.workflow.FunctionState;
import com.google.gerrit.server.BaseServiceImplementation.Action;
import com.google.gerrit.server.BaseServiceImplementation.Failure;
import com.google.gerrit.server.workflow.CategoryFunction;
import com.google.gerrit.server.workflow.FunctionState;
import com.google.gwtorm.client.OrmException;
import java.util.ArrayList;

View File

@@ -25,9 +25,9 @@ import com.google.gerrit.client.reviewdb.PatchSet;
import com.google.gerrit.client.reviewdb.ReviewDb;
import com.google.gerrit.client.rpc.Common;
import com.google.gerrit.client.rpc.NoSuchEntityException;
import com.google.gerrit.client.workflow.CategoryFunction;
import com.google.gerrit.client.workflow.FunctionState;
import com.google.gerrit.git.MergeQueue;
import com.google.gerrit.server.workflow.CategoryFunction;
import com.google.gerrit.server.workflow.FunctionState;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwtjsonrpc.client.VoidResult;
import com.google.gwtorm.client.OrmException;

View File

@@ -33,8 +33,6 @@ import com.google.gerrit.client.reviewdb.SystemConfig;
import com.google.gerrit.client.reviewdb.TrustedExternalId;
import com.google.gerrit.client.reviewdb.SystemConfig.LoginType;
import com.google.gerrit.client.rpc.Common;
import com.google.gerrit.client.workflow.NoOpFunction;
import com.google.gerrit.client.workflow.SubmitFunction;
import com.google.gerrit.git.MergeQueue;
import com.google.gerrit.git.PushAllProjectsOp;
import com.google.gerrit.git.PushQueue;
@@ -42,6 +40,8 @@ import com.google.gerrit.git.WorkQueue;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.patch.DiffCacheEntryFactory;
import com.google.gerrit.server.ssh.SshKeyCacheEntryFactory;
import com.google.gerrit.server.workflow.NoOpFunction;
import com.google.gerrit.server.workflow.SubmitFunction;
import com.google.gwtjsonrpc.server.SignedToken;
import com.google.gwtjsonrpc.server.XsrfException;
import com.google.gwtorm.client.OrmException;

View File

@@ -0,0 +1,100 @@
// Copyright (C) 2008 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.workflow;
import com.google.gerrit.client.data.ApprovalType;
import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.ApprovalCategory;
import com.google.gerrit.client.reviewdb.ChangeApproval;
import com.google.gerrit.client.reviewdb.ProjectRight;
import java.util.HashMap;
import java.util.Map;
/** Function to control {@link ChangeApproval}s in an {@link ApprovalCategory}. */
public abstract class CategoryFunction {
private static Map<String, CategoryFunction> all =
new HashMap<String, CategoryFunction>();
static {
all.put(SubmitFunction.NAME, new SubmitFunction());
all.put(MaxWithBlock.NAME, new MaxWithBlock());
all.put(NoOpFunction.NAME, new NoOpFunction());
}
/**
* Locate a function by category.
*
* @param category the category the function is for.
* @return the function implementation; {@link NoOpFunction} if the function
* is not known to Gerrit and thus cannot be executed.
*/
public static CategoryFunction forCategory(final ApprovalCategory category) {
final CategoryFunction r = forName(category.getFunctionName());
return r != null ? r : new NoOpFunction();
}
/**
* Locate a function by name.
*
* @param functionName the function's unique name.
* @return the function implementation; null if the function is not known to
* Gerrit and thus cannot be executed.
*/
public static CategoryFunction forName(final String functionName) {
return all.get(functionName);
}
/**
* Normalize ChangeApprovals and set the valid flag for this category.
* <p>
* Implementors should invoke:
*
* <pre>
* state.valid(at, true);
* </pre>
* <p>
* If the set of approvals from <code>state.getApprovals(at)</code> covers the
* requirements for the function, indicating the category has been completed.
* <p>
* An example implementation which requires at least one positive and no
* negatives might be:
*
* <pre>
* boolean neg = false, pos = false;
* for (final ChangeApproval ca : state.getApprovals(at)) {
* state.normalize(ca);
* neg |= ca.getValue() &lt; 0;
* pos |= ca.getValue() &gt; 0;
* }
* state.valid(at, !neg &amp;&amp; pos);
* </pre>
*
* @param at the cached category description to process.
* @param state state to read approvals and project rights from, and to update
* the valid status into.
*/
public abstract void run(ApprovalType at, FunctionState state);
public boolean isValid(final Account.Id accountId, final ApprovalType at,
final FunctionState state) {
for (final ProjectRight pr : state.getAllRights(at)) {
if (state.isMember(accountId, pr.getAccountGroupId())
&& (pr.getMinValue() < 0 || pr.getMaxValue() > 0)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,250 @@
// Copyright (C) 2008 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.workflow;
import com.google.gerrit.client.data.ApprovalType;
import com.google.gerrit.client.data.ProjectCache;
import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.AccountGroup;
import com.google.gerrit.client.reviewdb.ApprovalCategory;
import com.google.gerrit.client.reviewdb.ApprovalCategoryValue;
import com.google.gerrit.client.reviewdb.Change;
import com.google.gerrit.client.reviewdb.ChangeApproval;
import com.google.gerrit.client.reviewdb.Project;
import com.google.gerrit.client.reviewdb.ProjectRight;
import com.google.gerrit.client.reviewdb.ApprovalCategory.Id;
import com.google.gerrit.client.rpc.Common;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/** State passed through to a {@link CategoryFunction}. */
public class FunctionState {
private final Map<Account.Id, Set<AccountGroup.Id>> groupCache =
new HashMap<Account.Id, Set<AccountGroup.Id>>();
private final Map<ApprovalCategory.Id, Collection<ChangeApproval>> approvals =
new HashMap<ApprovalCategory.Id, Collection<ChangeApproval>>();
private final Map<ApprovalCategory.Id, Boolean> valid =
new HashMap<ApprovalCategory.Id, Boolean>();
private final Change change;
private final ProjectCache.Entry project;
private final Map<ApprovalCategory.Id, Collection<ProjectRight>> allRights =
new HashMap<ApprovalCategory.Id, Collection<ProjectRight>>();
private Map<ApprovalCategory.Id, Collection<ProjectRight>> projectRights;
private Map<ApprovalCategory.Id, Collection<ProjectRight>> wildcardRights;
private Set<ChangeApproval> modified;
public FunctionState(final Change c, final Collection<ChangeApproval> all) {
change = c;
project = Common.getProjectCache().get(change.getDest().getParentKey());
for (final ChangeApproval ca : all) {
Collection<ChangeApproval> l = approvals.get(ca.getCategoryId());
if (l == null) {
l = new ArrayList<ChangeApproval>();
approvals.put(ca.getCategoryId(), l);
}
l.add(ca);
}
}
public Change getChange() {
return change;
}
public Project getProject() {
return project.getProject();
}
public void valid(final ApprovalType at, final boolean v) {
valid.put(id(at), v);
}
public boolean isValid(final ApprovalType at) {
return isValid(id(at));
}
public boolean isValid(final ApprovalCategory.Id id) {
final Boolean b = valid.get(id);
return b != null && b;
}
public Collection<ChangeApproval> getApprovals(final ApprovalType at) {
return getApprovals(id(at));
}
public Collection<ChangeApproval> getApprovals(final ApprovalCategory.Id id) {
final Collection<ChangeApproval> l = approvals.get(id);
return l != null ? l : Collections.<ChangeApproval> emptySet();
}
public void dirty(final ChangeApproval ap) {
if (modified == null) {
modified = new HashSet<ChangeApproval>();
}
modified.add(ap);
}
public Collection<ChangeApproval> getDirtyChangeApprovals() {
if (modified != null) {
return modified;
}
return Collections.emptySet();
}
public Collection<ProjectRight> getProjectRights(final ApprovalType at) {
return getProjectRights(id(at));
}
public Collection<ProjectRight> getProjectRights(final ApprovalCategory.Id id) {
if (projectRights == null) {
projectRights = index(project.getRights());
}
final Collection<ProjectRight> l = projectRights.get(id);
return l != null ? l : Collections.<ProjectRight> emptySet();
}
public Collection<ProjectRight> getWildcardRights(final ApprovalType at) {
return getWildcardRights(id(at));
}
public Collection<ProjectRight> getWildcardRights(final ApprovalCategory.Id id) {
if (wildcardRights == null) {
wildcardRights = index(Common.getProjectCache().getWildcardRights());
}
final Collection<ProjectRight> l = wildcardRights.get(id);
return l != null ? l : Collections.<ProjectRight> emptySet();
}
public Collection<ProjectRight> getAllRights(final ApprovalType at) {
return getAllRights(id(at));
}
public Collection<ProjectRight> getAllRights(final ApprovalCategory.Id id) {
Collection<ProjectRight> l = allRights.get(id);
if (l == null) {
l = new ArrayList<ProjectRight>();
l.addAll(getProjectRights(id));
l.addAll(getWildcardRights(id));
l = Collections.unmodifiableCollection(l);
allRights.put(id, l);
}
return l;
}
private static Map<Id, Collection<ProjectRight>> index(
final Collection<ProjectRight> rights) {
final HashMap<ApprovalCategory.Id, Collection<ProjectRight>> r;
r = new HashMap<ApprovalCategory.Id, Collection<ProjectRight>>();
for (final ProjectRight pr : rights) {
Collection<ProjectRight> l = r.get(pr.getApprovalCategoryId());
if (l == null) {
l = new ArrayList<ProjectRight>();
r.put(pr.getApprovalCategoryId(), l);
}
l.add(pr);
}
return r;
}
public boolean isMember(final ChangeApproval ca, final ProjectRight r) {
return isMember(ca.getAccountId(), r.getAccountGroupId());
}
public boolean isMember(final Account.Id accountId,
final AccountGroup.Id groupId) {
return getGroups(accountId).contains(groupId);
}
public Set<AccountGroup.Id> getGroups(final Account.Id id) {
Set<AccountGroup.Id> g = groupCache.get(id);
if (g == null) {
g = Common.getGroupCache().getEffectiveGroups(id);
groupCache.put(id, g);
}
return g;
}
/**
* Normalize the approval record down to the range permitted by the type, in
* case the type was modified since the approval was originally granted.
* <p>
* If the record's value was modified, its automatically marked as dirty.
*/
public void applyTypeFloor(final ApprovalType at, final ChangeApproval a) {
final ApprovalCategoryValue atMin = at.getMin();
if (atMin != null && a.getValue() < atMin.getValue()) {
a.setValue(atMin.getValue());
dirty(a);
}
final ApprovalCategoryValue atMax = at.getMax();
if (atMax != null && a.getValue() > atMax.getValue()) {
a.setValue(atMax.getValue());
dirty(a);
}
}
/**
* Normalize the approval record to be inside the maximum range permitted by
* the ProjectRights granted to groups the account is a member of.
* <p>
* If multiple ProjectRights are matched (assigned to different groups the
* account is a member of) the lowest minValue and the highest maxValue of the
* union of them is used.
* <p>
* If the record's value was modified, its automatically marked as dirty.
*/
public void applyRightFloor(final ChangeApproval a) {
// Find the maximal range actually granted to the user.
//
short minAllowed = 0, maxAllowed = 0;
for (final ProjectRight r : getAllRights(a.getCategoryId())) {
if (isMember(a, r)) {
minAllowed = (short) Math.min(minAllowed, r.getMinValue());
maxAllowed = (short) Math.max(maxAllowed, r.getMaxValue());
}
}
// Normalize the value into that range, returning true if we changed
// the value.
//
if (a.getValue() < minAllowed) {
a.setValue(minAllowed);
dirty(a);
} else if (a.getValue() > maxAllowed) {
a.setValue(maxAllowed);
dirty(a);
}
}
/** Run <code>applyTypeFloor</code>, <code>applyRightFloor</code>. */
public void normalize(final ApprovalType at, final ChangeApproval ca) {
applyTypeFloor(at, ca);
applyRightFloor(ca);
}
private static Id id(final ApprovalType at) {
return at.getCategory().getId();
}
}

View File

@@ -0,0 +1,63 @@
// Copyright (C) 2008 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.workflow;
import com.google.gerrit.client.data.ApprovalType;
import com.google.gerrit.client.reviewdb.ApprovalCategory;
import com.google.gerrit.client.reviewdb.ChangeApproval;
/**
* Computes an {@link ApprovalCategory} by looking at maximum values.
* <p>
* In order to be considered "approved" this function requires that:
* <ul>
* <li>The maximum negative value is never used;</li>
* <li>The maximum positive value is used at least once;</li>
* <li>The user approving the maximum positive has been granted that.</li>
* </ul>
* <p>
* This function is primarily useful for review fields, with values such as:
* <ul>
* <li>+2: Approved change.</li>
* <li>+1: Looks ok, but get another approval from someone with more depth.</li>
* <li>-1: Soft reject, it isn't a great change but its OK if approved.</li>
* <li>-2: Rejected, must not be submitted.
* </ul>
* <p>
* Note that projects using this function would typically want to assign out the
* middle range (-1 .. +1) to almost everyone, so people can indicate how they
* feel about a change, but the extremes of -2 and +2 should be reserved for the
* project's long-term maintainers, those who are most familiar with its code.
*/
public class MaxWithBlock extends CategoryFunction {
public static String NAME = "MaxWithBlock";
@Override
public void run(final ApprovalType at, final FunctionState state) {
boolean rejected = false;
boolean passed = false;
for (final ChangeApproval a : state.getApprovals(at)) {
state.normalize(at, a);
rejected |= at.isMaxNegative(a);
passed |= at.isMaxPositive(a);
}
// The type must not have had its max negative (a forceful reject)
// and must have at least one max positive (a full accept).
//
state.valid(at, !rejected && passed);
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (C) 2008 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.workflow;
import com.google.gerrit.client.data.ApprovalType;
import com.google.gerrit.client.reviewdb.Account;
/** A function that does nothing. */
public class NoOpFunction extends CategoryFunction {
public static String NAME = "NoOp";
@Override
public void run(final ApprovalType at, final FunctionState state) {
}
@Override
public boolean isValid(final Account.Id accountId, final ApprovalType at,
final FunctionState state) {
return false;
}
}

View File

@@ -0,0 +1,67 @@
// Copyright (C) 2008 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.workflow;
import com.google.gerrit.client.data.ApprovalType;
import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.ApprovalCategory;
import com.google.gerrit.client.reviewdb.Change;
import com.google.gerrit.client.reviewdb.ProjectRight;
import com.google.gerrit.client.rpc.Common;
/**
* Computes if the submit function can be used.
* <p>
* In order to be considered "approved" this function requires that all approval
* categories with a position >= 0 (that is any whose
* {@link ApprovalCategory#isAction()} method returns false) is valid and that
* the change state be {@link Change.Status#NEW}.
* <p>
* This is mostly useful for actions, like {@link ApprovalCategory#SUBMIT}.
*/
public class SubmitFunction extends CategoryFunction {
public static String NAME = "Submit";
@Override
public void run(final ApprovalType at, final FunctionState state) {
state.valid(at, valid(at, state));
}
@Override
public boolean isValid(final Account.Id accountId, final ApprovalType at,
final FunctionState state) {
if (valid(at, state)) {
for (final ProjectRight pr : state.getAllRights(at)) {
if (state.isMember(accountId, pr.getAccountGroupId())
&& pr.getMaxValue() > 0) {
return true;
}
}
}
return false;
}
private static boolean valid(final ApprovalType at, final FunctionState state) {
if (state.getChange().getStatus() != Change.Status.NEW) {
return false;
}
for (final ApprovalType t : Common.getGerritConfig().getApprovalTypes()) {
if (!state.isValid(t)) {
return false;
}
}
return true;
}
}