All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.index.change.ChangeField Maven / Gradle / Ivy

The newest version!
// 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.
 *
 * 

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. * *

Field names are all lowercase alphanumeric plus underscore; index implementations may create * unambiguous derived field names containing other characters. * *

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 NUMERIC_ID_STR_FIELD = IndexedField.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.SearchSpec NUMERIC_ID_STR_SPEC = NUMERIC_ID_STR_FIELD.exact("legacy_id_str"); public static final IndexedField CHANGENUM_FIELD = IndexedField.integerBuilder("ChangeNumber") .stored() .required() .build(cd -> cd.getId().get()); public static final IndexedField.SearchSpec CHANGENUM_SPEC = CHANGENUM_FIELD.integer(FIELD_CHANGE_NUMBER); /** Newer style Change-Id key. */ public static final IndexedField CHANGE_ID_FIELD = IndexedField.stringBuilder("ChangeId") .required() // The new style key is in form Isha1 .size(41) .build(changeGetter(c -> c.getKey().get())); public static final IndexedField.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 STATUS_FIELD = IndexedField.stringBuilder("Status") .required() .size(20) .build(changeGetter(c -> ChangeStatusPredicate.canonicalize(c.getStatus()))); public static final IndexedField.SearchSpec STATUS_SPEC = STATUS_FIELD.exact(ChangeQueryBuilder.FIELD_STATUS); /** Project containing the change. */ public static final IndexedField PROJECT_FIELD = IndexedField.stringBuilder("Project") .required() .stored() .size(200) .build(changeGetter(c -> c.getProject().get())); public static final IndexedField.SearchSpec PROJECT_SPEC = PROJECT_FIELD.exact(ChangeQueryBuilder.FIELD_PROJECT); /** Project containing the change, as a prefix field. */ public static final IndexedField.SearchSpec PROJECTS_SPEC = PROJECT_FIELD.prefix(ChangeQueryBuilder.FIELD_PROJECTS); /** Reference (aka branch) the change will submit onto. */ public static final IndexedField REF_FIELD = IndexedField.stringBuilder("Ref") .required() .size(300) .build(changeGetter(c -> c.getDest().branch())); public static final IndexedField.SearchSpec REF_SPEC = REF_FIELD.exact(ChangeQueryBuilder.FIELD_REF); /** Topic, a short annotation on the branch. */ public static final IndexedField TOPIC_FIELD = IndexedField.stringBuilder("Topic").size(500).build(ChangeField::getTopic); public static final IndexedField.SearchSpec EXACT_TOPIC = TOPIC_FIELD.exact("topic4"); /** Topic, a short annotation on the branch. */ public static final IndexedField.SearchSpec FUZZY_TOPIC = TOPIC_FIELD.fullText("topic5"); /** Topic, a short annotation on the branch. */ public static final IndexedField.SearchSpec PREFIX_TOPIC = TOPIC_FIELD.prefix("topic6"); /** {@link com.google.gerrit.entities.SubmissionId} assigned by MergeOp. */ public static final IndexedField SUBMISSIONID_FIELD = IndexedField.stringBuilder("SubmissionId") .size(500) .build(changeGetter(Change::getSubmissionId)); public static final IndexedField.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 UPDATED_FIELD = IndexedField.timestampBuilder("LastUpdated") .stored() .build(changeGetter(change -> Timestamp.from(change.getLastUpdatedOn()))); public static final IndexedField.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 MERGED_ON_FIELD = IndexedField.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.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> PATH_FIELD = IndexedField.iterableStringBuilder("ModifiedFile") .build(cd -> firstNonNull(cd.currentFilePaths(), ImmutableList.of())); public static final IndexedField>.SearchSpec PATH_SPEC = PATH_FIELD // Named for backwards compatibility. .exact(ChangeQueryBuilder.FIELD_FILE); public static Set getFileParts(ChangeData cd) { List paths = cd.currentFilePaths(); Splitter s = Splitter.on('/').omitEmptyStrings(); Set 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> HASHTAG_FIELD = IndexedField.iterableStringBuilder("Hashtag") .size(200) .build(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet())); public static final IndexedField>.SearchSpec HASHTAG_SPEC = HASHTAG_FIELD.exact(ChangeQueryBuilder.FIELD_HASHTAG); /** Hashtags as fulltext field for in-string search. */ public static final IndexedField>.SearchSpec FUZZY_HASHTAG = HASHTAG_FIELD.fullText("hashtag2"); /** Hashtags as prefix field for in-string search. */ public static final IndexedField>.SearchSpec PREFIX_HASHTAG = HASHTAG_FIELD.prefix("hashtag3"); /** Hashtags with original case. */ public static final IndexedField> HASHTAG_CASE_AWARE_FIELD = IndexedField.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>.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> FILE_PART_FIELD = IndexedField.iterableStringBuilder("FilePart").build(ChangeField::getFileParts); public static final IndexedField>.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> EXTENSION_FIELD = IndexedField.iterableStringBuilder("Extension") .size(100) .build(ChangeField::getExtensions); public static final IndexedField>.SearchSpec EXTENSION_SPEC = EXTENSION_FIELD.exact(ChangeQueryBuilder.FIELD_EXTENSION); public static Set 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 ONLY_EXTENSIONS_FIELD = IndexedField.stringBuilder("OnlyExtensions") .build(ChangeField::getAllExtensionsAsList); public static final IndexedField.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. * *

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 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> FOOTER_FIELD = IndexedField.iterableStringBuilder("Footer").build(ChangeField::getFooters); public static final IndexedField>.SearchSpec FOOTER_SPEC = FOOTER_FIELD.exact(ChangeQueryBuilder.FIELD_FOOTER); public static Set 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> FOOTER_NAME_FIELD = IndexedField.iterableStringBuilder("FooterName") .build(ChangeField::getFootersNames); public static final IndexedField>.SearchSpec FOOTER_NAME = FOOTER_NAME_FIELD.exact(ChangeQueryBuilder.FIELD_FOOTER_NAME); public static Set 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> DIRECTORY_FIELD = IndexedField.iterableStringBuilder("DirField").build(ChangeField::getDirectories); public static final IndexedField>.SearchSpec DIRECTORY_SPEC = DIRECTORY_FIELD.exact(ChangeQueryBuilder.FIELD_DIRECTORY); public static Set getDirectories(ChangeData cd) { List paths = cd.currentFilePaths(); Splitter s = Splitter.on('/').omitEmptyStrings(); Set 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 OWNER_FIELD = IndexedField.integerBuilder("Owner") .required() .build(changeGetter(c -> c.getOwner().get())); public static final IndexedField.SearchSpec OWNER_SPEC = OWNER_FIELD.integer(ChangeQueryBuilder.FIELD_OWNER); /** Uploader of the latest patch set. */ public static final IndexedField UPLOADER_FIELD = IndexedField.integerBuilder("Uploader") .required() .build(cd -> cd.currentPatchSet().uploader().get()); public static final IndexedField.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 CHERRY_PICK_OF_CHANGE_FIELD = IndexedField.integerBuilder("CherryPickOfChange") .build( cd -> cd.change().getCherryPickOf() != null ? cd.change().getCherryPickOf().changeId().get() : null); public static final IndexedField.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 CHERRY_PICK_OF_PATCHSET_FIELD = IndexedField.integerBuilder("CherryPickOfPatchset") .build( cd -> cd.change().getCherryPickOf() != null ? cd.change().getCherryPickOf().get() : null); public static final IndexedField.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> ATTENTION_SET_USERS_FIELD = IndexedField.iterableIntegerBuilder("AttentionSetUsers") .build(ChangeField::getAttentionSetUserIds); public static final IndexedField>.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 ATTENTION_SET_USERS_COUNT_FIELD = IndexedField.integerBuilder("AttentionSetUsersCount") .stored() .build(cd -> additionsOnly(cd.attentionSet()).size()); public static final IndexedField.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> ATTENTION_SET_FULL_FIELD = IndexedField.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>.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 ASSIGNEE_FIELD = IndexedField.integerBuilder("Assignee").build(changeGetter(c -> NO_ASSIGNEE)); @Deprecated public static final IndexedField.SearchSpec ASSIGNEE_SPEC = ASSIGNEE_FIELD.integer(ChangeQueryBuilder.FIELD_ASSIGNEE); /** Reviewer(s) associated with the change. */ public static final IndexedField> REVIEWER_FIELD = IndexedField.iterableStringBuilder("Reviewer") .stored() .build( cd -> getReviewerFieldValues(cd.reviewers()), (cd, field) -> cd.setReviewers(parseReviewerFieldValues(cd.getId(), field))); public static final IndexedField>.SearchSpec REVIEWER_SPEC = REVIEWER_FIELD.exact("reviewer2"); /** Reviewer(s) associated with the change that do not have a gerrit account. */ public static final IndexedField> REVIEWER_BY_EMAIL_FIELD = IndexedField.iterableStringBuilder("ReviewerByEmail") .stored() .build( cd -> getReviewerByEmailFieldValues(cd.reviewersByEmail()), (cd, field) -> cd.setReviewersByEmail(parseReviewerByEmailFieldValues(cd.getId(), field))); public static final IndexedField>.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> PENDING_REVIEWER_FIELD = IndexedField.iterableStringBuilder("PendingReviewer") .stored() .build( cd -> getReviewerFieldValues(cd.pendingReviewers()), (cd, field) -> cd.setPendingReviewers(parseReviewerFieldValues(cd.getId(), field))); public static final IndexedField>.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> PENDING_REVIEWER_BY_EMAIL_FIELD = IndexedField.iterableStringBuilder("PendingReviewerByEmail") .stored() .build( cd -> getReviewerByEmailFieldValues(cd.pendingReviewersByEmail()), (cd, field) -> cd.setPendingReviewersByEmail( parseReviewerByEmailFieldValues(cd.getId(), field))); public static final IndexedField>.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 REVERT_OF_FIELD = IndexedField.integerBuilder("RevertOf") .build(cd -> cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null); public static final IndexedField.SearchSpec REVERT_OF = REVERT_OF_FIELD.integer(ChangeQueryBuilder.FIELD_REVERTOF); public static final IndexedField IS_PURE_REVERT_FIELD = IndexedField.stringBuilder("IsPureRevert") .size(1) .build(cd -> Boolean.TRUE.equals(cd.isPureRevert()) ? "1" : "0"); public static final IndexedField.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 IS_SUBMITTABLE_FIELD = IndexedField.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.SearchSpec IS_SUBMITTABLE_SPEC = IS_SUBMITTABLE_FIELD.exact(ChangeQueryBuilder.FIELD_IS_SUBMITTABLE); @VisibleForTesting static List getReviewerFieldValues(ReviewerSet reviewers) { List r = new ArrayList<>(reviewers.asTable().size() * 2); for (Table.Cell 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 getReviewerByEmailFieldValues(ReviewerByEmailSet reviewersByEmail) { List r = new ArrayList<>(reviewersByEmail.asTable().size() * 2); for (Table.Cell 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 values) { ImmutableTable.Builder 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 // "," and one value with the format // ",," (see #getReviewerFieldValues(ReviewerSet)). // For parsing we are only interested in the ",," // value and the "," value is ignored here. continue; } Optional 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 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 values) { ImmutableTable.Builder 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 // "," and one value with the format // ",," (see // #getReviewerByEmailFieldValues(ReviewerByEmailSet)). // For parsing we are only interested in the ",," value // and the "," value is ignored here. continue; } Optional 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 getReviewerState(String value) { try { return Optional.of(ReviewerStateInternal.valueOf(value)); } catch (IllegalArgumentException | NullPointerException e) { return Optional.empty(); } } private static ImmutableSet getAttentionSetUserIds(ChangeData changeData) { return additionsOnly(changeData.attentionSet()).stream() .map(update -> update.account().get()) .collect(toImmutableSet()); } private static ImmutableSet 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 storedAttentionSetEntriesJson, ChangeData changeData) { ImmutableSet 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> COMMIT_FIELD = IndexedField.iterableStringBuilder("CommitId") .size(40) .required() .build(ChangeField::getRevisions); public static final IndexedField>.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>.SearchSpec EXACT_COMMIT_SPEC = COMMIT_FIELD.exact(ChangeQueryBuilder.FIELD_EXACTCOMMIT); private static ImmutableSet getRevisions(ChangeData cd) { return cd.patchSets().stream().map(ps -> ps.commitId().name()).collect(toImmutableSet()); } /** Tracking id extracted from a footer. */ public static final IndexedField> TR_FIELD = IndexedField.iterableStringBuilder("TrackingFooter") .build(cd -> ImmutableSet.copyOf(cd.trackingFooters().values())); public static final IndexedField>.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> LABEL_FIELD = IndexedField.iterableStringBuilder("Label").required().build(cd -> getLabels(cd)); public static final IndexedField>.SearchSpec LABEL_SPEC = LABEL_FIELD.exact("label2"); private static Set getLabels(ChangeData cd) { Set allApprovals = new HashSet<>(); Set distinctApprovals = new HashSet<>(); Table voteCounts = HashBasedTable.create(); for (PatchSetApproval a : cd.currentApprovals()) { if (a.value() != 0 && !a.isLegacySubmit()) { increment(voteCounts, a.label(), a.value()); Optional 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 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 getCountLabelFormats( Table voteCounts, ChangeData cd) { List allFormats = new ArrayList<>(); for (String label : voteCounts.rowMap().keySet()) { Optional labelType = cd.getLabelTypes().byLabel(label); Map 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 getCountLabelFormats( Optional labelType, String label, short vote, int count) { List 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 getMagicLabelFormats( String label, short labelVal, Optional 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 getMagicLabelFormats( String label, short labelVal, Optional labelType, @Nullable Account.Id accountId, @Nullable Integer count) { List 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 getLabelOwnerFormats( PatchSetApproval a, ChangeData cd, Optional labelType) { List 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 getLabelNonUploaderFormats( PatchSetApproval a, ChangeData cd, Optional labelType) { List 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 getAuthorParts(ChangeData cd) { return SchemaUtil.getPersonParts(cd.getAuthor()); } public static ImmutableSet getAuthorNameAndEmail(ChangeData cd) { return getNameAndEmail(cd.getAuthor()); } public static Set getCommitterParts(ChangeData cd) { return SchemaUtil.getPersonParts(cd.getCommitter()); } public static ImmutableSet getCommitterNameAndEmail(ChangeData cd) { return getNameAndEmail(cd.getCommitter()); } private static ImmutableSet 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> AUTHOR_PARTS_FIELD = IndexedField.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>.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> EXACT_AUTHOR_FIELD = IndexedField.iterableStringBuilder("ExactAuthor") .required() .description("The exact name, email address and NameEmail of the author.") .build(ChangeField::getAuthorNameAndEmail); public static final IndexedField>.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> COMMITTER_PARTS_FIELD = IndexedField.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>.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> EXACT_COMMITTER_FIELD = IndexedField.iterableStringBuilder("ExactCommiter") .required() .description("The exact name, email address, and NameEmail of the committer.") .build(ChangeField::getCommitterNameAndEmail); public static final IndexedField>.SearchSpec EXACT_COMMITTER_SPEC = EXACT_COMMITTER_FIELD.exact(ChangeQueryBuilder.FIELD_EXACTCOMMITTER); /** Serialized change object, used for pre-populating results. */ private static final TypeToken CHANGE_TYPE_TOKEN = new TypeToken<>() { private static final long serialVersionUID = 1L; }; public static final IndexedField CHANGE_FIELD = IndexedField.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.SearchSpec CHANGE_SPEC = CHANGE_FIELD.storedOnly("_change"); /** Serialized approvals for the current patch set, used for pre-populating results. */ private static final TypeToken> APPROVAL_TYPE_TOKEN = new TypeToken<>() { private static final long serialVersionUID = 1L; }; public static final IndexedField> APPROVAL_FIELD = IndexedField.>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>.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 COMMIT_MESSAGE_FIELD = IndexedField.stringBuilder("CommitMessage") .required() .build(ChangeData::commitMessage); public static final IndexedField.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 COMMIT_MESSAGE_EXACT_FIELD = IndexedField.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.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 SUBJECT_FIELD = IndexedField.stringBuilder("Subject") .required() .build(changeGetter(Change::getSubject)); public static final IndexedField.SearchSpec SUBJECT_SPEC = SUBJECT_FIELD.fullText(ChangeQueryBuilder.FIELD_SUBJECT); public static final IndexedField.SearchSpec PREFIX_SUBJECT_SPEC = SUBJECT_FIELD.prefix(ChangeQueryBuilder.FIELD_PREFIX_SUBJECT); /** Summary or inline comment. */ public static final IndexedField> COMMENT_FIELD = IndexedField.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>.SearchSpec COMMENT_SPEC = COMMENT_FIELD.fullText(ChangeQueryBuilder.FIELD_COMMENT); /** Number of unresolved comment threads of the change, including robot comments. */ public static final IndexedField UNRESOLVED_COMMENT_COUNT_FIELD = IndexedField.integerBuilder("UnresolvedCommentCount") .stored() .build( ChangeData::unresolvedCommentCount, (cd, field) -> cd.setUnresolvedCommentCount(field)); public static final IndexedField.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 TOTAL_COMMENT_COUNT_FIELD = IndexedField.integerBuilder("TotalCommentCount") .stored() .build(ChangeData::totalCommentCount, (cd, field) -> cd.setTotalCommentCount(field)); public static final IndexedField.SearchSpec TOTAL_COMMENT_COUNT_SPEC = TOTAL_COMMENT_COUNT_FIELD.integerRange("total_comments"); /** Whether the change is mergeable. */ public static final IndexedField MERGEABLE_FIELD = IndexedField.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.SearchSpec MERGEABLE_SPEC = MERGEABLE_FIELD.exact(ChangeQueryBuilder.FIELD_MERGEABLE); /** Whether the change is a merge commit. */ public static final IndexedField MERGE_FIELD = IndexedField.stringBuilder("Merge") .stored() .size(1) .build( cd -> { Boolean m = cd.isMerge(); if (m == null) { return null; } return m ? "1" : "0"; }); public static final IndexedField.SearchSpec MERGE_SPEC = MERGE_FIELD.exact(ChangeQueryBuilder.FIELD_MERGE); /** Whether the change is a cherry pick of another change. */ public static final IndexedField CHERRY_PICK_FIELD = IndexedField.stringBuilder("CherryPick") .stored() .size(1) .build(cd -> cd.change().getCherryPickOf() != null ? "1" : "0"); public static final IndexedField.SearchSpec CHERRY_PICK_SPEC = CHERRY_PICK_FIELD.exact(ChangeQueryBuilder.FIELD_CHERRYPICK); /** The number of inserted lines in this change. */ public static final IndexedField ADDED_LINES_FIELD = IndexedField.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.SearchSpec ADDED_LINES_SPEC = ADDED_LINES_FIELD.integerRange(ChangeQueryBuilder.FIELD_ADDED); /** The number of deleted lines in this change. */ public static final IndexedField DELETED_LINES_FIELD = IndexedField.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.SearchSpec DELETED_LINES_SPEC = DELETED_LINES_FIELD.integerRange(ChangeQueryBuilder.FIELD_DELETED); /** The total number of modified lines in this change. */ public static final IndexedField DELTA_LINES_FIELD = IndexedField.integerBuilder("DeltaLines") .stored() .build(cd -> cd.changedLines().map(c -> c.insertions + c.deletions).orElse(null)); public static final IndexedField.SearchSpec DELTA_LINES_SPEC = DELTA_LINES_FIELD.integerRange(ChangeQueryBuilder.FIELD_DELTA); /** Determines if this change is private. */ public static final IndexedField PRIVATE_FIELD = IndexedField.stringBuilder("IsPrivate") .size(1) .build(cd -> cd.change().isPrivate() ? "1" : "0"); public static final IndexedField.SearchSpec PRIVATE_SPEC = PRIVATE_FIELD.exact(ChangeQueryBuilder.FIELD_PRIVATE); /** Determines if this change is work in progress. */ public static final IndexedField WIP_FIELD = IndexedField.stringBuilder("WIP") .size(1) .build(cd -> cd.change().isWorkInProgress() ? "1" : "0"); public static final IndexedField.SearchSpec WIP_SPEC = WIP_FIELD.exact(ChangeQueryBuilder.FIELD_WIP); /** Determines if this change has started review. */ public static final IndexedField STARTED_FIELD = IndexedField.stringBuilder("ReviewStarted") .size(1) .build(cd -> cd.change().hasReviewStarted() ? "1" : "0"); public static final IndexedField.SearchSpec STARTED_SPEC = STARTED_FIELD.exact(ChangeQueryBuilder.FIELD_STARTED); /** Users who have commented on this change. */ public static final IndexedField> COMMENTBY_FIELD = IndexedField.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>.SearchSpec COMMENTBY_SPEC = COMMENTBY_FIELD.integer(ChangeQueryBuilder.FIELD_COMMENTBY); /** Star labels on this change in the format: <account-id> */ public static final IndexedField> STAR_FIELD = IndexedField.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>.SearchSpec STAR_SPEC = STAR_FIELD.exact(ChangeQueryBuilder.FIELD_STAR); /** Users that have starred the change with any label. */ public static final IndexedField> STARBY_FIELD = IndexedField.iterableIntegerBuilder("StarBy") .build(cd -> Iterables.transform(cd.stars(), Account.Id::get)); public static final IndexedField>.SearchSpec STARBY_SPEC = STARBY_FIELD.integer(ChangeQueryBuilder.FIELD_STARBY); /** Opaque group identifiers for this change's patch sets. */ public static final IndexedField> GROUP_FIELD = IndexedField.iterableStringBuilder("Group") .build( cd -> cd.patchSets().stream().flatMap(ps -> ps.groups().stream()).collect(toSet())); public static final IndexedField>.SearchSpec GROUP_SPEC = GROUP_FIELD.exact(ChangeQueryBuilder.FIELD_GROUP); /** Serialized patch set object, used for pre-populating results. */ private static final TypeToken> PATCH_SET_TYPE_TOKEN = new TypeToken<>() { private static final long serialVersionUID = 1L; }; public static final IndexedField> PATCH_SET_FIELD = IndexedField.>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>.SearchSpec PATCH_SET_SPEC = PATCH_SET_FIELD.storedOnly("_patch_set"); /** Users who have edits on this change. */ public static final IndexedField> EDITBY_FIELD = IndexedField.iterableIntegerBuilder("EditBy") .build(cd -> cd.editsByUser().stream().map(Account.Id::get).collect(toSet())); public static final IndexedField>.SearchSpec EDITBY_SPEC = EDITBY_FIELD.integer(ChangeQueryBuilder.FIELD_EDITBY); /** Users who have draft comments on this change. */ public static final IndexedField> DRAFTBY_FIELD = IndexedField.iterableIntegerBuilder("DraftBy") .build(cd -> cd.draftsByUser().stream().map(Account.Id::get).collect(toSet())); public static final IndexedField>.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. * *

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. * *

If the latest update is by the change owner, then the special value {@link #NOT_REVIEWED} is * emitted. */ public static final IndexedField> REVIEWEDBY_FIELD = IndexedField.iterableIntegerBuilder("ReviewedBy") .stored() .build( cd -> { Set 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>.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> SUBMIT_RULE_RESULT_FIELD = IndexedField.iterableStringBuilder("SubmitRuleResult") .build( cd -> { List result = new ArrayList<>(); List submitRecords = cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT); for (SubmitRecord record : submitRecords) { result.add(record.ruleName + "=" + record.status.name()); } return result; }); public static final IndexedField>.SearchSpec SUBMIT_RULE_RESULT_SPEC = SUBMIT_RULE_RESULT_FIELD.exact("submit_rule_result"); /** * JSON type for storing SubmitRecords. * *

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 data; } String ruleName; SubmitRecord.Status status; List labels; List 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> SUBMIT_RECORD_FIELD = IndexedField.iterableStringBuilder("SubmitRecord") .build(ChangeField::formatSubmitRecordValues); public static final IndexedField>.SearchSpec SUBMIT_RECORD_SPEC = SUBMIT_RECORD_FIELD.exact("submit_record"); public static final IndexedField> STORED_SUBMIT_RECORD_STRICT_FIELD = IndexedField.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>.SearchSpec STORED_SUBMIT_RECORD_STRICT_SPEC = STORED_SUBMIT_RECORD_STRICT_FIELD.storedOnly("full_submit_record_strict"); public static final IndexedField> STORED_SUBMIT_RECORD_LENIENT_FIELD = IndexedField.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>.SearchSpec STORED_SUBMIT_RECORD_LENIENT_SPEC = STORED_SUBMIT_RECORD_LENIENT_FIELD.storedOnly("full_submit_record_lenient"); public static void parseSubmitRecords( Collection values, SubmitRuleOptions opts, ChangeData out) { List records = parseSubmitRecords(values); out.setSubmitRecords(opts, records); } @VisibleForTesting static List parseSubmitRecords(Collection values) { return values.stream() .map(v -> GSON.fromJson(v, StoredSubmitRecord.class).toSubmitRecord()) .collect(toList()); } @VisibleForTesting static List storedSubmitRecords(List records) { return Lists.transform(records, r -> GSON.toJson(new StoredSubmitRecord(r)).getBytes(UTF_8)); } private static List storedSubmitRecords(ChangeData cd, SubmitRuleOptions opts) { return storedSubmitRecords(cd.submitRecords(opts)); } public static List formatSubmitRecordValues(ChangeData cd) { Set 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 formatSubmitRecordValues(List records, Account.Id changeOwner) { List 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 formatSubmitRequirementValues(Collection srResults) { List 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> STORED_SUBMIT_REQUIREMENTS_TYPE_TOKEN = new TypeToken<>() { private static final long serialVersionUID = 1L; }; public static final IndexedField> STORED_SUBMIT_REQUIREMENTS_FIELD = IndexedField.>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> .SearchSpec STORED_SUBMIT_REQUIREMENTS_SPEC = STORED_SUBMIT_REQUIREMENTS_FIELD.storedOnly("full_submit_requirements"); private static void parseSubmitRequirements( Iterable 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. * *

Emitted as UTF-8 encoded strings of the form {@code project:ref/name:[hex sha]}. */ public static final IndexedField> REF_STATE_FIELD = IndexedField.iterableByteArrayBuilder("RefState") .stored() .build( cd -> { List 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>.SearchSpec REF_STATE_SPEC = REF_STATE_FIELD.storedOnly("ref_state"); /** * All ref wildcard patterns that were used in the course of indexing this document. * *

Emitted as UTF-8 encoded strings of the form {@code project:ref/name/*}. See {@link * RefStatePattern} for the pattern format. */ public static final IndexedField> REF_STATE_PATTERN_FIELD = IndexedField.iterableByteArrayBuilder("RefStatePattern") .stored() .build( cd -> { Change.Id id = cd.getId(); Project.NameKey project = cd.change().getProject(); List 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>.SearchSpec REF_STATE_PATTERN_SPEC = REF_STATE_PATTERN_FIELD.storedOnly("ref_state_pattern"); public static final IndexedField> CUSTOM_KEYED_VALUES_FIELD = IndexedField.iterableStringBuilder("CustomKeyedValues") .stored() .build( cd -> cd.customKeyedValues().entrySet().stream() .map(e -> e.getKey() + "=" + e.getValue()) .collect(toList()), (cd, field) -> { Map 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>.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 entityToProto( ProtoConverter converter, T object) { return converter.toProto(object); } private static ImmutableList entitiesToProtos( ProtoConverter converter, Collection objects) { return objects.stream() .map(object -> entityToProto(converter, object)) .collect(toImmutableList()); } private static ImmutableList decodeProtosToEntities( Iterable raw, ProtoConverter converter) { return StreamSupport.stream(raw.spliterator(), false) .map(proto -> decodeProtoToEntity(proto, converter)) .collect(toImmutableList()); } private static T decodeProtoToEntity( V proto, ProtoConverter converter) { return converter.fromProto(proto); } private static SchemaFieldDefs.Getter changeGetter(Function 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 // instead of :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"; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy