| // Copyright (C) 2016 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.index.account; |
| |
| import static com.google.common.base.Preconditions.checkState; |
| import static java.util.stream.Collectors.toSet; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.FluentIterable; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.gerrit.common.data.GlobalCapability; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.git.ObjectIds; |
| import com.google.gerrit.index.IndexedField; |
| import com.google.gerrit.index.RefState; |
| import com.google.gerrit.index.SchemaUtil; |
| import com.google.gerrit.server.account.AccountState; |
| import com.google.gerrit.server.account.externalids.ExternalId; |
| import com.google.gerrit.server.config.AllUsersName; |
| import com.google.gerrit.server.config.AllUsersNameProvider; |
| import java.sql.Timestamp; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Locale; |
| import java.util.Objects; |
| import java.util.Set; |
| import org.eclipse.jgit.lib.ObjectId; |
| |
| /** |
| * Secondary index schemas for accounts. |
| * |
| * <p>Note that this class does not override {@link Object#equals(Object)}. It relies on instances |
| * being singletons so that the default (i.e. reference) comparison works. |
| */ |
| public class AccountField { |
| |
| public static final IndexedField<AccountState, Integer> ID_FIELD = |
| IndexedField.<AccountState>integerBuilder("Id") |
| .stored() |
| .required() |
| .build(a -> a.account().id().get()); |
| |
| public static final IndexedField<AccountState, Integer>.SearchSpec ID_FIELD_SPEC = |
| ID_FIELD.integer("id"); |
| |
| public static final IndexedField<AccountState, String> ID_STR_FIELD = |
| IndexedField.<AccountState>stringBuilder("IdStr") |
| .stored() |
| .required() |
| .build(a -> String.valueOf(a.account().id().get())); |
| |
| public static final IndexedField<AccountState, String>.SearchSpec ID_STR_FIELD_SPEC = |
| ID_STR_FIELD.exact("id_str"); |
| |
| /** |
| * External IDs. |
| * |
| * <p>This field includes secondary emails. Use this field only if the current user is allowed to |
| * see secondary emails (requires the {@link GlobalCapability#VIEW_SECONDARY_EMAILS} capability or |
| * the {@link GlobalCapability#MODIFY_ACCOUNT} capability). |
| */ |
| public static final IndexedField<AccountState, Iterable<String>> EXTERNAL_ID_FIELD = |
| IndexedField.<AccountState>iterableStringBuilder("ExternalId") |
| .required() |
| .build(a -> Iterables.transform(a.externalIds(), id -> id.key().get())); |
| |
| public static final IndexedField<AccountState, Iterable<String>>.SearchSpec |
| EXTERNAL_ID_FIELD_SPEC = EXTERNAL_ID_FIELD.exact("external_id"); |
| |
| /** |
| * Fuzzy prefix match on name and email parts. |
| * |
| * <p>This field includes parts from the secondary emails. Use this field only if the current user |
| * is allowed to see secondary emails (requires requires the {@link |
| * GlobalCapability#VIEW_SECONDARY_EMAILS} capability or the {@link |
| * GlobalCapability#MODIFY_ACCOUNT} capability). |
| * |
| * <p>Use the {@link AccountField#NAME_PART_NO_SECONDARY_EMAIL_SPEC} if the current user can't see |
| * secondary emails. |
| */ |
| public static final IndexedField<AccountState, Iterable<String>> NAME_PART_FIELD = |
| IndexedField.<AccountState>iterableStringBuilder("FullNameAndAllEmailsParts") |
| .description("Full name, all linked emails and their parts (split at special characters)") |
| .required() |
| .build(a -> getNameParts(a, Iterables.transform(a.externalIds(), ExternalId::email))); |
| |
| public static final IndexedField<AccountState, Iterable<String>>.SearchSpec NAME_PART_SPEC = |
| NAME_PART_FIELD.prefix("name"); |
| |
| /** |
| * Fuzzy prefix match on name and preferred email parts. Parts of secondary emails are not |
| * included. |
| */ |
| public static final IndexedField<AccountState, Iterable<String>> |
| NAME_PART_NO_SECONDARY_EMAIL_FIELD = |
| IndexedField.<AccountState>iterableStringBuilder("FullNameAndPreferredEmailParts") |
| .description( |
| "Full name, preferred emails and its parts (split at special characters)") |
| .required() |
| .build(a -> getNameParts(a, Arrays.asList(a.account().preferredEmail()))); |
| |
| public static final IndexedField<AccountState, Iterable<String>>.SearchSpec |
| NAME_PART_NO_SECONDARY_EMAIL_SPEC = NAME_PART_NO_SECONDARY_EMAIL_FIELD.prefix("name2"); |
| |
| public static final IndexedField<AccountState, String> FULL_NAME_FIELD = |
| IndexedField.<AccountState>stringBuilder("FullName").build(a -> a.account().fullName()); |
| |
| public static final IndexedField<AccountState, String>.SearchSpec FULL_NAME_SPEC = |
| FULL_NAME_FIELD.exact("full_name"); |
| |
| public static final IndexedField<AccountState, String> ACTIVE_FIELD = |
| IndexedField.<AccountState>stringBuilder("Active") |
| .required() |
| .build(a -> a.account().isActive() ? "1" : "0"); |
| |
| public static final IndexedField<AccountState, String>.SearchSpec ACTIVE_FIELD_SPEC = |
| ACTIVE_FIELD.exact("inactive"); |
| /** |
| * All emails (preferred email + secondary emails). Use this field only if the current user is |
| * allowed to see secondary emails (requires the 'Modify Account' capability). |
| * |
| * <p>Use the {@link AccountField#PREFERRED_EMAIL_LOWER_CASE_SPEC} if the current user can't see |
| * secondary emails. |
| */ |
| public static final IndexedField<AccountState, Iterable<String>> EMAIL_FIELD = |
| IndexedField.<AccountState>iterableStringBuilder("Email") |
| .required() |
| .build( |
| a -> |
| FluentIterable.from(a.externalIds()) |
| .transform(ExternalId::email) |
| .append(Collections.singleton(a.account().preferredEmail())) |
| .filter(Objects::nonNull) |
| .transform(String::toLowerCase) |
| .toSet()); |
| |
| public static final IndexedField<AccountState, Iterable<String>>.SearchSpec EMAIL_SPEC = |
| EMAIL_FIELD.prefix("email"); |
| |
| public static final IndexedField<AccountState, String> PREFERRED_EMAIL_LOWER_CASE_FIELD = |
| IndexedField.<AccountState>stringBuilder("PreferredEmailLowerCase") |
| .build( |
| a -> { |
| String preferredEmail = a.account().preferredEmail(); |
| return preferredEmail != null ? preferredEmail.toLowerCase(Locale.US) : null; |
| }); |
| |
| public static final IndexedField<AccountState, String>.SearchSpec |
| PREFERRED_EMAIL_LOWER_CASE_SPEC = PREFERRED_EMAIL_LOWER_CASE_FIELD.prefix("preferredemail"); |
| |
| public static final IndexedField<AccountState, String> PREFERRED_EMAIL_EXACT_FIELD = |
| IndexedField.<AccountState>stringBuilder("PreferredEmail") |
| .build(a -> a.account().preferredEmail()); |
| |
| public static final IndexedField<AccountState, String>.SearchSpec PREFERRED_EMAIL_EXACT_SPEC = |
| PREFERRED_EMAIL_EXACT_FIELD.exact("preferredemail_exact"); |
| |
| // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant |
| public static final IndexedField<AccountState, Timestamp> REGISTERED_FIELD = |
| IndexedField.<AccountState>timestampBuilder("Registered") |
| .required() |
| .build(a -> Timestamp.from(a.account().registeredOn())); |
| |
| public static final IndexedField<AccountState, Timestamp>.SearchSpec REGISTERED_SPEC = |
| REGISTERED_FIELD.timestamp("registered"); |
| |
| public static final IndexedField<AccountState, String> USERNAME_FIELD = |
| IndexedField.<AccountState>stringBuilder("Username") |
| .build(a -> a.userName().map(String::toLowerCase).orElse("")); |
| |
| public static final IndexedField<AccountState, String>.SearchSpec USERNAME_SPEC = |
| USERNAME_FIELD.exact("username"); |
| |
| public static final IndexedField<AccountState, Iterable<String>> WATCHED_PROJECT_FIELD = |
| IndexedField.<AccountState>iterableStringBuilder("WatchedProject") |
| .build( |
| a -> |
| FluentIterable.from(a.projectWatches().keySet()) |
| .transform(k -> k.project().get()) |
| .toSet()); |
| |
| public static final IndexedField<AccountState, Iterable<String>>.SearchSpec WATCHED_PROJECT_SPEC = |
| WATCHED_PROJECT_FIELD.exact("watchedproject"); |
| |
| /** |
| * All values of all refs that were used in the course of indexing this document, except the |
| * refs/meta/external-ids notes branch which is handled specially (see {@link |
| * #EXTERNAL_ID_STATE_SPEC}). |
| * |
| * <p>Emitted as UTF-8 encoded strings of the form {@code project:ref/name:[hex sha]}. |
| */ |
| public static final IndexedField<AccountState, Iterable<byte[]>> REF_STATE_FIELD = |
| IndexedField.<AccountState>iterableByteArrayBuilder("RefState") |
| .stored() |
| .required() |
| .build( |
| a -> { |
| if (a.account().metaId() == null) { |
| return ImmutableList.of(); |
| } |
| |
| return ImmutableList.of( |
| RefState.create( |
| RefNames.refsUsers(a.account().id()), |
| ObjectId.fromString(a.account().metaId())) |
| // We use the default AllUsers name to avoid having to pass around that |
| // variable just for indexing. |
| // This field is only used for staleness detection which will discover the |
| // default name and replace it with the actually configured name. |
| .toByteArray(new AllUsersName(AllUsersNameProvider.DEFAULT))); |
| }); |
| |
| public static final IndexedField<AccountState, Iterable<byte[]>>.SearchSpec REF_STATE_SPEC = |
| REF_STATE_FIELD.storedOnly("ref_state"); |
| |
| /** |
| * All note values of all external IDs that were used in the course of indexing this document. |
| * |
| * <p>Emitted as UTF-8 encoded strings of the form {@code [hex sha of external ID]:[hex sha of |
| * note blob]}, or with other words {@code [note ID]:[note data ID]}. |
| */ |
| public static final IndexedField<AccountState, Iterable<byte[]>> EXTERNAL_ID_STATE_FIELD = |
| IndexedField.<AccountState>iterableByteArrayBuilder("ExternalIdState") |
| .stored() |
| .required() |
| .build( |
| a -> |
| a.externalIds().stream() |
| .filter(e -> e.blobId() != null) |
| .map(AccountField::serializeExternalId) |
| .collect(toSet())); |
| |
| public static final IndexedField<AccountState, Iterable<byte[]>>.SearchSpec |
| EXTERNAL_ID_STATE_SPEC = EXTERNAL_ID_STATE_FIELD.storedOnly("external_id_state"); |
| |
| @VisibleForTesting |
| public static byte[] serializeExternalId(ExternalId extId) { |
| checkState(extId.blobId() != null, "Missing blobId in external ID %s", extId.key().get()); |
| byte[] b = new byte[2 * ObjectIds.STR_LEN + 1]; |
| extId.key().sha1().copyTo(b, 0); |
| b[ObjectIds.STR_LEN] = ':'; |
| extId.blobId().copyTo(b, ObjectIds.STR_LEN + 1); |
| return b; |
| } |
| |
| private static final Set<String> getNameParts(AccountState a, Iterable<String> emails) { |
| String fullName = a.account().fullName(); |
| Set<String> parts = SchemaUtil.getNameParts(fullName, emails); |
| |
| // Additional values not currently added by getPersonParts. |
| // TODO(dborowitz): Move to getPersonParts and remove this hack. |
| if (fullName != null) { |
| parts.add(fullName.toLowerCase(Locale.US)); |
| } |
| return parts; |
| } |
| |
| private AccountField() {} |
| } |