| // 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.common.collect.ImmutableSet.toImmutableSet; |
| import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_CHANGE_NUMBER; |
| import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static java.util.stream.Collectors.joining; |
| import static java.util.stream.Collectors.toList; |
| import static java.util.stream.Collectors.toSet; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.HashBasedTable; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| 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.io.Files; |
| import com.google.common.primitives.Ints; |
| import com.google.common.primitives.Longs; |
| import com.google.common.reflect.TypeToken; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.Address; |
| import com.google.gerrit.entities.AttentionSetUpdate; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.ChangeMessage; |
| import com.google.gerrit.entities.LabelType; |
| import com.google.gerrit.entities.LegacySubmitRequirement; |
| import com.google.gerrit.entities.PatchSetApproval; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.entities.SubmitRecord; |
| import com.google.gerrit.entities.SubmitRequirementResult; |
| import com.google.gerrit.entities.converter.ChangeProtoConverter; |
| import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter; |
| import com.google.gerrit.entities.converter.PatchSetProtoConverter; |
| import com.google.gerrit.entities.converter.ProtoConverter; |
| import com.google.gerrit.index.IndexedField; |
| import com.google.gerrit.index.RefState; |
| import com.google.gerrit.index.SchemaFieldDefs; |
| import com.google.gerrit.index.SchemaUtil; |
| import com.google.gerrit.json.OutputFormat; |
| import com.google.gerrit.proto.Entities; |
| import com.google.gerrit.server.ReviewerByEmailSet; |
| import com.google.gerrit.server.ReviewerSet; |
| import com.google.gerrit.server.cache.proto.Cache; |
| import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern; |
| import com.google.gerrit.server.notedb.ReviewerStateInternal; |
| import com.google.gerrit.server.notedb.SubmitRequirementProtoConverter; |
| 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.gerrit.server.query.change.MagicLabelValue; |
| import com.google.gson.Gson; |
| import com.google.protobuf.MessageLite; |
| import java.sql.Timestamp; |
| import java.time.Instant; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashMap; |
| 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.Collectors; |
| import java.util.stream.Stream; |
| import java.util.stream.StreamSupport; |
| 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. |
| * |
| * <p>Note that this class does not override {@link Object#equals(Object)}. It relies on instances |
| * being singletons so that the default (i.e. reference) comparison works. |
| */ |
| 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(); |
| |
| /** |
| * To avoid the non-google dependency on org.apache.lucene.index.IndexWriter.MAX_TERM_LENGTH it is |
| * redefined here. |
| */ |
| public static final int MAX_TERM_LENGTH = (1 << 15) - 2; |
| |
| // TODO: Rename LEGACY_ID to NUMERIC_ID |
| /** Legacy change ID. */ |
| public static final IndexedField<ChangeData, String> NUMERIC_ID_STR_FIELD = |
| IndexedField.<ChangeData>stringBuilder("NumericIdStr") |
| .stored() |
| .required() |
| // The numeric change id is integer in string form |
| .size(10) |
| .build(cd -> String.valueOf(cd.virtualId().get())); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec NUMERIC_ID_STR_SPEC = |
| NUMERIC_ID_STR_FIELD.exact("legacy_id_str"); |
| |
| public static final IndexedField<ChangeData, Integer> CHANGENUM_FIELD = |
| IndexedField.<ChangeData>integerBuilder("ChangeNumber") |
| .stored() |
| .required() |
| .build(cd -> cd.getId().get()); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec CHANGENUM_SPEC = |
| CHANGENUM_FIELD.integer(FIELD_CHANGE_NUMBER); |
| |
| /** Newer style Change-Id key. */ |
| public static final IndexedField<ChangeData, String> CHANGE_ID_FIELD = |
| IndexedField.<ChangeData>stringBuilder("ChangeId") |
| .required() |
| // The new style key is in form Isha1 |
| .size(41) |
| .build(changeGetter(c -> c.getKey().get())); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec CHANGE_ID_SPEC = |
| CHANGE_ID_FIELD.prefix(ChangeQueryBuilder.FIELD_CHANGE_ID); |
| |
| /** Change status string, in the same format as {@code status:}. */ |
| public static final IndexedField<ChangeData, String> STATUS_FIELD = |
| IndexedField.<ChangeData>stringBuilder("Status") |
| .required() |
| .size(20) |
| .build(changeGetter(c -> ChangeStatusPredicate.canonicalize(c.getStatus()))); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec STATUS_SPEC = |
| STATUS_FIELD.exact(ChangeQueryBuilder.FIELD_STATUS); |
| |
| /** Project containing the change. */ |
| public static final IndexedField<ChangeData, String> PROJECT_FIELD = |
| IndexedField.<ChangeData>stringBuilder("Project") |
| .required() |
| .stored() |
| .size(200) |
| .build(changeGetter(c -> c.getProject().get())); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec PROJECT_SPEC = |
| PROJECT_FIELD.exact(ChangeQueryBuilder.FIELD_PROJECT); |
| |
| /** Project containing the change, as a prefix field. */ |
| public static final IndexedField<ChangeData, String>.SearchSpec PROJECTS_SPEC = |
| PROJECT_FIELD.prefix(ChangeQueryBuilder.FIELD_PROJECTS); |
| |
| /** Reference (aka branch) the change will submit onto. */ |
| public static final IndexedField<ChangeData, String> REF_FIELD = |
| IndexedField.<ChangeData>stringBuilder("Ref") |
| .required() |
| .size(300) |
| .build(changeGetter(c -> c.getDest().branch())); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec REF_SPEC = |
| REF_FIELD.exact(ChangeQueryBuilder.FIELD_REF); |
| |
| /** Topic, a short annotation on the branch. */ |
| public static final IndexedField<ChangeData, String> TOPIC_FIELD = |
| IndexedField.<ChangeData>stringBuilder("Topic").size(500).build(ChangeField::getTopic); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec EXACT_TOPIC = |
| TOPIC_FIELD.exact("topic4"); |
| |
| /** Topic, a short annotation on the branch. */ |
| public static final IndexedField<ChangeData, String>.SearchSpec FUZZY_TOPIC = |
| TOPIC_FIELD.fullText("topic5"); |
| |
| /** Topic, a short annotation on the branch. */ |
| public static final IndexedField<ChangeData, String>.SearchSpec PREFIX_TOPIC = |
| TOPIC_FIELD.prefix("topic6"); |
| |
| /** {@link com.google.gerrit.entities.SubmissionId} assigned by MergeOp. */ |
| public static final IndexedField<ChangeData, String> SUBMISSIONID_FIELD = |
| IndexedField.<ChangeData>stringBuilder("SubmissionId") |
| .size(500) |
| .build(changeGetter(Change::getSubmissionId)); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec SUBMISSIONID_SPEC = |
| SUBMISSIONID_FIELD.exact(ChangeQueryBuilder.FIELD_SUBMISSIONID); |
| |
| /** Last update time since January 1, 1970. */ |
| // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant |
| public static final IndexedField<ChangeData, Timestamp> UPDATED_FIELD = |
| IndexedField.<ChangeData>timestampBuilder("LastUpdated") |
| .stored() |
| .build(changeGetter(change -> Timestamp.from(change.getLastUpdatedOn()))); |
| |
| public static final IndexedField<ChangeData, Timestamp>.SearchSpec UPDATED_SPEC = |
| UPDATED_FIELD.timestamp("updated2"); |
| |
| /** When this change was merged, time since January 1, 1970. */ |
| // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant |
| public static final IndexedField<ChangeData, Timestamp> MERGED_ON_FIELD = |
| IndexedField.<ChangeData>timestampBuilder("MergedOn") |
| .stored() |
| .build( |
| cd -> cd.getMergedOn().map(Timestamp::from).orElse(null), |
| (cd, field) -> cd.setMergedOn(field != null ? field.toInstant() : null)); |
| |
| public static final IndexedField<ChangeData, Timestamp>.SearchSpec MERGED_ON_SPEC = |
| MERGED_ON_FIELD.timestamp(ChangeQueryBuilder.FIELD_MERGED_ON); |
| |
| /** List of full file paths modified in the current patch set. */ |
| public static final IndexedField<ChangeData, Iterable<String>> PATH_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("ModifiedFile") |
| .build(cd -> firstNonNull(cd.currentFilePaths(), ImmutableList.of())); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PATH_SPEC = |
| PATH_FIELD |
| // Named for backwards compatibility. |
| .exact(ChangeQueryBuilder.FIELD_FILE); |
| |
| public static Set<String> getFileParts(ChangeData cd) { |
| List<String> paths = cd.currentFilePaths(); |
| |
| 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 IndexedField<ChangeData, Iterable<String>> HASHTAG_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("Hashtag") |
| .size(200) |
| .build(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet())); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec HASHTAG_SPEC = |
| HASHTAG_FIELD.exact(ChangeQueryBuilder.FIELD_HASHTAG); |
| |
| /** Hashtags as fulltext field for in-string search. */ |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FUZZY_HASHTAG = |
| HASHTAG_FIELD.fullText("hashtag2"); |
| |
| /** Hashtags as prefix field for in-string search. */ |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PREFIX_HASHTAG = |
| HASHTAG_FIELD.prefix("hashtag3"); |
| |
| /** Hashtags with original case. */ |
| public static final IndexedField<ChangeData, Iterable<byte[]>> HASHTAG_CASE_AWARE_FIELD = |
| IndexedField.<ChangeData>iterableByteArrayBuilder("HashtagCaseAware") |
| .stored() |
| .build( |
| cd -> cd.hashtags().stream().map(t -> t.getBytes(UTF_8)).collect(toSet()), |
| (cd, field) -> |
| cd.setHashtags( |
| StreamSupport.stream(field.spliterator(), false) |
| .map(f -> new String(f, UTF_8)) |
| .collect(toImmutableSet()))); |
| |
| public static final IndexedField<ChangeData, Iterable<byte[]>>.SearchSpec |
| HASHTAG_CASE_AWARE_SPEC = HASHTAG_CASE_AWARE_FIELD.storedOnly("_hashtag"); |
| |
| /** Components of each file path modified in the current patch set. */ |
| public static final IndexedField<ChangeData, Iterable<String>> FILE_PART_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("FilePart").build(ChangeField::getFileParts); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FILE_PART_SPEC = |
| FILE_PART_FIELD.exact(ChangeQueryBuilder.FIELD_FILEPART); |
| |
| /** File extensions of each file modified in the current patch set. */ |
| public static final IndexedField<ChangeData, Iterable<String>> EXTENSION_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("Extension") |
| .size(100) |
| .build(ChangeField::getExtensions); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXTENSION_SPEC = |
| EXTENSION_FIELD.exact(ChangeQueryBuilder.FIELD_EXTENSION); |
| |
| public static Set<String> getExtensions(ChangeData cd) { |
| return extensions(cd).collect(toSet()); |
| } |
| |
| /** |
| * File extensions of each file modified in the current patch set as a sorted list. The purpose of |
| * this field is to allow matching changes that only touch files with certain file extensions. |
| */ |
| public static final IndexedField<ChangeData, String> ONLY_EXTENSIONS_FIELD = |
| IndexedField.<ChangeData>stringBuilder("OnlyExtensions") |
| .build(ChangeField::getAllExtensionsAsList); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec ONLY_EXTENSIONS_SPEC = |
| ONLY_EXTENSIONS_FIELD.exact(ChangeQueryBuilder.FIELD_ONLY_EXTENSIONS); |
| |
| public static String getAllExtensionsAsList(ChangeData cd) { |
| return extensions(cd).distinct().sorted().collect(joining(",")); |
| } |
| |
| /** |
| * Returns a stream with all file extensions that are used by files in the given change. A file |
| * extension is defined as the portion of the filename following the final `.`. Files with no `.` |
| * in their name have no extension. For them an empty string is returned as part of the stream. |
| * |
| * <p>If the change contains multiple files with the same extension the extension is returned |
| * multiple times in the stream (once per file). |
| */ |
| private static Stream<String> extensions(ChangeData cd) { |
| return cd.currentFilePaths().stream() |
| // Use case-insensitive file extensions even though other file fields are case-sensitive. |
| // If we want to find "all Java files", we want to match both .java and .JAVA, even if we |
| // normally care about case sensitivity. (Whether we should change the existing file/path |
| // predicates to be case insensitive is a separate question.) |
| .map(f -> Files.getFileExtension(f).toLowerCase(Locale.US)); |
| } |
| |
| /** Footers from the commit message of the current patch set. */ |
| public static final IndexedField<ChangeData, Iterable<String>> FOOTER_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("Footer").build(ChangeField::getFooters); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FOOTER_SPEC = |
| FOOTER_FIELD.exact(ChangeQueryBuilder.FIELD_FOOTER); |
| |
| public static Set<String> getFooters(ChangeData cd) { |
| return cd.commitFooters().stream() |
| .map(f -> f.toString().toLowerCase(Locale.US)) |
| .collect(toSet()); |
| } |
| |
| /** Footers from the commit message of the current patch set. */ |
| public static final IndexedField<ChangeData, Iterable<String>> FOOTER_NAME_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("FooterName") |
| .build(ChangeField::getFootersNames); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FOOTER_NAME = |
| FOOTER_NAME_FIELD.exact(ChangeQueryBuilder.FIELD_FOOTER_NAME); |
| |
| public static Set<String> getFootersNames(ChangeData cd) { |
| return cd.commitFooters().stream().map(f -> f.getKey()).collect(toSet()); |
| } |
| |
| /** Folders that are touched by the current patch set. */ |
| public static final IndexedField<ChangeData, Iterable<String>> DIRECTORY_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("DirField").build(ChangeField::getDirectories); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec DIRECTORY_SPEC = |
| DIRECTORY_FIELD.exact(ChangeQueryBuilder.FIELD_DIRECTORY); |
| |
| public static Set<String> getDirectories(ChangeData cd) { |
| List<String> paths = cd.currentFilePaths(); |
| |
| Splitter s = Splitter.on('/').omitEmptyStrings(); |
| Set<String> r = new HashSet<>(); |
| for (String path : paths) { |
| StringBuilder directory = new StringBuilder(); |
| r.add(directory.toString()); |
| String nextPart = null; |
| for (String part : s.split(path.toLowerCase(Locale.US))) { |
| if (nextPart != null) { |
| r.add(nextPart); |
| |
| if (directory.length() > 0) { |
| directory.append("/"); |
| } |
| directory.append(nextPart); |
| |
| String intermediateDir = directory.toString(); |
| int i = intermediateDir.indexOf('/'); |
| while (i >= 0) { |
| r.add(intermediateDir); |
| intermediateDir = intermediateDir.substring(i + 1); |
| i = intermediateDir.indexOf('/'); |
| } |
| } |
| nextPart = part; |
| } |
| } |
| return r; |
| } |
| |
| /** Owner/creator of the change. */ |
| public static final IndexedField<ChangeData, Integer> OWNER_FIELD = |
| IndexedField.<ChangeData>integerBuilder("Owner") |
| .required() |
| .build(changeGetter(c -> c.getOwner().get())); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec OWNER_SPEC = |
| OWNER_FIELD.integer(ChangeQueryBuilder.FIELD_OWNER); |
| |
| /** Uploader of the latest patch set. */ |
| public static final IndexedField<ChangeData, Integer> UPLOADER_FIELD = |
| IndexedField.<ChangeData>integerBuilder("Uploader") |
| .required() |
| .build(cd -> cd.currentPatchSet().uploader().get()); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec UPLOADER_SPEC = |
| UPLOADER_FIELD.integer(ChangeQueryBuilder.FIELD_UPLOADER); |
| |
| /** References the source change number that this change was cherry-picked from. */ |
| public static final IndexedField<ChangeData, Integer> CHERRY_PICK_OF_CHANGE_FIELD = |
| IndexedField.<ChangeData>integerBuilder("CherryPickOfChange") |
| .build( |
| cd -> |
| cd.change().getCherryPickOf() != null |
| ? cd.change().getCherryPickOf().changeId().get() |
| : null); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec CHERRY_PICK_OF_CHANGE = |
| CHERRY_PICK_OF_CHANGE_FIELD.integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_CHANGE); |
| |
| /** References the source change patch-set that this change was cherry-picked from. */ |
| public static final IndexedField<ChangeData, Integer> CHERRY_PICK_OF_PATCHSET_FIELD = |
| IndexedField.<ChangeData>integerBuilder("CherryPickOfPatchset") |
| .build( |
| cd -> |
| cd.change().getCherryPickOf() != null |
| ? cd.change().getCherryPickOf().get() |
| : null); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec CHERRY_PICK_OF_PATCHSET = |
| CHERRY_PICK_OF_PATCHSET_FIELD.integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_PATCHSET); |
| |
| /** This class decouples the internal and API types from storage. */ |
| private static class StoredAttentionSetEntry { |
| final long timestampMillis; |
| final int userId; |
| final String reason; |
| final AttentionSetUpdate.Operation operation; |
| |
| StoredAttentionSetEntry(AttentionSetUpdate attentionSetUpdate) { |
| timestampMillis = attentionSetUpdate.timestamp().toEpochMilli(); |
| userId = attentionSetUpdate.account().get(); |
| reason = attentionSetUpdate.reason(); |
| operation = attentionSetUpdate.operation(); |
| } |
| |
| AttentionSetUpdate toAttentionSetUpdate() { |
| return AttentionSetUpdate.createFromRead( |
| Instant.ofEpochMilli(timestampMillis), Account.id(userId), operation, reason); |
| } |
| } |
| |
| /** |
| * Users included in the attention set of the change. This omits timestamp, reason and possible |
| * future fields. |
| * |
| * @see #ATTENTION_SET_FULL_SPEC |
| */ |
| public static final IndexedField<ChangeData, Iterable<Integer>> ATTENTION_SET_USERS_FIELD = |
| IndexedField.<ChangeData>iterableIntegerBuilder("AttentionSetUsers") |
| .build(ChangeField::getAttentionSetUserIds); |
| |
| public static final IndexedField<ChangeData, Iterable<Integer>>.SearchSpec ATTENTION_SET_USERS = |
| ATTENTION_SET_USERS_FIELD.integer(ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS); |
| |
| /** Number of changes that contain attention set. */ |
| public static final IndexedField<ChangeData, Integer> ATTENTION_SET_USERS_COUNT_FIELD = |
| IndexedField.<ChangeData>integerBuilder("AttentionSetUsersCount") |
| .stored() |
| .build(cd -> additionsOnly(cd.attentionSet()).size()); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec ATTENTION_SET_USERS_COUNT = |
| ATTENTION_SET_USERS_COUNT_FIELD.integerRange( |
| ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS_COUNT); |
| |
| /** |
| * The full attention set data including timestamp, reason and possible future fields. |
| * |
| * @see #ATTENTION_SET_USERS |
| */ |
| public static final IndexedField<ChangeData, Iterable<byte[]>> ATTENTION_SET_FULL_FIELD = |
| IndexedField.<ChangeData>iterableByteArrayBuilder("AttentionSetFull") |
| .stored() |
| .required() |
| .build( |
| ChangeField::storedAttentionSet, |
| (cd, value) -> |
| parseAttentionSet( |
| StreamSupport.stream(value.spliterator(), false) |
| .map(v -> new String(v, UTF_8)) |
| .collect(toImmutableSet()), |
| cd)); |
| |
| public static final IndexedField<ChangeData, Iterable<byte[]>>.SearchSpec |
| ATTENTION_SET_FULL_SPEC = |
| ATTENTION_SET_FULL_FIELD.storedOnly(ChangeQueryBuilder.FIELD_ATTENTION_SET_FULL); |
| |
| /** The user assigned to the change. */ |
| // The getter always returns NO_ASSIGNEE, since assignee field is deprecated. |
| @Deprecated |
| public static final IndexedField<ChangeData, Integer> ASSIGNEE_FIELD = |
| IndexedField.<ChangeData>integerBuilder("Assignee").build(changeGetter(c -> NO_ASSIGNEE)); |
| |
| @Deprecated |
| public static final IndexedField<ChangeData, Integer>.SearchSpec ASSIGNEE_SPEC = |
| ASSIGNEE_FIELD.integer(ChangeQueryBuilder.FIELD_ASSIGNEE); |
| |
| /** Reviewer(s) associated with the change. */ |
| public static final IndexedField<ChangeData, Iterable<String>> REVIEWER_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("Reviewer") |
| .stored() |
| .build( |
| cd -> getReviewerFieldValues(cd.reviewers()), |
| (cd, field) -> cd.setReviewers(parseReviewerFieldValues(cd.getId(), field))); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec REVIEWER_SPEC = |
| REVIEWER_FIELD.exact("reviewer2"); |
| |
| /** Reviewer(s) associated with the change that do not have a gerrit account. */ |
| public static final IndexedField<ChangeData, Iterable<String>> REVIEWER_BY_EMAIL_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("ReviewerByEmail") |
| .stored() |
| .build( |
| cd -> getReviewerByEmailFieldValues(cd.reviewersByEmail()), |
| (cd, field) -> |
| cd.setReviewersByEmail(parseReviewerByEmailFieldValues(cd.getId(), field))); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec REVIEWER_BY_EMAIL = |
| REVIEWER_BY_EMAIL_FIELD.exact("reviewer_by_email"); |
| |
| /** Reviewer(s) modified during change's current WIP phase. */ |
| public static final IndexedField<ChangeData, Iterable<String>> PENDING_REVIEWER_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("PendingReviewer") |
| .stored() |
| .build( |
| cd -> getReviewerFieldValues(cd.pendingReviewers()), |
| (cd, field) -> cd.setPendingReviewers(parseReviewerFieldValues(cd.getId(), field))); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PENDING_REVIEWER_SPEC = |
| PENDING_REVIEWER_FIELD.exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER); |
| |
| /** Reviewer(s) by email modified during change's current WIP phase. */ |
| public static final IndexedField<ChangeData, Iterable<String>> PENDING_REVIEWER_BY_EMAIL_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("PendingReviewerByEmail") |
| .stored() |
| .build( |
| cd -> getReviewerByEmailFieldValues(cd.pendingReviewersByEmail()), |
| (cd, field) -> |
| cd.setPendingReviewersByEmail( |
| parseReviewerByEmailFieldValues(cd.getId(), field))); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec |
| PENDING_REVIEWER_BY_EMAIL = |
| PENDING_REVIEWER_BY_EMAIL_FIELD.exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER_BY_EMAIL); |
| |
| /** References a change that this change reverts. */ |
| public static final IndexedField<ChangeData, Integer> REVERT_OF_FIELD = |
| IndexedField.<ChangeData>integerBuilder("RevertOf") |
| .build(cd -> cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec REVERT_OF = |
| REVERT_OF_FIELD.integer(ChangeQueryBuilder.FIELD_REVERTOF); |
| |
| public static final IndexedField<ChangeData, String> IS_PURE_REVERT_FIELD = |
| IndexedField.<ChangeData>stringBuilder("IsPureRevert") |
| .size(1) |
| .build(cd -> Boolean.TRUE.equals(cd.isPureRevert()) ? "1" : "0"); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec IS_PURE_REVERT_SPEC = |
| IS_PURE_REVERT_FIELD.fullText(ChangeQueryBuilder.FIELD_PURE_REVERT); |
| |
| /** |
| * Determines if a change is submittable based on {@link |
| * com.google.gerrit.entities.SubmitRequirement}s. |
| */ |
| public static final IndexedField<ChangeData, String> IS_SUBMITTABLE_FIELD = |
| IndexedField.<ChangeData>stringBuilder("IsSubmittable") |
| .size(1) |
| .build( |
| cd -> |
| // All submit requirements should be fulfilled |
| cd.submitRequirementsIncludingLegacy().values().stream() |
| .allMatch(SubmitRequirementResult::fulfilled) |
| ? "1" |
| : "0"); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec IS_SUBMITTABLE_SPEC = |
| IS_SUBMITTABLE_FIELD.exact(ChangeQueryBuilder.FIELD_IS_SUBMITTABLE); |
| |
| @VisibleForTesting |
| static List<String> getReviewerFieldValues(ReviewerSet reviewers) { |
| List<String> r = new ArrayList<>(reviewers.asTable().size() * 2); |
| for (Table.Cell<ReviewerStateInternal, Account.Id, Instant> c : reviewers.asTable().cellSet()) { |
| String v = getReviewerFieldValue(c.getRowKey(), c.getColumnKey()); |
| r.add(v); |
| r.add(v + ',' + c.getValue().toEpochMilli()); |
| } |
| 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, Instant> c : |
| reviewersByEmail.asTable().cellSet()) { |
| String v = getReviewerByEmailFieldValue(c.getRowKey(), c.getColumnKey()); |
| r.add(v); |
| if (c.getColumnKey().name() != null) { |
| // Add another entry without the name to provide search functionality on the email |
| Address emailOnly = Address.create(c.getColumnKey().email()); |
| r.add(getReviewerByEmailFieldValue(c.getRowKey(), emailOnly)); |
| } |
| r.add(v + ',' + c.getValue().toEpochMilli()); |
| } |
| 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, Instant> 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; |
| } |
| |
| Optional<ReviewerStateInternal> reviewerState = getReviewerState(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)); |
| if (l == null) { |
| logger.atWarning().log( |
| "Failed to parse timestamp of reviewer field from change %s: %s", changeId.get(), v); |
| continue; |
| } |
| Instant timestamp = Instant.ofEpochMilli(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, Instant> 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; |
| } |
| |
| Optional<ReviewerStateInternal> reviewerState = getReviewerState(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)); |
| if (l == null) { |
| logger.atWarning().log( |
| "Failed to parse timestamp of reviewer by email field from change %s: %s", |
| changeId.get(), v); |
| continue; |
| } |
| Instant timestamp = Instant.ofEpochMilli(l); |
| |
| b.put(reviewerState.get(), address, timestamp); |
| } |
| return ReviewerByEmailSet.fromTable(b.build()); |
| } |
| |
| private static Optional<ReviewerStateInternal> getReviewerState(String value) { |
| try { |
| return Optional.of(ReviewerStateInternal.valueOf(value)); |
| } catch (IllegalArgumentException | NullPointerException e) { |
| return Optional.empty(); |
| } |
| } |
| |
| private static ImmutableSet<Integer> getAttentionSetUserIds(ChangeData changeData) { |
| return additionsOnly(changeData.attentionSet()).stream() |
| .map(update -> update.account().get()) |
| .collect(toImmutableSet()); |
| } |
| |
| private static ImmutableSet<byte[]> storedAttentionSet(ChangeData changeData) { |
| return changeData.attentionSet().stream() |
| .map(StoredAttentionSetEntry::new) |
| .map(storedAttentionSetEntry -> GSON.toJson(storedAttentionSetEntry).getBytes(UTF_8)) |
| .collect(toImmutableSet()); |
| } |
| |
| /** |
| * Deserializes the specified attention set entries from JSON and stores them in the specified |
| * change. |
| */ |
| public static void parseAttentionSet( |
| Collection<String> storedAttentionSetEntriesJson, ChangeData changeData) { |
| ImmutableSet<AttentionSetUpdate> attentionSet = |
| storedAttentionSetEntriesJson.stream() |
| .map( |
| entry -> GSON.fromJson(entry, StoredAttentionSetEntry.class).toAttentionSetUpdate()) |
| .collect(toImmutableSet()); |
| changeData.setAttentionSet(attentionSet); |
| } |
| |
| /** Commit ID of any patch set on the change, using prefix match. */ |
| public static final IndexedField<ChangeData, Iterable<String>> COMMIT_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("CommitId") |
| .size(40) |
| .required() |
| .build(ChangeField::getRevisions); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec COMMIT_SPEC = |
| COMMIT_FIELD.prefix(ChangeQueryBuilder.FIELD_COMMIT); |
| |
| /** Commit ID of any patch set on the change, using exact match. */ |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXACT_COMMIT_SPEC = |
| COMMIT_FIELD.exact(ChangeQueryBuilder.FIELD_EXACTCOMMIT); |
| |
| private static ImmutableSet<String> getRevisions(ChangeData cd) { |
| return cd.patchSets().stream().map(ps -> ps.commitId().name()).collect(toImmutableSet()); |
| } |
| |
| /** Tracking id extracted from a footer. */ |
| public static final IndexedField<ChangeData, Iterable<String>> TR_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("TrackingFooter") |
| .build(cd -> ImmutableSet.copyOf(cd.trackingFooters().values())); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec TR_SPEC = |
| TR_FIELD.exact(ChangeQueryBuilder.FIELD_TR); |
| |
| /** List of labels on the current patch set including change owner votes. */ |
| public static final IndexedField<ChangeData, Iterable<String>> LABEL_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("Label").required().build(cd -> getLabels(cd)); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec LABEL_SPEC = |
| LABEL_FIELD.exact("label2"); |
| |
| private static Set<String> getLabels(ChangeData cd) { |
| Set<String> allApprovals = new HashSet<>(); |
| Set<String> distinctApprovals = new HashSet<>(); |
| Table<String, Short, Integer> voteCounts = HashBasedTable.create(); |
| for (PatchSetApproval a : cd.currentApprovals()) { |
| if (a.value() != 0 && !a.isLegacySubmit()) { |
| increment(voteCounts, a.label(), a.value()); |
| Optional<LabelType> labelType = cd.getLabelTypes().byLabel(a.labelId()); |
| |
| allApprovals.add(formatLabel(a.label(), a.value(), a.accountId())); |
| allApprovals.addAll(getMagicLabelFormats(a.label(), a.value(), labelType, a.accountId())); |
| allApprovals.addAll(getLabelOwnerFormats(a, cd, labelType)); |
| allApprovals.addAll(getLabelNonUploaderFormats(a, cd, labelType)); |
| distinctApprovals.add(formatLabel(a.label(), a.value())); |
| distinctApprovals.addAll( |
| getMagicLabelFormats(a.label(), a.value(), labelType, /* accountId= */ null)); |
| } |
| } |
| allApprovals.addAll(distinctApprovals); |
| allApprovals.addAll(getCountLabelFormats(voteCounts, cd)); |
| return allApprovals; |
| } |
| |
| private static void increment(Table<String, Short, Integer> table, String k1, short k2) { |
| if (!table.contains(k1, k2)) { |
| table.put(k1, k2, 1); |
| } else { |
| int val = table.get(k1, k2); |
| table.put(k1, k2, val + 1); |
| } |
| } |
| |
| private static List<String> getCountLabelFormats( |
| Table<String, Short, Integer> voteCounts, ChangeData cd) { |
| List<String> allFormats = new ArrayList<>(); |
| for (String label : voteCounts.rowMap().keySet()) { |
| Optional<LabelType> labelType = cd.getLabelTypes().byLabel(label); |
| Map<Short, Integer> row = voteCounts.row(label); |
| for (short vote : row.keySet()) { |
| int count = row.get(vote); |
| allFormats.addAll(getCountLabelFormats(labelType, label, vote, count)); |
| } |
| } |
| return allFormats; |
| } |
| |
| private static List<String> getCountLabelFormats( |
| Optional<LabelType> labelType, String label, short vote, int count) { |
| List<String> formats = |
| getMagicLabelFormats(label, vote, labelType, /* accountId= */ null, /* count= */ count); |
| formats.add(formatLabel(label, vote, count)); |
| return formats; |
| } |
| |
| /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */ |
| private static List<String> getMagicLabelFormats( |
| String label, short labelVal, Optional<LabelType> labelType, @Nullable Account.Id accountId) { |
| return getMagicLabelFormats(label, labelVal, labelType, accountId, /* count= */ null); |
| } |
| |
| /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */ |
| private static List<String> getMagicLabelFormats( |
| String label, |
| short labelVal, |
| Optional<LabelType> labelType, |
| @Nullable Account.Id accountId, |
| @Nullable Integer count) { |
| List<String> labels = new ArrayList<>(); |
| if (labelType.isPresent()) { |
| if (labelVal == labelType.get().getMaxPositive()) { |
| labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId, count)); |
| } |
| if (labelVal == labelType.get().getMaxNegative()) { |
| labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId, count)); |
| } |
| } |
| labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId, count)); |
| return labels; |
| } |
| |
| private static List<String> getLabelOwnerFormats( |
| PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) { |
| List<String> allFormats = new ArrayList<>(); |
| if (cd.change().getOwner().equals(a.accountId())) { |
| allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID)); |
| allFormats.addAll( |
| getMagicLabelFormats( |
| a.label(), a.value(), labelType, ChangeQueryBuilder.OWNER_ACCOUNT_ID)); |
| } |
| return allFormats; |
| } |
| |
| private static List<String> getLabelNonUploaderFormats( |
| PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) { |
| List<String> allFormats = new ArrayList<>(); |
| if (!cd.currentPatchSet().uploader().equals(a.accountId())) { |
| allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)); |
| allFormats.addAll( |
| getMagicLabelFormats( |
| a.label(), a.value(), labelType, ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)); |
| } |
| return allFormats; |
| } |
| |
| public static Set<String> getAuthorParts(ChangeData cd) { |
| return SchemaUtil.getPersonParts(cd.getAuthor()); |
| } |
| |
| public static ImmutableSet<String> getAuthorNameAndEmail(ChangeData cd) { |
| return getNameAndEmail(cd.getAuthor()); |
| } |
| |
| public static Set<String> getCommitterParts(ChangeData cd) { |
| return SchemaUtil.getPersonParts(cd.getCommitter()); |
| } |
| |
| public static ImmutableSet<String> getCommitterNameAndEmail(ChangeData cd) { |
| return getNameAndEmail(cd.getCommitter()); |
| } |
| |
| private static ImmutableSet<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 IndexedField<ChangeData, Iterable<String>> AUTHOR_PARTS_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("AuthorParts") |
| .required() |
| .description( |
| "The exact email address, or any part of the author name or email address, in the current patch set.") |
| .build(ChangeField::getAuthorParts); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec AUTHOR_PARTS_SPEC = |
| AUTHOR_PARTS_FIELD.fullText(ChangeQueryBuilder.FIELD_AUTHOR); |
| |
| /** The exact name, email address and NameEmail of the author. */ |
| public static final IndexedField<ChangeData, Iterable<String>> EXACT_AUTHOR_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("ExactAuthor") |
| .required() |
| .description("The exact name, email address and NameEmail of the author.") |
| .build(ChangeField::getAuthorNameAndEmail); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXACT_AUTHOR_SPEC = |
| EXACT_AUTHOR_FIELD.exact(ChangeQueryBuilder.FIELD_EXACTAUTHOR); |
| |
| /** |
| * The exact email address, or any part of the committer name or email address, in the current |
| * patch set. |
| */ |
| public static final IndexedField<ChangeData, Iterable<String>> COMMITTER_PARTS_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("CommitterParts") |
| .description( |
| "The exact email address, or any part of the committer name or email address, in the current patch set.") |
| .required() |
| .build(ChangeField::getCommitterParts); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec COMMITTER_PARTS_SPEC = |
| COMMITTER_PARTS_FIELD.fullText(ChangeQueryBuilder.FIELD_COMMITTER); |
| |
| /** The exact name, email address, and NameEmail of the committer. */ |
| public static final IndexedField<ChangeData, Iterable<String>> EXACT_COMMITTER_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("ExactCommiter") |
| .required() |
| .description("The exact name, email address, and NameEmail of the committer.") |
| .build(ChangeField::getCommitterNameAndEmail); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXACT_COMMITTER_SPEC = |
| EXACT_COMMITTER_FIELD.exact(ChangeQueryBuilder.FIELD_EXACTCOMMITTER); |
| |
| /** Serialized change object, used for pre-populating results. */ |
| private static final TypeToken<Entities.Change> CHANGE_TYPE_TOKEN = |
| new TypeToken<>() { |
| private static final long serialVersionUID = 1L; |
| }; |
| |
| public static final IndexedField<ChangeData, Entities.Change> CHANGE_FIELD = |
| IndexedField.<ChangeData, Entities.Change>builder("Change", CHANGE_TYPE_TOKEN) |
| .stored() |
| .required() |
| .protoConverter(Optional.of(ChangeProtoConverter.INSTANCE)) |
| .build( |
| changeGetter(change -> entityToProto(ChangeProtoConverter.INSTANCE, change)), |
| (cd, value) -> |
| cd.setChange(decodeProtoToEntity(value, ChangeProtoConverter.INSTANCE))); |
| |
| public static final IndexedField<ChangeData, Entities.Change>.SearchSpec CHANGE_SPEC = |
| CHANGE_FIELD.storedOnly("_change"); |
| |
| /** Serialized approvals for the current patch set, used for pre-populating results. */ |
| private static final TypeToken<Iterable<Entities.PatchSetApproval>> APPROVAL_TYPE_TOKEN = |
| new TypeToken<>() { |
| private static final long serialVersionUID = 1L; |
| }; |
| |
| public static final IndexedField<ChangeData, Iterable<Entities.PatchSetApproval>> APPROVAL_FIELD = |
| IndexedField.<ChangeData, Iterable<Entities.PatchSetApproval>>builder( |
| "Approval", APPROVAL_TYPE_TOKEN) |
| .stored() |
| .required() |
| .protoConverter(Optional.of(PatchSetApprovalProtoConverter.INSTANCE)) |
| .build( |
| cd -> |
| entitiesToProtos(PatchSetApprovalProtoConverter.INSTANCE, cd.currentApprovals()), |
| (cd, field) -> |
| cd.setCurrentApprovals( |
| decodeProtosToEntities(field, PatchSetApprovalProtoConverter.INSTANCE))); |
| |
| public static final IndexedField<ChangeData, Iterable<Entities.PatchSetApproval>>.SearchSpec |
| APPROVAL_SPEC = APPROVAL_FIELD.storedOnly("_approval"); |
| |
| public static String formatLabel(String label, int value) { |
| return formatLabel(label, value, /* accountId= */ null, /* count= */ null); |
| } |
| |
| public static String formatLabel(String label, int value, @Nullable Integer count) { |
| return formatLabel(label, value, /* accountId= */ null, count); |
| } |
| |
| public static String formatLabel(String label, int value, Account.Id accountId) { |
| return formatLabel(label, value, accountId, /* count= */ null); |
| } |
| |
| public static String formatLabel( |
| String label, int value, @Nullable Account.Id accountId, @Nullable Integer count) { |
| return label.toLowerCase(Locale.US) |
| + (value >= 0 ? "+" : "") |
| + value |
| + (accountId != null ? "," + formatAccount(accountId) : "") |
| + (count != null ? ",count=" + count : ""); |
| } |
| |
| public static String formatLabel(String label, String value, @Nullable Integer count) { |
| return formatLabel(label, value, /* accountId= */ null, count); |
| } |
| |
| public static String formatLabel( |
| String label, String value, @Nullable Account.Id accountId, @Nullable Integer count) { |
| return label.toLowerCase(Locale.US) |
| + "=" |
| + value |
| + (accountId != null ? "," + formatAccount(accountId) : "") |
| + (count != null ? ",count=" + count : ""); |
| } |
| |
| private static String formatAccount(Account.Id accountId) { |
| if (ChangeQueryBuilder.OWNER_ACCOUNT_ID.equals(accountId)) { |
| return ChangeQueryBuilder.ARG_ID_OWNER; |
| } else if (ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID.equals(accountId)) { |
| return ChangeQueryBuilder.ARG_ID_NON_UPLOADER; |
| } |
| return Integer.toString(accountId.get()); |
| } |
| |
| /** Commit message of the current patch set. */ |
| public static final IndexedField<ChangeData, String> COMMIT_MESSAGE_FIELD = |
| IndexedField.<ChangeData>stringBuilder("CommitMessage") |
| .required() |
| .build(ChangeData::commitMessage); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec COMMIT_MESSAGE = |
| COMMIT_MESSAGE_FIELD.fullText(ChangeQueryBuilder.FIELD_MESSAGE); |
| |
| /** Commit message of the current patch set, used to exactly match the commit message */ |
| public static final IndexedField<ChangeData, String> COMMIT_MESSAGE_EXACT_FIELD = |
| IndexedField.<ChangeData>stringBuilder("CommitMessageExact") |
| .required() |
| .description( |
| "Same as CommitMessage, but truncated, since supporting such large tokens may be problematic for indexes.") |
| .build(cd -> truncateStringValueToMaxTermLength(cd.commitMessage())); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec COMMIT_MESSAGE_EXACT = |
| COMMIT_MESSAGE_EXACT_FIELD.exact(ChangeQueryBuilder.FIELD_MESSAGE_EXACT); |
| |
| /** Subject of the current patch set (aka first line of the commit message). */ |
| public static final IndexedField<ChangeData, String> SUBJECT_FIELD = |
| IndexedField.<ChangeData>stringBuilder("Subject") |
| .required() |
| .build(changeGetter(Change::getSubject)); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec SUBJECT_SPEC = |
| SUBJECT_FIELD.fullText(ChangeQueryBuilder.FIELD_SUBJECT); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec PREFIX_SUBJECT_SPEC = |
| SUBJECT_FIELD.prefix(ChangeQueryBuilder.FIELD_PREFIX_SUBJECT); |
| |
| /** Summary or inline comment. */ |
| public static final IndexedField<ChangeData, Iterable<String>> COMMENT_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("Comment") |
| .build( |
| cd -> |
| Stream.concat( |
| cd.publishedComments().stream().map(c -> c.message), |
| // Some endpoint allow passing user message in input, and we still want to |
| // search by that. Index on message template with placeholders for user |
| // data, so we don't |
| // persist user identifiable information data in index. |
| cd.messages().stream().map(ChangeMessage::getMessage)) |
| .collect(toSet())); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec COMMENT_SPEC = |
| COMMENT_FIELD.fullText(ChangeQueryBuilder.FIELD_COMMENT); |
| |
| /** Number of unresolved comment threads of the change, including robot comments. */ |
| public static final IndexedField<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT_FIELD = |
| IndexedField.<ChangeData>integerBuilder("UnresolvedCommentCount") |
| .stored() |
| .build( |
| ChangeData::unresolvedCommentCount, |
| (cd, field) -> cd.setUnresolvedCommentCount(field)); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec UNRESOLVED_COMMENT_COUNT_SPEC = |
| UNRESOLVED_COMMENT_COUNT_FIELD.integerRange( |
| ChangeQueryBuilder.FIELD_UNRESOLVED_COMMENT_COUNT); |
| |
| /** Total number of published inline comments of the change, including robot comments. */ |
| public static final IndexedField<ChangeData, Integer> TOTAL_COMMENT_COUNT_FIELD = |
| IndexedField.<ChangeData>integerBuilder("TotalCommentCount") |
| .stored() |
| .build(ChangeData::totalCommentCount, (cd, field) -> cd.setTotalCommentCount(field)); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec TOTAL_COMMENT_COUNT_SPEC = |
| TOTAL_COMMENT_COUNT_FIELD.integerRange("total_comments"); |
| |
| /** Whether the change is mergeable. */ |
| public static final IndexedField<ChangeData, String> MERGEABLE_FIELD = |
| IndexedField.<ChangeData>stringBuilder("Mergeable") |
| .stored() |
| .size(1) |
| .build( |
| cd -> { |
| Boolean m = cd.isMergeable(); |
| if (m == null) { |
| return null; |
| } |
| return m ? "1" : "0"; |
| }, |
| (cd, field) -> cd.setMergeable(field == null ? false : field.equals("1"))); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec MERGEABLE_SPEC = |
| MERGEABLE_FIELD.exact(ChangeQueryBuilder.FIELD_MERGEABLE); |
| |
| /** Whether the change is a merge commit. */ |
| public static final IndexedField<ChangeData, String> MERGE_FIELD = |
| IndexedField.<ChangeData>stringBuilder("Merge") |
| .stored() |
| .size(1) |
| .build( |
| cd -> { |
| Boolean m = cd.isMerge(); |
| if (m == null) { |
| return null; |
| } |
| return m ? "1" : "0"; |
| }); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec MERGE_SPEC = |
| MERGE_FIELD.exact(ChangeQueryBuilder.FIELD_MERGE); |
| |
| /** Whether the change is a cherry pick of another change. */ |
| public static final IndexedField<ChangeData, String> CHERRY_PICK_FIELD = |
| IndexedField.<ChangeData>stringBuilder("CherryPick") |
| .stored() |
| .size(1) |
| .build(cd -> cd.change().getCherryPickOf() != null ? "1" : "0"); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec CHERRY_PICK_SPEC = |
| CHERRY_PICK_FIELD.exact(ChangeQueryBuilder.FIELD_CHERRYPICK); |
| |
| /** The number of inserted lines in this change. */ |
| public static final IndexedField<ChangeData, Integer> ADDED_LINES_FIELD = |
| IndexedField.<ChangeData>integerBuilder("AddedLines") |
| .stored() |
| .build( |
| cd -> cd.changedLines().isPresent() ? cd.changedLines().get().insertions : null, |
| (cd, field) -> { |
| if (field != null) { |
| cd.setLinesInserted(field); |
| } |
| }); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec ADDED_LINES_SPEC = |
| ADDED_LINES_FIELD.integerRange(ChangeQueryBuilder.FIELD_ADDED); |
| |
| /** The number of deleted lines in this change. */ |
| public static final IndexedField<ChangeData, Integer> DELETED_LINES_FIELD = |
| IndexedField.<ChangeData>integerBuilder("DeletedLines") |
| .stored() |
| .build( |
| cd -> cd.changedLines().isPresent() ? cd.changedLines().get().deletions : null, |
| (cd, field) -> { |
| if (field != null) { |
| cd.setLinesDeleted(field); |
| } |
| }); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec DELETED_LINES_SPEC = |
| DELETED_LINES_FIELD.integerRange(ChangeQueryBuilder.FIELD_DELETED); |
| |
| /** The total number of modified lines in this change. */ |
| public static final IndexedField<ChangeData, Integer> DELTA_LINES_FIELD = |
| IndexedField.<ChangeData>integerBuilder("DeltaLines") |
| .stored() |
| .build(cd -> cd.changedLines().map(c -> c.insertions + c.deletions).orElse(null)); |
| |
| public static final IndexedField<ChangeData, Integer>.SearchSpec DELTA_LINES_SPEC = |
| DELTA_LINES_FIELD.integerRange(ChangeQueryBuilder.FIELD_DELTA); |
| |
| /** Determines if this change is private. */ |
| public static final IndexedField<ChangeData, String> PRIVATE_FIELD = |
| IndexedField.<ChangeData>stringBuilder("IsPrivate") |
| .size(1) |
| .build(cd -> cd.change().isPrivate() ? "1" : "0"); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec PRIVATE_SPEC = |
| PRIVATE_FIELD.exact(ChangeQueryBuilder.FIELD_PRIVATE); |
| |
| /** Determines if this change is work in progress. */ |
| public static final IndexedField<ChangeData, String> WIP_FIELD = |
| IndexedField.<ChangeData>stringBuilder("WIP") |
| .size(1) |
| .build(cd -> cd.change().isWorkInProgress() ? "1" : "0"); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec WIP_SPEC = |
| WIP_FIELD.exact(ChangeQueryBuilder.FIELD_WIP); |
| |
| /** Determines if this change has started review. */ |
| public static final IndexedField<ChangeData, String> STARTED_FIELD = |
| IndexedField.<ChangeData>stringBuilder("ReviewStarted") |
| .size(1) |
| .build(cd -> cd.change().hasReviewStarted() ? "1" : "0"); |
| |
| public static final IndexedField<ChangeData, String>.SearchSpec STARTED_SPEC = |
| STARTED_FIELD.exact(ChangeQueryBuilder.FIELD_STARTED); |
| |
| /** Users who have commented on this change. */ |
| public static final IndexedField<ChangeData, Iterable<Integer>> COMMENTBY_FIELD = |
| IndexedField.<ChangeData>iterableIntegerBuilder("CommentBy") |
| .build( |
| 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())); |
| |
| public static final IndexedField<ChangeData, Iterable<Integer>>.SearchSpec COMMENTBY_SPEC = |
| COMMENTBY_FIELD.integer(ChangeQueryBuilder.FIELD_COMMENTBY); |
| |
| /** Star labels on this change in the format: <account-id> */ |
| public static final IndexedField<ChangeData, Iterable<String>> STAR_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("Star") |
| .stored() |
| .build( |
| cd -> |
| Iterables.transform( |
| cd.stars(), accountId -> StarField.create(accountId).toString()), |
| (cd, field) -> |
| cd.setStars( |
| StreamSupport.stream(field.spliterator(), false) |
| .map(f -> StarField.parse(f).accountId()) |
| .collect(toImmutableList()))); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec STAR_SPEC = |
| STAR_FIELD.exact(ChangeQueryBuilder.FIELD_STAR); |
| |
| /** Users that have starred the change with any label. */ |
| public static final IndexedField<ChangeData, Iterable<Integer>> STARBY_FIELD = |
| IndexedField.<ChangeData>iterableIntegerBuilder("StarBy") |
| .build(cd -> Iterables.transform(cd.stars(), Account.Id::get)); |
| |
| public static final IndexedField<ChangeData, Iterable<Integer>>.SearchSpec STARBY_SPEC = |
| STARBY_FIELD.integer(ChangeQueryBuilder.FIELD_STARBY); |
| |
| /** Opaque group identifiers for this change's patch sets. */ |
| public static final IndexedField<ChangeData, Iterable<String>> GROUP_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("Group") |
| .build( |
| cd -> cd.patchSets().stream().flatMap(ps -> ps.groups().stream()).collect(toSet())); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec GROUP_SPEC = |
| GROUP_FIELD.exact(ChangeQueryBuilder.FIELD_GROUP); |
| |
| /** Serialized patch set object, used for pre-populating results. */ |
| private static final TypeToken<Iterable<Entities.PatchSet>> PATCH_SET_TYPE_TOKEN = |
| new TypeToken<>() { |
| private static final long serialVersionUID = 1L; |
| }; |
| |
| public static final IndexedField<ChangeData, Iterable<Entities.PatchSet>> PATCH_SET_FIELD = |
| IndexedField.<ChangeData, Iterable<Entities.PatchSet>>builder( |
| "PatchSet", PATCH_SET_TYPE_TOKEN) |
| .stored() |
| .required() |
| .protoConverter(Optional.of(PatchSetProtoConverter.INSTANCE)) |
| .build( |
| cd -> entitiesToProtos(PatchSetProtoConverter.INSTANCE, cd.patchSets()), |
| (cd, value) -> |
| cd.setPatchSets(decodeProtosToEntities(value, PatchSetProtoConverter.INSTANCE))); |
| |
| public static final IndexedField<ChangeData, Iterable<Entities.PatchSet>>.SearchSpec |
| PATCH_SET_SPEC = PATCH_SET_FIELD.storedOnly("_patch_set"); |
| |
| /** Users who have edits on this change. */ |
| public static final IndexedField<ChangeData, Iterable<Integer>> EDITBY_FIELD = |
| IndexedField.<ChangeData>iterableIntegerBuilder("EditBy") |
| .build(cd -> cd.editsByUser().stream().map(Account.Id::get).collect(toSet())); |
| |
| public static final IndexedField<ChangeData, Iterable<Integer>>.SearchSpec EDITBY_SPEC = |
| EDITBY_FIELD.integer(ChangeQueryBuilder.FIELD_EDITBY); |
| |
| /** Users who have draft comments on this change. */ |
| public static final IndexedField<ChangeData, Iterable<Integer>> DRAFTBY_FIELD = |
| IndexedField.<ChangeData>iterableIntegerBuilder("DraftBy") |
| .build(cd -> cd.draftsByUser().stream().map(Account.Id::get).collect(toSet())); |
| |
| public static final IndexedField<ChangeData, Iterable<Integer>>.SearchSpec DRAFTBY_SPEC = |
| DRAFTBY_FIELD.integer(ChangeQueryBuilder.FIELD_DRAFTBY); |
| |
| 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 IndexedField<ChangeData, Iterable<Integer>> REVIEWEDBY_FIELD = |
| IndexedField.<ChangeData>iterableIntegerBuilder("ReviewedBy") |
| .stored() |
| .build( |
| cd -> { |
| Set<Account.Id> reviewedBy = cd.reviewedBy(); |
| if (reviewedBy.isEmpty()) { |
| return ImmutableSet.of(NOT_REVIEWED); |
| } |
| return reviewedBy.stream().map(Account.Id::get).collect(toList()); |
| }, |
| (cd, field) -> |
| cd.setReviewedBy( |
| StreamSupport.stream(field.spliterator(), false) |
| .map(Account::id) |
| .collect(toImmutableSet()))); |
| |
| public static final IndexedField<ChangeData, Iterable<Integer>>.SearchSpec REVIEWEDBY_SPEC = |
| REVIEWEDBY_FIELD.integer(ChangeQueryBuilder.FIELD_REVIEWEDBY); |
| |
| public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT = |
| SubmitRuleOptions.builder().recomputeOnClosedChanges(true).build(); |
| |
| public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT = |
| SubmitRuleOptions.builder().build(); |
| |
| /** All submit rules results in the form of "$ruleName,$status". */ |
| public static final IndexedField<ChangeData, Iterable<String>> SUBMIT_RULE_RESULT_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("SubmitRuleResult") |
| .build( |
| cd -> { |
| List<String> result = new ArrayList<>(); |
| List<SubmitRecord> submitRecords = cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT); |
| for (SubmitRecord record : submitRecords) { |
| result.add(record.ruleName + "=" + record.status.name()); |
| } |
| return result; |
| }); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec |
| SUBMIT_RULE_RESULT_SPEC = SUBMIT_RULE_RESULT_FIELD.exact("submit_rule_result"); |
| |
| /** |
| * 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; |
| @Deprecated Map<String, String> data; |
| } |
| |
| String ruleName; |
| SubmitRecord.Status status; |
| List<StoredLabel> labels; |
| List<StoredRequirement> requirements; |
| String errorMessage; |
| |
| public StoredSubmitRecord(SubmitRecord rec) { |
| this.ruleName = rec.ruleName; |
| 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 (LegacySubmitRequirement requirement : rec.requirements) { |
| StoredRequirement sr = new StoredRequirement(); |
| sr.type = requirement.type(); |
| sr.fallbackText = requirement.fallbackText(); |
| // For backwards compatibility, write an empty map to the index. |
| // This is required, because the LegacySubmitRequirement AutoValue can't |
| // handle null in the old code. |
| // TODO(hiesel): Remove once we have rolled out the new code |
| // and waited long enough to not need to roll back. |
| sr.data = ImmutableMap.of(); |
| this.requirements.add(sr); |
| } |
| } |
| } |
| |
| public SubmitRecord toSubmitRecord() { |
| SubmitRecord rec = new SubmitRecord(); |
| rec.ruleName = ruleName; |
| 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 ? Account.id(label.appliedBy) : null; |
| rec.labels.add(srl); |
| } |
| } |
| if (requirements != null) { |
| rec.requirements = new ArrayList<>(requirements.size()); |
| for (StoredRequirement req : requirements) { |
| LegacySubmitRequirement sr = |
| LegacySubmitRequirement.builder() |
| .setType(req.type) |
| .setFallbackText(req.fallbackText) |
| .build(); |
| rec.requirements.add(sr); |
| } |
| } |
| return rec; |
| } |
| } |
| |
| public static final IndexedField<ChangeData, Iterable<String>> SUBMIT_RECORD_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("SubmitRecord") |
| .build(ChangeField::formatSubmitRecordValues); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec SUBMIT_RECORD_SPEC = |
| SUBMIT_RECORD_FIELD.exact("submit_record"); |
| |
| public static final IndexedField<ChangeData, Iterable<byte[]>> STORED_SUBMIT_RECORD_STRICT_FIELD = |
| IndexedField.<ChangeData>iterableByteArrayBuilder("FullSubmitRecordStrict") |
| .stored() |
| .build( |
| cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_STRICT), |
| (cd, field) -> |
| parseSubmitRecords( |
| StreamSupport.stream(field.spliterator(), false) |
| .map(f -> new String(f, UTF_8)) |
| .collect(toSet()), |
| SUBMIT_RULE_OPTIONS_STRICT, |
| cd)); |
| |
| public static final IndexedField<ChangeData, Iterable<byte[]>>.SearchSpec |
| STORED_SUBMIT_RECORD_STRICT_SPEC = |
| STORED_SUBMIT_RECORD_STRICT_FIELD.storedOnly("full_submit_record_strict"); |
| |
| public static final IndexedField<ChangeData, Iterable<byte[]>> |
| STORED_SUBMIT_RECORD_LENIENT_FIELD = |
| IndexedField.<ChangeData>iterableByteArrayBuilder("FullSubmitRecordLenient") |
| .stored() |
| .build( |
| cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_LENIENT), |
| (cd, field) -> |
| parseSubmitRecords( |
| StreamSupport.stream(field.spliterator(), false) |
| .map(f -> new String(f, UTF_8)) |
| .collect(toSet()), |
| SUBMIT_RULE_OPTIONS_LENIENT, |
| cd)); |
| |
| public static final IndexedField<ChangeData, Iterable<byte[]>>.SearchSpec |
| STORED_SUBMIT_RECORD_LENIENT_SPEC = |
| STORED_SUBMIT_RECORD_LENIENT_FIELD.storedOnly("full_submit_record_lenient"); |
| |
| public static void parseSubmitRecords( |
| Collection<String> values, SubmitRuleOptions opts, ChangeData out) { |
| List<SubmitRecord> records = parseSubmitRecords(values); |
| 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 List<byte[]> storedSubmitRecords(ChangeData cd, SubmitRuleOptions opts) { |
| return storedSubmitRecords(cd.submitRecords(opts)); |
| } |
| |
| public static List<String> formatSubmitRecordValues(ChangeData cd) { |
| Set<String> submitRecordValues = new HashSet<>(); |
| submitRecordValues.addAll( |
| formatSubmitRecordValues( |
| cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT), cd.change().getOwner())); |
| // Also backfill results of submit requirements such that users can query submit requirement |
| // results using the label operator, for example a query with "label:CR=NEED" will match with |
| // changes that have a submit-requirement with name="CR" and status=UNSATISFIED. |
| // Reason: We are preserving backward compatibility of the operators `label:$name=$status` |
| // which were previously working with submit records. Now admins can configure submit |
| // requirements and continue querying them with the label operator. |
| submitRecordValues.addAll(formatSubmitRequirementValues(cd.submitRequirements().values())); |
| return submitRecordValues.stream().collect(Collectors.toList()); |
| } |
| |
| @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(Locale.US); |
| 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; |
| } |
| |
| /** |
| * Generate submit requirement result formats that are compatible with the legacy submit record |
| * statuses. |
| */ |
| @VisibleForTesting |
| static List<String> formatSubmitRequirementValues(Collection<SubmitRequirementResult> srResults) { |
| List<String> result = new ArrayList<>(); |
| for (SubmitRequirementResult srResult : srResults) { |
| switch (srResult.status()) { |
| case SATISFIED: |
| case OVERRIDDEN: |
| case FORCED: |
| result.add( |
| SubmitRecord.Label.Status.OK.name() |
| + "," |
| + srResult.submitRequirement().name().toLowerCase(Locale.US)); |
| result.add( |
| SubmitRecord.Label.Status.MAY.name() |
| + "," |
| + srResult.submitRequirement().name().toLowerCase(Locale.US)); |
| break; |
| case UNSATISFIED: |
| result.add( |
| SubmitRecord.Label.Status.NEED.name() |
| + "," |
| + srResult.submitRequirement().name().toLowerCase(Locale.US)); |
| result.add( |
| SubmitRecord.Label.Status.REJECT.name() |
| + "," |
| + srResult.submitRequirement().name().toLowerCase(Locale.US)); |
| break; |
| case NOT_APPLICABLE: |
| case ERROR: |
| result.add( |
| SubmitRecord.Label.Status.IMPOSSIBLE.name() |
| + "," |
| + srResult.submitRequirement().name().toLowerCase(Locale.US)); |
| } |
| } |
| return result; |
| } |
| |
| /** Serialized submit requirements, used for pre-populating results. */ |
| private static final TypeToken<Iterable<Cache.SubmitRequirementResultProto>> |
| STORED_SUBMIT_REQUIREMENTS_TYPE_TOKEN = |
| new TypeToken<>() { |
| private static final long serialVersionUID = 1L; |
| }; |
| |
| public static final IndexedField<ChangeData, Iterable<Cache.SubmitRequirementResultProto>> |
| STORED_SUBMIT_REQUIREMENTS_FIELD = |
| IndexedField.<ChangeData, Iterable<Cache.SubmitRequirementResultProto>>builder( |
| "StoredSubmitRequirements", STORED_SUBMIT_REQUIREMENTS_TYPE_TOKEN) |
| .stored() |
| .required() |
| .protoConverter(Optional.of(SubmitRequirementProtoConverter.INSTANCE)) |
| .build( |
| cd -> |
| entitiesToProtos( |
| SubmitRequirementProtoConverter.INSTANCE, |
| cd.submitRequirements().values()), |
| (cd, value) -> parseSubmitRequirements(value, cd)); |
| |
| public static final IndexedField<ChangeData, Iterable<Cache.SubmitRequirementResultProto>> |
| .SearchSpec |
| STORED_SUBMIT_REQUIREMENTS_SPEC = |
| STORED_SUBMIT_REQUIREMENTS_FIELD.storedOnly("full_submit_requirements"); |
| |
| private static void parseSubmitRequirements( |
| Iterable<Cache.SubmitRequirementResultProto> values, ChangeData out) { |
| out.setSubmitRequirements( |
| decodeProtosToEntities(values, SubmitRequirementProtoConverter.INSTANCE).stream() |
| .filter(sr -> !sr.isLegacy()) |
| .collect( |
| ImmutableMap.toImmutableMap(sr -> sr.submitRequirement(), Function.identity()))); |
| } |
| |
| /** |
| * 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 IndexedField<ChangeData, Iterable<byte[]>> REF_STATE_FIELD = |
| IndexedField.<ChangeData>iterableByteArrayBuilder("RefState") |
| .stored() |
| .build( |
| cd -> { |
| List<byte[]> result = new ArrayList<>(); |
| cd.getRefStates() |
| .entries() |
| .forEach(e -> result.add(e.getValue().toByteArray(e.getKey()))); |
| return result; |
| }, |
| (cd, field) -> cd.setRefStates(RefState.parseStates(field))); |
| |
| public static final IndexedField<ChangeData, Iterable<byte[]>>.SearchSpec REF_STATE_SPEC = |
| REF_STATE_FIELD.storedOnly("ref_state"); |
| |
| /** |
| * 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 IndexedField<ChangeData, Iterable<byte[]>> REF_STATE_PATTERN_FIELD = |
| IndexedField.<ChangeData>iterableByteArrayBuilder("RefStatePattern") |
| .stored() |
| .build( |
| 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)); |
| return result; |
| }, |
| (cd, field) -> cd.setRefStatePatterns(field)); |
| |
| public static final IndexedField<ChangeData, Iterable<byte[]>>.SearchSpec REF_STATE_PATTERN_SPEC = |
| REF_STATE_PATTERN_FIELD.storedOnly("ref_state_pattern"); |
| |
| public static final IndexedField<ChangeData, Iterable<String>> CUSTOM_KEYED_VALUES_FIELD = |
| IndexedField.<ChangeData>iterableStringBuilder("CustomKeyedValues") |
| .stored() |
| .build( |
| cd -> |
| cd.customKeyedValues().entrySet().stream() |
| .map(e -> e.getKey() + "=" + e.getValue()) |
| .collect(toList()), |
| (cd, field) -> { |
| Map<String, String> ckv = new HashMap<>(); |
| for (String entry : field) { |
| int splitPoint = entry.indexOf('='); |
| if (splitPoint < 0) { |
| continue; |
| } |
| ckv.put(entry.substring(0, splitPoint), entry.substring(splitPoint + 1)); |
| } |
| cd.setCustomKeyedValues(ckv); |
| }); |
| |
| public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec |
| CUSTOM_KEYED_VALUES_SPEC = |
| CUSTOM_KEYED_VALUES_FIELD.prefix(ChangeQueryBuilder.FIELD_CUSTOM_KEYED_VALUES); |
| |
| @Nullable |
| private static String getTopic(ChangeData cd) { |
| Change c = cd.change(); |
| if (c == null) { |
| return null; |
| } |
| return firstNonNull(c.getTopic(), ""); |
| } |
| |
| private static <V extends MessageLite, T> V entityToProto( |
| ProtoConverter<V, T> converter, T object) { |
| return converter.toProto(object); |
| } |
| |
| private static <V extends MessageLite, T> ImmutableList<V> entitiesToProtos( |
| ProtoConverter<V, T> converter, Collection<T> objects) { |
| return objects.stream() |
| .map(object -> entityToProto(converter, object)) |
| .collect(toImmutableList()); |
| } |
| |
| private static <V extends MessageLite, T> ImmutableList<T> decodeProtosToEntities( |
| Iterable<V> raw, ProtoConverter<V, T> converter) { |
| return StreamSupport.stream(raw.spliterator(), false) |
| .map(proto -> decodeProtoToEntity(proto, converter)) |
| .collect(toImmutableList()); |
| } |
| |
| private static <V extends MessageLite, T> T decodeProtoToEntity( |
| V proto, ProtoConverter<V, T> converter) { |
| return converter.fromProto(proto); |
| } |
| |
| private static <T> SchemaFieldDefs.Getter<ChangeData, T> changeGetter(Function<Change, T> func) { |
| return in -> in.change() != null ? func.apply(in.change()) : null; |
| } |
| |
| private static String truncateStringValueToMaxTermLength(String str) { |
| return truncateStringValue(str, MAX_TERM_LENGTH); |
| } |
| |
| @VisibleForTesting |
| static String truncateStringValue(String str, int maxBytes) { |
| if (maxBytes < 0) { |
| throw new IllegalArgumentException("maxBytes < 0 not allowed"); |
| } |
| |
| if (maxBytes == 0) { |
| return ""; |
| } |
| |
| if (str.length() > maxBytes) { |
| if (Character.isHighSurrogate(str.charAt(maxBytes - 1))) { |
| str = str.substring(0, maxBytes - 1); |
| } else { |
| str = str.substring(0, maxBytes); |
| } |
| } |
| byte[] strBytes = str.getBytes(UTF_8); |
| if (strBytes.length > maxBytes) { |
| while (maxBytes > 0 && (strBytes[maxBytes] & 0xC0) == 0x80) { |
| maxBytes -= 1; |
| } |
| if (maxBytes > 0) { |
| if (strBytes.length >= maxBytes && (strBytes[maxBytes - 1] & 0xE0) == 0xC0) { |
| maxBytes -= 1; |
| } |
| if (strBytes.length >= maxBytes && (strBytes[maxBytes - 1] & 0xF0) == 0xE0) { |
| maxBytes -= 1; |
| } |
| if (strBytes.length >= maxBytes && (strBytes[maxBytes - 1] & 0xF8) == 0xF0) { |
| maxBytes -= 1; |
| } |
| } |
| return new String(Arrays.copyOfRange(strBytes, 0, maxBytes), UTF_8); |
| } |
| return str; |
| } |
| |
| @AutoValue |
| abstract static class StarField { |
| private static final String SEPARATOR = ":"; |
| |
| @Nullable |
| static StarField parse(String s) { |
| Integer id; |
| int p = s.indexOf(SEPARATOR); |
| if (p >= 0) { |
| id = Ints.tryParse(s.substring(0, p)); |
| } else { |
| // NOTE: This code branch should not be removed. This code is used internally by Google and |
| // must not be changed without approval from a Google contributor. In |
| // 992877d06d3492f78a3b189eb5579ddb86b9f0da we accidentally changed index writing to write |
| // <account_id> instead of <account_id>:star. As some servers have picked that up and wrote |
| // index entries with the short format, we should keep support its parsing. |
| id = Ints.tryParse(s); |
| } |
| if (id == null) { |
| return null; |
| } |
| return create(Account.id(id)); |
| } |
| |
| static StarField create(Account.Id accountId) { |
| return new AutoValue_ChangeField_StarField(accountId); |
| } |
| |
| public abstract Account.Id accountId(); |
| |
| @Override |
| public final String toString() { |
| // NOTE: The ":star" addition is used internally by Google and must not be removed without |
| // approval from a Google contributor. This method is used for writing change index data. |
| // Historically, we supported different kinds of labels, which were stored in this |
| // format, with "star" being the only label in use. This label addition stayed in order to |
| // keep the index format consistent while removing the star-label support. |
| return accountId() + SEPARATOR + "star"; |
| } |
| } |
| } |