LCOV - code coverage report
Current view: top level - server/account - AccountConfig.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 99 117 84.6 %
Date: 2022-11-19 15:00:39 Functions: 19 21 90.5 %

          Line data    Source code
       1             : // Copyright (C) 2017 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.account;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkState;
      18             : import static java.util.Objects.requireNonNull;
      19             : 
      20             : import com.google.common.base.Strings;
      21             : import com.google.common.collect.ImmutableList;
      22             : import com.google.common.collect.ImmutableMap;
      23             : import com.google.common.collect.ImmutableSet;
      24             : import com.google.gerrit.entities.Account;
      25             : import com.google.gerrit.entities.NotifyConfig.NotifyType;
      26             : import com.google.gerrit.entities.RefNames;
      27             : import com.google.gerrit.exceptions.DuplicateKeyException;
      28             : import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
      29             : import com.google.gerrit.server.account.externalids.ExternalIds;
      30             : import com.google.gerrit.server.config.AllUsersName;
      31             : import com.google.gerrit.server.config.CachedPreferences;
      32             : import com.google.gerrit.server.git.ValidationError;
      33             : import com.google.gerrit.server.git.meta.MetaDataUpdate;
      34             : import com.google.gerrit.server.git.meta.VersionedMetaData;
      35             : import com.google.gerrit.server.util.time.TimeUtil;
      36             : import java.io.IOException;
      37             : import java.time.Instant;
      38             : import java.util.ArrayList;
      39             : import java.util.HashMap;
      40             : import java.util.List;
      41             : import java.util.Map;
      42             : import java.util.Optional;
      43             : import java.util.Set;
      44             : import org.eclipse.jgit.errors.ConfigInvalidException;
      45             : import org.eclipse.jgit.lib.CommitBuilder;
      46             : import org.eclipse.jgit.lib.Config;
      47             : import org.eclipse.jgit.lib.ObjectId;
      48             : import org.eclipse.jgit.lib.PersonIdent;
      49             : import org.eclipse.jgit.lib.Ref;
      50             : import org.eclipse.jgit.lib.Repository;
      51             : import org.eclipse.jgit.revwalk.RevCommit;
      52             : import org.eclipse.jgit.revwalk.RevSort;
      53             : 
      54             : /**
      55             :  * Reads/writes account data from/to a user branch in the {@code All-Users} repository.
      56             :  *
      57             :  * <p>This is the low-level API for account creation and account updates. Most callers should use
      58             :  * {@link AccountsUpdate} for creating and updating accounts.
      59             :  *
      60             :  * <p>This class can read/write account properties, preferences (general, diff and edit preferences)
      61             :  * and project watches.
      62             :  *
      63             :  * <p>The following files are read/written:
      64             :  *
      65             :  * <ul>
      66             :  *   <li>'account.config': Contains the account properties. Parsing and writing it is delegated to
      67             :  *       {@link AccountProperties}.
      68             :  *   <li>'preferences.config': Contains the preferences. Parsing and writing it is delegated to
      69             :  *       {@link StoredPreferences}.
      70             :  *   <li>'account.config': Contains the project watches. Parsing and writing it is delegated to
      71             :  *       {@link ProjectWatches}.
      72             :  * </ul>
      73             :  *
      74             :  * <p>The commit date of the first commit on the user branch is used as registration date of the
      75             :  * account. The first commit may be an empty commit (since all config files are optional).
      76             :  */
      77             : public class AccountConfig extends VersionedMetaData implements ValidationError.Sink {
      78             :   private final Account.Id accountId;
      79             :   private final AllUsersName allUsersName;
      80             :   private final Repository repo;
      81             :   private final String ref;
      82             : 
      83             :   private Optional<AccountProperties> loadedAccountProperties;
      84             :   private Optional<ObjectId> externalIdsRev;
      85             :   private ProjectWatches projectWatches;
      86             :   private StoredPreferences preferences;
      87         151 :   private Optional<AccountDelta> accountDelta = Optional.empty();
      88             :   private List<ValidationError> validationErrors;
      89             : 
      90         151 :   public AccountConfig(Account.Id accountId, AllUsersName allUsersName, Repository allUsersRepo) {
      91         151 :     this.accountId = requireNonNull(accountId, "accountId");
      92         151 :     this.allUsersName = requireNonNull(allUsersName, "allUsersName");
      93         151 :     this.repo = requireNonNull(allUsersRepo, "allUsersRepo");
      94         151 :     this.ref = RefNames.refsUsers(accountId);
      95         151 :   }
      96             : 
      97             :   @Override
      98             :   protected String getRefName() {
      99         151 :     return ref;
     100             :   }
     101             : 
     102             :   public AccountConfig load() throws IOException, ConfigInvalidException {
     103         151 :     load(allUsersName, repo);
     104         151 :     return this;
     105             :   }
     106             : 
     107             :   public AccountConfig load(ObjectId rev) throws IOException, ConfigInvalidException {
     108         151 :     load(allUsersName, repo, rev);
     109         151 :     return this;
     110             :   }
     111             : 
     112             :   /**
     113             :    * Get the loaded account.
     114             :    *
     115             :    * @return the loaded account, {@link Optional#empty()} if load didn't find the account because it
     116             :    *     doesn't exist
     117             :    * @throws IllegalStateException if the account was not loaded yet
     118             :    */
     119             :   public Optional<Account> getLoadedAccount() {
     120         151 :     checkLoaded();
     121         151 :     return loadedAccountProperties.map(AccountProperties::getAccount);
     122             :   }
     123             : 
     124             :   /**
     125             :    * Returns the revision of the {@code refs/meta/external-ids} branch.
     126             :    *
     127             :    * <p>This revision can be used to load the external IDs of the loaded account lazily via {@link
     128             :    * ExternalIds#byAccount(com.google.gerrit.entities.Account.Id, ObjectId)}.
     129             :    *
     130             :    * @return revision of the {@code refs/meta/external-ids} branch, {@link Optional#empty()} if no
     131             :    *     {@code refs/meta/external-ids} branch exists
     132             :    */
     133             :   public Optional<ObjectId> getExternalIdsRev() {
     134         151 :     checkLoaded();
     135         151 :     return externalIdsRev;
     136             :   }
     137             : 
     138             :   /**
     139             :    * Get the project watches of the loaded account.
     140             :    *
     141             :    * @return the project watches of the loaded account
     142             :    */
     143             :   public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() {
     144         151 :     checkLoaded();
     145         151 :     return projectWatches.getProjectWatches();
     146             :   }
     147             : 
     148             :   /**
     149             :    * Sets the account. This means the loaded account will be overwritten with the given account.
     150             :    *
     151             :    * <p>Changing the registration date of an account is not supported.
     152             :    *
     153             :    * @param account account that should be set
     154             :    * @throws IllegalStateException if the account was not loaded yet
     155             :    */
     156             :   public AccountConfig setAccount(Account account) {
     157           0 :     checkLoaded();
     158           0 :     this.loadedAccountProperties =
     159           0 :         Optional.of(
     160           0 :             new AccountProperties(account.id(), account.registeredOn(), new Config(), null));
     161           0 :     this.accountDelta =
     162           0 :         Optional.of(
     163           0 :             AccountDelta.builder()
     164           0 :                 .setActive(account.isActive())
     165           0 :                 .setFullName(account.fullName())
     166           0 :                 .setDisplayName(account.displayName())
     167           0 :                 .setPreferredEmail(account.preferredEmail())
     168           0 :                 .setStatus(account.status())
     169           0 :                 .build());
     170           0 :     return this;
     171             :   }
     172             : 
     173             :   /**
     174             :    * Creates a new account.
     175             :    *
     176             :    * @return the new account
     177             :    * @throws DuplicateKeyException if the user branch already exists
     178             :    */
     179             :   public Account getNewAccount() throws DuplicateKeyException {
     180           0 :     return getNewAccount(TimeUtil.now());
     181             :   }
     182             : 
     183             :   /**
     184             :    * Creates a new account.
     185             :    *
     186             :    * @return the new account
     187             :    * @throws DuplicateKeyException if the user branch already exists
     188             :    */
     189             :   Account getNewAccount(Instant registeredOn) throws DuplicateKeyException {
     190         151 :     checkLoaded();
     191         151 :     if (revision != null) {
     192           0 :       throw new DuplicateKeyException(String.format("account %s already exists", accountId));
     193             :     }
     194         151 :     this.loadedAccountProperties =
     195         151 :         Optional.of(new AccountProperties(accountId, registeredOn, new Config(), null));
     196         151 :     return loadedAccountProperties.map(AccountProperties::getAccount).get();
     197             :   }
     198             : 
     199             :   public AccountConfig setAccountDelta(AccountDelta accountDelta) {
     200         151 :     this.accountDelta = Optional.of(accountDelta);
     201         151 :     return this;
     202             :   }
     203             : 
     204             :   /**
     205             :    * Returns the content of the {@code preferences.config} file wrapped as {@link
     206             :    * CachedPreferences}.
     207             :    */
     208             :   CachedPreferences asCachedPreferences() {
     209         151 :     checkLoaded();
     210         151 :     return CachedPreferences.fromConfig(preferences.getRaw());
     211             :   }
     212             : 
     213             :   @Override
     214             :   protected void onLoad() throws IOException, ConfigInvalidException {
     215         151 :     if (revision != null) {
     216         151 :       rw.reset();
     217         151 :       rw.markStart(revision);
     218         151 :       rw.sort(RevSort.REVERSE);
     219         151 :       Instant registeredOn = Instant.ofEpochMilli(rw.next().getCommitTime() * 1000L);
     220             : 
     221         151 :       Config accountConfig = readConfig(AccountProperties.ACCOUNT_CONFIG);
     222         151 :       loadedAccountProperties =
     223         151 :           Optional.of(new AccountProperties(accountId, registeredOn, accountConfig, revision));
     224             : 
     225         151 :       projectWatches = new ProjectWatches(accountId, readConfig(ProjectWatches.WATCH_CONFIG), this);
     226             : 
     227         151 :       preferences =
     228             :           new StoredPreferences(
     229             :               accountId,
     230         151 :               readConfig(StoredPreferences.PREFERENCES_CONFIG),
     231         151 :               StoredPreferences.readDefaultConfig(allUsersName, repo),
     232             :               this);
     233             : 
     234         151 :       projectWatches.parse();
     235         151 :       preferences.parse();
     236         151 :     } else {
     237         151 :       loadedAccountProperties = Optional.empty();
     238             : 
     239         151 :       projectWatches = new ProjectWatches(accountId, new Config(), this);
     240             : 
     241         151 :       preferences =
     242             :           new StoredPreferences(
     243             :               accountId,
     244             :               new Config(),
     245         151 :               StoredPreferences.readDefaultConfig(allUsersName, repo),
     246             :               this);
     247             :     }
     248             : 
     249         151 :     Ref externalIdsRef = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
     250         151 :     externalIdsRev = Optional.ofNullable(externalIdsRef).map(Ref::getObjectId);
     251         151 :   }
     252             : 
     253             :   @Override
     254             :   public RevCommit commit(MetaDataUpdate update) throws IOException {
     255         151 :     RevCommit c = super.commit(update);
     256         151 :     loadedAccountProperties.get().setMetaId(c);
     257         151 :     return c;
     258             :   }
     259             : 
     260             :   @Override
     261             :   protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
     262         151 :     checkLoaded();
     263             : 
     264         151 :     if (!loadedAccountProperties.isPresent()) {
     265           0 :       return false;
     266             :     }
     267             : 
     268         151 :     if (revision != null) {
     269          33 :       if (Strings.isNullOrEmpty(commit.getMessage())) {
     270           3 :         commit.setMessage("Update account\n");
     271             :       }
     272             :     } else {
     273         151 :       if (Strings.isNullOrEmpty(commit.getMessage())) {
     274           0 :         commit.setMessage("Create account\n");
     275             :       }
     276             : 
     277         151 :       Instant registeredOn = loadedAccountProperties.get().getRegisteredOn();
     278         151 :       commit.setAuthor(new PersonIdent(commit.getAuthor(), registeredOn));
     279         151 :       commit.setCommitter(new PersonIdent(commit.getCommitter(), registeredOn));
     280             :     }
     281             : 
     282         151 :     saveAccount();
     283         151 :     saveProjectWatches();
     284         151 :     savePreferences();
     285             : 
     286         151 :     accountDelta = Optional.empty();
     287             : 
     288         151 :     return true;
     289             :   }
     290             : 
     291             :   private void saveAccount() throws IOException {
     292         151 :     if (accountDelta.isPresent()) {
     293         151 :       saveConfig(
     294         151 :           AccountProperties.ACCOUNT_CONFIG, loadedAccountProperties.get().save(accountDelta.get()));
     295             :     }
     296         151 :   }
     297             : 
     298             :   private void saveProjectWatches() throws IOException {
     299         151 :     if (accountDelta.isPresent()
     300         151 :         && (!accountDelta.get().getDeletedProjectWatches().isEmpty()
     301         151 :             || !accountDelta.get().getUpdatedProjectWatches().isEmpty())) {
     302          16 :       Map<ProjectWatchKey, Set<NotifyType>> newProjectWatches =
     303          16 :           new HashMap<>(projectWatches.getProjectWatches());
     304          16 :       accountDelta.get().getDeletedProjectWatches().forEach(newProjectWatches::remove);
     305          16 :       accountDelta.get().getUpdatedProjectWatches().forEach(newProjectWatches::put);
     306          16 :       saveConfig(ProjectWatches.WATCH_CONFIG, projectWatches.save(newProjectWatches));
     307             :     }
     308         151 :   }
     309             : 
     310             :   private void savePreferences() throws IOException, ConfigInvalidException {
     311         151 :     if (!accountDelta.isPresent()
     312         151 :         || (!accountDelta.get().getGeneralPreferences().isPresent()
     313         151 :             && !accountDelta.get().getDiffPreferences().isPresent()
     314         151 :             && !accountDelta.get().getEditPreferences().isPresent())) {
     315         151 :       return;
     316             :     }
     317             : 
     318           8 :     saveConfig(
     319             :         StoredPreferences.PREFERENCES_CONFIG,
     320           8 :         preferences.saveGeneralPreferences(
     321           8 :             accountDelta.get().getGeneralPreferences(),
     322           8 :             accountDelta.get().getDiffPreferences(),
     323           8 :             accountDelta.get().getEditPreferences()));
     324           8 :   }
     325             : 
     326             :   private void checkLoaded() {
     327         151 :     checkState(loadedAccountProperties != null, "Account %s not loaded yet", accountId.get());
     328         151 :   }
     329             : 
     330             :   /**
     331             :    * Get the validation errors, if any were discovered during parsing the account data.
     332             :    *
     333             :    * @return list of errors; empty list if there are no errors.
     334             :    */
     335             :   public List<ValidationError> getValidationErrors() {
     336           2 :     if (validationErrors != null) {
     337           1 :       return ImmutableList.copyOf(validationErrors);
     338             :     }
     339           2 :     return ImmutableList.of();
     340             :   }
     341             : 
     342             :   @Override
     343             :   public void error(ValidationError error) {
     344           1 :     if (validationErrors == null) {
     345           1 :       validationErrors = new ArrayList<>(4);
     346             :     }
     347           1 :     validationErrors.add(error);
     348           1 :   }
     349             : }

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