// Copyright (C) 2014 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.notedb; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES; import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.gerrit.common.Nullable; import com.google.gerrit.metrics.Timer1; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; /** View of contents at a single ref related to some change. * */ public abstract class AbstractChangeNotes<T> { @VisibleForTesting @Singleton public static class Args { final GitRepositoryManager repoManager; final NotesMigration migration; final AllUsersName allUsers; final ChangeNoteUtil noteUtil; final NoteDbMetrics metrics; final Provider<ReviewDb> db; // Providers required to avoid dependency cycles. // ChangeRebuilder -> ChangeNotes.Factory -> Args final Provider<ChangeRebuilder> rebuilder; // ChangeNoteCache -> Args final Provider<ChangeNotesCache> cache; @Inject Args( GitRepositoryManager repoManager, NotesMigration migration, AllUsersName allUsers, ChangeNoteUtil noteUtil, NoteDbMetrics metrics, Provider<ReviewDb> db, Provider<ChangeRebuilder> rebuilder, Provider<ChangeNotesCache> cache) { this.repoManager = repoManager; this.migration = migration; this.allUsers = allUsers; this.noteUtil = noteUtil; this.metrics = metrics; this.db = db; this.rebuilder = rebuilder; this.cache = cache; } } @AutoValue public abstract static class LoadHandle implements AutoCloseable { public static LoadHandle create(ChangeNotesRevWalk walk, ObjectId id) { if (ObjectId.zeroId().equals(id)) { id = null; } else if (id != null) { id = id.copy(); } return new AutoValue_AbstractChangeNotes_LoadHandle(checkNotNull(walk), id); } public static LoadHandle missing() { return new AutoValue_AbstractChangeNotes_LoadHandle(null, null); } @Nullable public abstract ChangeNotesRevWalk walk(); @Nullable public abstract ObjectId id(); @Override public void close() { if (walk() != null) { walk().close(); } } } protected final Args args; protected final PrimaryStorage primaryStorage; protected final boolean autoRebuild; private final Change.Id changeId; private ObjectId revision; private boolean loaded; AbstractChangeNotes( Args args, Change.Id changeId, @Nullable PrimaryStorage primaryStorage, boolean autoRebuild) { this.args = checkNotNull(args); this.changeId = checkNotNull(changeId); this.primaryStorage = primaryStorage; this.autoRebuild = primaryStorage == PrimaryStorage.REVIEW_DB && !args.migration.disableChangeReviewDb() && autoRebuild; } public Change.Id getChangeId() { return changeId; } /** @return revision of the metadata that was loaded. */ public ObjectId getRevision() { return revision; } public T load() throws OrmException { if (loaded) { return self(); } boolean read = args.migration.readChanges(); if (!read && primaryStorage == PrimaryStorage.NOTE_DB) { throw new OrmException("NoteDb is required to read change " + changeId); } boolean readOrWrite = read || args.migration.rawWriteChangesSetting(); if (!readOrWrite && !autoRebuild) { loadDefaults(); return self(); } if (args.migration.failOnLoad()) { throw new OrmException("Reading from NoteDb is disabled"); } try (Timer1.Context timer = args.metrics.readLatency.start(CHANGES); Repository repo = args.repoManager.openRepository(getProjectName()); // Call openHandle even if reading is disabled, to trigger // auto-rebuilding before this object may get passed to a ChangeUpdate. LoadHandle handle = openHandle(repo)) { if (read) { revision = handle.id(); onLoad(handle); } else { loadDefaults(); } loaded = true; } catch (ConfigInvalidException | IOException e) { throw new OrmException(e); } return self(); } protected ObjectId readRef(Repository repo) throws IOException { Ref ref = repo.getRefDatabase().exactRef(getRefName()); return ref != null ? ref.getObjectId() : null; } /** * Open a handle for reading this entity from a repository. * * <p>Implementations may override this method to provide auto-rebuilding behavior. * * @param repo open repository. * @return handle for reading the entity. * @throws NoSuchChangeException change does not exist. * @throws IOException a repo-level error occurred. */ protected LoadHandle openHandle(Repository repo) throws NoSuchChangeException, IOException { return openHandle(repo, readRef(repo)); } protected LoadHandle openHandle(Repository repo, ObjectId id) { return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), id); } public T reload() throws NoSuchChangeException, OrmException { loaded = false; return load(); } public ObjectId loadRevision() throws OrmException { if (loaded) { return getRevision(); } else if (!args.migration.enabled()) { return null; } try (Repository repo = args.repoManager.openRepository(getProjectName())) { Ref ref = repo.getRefDatabase().exactRef(getRefName()); return ref != null ? ref.getObjectId() : null; } catch (IOException e) { throw new OrmException(e); } } /** Load default values for any instance variables when NoteDb is disabled. */ protected abstract void loadDefaults(); /** * @return the NameKey for the project where the notes should be stored, which is not necessarily * the same as the change's project. */ public abstract Project.NameKey getProjectName(); /** @return name of the reference storing this configuration. */ protected abstract String getRefName(); /** Set up the metadata, parsing any state from the loaded revision. */ protected abstract void onLoad(LoadHandle handle) throws NoSuchChangeException, IOException, ConfigInvalidException; @SuppressWarnings("unchecked") protected final T self() { return (T) this; } }