| // 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.checkState; |
| import static java.util.Objects.requireNonNull; |
| |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.NotifyConfig.NotifyType; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.exceptions.DuplicateKeyException; |
| import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey; |
| import com.google.gerrit.server.config.AllUsersName; |
| import com.google.gerrit.server.config.CachedPreferences; |
| import com.google.gerrit.server.git.ValidationError; |
| import com.google.gerrit.server.git.meta.MetaDataUpdate; |
| import com.google.gerrit.server.git.meta.VersionedMetaData; |
| import java.io.IOException; |
| import java.time.Instant; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.CommitBuilder; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevSort; |
| |
| /** |
| * Reads/writes account data from/to a user branch in the {@code All-Users} repository. |
| * |
| * <p>This is the low-level API for account creation and account updates. Most callers should use |
| * {@link AccountsUpdate} for creating and updating accounts. |
| * |
| * <p>This class can read/write account properties, preferences (general, diff and edit preferences) |
| * and project watches. |
| * |
| * <p>The following files are read/written: |
| * |
| * <ul> |
| * <li>'account.config': Contains the account properties. Parsing and writing it is delegated to |
| * {@link AccountProperties}. |
| * <li>'preferences.config': Contains the preferences. Parsing and writing it is delegated to |
| * {@link StoredPreferences}. |
| * <li>'account.config': Contains the project watches. Parsing and writing it is delegated to |
| * {@link ProjectWatches}. |
| * </ul> |
| * |
| * <p>The commit date of the first commit on the user branch is used as registration date of the |
| * account. The first commit may be an empty commit (since all config files are optional). |
| */ |
| public class AccountConfig extends VersionedMetaData implements ValidationError.Sink { |
| private final Account.Id accountId; |
| private final AllUsersName allUsersName; |
| private final Repository repo; |
| private final String ref; |
| |
| private Optional<AccountProperties> loadedAccountProperties; |
| private Optional<ObjectId> externalIdsRev; |
| private ProjectWatches projectWatches; |
| private StoredPreferences preferences; |
| private Optional<AccountDelta> accountDelta = Optional.empty(); |
| private List<ValidationError> validationErrors; |
| |
| public AccountConfig(Account.Id accountId, AllUsersName allUsersName, Repository allUsersRepo) { |
| this.accountId = requireNonNull(accountId, "accountId"); |
| this.allUsersName = requireNonNull(allUsersName, "allUsersName"); |
| this.repo = requireNonNull(allUsersRepo, "allUsersRepo"); |
| this.ref = RefNames.refsUsers(accountId); |
| } |
| |
| @Override |
| public String getRefName() { |
| return ref; |
| } |
| |
| public AccountConfig load() throws IOException, ConfigInvalidException { |
| load(allUsersName, repo); |
| return this; |
| } |
| |
| public AccountConfig load(ObjectId rev) throws IOException, ConfigInvalidException { |
| load(allUsersName, repo, rev); |
| return this; |
| } |
| |
| /** |
| * Get the loaded account. |
| * |
| * @return the loaded account, {@link Optional#empty()} if load didn't find the account because it |
| * doesn't exist |
| * @throws IllegalStateException if the account was not loaded yet |
| */ |
| public Optional<Account> getLoadedAccount() { |
| checkLoaded(); |
| return loadedAccountProperties.map(AccountProperties::getAccount); |
| } |
| |
| /** |
| * Returns the revision of the {@code refs/meta/external-ids} branch. |
| * |
| * <p>This revision can be used to load the external IDs of the loaded account lazily via {@link |
| * com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdsNoteDbImpl#byAccount(com.google.gerrit.entities.Account.Id, |
| * ObjectId)}. |
| * |
| * @return revision of the {@code refs/meta/external-ids} branch, {@link Optional#empty()} if no |
| * {@code refs/meta/external-ids} branch exists |
| */ |
| public Optional<ObjectId> getExternalIdsRev() { |
| checkLoaded(); |
| return externalIdsRev; |
| } |
| |
| /** |
| * Get the project watches of the loaded account. |
| * |
| * @return the project watches of the loaded account |
| */ |
| public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() { |
| checkLoaded(); |
| return projectWatches.getProjectWatches(); |
| } |
| |
| /** |
| * Sets the account. This means the loaded account will be overwritten with the given account. |
| * |
| * <p>Changing the registration date of an account is not supported. |
| * |
| * @param account account that should be set |
| * @throws IllegalStateException if the account was not loaded yet |
| */ |
| public AccountConfig setAccount(Account account) { |
| checkLoaded(); |
| this.loadedAccountProperties = |
| Optional.of( |
| new AccountProperties(account.id(), account.registeredOn(), new Config(), null)); |
| this.accountDelta = |
| Optional.of( |
| AccountDelta.builder() |
| .setActive(account.isActive()) |
| .setFullName(account.fullName()) |
| .setDisplayName(account.displayName()) |
| .setPreferredEmail(account.preferredEmail()) |
| .setStatus(account.status()) |
| .build()); |
| return this; |
| } |
| |
| /** |
| * Creates a new account. |
| * |
| * @return the new account |
| * @throws DuplicateKeyException if the user branch already exists |
| */ |
| public Account getNewAccount(Instant registeredOn) throws DuplicateKeyException { |
| checkLoaded(); |
| if (revision != null) { |
| throw new DuplicateKeyException(String.format("account %s already exists", accountId)); |
| } |
| this.loadedAccountProperties = |
| Optional.of(new AccountProperties(accountId, registeredOn, new Config(), null)); |
| return loadedAccountProperties.map(AccountProperties::getAccount).get(); |
| } |
| |
| public AccountConfig setAccountDelta(AccountDelta accountDelta) { |
| this.accountDelta = Optional.of(accountDelta); |
| return this; |
| } |
| |
| /** |
| * Returns the content of the {@code preferences.config} file wrapped as {@link |
| * CachedPreferences}. |
| */ |
| public CachedPreferences asCachedPreferences() { |
| checkLoaded(); |
| return CachedPreferences.fromLegacyConfig(preferences.getRaw()); |
| } |
| |
| @Override |
| protected void onLoad() throws IOException, ConfigInvalidException { |
| if (revision != null) { |
| rw.reset(); |
| rw.markStart(revision); |
| rw.sort(RevSort.REVERSE); |
| Instant registeredOn = Instant.ofEpochMilli(rw.next().getCommitTime() * 1000L); |
| |
| Config accountConfig = readConfig(AccountProperties.ACCOUNT_CONFIG); |
| loadedAccountProperties = |
| Optional.of(new AccountProperties(accountId, registeredOn, accountConfig, revision)); |
| |
| projectWatches = new ProjectWatches(accountId, readConfig(ProjectWatches.WATCH_CONFIG), this); |
| |
| preferences = |
| new StoredPreferences( |
| accountId, |
| readConfig(StoredPreferences.PREFERENCES_CONFIG), |
| StoredPreferences.readDefaultConfig(allUsersName, repo), |
| this); |
| |
| projectWatches.parse(); |
| preferences.parse(); |
| } else { |
| loadedAccountProperties = Optional.empty(); |
| |
| projectWatches = new ProjectWatches(accountId, new Config(), this); |
| |
| preferences = |
| new StoredPreferences( |
| accountId, |
| new Config(), |
| StoredPreferences.readDefaultConfig(allUsersName, repo), |
| this); |
| } |
| |
| Ref externalIdsRef = repo.exactRef(RefNames.REFS_EXTERNAL_IDS); |
| externalIdsRev = Optional.ofNullable(externalIdsRef).map(Ref::getObjectId); |
| } |
| |
| @Override |
| public RevCommit commit(MetaDataUpdate update) throws IOException { |
| RevCommit c = super.commit(update); |
| loadedAccountProperties.get().setMetaId(c); |
| return c; |
| } |
| |
| @Override |
| protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException { |
| checkLoaded(); |
| |
| if (!loadedAccountProperties.isPresent()) { |
| return false; |
| } |
| |
| if (revision != null) { |
| if (Strings.isNullOrEmpty(commit.getMessage())) { |
| commit.setMessage("Update account\n"); |
| } |
| } else { |
| if (Strings.isNullOrEmpty(commit.getMessage())) { |
| commit.setMessage("Create account\n"); |
| } |
| |
| Instant registeredOn = loadedAccountProperties.get().getRegisteredOn(); |
| commit.setAuthor(new PersonIdent(commit.getAuthor(), registeredOn)); |
| commit.setCommitter(new PersonIdent(commit.getCommitter(), registeredOn)); |
| } |
| |
| saveAccount(); |
| saveProjectWatches(); |
| savePreferences(); |
| |
| accountDelta = Optional.empty(); |
| |
| return true; |
| } |
| |
| private void saveAccount() throws IOException { |
| if (accountDelta.isPresent()) { |
| saveConfig( |
| AccountProperties.ACCOUNT_CONFIG, loadedAccountProperties.get().save(accountDelta.get())); |
| } |
| } |
| |
| private void saveProjectWatches() throws IOException { |
| if (accountDelta.isPresent() |
| && (!accountDelta.get().getDeletedProjectWatches().isEmpty() |
| || !accountDelta.get().getUpdatedProjectWatches().isEmpty())) { |
| Map<ProjectWatchKey, Set<NotifyType>> newProjectWatches = |
| new HashMap<>(projectWatches.getProjectWatches()); |
| accountDelta.get().getDeletedProjectWatches().forEach(newProjectWatches::remove); |
| accountDelta.get().getUpdatedProjectWatches().forEach(newProjectWatches::put); |
| saveConfig(ProjectWatches.WATCH_CONFIG, projectWatches.save(newProjectWatches)); |
| } |
| } |
| |
| private void savePreferences() throws IOException, ConfigInvalidException { |
| if (!accountDelta.isPresent() |
| || (!accountDelta.get().getGeneralPreferences().isPresent() |
| && !accountDelta.get().getDiffPreferences().isPresent() |
| && !accountDelta.get().getEditPreferences().isPresent())) { |
| return; |
| } |
| |
| saveConfig( |
| StoredPreferences.PREFERENCES_CONFIG, |
| preferences.saveGeneralPreferences( |
| accountDelta.get().getGeneralPreferences(), |
| accountDelta.get().getDiffPreferences(), |
| accountDelta.get().getEditPreferences())); |
| } |
| |
| private void checkLoaded() { |
| checkState(loadedAccountProperties != null, "Account %s not loaded yet", accountId.get()); |
| } |
| |
| /** |
| * Get the validation errors, if any were discovered during parsing the account data. |
| * |
| * @return list of errors; empty list if there are no errors. |
| */ |
| public List<ValidationError> getValidationErrors() { |
| if (validationErrors != null) { |
| return ImmutableList.copyOf(validationErrors); |
| } |
| return ImmutableList.of(); |
| } |
| |
| @Override |
| public void error(ValidationError error) { |
| if (validationErrors == null) { |
| validationErrors = new ArrayList<>(4); |
| } |
| validationErrors.add(error); |
| } |
| } |