Files
gerrit/java/com/google/gerrit/server/index/change/ChangeField.java
Alice Kober-Sotzek 921d9d67f9 Add converter for Change protobuf messages
The use of ProtobufCodec requires that value classes need to be
annotated with @Column, which won't be possible as soon as we have
removed gwtorm. Hence, provide a hand-written converter for
Change protobuf messages.

As protobuf Changes are currently used in indices
and we don't want to invalidate those, we have to ensure binary
compatibility. Prove that the new converter generates binary compatible
results via dedicated tests. Those tests will be removed after this
change.

Change-Id: Ie1dcd3dd28c628cb8c20d0f95147417e8b2fd260
2018-12-18 11:56:56 -08:00

873 lines
36 KiB
Java

// Copyright (C) 2013 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.index.change;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.index.FieldDef.exact;
import static com.google.gerrit.index.FieldDef.fullText;
import static com.google.gerrit.index.FieldDef.intRange;
import static com.google.gerrit.index.FieldDef.integer;
import static com.google.gerrit.index.FieldDef.prefix;
import static com.google.gerrit.index.FieldDef.storedOnly;
import static com.google.gerrit.index.FieldDef.timestamp;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Enums;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Table;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Longs;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.RefState;
import com.google.gerrit.index.SchemaUtil;
import com.google.gerrit.mail.Address;
import com.google.gerrit.proto.Protos;
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.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.converter.ChangeProtoConverter;
import com.google.gerrit.reviewdb.converter.PatchSetApprovalProtoConverter;
import com.google.gerrit.reviewdb.converter.PatchSetProtoConverter;
import com.google.gerrit.reviewdb.converter.ProtoConverter;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.notedb.RobotCommentNotes;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gson.Gson;
import com.google.gwtorm.server.OrmException;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import org.eclipse.jgit.lib.PersonIdent;
/**
* Fields indexed on change documents.
*
* <p>Each field corresponds to both a field name supported by {@link ChangeQueryBuilder} for
* querying that field, and a method on {@link ChangeData} used for populating the corresponding
* document fields in the secondary index.
*
* <p>Field names are all lowercase alphanumeric plus underscore; index implementations may create
* unambiguous derived field names containing other characters.
*/
public class ChangeField {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static final int NO_ASSIGNEE = -1;
private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
/** Legacy change ID. */
public static final FieldDef<ChangeData, Integer> LEGACY_ID =
integer("legacy_id").stored().build(cd -> cd.getId().get());
/** Newer style Change-Id key. */
public static final FieldDef<ChangeData, String> ID =
prefix(ChangeQueryBuilder.FIELD_CHANGE_ID).build(changeGetter(c -> c.getKey().get()));
/** Change status string, in the same format as {@code status:}. */
public static final FieldDef<ChangeData, String> STATUS =
exact(ChangeQueryBuilder.FIELD_STATUS)
.build(changeGetter(c -> ChangeStatusPredicate.canonicalize(c.getStatus())));
/** Project containing the change. */
public static final FieldDef<ChangeData, String> PROJECT =
exact(ChangeQueryBuilder.FIELD_PROJECT)
.stored()
.build(changeGetter(c -> c.getProject().get()));
/** Project containing the change, as a prefix field. */
public static final FieldDef<ChangeData, String> PROJECTS =
prefix(ChangeQueryBuilder.FIELD_PROJECTS).build(changeGetter(c -> c.getProject().get()));
/** Reference (aka branch) the change will submit onto. */
public static final FieldDef<ChangeData, String> REF =
exact(ChangeQueryBuilder.FIELD_REF).build(changeGetter(c -> c.getDest().get()));
/** Topic, a short annotation on the branch. */
public static final FieldDef<ChangeData, String> EXACT_TOPIC =
exact("topic4").build(ChangeField::getTopic);
/** Topic, a short annotation on the branch. */
public static final FieldDef<ChangeData, String> FUZZY_TOPIC =
fullText("topic5").build(ChangeField::getTopic);
/** Submission id assigned by MergeOp. */
public static final FieldDef<ChangeData, String> SUBMISSIONID =
exact(ChangeQueryBuilder.FIELD_SUBMISSIONID).build(changeGetter(Change::getSubmissionId));
/** Last update time since January 1, 1970. */
public static final FieldDef<ChangeData, Timestamp> UPDATED =
timestamp("updated2").stored().build(changeGetter(Change::getLastUpdatedOn));
/** List of full file paths modified in the current patch set. */
public static final FieldDef<ChangeData, Iterable<String>> PATH =
// Named for backwards compatibility.
exact(ChangeQueryBuilder.FIELD_FILE)
.buildRepeatable(cd -> firstNonNull(cd.currentFilePaths(), ImmutableList.of()));
public static Set<String> getFileParts(ChangeData cd) throws OrmException {
List<String> paths;
try {
paths = cd.currentFilePaths();
} catch (IOException e) {
throw new OrmException(e);
}
Splitter s = Splitter.on('/').omitEmptyStrings();
Set<String> r = new HashSet<>();
for (String path : paths) {
for (String part : s.split(path)) {
r.add(part);
}
}
return r;
}
/** Hashtags tied to a change */
public static final FieldDef<ChangeData, Iterable<String>> HASHTAG =
exact(ChangeQueryBuilder.FIELD_HASHTAG)
.buildRepeatable(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
/** Hashtags with original case. */
public static final FieldDef<ChangeData, Iterable<byte[]>> HASHTAG_CASE_AWARE =
storedOnly("_hashtag")
.buildRepeatable(
cd -> cd.hashtags().stream().map(t -> t.getBytes(UTF_8)).collect(toSet()));
/** Components of each file path modified in the current patch set. */
public static final FieldDef<ChangeData, Iterable<String>> FILE_PART =
exact(ChangeQueryBuilder.FIELD_FILEPART).buildRepeatable(ChangeField::getFileParts);
/** Owner/creator of the change. */
public static final FieldDef<ChangeData, Integer> OWNER =
integer(ChangeQueryBuilder.FIELD_OWNER).build(changeGetter(c -> c.getOwner().get()));
/** The user assigned to the change. */
public static final FieldDef<ChangeData, Integer> ASSIGNEE =
integer(ChangeQueryBuilder.FIELD_ASSIGNEE)
.build(changeGetter(c -> c.getAssignee() != null ? c.getAssignee().get() : NO_ASSIGNEE));
/** Reviewer(s) associated with the change. */
public static final FieldDef<ChangeData, Iterable<String>> REVIEWER =
exact("reviewer2").stored().buildRepeatable(cd -> getReviewerFieldValues(cd.reviewers()));
/** Reviewer(s) associated with the change that do not have a gerrit account. */
public static final FieldDef<ChangeData, Iterable<String>> REVIEWER_BY_EMAIL =
exact("reviewer_by_email")
.stored()
.buildRepeatable(cd -> getReviewerByEmailFieldValues(cd.reviewersByEmail()));
/** Reviewer(s) modified during change's current WIP phase. */
public static final FieldDef<ChangeData, Iterable<String>> PENDING_REVIEWER =
exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER)
.stored()
.buildRepeatable(cd -> getReviewerFieldValues(cd.pendingReviewers()));
/** Reviewer(s) by email modified during change's current WIP phase. */
public static final FieldDef<ChangeData, Iterable<String>> PENDING_REVIEWER_BY_EMAIL =
exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER_BY_EMAIL)
.stored()
.buildRepeatable(cd -> getReviewerByEmailFieldValues(cd.pendingReviewersByEmail()));
/** References a change that this change reverts. */
public static final FieldDef<ChangeData, Integer> REVERT_OF =
integer(ChangeQueryBuilder.FIELD_REVERTOF)
.build(cd -> cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null);
@VisibleForTesting
static List<String> getReviewerFieldValues(ReviewerSet reviewers) {
List<String> r = new ArrayList<>(reviewers.asTable().size() * 2);
for (Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> c :
reviewers.asTable().cellSet()) {
String v = getReviewerFieldValue(c.getRowKey(), c.getColumnKey());
r.add(v);
r.add(v + ',' + c.getValue().getTime());
}
return r;
}
public static String getReviewerFieldValue(ReviewerStateInternal state, Account.Id id) {
return state.toString() + ',' + id;
}
@VisibleForTesting
static List<String> getReviewerByEmailFieldValues(ReviewerByEmailSet reviewersByEmail) {
List<String> r = new ArrayList<>(reviewersByEmail.asTable().size() * 2);
for (Table.Cell<ReviewerStateInternal, Address, Timestamp> c :
reviewersByEmail.asTable().cellSet()) {
String v = getReviewerByEmailFieldValue(c.getRowKey(), c.getColumnKey());
r.add(v);
if (c.getColumnKey().getName() != null) {
// Add another entry without the name to provide search functionality on the email
Address emailOnly = new Address(c.getColumnKey().getEmail());
r.add(getReviewerByEmailFieldValue(c.getRowKey(), emailOnly));
}
r.add(v + ',' + c.getValue().getTime());
}
return r;
}
public static String getReviewerByEmailFieldValue(ReviewerStateInternal state, Address adr) {
return state.toString() + ',' + adr;
}
public static ReviewerSet parseReviewerFieldValues(Change.Id changeId, Iterable<String> values) {
ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Timestamp> b =
ImmutableTable.builder();
for (String v : values) {
int i = v.indexOf(',');
if (i < 0) {
logger.atWarning().log(
"Invalid value for reviewer field from change %s: %s", changeId.get(), v);
continue;
}
int i2 = v.lastIndexOf(',');
if (i2 == i) {
// Don't log a warning here.
// For each reviewer we store 2 values in the reviewer field, one value with the format
// "<reviewer-type>,<account-id>" and one value with the format
// "<reviewer-type>,<account-id>,<timestamp>" (see #getReviewerFieldValues(ReviewerSet)).
// For parsing we are only interested in the "<reviewer-type>,<account-id>,<timestamp>"
// value and the "<reviewer-type>,<account-id>" value is ignored here.
continue;
}
com.google.common.base.Optional<ReviewerStateInternal> reviewerState =
Enums.getIfPresent(ReviewerStateInternal.class, v.substring(0, i));
if (!reviewerState.isPresent()) {
logger.atWarning().log(
"Failed to parse reviewer state of reviewer field from change %s: %s",
changeId.get(), v);
continue;
}
Optional<Account.Id> accountId = Account.Id.tryParse(v.substring(i + 1, i2));
if (!accountId.isPresent()) {
logger.atWarning().log(
"Failed to parse account ID of reviewer field from change %s: %s", changeId.get(), v);
continue;
}
Long l = Longs.tryParse(v.substring(i2 + 1, v.length()));
if (l == null) {
logger.atWarning().log(
"Failed to parse timestamp of reviewer field from change %s: %s", changeId.get(), v);
continue;
}
Timestamp timestamp = new Timestamp(l);
b.put(reviewerState.get(), accountId.get(), timestamp);
}
return ReviewerSet.fromTable(b.build());
}
public static ReviewerByEmailSet parseReviewerByEmailFieldValues(
Change.Id changeId, Iterable<String> values) {
ImmutableTable.Builder<ReviewerStateInternal, Address, Timestamp> b = ImmutableTable.builder();
for (String v : values) {
int i = v.indexOf(',');
if (i < 0) {
logger.atWarning().log(
"Invalid value for reviewer by email field from change %s: %s", changeId.get(), v);
continue;
}
int i2 = v.lastIndexOf(',');
if (i2 == i) {
// Don't log a warning here.
// For each reviewer we store 2 values in the reviewer field, one value with the format
// "<reviewer-type>,<email>" and one value with the format
// "<reviewer-type>,<email>,<timestamp>" (see
// #getReviewerByEmailFieldValues(ReviewerByEmailSet)).
// For parsing we are only interested in the "<reviewer-type>,<email>,<timestamp>" value
// and the "<reviewer-type>,<email>" value is ignored here.
continue;
}
com.google.common.base.Optional<ReviewerStateInternal> reviewerState =
Enums.getIfPresent(ReviewerStateInternal.class, v.substring(0, i));
if (!reviewerState.isPresent()) {
logger.atWarning().log(
"Failed to parse reviewer state of reviewer by email field from change %s: %s",
changeId.get(), v);
continue;
}
Address address = Address.tryParse(v.substring(i + 1, i2));
if (address == null) {
logger.atWarning().log(
"Failed to parse address of reviewer by email field from change %s: %s",
changeId.get(), v);
continue;
}
Long l = Longs.tryParse(v.substring(i2 + 1, v.length()));
if (l == null) {
logger.atWarning().log(
"Failed to parse timestamp of reviewer by email field from change %s: %s",
changeId.get(), v);
continue;
}
Timestamp timestamp = new Timestamp(l);
b.put(reviewerState.get(), address, timestamp);
}
return ReviewerByEmailSet.fromTable(b.build());
}
/** Commit ID of any patch set on the change, using prefix match. */
public static final FieldDef<ChangeData, Iterable<String>> COMMIT =
prefix(ChangeQueryBuilder.FIELD_COMMIT).buildRepeatable(ChangeField::getRevisions);
/** Commit ID of any patch set on the change, using exact match. */
public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMIT =
exact(ChangeQueryBuilder.FIELD_EXACTCOMMIT).buildRepeatable(ChangeField::getRevisions);
private static Set<String> getRevisions(ChangeData cd) throws OrmException {
Set<String> revisions = new HashSet<>();
for (PatchSet ps : cd.patchSets()) {
if (ps.getRevision() != null) {
revisions.add(ps.getRevision().get());
}
}
return revisions;
}
/** Tracking id extracted from a footer. */
public static final FieldDef<ChangeData, Iterable<String>> TR =
exact(ChangeQueryBuilder.FIELD_TR)
.buildRepeatable(cd -> ImmutableSet.copyOf(cd.trackingFooters().values()));
/** List of labels on the current patch set including change owner votes. */
public static final FieldDef<ChangeData, Iterable<String>> LABEL =
exact("label2").buildRepeatable(cd -> getLabels(cd, true));
private static Iterable<String> getLabels(ChangeData cd, boolean owners) throws OrmException {
Set<String> allApprovals = new HashSet<>();
Set<String> distinctApprovals = new HashSet<>();
for (PatchSetApproval a : cd.currentApprovals()) {
if (a.getValue() != 0 && !a.isLegacySubmit()) {
allApprovals.add(formatLabel(a.getLabel(), a.getValue(), a.getAccountId()));
if (owners && cd.change().getOwner().equals(a.getAccountId())) {
allApprovals.add(
formatLabel(a.getLabel(), a.getValue(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
}
distinctApprovals.add(formatLabel(a.getLabel(), a.getValue()));
}
}
allApprovals.addAll(distinctApprovals);
return allApprovals;
}
public static Set<String> getAuthorParts(ChangeData cd) throws OrmException, IOException {
return SchemaUtil.getPersonParts(cd.getAuthor());
}
public static Set<String> getAuthorNameAndEmail(ChangeData cd) throws OrmException, IOException {
return getNameAndEmail(cd.getAuthor());
}
public static Set<String> getCommitterParts(ChangeData cd) throws OrmException, IOException {
return SchemaUtil.getPersonParts(cd.getCommitter());
}
public static Set<String> getCommitterNameAndEmail(ChangeData cd)
throws OrmException, IOException {
return getNameAndEmail(cd.getCommitter());
}
private static Set<String> getNameAndEmail(PersonIdent person) {
if (person == null) {
return ImmutableSet.of();
}
String name = person.getName().toLowerCase(Locale.US);
String email = person.getEmailAddress().toLowerCase(Locale.US);
StringBuilder nameEmailBuilder = new StringBuilder();
PersonIdent.appendSanitized(nameEmailBuilder, name);
nameEmailBuilder.append(" <");
PersonIdent.appendSanitized(nameEmailBuilder, email);
nameEmailBuilder.append('>');
return ImmutableSet.of(name, email, nameEmailBuilder.toString());
}
/**
* The exact email address, or any part of the author name or email address, in the current patch
* set.
*/
public static final FieldDef<ChangeData, Iterable<String>> AUTHOR =
fullText(ChangeQueryBuilder.FIELD_AUTHOR).buildRepeatable(ChangeField::getAuthorParts);
/** The exact name, email address and NameEmail of the author. */
public static final FieldDef<ChangeData, Iterable<String>> EXACT_AUTHOR =
exact(ChangeQueryBuilder.FIELD_EXACTAUTHOR)
.buildRepeatable(ChangeField::getAuthorNameAndEmail);
/**
* The exact email address, or any part of the committer name or email address, in the current
* patch set.
*/
public static final FieldDef<ChangeData, Iterable<String>> COMMITTER =
fullText(ChangeQueryBuilder.FIELD_COMMITTER).buildRepeatable(ChangeField::getCommitterParts);
/** The exact name, email address, and NameEmail of the committer. */
public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMITTER =
exact(ChangeQueryBuilder.FIELD_EXACTCOMMITTER)
.buildRepeatable(ChangeField::getCommitterNameAndEmail);
/** Serialized change object, used for pre-populating results. */
public static final FieldDef<ChangeData, byte[]> CHANGE =
storedOnly("_change")
.build(changeGetter(change -> toProto(ChangeProtoConverter.INSTANCE, change)));
/** Serialized approvals for the current patch set, used for pre-populating results. */
public static final FieldDef<ChangeData, Iterable<byte[]>> APPROVAL =
storedOnly("_approval")
.buildRepeatable(
cd -> toProtos(PatchSetApprovalProtoConverter.INSTANCE, cd.currentApprovals()));
public static String formatLabel(String label, int value) {
return formatLabel(label, value, null);
}
public static String formatLabel(String label, int value, Account.Id accountId) {
return label.toLowerCase()
+ (value >= 0 ? "+" : "")
+ value
+ (accountId != null ? "," + formatAccount(accountId) : "");
}
private static String formatAccount(Account.Id accountId) {
if (ChangeQueryBuilder.OWNER_ACCOUNT_ID.equals(accountId)) {
return ChangeQueryBuilder.ARG_ID_OWNER;
}
return Integer.toString(accountId.get());
}
/** Commit message of the current patch set. */
public static final FieldDef<ChangeData, String> COMMIT_MESSAGE =
fullText(ChangeQueryBuilder.FIELD_MESSAGE).build(ChangeData::commitMessage);
/** Summary or inline comment. */
public static final FieldDef<ChangeData, Iterable<String>> COMMENT =
fullText(ChangeQueryBuilder.FIELD_COMMENT)
.buildRepeatable(
cd ->
Stream.concat(
cd.publishedComments().stream().map(c -> c.message),
cd.messages().stream().map(ChangeMessage::getMessage))
.collect(toSet()));
/** Number of unresolved comment threads of the change, including robot comments. */
public static final FieldDef<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT =
intRange(ChangeQueryBuilder.FIELD_UNRESOLVED_COMMENT_COUNT)
.build(ChangeData::unresolvedCommentCount);
/** Total number of published inline comments of the change, including robot comments. */
public static final FieldDef<ChangeData, Integer> TOTAL_COMMENT_COUNT =
intRange("total_comments").build(ChangeData::totalCommentCount);
/** Whether the change is mergeable. */
public static final FieldDef<ChangeData, String> MERGEABLE =
exact(ChangeQueryBuilder.FIELD_MERGEABLE)
.stored()
.build(
cd -> {
Boolean m = cd.isMergeable();
if (m == null) {
return null;
}
return m ? "1" : "0";
});
/** The number of inserted lines in this change. */
public static final FieldDef<ChangeData, Integer> ADDED =
intRange(ChangeQueryBuilder.FIELD_ADDED)
.build(cd -> cd.changedLines().isPresent() ? cd.changedLines().get().insertions : null);
/** The number of deleted lines in this change. */
public static final FieldDef<ChangeData, Integer> DELETED =
intRange(ChangeQueryBuilder.FIELD_DELETED)
.build(cd -> cd.changedLines().isPresent() ? cd.changedLines().get().deletions : null);
/** The total number of modified lines in this change. */
public static final FieldDef<ChangeData, Integer> DELTA =
intRange(ChangeQueryBuilder.FIELD_DELTA)
.build(cd -> cd.changedLines().map(c -> c.insertions + c.deletions).orElse(null));
/** Determines if this change is private. */
public static final FieldDef<ChangeData, String> PRIVATE =
exact(ChangeQueryBuilder.FIELD_PRIVATE).build(cd -> cd.change().isPrivate() ? "1" : "0");
/** Determines if this change is work in progress. */
public static final FieldDef<ChangeData, String> WIP =
exact(ChangeQueryBuilder.FIELD_WIP).build(cd -> cd.change().isWorkInProgress() ? "1" : "0");
/** Determines if this change has started review. */
public static final FieldDef<ChangeData, String> STARTED =
exact(ChangeQueryBuilder.FIELD_STARTED)
.build(cd -> cd.change().hasReviewStarted() ? "1" : "0");
/** Users who have commented on this change. */
public static final FieldDef<ChangeData, Iterable<Integer>> COMMENTBY =
integer(ChangeQueryBuilder.FIELD_COMMENTBY)
.buildRepeatable(
cd ->
Stream.concat(
cd.messages().stream().map(ChangeMessage::getAuthor),
cd.publishedComments().stream().map(c -> c.author.getId()))
.filter(Objects::nonNull)
.map(Account.Id::get)
.collect(toSet()));
/** Star labels on this change in the format: &lt;account-id&gt;:&lt;label&gt; */
public static final FieldDef<ChangeData, Iterable<String>> STAR =
exact(ChangeQueryBuilder.FIELD_STAR)
.stored()
.buildRepeatable(
cd ->
Iterables.transform(
cd.stars().entries(),
e ->
StarredChangesUtil.StarField.create(e.getKey(), e.getValue())
.toString()));
/** Users that have starred the change with any label. */
public static final FieldDef<ChangeData, Iterable<Integer>> STARBY =
integer(ChangeQueryBuilder.FIELD_STARBY)
.buildRepeatable(cd -> Iterables.transform(cd.stars().keySet(), Account.Id::get));
/** Opaque group identifiers for this change's patch sets. */
public static final FieldDef<ChangeData, Iterable<String>> GROUP =
exact(ChangeQueryBuilder.FIELD_GROUP)
.buildRepeatable(
cd ->
cd.patchSets().stream().flatMap(ps -> ps.getGroups().stream()).collect(toSet()));
/** Serialized patch set object, used for pre-populating results. */
public static final FieldDef<ChangeData, Iterable<byte[]>> PATCH_SET =
storedOnly("_patch_set")
.buildRepeatable(cd -> toProtos(PatchSetProtoConverter.INSTANCE, cd.patchSets()));
/** Users who have edits on this change. */
public static final FieldDef<ChangeData, Iterable<Integer>> EDITBY =
integer(ChangeQueryBuilder.FIELD_EDITBY)
.buildRepeatable(cd -> cd.editsByUser().stream().map(Account.Id::get).collect(toSet()));
/** Users who have draft comments on this change. */
public static final FieldDef<ChangeData, Iterable<Integer>> DRAFTBY =
integer(ChangeQueryBuilder.FIELD_DRAFTBY)
.buildRepeatable(cd -> cd.draftsByUser().stream().map(Account.Id::get).collect(toSet()));
public static final Integer NOT_REVIEWED = -1;
/**
* Users the change was reviewed by since the last author update.
*
* <p>A change is considered reviewed by a user if the latest update by that user is newer than
* the latest update by the change author. Both top-level change messages and new patch sets are
* considered to be updates.
*
* <p>If the latest update is by the change owner, then the special value {@link #NOT_REVIEWED} is
* emitted.
*/
public static final FieldDef<ChangeData, Iterable<Integer>> REVIEWEDBY =
integer(ChangeQueryBuilder.FIELD_REVIEWEDBY)
.stored()
.buildRepeatable(
cd -> {
Set<Account.Id> reviewedBy = cd.reviewedBy();
if (reviewedBy.isEmpty()) {
return ImmutableSet.of(NOT_REVIEWED);
}
return reviewedBy.stream().map(Account.Id::get).collect(toList());
});
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
SubmitRuleOptions.builder().allowClosed(true).build();
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
SubmitRuleOptions.builder().build();
/**
* JSON type for storing SubmitRecords.
*
* <p>Stored fields need to use a stable format over a long period; this type insulates the index
* from implementation changes in SubmitRecord itself.
*/
public static class StoredSubmitRecord {
static class StoredLabel {
String label;
SubmitRecord.Label.Status status;
Integer appliedBy;
}
static class StoredRequirement {
String fallbackText;
String type;
Map<String, String> data;
}
SubmitRecord.Status status;
List<StoredLabel> labels;
List<StoredRequirement> requirements;
String errorMessage;
public StoredSubmitRecord(SubmitRecord rec) {
this.status = rec.status;
this.errorMessage = rec.errorMessage;
if (rec.labels != null) {
this.labels = new ArrayList<>(rec.labels.size());
for (SubmitRecord.Label label : rec.labels) {
StoredLabel sl = new StoredLabel();
sl.label = label.label;
sl.status = label.status;
sl.appliedBy = label.appliedBy != null ? label.appliedBy.get() : null;
this.labels.add(sl);
}
}
if (rec.requirements != null) {
this.requirements = new ArrayList<>(rec.requirements.size());
for (SubmitRequirement requirement : rec.requirements) {
StoredRequirement sr = new StoredRequirement();
sr.type = requirement.type();
sr.fallbackText = requirement.fallbackText();
sr.data = requirement.data();
this.requirements.add(sr);
}
}
}
public SubmitRecord toSubmitRecord() {
SubmitRecord rec = new SubmitRecord();
rec.status = status;
rec.errorMessage = errorMessage;
if (labels != null) {
rec.labels = new ArrayList<>(labels.size());
for (StoredLabel label : labels) {
SubmitRecord.Label srl = new SubmitRecord.Label();
srl.label = label.label;
srl.status = label.status;
srl.appliedBy = label.appliedBy != null ? new Account.Id(label.appliedBy) : null;
rec.labels.add(srl);
}
}
if (requirements != null) {
rec.requirements = new ArrayList<>(requirements.size());
for (StoredRequirement req : requirements) {
SubmitRequirement sr =
SubmitRequirement.builder()
.setType(req.type)
.setFallbackText(req.fallbackText)
.setData(req.data)
.build();
rec.requirements.add(sr);
}
}
return rec;
}
}
public static final FieldDef<ChangeData, Iterable<String>> SUBMIT_RECORD =
exact("submit_record").buildRepeatable(ChangeField::formatSubmitRecordValues);
public static final FieldDef<ChangeData, Iterable<byte[]>> STORED_SUBMIT_RECORD_STRICT =
storedOnly("full_submit_record_strict")
.buildRepeatable(cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_STRICT));
public static final FieldDef<ChangeData, Iterable<byte[]>> STORED_SUBMIT_RECORD_LENIENT =
storedOnly("full_submit_record_lenient")
.buildRepeatable(cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_LENIENT));
public static void parseSubmitRecords(
Collection<String> values, SubmitRuleOptions opts, ChangeData out) {
List<SubmitRecord> records = parseSubmitRecords(values);
if (records.isEmpty()) {
// Assume no values means the field is not in the index;
// SubmitRuleEvaluator ensures the list is non-empty.
return;
}
out.setSubmitRecords(opts, records);
}
@VisibleForTesting
static List<SubmitRecord> parseSubmitRecords(Collection<String> values) {
return values
.stream()
.map(v -> GSON.fromJson(v, StoredSubmitRecord.class).toSubmitRecord())
.collect(toList());
}
@VisibleForTesting
static List<byte[]> storedSubmitRecords(List<SubmitRecord> records) {
return Lists.transform(records, r -> GSON.toJson(new StoredSubmitRecord(r)).getBytes(UTF_8));
}
private static Iterable<byte[]> storedSubmitRecords(ChangeData cd, SubmitRuleOptions opts) {
return storedSubmitRecords(cd.submitRecords(opts));
}
public static List<String> formatSubmitRecordValues(ChangeData cd) throws OrmException {
return formatSubmitRecordValues(
cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT), cd.change().getOwner());
}
@VisibleForTesting
static List<String> formatSubmitRecordValues(List<SubmitRecord> records, Account.Id changeOwner) {
List<String> result = new ArrayList<>();
for (SubmitRecord rec : records) {
result.add(rec.status.name());
if (rec.labels == null) {
continue;
}
for (SubmitRecord.Label label : rec.labels) {
String sl = label.status.toString() + ',' + label.label.toLowerCase();
result.add(sl);
String slc = sl + ',';
if (label.appliedBy != null) {
result.add(slc + label.appliedBy.get());
if (label.appliedBy.equals(changeOwner)) {
result.add(slc + ChangeQueryBuilder.OWNER_ACCOUNT_ID.get());
}
}
}
}
return result;
}
/**
* All values of all refs that were used in the course of indexing this document.
*
* <p>Emitted as UTF-8 encoded strings of the form {@code project:ref/name:[hex sha]}.
*/
public static final FieldDef<ChangeData, Iterable<byte[]>> REF_STATE =
storedOnly("ref_state")
.buildRepeatable(
cd -> {
List<byte[]> result = new ArrayList<>();
Project.NameKey project = cd.change().getProject();
cd.editRefs()
.values()
.forEach(r -> result.add(RefState.of(r).toByteArray(project)));
cd.starRefs()
.values()
.forEach(r -> result.add(RefState.of(r.ref()).toByteArray(allUsers(cd))));
ChangeNotes notes = cd.notes();
result.add(
RefState.create(notes.getRefName(), notes.getMetaId()).toByteArray(project));
notes.getRobotComments(); // Force loading robot comments.
RobotCommentNotes robotNotes = notes.getRobotCommentNotes();
result.add(
RefState.create(robotNotes.getRefName(), robotNotes.getMetaId())
.toByteArray(project));
cd.draftRefs()
.values()
.forEach(r -> result.add(RefState.of(r).toByteArray(allUsers(cd))));
return result;
});
/**
* All ref wildcard patterns that were used in the course of indexing this document.
*
* <p>Emitted as UTF-8 encoded strings of the form {@code project:ref/name/*}. See {@link
* RefStatePattern} for the pattern format.
*/
public static final FieldDef<ChangeData, Iterable<byte[]>> REF_STATE_PATTERN =
storedOnly("ref_state_pattern")
.buildRepeatable(
cd -> {
Change.Id id = cd.getId();
Project.NameKey project = cd.change().getProject();
List<byte[]> result = new ArrayList<>(3);
result.add(
RefStatePattern.create(
RefNames.REFS_USERS + "*/" + RefNames.EDIT_PREFIX + id + "/*")
.toByteArray(project));
result.add(
RefStatePattern.create(RefNames.refsStarredChangesPrefix(id) + "*")
.toByteArray(allUsers(cd)));
result.add(
RefStatePattern.create(RefNames.refsDraftCommentsPrefix(id) + "*")
.toByteArray(allUsers(cd)));
return result;
});
private static String getTopic(ChangeData cd) throws OrmException {
Change c = cd.change();
if (c == null) {
return null;
}
return firstNonNull(c.getTopic(), "");
}
private static <T> List<byte[]> toProtos(ProtoConverter<?, T> converter, Collection<T> objects) {
return objects.stream().map(object -> toProto(converter, object)).collect(toImmutableList());
}
private static <T> byte[] toProto(ProtoConverter<?, T> converter, T object) {
return Protos.toByteArray(converter.toProto(object));
}
private static <T> FieldDef.Getter<ChangeData, T> changeGetter(Function<Change, T> func) {
return in -> in.change() != null ? func.apply(in.change()) : null;
}
private static AllUsersName allUsers(ChangeData cd) {
return cd.getAllUsersNameForIndexing();
}
}