summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/notedb/ChangeNotesState.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/notedb/ChangeNotesState.java')
-rw-r--r--java/com/google/gerrit/server/notedb/ChangeNotesState.java684
1 files changed, 684 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
new file mode 100644
index 0000000000..02fe4167a6
--- /dev/null
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -0,0 +1,684 @@
+// Copyright (C) 2016 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.notedb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
+import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.MESSAGE_CODEC;
+import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
+import static com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.toByteString;
+import static java.util.Objects.requireNonNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Converter;
+import com.google.common.base.Enums;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Table;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.mail.Address;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Comment;
+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.reviewdb.client.RevId;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.ReviewerByEmailSet;
+import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.ReviewerStatusUpdate;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Immutable state associated with a change meta ref at a given commit.
+ *
+ * <p>One instance is the output of a single {@link ChangeNotesParser}, and contains types required
+ * to support public methods on {@link ChangeNotes}. It is intended to be cached in-process.
+ *
+ * <p>Note that {@link ChangeNotes} contains more than just a single {@code ChangeNoteState}, such
+ * as per-draft information, so that class is not cached directly.
+ */
+@AutoValue
+public abstract class ChangeNotesState {
+ static ChangeNotesState empty(Change change) {
+ return Builder.empty(change.getId()).build();
+ }
+
+ static Builder builder() {
+ return new AutoValue_ChangeNotesState.Builder();
+ }
+
+ static ChangeNotesState create(
+ ObjectId metaId,
+ Change.Id changeId,
+ Change.Key changeKey,
+ Timestamp createdOn,
+ Timestamp lastUpdatedOn,
+ Account.Id owner,
+ String branch,
+ @Nullable PatchSet.Id currentPatchSetId,
+ String subject,
+ @Nullable String topic,
+ @Nullable String originalSubject,
+ @Nullable String submissionId,
+ @Nullable Account.Id assignee,
+ @Nullable Change.Status status,
+ Set<Account.Id> pastAssignees,
+ Set<String> hashtags,
+ Map<PatchSet.Id, PatchSet> patchSets,
+ ListMultimap<PatchSet.Id, PatchSetApproval> approvals,
+ ReviewerSet reviewers,
+ ReviewerByEmailSet reviewersByEmail,
+ ReviewerSet pendingReviewers,
+ ReviewerByEmailSet pendingReviewersByEmail,
+ List<Account.Id> allPastReviewers,
+ List<ReviewerStatusUpdate> reviewerUpdates,
+ List<SubmitRecord> submitRecords,
+ List<ChangeMessage> changeMessages,
+ ListMultimap<RevId, Comment> publishedComments,
+ @Nullable Timestamp readOnlyUntil,
+ boolean isPrivate,
+ boolean workInProgress,
+ boolean reviewStarted,
+ @Nullable Change.Id revertOf) {
+ requireNonNull(
+ metaId,
+ () ->
+ String.format(
+ "metaId is required when passing arguments to create(...)."
+ + " To create an empty %s without"
+ + " NoteDb data, use empty(...) instead",
+ ChangeNotesState.class.getSimpleName()));
+ return builder()
+ .metaId(metaId)
+ .changeId(changeId)
+ .columns(
+ ChangeColumns.builder()
+ .changeKey(changeKey)
+ .createdOn(createdOn)
+ .lastUpdatedOn(lastUpdatedOn)
+ .owner(owner)
+ .branch(branch)
+ .status(status)
+ .currentPatchSetId(currentPatchSetId)
+ .subject(subject)
+ .topic(topic)
+ .originalSubject(originalSubject)
+ .submissionId(submissionId)
+ .assignee(assignee)
+ .isPrivate(isPrivate)
+ .workInProgress(workInProgress)
+ .reviewStarted(reviewStarted)
+ .revertOf(revertOf)
+ .build())
+ .pastAssignees(pastAssignees)
+ .hashtags(hashtags)
+ .patchSets(patchSets.entrySet())
+ .approvals(approvals.entries())
+ .reviewers(reviewers)
+ .reviewersByEmail(reviewersByEmail)
+ .pendingReviewers(pendingReviewers)
+ .pendingReviewersByEmail(pendingReviewersByEmail)
+ .allPastReviewers(allPastReviewers)
+ .reviewerUpdates(reviewerUpdates)
+ .submitRecords(submitRecords)
+ .changeMessages(changeMessages)
+ .publishedComments(publishedComments)
+ .readOnlyUntil(readOnlyUntil)
+ .build();
+ }
+
+ /**
+ * Subset of Change columns that can be represented in NoteDb.
+ *
+ * <p>Notable exceptions include rowVersion and noteDbState, which are only make sense when read
+ * from NoteDb, so they cannot be cached.
+ *
+ * <p>Fields should match the column names in {@link Change}, and are in listed column order.
+ */
+ @AutoValue
+ abstract static class ChangeColumns {
+ static Builder builder() {
+ return new AutoValue_ChangeNotesState_ChangeColumns.Builder();
+ }
+
+ abstract Change.Key changeKey();
+
+ abstract Timestamp createdOn();
+
+ abstract Timestamp lastUpdatedOn();
+
+ abstract Account.Id owner();
+
+ // Project not included, as it's not stored anywhere in the meta ref.
+ abstract String branch();
+
+ // TODO(dborowitz): Use a sensible default other than null
+ @Nullable
+ abstract Change.Status status();
+
+ @Nullable
+ abstract PatchSet.Id currentPatchSetId();
+
+ abstract String subject();
+
+ @Nullable
+ abstract String topic();
+
+ @Nullable
+ abstract String originalSubject();
+
+ @Nullable
+ abstract String submissionId();
+
+ @Nullable
+ abstract Account.Id assignee();
+
+ abstract boolean isPrivate();
+
+ abstract boolean workInProgress();
+
+ abstract boolean reviewStarted();
+
+ @Nullable
+ abstract Change.Id revertOf();
+
+ abstract Builder toBuilder();
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder changeKey(Change.Key changeKey);
+
+ abstract Builder createdOn(Timestamp createdOn);
+
+ abstract Builder lastUpdatedOn(Timestamp lastUpdatedOn);
+
+ abstract Builder owner(Account.Id owner);
+
+ abstract Builder branch(String branch);
+
+ abstract Builder currentPatchSetId(@Nullable PatchSet.Id currentPatchSetId);
+
+ abstract Builder subject(String subject);
+
+ abstract Builder topic(@Nullable String topic);
+
+ abstract Builder originalSubject(@Nullable String originalSubject);
+
+ abstract Builder submissionId(@Nullable String submissionId);
+
+ abstract Builder assignee(@Nullable Account.Id assignee);
+
+ abstract Builder status(@Nullable Change.Status status);
+
+ abstract Builder isPrivate(boolean isPrivate);
+
+ abstract Builder workInProgress(boolean workInProgress);
+
+ abstract Builder reviewStarted(boolean reviewStarted);
+
+ abstract Builder revertOf(@Nullable Change.Id revertOf);
+
+ abstract ChangeColumns build();
+ }
+ }
+
+ // Only null if NoteDb is disabled.
+ @Nullable
+ abstract ObjectId metaId();
+
+ abstract Change.Id changeId();
+
+ // Only null if NoteDb is disabled.
+ @Nullable
+ abstract ChangeColumns columns();
+
+ // Other related to this Change.
+ abstract ImmutableSet<Account.Id> pastAssignees();
+
+ abstract ImmutableSet<String> hashtags();
+
+ abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSet>> patchSets();
+
+ abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSetApproval>> approvals();
+
+ abstract ReviewerSet reviewers();
+
+ abstract ReviewerByEmailSet reviewersByEmail();
+
+ abstract ReviewerSet pendingReviewers();
+
+ abstract ReviewerByEmailSet pendingReviewersByEmail();
+
+ abstract ImmutableList<Account.Id> allPastReviewers();
+
+ abstract ImmutableList<ReviewerStatusUpdate> reviewerUpdates();
+
+ abstract ImmutableList<SubmitRecord> submitRecords();
+
+ abstract ImmutableList<ChangeMessage> changeMessages();
+
+ abstract ImmutableListMultimap<RevId, Comment> publishedComments();
+
+ @Nullable
+ abstract Timestamp readOnlyUntil();
+
+ Change newChange(Project.NameKey project) {
+ ChangeColumns c = requireNonNull(columns(), "columns are required");
+ Change change =
+ new Change(
+ c.changeKey(),
+ changeId(),
+ c.owner(),
+ new Branch.NameKey(project, c.branch()),
+ c.createdOn());
+ copyNonConstructorColumnsTo(change);
+ change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
+ return change;
+ }
+
+ void copyColumnsTo(Change change) throws IOException {
+ ChangeColumns c = columns();
+ checkState(
+ c != null && metaId() != null,
+ "missing columns or metaId in ChangeNotesState; is NoteDb enabled? %s",
+ this);
+ checkMetaId(change);
+ change.setKey(c.changeKey());
+ change.setOwner(c.owner());
+ change.setDest(new Branch.NameKey(change.getProject(), c.branch()));
+ change.setCreatedOn(c.createdOn());
+ copyNonConstructorColumnsTo(change);
+ }
+
+ private void checkMetaId(Change change) throws IOException {
+ NoteDbChangeState state = NoteDbChangeState.parse(change);
+ if (state == null) {
+ return; // Can happen during small NoteDb tests.
+ } else if (state.getPrimaryStorage() == PrimaryStorage.NOTE_DB) {
+ return;
+ }
+ checkState(state.getRefState().isPresent(), "expected RefState: %s", state);
+ ObjectId idFromState = state.getRefState().get().changeMetaId();
+ if (!idFromState.equals(metaId())) {
+ throw new IOException(
+ "cannot copy ChangeNotesState into Change "
+ + changeId()
+ + "; this ChangeNotesState was created from "
+ + metaId()
+ + ", but change requires state "
+ + idFromState);
+ }
+ }
+
+ private void copyNonConstructorColumnsTo(Change change) {
+ ChangeColumns c = requireNonNull(columns(), "columns are required");
+ if (c.status() != null) {
+ change.setStatus(c.status());
+ }
+ change.setTopic(Strings.emptyToNull(c.topic()));
+ change.setLastUpdatedOn(c.lastUpdatedOn());
+ change.setSubmissionId(c.submissionId());
+ change.setAssignee(c.assignee());
+ change.setPrivate(c.isPrivate());
+ change.setWorkInProgress(c.workInProgress());
+ change.setReviewStarted(c.reviewStarted());
+ change.setRevertOf(c.revertOf());
+
+ if (!patchSets().isEmpty()) {
+ change.setCurrentPatchSet(c.currentPatchSetId(), c.subject(), c.originalSubject());
+ } else {
+ // TODO(dborowitz): This should be an error, but for now it's required for
+ // some tests to pass.
+ change.clearCurrentPatchSet();
+ }
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ static Builder empty(Change.Id changeId) {
+ return new AutoValue_ChangeNotesState.Builder()
+ .changeId(changeId)
+ .pastAssignees(ImmutableSet.of())
+ .hashtags(ImmutableSet.of())
+ .patchSets(ImmutableList.of())
+ .approvals(ImmutableList.of())
+ .reviewers(ReviewerSet.empty())
+ .reviewersByEmail(ReviewerByEmailSet.empty())
+ .pendingReviewers(ReviewerSet.empty())
+ .pendingReviewersByEmail(ReviewerByEmailSet.empty())
+ .allPastReviewers(ImmutableList.of())
+ .reviewerUpdates(ImmutableList.of())
+ .submitRecords(ImmutableList.of())
+ .changeMessages(ImmutableList.of())
+ .publishedComments(ImmutableListMultimap.of());
+ }
+
+ abstract Builder metaId(ObjectId metaId);
+
+ abstract Builder changeId(Change.Id changeId);
+
+ abstract Builder columns(ChangeColumns columns);
+
+ abstract Builder pastAssignees(Set<Account.Id> pastAssignees);
+
+ abstract Builder hashtags(Iterable<String> hashtags);
+
+ abstract Builder patchSets(Iterable<Map.Entry<PatchSet.Id, PatchSet>> patchSets);
+
+ abstract Builder approvals(Iterable<Map.Entry<PatchSet.Id, PatchSetApproval>> approvals);
+
+ abstract Builder reviewers(ReviewerSet reviewers);
+
+ abstract Builder reviewersByEmail(ReviewerByEmailSet reviewersByEmail);
+
+ abstract Builder pendingReviewers(ReviewerSet pendingReviewers);
+
+ abstract Builder pendingReviewersByEmail(ReviewerByEmailSet pendingReviewersByEmail);
+
+ abstract Builder allPastReviewers(List<Account.Id> allPastReviewers);
+
+ abstract Builder reviewerUpdates(List<ReviewerStatusUpdate> reviewerUpdates);
+
+ abstract Builder submitRecords(List<SubmitRecord> submitRecords);
+
+ abstract Builder changeMessages(List<ChangeMessage> changeMessages);
+
+ abstract Builder publishedComments(ListMultimap<RevId, Comment> publishedComments);
+
+ abstract Builder readOnlyUntil(@Nullable Timestamp readOnlyUntil);
+
+ abstract ChangeNotesState build();
+ }
+
+ enum Serializer implements CacheSerializer<ChangeNotesState> {
+ INSTANCE;
+
+ @VisibleForTesting static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
+
+ private static final Converter<String, Change.Status> STATUS_CONVERTER =
+ Enums.stringConverter(Change.Status.class);
+ private static final Converter<String, ReviewerStateInternal> REVIEWER_STATE_CONVERTER =
+ Enums.stringConverter(ReviewerStateInternal.class);
+
+ @Override
+ public byte[] serialize(ChangeNotesState object) {
+ checkArgument(object.metaId() != null, "meta ID is required in: %s", object);
+ checkArgument(object.columns() != null, "ChangeColumns is required in: %s", object);
+ ChangeNotesStateProto.Builder b = ChangeNotesStateProto.newBuilder();
+
+ b.setMetaId(ObjectIdConverter.create().toByteString(object.metaId()))
+ .setChangeId(object.changeId().get())
+ .setColumns(toChangeColumnsProto(object.columns()));
+
+ object.pastAssignees().forEach(a -> b.addPastAssignee(a.get()));
+ object.hashtags().forEach(b::addHashtag);
+ object.patchSets().forEach(e -> b.addPatchSet(toByteString(e.getValue(), PATCH_SET_CODEC)));
+ object.approvals().forEach(e -> b.addApproval(toByteString(e.getValue(), APPROVAL_CODEC)));
+
+ object.reviewers().asTable().cellSet().forEach(c -> b.addReviewer(toReviewerSetEntry(c)));
+ object
+ .reviewersByEmail()
+ .asTable()
+ .cellSet()
+ .forEach(c -> b.addReviewerByEmail(toReviewerByEmailSetEntry(c)));
+ object
+ .pendingReviewers()
+ .asTable()
+ .cellSet()
+ .forEach(c -> b.addPendingReviewer(toReviewerSetEntry(c)));
+ object
+ .pendingReviewersByEmail()
+ .asTable()
+ .cellSet()
+ .forEach(c -> b.addPendingReviewerByEmail(toReviewerByEmailSetEntry(c)));
+
+ object.allPastReviewers().forEach(a -> b.addPastReviewer(a.get()));
+ object.reviewerUpdates().forEach(u -> b.addReviewerUpdate(toReviewerStatusUpdateProto(u)));
+ object
+ .submitRecords()
+ .forEach(r -> b.addSubmitRecord(GSON.toJson(new StoredSubmitRecord(r))));
+ object.changeMessages().forEach(m -> b.addChangeMessage(toByteString(m, MESSAGE_CODEC)));
+ object.publishedComments().values().forEach(c -> b.addPublishedComment(GSON.toJson(c)));
+
+ if (object.readOnlyUntil() != null) {
+ b.setReadOnlyUntil(object.readOnlyUntil().getTime()).setHasReadOnlyUntil(true);
+ }
+
+ return ProtoCacheSerializers.toByteArray(b.build());
+ }
+
+ private static ChangeColumnsProto toChangeColumnsProto(ChangeColumns cols) {
+ ChangeColumnsProto.Builder b =
+ ChangeColumnsProto.newBuilder()
+ .setChangeKey(cols.changeKey().get())
+ .setCreatedOn(cols.createdOn().getTime())
+ .setLastUpdatedOn(cols.lastUpdatedOn().getTime())
+ .setOwner(cols.owner().get())
+ .setBranch(cols.branch());
+ if (cols.currentPatchSetId() != null) {
+ b.setCurrentPatchSetId(cols.currentPatchSetId().get()).setHasCurrentPatchSetId(true);
+ }
+ b.setSubject(cols.subject());
+ if (cols.topic() != null) {
+ b.setTopic(cols.topic()).setHasTopic(true);
+ }
+ if (cols.originalSubject() != null) {
+ b.setOriginalSubject(cols.originalSubject()).setHasOriginalSubject(true);
+ }
+ if (cols.submissionId() != null) {
+ b.setSubmissionId(cols.submissionId()).setHasSubmissionId(true);
+ }
+ if (cols.assignee() != null) {
+ b.setAssignee(cols.assignee().get()).setHasAssignee(true);
+ }
+ if (cols.status() != null) {
+ b.setStatus(STATUS_CONVERTER.reverse().convert(cols.status())).setHasStatus(true);
+ }
+ b.setIsPrivate(cols.isPrivate())
+ .setWorkInProgress(cols.workInProgress())
+ .setReviewStarted(cols.reviewStarted());
+ if (cols.revertOf() != null) {
+ b.setRevertOf(cols.revertOf().get()).setHasRevertOf(true);
+ }
+ return b.build();
+ }
+
+ private static ReviewerSetEntryProto toReviewerSetEntry(
+ Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> c) {
+ return ReviewerSetEntryProto.newBuilder()
+ .setState(REVIEWER_STATE_CONVERTER.reverse().convert(c.getRowKey()))
+ .setAccountId(c.getColumnKey().get())
+ .setTimestamp(c.getValue().getTime())
+ .build();
+ }
+
+ private static ReviewerByEmailSetEntryProto toReviewerByEmailSetEntry(
+ Table.Cell<ReviewerStateInternal, Address, Timestamp> c) {
+ return ReviewerByEmailSetEntryProto.newBuilder()
+ .setState(REVIEWER_STATE_CONVERTER.reverse().convert(c.getRowKey()))
+ .setAddress(c.getColumnKey().toHeaderString())
+ .setTimestamp(c.getValue().getTime())
+ .build();
+ }
+
+ private static ReviewerStatusUpdateProto toReviewerStatusUpdateProto(ReviewerStatusUpdate u) {
+ return ReviewerStatusUpdateProto.newBuilder()
+ .setDate(u.date().getTime())
+ .setUpdatedBy(u.updatedBy().get())
+ .setReviewer(u.reviewer().get())
+ .setState(REVIEWER_STATE_CONVERTER.reverse().convert(u.state()))
+ .build();
+ }
+
+ @Override
+ public ChangeNotesState deserialize(byte[] in) {
+ ChangeNotesStateProto proto =
+ ProtoCacheSerializers.parseUnchecked(ChangeNotesStateProto.parser(), in);
+ Change.Id changeId = new Change.Id(proto.getChangeId());
+
+ ChangeNotesState.Builder b =
+ builder()
+ .metaId(ObjectIdConverter.create().fromByteString(proto.getMetaId()))
+ .changeId(changeId)
+ .columns(toChangeColumns(changeId, proto.getColumns()))
+ .pastAssignees(
+ proto.getPastAssigneeList().stream()
+ .map(Account.Id::new)
+ .collect(toImmutableSet()))
+ .hashtags(proto.getHashtagList())
+ .patchSets(
+ proto.getPatchSetList().stream()
+ .map(PATCH_SET_CODEC::decode)
+ .map(ps -> Maps.immutableEntry(ps.getId(), ps))
+ .collect(toImmutableList()))
+ .approvals(
+ proto.getApprovalList().stream()
+ .map(APPROVAL_CODEC::decode)
+ .map(a -> Maps.immutableEntry(a.getPatchSetId(), a))
+ .collect(toImmutableList()))
+ .reviewers(toReviewerSet(proto.getReviewerList()))
+ .reviewersByEmail(toReviewerByEmailSet(proto.getReviewerByEmailList()))
+ .pendingReviewers(toReviewerSet(proto.getPendingReviewerList()))
+ .pendingReviewersByEmail(toReviewerByEmailSet(proto.getPendingReviewerByEmailList()))
+ .allPastReviewers(
+ proto.getPastReviewerList().stream()
+ .map(Account.Id::new)
+ .collect(toImmutableList()))
+ .reviewerUpdates(toReviewerStatusUpdateList(proto.getReviewerUpdateList()))
+ .submitRecords(
+ proto.getSubmitRecordList().stream()
+ .map(r -> GSON.fromJson(r, StoredSubmitRecord.class).toSubmitRecord())
+ .collect(toImmutableList()))
+ .changeMessages(
+ proto.getChangeMessageList().stream()
+ .map(MESSAGE_CODEC::decode)
+ .collect(toImmutableList()))
+ .publishedComments(
+ proto.getPublishedCommentList().stream()
+ .map(r -> GSON.fromJson(r, Comment.class))
+ .collect(toImmutableListMultimap(c -> new RevId(c.revId), c -> c)));
+ if (proto.getHasReadOnlyUntil()) {
+ b.readOnlyUntil(new Timestamp(proto.getReadOnlyUntil()));
+ }
+ return b.build();
+ }
+
+ private static ChangeColumns toChangeColumns(Change.Id changeId, ChangeColumnsProto proto) {
+ ChangeColumns.Builder b =
+ ChangeColumns.builder()
+ .changeKey(new Change.Key(proto.getChangeKey()))
+ .createdOn(new Timestamp(proto.getCreatedOn()))
+ .lastUpdatedOn(new Timestamp(proto.getLastUpdatedOn()))
+ .owner(new Account.Id(proto.getOwner()))
+ .branch(proto.getBranch());
+ if (proto.getHasCurrentPatchSetId()) {
+ b.currentPatchSetId(new PatchSet.Id(changeId, proto.getCurrentPatchSetId()));
+ }
+ b.subject(proto.getSubject());
+ if (proto.getHasTopic()) {
+ b.topic(proto.getTopic());
+ }
+ if (proto.getHasOriginalSubject()) {
+ b.originalSubject(proto.getOriginalSubject());
+ }
+ if (proto.getHasSubmissionId()) {
+ b.submissionId(proto.getSubmissionId());
+ }
+ if (proto.getHasAssignee()) {
+ b.assignee(new Account.Id(proto.getAssignee()));
+ }
+ if (proto.getHasStatus()) {
+ b.status(STATUS_CONVERTER.convert(proto.getStatus()));
+ }
+ b.isPrivate(proto.getIsPrivate())
+ .workInProgress(proto.getWorkInProgress())
+ .reviewStarted(proto.getReviewStarted());
+ if (proto.getHasRevertOf()) {
+ b.revertOf(new Change.Id(proto.getRevertOf()));
+ }
+ return b.build();
+ }
+
+ private static ReviewerSet toReviewerSet(List<ReviewerSetEntryProto> protos) {
+ ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Timestamp> b =
+ ImmutableTable.builder();
+ for (ReviewerSetEntryProto e : protos) {
+ b.put(
+ REVIEWER_STATE_CONVERTER.convert(e.getState()),
+ new Account.Id(e.getAccountId()),
+ new Timestamp(e.getTimestamp()));
+ }
+ return ReviewerSet.fromTable(b.build());
+ }
+
+ private static ReviewerByEmailSet toReviewerByEmailSet(
+ List<ReviewerByEmailSetEntryProto> protos) {
+ ImmutableTable.Builder<ReviewerStateInternal, Address, Timestamp> b =
+ ImmutableTable.builder();
+ for (ReviewerByEmailSetEntryProto e : protos) {
+ b.put(
+ REVIEWER_STATE_CONVERTER.convert(e.getState()),
+ Address.parse(e.getAddress()),
+ new Timestamp(e.getTimestamp()));
+ }
+ return ReviewerByEmailSet.fromTable(b.build());
+ }
+
+ private static ImmutableList<ReviewerStatusUpdate> toReviewerStatusUpdateList(
+ List<ReviewerStatusUpdateProto> protos) {
+ ImmutableList.Builder<ReviewerStatusUpdate> b = ImmutableList.builder();
+ for (ReviewerStatusUpdateProto proto : protos) {
+ b.add(
+ ReviewerStatusUpdate.create(
+ new Timestamp(proto.getDate()),
+ new Account.Id(proto.getUpdatedBy()),
+ new Account.Id(proto.getReviewer()),
+ REVIEWER_STATE_CONVERTER.convert(proto.getState())));
+ }
+ return b.build();
+ }
+ }
+}