com.google.gerrit.server.account.AccountsUpdate Maven / Gradle / Ivy
// Copyright (C) 2017 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.account;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.Account;
import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.inject.BindingAnnotation;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.PersonIdent;
/**
 * Creates and updates accounts.
 *
 * This interface should be used for all account updates. See {@link AccountDelta} for what can
 * be updated.
 *
 * 
For creating a new account a new account ID can be retrieved from {@link
 * Sequences#nextAccountId()}.
 *
 * 
See the implementing classes for more information.
 */
public abstract class AccountsUpdate {
  /** Loader for {@link AccountsUpdate}s. */
  public interface AccountsUpdateLoader {
    /**
     * Creates an {@code AccountsUpdate} which uses the identity of the specified user as author for
     * all commits related to accounts. The server identity will be used as committer.
     *
     * 
Note: Please use this method with care and consider using the {@link
     * com.google.gerrit.server.UserInitiated} annotation on the provider of an {@code
     * AccountsUpdate} instead.
     *
     * @param currentUser the user to which modifications should be attributed
     */
    AccountsUpdate create(IdentifiedUser currentUser);
    /**
     * Creates an {@code AccountsUpdate} which uses the server identity as author and committer for
     * all commits related to accounts.
     *
     * 
Note: Please use this method with care and consider using the {@link
     * com.google.gerrit.server.ServerInitiated} annotation on the provider of an {@code
     * AccountsUpdate} instead.
     */
    AccountsUpdate createWithServerIdent();
    @BindingAnnotation
    @Target({FIELD, PARAMETER, METHOD})
    @Retention(RUNTIME)
    @interface WithReindex {}
    @BindingAnnotation
    @Target({FIELD, PARAMETER, METHOD})
    @Retention(RUNTIME)
    @interface NoReindex {}
  }
  /** Data holder for the set of arguments required to update an account. Used for batch updates. */
  public static class UpdateArguments {
    public final String message;
    public final Account.Id accountId;
    public final ConfigureDeltaFromStateAndContext configureDelta;
    public UpdateArguments(
        String message, Account.Id accountId, ConfigureStatelessDelta configureDelta) {
      this(message, accountId, withContext(configureDelta));
    }
    public UpdateArguments(
        String message, Account.Id accountId, ConfigureDeltaFromState configureDelta) {
      this(message, accountId, withContext(configureDelta));
    }
    public UpdateArguments(
        String message, Account.Id accountId, ConfigureDeltaFromStateAndContext configureDelta) {
      this.message = message;
      this.accountId = accountId;
      this.configureDelta = configureDelta;
    }
    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("message", message)
          .add("accountId", accountId)
          .toString();
    }
  }
  /**
   * Storage readers/writers which are accessible through {@link ConfigureDeltaFromStateAndContext}.
   *
   * 
If you need to perform extra reads/writes during the update, prefer using these, to avoid
   * mishmash between different storage systems where multiple ones are supported. If you need an
   * accessor which is not yet here, please do add it.
   */
  public interface InUpdateStorageAccessors {
    ExternalIds externalIdsReader();
  }
  /**
   * The most basic interface for updating the account delta, providing no state nor context.
   *
   * 
Account updates that do not need to know the current account state, nor read/write any extra
   * data during the update, should use this interface.
   *
   * 
If the above capabilities are needed, use {@link ConfigureDeltaFromState} or {@link
   * ConfigureDeltaFromStateAndContext} instead.
   */
  @FunctionalInterface
  public interface ConfigureStatelessDelta {
    /**
     * Configures an {@link com.google.gerrit.server.account.AccountDelta.Builder} with changes to
     * the account.
     *
     * @param delta the changes to be applied
     */
    void configure(AccountDelta.Builder delta);
  }
  /**
   * Interface for updating the account delta, providing the current state.
   *
   * 
Account updates are commonly performed by evaluating the current account state and creating
   * a delta to be applied to it in a later step. This is done by implementing this interface.
   *
   * 
If the current account state is not needed, use {@link ConfigureStatelessDelta} instead.
   * Alternatively, if you need to perform extra storage reads/writes during the update, use {@link
   * ConfigureDeltaFromStateAndContext}.
   */
  @FunctionalInterface
  public interface ConfigureDeltaFromState {
    /**
     * Receives the current {@link AccountState} (which is immutable) and configures an {@link
     * com.google.gerrit.server.account.AccountDelta.Builder} with changes to the account.
     *
     * @param accountState the state of the account that is being updated
     * @param delta the changes to be applied
     */
    void configure(AccountState accountState, AccountDelta.Builder delta) throws IOException;
  }
  /**
   * Interface for updating the account delta, providing the current state and storage accessors.
   *
   * 
Account updates which need to perform extra storage reads/writes during the update, should
   * use this interface.
   *
   * 
If storage accessors are not needed, use {@link ConfigureStatelessDelta} or {@link
   * ConfigureDeltaFromState} instead.
   */
  @FunctionalInterface
  public interface ConfigureDeltaFromStateAndContext {
    /**
     * Receives {@link InUpdateStorageAccessors} for reading/modifying data on the respective
     * storage system, as well as the current {@link AccountState} (which is immutable). Configures
     * an {@link com.google.gerrit.server.account.AccountDelta.Builder} with changes to the account.
     *
     * @param inUpdateStorageAccessors storage accessor which have the context of the update (e.g.,
     *     use the same storage system as the calling updater)
     * @param accountState the state of the account that is being updated
     * @param delta the changes to be applied
     * @see InUpdateStorageAccessors
     */
    void configure(
        InUpdateStorageAccessors inUpdateStorageAccessors,
        AccountState accountState,
        AccountDelta.Builder delta)
        throws IOException;
  }
  /** Returns an instance that runs all specified consumers. */
  public static ConfigureStatelessDelta joinDeltaConfigures(
      List deltaConfigures) {
    return (update) -> deltaConfigures.forEach(c -> c.configure(update));
  }
  protected final PersonIdent committerIdent;
  protected final PersonIdent authorIdent;
  protected final Optional currentUser;
  private final InUpdateStorageAccessors inUpdateStorageAccessors;
  @SuppressWarnings("Convert2Lambda")
  protected AccountsUpdate(
      PersonIdent serverIdent, Optional user, ExternalIds externalIdsReader) {
    this.currentUser = user;
    this.committerIdent = serverIdent;
    this.authorIdent = createPersonIdent(serverIdent, user);
    this.inUpdateStorageAccessors =
        new InUpdateStorageAccessors() {
          @Override
          public ExternalIds externalIdsReader() {
            return externalIdsReader;
          }
        };
  }
  /**
   * Perform update provided. Some implementations of AccountsUpdate have different behaviours for
   * account updates initiated by the users and those initiated by account management.
   */
  @CanIgnoreReturnValue
  public ImmutableList> updateForUserManagementRequests(
      List updates) throws ConfigInvalidException, IOException {
    return executeUpdates(updates);
  }
  /**
   * Perform update provided. Some implementations of AccountsUpdate have different behaviours for
   * account updates initiated by the users and those initiated by account management.
   */
  @CanIgnoreReturnValue
  public Optional updateForUserManagementRequests(
      String message, Account.Id accountId, ConfigureDeltaFromStateAndContext configureDelta)
      throws IOException, ConfigInvalidException {
    return updateForUserManagementRequests(
            ImmutableList.of(new UpdateArguments(message, accountId, configureDelta)))
        .get(0);
  }
  /**
   * Perform update provided. Some implementations of AccountsUpdate have different behaviours for
   * account updates initiated by the users and those initiated by account management.
   */
  @CanIgnoreReturnValue
  public Optional updateForUserManagementRequests(
      String message, Account.Id accountId, ConfigureStatelessDelta configureDelta)
      throws IOException, ConfigInvalidException {
    return updateForUserManagementRequests(
            ImmutableList.of(new UpdateArguments(message, accountId, configureDelta)))
        .get(0);
  }
  /**
   * Perform update provided. Some implementations of AccountsUpdate have different behaviours for
   * account updates initiated by the users and those initiated by account management.
   */
  @CanIgnoreReturnValue
  public Optional updateForUserManagementRequests(
      String message, Account.Id accountId, ConfigureDeltaFromState configureDelta)
      throws IOException, ConfigInvalidException {
    return updateForUserManagementRequests(
            ImmutableList.of(new UpdateArguments(message, accountId, configureDelta)))
        .get(0);
  }
  /**
   * Inserts a new account.
   *
   * If the current account state is not needed, use {@link #insert(String, Account.Id,
   * ConfigureStatelessDelta)} instead.
   *
   * @param message commit message for the account creation, must not be {@code null or empty}
   * @param accountId ID of the new account
   * @param init to populate the new account
   * @return the newly created account
   * @throws DuplicateKeyException if the account already exists
   * @throws IOException if creating the user branch fails due to an IO error
   * @throws ConfigInvalidException if any of the account fields has an invalid value
   */
  @CanIgnoreReturnValue
  public abstract AccountState insert(
      String message, Account.Id accountId, ConfigureDeltaFromStateAndContext init)
      throws IOException, ConfigInvalidException;
  /**
   * Like {@link #insert(String, Account.Id, ConfigureDeltaFromStateAndContext)}, but using {@link
   * ConfigureDeltaFromState} instead. I.e. the update does not require any extra storage
   * reads/writes, except for the current {@link AccountState}.
   *
   * 
If the current account state is not needed as well, use {@link #insert(String, Account.Id,
   * ConfigureStatelessDelta)} instead.
   */
  @CanIgnoreReturnValue
  public final AccountState insert(
      String message, Account.Id accountId, ConfigureDeltaFromState init)
      throws IOException, ConfigInvalidException {
    return insert(message, accountId, withContext(init));
  }
  /**
   * Like {@link #insert(String, Account.Id, ConfigureDeltaFromStateAndContext)}, but using {@link
   * ConfigureStatelessDelta} instead. I.e. the update does not depend on the current account state,
   * nor requires any extra storage reads/writes.
   */
  @CanIgnoreReturnValue
  public final AccountState insert(
      String message, Account.Id accountId, ConfigureStatelessDelta init)
      throws IOException, ConfigInvalidException {
    return insert(message, accountId, withContext(init));
  }
  /**
   * Gets the account and updates it atomically.
   *
   * 
Changing the registration date of an account is not supported.
   *
   * 
If the current account state is not needed, use {@link #update(String, Account.Id,
   * ConfigureStatelessDelta)} instead.
   *
   * @param message commit message for the account update, must not be {@code null or empty}
   * @param accountId ID of the account
   * @param configureDelta deltaBuilder to update the account, only invoked if the account exists
   * @return the updated account, {@link Optional#empty} if the account doesn't exist
   * @throws IOException if updating the user branch fails due to an IO error
   * @throws LockFailureException if updating the user branch still fails due to concurrent updates
   *     after the retry timeout exceeded
   * @throws ConfigInvalidException if any of the account fields has an invalid value
   */
  @CanIgnoreReturnValue
  public final Optional update(
      String message, Account.Id accountId, ConfigureDeltaFromStateAndContext configureDelta)
      throws IOException, ConfigInvalidException {
    return updateBatch(ImmutableList.of(new UpdateArguments(message, accountId, configureDelta)))
        .get(0);
  }
  /**
   * Like {@link #update(String, Account.Id, ConfigureDeltaFromStateAndContext)}, but using {@link
   * ConfigureDeltaFromState} instead. I.e. the update does not require any extra storage
   * reads/writes, except for the current {@link AccountState}.
   *
   * If the current account state is not needed as well, use {@link #update(String, Account.Id,
   * ConfigureStatelessDelta)} instead.
   */
  @CanIgnoreReturnValue
  public final Optional update(
      String message, Account.Id accountId, ConfigureDeltaFromState configureDelta)
      throws IOException, ConfigInvalidException {
    return update(message, accountId, withContext(configureDelta));
  }
  /**
   * Like {@link #update(String, Account.Id, ConfigureDeltaFromStateAndContext)} , but using {@link
   * ConfigureStatelessDelta} instead. I.e. the update does not depend on the current account state,
   * nor requires any extra storage reads/writes.
   */
  @CanIgnoreReturnValue
  public final Optional update(
      String message, Account.Id accountId, ConfigureStatelessDelta configureDelta)
      throws IOException, ConfigInvalidException {
    return update(message, accountId, withContext(configureDelta));
  }
  /**
   * Updates multiple different accounts atomically. This will only store a single new value (aka
   * set of all external IDs of the host) in the external ID cache, which is important for storage
   * economy. All {@code updates} must be for different accounts.
   *
   * NOTE on error handling: Since updates are executed in multiple stages, with some stages
   * resulting from the union of all individual updates, we cannot point to the update that caused
   * the error. Callers should be aware that a single "update of death" (or a set of updates that
   * together have this property) will always prevent the entire batch from being executed.
   */
  @CanIgnoreReturnValue
  public final ImmutableList> updateBatch(List updates)
      throws IOException, ConfigInvalidException {
    checkArgument(
        updates.stream().map(u -> u.accountId.get()).distinct().count() == updates.size(),
        "updates must all be for different accounts");
    return executeUpdates(updates);
  }
  /**
   * Deletes all the account state data.
   *
   * @param message commit message for the account update, must not be {@code null or empty}
   * @param accountId ID of the account
   * @throws IOException if updating the user branch fails due to an IO error
   * @throws ConfigInvalidException if any of the account fields has an invalid value
   */
  public abstract void delete(String message, Account.Id accountId)
      throws IOException, ConfigInvalidException;
  @VisibleForTesting // productionVisibility: protected
  public abstract ImmutableList> executeUpdates(
      List updates) throws ConfigInvalidException, IOException;
  /**
   * Intended for internal usage only. This is public because some implementations are calling this
   * method for other instances.
   */
  @UsedAt(UsedAt.Project.GOOGLE)
  public final void configureDelta(
      ConfigureDeltaFromStateAndContext configureDelta,
      AccountState accountState,
      AccountDelta.Builder delta)
      throws IOException {
    configureDelta.configure(inUpdateStorageAccessors, accountState, delta);
  }
  private static ConfigureDeltaFromStateAndContext withContext(
      ConfigureDeltaFromState configureDelta) {
    return (unusedAccessors, accountState, delta) -> configureDelta.configure(accountState, delta);
  }
  private static ConfigureDeltaFromStateAndContext withContext(
      ConfigureStatelessDelta configureDelta) {
    return (unusedAccessors, unusedAccountState, delta) -> configureDelta.configure(delta);
  }
  private static PersonIdent createPersonIdent(
      PersonIdent serverIdent, Optional user) {
    return user.isPresent() ? user.get().newCommitterIdent(serverIdent) : serverIdent;
  }
}