LCOV - code coverage report
Current view: top level - server/index/change - ChangeField.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 779 814 95.7 %
Date: 2022-11-19 15:00:39 Functions: 160 165 97.0 %

          Line data    Source code
       1             : // Copyright (C) 2013 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.index.change;
      16             : 
      17             : import static com.google.common.base.MoreObjects.firstNonNull;
      18             : import static com.google.common.collect.ImmutableList.toImmutableList;
      19             : import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
      20             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      21             : import static com.google.gerrit.index.FieldDef.exact;
      22             : import static com.google.gerrit.index.FieldDef.fullText;
      23             : import static com.google.gerrit.index.FieldDef.intRange;
      24             : import static com.google.gerrit.index.FieldDef.integer;
      25             : import static com.google.gerrit.index.FieldDef.prefix;
      26             : import static com.google.gerrit.index.FieldDef.storedOnly;
      27             : import static com.google.gerrit.index.FieldDef.timestamp;
      28             : import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
      29             : import static java.nio.charset.StandardCharsets.UTF_8;
      30             : import static java.util.stream.Collectors.joining;
      31             : import static java.util.stream.Collectors.toList;
      32             : import static java.util.stream.Collectors.toSet;
      33             : 
      34             : import com.google.common.annotations.VisibleForTesting;
      35             : import com.google.common.base.Splitter;
      36             : import com.google.common.collect.HashBasedTable;
      37             : import com.google.common.collect.ImmutableList;
      38             : import com.google.common.collect.ImmutableMap;
      39             : import com.google.common.collect.ImmutableSet;
      40             : import com.google.common.collect.ImmutableTable;
      41             : import com.google.common.collect.Iterables;
      42             : import com.google.common.collect.Lists;
      43             : import com.google.common.collect.Table;
      44             : import com.google.common.flogger.FluentLogger;
      45             : import com.google.common.io.Files;
      46             : import com.google.common.primitives.Longs;
      47             : import com.google.gerrit.common.Nullable;
      48             : import com.google.gerrit.entities.Account;
      49             : import com.google.gerrit.entities.Address;
      50             : import com.google.gerrit.entities.AttentionSetUpdate;
      51             : import com.google.gerrit.entities.Change;
      52             : import com.google.gerrit.entities.ChangeMessage;
      53             : import com.google.gerrit.entities.LabelType;
      54             : import com.google.gerrit.entities.LegacySubmitRequirement;
      55             : import com.google.gerrit.entities.PatchSetApproval;
      56             : import com.google.gerrit.entities.Project;
      57             : import com.google.gerrit.entities.RefNames;
      58             : import com.google.gerrit.entities.SubmitRecord;
      59             : import com.google.gerrit.entities.SubmitRequirementResult;
      60             : import com.google.gerrit.entities.converter.ChangeProtoConverter;
      61             : import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
      62             : import com.google.gerrit.entities.converter.PatchSetProtoConverter;
      63             : import com.google.gerrit.entities.converter.ProtoConverter;
      64             : import com.google.gerrit.index.FieldDef;
      65             : import com.google.gerrit.index.IndexedField;
      66             : import com.google.gerrit.index.RefState;
      67             : import com.google.gerrit.index.SchemaFieldDefs;
      68             : import com.google.gerrit.index.SchemaUtil;
      69             : import com.google.gerrit.json.OutputFormat;
      70             : import com.google.gerrit.proto.Protos;
      71             : import com.google.gerrit.server.ReviewerByEmailSet;
      72             : import com.google.gerrit.server.ReviewerSet;
      73             : import com.google.gerrit.server.StarredChangesUtil;
      74             : import com.google.gerrit.server.config.AllUsersName;
      75             : import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
      76             : import com.google.gerrit.server.notedb.ReviewerStateInternal;
      77             : import com.google.gerrit.server.notedb.SubmitRequirementProtoConverter;
      78             : import com.google.gerrit.server.project.SubmitRuleOptions;
      79             : import com.google.gerrit.server.query.change.ChangeData;
      80             : import com.google.gerrit.server.query.change.ChangeQueryBuilder;
      81             : import com.google.gerrit.server.query.change.ChangeStatusPredicate;
      82             : import com.google.gerrit.server.query.change.MagicLabelValue;
      83             : import com.google.gson.Gson;
      84             : import com.google.protobuf.MessageLite;
      85             : import java.sql.Timestamp;
      86             : import java.time.Instant;
      87             : import java.util.ArrayList;
      88             : import java.util.Arrays;
      89             : import java.util.Collection;
      90             : import java.util.HashSet;
      91             : import java.util.List;
      92             : import java.util.Locale;
      93             : import java.util.Map;
      94             : import java.util.Objects;
      95             : import java.util.Optional;
      96             : import java.util.Set;
      97             : import java.util.function.Function;
      98             : import java.util.stream.Collectors;
      99             : import java.util.stream.Stream;
     100             : import java.util.stream.StreamSupport;
     101             : import org.eclipse.jgit.lib.PersonIdent;
     102             : 
     103             : /**
     104             :  * Fields indexed on change documents.
     105             :  *
     106             :  * <p>Each field corresponds to both a field name supported by {@link ChangeQueryBuilder} for
     107             :  * querying that field, and a method on {@link ChangeData} used for populating the corresponding
     108             :  * document fields in the secondary index.
     109             :  *
     110             :  * <p>Field names are all lowercase alphanumeric plus underscore; index implementations may create
     111             :  * unambiguous derived field names containing other characters.
     112             :  *
     113             :  * <p>Note that this class does not override {@link Object#equals(Object)}. It relies on instances
     114             :  * being singletons so that the default (i.e. reference) comparison works.
     115             :  */
     116           0 : public class ChangeField {
     117         155 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     118             : 
     119             :   public static final int NO_ASSIGNEE = -1;
     120             : 
     121         155 :   private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
     122             : 
     123             :   /**
     124             :    * To avoid the non-google dependency on org.apache.lucene.index.IndexWriter.MAX_TERM_LENGTH it is
     125             :    * redefined here.
     126             :    */
     127             :   public static final int MAX_TERM_LENGTH = (1 << 15) - 2;
     128             : 
     129             :   // TODO: Rename LEGACY_ID to NUMERIC_ID
     130             :   /** Legacy change ID. */
     131         155 :   public static final FieldDef<ChangeData, String> LEGACY_ID_STR =
     132         155 :       exact("legacy_id_str").stored().build(cd -> String.valueOf(cd.getId().get()));
     133             : 
     134             :   /** Newer style Change-Id key. */
     135         155 :   public static final FieldDef<ChangeData, String> ID =
     136         155 :       prefix(ChangeQueryBuilder.FIELD_CHANGE_ID).build(changeGetter(c -> c.getKey().get()));
     137             : 
     138             :   /** Change status string, in the same format as {@code status:}. */
     139         155 :   public static final IndexedField<ChangeData, String> STATUS_FIELD =
     140         155 :       IndexedField.<ChangeData>stringBuilder("Status")
     141         155 :           .required()
     142         155 :           .size(20)
     143         155 :           .build(changeGetter(c -> ChangeStatusPredicate.canonicalize(c.getStatus())));
     144             : 
     145         155 :   public static final IndexedField<ChangeData, String>.SearchSpec STATUS_SPEC =
     146         155 :       STATUS_FIELD.exact(ChangeQueryBuilder.FIELD_STATUS);
     147             : 
     148             :   /** Project containing the change. */
     149         155 :   public static final IndexedField<ChangeData, String> PROJECT_FIELD =
     150         155 :       IndexedField.<ChangeData>stringBuilder("Project")
     151         155 :           .required()
     152         155 :           .stored()
     153         155 :           .size(200)
     154         155 :           .build(changeGetter(c -> c.getProject().get()));
     155             : 
     156         155 :   public static final IndexedField<ChangeData, String>.SearchSpec PROJECT_SPEC =
     157         155 :       PROJECT_FIELD.exact(ChangeQueryBuilder.FIELD_PROJECT);
     158             : 
     159             :   /** Project containing the change, as a prefix field. */
     160         155 :   public static final IndexedField<ChangeData, String>.SearchSpec PROJECTS_SPEC =
     161         155 :       PROJECT_FIELD.prefix(ChangeQueryBuilder.FIELD_PROJECTS);
     162             : 
     163             :   /** Reference (aka branch) the change will submit onto. */
     164         155 :   public static final IndexedField<ChangeData, String> REF_FIELD =
     165         155 :       IndexedField.<ChangeData>stringBuilder("Ref")
     166         155 :           .required()
     167         155 :           .size(300)
     168         155 :           .build(changeGetter(c -> c.getDest().branch()));
     169             : 
     170         155 :   public static final IndexedField<ChangeData, String>.SearchSpec REF_SPEC =
     171         155 :       REF_FIELD.exact(ChangeQueryBuilder.FIELD_REF);
     172             : 
     173             :   /** Topic, a short annotation on the branch. */
     174         155 :   public static final IndexedField<ChangeData, String> TOPIC_FIELD =
     175         155 :       IndexedField.<ChangeData>stringBuilder("Topic").size(500).build(ChangeField::getTopic);
     176             : 
     177         155 :   public static final IndexedField<ChangeData, String>.SearchSpec EXACT_TOPIC =
     178         155 :       TOPIC_FIELD.exact("topic4");
     179             : 
     180             :   /** Topic, a short annotation on the branch. */
     181         155 :   public static final IndexedField<ChangeData, String>.SearchSpec FUZZY_TOPIC =
     182         155 :       TOPIC_FIELD.fullText("topic5");
     183             : 
     184             :   /** Topic, a short annotation on the branch. */
     185         155 :   public static final IndexedField<ChangeData, String>.SearchSpec PREFIX_TOPIC =
     186         155 :       TOPIC_FIELD.prefix("topic6");
     187             : 
     188             :   /** {@link com.google.gerrit.entities.SubmissionId} assigned by MergeOp. */
     189         155 :   public static final IndexedField<ChangeData, String> SUBMISSIONID_FIELD =
     190         155 :       IndexedField.<ChangeData>stringBuilder("SubmissionId")
     191         155 :           .size(500)
     192         155 :           .build(changeGetter(Change::getSubmissionId));
     193             : 
     194         155 :   public static final IndexedField<ChangeData, String>.SearchSpec SUBMISSIONID_SPEC =
     195         155 :       SUBMISSIONID_FIELD.exact(ChangeQueryBuilder.FIELD_SUBMISSIONID);
     196             : 
     197             :   /** Last update time since January 1, 1970. */
     198             :   // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
     199         155 :   public static final FieldDef<ChangeData, Timestamp> UPDATED =
     200         155 :       timestamp("updated2")
     201         155 :           .stored()
     202         155 :           .build(changeGetter(change -> Timestamp.from(change.getLastUpdatedOn())));
     203             : 
     204             :   /** When this change was merged, time since January 1, 1970. */
     205             :   // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
     206         155 :   public static final IndexedField<ChangeData, Timestamp> MERGED_ON_FIELD =
     207         155 :       IndexedField.<ChangeData>timestampBuilder("MergedOn")
     208         155 :           .stored()
     209         155 :           .build(
     210         103 :               cd -> cd.getMergedOn().map(Timestamp::from).orElse(null),
     211         100 :               (cd, field) -> cd.setMergedOn(field != null ? field.toInstant() : null));
     212             : 
     213         155 :   public static final IndexedField<ChangeData, Timestamp>.SearchSpec MERGED_ON_SPEC =
     214         155 :       MERGED_ON_FIELD.timestamp(ChangeQueryBuilder.FIELD_MERGED_ON);
     215             : 
     216             :   /** List of full file paths modified in the current patch set. */
     217         155 :   public static final IndexedField<ChangeData, Iterable<String>> PATH_FIELD =
     218             :       // Named for backwards compatibility.
     219         155 :       IndexedField.<ChangeData>iterableStringBuilder("File")
     220         155 :           .build(cd -> firstNonNull(cd.currentFilePaths(), ImmutableList.of()));
     221             : 
     222         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PATH_SPEC =
     223             :       PATH_FIELD
     224             :           // Named for backwards compatibility.
     225         155 :           .exact(ChangeQueryBuilder.FIELD_FILE);
     226             : 
     227             :   public static Set<String> getFileParts(ChangeData cd) {
     228         103 :     List<String> paths = cd.currentFilePaths();
     229             : 
     230         103 :     Splitter s = Splitter.on('/').omitEmptyStrings();
     231         103 :     Set<String> r = new HashSet<>();
     232         103 :     for (String path : paths) {
     233          90 :       for (String part : s.split(path)) {
     234          90 :         r.add(part);
     235          90 :       }
     236          90 :     }
     237         103 :     return r;
     238             :   }
     239             : 
     240             :   /** Hashtags tied to a change */
     241         155 :   public static final IndexedField<ChangeData, Iterable<String>> HASHTAG_FIELD =
     242         155 :       IndexedField.<ChangeData>iterableStringBuilder("Hashtag")
     243         155 :           .size(200)
     244         155 :           .build(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
     245             : 
     246         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec HASHTAG_SPEC =
     247         155 :       HASHTAG_FIELD.exact(ChangeQueryBuilder.FIELD_HASHTAG);
     248             : 
     249             :   /** Hashtags as fulltext field for in-string search. */
     250         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FUZZY_HASHTAG =
     251         155 :       HASHTAG_FIELD.fullText("hashtag2");
     252             : 
     253             :   /** Hashtags as prefix field for in-string search. */
     254         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PREFIX_HASHTAG =
     255         155 :       HASHTAG_FIELD.prefix("hashtag3");
     256             : 
     257             :   /** Hashtags with original case. */
     258         155 :   public static final IndexedField<ChangeData, Iterable<byte[]>> HASHTAG_CASE_AWARE_FIELD =
     259         155 :       IndexedField.<ChangeData>iterableByteArrayBuilder("HashtagCaseAware")
     260         155 :           .stored()
     261         155 :           .build(
     262         103 :               cd -> cd.hashtags().stream().map(t -> t.getBytes(UTF_8)).collect(toSet()),
     263             :               (cd, field) ->
     264         100 :                   cd.setHashtags(
     265         100 :                       StreamSupport.stream(field.spliterator(), false)
     266         100 :                           .map(f -> new String(f, UTF_8))
     267         100 :                           .collect(toImmutableSet())));
     268             : 
     269             :   public static final IndexedField<ChangeData, Iterable<byte[]>>.SearchSpec
     270         155 :       HASHTAG_CASE_AWARE_SPEC = HASHTAG_CASE_AWARE_FIELD.storedOnly("_hashtag");
     271             : 
     272             :   /** Components of each file path modified in the current patch set. */
     273         155 :   public static final IndexedField<ChangeData, Iterable<String>> FILE_PART_FIELD =
     274         155 :       IndexedField.<ChangeData>iterableStringBuilder("FilePart").build(ChangeField::getFileParts);
     275             : 
     276         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FILE_PART_SPEC =
     277         155 :       FILE_PART_FIELD.exact(ChangeQueryBuilder.FIELD_FILEPART);
     278             : 
     279             :   /** File extensions of each file modified in the current patch set. */
     280         155 :   public static final IndexedField<ChangeData, Iterable<String>> EXTENSION_FIELD =
     281         155 :       IndexedField.<ChangeData>iterableStringBuilder("Extension")
     282         155 :           .size(100)
     283         155 :           .build(ChangeField::getExtensions);
     284             : 
     285         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXTENSION_SPEC =
     286         155 :       EXTENSION_FIELD.exact(ChangeQueryBuilder.FIELD_EXTENSION);
     287             : 
     288             :   public static Set<String> getExtensions(ChangeData cd) {
     289         103 :     return extensions(cd).collect(toSet());
     290             :   }
     291             : 
     292             :   /**
     293             :    * File extensions of each file modified in the current patch set as a sorted list. The purpose of
     294             :    * this field is to allow matching changes that only touch files with certain file extensions.
     295             :    */
     296         155 :   public static final IndexedField<ChangeData, String> ONLY_EXTENSIONS_FIELD =
     297         155 :       IndexedField.<ChangeData>stringBuilder("OnlyExtensions")
     298         155 :           .build(ChangeField::getAllExtensionsAsList);
     299             : 
     300         155 :   public static final IndexedField<ChangeData, String>.SearchSpec ONLY_EXTENSIONS_SPEC =
     301         155 :       ONLY_EXTENSIONS_FIELD.exact(ChangeQueryBuilder.FIELD_ONLY_EXTENSIONS);
     302             : 
     303             :   public static String getAllExtensionsAsList(ChangeData cd) {
     304         103 :     return extensions(cd).distinct().sorted().collect(joining(","));
     305             :   }
     306             : 
     307             :   /**
     308             :    * Returns a stream with all file extensions that are used by files in the given change. A file
     309             :    * extension is defined as the portion of the filename following the final `.`. Files with no `.`
     310             :    * in their name have no extension. For them an empty string is returned as part of the stream.
     311             :    *
     312             :    * <p>If the change contains multiple files with the same extension the extension is returned
     313             :    * multiple times in the stream (once per file).
     314             :    */
     315             :   private static Stream<String> extensions(ChangeData cd) {
     316         103 :     return cd.currentFilePaths().stream()
     317             :         // Use case-insensitive file extensions even though other file fields are case-sensitive.
     318             :         // If we want to find "all Java files", we want to match both .java and .JAVA, even if we
     319             :         // normally care about case sensitivity. (Whether we should change the existing file/path
     320             :         // predicates to be case insensitive is a separate question.)
     321         103 :         .map(f -> Files.getFileExtension(f).toLowerCase(Locale.US));
     322             :   }
     323             : 
     324             :   /** Footers from the commit message of the current patch set. */
     325         155 :   public static final IndexedField<ChangeData, Iterable<String>> FOOTER_FIELD =
     326         155 :       IndexedField.<ChangeData>iterableStringBuilder("Footer").build(ChangeField::getFooters);
     327             : 
     328         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FOOTER_SPEC =
     329         155 :       FOOTER_FIELD.exact(ChangeQueryBuilder.FIELD_FOOTER);
     330             : 
     331             :   public static Set<String> getFooters(ChangeData cd) {
     332         103 :     return cd.commitFooters().stream()
     333         103 :         .map(f -> f.toString().toLowerCase(Locale.US))
     334         103 :         .collect(toSet());
     335             :   }
     336             : 
     337             :   /** Footers from the commit message of the current patch set. */
     338         155 :   public static final IndexedField<ChangeData, Iterable<String>> FOOTER_NAME_FIELD =
     339         155 :       IndexedField.<ChangeData>iterableStringBuilder("FooterName")
     340         155 :           .build(ChangeField::getFootersNames);
     341             : 
     342         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FOOTER_NAME =
     343         155 :       FOOTER_NAME_FIELD.exact(ChangeQueryBuilder.FIELD_FOOTER_NAME);
     344             : 
     345             :   public static Set<String> getFootersNames(ChangeData cd) {
     346         103 :     return cd.commitFooters().stream().map(f -> f.getKey()).collect(toSet());
     347             :   }
     348             : 
     349             :   /** Folders that are touched by the current patch set. */
     350         155 :   public static final IndexedField<ChangeData, Iterable<String>> DIRECTORY_FIELD =
     351         155 :       IndexedField.<ChangeData>iterableStringBuilder("Directory")
     352         155 :           .build(ChangeField::getDirectories);
     353             : 
     354         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec DIRECTORY_SPEC =
     355         155 :       DIRECTORY_FIELD.exact(ChangeQueryBuilder.FIELD_DIRECTORY);
     356             : 
     357             :   public static Set<String> getDirectories(ChangeData cd) {
     358         103 :     List<String> paths = cd.currentFilePaths();
     359             : 
     360         103 :     Splitter s = Splitter.on('/').omitEmptyStrings();
     361         103 :     Set<String> r = new HashSet<>();
     362         103 :     for (String path : paths) {
     363          90 :       StringBuilder directory = new StringBuilder();
     364          90 :       r.add(directory.toString());
     365          90 :       String nextPart = null;
     366          90 :       for (String part : s.split(path.toLowerCase(Locale.US))) {
     367          90 :         if (nextPart != null) {
     368          14 :           r.add(nextPart);
     369             : 
     370          14 :           if (directory.length() > 0) {
     371           6 :             directory.append("/");
     372             :           }
     373          14 :           directory.append(nextPart);
     374             : 
     375          14 :           String intermediateDir = directory.toString();
     376          14 :           int i = intermediateDir.indexOf('/');
     377          14 :           while (i >= 0) {
     378           6 :             r.add(intermediateDir);
     379           6 :             intermediateDir = intermediateDir.substring(i + 1);
     380           6 :             i = intermediateDir.indexOf('/');
     381             :           }
     382             :         }
     383          90 :         nextPart = part;
     384          90 :       }
     385          90 :     }
     386         103 :     return r;
     387             :   }
     388             : 
     389             :   /** Owner/creator of the change. */
     390         155 :   public static final IndexedField<ChangeData, Integer> OWNER_FIELD =
     391         155 :       IndexedField.<ChangeData>integerBuilder("Owner")
     392         155 :           .required()
     393         155 :           .build(changeGetter(c -> c.getOwner().get()));
     394             : 
     395         155 :   public static final IndexedField<ChangeData, Integer>.SearchSpec OWNER_SPEC =
     396         155 :       OWNER_FIELD.integer(ChangeQueryBuilder.FIELD_OWNER);
     397             : 
     398             :   /** Uploader of the latest patch set. */
     399         155 :   public static final IndexedField<ChangeData, Integer> UPLOADER_FIELD =
     400         155 :       IndexedField.<ChangeData>integerBuilder("Uploader")
     401         155 :           .required()
     402         155 :           .build(cd -> cd.currentPatchSet().uploader().get());
     403             : 
     404         155 :   public static final IndexedField<ChangeData, Integer>.SearchSpec UPLOADER_SPEC =
     405         155 :       UPLOADER_FIELD.integer(ChangeQueryBuilder.FIELD_UPLOADER);
     406             : 
     407             :   /** References the source change number that this change was cherry-picked from. */
     408         155 :   public static final IndexedField<ChangeData, Integer> CHERRY_PICK_OF_CHANGE_FIELD =
     409         155 :       IndexedField.<ChangeData>integerBuilder("CherryPickOfChange")
     410         155 :           .build(
     411             :               cd ->
     412         103 :                   cd.change().getCherryPickOf() != null
     413          10 :                       ? cd.change().getCherryPickOf().changeId().get()
     414         103 :                       : null);
     415             : 
     416         155 :   public static final IndexedField<ChangeData, Integer>.SearchSpec CHERRY_PICK_OF_CHANGE =
     417         155 :       CHERRY_PICK_OF_CHANGE_FIELD.integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_CHANGE);
     418             : 
     419             :   /** References the source change patch-set that this change was cherry-picked from. */
     420         155 :   public static final IndexedField<ChangeData, Integer> CHERRY_PICK_OF_PATCHSET_FIELD =
     421         155 :       IndexedField.<ChangeData>integerBuilder("CherryPickOfPatchset")
     422         155 :           .build(
     423             :               cd ->
     424         103 :                   cd.change().getCherryPickOf() != null
     425          10 :                       ? cd.change().getCherryPickOf().get()
     426         103 :                       : null);
     427             : 
     428         155 :   public static final IndexedField<ChangeData, Integer>.SearchSpec CHERRY_PICK_OF_PATCHSET =
     429         155 :       CHERRY_PICK_OF_PATCHSET_FIELD.integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_PATCHSET);
     430             : 
     431             :   /** This class decouples the internal and API types from storage. */
     432             :   private static class StoredAttentionSetEntry {
     433             :     final long timestampMillis;
     434             :     final int userId;
     435             :     final String reason;
     436             :     final AttentionSetUpdate.Operation operation;
     437             : 
     438          51 :     StoredAttentionSetEntry(AttentionSetUpdate attentionSetUpdate) {
     439          51 :       timestampMillis = attentionSetUpdate.timestamp().toEpochMilli();
     440          51 :       userId = attentionSetUpdate.account().get();
     441          51 :       reason = attentionSetUpdate.reason();
     442          51 :       operation = attentionSetUpdate.operation();
     443          51 :     }
     444             : 
     445             :     AttentionSetUpdate toAttentionSetUpdate() {
     446          49 :       return AttentionSetUpdate.createFromRead(
     447          49 :           Instant.ofEpochMilli(timestampMillis), Account.id(userId), operation, reason);
     448             :     }
     449             :   }
     450             : 
     451             :   /**
     452             :    * Users included in the attention set of the change. This omits timestamp, reason and possible
     453             :    * future fields.
     454             :    *
     455             :    * @see #ATTENTION_SET_FULL
     456             :    */
     457         155 :   public static final IndexedField<ChangeData, Iterable<Integer>> ATTENTION_SET_USERS_FIELD =
     458         155 :       IndexedField.<ChangeData>iterableIntegerBuilder("AttentionSetUsers")
     459         155 :           .build(ChangeField::getAttentionSetUserIds);
     460             : 
     461         155 :   public static final IndexedField<ChangeData, Iterable<Integer>>.SearchSpec ATTENTION_SET_USERS =
     462         155 :       ATTENTION_SET_USERS_FIELD.integer(ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS);
     463             : 
     464             :   /** Number of changes that contain attention set. */
     465         155 :   public static final IndexedField<ChangeData, Integer> ATTENTION_SET_USERS_COUNT_FIELD =
     466         155 :       IndexedField.<ChangeData>integerBuilder("AttentionSetUsersCount")
     467         155 :           .build(cd -> additionsOnly(cd.attentionSet()).size());
     468             : 
     469         155 :   public static final IndexedField<ChangeData, Integer>.SearchSpec ATTENTION_SET_USERS_COUNT =
     470         155 :       ATTENTION_SET_USERS_COUNT_FIELD.integerRange(
     471             :           ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS_COUNT);
     472             : 
     473             :   /**
     474             :    * The full attention set data including timestamp, reason and possible future fields.
     475             :    *
     476             :    * @see #ATTENTION_SET_USERS
     477             :    */
     478         155 :   public static final FieldDef<ChangeData, Iterable<byte[]>> ATTENTION_SET_FULL =
     479         155 :       storedOnly(ChangeQueryBuilder.FIELD_ATTENTION_SET_FULL)
     480         155 :           .buildRepeatable(
     481             :               ChangeField::storedAttentionSet,
     482             :               (cd, value) ->
     483         100 :                   parseAttentionSet(
     484         100 :                       StreamSupport.stream(value.spliterator(), false)
     485         100 :                           .map(v -> new String(v, UTF_8))
     486         100 :                           .collect(toImmutableSet()),
     487             :                       cd));
     488             : 
     489             :   /** The user assigned to the change. */
     490         155 :   public static final IndexedField<ChangeData, Integer> ASSIGNEE_FIELD =
     491         155 :       IndexedField.<ChangeData>integerBuilder("Assignee")
     492         155 :           .build(changeGetter(c -> c.getAssignee() != null ? c.getAssignee().get() : NO_ASSIGNEE));
     493             : 
     494         155 :   public static final IndexedField<ChangeData, Integer>.SearchSpec ASSIGNEE_SPEC =
     495         155 :       ASSIGNEE_FIELD.integer(ChangeQueryBuilder.FIELD_ASSIGNEE);
     496             : 
     497             :   /** Reviewer(s) associated with the change. */
     498         155 :   public static final IndexedField<ChangeData, Iterable<String>> REVIEWER_FIELD =
     499         155 :       IndexedField.<ChangeData>iterableStringBuilder("Reviewer")
     500         155 :           .stored()
     501         155 :           .build(
     502         103 :               cd -> getReviewerFieldValues(cd.reviewers()),
     503         100 :               (cd, field) -> cd.setReviewers(parseReviewerFieldValues(cd.getId(), field)));
     504             : 
     505         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec REVIEWER_SPEC =
     506         155 :       REVIEWER_FIELD.exact("reviewer2");
     507             : 
     508             :   /** Reviewer(s) associated with the change that do not have a gerrit account. */
     509         155 :   public static final IndexedField<ChangeData, Iterable<String>> REVIEWER_BY_EMAIL_FIELD =
     510         155 :       IndexedField.<ChangeData>iterableStringBuilder("ReviewerByEmail")
     511         155 :           .stored()
     512         155 :           .build(
     513         103 :               cd -> getReviewerByEmailFieldValues(cd.reviewersByEmail()),
     514             :               (cd, field) ->
     515         100 :                   cd.setReviewersByEmail(parseReviewerByEmailFieldValues(cd.getId(), field)));
     516             : 
     517         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec REVIEWER_BY_EMAIL =
     518         155 :       REVIEWER_BY_EMAIL_FIELD.exact("reviewer_by_email");
     519             : 
     520             :   /** Reviewer(s) modified during change's current WIP phase. */
     521         155 :   public static final IndexedField<ChangeData, Iterable<String>> PENDING_REVIEWER_FIELD =
     522         155 :       IndexedField.<ChangeData>iterableStringBuilder("PendingReviewer")
     523         155 :           .stored()
     524         155 :           .build(
     525         103 :               cd -> getReviewerFieldValues(cd.pendingReviewers()),
     526         100 :               (cd, field) -> cd.setPendingReviewers(parseReviewerFieldValues(cd.getId(), field)));
     527             : 
     528         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PENDING_REVIEWER_SPEC =
     529         155 :       PENDING_REVIEWER_FIELD.exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER);
     530             : 
     531             :   /** Reviewer(s) by email modified during change's current WIP phase. */
     532         155 :   public static final IndexedField<ChangeData, Iterable<String>> PENDING_REVIEWER_BY_EMAIL_FIELD =
     533         155 :       IndexedField.<ChangeData>iterableStringBuilder("PendingReviewerByEmail")
     534         155 :           .stored()
     535         155 :           .build(
     536         103 :               cd -> getReviewerByEmailFieldValues(cd.pendingReviewersByEmail()),
     537             :               (cd, field) ->
     538         100 :                   cd.setPendingReviewersByEmail(
     539         100 :                       parseReviewerByEmailFieldValues(cd.getId(), field)));
     540             : 
     541             :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec
     542         155 :       PENDING_REVIEWER_BY_EMAIL =
     543         155 :           PENDING_REVIEWER_BY_EMAIL_FIELD.exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER_BY_EMAIL);
     544             : 
     545             :   /** References a change that this change reverts. */
     546         155 :   public static final IndexedField<ChangeData, Integer> REVERT_OF_FIELD =
     547         155 :       IndexedField.<ChangeData>integerBuilder("RevertOf")
     548         155 :           .build(cd -> cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null);
     549             : 
     550         155 :   public static final IndexedField<ChangeData, Integer>.SearchSpec REVERT_OF =
     551         155 :       REVERT_OF_FIELD.integer(ChangeQueryBuilder.FIELD_REVERTOF);
     552             : 
     553         155 :   public static final IndexedField<ChangeData, String> IS_PURE_REVERT_FIELD =
     554         155 :       IndexedField.<ChangeData>stringBuilder("IsPureRevert")
     555         155 :           .build(cd -> Boolean.TRUE.equals(cd.isPureRevert()) ? "1" : "0");
     556             : 
     557         155 :   public static final IndexedField<ChangeData, String>.SearchSpec IS_PURE_REVERT_SPEC =
     558         155 :       IS_PURE_REVERT_FIELD.fullText(ChangeQueryBuilder.FIELD_PURE_REVERT);
     559             : 
     560             :   /**
     561             :    * Determines if a change is submittable based on {@link
     562             :    * com.google.gerrit.entities.SubmitRequirement}s.
     563             :    */
     564         155 :   public static final IndexedField<ChangeData, String> IS_SUBMITTABLE_FIELD =
     565         155 :       IndexedField.<ChangeData>stringBuilder("IsSubmittable")
     566         155 :           .build(
     567             :               cd ->
     568             :                   // All submit requirements should be fulfilled
     569         103 :                   cd.submitRequirementsIncludingLegacy().values().stream()
     570         103 :                           .allMatch(SubmitRequirementResult::fulfilled)
     571          63 :                       ? "1"
     572         103 :                       : "0");
     573             : 
     574         155 :   public static final IndexedField<ChangeData, String>.SearchSpec IS_SUBMITTABLE_SPEC =
     575         155 :       IS_SUBMITTABLE_FIELD.exact(ChangeQueryBuilder.FIELD_IS_SUBMITTABLE);
     576             : 
     577             :   @VisibleForTesting
     578             :   static List<String> getReviewerFieldValues(ReviewerSet reviewers) {
     579         103 :     List<String> r = new ArrayList<>(reviewers.asTable().size() * 2);
     580         103 :     for (Table.Cell<ReviewerStateInternal, Account.Id, Instant> c : reviewers.asTable().cellSet()) {
     581          75 :       String v = getReviewerFieldValue(c.getRowKey(), c.getColumnKey());
     582          75 :       r.add(v);
     583          75 :       r.add(v + ',' + c.getValue().toEpochMilli());
     584          75 :     }
     585         103 :     return r;
     586             :   }
     587             : 
     588             :   public static String getReviewerFieldValue(ReviewerStateInternal state, Account.Id id) {
     589          76 :     return state.toString() + ',' + id;
     590             :   }
     591             : 
     592             :   @VisibleForTesting
     593             :   static List<String> getReviewerByEmailFieldValues(ReviewerByEmailSet reviewersByEmail) {
     594         103 :     List<String> r = new ArrayList<>(reviewersByEmail.asTable().size() * 2);
     595             :     for (Table.Cell<ReviewerStateInternal, Address, Instant> c :
     596         103 :         reviewersByEmail.asTable().cellSet()) {
     597          10 :       String v = getReviewerByEmailFieldValue(c.getRowKey(), c.getColumnKey());
     598          10 :       r.add(v);
     599          10 :       if (c.getColumnKey().name() != null) {
     600             :         // Add another entry without the name to provide search functionality on the email
     601           5 :         Address emailOnly = Address.create(c.getColumnKey().email());
     602           5 :         r.add(getReviewerByEmailFieldValue(c.getRowKey(), emailOnly));
     603             :       }
     604          10 :       r.add(v + ',' + c.getValue().toEpochMilli());
     605          10 :     }
     606         103 :     return r;
     607             :   }
     608             : 
     609             :   public static String getReviewerByEmailFieldValue(ReviewerStateInternal state, Address adr) {
     610          11 :     return state.toString() + ',' + adr;
     611             :   }
     612             : 
     613             :   public static ReviewerSet parseReviewerFieldValues(Change.Id changeId, Iterable<String> values) {
     614         100 :     ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Instant> b = ImmutableTable.builder();
     615         100 :     for (String v : values) {
     616             : 
     617          73 :       int i = v.indexOf(',');
     618          73 :       if (i < 0) {
     619           0 :         logger.atWarning().log(
     620           0 :             "Invalid value for reviewer field from change %s: %s", changeId.get(), v);
     621           0 :         continue;
     622             :       }
     623             : 
     624          73 :       int i2 = v.lastIndexOf(',');
     625          73 :       if (i2 == i) {
     626             :         // Don't log a warning here.
     627             :         // For each reviewer we store 2 values in the reviewer field, one value with the format
     628             :         // "<reviewer-type>,<account-id>" and one value with the format
     629             :         // "<reviewer-type>,<account-id>,<timestamp>" (see #getReviewerFieldValues(ReviewerSet)).
     630             :         // For parsing we are only interested in the "<reviewer-type>,<account-id>,<timestamp>"
     631             :         // value and the "<reviewer-type>,<account-id>" value is ignored here.
     632          73 :         continue;
     633             :       }
     634             : 
     635          73 :       Optional<ReviewerStateInternal> reviewerState = getReviewerState(v.substring(0, i));
     636          73 :       if (!reviewerState.isPresent()) {
     637           0 :         logger.atWarning().log(
     638             :             "Failed to parse reviewer state of reviewer field from change %s: %s",
     639           0 :             changeId.get(), v);
     640           0 :         continue;
     641             :       }
     642             : 
     643          73 :       Optional<Account.Id> accountId = Account.Id.tryParse(v.substring(i + 1, i2));
     644          73 :       if (!accountId.isPresent()) {
     645           0 :         logger.atWarning().log(
     646           0 :             "Failed to parse account ID of reviewer field from change %s: %s", changeId.get(), v);
     647           0 :         continue;
     648             :       }
     649             : 
     650          73 :       Long l = Longs.tryParse(v.substring(i2 + 1));
     651          73 :       if (l == null) {
     652           0 :         logger.atWarning().log(
     653           0 :             "Failed to parse timestamp of reviewer field from change %s: %s", changeId.get(), v);
     654           0 :         continue;
     655             :       }
     656          73 :       Instant timestamp = Instant.ofEpochMilli(l);
     657             : 
     658          73 :       b.put(reviewerState.get(), accountId.get(), timestamp);
     659          73 :     }
     660         100 :     return ReviewerSet.fromTable(b.build());
     661             :   }
     662             : 
     663             :   public static ReviewerByEmailSet parseReviewerByEmailFieldValues(
     664             :       Change.Id changeId, Iterable<String> values) {
     665         100 :     ImmutableTable.Builder<ReviewerStateInternal, Address, Instant> b = ImmutableTable.builder();
     666         100 :     for (String v : values) {
     667          10 :       int i = v.indexOf(',');
     668          10 :       if (i < 0) {
     669           0 :         logger.atWarning().log(
     670           0 :             "Invalid value for reviewer by email field from change %s: %s", changeId.get(), v);
     671           0 :         continue;
     672             :       }
     673             : 
     674          10 :       int i2 = v.lastIndexOf(',');
     675          10 :       if (i2 == i) {
     676             :         // Don't log a warning here.
     677             :         // For each reviewer we store 2 values in the reviewer field, one value with the format
     678             :         // "<reviewer-type>,<email>" and one value with the format
     679             :         // "<reviewer-type>,<email>,<timestamp>" (see
     680             :         // #getReviewerByEmailFieldValues(ReviewerByEmailSet)).
     681             :         // For parsing we are only interested in the "<reviewer-type>,<email>,<timestamp>" value
     682             :         // and the "<reviewer-type>,<email>" value is ignored here.
     683          10 :         continue;
     684             :       }
     685             : 
     686          10 :       Optional<ReviewerStateInternal> reviewerState = getReviewerState(v.substring(0, i));
     687          10 :       if (!reviewerState.isPresent()) {
     688           0 :         logger.atWarning().log(
     689             :             "Failed to parse reviewer state of reviewer by email field from change %s: %s",
     690           0 :             changeId.get(), v);
     691           0 :         continue;
     692             :       }
     693             : 
     694          10 :       Address address = Address.tryParse(v.substring(i + 1, i2));
     695          10 :       if (address == null) {
     696           0 :         logger.atWarning().log(
     697             :             "Failed to parse address of reviewer by email field from change %s: %s",
     698           0 :             changeId.get(), v);
     699           0 :         continue;
     700             :       }
     701             : 
     702          10 :       Long l = Longs.tryParse(v.substring(i2 + 1));
     703          10 :       if (l == null) {
     704           0 :         logger.atWarning().log(
     705             :             "Failed to parse timestamp of reviewer by email field from change %s: %s",
     706           0 :             changeId.get(), v);
     707           0 :         continue;
     708             :       }
     709          10 :       Instant timestamp = Instant.ofEpochMilli(l);
     710             : 
     711          10 :       b.put(reviewerState.get(), address, timestamp);
     712          10 :     }
     713         100 :     return ReviewerByEmailSet.fromTable(b.build());
     714             :   }
     715             : 
     716             :   private static Optional<ReviewerStateInternal> getReviewerState(String value) {
     717             :     try {
     718          73 :       return Optional.of(ReviewerStateInternal.valueOf(value));
     719           0 :     } catch (IllegalArgumentException | NullPointerException e) {
     720           0 :       return Optional.empty();
     721             :     }
     722             :   }
     723             : 
     724             :   private static ImmutableSet<Integer> getAttentionSetUserIds(ChangeData changeData) {
     725         103 :     return additionsOnly(changeData.attentionSet()).stream()
     726         103 :         .map(update -> update.account().get())
     727         103 :         .collect(toImmutableSet());
     728             :   }
     729             : 
     730             :   private static ImmutableSet<byte[]> storedAttentionSet(ChangeData changeData) {
     731         103 :     return changeData.attentionSet().stream()
     732         103 :         .map(StoredAttentionSetEntry::new)
     733         103 :         .map(storedAttentionSetEntry -> GSON.toJson(storedAttentionSetEntry).getBytes(UTF_8))
     734         103 :         .collect(toImmutableSet());
     735             :   }
     736             : 
     737             :   /**
     738             :    * Deserializes the specified attention set entries from JSON and stores them in the specified
     739             :    * change.
     740             :    */
     741             :   public static void parseAttentionSet(
     742             :       Collection<String> storedAttentionSetEntriesJson, ChangeData changeData) {
     743         100 :     ImmutableSet<AttentionSetUpdate> attentionSet =
     744         100 :         storedAttentionSetEntriesJson.stream()
     745         100 :             .map(
     746          49 :                 entry -> GSON.fromJson(entry, StoredAttentionSetEntry.class).toAttentionSetUpdate())
     747         100 :             .collect(toImmutableSet());
     748         100 :     changeData.setAttentionSet(attentionSet);
     749         100 :   }
     750             : 
     751             :   /** Commit ID of any patch set on the change, using prefix match. */
     752         155 :   public static final FieldDef<ChangeData, Iterable<String>> COMMIT =
     753         155 :       prefix(ChangeQueryBuilder.FIELD_COMMIT).buildRepeatable(ChangeField::getRevisions);
     754             : 
     755             :   /** Commit ID of any patch set on the change, using exact match. */
     756         155 :   public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMIT =
     757         155 :       exact(ChangeQueryBuilder.FIELD_EXACTCOMMIT).buildRepeatable(ChangeField::getRevisions);
     758             : 
     759             :   private static ImmutableSet<String> getRevisions(ChangeData cd) {
     760         103 :     return cd.patchSets().stream().map(ps -> ps.commitId().name()).collect(toImmutableSet());
     761             :   }
     762             : 
     763             :   /** Tracking id extracted from a footer. */
     764         155 :   public static final FieldDef<ChangeData, Iterable<String>> TR =
     765         155 :       exact(ChangeQueryBuilder.FIELD_TR)
     766         155 :           .buildRepeatable(cd -> ImmutableSet.copyOf(cd.trackingFooters().values()));
     767             : 
     768             :   /** List of labels on the current patch set including change owner votes. */
     769         155 :   public static final FieldDef<ChangeData, Iterable<String>> LABEL =
     770         155 :       exact("label2").buildRepeatable(cd -> getLabels(cd));
     771             : 
     772             :   private static Iterable<String> getLabels(ChangeData cd) {
     773         103 :     Set<String> allApprovals = new HashSet<>();
     774         103 :     Set<String> distinctApprovals = new HashSet<>();
     775         103 :     Table<String, Short, Integer> voteCounts = HashBasedTable.create();
     776         103 :     for (PatchSetApproval a : cd.currentApprovals()) {
     777          67 :       if (a.value() != 0 && !a.isLegacySubmit()) {
     778          58 :         increment(voteCounts, a.label(), a.value());
     779          58 :         Optional<LabelType> labelType = cd.getLabelTypes().byLabel(a.labelId());
     780             : 
     781          58 :         allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
     782          58 :         allApprovals.addAll(getMagicLabelFormats(a.label(), a.value(), labelType, a.accountId()));
     783          58 :         allApprovals.addAll(getLabelOwnerFormats(a, cd, labelType));
     784          58 :         allApprovals.addAll(getLabelNonUploaderFormats(a, cd, labelType));
     785          58 :         distinctApprovals.add(formatLabel(a.label(), a.value()));
     786          58 :         distinctApprovals.addAll(
     787          58 :             getMagicLabelFormats(a.label(), a.value(), labelType, /* accountId= */ null));
     788             :       }
     789          67 :     }
     790         103 :     allApprovals.addAll(distinctApprovals);
     791         103 :     allApprovals.addAll(getCountLabelFormats(voteCounts, cd));
     792         103 :     return allApprovals;
     793             :   }
     794             : 
     795             :   private static void increment(Table<String, Short, Integer> table, String k1, short k2) {
     796          58 :     if (!table.contains(k1, k2)) {
     797          58 :       table.put(k1, k2, 1);
     798             :     } else {
     799          18 :       int val = table.get(k1, k2);
     800          18 :       table.put(k1, k2, val + 1);
     801             :     }
     802          58 :   }
     803             : 
     804             :   private static List<String> getCountLabelFormats(
     805             :       Table<String, Short, Integer> voteCounts, ChangeData cd) {
     806         103 :     List<String> allFormats = new ArrayList<>();
     807         103 :     for (String label : voteCounts.rowMap().keySet()) {
     808          58 :       Optional<LabelType> labelType = cd.getLabelTypes().byLabel(label);
     809          58 :       Map<Short, Integer> row = voteCounts.row(label);
     810          58 :       for (short vote : row.keySet()) {
     811          58 :         int count = row.get(vote);
     812          58 :         allFormats.addAll(getCountLabelFormats(labelType, label, vote, count));
     813          58 :       }
     814          58 :     }
     815         103 :     return allFormats;
     816             :   }
     817             : 
     818             :   private static List<String> getCountLabelFormats(
     819             :       Optional<LabelType> labelType, String label, short vote, int count) {
     820          58 :     List<String> formats =
     821          58 :         getMagicLabelFormats(label, vote, labelType, /* accountId= */ null, /* count= */ count);
     822          58 :     formats.add(formatLabel(label, vote, count));
     823          58 :     return formats;
     824             :   }
     825             : 
     826             :   /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
     827             :   private static List<String> getMagicLabelFormats(
     828             :       String label, short labelVal, Optional<LabelType> labelType, @Nullable Account.Id accountId) {
     829          58 :     return getMagicLabelFormats(label, labelVal, labelType, accountId, /* count= */ null);
     830             :   }
     831             : 
     832             :   /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
     833             :   private static List<String> getMagicLabelFormats(
     834             :       String label,
     835             :       short labelVal,
     836             :       Optional<LabelType> labelType,
     837             :       @Nullable Account.Id accountId,
     838             :       @Nullable Integer count) {
     839          58 :     List<String> labels = new ArrayList<>();
     840          58 :     if (labelType.isPresent()) {
     841          58 :       if (labelVal == labelType.get().getMaxPositive()) {
     842          56 :         labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId, count));
     843             :       }
     844          58 :       if (labelVal == labelType.get().getMaxNegative()) {
     845          19 :         labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId, count));
     846             :       }
     847             :     }
     848          58 :     labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId, count));
     849          58 :     return labels;
     850             :   }
     851             : 
     852             :   private static List<String> getLabelOwnerFormats(
     853             :       PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) {
     854          58 :     List<String> allFormats = new ArrayList<>();
     855          58 :     if (cd.change().getOwner().equals(a.accountId())) {
     856          57 :       allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
     857          57 :       allFormats.addAll(
     858          57 :           getMagicLabelFormats(
     859          57 :               a.label(), a.value(), labelType, ChangeQueryBuilder.OWNER_ACCOUNT_ID));
     860             :     }
     861          58 :     return allFormats;
     862             :   }
     863             : 
     864             :   private static List<String> getLabelNonUploaderFormats(
     865             :       PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) {
     866          58 :     List<String> allFormats = new ArrayList<>();
     867          58 :     if (!cd.currentPatchSet().uploader().equals(a.accountId())) {
     868          28 :       allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
     869          28 :       allFormats.addAll(
     870          28 :           getMagicLabelFormats(
     871          28 :               a.label(), a.value(), labelType, ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
     872             :     }
     873          58 :     return allFormats;
     874             :   }
     875             : 
     876             :   public static Set<String> getAuthorParts(ChangeData cd) {
     877         103 :     return SchemaUtil.getPersonParts(cd.getAuthor());
     878             :   }
     879             : 
     880             :   public static Set<String> getAuthorNameAndEmail(ChangeData cd) {
     881         103 :     return getNameAndEmail(cd.getAuthor());
     882             :   }
     883             : 
     884             :   public static Set<String> getCommitterParts(ChangeData cd) {
     885         103 :     return SchemaUtil.getPersonParts(cd.getCommitter());
     886             :   }
     887             : 
     888             :   public static Set<String> getCommitterNameAndEmail(ChangeData cd) {
     889         103 :     return getNameAndEmail(cd.getCommitter());
     890             :   }
     891             : 
     892             :   private static Set<String> getNameAndEmail(PersonIdent person) {
     893         103 :     if (person == null) {
     894           0 :       return ImmutableSet.of();
     895             :     }
     896             : 
     897         103 :     String name = person.getName().toLowerCase(Locale.US);
     898         103 :     String email = person.getEmailAddress().toLowerCase(Locale.US);
     899             : 
     900         103 :     StringBuilder nameEmailBuilder = new StringBuilder();
     901         103 :     PersonIdent.appendSanitized(nameEmailBuilder, name);
     902         103 :     nameEmailBuilder.append(" <");
     903         103 :     PersonIdent.appendSanitized(nameEmailBuilder, email);
     904         103 :     nameEmailBuilder.append('>');
     905             : 
     906         103 :     return ImmutableSet.of(name, email, nameEmailBuilder.toString());
     907             :   }
     908             : 
     909             :   /**
     910             :    * The exact email address, or any part of the author name or email address, in the current patch
     911             :    * set.
     912             :    */
     913         155 :   public static final IndexedField<ChangeData, Iterable<String>> AUTHOR_PARTS_FIELD =
     914         155 :       IndexedField.<ChangeData>iterableStringBuilder("AuthorParts")
     915         155 :           .required()
     916         155 :           .description(
     917             :               "The exact email address, or any part of the author name or email address, in the current patch set.")
     918         155 :           .build(ChangeField::getAuthorParts);
     919             : 
     920         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec AUTHOR_PARTS_SPEC =
     921         155 :       AUTHOR_PARTS_FIELD.fullText(ChangeQueryBuilder.FIELD_AUTHOR);
     922             : 
     923             :   /** The exact name, email address and NameEmail of the author. */
     924         155 :   public static final IndexedField<ChangeData, Iterable<String>> EXACT_AUTHOR_FIELD =
     925         155 :       IndexedField.<ChangeData>iterableStringBuilder("ExactAuthor")
     926         155 :           .required()
     927         155 :           .description("The exact name, email address and NameEmail of the author.")
     928         155 :           .build(ChangeField::getAuthorNameAndEmail);
     929             : 
     930         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXACT_AUTHOR_SPEC =
     931         155 :       EXACT_AUTHOR_FIELD.exact(ChangeQueryBuilder.FIELD_EXACTAUTHOR);
     932             : 
     933             :   /**
     934             :    * The exact email address, or any part of the committer name or email address, in the current
     935             :    * patch set.
     936             :    */
     937         155 :   public static final IndexedField<ChangeData, Iterable<String>> COMMITTER_PARTS_FIELD =
     938         155 :       IndexedField.<ChangeData>iterableStringBuilder("CommitterParts")
     939         155 :           .description(
     940             :               "The exact email address, or any part of the committer name or email address, in the current patch set.")
     941         155 :           .required()
     942         155 :           .build(ChangeField::getCommitterParts);
     943             : 
     944         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec COMMITTER_PARTS_SPEC =
     945         155 :       COMMITTER_PARTS_FIELD.fullText(ChangeQueryBuilder.FIELD_COMMITTER);
     946             : 
     947             :   /** The exact name, email address, and NameEmail of the committer. */
     948         155 :   public static final IndexedField<ChangeData, Iterable<String>> EXACT_COMMITTER_FIELD =
     949         155 :       IndexedField.<ChangeData>iterableStringBuilder("ExactCommiter")
     950         155 :           .required()
     951         155 :           .description("The exact name, email address, and NameEmail of the committer.")
     952         155 :           .build(ChangeField::getCommitterNameAndEmail);
     953             : 
     954         155 :   public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXACT_COMMITTER_SPEC =
     955         155 :       EXACT_COMMITTER_FIELD.exact(ChangeQueryBuilder.FIELD_EXACTCOMMITTER);
     956             : 
     957             :   /** Serialized change object, used for pre-populating results. */
     958         155 :   public static final FieldDef<ChangeData, byte[]> CHANGE =
     959         155 :       storedOnly("_change")
     960         155 :           .build(
     961         155 :               changeGetter(change -> toProto(ChangeProtoConverter.INSTANCE, change)),
     962         100 :               (cd, field) -> cd.setChange(parseProtoFrom(field, ChangeProtoConverter.INSTANCE)));
     963             : 
     964             :   /** Serialized approvals for the current patch set, used for pre-populating results. */
     965         155 :   public static final FieldDef<ChangeData, Iterable<byte[]>> APPROVAL =
     966         155 :       storedOnly("_approval")
     967         155 :           .buildRepeatable(
     968         103 :               cd -> toProtos(PatchSetApprovalProtoConverter.INSTANCE, cd.currentApprovals()),
     969             :               (cd, field) ->
     970         100 :                   cd.setCurrentApprovals(
     971         100 :                       decodeProtos(field, PatchSetApprovalProtoConverter.INSTANCE)));
     972             : 
     973             :   public static String formatLabel(String label, int value) {
     974          58 :     return formatLabel(label, value, /* accountId= */ null, /* count= */ null);
     975             :   }
     976             : 
     977             :   public static String formatLabel(String label, int value, @Nullable Integer count) {
     978          58 :     return formatLabel(label, value, /* accountId= */ null, count);
     979             :   }
     980             : 
     981             :   public static String formatLabel(String label, int value, Account.Id accountId) {
     982          58 :     return formatLabel(label, value, accountId, /* count= */ null);
     983             :   }
     984             : 
     985             :   public static String formatLabel(
     986             :       String label, int value, @Nullable Account.Id accountId, @Nullable Integer count) {
     987          59 :     return label.toLowerCase()
     988          59 :         + (value >= 0 ? "+" : "")
     989             :         + value
     990          59 :         + (accountId != null ? "," + formatAccount(accountId) : "")
     991          59 :         + (count != null ? ",count=" + count : "");
     992             :   }
     993             : 
     994             :   public static String formatLabel(
     995             :       String label, String value, @Nullable Account.Id accountId, @Nullable Integer count) {
     996          59 :     return label.toLowerCase()
     997             :         + "="
     998             :         + value
     999          59 :         + (accountId != null ? "," + formatAccount(accountId) : "")
    1000          59 :         + (count != null ? ",count=" + count : "");
    1001             :   }
    1002             : 
    1003             :   private static String formatAccount(Account.Id accountId) {
    1004          58 :     if (ChangeQueryBuilder.OWNER_ACCOUNT_ID.equals(accountId)) {
    1005          57 :       return ChangeQueryBuilder.ARG_ID_OWNER;
    1006          58 :     } else if (ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID.equals(accountId)) {
    1007          28 :       return ChangeQueryBuilder.ARG_ID_NON_UPLOADER;
    1008             :     }
    1009          58 :     return Integer.toString(accountId.get());
    1010             :   }
    1011             : 
    1012             :   /** Commit message of the current patch set. */
    1013         155 :   public static final FieldDef<ChangeData, String> COMMIT_MESSAGE =
    1014         155 :       fullText(ChangeQueryBuilder.FIELD_MESSAGE).build(ChangeData::commitMessage);
    1015             : 
    1016             :   /** Commit message of the current patch set. */
    1017         155 :   public static final FieldDef<ChangeData, String> COMMIT_MESSAGE_EXACT =
    1018         155 :       exact(ChangeQueryBuilder.FIELD_MESSAGE_EXACT)
    1019         155 :           .build(cd -> truncateStringValueToMaxTermLength(cd.commitMessage()));
    1020             : 
    1021             :   /** Summary or inline comment. */
    1022         155 :   public static final FieldDef<ChangeData, Iterable<String>> COMMENT =
    1023         155 :       fullText(ChangeQueryBuilder.FIELD_COMMENT)
    1024         155 :           .buildRepeatable(
    1025             :               cd ->
    1026         103 :                   Stream.concat(
    1027         103 :                           cd.publishedComments().stream().map(c -> c.message),
    1028             :                           // Some endpoint allow passing user message in input, and we still want to
    1029             :                           // search by that. Index on message template with placeholders for user
    1030             :                           // data, so we don't
    1031             :                           // persist user identifiable information data in index.
    1032         103 :                           cd.messages().stream().map(ChangeMessage::getMessage))
    1033         103 :                       .collect(toSet()));
    1034             : 
    1035             :   /** Number of unresolved comment threads of the change, including robot comments. */
    1036         155 :   public static final FieldDef<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT =
    1037         155 :       intRange(ChangeQueryBuilder.FIELD_UNRESOLVED_COMMENT_COUNT)
    1038         155 :           .build(
    1039             :               ChangeData::unresolvedCommentCount,
    1040         100 :               (cd, field) -> cd.setUnresolvedCommentCount(field));
    1041             : 
    1042             :   /** Total number of published inline comments of the change, including robot comments. */
    1043         155 :   public static final FieldDef<ChangeData, Integer> TOTAL_COMMENT_COUNT =
    1044         155 :       intRange("total_comments")
    1045         155 :           .build(ChangeData::totalCommentCount, (cd, field) -> cd.setTotalCommentCount(field));
    1046             : 
    1047             :   /** Whether the change is mergeable. */
    1048         155 :   public static final FieldDef<ChangeData, String> MERGEABLE =
    1049         155 :       exact(ChangeQueryBuilder.FIELD_MERGEABLE)
    1050         155 :           .stored()
    1051         155 :           .build(
    1052             :               cd -> {
    1053          14 :                 Boolean m = cd.isMergeable();
    1054          14 :                 if (m == null) {
    1055           1 :                   return null;
    1056             :                 }
    1057          14 :                 return m ? "1" : "0";
    1058             :               },
    1059         100 :               (cd, field) -> cd.setMergeable(field == null ? false : field.equals("1")));
    1060             : 
    1061             :   /** Whether the change is a merge commit. */
    1062         155 :   public static final FieldDef<ChangeData, String> MERGE =
    1063         155 :       exact(ChangeQueryBuilder.FIELD_MERGE)
    1064         155 :           .stored()
    1065         155 :           .build(
    1066             :               cd -> {
    1067         103 :                 Boolean m = cd.isMerge();
    1068         103 :                 if (m == null) {
    1069           0 :                   return null;
    1070             :                 }
    1071         103 :                 return m ? "1" : "0";
    1072             :               });
    1073             : 
    1074             :   /** Whether the change is a cherry pick of another change. */
    1075         155 :   public static final FieldDef<ChangeData, String> CHERRY_PICK =
    1076         155 :       exact(ChangeQueryBuilder.FIELD_CHERRYPICK)
    1077         155 :           .stored()
    1078         155 :           .build(cd -> cd.change().getCherryPickOf() != null ? "1" : "0");
    1079             : 
    1080             :   /** The number of inserted lines in this change. */
    1081         155 :   public static final FieldDef<ChangeData, Integer> ADDED =
    1082         155 :       intRange(ChangeQueryBuilder.FIELD_ADDED)
    1083         155 :           .build(
    1084         103 :               cd -> cd.changedLines().isPresent() ? cd.changedLines().get().insertions : null,
    1085             :               (cd, field) -> {
    1086         100 :                 if (field != null) {
    1087         100 :                   cd.setLinesInserted(field);
    1088             :                 }
    1089         100 :               });
    1090             : 
    1091             :   /** The number of deleted lines in this change. */
    1092         155 :   public static final FieldDef<ChangeData, Integer> DELETED =
    1093         155 :       intRange(ChangeQueryBuilder.FIELD_DELETED)
    1094         155 :           .build(
    1095         103 :               cd -> cd.changedLines().isPresent() ? cd.changedLines().get().deletions : null,
    1096             :               (cd, field) -> {
    1097         100 :                 if (field != null) {
    1098         100 :                   cd.setLinesDeleted(field);
    1099             :                 }
    1100         100 :               });
    1101             : 
    1102             :   /** The total number of modified lines in this change. */
    1103         155 :   public static final FieldDef<ChangeData, Integer> DELTA =
    1104         155 :       intRange(ChangeQueryBuilder.FIELD_DELTA)
    1105         155 :           .build(cd -> cd.changedLines().map(c -> c.insertions + c.deletions).orElse(null));
    1106             : 
    1107             :   /** Determines if this change is private. */
    1108         155 :   public static final FieldDef<ChangeData, String> PRIVATE =
    1109         155 :       exact(ChangeQueryBuilder.FIELD_PRIVATE).build(cd -> cd.change().isPrivate() ? "1" : "0");
    1110             : 
    1111             :   /** Determines if this change is work in progress. */
    1112         155 :   public static final FieldDef<ChangeData, String> WIP =
    1113         155 :       exact(ChangeQueryBuilder.FIELD_WIP).build(cd -> cd.change().isWorkInProgress() ? "1" : "0");
    1114             : 
    1115             :   /** Determines if this change has started review. */
    1116         155 :   public static final FieldDef<ChangeData, String> STARTED =
    1117         155 :       exact(ChangeQueryBuilder.FIELD_STARTED)
    1118         155 :           .build(cd -> cd.change().hasReviewStarted() ? "1" : "0");
    1119             : 
    1120             :   /** Users who have commented on this change. */
    1121         155 :   public static final FieldDef<ChangeData, Iterable<Integer>> COMMENTBY =
    1122         155 :       integer(ChangeQueryBuilder.FIELD_COMMENTBY)
    1123         155 :           .buildRepeatable(
    1124             :               cd ->
    1125         103 :                   Stream.concat(
    1126         103 :                           cd.messages().stream().map(ChangeMessage::getAuthor),
    1127         103 :                           cd.publishedComments().stream().map(c -> c.author.getId()))
    1128         103 :                       .filter(Objects::nonNull)
    1129         103 :                       .map(Account.Id::get)
    1130         103 :                       .collect(toSet()));
    1131             : 
    1132             :   /** Star labels on this change in the format: &lt;account-id&gt;:&lt;label&gt; */
    1133         155 :   public static final FieldDef<ChangeData, Iterable<String>> STAR =
    1134         155 :       exact(ChangeQueryBuilder.FIELD_STAR)
    1135         155 :           .stored()
    1136         155 :           .buildRepeatable(
    1137             :               cd ->
    1138           3 :                   Iterables.transform(
    1139           3 :                       cd.stars().entries(),
    1140             :                       e ->
    1141           0 :                           StarredChangesUtil.StarField.create(e.getKey(), e.getValue()).toString()),
    1142             :               (cd, field) ->
    1143           3 :                   cd.setStars(
    1144           3 :                       StreamSupport.stream(field.spliterator(), false)
    1145           3 :                           .map(f -> StarredChangesUtil.StarField.parse(f))
    1146           3 :                           .collect(toImmutableListMultimap(e -> e.accountId(), e -> e.label()))));
    1147             : 
    1148             :   /** Users that have starred the change with any label. */
    1149         155 :   public static final FieldDef<ChangeData, Iterable<Integer>> STARBY =
    1150         155 :       integer(ChangeQueryBuilder.FIELD_STARBY)
    1151         155 :           .buildRepeatable(cd -> Iterables.transform(cd.stars().keySet(), Account.Id::get));
    1152             : 
    1153             :   /** Opaque group identifiers for this change's patch sets. */
    1154         155 :   public static final FieldDef<ChangeData, Iterable<String>> GROUP =
    1155         155 :       exact(ChangeQueryBuilder.FIELD_GROUP)
    1156         155 :           .buildRepeatable(
    1157         103 :               cd -> cd.patchSets().stream().flatMap(ps -> ps.groups().stream()).collect(toSet()));
    1158             : 
    1159             :   /** Serialized patch set object, used for pre-populating results. */
    1160         155 :   public static final FieldDef<ChangeData, Iterable<byte[]>> PATCH_SET =
    1161         155 :       storedOnly("_patch_set")
    1162         155 :           .buildRepeatable(
    1163         103 :               cd -> toProtos(PatchSetProtoConverter.INSTANCE, cd.patchSets()),
    1164         100 :               (cd, field) -> cd.setPatchSets(decodeProtos(field, PatchSetProtoConverter.INSTANCE)));
    1165             : 
    1166             :   /** Users who have edits on this change. */
    1167         155 :   public static final FieldDef<ChangeData, Iterable<Integer>> EDITBY =
    1168         155 :       integer(ChangeQueryBuilder.FIELD_EDITBY)
    1169         155 :           .buildRepeatable(cd -> cd.editsByUser().stream().map(Account.Id::get).collect(toSet()));
    1170             : 
    1171             :   /** Users who have draft comments on this change. */
    1172         155 :   public static final FieldDef<ChangeData, Iterable<Integer>> DRAFTBY =
    1173         155 :       integer(ChangeQueryBuilder.FIELD_DRAFTBY)
    1174         155 :           .buildRepeatable(cd -> cd.draftsByUser().stream().map(Account.Id::get).collect(toSet()));
    1175             : 
    1176         155 :   public static final Integer NOT_REVIEWED = -1;
    1177             : 
    1178             :   /**
    1179             :    * Users the change was reviewed by since the last author update.
    1180             :    *
    1181             :    * <p>A change is considered reviewed by a user if the latest update by that user is newer than
    1182             :    * the latest update by the change author. Both top-level change messages and new patch sets are
    1183             :    * considered to be updates.
    1184             :    *
    1185             :    * <p>If the latest update is by the change owner, then the special value {@link #NOT_REVIEWED} is
    1186             :    * emitted.
    1187             :    */
    1188         155 :   public static final FieldDef<ChangeData, Iterable<Integer>> REVIEWEDBY =
    1189         155 :       integer(ChangeQueryBuilder.FIELD_REVIEWEDBY)
    1190         155 :           .stored()
    1191         155 :           .buildRepeatable(
    1192             :               cd -> {
    1193         103 :                 Set<Account.Id> reviewedBy = cd.reviewedBy();
    1194         103 :                 if (reviewedBy.isEmpty()) {
    1195         103 :                   return ImmutableSet.of(NOT_REVIEWED);
    1196             :                 }
    1197          41 :                 return reviewedBy.stream().map(Account.Id::get).collect(toList());
    1198             :               },
    1199             :               (cd, field) ->
    1200         100 :                   cd.setReviewedBy(
    1201         100 :                       StreamSupport.stream(field.spliterator(), false)
    1202         100 :                           .map(Account::id)
    1203         100 :                           .collect(toImmutableSet())));
    1204             : 
    1205             :   public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
    1206         155 :       SubmitRuleOptions.builder().recomputeOnClosedChanges(true).build();
    1207             : 
    1208             :   public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
    1209         155 :       SubmitRuleOptions.builder().build();
    1210             : 
    1211             :   /** All submit rules results in the form of "$ruleName,$status". */
    1212         155 :   public static final FieldDef<ChangeData, Iterable<String>> SUBMIT_RULE_RESULT =
    1213         155 :       exact("submit_rule_result")
    1214         155 :           .buildRepeatable(
    1215             :               cd -> {
    1216         103 :                 List<String> result = new ArrayList<>();
    1217         103 :                 List<SubmitRecord> submitRecords = cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT);
    1218         103 :                 for (SubmitRecord record : submitRecords) {
    1219         103 :                   result.add(record.ruleName + "=" + record.status.name());
    1220         103 :                 }
    1221         103 :                 return result;
    1222             :               });
    1223             : 
    1224             :   /**
    1225             :    * JSON type for storing SubmitRecords.
    1226             :    *
    1227             :    * <p>Stored fields need to use a stable format over a long period; this type insulates the index
    1228             :    * from implementation changes in SubmitRecord itself.
    1229             :    */
    1230             :   public static class StoredSubmitRecord {
    1231         101 :     static class StoredLabel {
    1232             :       String label;
    1233             :       SubmitRecord.Label.Status status;
    1234             :       Integer appliedBy;
    1235             :     }
    1236             : 
    1237           4 :     static class StoredRequirement {
    1238             :       String fallbackText;
    1239             :       String type;
    1240             :       @Deprecated Map<String, String> data;
    1241             :     }
    1242             : 
    1243             :     String ruleName;
    1244             :     SubmitRecord.Status status;
    1245             :     List<StoredLabel> labels;
    1246             :     List<StoredRequirement> requirements;
    1247             :     String errorMessage;
    1248             : 
    1249         101 :     public StoredSubmitRecord(SubmitRecord rec) {
    1250         101 :       this.ruleName = rec.ruleName;
    1251         101 :       this.status = rec.status;
    1252         101 :       this.errorMessage = rec.errorMessage;
    1253         101 :       if (rec.labels != null) {
    1254         101 :         this.labels = new ArrayList<>(rec.labels.size());
    1255         101 :         for (SubmitRecord.Label label : rec.labels) {
    1256         101 :           StoredLabel sl = new StoredLabel();
    1257         101 :           sl.label = label.label;
    1258         101 :           sl.status = label.status;
    1259         101 :           sl.appliedBy = label.appliedBy != null ? label.appliedBy.get() : null;
    1260         101 :           this.labels.add(sl);
    1261         101 :         }
    1262             :       }
    1263         101 :       if (rec.requirements != null) {
    1264           4 :         this.requirements = new ArrayList<>(rec.requirements.size());
    1265           4 :         for (LegacySubmitRequirement requirement : rec.requirements) {
    1266           4 :           StoredRequirement sr = new StoredRequirement();
    1267           4 :           sr.type = requirement.type();
    1268           4 :           sr.fallbackText = requirement.fallbackText();
    1269             :           // For backwards compatibility, write an empty map to the index.
    1270             :           // This is required, because the LegacySubmitRequirement AutoValue can't
    1271             :           // handle null in the old code.
    1272             :           // TODO(hiesel): Remove once we have rolled out the new code
    1273             :           //  and waited long enough to not need to roll back.
    1274           4 :           sr.data = ImmutableMap.of();
    1275           4 :           this.requirements.add(sr);
    1276           4 :         }
    1277             :       }
    1278         101 :     }
    1279             : 
    1280             :     public SubmitRecord toSubmitRecord() {
    1281         100 :       SubmitRecord rec = new SubmitRecord();
    1282         100 :       rec.ruleName = ruleName;
    1283         100 :       rec.status = status;
    1284         100 :       rec.errorMessage = errorMessage;
    1285         100 :       if (labels != null) {
    1286         100 :         rec.labels = new ArrayList<>(labels.size());
    1287         100 :         for (StoredLabel label : labels) {
    1288         100 :           SubmitRecord.Label srl = new SubmitRecord.Label();
    1289         100 :           srl.label = label.label;
    1290         100 :           srl.status = label.status;
    1291         100 :           srl.appliedBy = label.appliedBy != null ? Account.id(label.appliedBy) : null;
    1292         100 :           rec.labels.add(srl);
    1293         100 :         }
    1294             :       }
    1295         100 :       if (requirements != null) {
    1296           4 :         rec.requirements = new ArrayList<>(requirements.size());
    1297           4 :         for (StoredRequirement req : requirements) {
    1298             :           LegacySubmitRequirement sr =
    1299           4 :               LegacySubmitRequirement.builder()
    1300           4 :                   .setType(req.type)
    1301           4 :                   .setFallbackText(req.fallbackText)
    1302           4 :                   .build();
    1303           4 :           rec.requirements.add(sr);
    1304           4 :         }
    1305             :       }
    1306         100 :       return rec;
    1307             :     }
    1308             :   }
    1309             : 
    1310         155 :   public static final FieldDef<ChangeData, Iterable<String>> SUBMIT_RECORD =
    1311         155 :       exact("submit_record").buildRepeatable(ChangeField::formatSubmitRecordValues);
    1312             : 
    1313         155 :   public static final FieldDef<ChangeData, Iterable<byte[]>> STORED_SUBMIT_RECORD_STRICT =
    1314         155 :       storedOnly("full_submit_record_strict")
    1315         155 :           .buildRepeatable(
    1316         103 :               cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_STRICT),
    1317             :               (cd, field) ->
    1318         100 :                   parseSubmitRecords(
    1319         100 :                       StreamSupport.stream(field.spliterator(), false)
    1320         100 :                           .map(f -> new String(f, UTF_8))
    1321         100 :                           .collect(toSet()),
    1322             :                       SUBMIT_RULE_OPTIONS_STRICT,
    1323             :                       cd));
    1324             : 
    1325         155 :   public static final FieldDef<ChangeData, Iterable<byte[]>> STORED_SUBMIT_RECORD_LENIENT =
    1326         155 :       storedOnly("full_submit_record_lenient")
    1327         155 :           .buildRepeatable(
    1328         103 :               cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_LENIENT),
    1329             :               (cd, field) ->
    1330         100 :                   parseSubmitRecords(
    1331         100 :                       StreamSupport.stream(field.spliterator(), false)
    1332         100 :                           .map(f -> new String(f, UTF_8))
    1333         100 :                           .collect(toSet()),
    1334             :                       SUBMIT_RULE_OPTIONS_LENIENT,
    1335             :                       cd));
    1336             : 
    1337             :   public static void parseSubmitRecords(
    1338             :       Collection<String> values, SubmitRuleOptions opts, ChangeData out) {
    1339         100 :     List<SubmitRecord> records = parseSubmitRecords(values);
    1340         100 :     out.setSubmitRecords(opts, records);
    1341         100 :   }
    1342             : 
    1343             :   @VisibleForTesting
    1344             :   static List<SubmitRecord> parseSubmitRecords(Collection<String> values) {
    1345         100 :     return values.stream()
    1346         100 :         .map(v -> GSON.fromJson(v, StoredSubmitRecord.class).toSubmitRecord())
    1347         100 :         .collect(toList());
    1348             :   }
    1349             : 
    1350             :   @VisibleForTesting
    1351             :   static List<byte[]> storedSubmitRecords(List<SubmitRecord> records) {
    1352         103 :     return Lists.transform(records, r -> GSON.toJson(new StoredSubmitRecord(r)).getBytes(UTF_8));
    1353             :   }
    1354             : 
    1355             :   private static Iterable<byte[]> storedSubmitRecords(ChangeData cd, SubmitRuleOptions opts) {
    1356         103 :     return storedSubmitRecords(cd.submitRecords(opts));
    1357             :   }
    1358             : 
    1359             :   public static List<String> formatSubmitRecordValues(ChangeData cd) {
    1360         103 :     Set<String> submitRecordValues = new HashSet<>();
    1361         103 :     submitRecordValues.addAll(
    1362         103 :         formatSubmitRecordValues(
    1363         103 :             cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT), cd.change().getOwner()));
    1364             :     // Also backfill results of submit requirements such that users can query submit requirement
    1365             :     // results using the label operator, for example a query with "label:CR=NEED" will match with
    1366             :     // changes that have a submit-requirement with name="CR" and status=UNSATISFIED.
    1367             :     // Reason: We are preserving backward compatibility of the operators `label:$name=$status`
    1368             :     // which were previously working with submit records. Now admins can configure submit
    1369             :     // requirements and continue querying them with the label operator.
    1370         103 :     submitRecordValues.addAll(formatSubmitRequirementValues(cd.submitRequirements().values()));
    1371         103 :     return submitRecordValues.stream().collect(Collectors.toList());
    1372             :   }
    1373             : 
    1374             :   @VisibleForTesting
    1375             :   static List<String> formatSubmitRecordValues(List<SubmitRecord> records, Account.Id changeOwner) {
    1376         103 :     List<String> result = new ArrayList<>();
    1377         103 :     for (SubmitRecord rec : records) {
    1378         103 :       result.add(rec.status.name());
    1379         103 :       if (rec.labels == null) {
    1380          13 :         continue;
    1381             :       }
    1382         103 :       for (SubmitRecord.Label label : rec.labels) {
    1383         103 :         String sl = label.status.toString() + ',' + label.label.toLowerCase();
    1384         103 :         result.add(sl);
    1385         103 :         String slc = sl + ',';
    1386         103 :         if (label.appliedBy != null) {
    1387          58 :           result.add(slc + label.appliedBy.get());
    1388          58 :           if (label.appliedBy.equals(changeOwner)) {
    1389          57 :             result.add(slc + ChangeQueryBuilder.OWNER_ACCOUNT_ID.get());
    1390             :           }
    1391             :         }
    1392         103 :       }
    1393         103 :     }
    1394         103 :     return result;
    1395             :   }
    1396             : 
    1397             :   /**
    1398             :    * Generate submit requirement result formats that are compatible with the legacy submit record
    1399             :    * statuses.
    1400             :    */
    1401             :   @VisibleForTesting
    1402             :   static List<String> formatSubmitRequirementValues(Collection<SubmitRequirementResult> srResults) {
    1403         103 :     List<String> result = new ArrayList<>();
    1404         103 :     for (SubmitRequirementResult srResult : srResults) {
    1405           3 :       switch (srResult.status()) {
    1406             :         case SATISFIED:
    1407             :         case OVERRIDDEN:
    1408             :         case FORCED:
    1409           3 :           result.add(
    1410           3 :               SubmitRecord.Label.Status.OK.name()
    1411             :                   + ","
    1412           3 :                   + srResult.submitRequirement().name().toLowerCase());
    1413           3 :           result.add(
    1414           3 :               SubmitRecord.Label.Status.MAY.name()
    1415             :                   + ","
    1416           3 :                   + srResult.submitRequirement().name().toLowerCase());
    1417           3 :           break;
    1418             :         case UNSATISFIED:
    1419           3 :           result.add(
    1420           3 :               SubmitRecord.Label.Status.NEED.name()
    1421             :                   + ","
    1422           3 :                   + srResult.submitRequirement().name().toLowerCase());
    1423           3 :           result.add(
    1424           3 :               SubmitRecord.Label.Status.REJECT.name()
    1425             :                   + ","
    1426           3 :                   + srResult.submitRequirement().name().toLowerCase());
    1427           3 :           break;
    1428             :         case NOT_APPLICABLE:
    1429             :         case ERROR:
    1430           1 :           result.add(
    1431           1 :               SubmitRecord.Label.Status.IMPOSSIBLE.name()
    1432             :                   + ","
    1433           1 :                   + srResult.submitRequirement().name().toLowerCase());
    1434             :       }
    1435           3 :     }
    1436         103 :     return result;
    1437             :   }
    1438             : 
    1439             :   /** Serialized submit requirements, used for pre-populating results. */
    1440         155 :   public static final FieldDef<ChangeData, Iterable<byte[]>> STORED_SUBMIT_REQUIREMENTS =
    1441         155 :       storedOnly("full_submit_requirements")
    1442         155 :           .buildRepeatable(
    1443             :               cd ->
    1444         103 :                   toProtos(
    1445         103 :                       SubmitRequirementProtoConverter.INSTANCE, cd.submitRequirements().values()),
    1446         100 :               (cd, field) -> parseSubmitRequirements(field, cd));
    1447             : 
    1448             :   private static void parseSubmitRequirements(Iterable<byte[]> values, ChangeData out) {
    1449         100 :     out.setSubmitRequirements(
    1450         100 :         StreamSupport.stream(values.spliterator(), false)
    1451         100 :             .map(
    1452             :                 f ->
    1453           2 :                     SubmitRequirementProtoConverter.INSTANCE.fromProto(
    1454           2 :                         Protos.parseUnchecked(
    1455           2 :                             SubmitRequirementProtoConverter.INSTANCE.getParser(), f)))
    1456         100 :             .filter(sr -> !sr.isLegacy())
    1457         100 :             .collect(
    1458         100 :                 ImmutableMap.toImmutableMap(sr -> sr.submitRequirement(), Function.identity())));
    1459         100 :   }
    1460             : 
    1461             :   /**
    1462             :    * All values of all refs that were used in the course of indexing this document.
    1463             :    *
    1464             :    * <p>Emitted as UTF-8 encoded strings of the form {@code project:ref/name:[hex sha]}.
    1465             :    */
    1466         155 :   public static final FieldDef<ChangeData, Iterable<byte[]>> REF_STATE =
    1467         155 :       storedOnly("ref_state")
    1468         155 :           .buildRepeatable(
    1469             :               cd -> {
    1470         103 :                 List<byte[]> result = new ArrayList<>();
    1471         103 :                 cd.getRefStates()
    1472         103 :                     .entries()
    1473         103 :                     .forEach(e -> result.add(e.getValue().toByteArray(e.getKey())));
    1474         103 :                 return result;
    1475             :               },
    1476         100 :               (cd, field) -> cd.setRefStates(RefState.parseStates(field)));
    1477             : 
    1478             :   /**
    1479             :    * All ref wildcard patterns that were used in the course of indexing this document.
    1480             :    *
    1481             :    * <p>Emitted as UTF-8 encoded strings of the form {@code project:ref/name/*}. See {@link
    1482             :    * RefStatePattern} for the pattern format.
    1483             :    */
    1484         155 :   public static final FieldDef<ChangeData, Iterable<byte[]>> REF_STATE_PATTERN =
    1485         155 :       storedOnly("ref_state_pattern")
    1486         155 :           .buildRepeatable(
    1487             :               cd -> {
    1488         103 :                 Change.Id id = cd.getId();
    1489         103 :                 Project.NameKey project = cd.change().getProject();
    1490         103 :                 List<byte[]> result = new ArrayList<>(3);
    1491         103 :                 result.add(
    1492         103 :                     RefStatePattern.create(
    1493             :                             RefNames.REFS_USERS + "*/" + RefNames.EDIT_PREFIX + id + "/*")
    1494         103 :                         .toByteArray(project));
    1495         103 :                 result.add(
    1496         103 :                     RefStatePattern.create(RefNames.refsStarredChangesPrefix(id) + "*")
    1497         103 :                         .toByteArray(allUsers(cd)));
    1498         103 :                 result.add(
    1499         103 :                     RefStatePattern.create(RefNames.refsDraftCommentsPrefix(id) + "*")
    1500         103 :                         .toByteArray(allUsers(cd)));
    1501         103 :                 return result;
    1502             :               },
    1503         100 :               (cd, field) -> cd.setRefStatePatterns(field));
    1504             : 
    1505             :   @Nullable
    1506             :   private static String getTopic(ChangeData cd) {
    1507         103 :     Change c = cd.change();
    1508         103 :     if (c == null) {
    1509           0 :       return null;
    1510             :     }
    1511         103 :     return firstNonNull(c.getTopic(), "");
    1512             :   }
    1513             : 
    1514             :   private static <T> List<byte[]> toProtos(ProtoConverter<?, T> converter, Collection<T> objects) {
    1515         103 :     return objects.stream().map(object -> toProto(converter, object)).collect(toImmutableList());
    1516             :   }
    1517             : 
    1518             :   private static <T> byte[] toProto(ProtoConverter<?, T> converter, T object) {
    1519         103 :     return Protos.toByteArray(converter.toProto(object));
    1520             :   }
    1521             : 
    1522             :   private static <T> List<T> decodeProtos(Iterable<byte[]> raw, ProtoConverter<?, T> converter) {
    1523         100 :     return StreamSupport.stream(raw.spliterator(), false)
    1524         100 :         .map(bytes -> parseProtoFrom(bytes, converter))
    1525         100 :         .collect(toImmutableList());
    1526             :   }
    1527             : 
    1528             :   private static <P extends MessageLite, T> T parseProtoFrom(
    1529             :       byte[] bytes, ProtoConverter<P, T> converter) {
    1530         100 :     P message = Protos.parseUnchecked(converter.getParser(), bytes, 0, bytes.length);
    1531         100 :     return converter.fromProto(message);
    1532             :   }
    1533             : 
    1534             :   private static <T> SchemaFieldDefs.Getter<ChangeData, T> changeGetter(Function<Change, T> func) {
    1535         155 :     return in -> in.change() != null ? func.apply(in.change()) : null;
    1536             :   }
    1537             : 
    1538             :   private static AllUsersName allUsers(ChangeData cd) {
    1539         103 :     return cd.getAllUsersNameForIndexing();
    1540             :   }
    1541             : 
    1542             :   private static String truncateStringValueToMaxTermLength(String str) {
    1543         103 :     return truncateStringValue(str, MAX_TERM_LENGTH);
    1544             :   }
    1545             : 
    1546             :   @VisibleForTesting
    1547             :   static String truncateStringValue(String str, int maxBytes) {
    1548         103 :     if (maxBytes < 0) {
    1549           0 :       throw new IllegalArgumentException("maxBytes < 0 not allowed");
    1550             :     }
    1551             : 
    1552         103 :     if (maxBytes == 0) {
    1553           1 :       return "";
    1554             :     }
    1555             : 
    1556         103 :     if (str.length() > maxBytes) {
    1557           1 :       if (Character.isHighSurrogate(str.charAt(maxBytes - 1))) {
    1558           1 :         str = str.substring(0, maxBytes - 1);
    1559             :       } else {
    1560           1 :         str = str.substring(0, maxBytes);
    1561             :       }
    1562             :     }
    1563         103 :     byte[] strBytes = str.getBytes(UTF_8);
    1564         103 :     if (strBytes.length > maxBytes) {
    1565           1 :       while (maxBytes > 0 && (strBytes[maxBytes] & 0xC0) == 0x80) {
    1566           1 :         maxBytes -= 1;
    1567             :       }
    1568           1 :       if (maxBytes > 0) {
    1569           1 :         if (strBytes.length >= maxBytes && (strBytes[maxBytes - 1] & 0xE0) == 0xC0) {
    1570           0 :           maxBytes -= 1;
    1571             :         }
    1572           1 :         if (strBytes.length >= maxBytes && (strBytes[maxBytes - 1] & 0xF0) == 0xE0) {
    1573           0 :           maxBytes -= 1;
    1574             :         }
    1575           1 :         if (strBytes.length >= maxBytes && (strBytes[maxBytes - 1] & 0xF8) == 0xF0) {
    1576           0 :           maxBytes -= 1;
    1577             :         }
    1578             :       }
    1579           1 :       return new String(Arrays.copyOfRange(strBytes, 0, maxBytes), UTF_8);
    1580             :     }
    1581         103 :     return str;
    1582             :   }
    1583             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750