978 lines
34 KiB
Java
978 lines
34 KiB
Java
// 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.externalids;
|
|
|
|
import static com.google.common.base.Preconditions.checkState;
|
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
|
import static java.util.Objects.requireNonNull;
|
|
import static java.util.stream.Collectors.joining;
|
|
import static java.util.stream.Collectors.toSet;
|
|
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
|
|
|
import com.google.common.base.Strings;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.google.common.collect.Iterables;
|
|
import com.google.common.collect.Sets;
|
|
import com.google.common.collect.Streams;
|
|
import com.google.common.flogger.FluentLogger;
|
|
import com.google.gerrit.common.Nullable;
|
|
import com.google.gerrit.metrics.Counter0;
|
|
import com.google.gerrit.metrics.Description;
|
|
import com.google.gerrit.metrics.DisabledMetricMaker;
|
|
import com.google.gerrit.metrics.MetricMaker;
|
|
import com.google.gerrit.reviewdb.client.Account;
|
|
import com.google.gerrit.reviewdb.client.RefNames;
|
|
import com.google.gerrit.server.account.AccountCache;
|
|
import com.google.gerrit.server.config.AllUsersName;
|
|
import com.google.gerrit.server.git.meta.MetaDataUpdate;
|
|
import com.google.gerrit.server.git.meta.VersionedMetaData;
|
|
import com.google.gerrit.server.index.account.AccountIndexer;
|
|
import com.google.inject.Inject;
|
|
import com.google.inject.Provider;
|
|
import com.google.inject.Singleton;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
|
import org.eclipse.jgit.lib.BlobBasedConfig;
|
|
import org.eclipse.jgit.lib.CommitBuilder;
|
|
import org.eclipse.jgit.lib.Config;
|
|
import org.eclipse.jgit.lib.ObjectId;
|
|
import org.eclipse.jgit.lib.ObjectInserter;
|
|
import org.eclipse.jgit.lib.Repository;
|
|
import org.eclipse.jgit.notes.Note;
|
|
import org.eclipse.jgit.notes.NoteMap;
|
|
import org.eclipse.jgit.revwalk.RevCommit;
|
|
import org.eclipse.jgit.revwalk.RevTree;
|
|
import org.eclipse.jgit.revwalk.RevWalk;
|
|
|
|
/**
|
|
* {@link VersionedMetaData} subclass to update external IDs.
|
|
*
|
|
* <p>This is a low-level API. Read/write of external IDs should be done through {@link
|
|
* com.google.gerrit.server.account.AccountsUpdate} or {@link
|
|
* com.google.gerrit.server.account.AccountConfig}.
|
|
*
|
|
* <p>On load the note map from {@code refs/meta/external-ids} is read, but the external IDs are not
|
|
* parsed yet (see {@link #onLoad()}).
|
|
*
|
|
* <p>After loading the note map callers can access single or all external IDs. Only now the
|
|
* requested external IDs are parsed.
|
|
*
|
|
* <p>After loading the note map callers can stage various external ID updates (insert, upsert,
|
|
* delete, replace).
|
|
*
|
|
* <p>On save the staged external ID updates are performed (see {@link #onSave(CommitBuilder)}).
|
|
*
|
|
* <p>After committing the external IDs a cache update can be requested which also reindexes the
|
|
* accounts for which external IDs have been updated (see {@link #updateCaches()}).
|
|
*/
|
|
public class ExternalIdNotes extends VersionedMetaData {
|
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
|
|
|
private static final int MAX_NOTE_SZ = 1 << 19;
|
|
|
|
public interface ExternalIdNotesLoader {
|
|
/**
|
|
* Loads the external ID notes from the current tip of the {@code refs/meta/external-ids}
|
|
* branch.
|
|
*
|
|
* @param allUsersRepo the All-Users repository
|
|
*/
|
|
ExternalIdNotes load(Repository allUsersRepo) throws IOException, ConfigInvalidException;
|
|
|
|
/**
|
|
* Loads the external ID notes from the specified revision of the {@code refs/meta/external-ids}
|
|
* branch.
|
|
*
|
|
* @param allUsersRepo the All-Users repository
|
|
* @param rev the revision from which the external ID notes should be loaded, if {@code null}
|
|
* the external ID notes are loaded from the current tip, if {@link ObjectId#zeroId()} it's
|
|
* assumed that the {@code refs/meta/external-ids} branch doesn't exist and the loaded
|
|
* external IDs will be empty
|
|
*/
|
|
ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
|
|
throws IOException, ConfigInvalidException;
|
|
}
|
|
|
|
@Singleton
|
|
public static class Factory implements ExternalIdNotesLoader {
|
|
private final ExternalIdCache externalIdCache;
|
|
private final AccountCache accountCache;
|
|
private final Provider<AccountIndexer> accountIndexer;
|
|
private final MetricMaker metricMaker;
|
|
private final AllUsersName allUsersName;
|
|
|
|
@Inject
|
|
Factory(
|
|
ExternalIdCache externalIdCache,
|
|
AccountCache accountCache,
|
|
Provider<AccountIndexer> accountIndexer,
|
|
MetricMaker metricMaker,
|
|
AllUsersName allUsersName) {
|
|
this.externalIdCache = externalIdCache;
|
|
this.accountCache = accountCache;
|
|
this.accountIndexer = accountIndexer;
|
|
this.metricMaker = metricMaker;
|
|
this.allUsersName = allUsersName;
|
|
}
|
|
|
|
@Override
|
|
public ExternalIdNotes load(Repository allUsersRepo)
|
|
throws IOException, ConfigInvalidException {
|
|
return new ExternalIdNotes(
|
|
externalIdCache,
|
|
accountCache,
|
|
accountIndexer,
|
|
metricMaker,
|
|
allUsersName,
|
|
allUsersRepo)
|
|
.load();
|
|
}
|
|
|
|
@Override
|
|
public ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
|
|
throws IOException, ConfigInvalidException {
|
|
return new ExternalIdNotes(
|
|
externalIdCache,
|
|
accountCache,
|
|
accountIndexer,
|
|
metricMaker,
|
|
allUsersName,
|
|
allUsersRepo)
|
|
.load(rev);
|
|
}
|
|
}
|
|
|
|
@Singleton
|
|
public static class FactoryNoReindex implements ExternalIdNotesLoader {
|
|
private final ExternalIdCache externalIdCache;
|
|
private final MetricMaker metricMaker;
|
|
private final AllUsersName allUsersName;
|
|
|
|
@Inject
|
|
FactoryNoReindex(
|
|
ExternalIdCache externalIdCache, MetricMaker metricMaker, AllUsersName allUsersName) {
|
|
this.externalIdCache = externalIdCache;
|
|
this.metricMaker = metricMaker;
|
|
this.allUsersName = allUsersName;
|
|
}
|
|
|
|
@Override
|
|
public ExternalIdNotes load(Repository allUsersRepo)
|
|
throws IOException, ConfigInvalidException {
|
|
return new ExternalIdNotes(
|
|
externalIdCache, null, null, metricMaker, allUsersName, allUsersRepo)
|
|
.load();
|
|
}
|
|
|
|
@Override
|
|
public ExternalIdNotes load(Repository allUsersRepo, @Nullable ObjectId rev)
|
|
throws IOException, ConfigInvalidException {
|
|
return new ExternalIdNotes(
|
|
externalIdCache, null, null, metricMaker, allUsersName, allUsersRepo)
|
|
.load(rev);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads the external ID notes for reading only. The external ID notes are loaded from the current
|
|
* tip of the {@code refs/meta/external-ids} branch.
|
|
*
|
|
* @return read-only {@link ExternalIdNotes} instance
|
|
*/
|
|
public static ExternalIdNotes loadReadOnly(AllUsersName allUsersName, Repository allUsersRepo)
|
|
throws IOException, ConfigInvalidException {
|
|
return new ExternalIdNotes(
|
|
new DisabledExternalIdCache(),
|
|
null,
|
|
null,
|
|
new DisabledMetricMaker(),
|
|
allUsersName,
|
|
allUsersRepo)
|
|
.setReadOnly()
|
|
.load();
|
|
}
|
|
|
|
/**
|
|
* Loads the external ID notes for reading only. The external ID notes are loaded from the
|
|
* specified revision of the {@code refs/meta/external-ids} branch.
|
|
*
|
|
* @param rev the revision from which the external ID notes should be loaded, if {@code null} the
|
|
* external ID notes are loaded from the current tip, if {@link ObjectId#zeroId()} it's
|
|
* assumed that the {@code refs/meta/external-ids} branch doesn't exist and the loaded
|
|
* external IDs will be empty
|
|
* @return read-only {@link ExternalIdNotes} instance
|
|
*/
|
|
public static ExternalIdNotes loadReadOnly(
|
|
AllUsersName allUsersName, Repository allUsersRepo, @Nullable ObjectId rev)
|
|
throws IOException, ConfigInvalidException {
|
|
return new ExternalIdNotes(
|
|
new DisabledExternalIdCache(),
|
|
null,
|
|
null,
|
|
new DisabledMetricMaker(),
|
|
allUsersName,
|
|
allUsersRepo)
|
|
.setReadOnly()
|
|
.load(rev);
|
|
}
|
|
|
|
/**
|
|
* Loads the external ID notes for updates without cache evictions. The external ID notes are
|
|
* loaded from the current tip of the {@code refs/meta/external-ids} branch.
|
|
*
|
|
* <p>Use this only from init, schema upgrades and tests.
|
|
*
|
|
* <p>Metrics are disabled.
|
|
*
|
|
* @return {@link ExternalIdNotes} instance that doesn't updates caches on save
|
|
*/
|
|
public static ExternalIdNotes loadNoCacheUpdate(
|
|
AllUsersName allUsersName, Repository allUsersRepo)
|
|
throws IOException, ConfigInvalidException {
|
|
return new ExternalIdNotes(
|
|
new DisabledExternalIdCache(),
|
|
null,
|
|
null,
|
|
new DisabledMetricMaker(),
|
|
allUsersName,
|
|
allUsersRepo)
|
|
.load();
|
|
}
|
|
|
|
private final ExternalIdCache externalIdCache;
|
|
@Nullable private final AccountCache accountCache;
|
|
@Nullable private final Provider<AccountIndexer> accountIndexer;
|
|
private final AllUsersName allUsersName;
|
|
private final Counter0 updateCount;
|
|
private final Repository repo;
|
|
|
|
private NoteMap noteMap;
|
|
private ObjectId oldRev;
|
|
|
|
// Staged note map updates that should be executed on save.
|
|
private List<NoteMapUpdate> noteMapUpdates = new ArrayList<>();
|
|
|
|
// Staged cache updates that should be executed after external ID changes have been committed.
|
|
private List<CacheUpdate> cacheUpdates = new ArrayList<>();
|
|
|
|
private Runnable afterReadRevision;
|
|
private boolean readOnly = false;
|
|
|
|
private ExternalIdNotes(
|
|
ExternalIdCache externalIdCache,
|
|
@Nullable AccountCache accountCache,
|
|
@Nullable Provider<AccountIndexer> accountIndexer,
|
|
MetricMaker metricMaker,
|
|
AllUsersName allUsersName,
|
|
Repository allUsersRepo) {
|
|
this.externalIdCache = requireNonNull(externalIdCache, "externalIdCache");
|
|
this.accountCache = accountCache;
|
|
this.accountIndexer = accountIndexer;
|
|
this.updateCount =
|
|
metricMaker.newCounter(
|
|
"notedb/external_id_update_count",
|
|
new Description("Total number of external ID updates.").setRate().setUnit("updates"));
|
|
this.allUsersName = requireNonNull(allUsersName, "allUsersRepo");
|
|
this.repo = requireNonNull(allUsersRepo, "allUsersRepo");
|
|
}
|
|
|
|
public ExternalIdNotes setAfterReadRevision(Runnable afterReadRevision) {
|
|
this.afterReadRevision = afterReadRevision;
|
|
return this;
|
|
}
|
|
|
|
private ExternalIdNotes setReadOnly() {
|
|
this.readOnly = true;
|
|
return this;
|
|
}
|
|
|
|
public Repository getRepository() {
|
|
return repo;
|
|
}
|
|
|
|
@Override
|
|
protected String getRefName() {
|
|
return RefNames.REFS_EXTERNAL_IDS;
|
|
}
|
|
|
|
/**
|
|
* Loads the external ID notes from the current tip of the {@code refs/meta/external-ids} branch.
|
|
*
|
|
* @return {@link ExternalIdNotes} instance for chaining
|
|
*/
|
|
private ExternalIdNotes load() throws IOException, ConfigInvalidException {
|
|
load(allUsersName, repo);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Loads the external ID notes from the specified revision of the {@code refs/meta/external-ids}
|
|
* branch.
|
|
*
|
|
* @param rev the revision from which the external ID notes should be loaded, if {@code null} the
|
|
* external ID notes are loaded from the current tip, if {@link ObjectId#zeroId()} it's
|
|
* assumed that the {@code refs/meta/external-ids} branch doesn't exist and the loaded
|
|
* external IDs will be empty
|
|
* @return {@link ExternalIdNotes} instance for chaining
|
|
*/
|
|
ExternalIdNotes load(@Nullable ObjectId rev) throws IOException, ConfigInvalidException {
|
|
if (rev == null) {
|
|
return load();
|
|
}
|
|
if (ObjectId.zeroId().equals(rev)) {
|
|
load(allUsersName, repo, null);
|
|
return this;
|
|
}
|
|
load(allUsersName, repo, rev);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Parses and returns the specified external ID.
|
|
*
|
|
* @param key the key of the external ID
|
|
* @return the external ID, {@code Optional.empty()} if it doesn't exist
|
|
*/
|
|
public Optional<ExternalId> get(ExternalId.Key key) throws IOException, ConfigInvalidException {
|
|
checkLoaded();
|
|
ObjectId noteId = key.sha1();
|
|
if (!noteMap.contains(noteId)) {
|
|
return Optional.empty();
|
|
}
|
|
|
|
try (RevWalk rw = new RevWalk(repo)) {
|
|
ObjectId noteDataId = noteMap.get(noteId);
|
|
byte[] raw = readNoteData(rw, noteDataId);
|
|
return Optional.of(ExternalId.parse(noteId.name(), raw, noteDataId));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses and returns the specified external IDs.
|
|
*
|
|
* @param keys the keys of the external IDs
|
|
* @return the external IDs
|
|
*/
|
|
public Set<ExternalId> get(Collection<ExternalId.Key> keys)
|
|
throws IOException, ConfigInvalidException {
|
|
checkLoaded();
|
|
HashSet<ExternalId> externalIds = Sets.newHashSetWithExpectedSize(keys.size());
|
|
for (ExternalId.Key key : keys) {
|
|
get(key).ifPresent(externalIds::add);
|
|
}
|
|
return externalIds;
|
|
}
|
|
|
|
/**
|
|
* Parses and returns all external IDs.
|
|
*
|
|
* <p>Invalid external IDs are ignored.
|
|
*
|
|
* @return all external IDs
|
|
*/
|
|
public ImmutableSet<ExternalId> all() throws IOException {
|
|
checkLoaded();
|
|
try (RevWalk rw = new RevWalk(repo)) {
|
|
ImmutableSet.Builder<ExternalId> b = ImmutableSet.builder();
|
|
for (Note note : noteMap) {
|
|
byte[] raw = readNoteData(rw, note.getData());
|
|
try {
|
|
b.add(ExternalId.parse(note.getName(), raw, note.getData()));
|
|
} catch (ConfigInvalidException | RuntimeException e) {
|
|
logger.atSevere().withCause(e).log(
|
|
"Ignoring invalid external ID note %s", note.getName());
|
|
}
|
|
}
|
|
return b.build();
|
|
}
|
|
}
|
|
|
|
NoteMap getNoteMap() {
|
|
checkLoaded();
|
|
return noteMap;
|
|
}
|
|
|
|
static byte[] readNoteData(RevWalk rw, ObjectId noteDataId) throws IOException {
|
|
return rw.getObjectReader().open(noteDataId, OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
|
|
}
|
|
|
|
/**
|
|
* Inserts a new external ID.
|
|
*
|
|
* @throws IOException on IO error while checking if external ID already exists
|
|
* @throws DuplicateExternalIdKeyException if the external ID already exists
|
|
*/
|
|
public void insert(ExternalId extId) throws IOException, DuplicateExternalIdKeyException {
|
|
insert(Collections.singleton(extId));
|
|
}
|
|
|
|
/**
|
|
* Inserts new external IDs.
|
|
*
|
|
* @throws IOException on IO error while checking if external IDs already exist
|
|
* @throws DuplicateExternalIdKeyException if any of the external ID already exists
|
|
*/
|
|
public void insert(Collection<ExternalId> extIds)
|
|
throws IOException, DuplicateExternalIdKeyException {
|
|
checkLoaded();
|
|
checkExternalIdsDontExist(extIds);
|
|
|
|
Set<ExternalId> newExtIds = new HashSet<>();
|
|
noteMapUpdates.add(
|
|
(rw, n, f) -> {
|
|
for (ExternalId extId : extIds) {
|
|
ExternalId insertedExtId = upsert(rw, inserter, noteMap, f, extId);
|
|
newExtIds.add(insertedExtId);
|
|
}
|
|
});
|
|
cacheUpdates.add(cu -> cu.add(newExtIds));
|
|
}
|
|
|
|
/**
|
|
* Inserts or updates an external ID.
|
|
*
|
|
* <p>If the external ID already exists, it is overwritten, otherwise it is inserted.
|
|
*/
|
|
public void upsert(ExternalId extId) throws IOException, ConfigInvalidException {
|
|
upsert(Collections.singleton(extId));
|
|
}
|
|
|
|
/**
|
|
* Inserts or updates external IDs.
|
|
*
|
|
* <p>If any of the external IDs already exists, it is overwritten. New external IDs are inserted.
|
|
*/
|
|
public void upsert(Collection<ExternalId> extIds) throws IOException, ConfigInvalidException {
|
|
checkLoaded();
|
|
Set<ExternalId> removedExtIds = get(ExternalId.Key.from(extIds));
|
|
Set<ExternalId> updatedExtIds = new HashSet<>();
|
|
noteMapUpdates.add(
|
|
(rw, n, f) -> {
|
|
for (ExternalId extId : extIds) {
|
|
ExternalId updatedExtId = upsert(rw, inserter, noteMap, f, extId);
|
|
updatedExtIds.add(updatedExtId);
|
|
}
|
|
});
|
|
cacheUpdates.add(cu -> cu.remove(removedExtIds).add(updatedExtIds));
|
|
}
|
|
|
|
/**
|
|
* Deletes an external ID.
|
|
*
|
|
* @throws IllegalStateException is thrown if there is an existing external ID that has the same
|
|
* key, but otherwise doesn't match the specified external ID.
|
|
*/
|
|
public void delete(ExternalId extId) {
|
|
delete(Collections.singleton(extId));
|
|
}
|
|
|
|
/**
|
|
* Deletes external IDs.
|
|
*
|
|
* @throws IllegalStateException is thrown if there is an existing external ID that has the same
|
|
* key as any of the external IDs that should be deleted, but otherwise doesn't match the that
|
|
* external ID.
|
|
*/
|
|
public void delete(Collection<ExternalId> extIds) {
|
|
checkLoaded();
|
|
Set<ExternalId> removedExtIds = new HashSet<>();
|
|
noteMapUpdates.add(
|
|
(rw, n, f) -> {
|
|
for (ExternalId extId : extIds) {
|
|
remove(rw, noteMap, f, extId);
|
|
removedExtIds.add(extId);
|
|
}
|
|
});
|
|
cacheUpdates.add(cu -> cu.remove(removedExtIds));
|
|
}
|
|
|
|
/**
|
|
* Delete an external ID by key.
|
|
*
|
|
* @throws IllegalStateException is thrown if the external ID does not belong to the specified
|
|
* account.
|
|
*/
|
|
public void delete(Account.Id accountId, ExternalId.Key extIdKey) {
|
|
delete(accountId, Collections.singleton(extIdKey));
|
|
}
|
|
|
|
/**
|
|
* Delete external IDs by external ID key.
|
|
*
|
|
* @throws IllegalStateException is thrown if any of the external IDs does not belong to the
|
|
* specified account.
|
|
*/
|
|
public void delete(Account.Id accountId, Collection<ExternalId.Key> extIdKeys) {
|
|
checkLoaded();
|
|
Set<ExternalId> removedExtIds = new HashSet<>();
|
|
noteMapUpdates.add(
|
|
(rw, n, f) -> {
|
|
for (ExternalId.Key extIdKey : extIdKeys) {
|
|
ExternalId removedExtId = remove(rw, noteMap, f, extIdKey, accountId);
|
|
removedExtIds.add(removedExtId);
|
|
}
|
|
});
|
|
cacheUpdates.add(cu -> cu.remove(removedExtIds));
|
|
}
|
|
|
|
/**
|
|
* Delete external IDs by external ID key.
|
|
*
|
|
* <p>The external IDs are deleted regardless of which account they belong to.
|
|
*/
|
|
public void deleteByKeys(Collection<ExternalId.Key> extIdKeys) {
|
|
checkLoaded();
|
|
Set<ExternalId> removedExtIds = new HashSet<>();
|
|
noteMapUpdates.add(
|
|
(rw, n, f) -> {
|
|
for (ExternalId.Key extIdKey : extIdKeys) {
|
|
ExternalId extId = remove(rw, noteMap, f, extIdKey, null);
|
|
removedExtIds.add(extId);
|
|
}
|
|
});
|
|
cacheUpdates.add(cu -> cu.remove(removedExtIds));
|
|
}
|
|
|
|
/**
|
|
* Replaces external IDs for an account by external ID keys.
|
|
*
|
|
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
|
|
* external ID key is specified for deletion and an external ID with the same key is specified to
|
|
* be added, the old external ID with that key is deleted first and then the new external ID is
|
|
* added (so the external ID for that key is replaced).
|
|
*
|
|
* @throws IllegalStateException is thrown if any of the specified external IDs does not belong to
|
|
* the specified account.
|
|
*/
|
|
public void replace(
|
|
Account.Id accountId, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
|
|
throws IOException, DuplicateExternalIdKeyException {
|
|
checkLoaded();
|
|
checkSameAccount(toAdd, accountId);
|
|
checkExternalIdKeysDontExist(ExternalId.Key.from(toAdd), toDelete);
|
|
|
|
Set<ExternalId> removedExtIds = new HashSet<>();
|
|
Set<ExternalId> updatedExtIds = new HashSet<>();
|
|
noteMapUpdates.add(
|
|
(rw, n, f) -> {
|
|
for (ExternalId.Key extIdKey : toDelete) {
|
|
ExternalId removedExtId = remove(rw, noteMap, f, extIdKey, accountId);
|
|
if (removedExtId != null) {
|
|
removedExtIds.add(removedExtId);
|
|
}
|
|
}
|
|
|
|
for (ExternalId extId : toAdd) {
|
|
ExternalId insertedExtId = upsert(rw, inserter, noteMap, f, extId);
|
|
updatedExtIds.add(insertedExtId);
|
|
}
|
|
});
|
|
cacheUpdates.add(cu -> cu.add(updatedExtIds).remove(removedExtIds));
|
|
}
|
|
|
|
/**
|
|
* Replaces external IDs for an account by external ID keys.
|
|
*
|
|
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
|
|
* external ID key is specified for deletion and an external ID with the same key is specified to
|
|
* be added, the old external ID with that key is deleted first and then the new external ID is
|
|
* added (so the external ID for that key is replaced).
|
|
*
|
|
* <p>The external IDs are replaced regardless of which account they belong to.
|
|
*/
|
|
public void replaceByKeys(Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
|
|
throws IOException, DuplicateExternalIdKeyException {
|
|
checkLoaded();
|
|
checkExternalIdKeysDontExist(ExternalId.Key.from(toAdd), toDelete);
|
|
|
|
Set<ExternalId> removedExtIds = new HashSet<>();
|
|
Set<ExternalId> updatedExtIds = new HashSet<>();
|
|
noteMapUpdates.add(
|
|
(rw, n, f) -> {
|
|
for (ExternalId.Key extIdKey : toDelete) {
|
|
ExternalId removedExtId = remove(rw, noteMap, f, extIdKey, null);
|
|
removedExtIds.add(removedExtId);
|
|
}
|
|
|
|
for (ExternalId extId : toAdd) {
|
|
ExternalId insertedExtId = upsert(rw, inserter, noteMap, f, extId);
|
|
updatedExtIds.add(insertedExtId);
|
|
}
|
|
});
|
|
cacheUpdates.add(cu -> cu.add(updatedExtIds).remove(removedExtIds));
|
|
}
|
|
|
|
/**
|
|
* Replaces an external ID.
|
|
*
|
|
* @throws IllegalStateException is thrown if the specified external IDs belong to different
|
|
* accounts.
|
|
*/
|
|
public void replace(ExternalId toDelete, ExternalId toAdd)
|
|
throws IOException, DuplicateExternalIdKeyException {
|
|
replace(Collections.singleton(toDelete), Collections.singleton(toAdd));
|
|
}
|
|
|
|
/**
|
|
* Replaces external IDs.
|
|
*
|
|
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
|
|
* external ID is specified for deletion and an external ID with the same key is specified to be
|
|
* added, the old external ID with that key is deleted first and then the new external ID is added
|
|
* (so the external ID for that key is replaced).
|
|
*
|
|
* @throws IllegalStateException is thrown if the specified external IDs belong to different
|
|
* accounts.
|
|
*/
|
|
public void replace(Collection<ExternalId> toDelete, Collection<ExternalId> toAdd)
|
|
throws IOException, DuplicateExternalIdKeyException {
|
|
Account.Id accountId = checkSameAccount(Iterables.concat(toDelete, toAdd));
|
|
if (accountId == null) {
|
|
// toDelete and toAdd are empty -> nothing to do
|
|
return;
|
|
}
|
|
|
|
replace(accountId, toDelete.stream().map(ExternalId::key).collect(toSet()), toAdd);
|
|
}
|
|
|
|
@Override
|
|
protected void onLoad() throws IOException, ConfigInvalidException {
|
|
if (revision != null) {
|
|
logger.atFine().log("Reading external ID note map");
|
|
noteMap = NoteMap.read(reader, revision);
|
|
} else {
|
|
noteMap = NoteMap.newEmptyMap();
|
|
}
|
|
|
|
if (afterReadRevision != null) {
|
|
afterReadRevision.run();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public RevCommit commit(MetaDataUpdate update) throws IOException {
|
|
oldRev = revision != null ? revision.copy() : ObjectId.zeroId();
|
|
RevCommit commit = super.commit(update);
|
|
updateCount.increment();
|
|
return commit;
|
|
}
|
|
|
|
/**
|
|
* Updates the caches (external ID cache, account cache) and reindexes the accounts for which
|
|
* external IDs were modified.
|
|
*
|
|
* <p>Must only be called after committing changes.
|
|
*
|
|
* <p>No-op if this instance was created by {@link #loadNoCacheUpdate(AllUsersName, Repository)}.
|
|
*
|
|
* <p>No eviction from account cache and no reindex if this instance was created by {@link
|
|
* FactoryNoReindex}.
|
|
*/
|
|
public void updateCaches() throws IOException {
|
|
updateCaches(ImmutableSet.of());
|
|
}
|
|
|
|
/**
|
|
* Updates the caches (external ID cache, account cache) and reindexes the accounts for which
|
|
* external IDs were modified.
|
|
*
|
|
* <p>Must only be called after committing changes.
|
|
*
|
|
* <p>No-op if this instance was created by {@link #loadNoCacheUpdate(AllUsersName, Repository)}.
|
|
*
|
|
* <p>No eviction from account cache if this instance was created by {@link FactoryNoReindex}.
|
|
*
|
|
* @param accountsToSkip set of accounts that should not be evicted from the account cache, in
|
|
* this case the caller must take care to evict them otherwise
|
|
*/
|
|
public void updateCaches(Collection<Account.Id> accountsToSkip) throws IOException {
|
|
checkState(oldRev != null, "no changes committed yet");
|
|
|
|
ExternalIdCacheUpdates externalIdCacheUpdates = new ExternalIdCacheUpdates();
|
|
for (CacheUpdate cacheUpdate : cacheUpdates) {
|
|
cacheUpdate.execute(externalIdCacheUpdates);
|
|
}
|
|
|
|
externalIdCache.onReplace(
|
|
oldRev,
|
|
getRevision(),
|
|
externalIdCacheUpdates.getRemoved(),
|
|
externalIdCacheUpdates.getAdded());
|
|
|
|
if (accountCache != null || accountIndexer != null) {
|
|
for (Account.Id id :
|
|
Streams.concat(
|
|
externalIdCacheUpdates.getAdded().stream(),
|
|
externalIdCacheUpdates.getRemoved().stream())
|
|
.map(ExternalId::accountId)
|
|
.filter(i -> !accountsToSkip.contains(i))
|
|
.collect(toSet())) {
|
|
if (accountCache != null) {
|
|
accountCache.evict(id);
|
|
}
|
|
if (accountIndexer != null) {
|
|
accountIndexer.get().index(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
cacheUpdates.clear();
|
|
oldRev = null;
|
|
}
|
|
|
|
@Override
|
|
protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
|
|
checkState(!readOnly, "Updating external IDs is disabled");
|
|
|
|
if (noteMapUpdates.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
logger.atFine().log("Updating external IDs");
|
|
|
|
if (Strings.isNullOrEmpty(commit.getMessage())) {
|
|
commit.setMessage("Update external IDs\n");
|
|
}
|
|
|
|
try (RevWalk rw = new RevWalk(reader)) {
|
|
Set<String> footers = new HashSet<>();
|
|
for (NoteMapUpdate noteMapUpdate : noteMapUpdates) {
|
|
try {
|
|
noteMapUpdate.execute(rw, noteMap, footers);
|
|
} catch (DuplicateExternalIdKeyException e) {
|
|
throw new IOException(e);
|
|
}
|
|
}
|
|
noteMapUpdates.clear();
|
|
if (!footers.isEmpty()) {
|
|
commit.setMessage(
|
|
footers.stream()
|
|
.sorted()
|
|
.collect(joining("\n", commit.getMessage().trim() + "\n\n", "")));
|
|
}
|
|
|
|
RevTree oldTree = revision != null ? rw.parseTree(revision) : null;
|
|
ObjectId newTreeId = noteMap.writeTree(inserter);
|
|
if (newTreeId.equals(oldTree)) {
|
|
return false;
|
|
}
|
|
|
|
commit.setTreeId(newTreeId);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks that all specified external IDs belong to the same account.
|
|
*
|
|
* @return the ID of the account to which all specified external IDs belong.
|
|
*/
|
|
private static Account.Id checkSameAccount(Iterable<ExternalId> extIds) {
|
|
return checkSameAccount(extIds, null);
|
|
}
|
|
|
|
/**
|
|
* Checks that all specified external IDs belong to specified account. If no account is specified
|
|
* it is checked that all specified external IDs belong to the same account.
|
|
*
|
|
* @return the ID of the account to which all specified external IDs belong.
|
|
*/
|
|
public static Account.Id checkSameAccount(
|
|
Iterable<ExternalId> extIds, @Nullable Account.Id accountId) {
|
|
for (ExternalId extId : extIds) {
|
|
if (accountId == null) {
|
|
accountId = extId.accountId();
|
|
continue;
|
|
}
|
|
checkState(
|
|
accountId.equals(extId.accountId()),
|
|
"external id %s belongs to account %s, expected account %s",
|
|
extId.key().get(),
|
|
extId.accountId().get(),
|
|
accountId.get());
|
|
}
|
|
return accountId;
|
|
}
|
|
|
|
/**
|
|
* Insert or updates an new external ID and sets it in the note map.
|
|
*
|
|
* <p>If the external ID already exists it is overwritten.
|
|
*/
|
|
private static ExternalId upsert(
|
|
RevWalk rw, ObjectInserter ins, NoteMap noteMap, Set<String> footers, ExternalId extId)
|
|
throws IOException, ConfigInvalidException {
|
|
ObjectId noteId = extId.key().sha1();
|
|
Config c = new Config();
|
|
if (noteMap.contains(extId.key().sha1())) {
|
|
ObjectId noteDataId = noteMap.get(noteId);
|
|
byte[] raw = readNoteData(rw, noteDataId);
|
|
try {
|
|
c = new BlobBasedConfig(null, raw);
|
|
ExternalId oldExtId = ExternalId.parse(noteId.name(), c, noteDataId);
|
|
addFooters(footers, oldExtId);
|
|
} catch (ConfigInvalidException e) {
|
|
throw new ConfigInvalidException(
|
|
String.format("Invalid external id config for note %s: %s", noteId, e.getMessage()));
|
|
}
|
|
}
|
|
extId.writeToConfig(c);
|
|
byte[] raw = c.toText().getBytes(UTF_8);
|
|
ObjectId noteData = ins.insert(OBJ_BLOB, raw);
|
|
noteMap.set(noteId, noteData);
|
|
ExternalId newExtId = ExternalId.create(extId, noteData);
|
|
addFooters(footers, newExtId);
|
|
return newExtId;
|
|
}
|
|
|
|
/**
|
|
* Removes an external ID from the note map.
|
|
*
|
|
* @throws IllegalStateException is thrown if there is an existing external ID that has the same
|
|
* key, but otherwise doesn't match the specified external ID.
|
|
*/
|
|
private static ExternalId remove(
|
|
RevWalk rw, NoteMap noteMap, Set<String> footers, ExternalId extId)
|
|
throws IOException, ConfigInvalidException {
|
|
ObjectId noteId = extId.key().sha1();
|
|
if (!noteMap.contains(noteId)) {
|
|
return null;
|
|
}
|
|
|
|
ObjectId noteDataId = noteMap.get(noteId);
|
|
byte[] raw = readNoteData(rw, noteDataId);
|
|
ExternalId actualExtId = ExternalId.parse(noteId.name(), raw, noteDataId);
|
|
checkState(
|
|
extId.equals(actualExtId),
|
|
"external id %s should be removed, but it's not matching the actual external id %s",
|
|
extId.toString(),
|
|
actualExtId.toString());
|
|
noteMap.remove(noteId);
|
|
addFooters(footers, actualExtId);
|
|
return actualExtId;
|
|
}
|
|
|
|
/**
|
|
* Removes an external ID from the note map by external ID key.
|
|
*
|
|
* @throws IllegalStateException is thrown if an expected account ID is provided and an external
|
|
* ID with the specified key exists, but belongs to another account.
|
|
* @return the external ID that was removed, {@code null} if no external ID with the specified key
|
|
* exists
|
|
*/
|
|
private static ExternalId remove(
|
|
RevWalk rw,
|
|
NoteMap noteMap,
|
|
Set<String> footers,
|
|
ExternalId.Key extIdKey,
|
|
Account.Id expectedAccountId)
|
|
throws IOException, ConfigInvalidException {
|
|
ObjectId noteId = extIdKey.sha1();
|
|
if (!noteMap.contains(noteId)) {
|
|
return null;
|
|
}
|
|
|
|
ObjectId noteDataId = noteMap.get(noteId);
|
|
byte[] raw = readNoteData(rw, noteDataId);
|
|
ExternalId extId = ExternalId.parse(noteId.name(), raw, noteDataId);
|
|
if (expectedAccountId != null) {
|
|
checkState(
|
|
expectedAccountId.equals(extId.accountId()),
|
|
"external id %s should be removed for account %s,"
|
|
+ " but external id belongs to account %s",
|
|
extIdKey.get(),
|
|
expectedAccountId.get(),
|
|
extId.accountId().get());
|
|
}
|
|
noteMap.remove(noteId);
|
|
addFooters(footers, extId);
|
|
return extId;
|
|
}
|
|
|
|
private static void addFooters(Set<String> footers, ExternalId extId) {
|
|
footers.add("Account: " + extId.accountId().get());
|
|
if (extId.email() != null) {
|
|
footers.add("Email: " + extId.email());
|
|
}
|
|
}
|
|
|
|
private void checkExternalIdsDontExist(Collection<ExternalId> extIds)
|
|
throws DuplicateExternalIdKeyException, IOException {
|
|
checkExternalIdKeysDontExist(ExternalId.Key.from(extIds));
|
|
}
|
|
|
|
private void checkExternalIdKeysDontExist(
|
|
Collection<ExternalId.Key> extIdKeysToAdd, Collection<ExternalId.Key> extIdKeysToDelete)
|
|
throws DuplicateExternalIdKeyException, IOException {
|
|
HashSet<ExternalId.Key> newKeys = new HashSet<>(extIdKeysToAdd);
|
|
newKeys.removeAll(extIdKeysToDelete);
|
|
checkExternalIdKeysDontExist(newKeys);
|
|
}
|
|
|
|
private void checkExternalIdKeysDontExist(Collection<ExternalId.Key> extIdKeys)
|
|
throws IOException, DuplicateExternalIdKeyException {
|
|
for (ExternalId.Key extIdKey : extIdKeys) {
|
|
if (noteMap.contains(extIdKey.sha1())) {
|
|
throw new DuplicateExternalIdKeyException(extIdKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void checkLoaded() {
|
|
checkState(noteMap != null, "External IDs not loaded yet");
|
|
}
|
|
|
|
@FunctionalInterface
|
|
private interface NoteMapUpdate {
|
|
void execute(RevWalk rw, NoteMap noteMap, Set<String> footers)
|
|
throws IOException, ConfigInvalidException, DuplicateExternalIdKeyException;
|
|
}
|
|
|
|
@FunctionalInterface
|
|
private interface CacheUpdate {
|
|
void execute(ExternalIdCacheUpdates cacheUpdates) throws IOException;
|
|
}
|
|
|
|
private static class ExternalIdCacheUpdates {
|
|
private final Set<ExternalId> added = new HashSet<>();
|
|
private final Set<ExternalId> removed = new HashSet<>();
|
|
|
|
ExternalIdCacheUpdates add(Collection<ExternalId> extIds) {
|
|
this.added.addAll(extIds);
|
|
return this;
|
|
}
|
|
|
|
public Set<ExternalId> getAdded() {
|
|
return ImmutableSet.copyOf(added);
|
|
}
|
|
|
|
ExternalIdCacheUpdates remove(Collection<ExternalId> extIds) {
|
|
this.removed.addAll(extIds);
|
|
return this;
|
|
}
|
|
|
|
public Set<ExternalId> getRemoved() {
|
|
return ImmutableSet.copyOf(removed);
|
|
}
|
|
}
|
|
}
|