| // 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.account.externalids.ExternalIds; | |
| 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 com.google.gerrit.server.util.time.TimeUtil; | |
| 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 | |
| * ExternalIds#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() throws DuplicateKeyException { | |
| return getNewAccount(TimeUtil.now()); | |
| } | |
| /** | |
| * 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.fromConfig(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); | |
| } | |
| } |