Files
gerrit/java/com/google/gerrit/server/change/ChangeJson.java
Dave Borowitz a4fe999a75 Make StorageException a RuntimeException
In the context of a Gerrit server, StorageExceptions are generally
speaking not recoverable: the only place that should be catching them is
a top-level request handler such as RestApiServlet. Such a handler also
needs to be able to catch RuntimeExceptions, and indeed RestApiServlet
already catches bare Exception.

The general best practice in Java today is to use unchecked exception
when errors are not intended to be recoverable[citation needed]. This
avoids polluting interfaces with too many checked exception types.

After this change, most "throws StorageException" clauses are redundant
and can be cleaned up. However, that work is left for future commits;
this commit does the minimum work required to compile.

This commit does not additionally touch DuplicateKeyException, which
also becomes a RuntimeException since it inherits from StorageException.

Change-Id: I75230cf99145d886a5d872150e333519eb54acbb
2019-04-17 19:42:10 -07:00

810 lines
31 KiB
Java

// 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.change;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_COMMITS;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_ACCOUNTS;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.extensions.client.ListChangesOption.LABELS;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWER_UPDATES;
import static com.google.gerrit.extensions.client.ListChangesOption.SKIP_MERGEABLE;
import static com.google.gerrit.extensions.client.ListChangesOption.SUBMITTABLE;
import static com.google.gerrit.extensions.client.ListChangesOption.TRACKING_IDS;
import static com.google.gerrit.server.ChangeMessagesUtil.createChangeMessageInfo;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRecord.Status;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.api.changes.FixInput;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.ProblemInfo;
import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.index.query.QueryResult;
import com.google.gerrit.mail.Address;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.account.AccountInfoComparator;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.RemoveReviewerControl;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import org.eclipse.jgit.lib.Config;
/**
* Produces {@link ChangeInfo} (which is serialized to JSON afterwards) from {@link ChangeData}.
*
* <p>This is intended to be used on request scope, but may be used for converting multiple {@link
* ChangeData} objects from different sources.
*/
public class ChangeJson {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
ChangeField.SUBMIT_RULE_OPTIONS_LENIENT.toBuilder().build();
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
ChangeField.SUBMIT_RULE_OPTIONS_STRICT.toBuilder().build();
static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
ImmutableSet.of(
ALL_COMMITS,
ALL_REVISIONS,
CHANGE_ACTIONS,
CHECK,
COMMIT_FOOTERS,
CURRENT_ACTIONS,
CURRENT_COMMIT,
MESSAGES);
@Singleton
public static class Factory {
private final AssistedFactory factory;
@Inject
Factory(AssistedFactory factory) {
this.factory = factory;
}
public ChangeJson noOptions() {
return create(ImmutableSet.of());
}
public ChangeJson create(Iterable<ListChangesOption> options) {
return factory.create(options, Optional.empty());
}
public ChangeJson create(
Iterable<ListChangesOption> options,
PluginDefinedAttributesFactory pluginDefinedAttributesFactory) {
return factory.create(options, Optional.of(pluginDefinedAttributesFactory));
}
public ChangeJson create(ListChangesOption first, ListChangesOption... rest) {
return create(Sets.immutableEnumSet(first, rest));
}
}
public interface AssistedFactory {
ChangeJson create(
Iterable<ListChangesOption> options,
Optional<PluginDefinedAttributesFactory> pluginDefinedAttributesFactory);
}
@Singleton
private static class Metrics {
private final Timer0 toChangeInfoLatency;
private final Timer0 toChangeInfosLatency;
private final Timer0 formatQueryResultsLatency;
@Inject
Metrics(MetricMaker metricMaker) {
toChangeInfoLatency =
metricMaker.newTimer(
"http/server/rest_api/change_json/to_change_info_latency",
new Description("Latency for toChangeInfo invocations in ChangeJson")
.setCumulative()
.setUnit(Units.MILLISECONDS));
toChangeInfosLatency =
metricMaker.newTimer(
"http/server/rest_api/change_json/to_change_infos_latency",
new Description("Latency for toChangeInfos invocations in ChangeJson")
.setCumulative()
.setUnit(Units.MILLISECONDS));
formatQueryResultsLatency =
metricMaker.newTimer(
"http/server/rest_api/change_json/format_query_results_latency",
new Description("Latency for formatQueryResults invocations in ChangeJson")
.setCumulative()
.setUnit(Units.MILLISECONDS));
}
}
private final Provider<CurrentUser> userProvider;
private final PermissionBackend permissionBackend;
private final ChangeData.Factory changeDataFactory;
private final AccountLoader.Factory accountLoaderFactory;
private final ImmutableSet<ListChangesOption> options;
private final ChangeMessagesUtil cmUtil;
private final Provider<ConsistencyChecker> checkerProvider;
private final ActionJson actionJson;
private final ChangeNotes.Factory notesFactory;
private final LabelsJson labelsJson;
private final RemoveReviewerControl removeReviewerControl;
private final TrackingFooters trackingFooters;
private final Metrics metrics;
private final RevisionJson revisionJson;
private final Optional<PluginDefinedAttributesFactory> pluginDefinedAttributesFactory;
private final boolean excludeMergeableInChangeInfo;
private final boolean lazyLoad;
private AccountLoader accountLoader;
private FixInput fix;
@Inject
ChangeJson(
Provider<CurrentUser> user,
PermissionBackend permissionBackend,
ChangeData.Factory cdf,
AccountLoader.Factory ailf,
ChangeMessagesUtil cmUtil,
Provider<ConsistencyChecker> checkerProvider,
ActionJson actionJson,
ChangeNotes.Factory notesFactory,
LabelsJson.Factory labelsJsonFactory,
RemoveReviewerControl removeReviewerControl,
TrackingFooters trackingFooters,
Metrics metrics,
RevisionJson.Factory revisionJsonFactory,
@GerritServerConfig Config cfg,
@Assisted Iterable<ListChangesOption> options,
@Assisted Optional<PluginDefinedAttributesFactory> pluginDefinedAttributesFactory) {
this.userProvider = user;
this.changeDataFactory = cdf;
this.permissionBackend = permissionBackend;
this.accountLoaderFactory = ailf;
this.cmUtil = cmUtil;
this.checkerProvider = checkerProvider;
this.actionJson = actionJson;
this.notesFactory = notesFactory;
this.labelsJson = labelsJsonFactory.create(options);
this.removeReviewerControl = removeReviewerControl;
this.trackingFooters = trackingFooters;
this.metrics = metrics;
this.revisionJson = revisionJsonFactory.create(options);
this.options = Sets.immutableEnumSet(options);
this.excludeMergeableInChangeInfo =
cfg.getBoolean("change", "api", "excludeMergeableInChangeInfo", false);
this.lazyLoad = containsAnyOf(this.options, REQUIRE_LAZY_LOAD);
this.pluginDefinedAttributesFactory = pluginDefinedAttributesFactory;
logger.atFine().log("options = %s", options);
}
public ChangeJson fix(FixInput fix) {
this.fix = fix;
return this;
}
public ChangeInfo format(ChangeResource rsrc) throws StorageException {
return format(changeDataFactory.create(rsrc.getNotes()));
}
public ChangeInfo format(Change change) throws StorageException {
return format(changeDataFactory.create(change));
}
public ChangeInfo format(Project.NameKey project, Change.Id id) throws StorageException {
return format(project, id, ChangeInfo::new);
}
public ChangeInfo format(ChangeData cd) throws StorageException {
return format(cd, Optional.empty(), true, ChangeInfo::new);
}
public ChangeInfo format(RevisionResource rsrc) throws StorageException {
ChangeData cd = changeDataFactory.create(rsrc.getNotes());
return format(cd, Optional.of(rsrc.getPatchSet().getId()), true, ChangeInfo::new);
}
public List<List<ChangeInfo>> format(List<QueryResult<ChangeData>> in)
throws PermissionBackendException {
try (Timer0.Context ignored = metrics.formatQueryResultsLatency.start()) {
accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
List<List<ChangeInfo>> res = new ArrayList<>(in.size());
Map<Change.Id, ChangeInfo> cache = Maps.newHashMapWithExpectedSize(in.size());
for (QueryResult<ChangeData> r : in) {
List<ChangeInfo> infos = toChangeInfos(r.entities(), cache);
infos.forEach(c -> cache.put(new Change.Id(c._number), c));
if (!infos.isEmpty() && r.more()) {
infos.get(infos.size() - 1)._moreChanges = true;
}
res.add(infos);
}
accountLoader.fill();
return res;
}
}
public List<ChangeInfo> format(Collection<ChangeData> in)
throws StorageException, PermissionBackendException {
accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
ensureLoaded(in);
List<ChangeInfo> out = new ArrayList<>(in.size());
for (ChangeData cd : in) {
out.add(format(cd, Optional.empty(), false, ChangeInfo::new));
}
accountLoader.fill();
return out;
}
public <I extends ChangeInfo> I format(
Project.NameKey project, Change.Id id, Supplier<I> changeInfoSupplier)
throws StorageException {
ChangeNotes notes;
try {
notes = notesFactory.createChecked(project, id);
} catch (StorageException e) {
if (!has(CHECK)) {
throw e;
}
return checkOnly(changeDataFactory.create(project, id), changeInfoSupplier);
}
return format(changeDataFactory.create(notes), Optional.empty(), true, changeInfoSupplier);
}
private static Collection<SubmitRequirementInfo> requirementsFor(ChangeData cd) {
Collection<SubmitRequirementInfo> reqInfos = new ArrayList<>();
for (SubmitRecord submitRecord : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
if (submitRecord.requirements == null) {
continue;
}
for (SubmitRequirement requirement : submitRecord.requirements) {
reqInfos.add(requirementToInfo(requirement, submitRecord.status));
}
}
return reqInfos;
}
private static SubmitRequirementInfo requirementToInfo(SubmitRequirement req, Status status) {
return new SubmitRequirementInfo(status.name(), req.fallbackText(), req.type(), req.data());
}
private static void finish(ChangeInfo info) {
info.id =
Joiner.on('~')
.join(Url.encode(info.project), Url.encode(info.branch), Url.encode(info.changeId));
}
private static boolean containsAnyOf(
ImmutableSet<ListChangesOption> set, ImmutableSet<ListChangesOption> toFind) {
return !Sets.intersection(toFind, set).isEmpty();
}
private <I extends ChangeInfo> I format(
ChangeData cd,
Optional<PatchSet.Id> limitToPsId,
boolean fillAccountLoader,
Supplier<I> changeInfoSupplier)
throws StorageException {
try {
if (fillAccountLoader) {
accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
I res = toChangeInfo(cd, limitToPsId, changeInfoSupplier);
accountLoader.fill();
return res;
}
return toChangeInfo(cd, limitToPsId, changeInfoSupplier);
} catch (PatchListNotAvailableException
| GpgException
| IOException
| PermissionBackendException
| RuntimeException e) {
if (!has(CHECK)) {
Throwables.throwIfInstanceOf(e, StorageException.class);
throw new StorageException(e);
}
return checkOnly(cd, changeInfoSupplier);
}
}
private void ensureLoaded(Iterable<ChangeData> all) throws StorageException {
if (lazyLoad) {
ChangeData.ensureChangeLoaded(all);
if (has(ALL_REVISIONS)) {
ChangeData.ensureAllPatchSetsLoaded(all);
} else if (has(CURRENT_REVISION) || has(MESSAGES)) {
ChangeData.ensureCurrentPatchSetLoaded(all);
}
if (has(REVIEWED) && userProvider.get().isIdentifiedUser()) {
ChangeData.ensureReviewedByLoadedForOpenChanges(all);
}
ChangeData.ensureCurrentApprovalsLoaded(all);
} else {
for (ChangeData cd : all) {
cd.setLazyLoad(false);
}
}
}
private boolean has(ListChangesOption option) {
return options.contains(option);
}
private List<ChangeInfo> toChangeInfos(
List<ChangeData> changes, Map<Change.Id, ChangeInfo> cache) {
try (Timer0.Context ignored = metrics.toChangeInfosLatency.start()) {
List<ChangeInfo> changeInfos = new ArrayList<>(changes.size());
for (ChangeData cd : changes) {
ChangeInfo i = cache.get(cd.getId());
if (i != null) {
continue;
}
try {
ensureLoaded(Collections.singleton(cd));
changeInfos.add(format(cd, Optional.empty(), false, ChangeInfo::new));
} catch (RuntimeException e) {
logger.atWarning().withCause(e).log(
"Omitting corrupt change %s from results", cd.getId());
}
}
return changeInfos;
}
}
private <I extends ChangeInfo> I checkOnly(ChangeData cd, Supplier<I> changeInfoSupplier) {
ChangeNotes notes;
try {
notes = cd.notes();
} catch (StorageException e) {
String msg = "Error loading change";
logger.atWarning().withCause(e).log(msg + " %s", cd.getId());
I info = changeInfoSupplier.get();
info._number = cd.getId().get();
ProblemInfo p = new ProblemInfo();
p.message = msg;
info.problems = Lists.newArrayList(p);
return info;
}
ConsistencyChecker.Result result = checkerProvider.get().check(notes, fix);
I info = changeInfoSupplier.get();
Change c = result.change();
if (c != null) {
info.project = c.getProject().get();
info.branch = c.getDest().getShortName();
info.topic = c.getTopic();
info.changeId = c.getKey().get();
info.subject = c.getSubject();
info.status = c.getStatus().asChangeStatus();
info.owner = new AccountInfo(c.getOwner().get());
info.created = c.getCreatedOn();
info.updated = c.getLastUpdatedOn();
info._number = c.getId().get();
info.problems = result.problems();
info.isPrivate = c.isPrivate() ? true : null;
info.workInProgress = c.isWorkInProgress() ? true : null;
info.hasReviewStarted = c.hasReviewStarted();
finish(info);
} else {
info._number = result.id().get();
info.problems = result.problems();
}
return info;
}
private <I extends ChangeInfo> I toChangeInfo(
ChangeData cd, Optional<PatchSet.Id> limitToPsId, Supplier<I> changeInfoSupplier)
throws PatchListNotAvailableException, GpgException, StorageException,
PermissionBackendException, IOException {
try (Timer0.Context ignored = metrics.toChangeInfoLatency.start()) {
return toChangeInfoImpl(cd, limitToPsId, changeInfoSupplier);
}
}
private <I extends ChangeInfo> I toChangeInfoImpl(
ChangeData cd, Optional<PatchSet.Id> limitToPsId, Supplier<I> changeInfoSupplier)
throws PatchListNotAvailableException, GpgException, StorageException,
PermissionBackendException, IOException {
I out = changeInfoSupplier.get();
CurrentUser user = userProvider.get();
if (has(CHECK)) {
out.problems = checkerProvider.get().check(cd.notes(), fix).problems();
// If any problems were fixed, the ChangeData needs to be reloaded.
for (ProblemInfo p : out.problems) {
if (p.status == ProblemInfo.Status.FIXED) {
cd = changeDataFactory.create(cd.project(), cd.getId());
break;
}
}
}
Change in = cd.change();
out.project = in.getProject().get();
out.branch = in.getDest().getShortName();
out.topic = in.getTopic();
out.assignee = in.getAssignee() != null ? accountLoader.get(in.getAssignee()) : null;
out.hashtags = cd.hashtags();
out.changeId = in.getKey().get();
if (in.isNew()) {
SubmitTypeRecord str = cd.submitTypeRecord();
if (str.isOk()) {
out.submitType = str.type;
}
if (!excludeMergeableInChangeInfo && !has(SKIP_MERGEABLE)) {
out.mergeable = cd.isMergeable();
}
if (has(SUBMITTABLE)) {
out.submittable = submittable(cd);
}
}
Optional<ChangedLines> changedLines = cd.changedLines();
if (changedLines.isPresent()) {
out.insertions = changedLines.get().insertions;
out.deletions = changedLines.get().deletions;
}
out.isPrivate = in.isPrivate() ? true : null;
out.workInProgress = in.isWorkInProgress() ? true : null;
out.hasReviewStarted = in.hasReviewStarted();
out.subject = in.getSubject();
out.status = in.getStatus().asChangeStatus();
out.owner = accountLoader.get(in.getOwner());
out.created = in.getCreatedOn();
out.updated = in.getLastUpdatedOn();
out._number = in.getId().get();
out.totalCommentCount = cd.totalCommentCount();
out.unresolvedCommentCount = cd.unresolvedCommentCount();
if (user.isIdentifiedUser()) {
Collection<String> stars = cd.stars(user.getAccountId());
out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null;
if (!stars.isEmpty()) {
out.stars = stars;
}
}
if (in.isNew() && has(REVIEWED) && user.isIdentifiedUser()) {
out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null;
}
out.labels = labelsJson.labelsFor(accountLoader, cd, has(LABELS), has(DETAILED_LABELS));
out.requirements = requirementsFor(cd);
if (out.labels != null && has(DETAILED_LABELS)) {
// If limited to specific patch sets but not the current patch set, don't
// list permitted labels, since users can't vote on those patch sets.
if (user.isIdentifiedUser()
&& (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId()))) {
out.permittedLabels =
!cd.change().isAbandoned()
? labelsJson.permittedLabels(user.getAccountId(), cd)
: ImmutableMap.of();
}
out.reviewers = reviewerMap(cd.reviewers(), cd.reviewersByEmail(), false);
out.pendingReviewers = reviewerMap(cd.pendingReviewers(), cd.pendingReviewersByEmail(), true);
out.removableReviewers = removableReviewers(cd, out);
}
setSubmitter(cd, out);
if (pluginDefinedAttributesFactory.isPresent()) {
out.plugins = pluginDefinedAttributesFactory.get().create(cd);
}
out.revertOf = cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null;
if (has(REVIEWER_UPDATES)) {
out.reviewerUpdates = reviewerUpdates(cd);
}
boolean needMessages = has(MESSAGES);
boolean needRevisions = has(ALL_REVISIONS) || has(CURRENT_REVISION) || limitToPsId.isPresent();
Map<PatchSet.Id, PatchSet> src;
if (needMessages || needRevisions) {
src = loadPatchSets(cd, limitToPsId);
} else {
src = null;
}
if (needMessages) {
out.messages = messages(cd);
}
finish(out);
// This block must come after the ChangeInfo is mostly populated, since
// it will be passed to ActionVisitors as-is.
if (needRevisions) {
out.revisions = revisionJson.getRevisions(accountLoader, cd, src, limitToPsId, out);
if (out.revisions != null) {
for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) {
if (entry.getValue().isCurrent) {
out.currentRevision = entry.getKey();
break;
}
}
}
}
if (has(CURRENT_ACTIONS) || has(CHANGE_ACTIONS)) {
actionJson.addChangeActions(out, cd.notes());
}
if (has(TRACKING_IDS)) {
ListMultimap<String, String> set = trackingFooters.extract(cd.commitFooters());
out.trackingIds =
set.entries().stream()
.map(e -> new TrackingIdInfo(e.getKey(), e.getValue()))
.collect(toList());
}
return out;
}
private Map<ReviewerState, Collection<AccountInfo>> reviewerMap(
ReviewerSet reviewers, ReviewerByEmailSet reviewersByEmail, boolean includeRemoved) {
Map<ReviewerState, Collection<AccountInfo>> reviewerMap = new HashMap<>();
for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
if (!includeRemoved && state == ReviewerStateInternal.REMOVED) {
continue;
}
Collection<AccountInfo> reviewersByState = toAccountInfo(reviewers.byState(state));
reviewersByState.addAll(toAccountInfoByEmail(reviewersByEmail.byState(state)));
if (!reviewersByState.isEmpty()) {
reviewerMap.put(state.asReviewerState(), reviewersByState);
}
}
return reviewerMap;
}
private Collection<ReviewerUpdateInfo> reviewerUpdates(ChangeData cd) throws StorageException {
List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates();
List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size());
for (ReviewerStatusUpdate c : reviewerUpdates) {
ReviewerUpdateInfo change = new ReviewerUpdateInfo();
change.updated = c.date();
change.state = c.state().asReviewerState();
change.updatedBy = accountLoader.get(c.updatedBy());
change.reviewer = accountLoader.get(c.reviewer());
result.add(change);
}
return result;
}
private boolean submittable(ChangeData cd) {
return SubmitRecord.allRecordsOK(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT));
}
private void setSubmitter(ChangeData cd, ChangeInfo out) throws StorageException {
Optional<PatchSetApproval> s = cd.getSubmitApproval();
if (!s.isPresent()) {
return;
}
out.submitted = s.get().getGranted();
out.submitter = accountLoader.get(s.get().getAccountId());
}
private Collection<ChangeMessageInfo> messages(ChangeData cd) throws StorageException {
List<ChangeMessage> messages = cmUtil.byChange(cd.notes());
if (messages.isEmpty()) {
return Collections.emptyList();
}
List<ChangeMessageInfo> result = Lists.newArrayListWithCapacity(messages.size());
for (ChangeMessage message : messages) {
result.add(createChangeMessageInfo(message, accountLoader));
}
return result;
}
private Collection<AccountInfo> removableReviewers(ChangeData cd, ChangeInfo out)
throws PermissionBackendException, StorageException {
// Although this is called removableReviewers, this method also determines
// which CCs are removable.
//
// For reviewers, we need to look at each approval, because the reviewer
// should only be considered removable if *all* of their approvals can be
// removed. First, add all reviewers with *any* removable approval to the
// "removable" set. Along the way, if we encounter a non-removable approval,
// add the reviewer to the "fixed" set. Before we return, remove all members
// of "fixed" from "removable", because not all of their approvals can be
// removed.
Collection<LabelInfo> labels = out.labels.values();
Set<Account.Id> fixed = Sets.newHashSetWithExpectedSize(labels.size());
Set<Account.Id> removable = Sets.newHashSetWithExpectedSize(labels.size());
// Check if the user has the permission to remove a reviewer. This means we can bypass the
// testRemoveReviewer check for a specific reviewer in the loop saving potentially many
// permission checks.
boolean canRemoveAnyReviewer =
permissionBackendForChange(userProvider.get(), cd).test(ChangePermission.REMOVE_REVIEWER);
for (LabelInfo label : labels) {
if (label.all == null) {
continue;
}
for (ApprovalInfo ai : label.all) {
Account.Id id = new Account.Id(ai._accountId);
if (canRemoveAnyReviewer
|| removeReviewerControl.testRemoveReviewer(
cd, userProvider.get(), id, MoreObjects.firstNonNull(ai.value, 0))) {
removable.add(id);
} else {
fixed.add(id);
}
}
}
// CCs are simpler than reviewers. They are removable if the ChangeControl
// would permit a non-negative approval by that account to be removed, in
// which case add them to removable. We don't need to add unremovable CCs to
// "fixed" because we only visit each CC once here.
Collection<AccountInfo> ccs = out.reviewers.get(ReviewerState.CC);
if (ccs != null) {
for (AccountInfo ai : ccs) {
if (ai._accountId != null) {
Account.Id id = new Account.Id(ai._accountId);
if (canRemoveAnyReviewer
|| removeReviewerControl.testRemoveReviewer(cd, userProvider.get(), id, 0)) {
removable.add(id);
}
}
}
}
// Subtract any reviewers with non-removable approvals from the "removable"
// set. This also subtracts any CCs that for some reason also hold
// unremovable approvals.
removable.removeAll(fixed);
List<AccountInfo> result = Lists.newArrayListWithCapacity(removable.size());
for (Account.Id id : removable) {
result.add(accountLoader.get(id));
}
// Reviewers added by email are always removable
for (Collection<AccountInfo> infos : out.reviewers.values()) {
for (AccountInfo info : infos) {
if (info._accountId == null) {
result.add(info);
}
}
}
return result;
}
private Collection<AccountInfo> toAccountInfo(Collection<Account.Id> accounts) {
return accounts.stream()
.map(accountLoader::get)
.sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
.collect(toList());
}
private Collection<AccountInfo> toAccountInfoByEmail(Collection<Address> addresses) {
return addresses.stream()
.map(a -> new AccountInfo(a.getName(), a.getEmail()))
.sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
.collect(toList());
}
private Map<PatchSet.Id, PatchSet> loadPatchSets(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
throws StorageException {
Collection<PatchSet> src;
if (has(ALL_REVISIONS) || has(MESSAGES)) {
src = cd.patchSets();
} else {
PatchSet ps;
if (limitToPsId.isPresent()) {
ps = cd.patchSet(limitToPsId.get());
if (ps == null) {
throw new StorageException("missing patch set " + limitToPsId.get());
}
} else {
ps = cd.currentPatchSet();
if (ps == null) {
throw new StorageException("missing current patch set for change " + cd.getId());
}
}
src = Collections.singletonList(ps);
}
Map<PatchSet.Id, PatchSet> map = Maps.newHashMapWithExpectedSize(src.size());
for (PatchSet patchSet : src) {
map.put(patchSet.getId(), patchSet);
}
return map;
}
/**
* @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed
* from either an index-backed or a database-backed {@link ChangeData} depending on {@code
* lazyload}.
*/
private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd)
throws StorageException {
PermissionBackend.WithUser withUser = permissionBackend.user(user);
return lazyLoad
? withUser.change(cd)
: withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
}
}