Java tutorial
// 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.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.gerrit.server.notedb.CommentsInNotesUtil.addCommentToMap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.PatchLineComment; import com.google.gerrit.reviewdb.client.PatchLineComment.Status; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.config.AnonymousCowardName; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.project.ChangeControl; import com.google.gwtorm.server.OrmException; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.notes.NoteMap; import org.eclipse.jgit.revwalk.RevCommit; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** * A single delta to apply atomically to a change. * <p> * This delta contains only draft comments on a single patch set of a change by * a single author. This delta will become a single commit in the All-Users * repository. * <p> * This class is not thread safe. */ public class ChangeDraftUpdate extends AbstractChangeUpdate { public interface Factory { ChangeDraftUpdate create(ChangeControl ctl, Date when); } private final AllUsersName draftsProject; private final Account.Id accountId; private final CommentsInNotesUtil commentsUtil; private final ChangeNotes changeNotes; private final DraftCommentNotes draftNotes; private List<PatchLineComment> upsertComments; private List<PatchLineComment> deleteComments; @AssistedInject private ChangeDraftUpdate(@GerritPersonIdent PersonIdent serverIdent, @AnonymousCowardName String anonymousCowardName, GitRepositoryManager repoManager, NotesMigration migration, MetaDataUpdate.User updateFactory, DraftCommentNotes.Factory draftNotesFactory, AllUsersName allUsers, CommentsInNotesUtil commentsUtil, @Assisted ChangeControl ctl, @Assisted Date when) throws OrmException { super(migration, repoManager, updateFactory, ctl, serverIdent, anonymousCowardName, when); this.draftsProject = allUsers; this.commentsUtil = commentsUtil; checkState(ctl.getCurrentUser().isIdentifiedUser(), "Current user must be identified"); IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser(); this.accountId = user.getAccountId(); this.changeNotes = getChangeNotes().load(); this.draftNotes = draftNotesFactory.create(ctl.getChange().getId(), user.getAccountId()); this.upsertComments = Lists.newArrayList(); this.deleteComments = Lists.newArrayList(); } public void insertComment(PatchLineComment c) throws OrmException { verifyComment(c); checkArgument(c.getStatus() == Status.DRAFT, "Cannot insert a published comment into a ChangeDraftUpdate"); if (migration.readChanges()) { checkArgument(!changeNotes.containsComment(c), "A comment already exists with the same key," + " so the following comment cannot be inserted: %s", c); } upsertComments.add(c); } public void upsertComment(PatchLineComment c) { verifyComment(c); checkArgument(c.getStatus() == Status.DRAFT, "Cannot upsert a published comment into a ChangeDraftUpdate"); upsertComments.add(c); } public void updateComment(PatchLineComment c) throws OrmException { verifyComment(c); checkArgument(c.getStatus() == Status.DRAFT, "Cannot update a published comment into a ChangeDraftUpdate"); // Here, we check to see if this comment existed previously as a draft. // However, this could cause a race condition if there is a delete and an // update operation happening concurrently (or two deletes) and they both // believe that the comment exists. If a delete happens first, then // the update will fail. However, this is an acceptable risk since the // caller wanted the comment deleted anyways, so the end result will be the // same either way. if (migration.readChanges()) { checkArgument(draftNotes.load().containsComment(c), "Cannot update this comment because it didn't exist previously"); } upsertComments.add(c); } public void deleteComment(PatchLineComment c) throws OrmException { verifyComment(c); // See the comment above about potential race condition. if (migration.readChanges()) { checkArgument(draftNotes.load().containsComment(c), "Cannot delete this comment because it didn't previously exist as a" + " draft"); } if (migration.writeChanges()) { if (draftNotes.load().containsComment(c)) { deleteComments.add(c); } } } /** * Deletes a PatchLineComment from the list of drafts only if it existed * previously as a draft. If it wasn't a draft previously, this is a no-op. */ public void deleteCommentIfPresent(PatchLineComment c) throws OrmException { if (draftNotes.load().containsComment(c)) { verifyComment(c); deleteComments.add(c); } } private void verifyComment(PatchLineComment comment) { if (migration.writeChanges()) { checkArgument(comment.getRevId() != null); } checkArgument(comment.getAuthor().equals(accountId), "The author for the following comment does not match the author of" + " this ChangeDraftUpdate (%s): %s", accountId, comment); } /** @return the tree id for the updated tree */ private ObjectId storeCommentsInNotes(AtomicBoolean removedAllComments) throws OrmException, IOException { if (isEmpty()) { return null; } NoteMap noteMap = draftNotes.load().getNoteMap(); if (noteMap == null) { noteMap = NoteMap.newEmptyMap(); } Map<RevId, List<PatchLineComment>> allComments = new HashMap<>(); boolean hasComments = false; int n = deleteComments.size() + upsertComments.size(); Set<RevId> updatedRevs = Sets.newHashSetWithExpectedSize(n); Set<PatchLineComment.Key> updatedKeys = Sets.newHashSetWithExpectedSize(n); for (PatchLineComment c : deleteComments) { allComments.put(c.getRevId(), new ArrayList<PatchLineComment>()); updatedRevs.add(c.getRevId()); updatedKeys.add(c.getKey()); } for (PatchLineComment c : upsertComments) { hasComments = true; addCommentToMap(allComments, c); updatedRevs.add(c.getRevId()); updatedKeys.add(c.getKey()); } // Re-add old comments for updated revisions so the new note contents // includes both old and new comments merged in the right order. // // writeCommentsToNoteMap doesn't touch notes for SHA-1s that are not // mentioned in the input map, so by omitting comments for those revisions, // we avoid the work of having to re-serialize identical comment data for // those revisions. ListMultimap<RevId, PatchLineComment> existing = draftNotes.getComments(); for (Map.Entry<RevId, PatchLineComment> e : existing.entries()) { PatchLineComment c = e.getValue(); if (updatedRevs.contains(c.getRevId()) && !updatedKeys.contains(c.getKey())) { hasComments = true; addCommentToMap(allComments, e.getValue()); } } // If we touched every revision and there are no comments left, set the flag // for the caller to delete the entire ref. boolean touchedAllRevs = updatedRevs.equals(existing.keySet()); if (touchedAllRevs && !hasComments) { removedAllComments.set(touchedAllRevs && !hasComments); return null; } commentsUtil.writeCommentsToNoteMap(noteMap, allComments, inserter); return noteMap.writeTree(inserter); } public RevCommit commit() throws IOException { BatchMetaDataUpdate batch = openUpdate(); try { writeCommit(batch); return batch.commit(); } catch (OrmException e) { throw new IOException(e); } finally { batch.close(); } } @Override public void writeCommit(BatchMetaDataUpdate batch) throws OrmException, IOException { CommitBuilder builder = new CommitBuilder(); if (migration.writeChanges()) { AtomicBoolean removedAllComments = new AtomicBoolean(); ObjectId treeId = storeCommentsInNotes(removedAllComments); if (removedAllComments.get()) { batch.removeRef(getRefName()); } else if (treeId != null) { builder.setTreeId(treeId); batch.write(builder); } } } @Override protected Project.NameKey getProjectName() { return draftsProject; } @Override protected String getRefName() { return RefNames.refsDraftComments(accountId, getChange().getId()); } @Override protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException { if (isEmpty()) { return false; } commit.setAuthor(newIdent(getUser().getAccount(), when)); commit.setCommitter(new PersonIdent(serverIdent, when)); commit.setMessage("Update draft comments"); return true; } private boolean isEmpty() { return deleteComments.isEmpty() && upsertComments.isEmpty(); } }