| // Copyright (C) 2016 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.checkNotNull; |
| import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef; |
| import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Predicates; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.reviewdb.client.Account; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.server.ReviewDbUtil; |
| import com.google.gerrit.server.git.RefCache; |
| |
| import org.eclipse.jgit.lib.ObjectId; |
| |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * The state of all relevant NoteDb refs across all repos corresponding to a |
| * given Change entity. |
| * <p> |
| * Stored serialized in the {@code Change#noteDbState} field, and used to |
| * determine whether the state in NoteDb is out of date. |
| * <p> |
| * Serialized in the form: |
| * <pre> |
| * [meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]... |
| * </pre> |
| * in numeric account ID order, with hex SHA-1s for human readability. |
| */ |
| public class NoteDbChangeState { |
| @AutoValue |
| public abstract static class Delta { |
| static Delta create(Change.Id changeId, Optional<ObjectId> newChangeMetaId, |
| Map<Account.Id, ObjectId> newDraftIds) { |
| if (newDraftIds == null) { |
| newDraftIds = ImmutableMap.of(); |
| } |
| return new AutoValue_NoteDbChangeState_Delta( |
| changeId, |
| newChangeMetaId, |
| ImmutableMap.copyOf(newDraftIds)); |
| } |
| |
| abstract Change.Id changeId(); |
| abstract Optional<ObjectId> newChangeMetaId(); |
| abstract ImmutableMap<Account.Id, ObjectId> newDraftIds(); |
| } |
| |
| public static NoteDbChangeState parse(Change c) { |
| return parse(c.getId(), c.getNoteDbState()); |
| } |
| |
| @VisibleForTesting |
| static NoteDbChangeState parse(Change.Id id, String str) { |
| if (str == null) { |
| return null; |
| } |
| List<String> parts = Splitter.on(',').splitToList(str); |
| checkArgument(!parts.isEmpty(), |
| "invalid state string for change %s: %s", id, str); |
| ObjectId changeMetaId = ObjectId.fromString(parts.get(0)); |
| Map<Account.Id, ObjectId> draftIds = |
| Maps.newHashMapWithExpectedSize(parts.size() - 1); |
| Splitter s = Splitter.on('='); |
| for (int i = 1; i < parts.size(); i++) { |
| String p = parts.get(i); |
| List<String> draftParts = s.splitToList(p); |
| checkArgument(draftParts.size() == 2, |
| "invalid draft state part for change %s: %s", id, p); |
| draftIds.put(Account.Id.parse(draftParts.get(0)), |
| ObjectId.fromString(draftParts.get(1))); |
| } |
| return new NoteDbChangeState(id, changeMetaId, draftIds); |
| } |
| |
| public static NoteDbChangeState applyDelta(Change change, Delta delta) { |
| if (delta == null) { |
| return null; |
| } |
| String oldStr = change.getNoteDbState(); |
| if (oldStr == null && !delta.newChangeMetaId().isPresent()) { |
| // Neither an old nor a new meta ID was present, most likely because we |
| // aren't writing a NoteDb graph at all for this change at this point. No |
| // point in proceeding. |
| return null; |
| } |
| NoteDbChangeState oldState = parse(change.getId(), oldStr); |
| |
| ObjectId changeMetaId; |
| if (delta.newChangeMetaId().isPresent()) { |
| changeMetaId = delta.newChangeMetaId().get(); |
| if (changeMetaId.equals(ObjectId.zeroId())) { |
| change.setNoteDbState(null); |
| return null; |
| } |
| } else { |
| changeMetaId = oldState.changeMetaId; |
| } |
| |
| Map<Account.Id, ObjectId> draftIds = new HashMap<>(); |
| if (oldState != null) { |
| draftIds.putAll(oldState.draftIds); |
| } |
| for (Map.Entry<Account.Id, ObjectId> e : delta.newDraftIds().entrySet()) { |
| if (e.getValue().equals(ObjectId.zeroId())) { |
| draftIds.remove(e.getKey()); |
| } else { |
| draftIds.put(e.getKey(), e.getValue()); |
| } |
| } |
| |
| NoteDbChangeState state = new NoteDbChangeState( |
| change.getId(), changeMetaId, draftIds); |
| change.setNoteDbState(state.toString()); |
| return state; |
| } |
| |
| public static boolean isChangeUpToDate(@Nullable NoteDbChangeState state, |
| RefCache changeRepoRefs, Change.Id changeId) throws IOException { |
| if (state == null) { |
| return !changeRepoRefs.get(changeMetaRef(changeId)).isPresent(); |
| } |
| return state.isChangeUpToDate(changeRepoRefs); |
| } |
| |
| public static boolean areDraftsUpToDate(@Nullable NoteDbChangeState state, |
| RefCache draftsRepoRefs, Change.Id changeId, Account.Id accountId) |
| throws IOException { |
| if (state == null) { |
| return !draftsRepoRefs.get(refsDraftComments(changeId, accountId)) |
| .isPresent(); |
| } |
| return state.areDraftsUpToDate(draftsRepoRefs, accountId); |
| } |
| |
| public static String toString(ObjectId changeMetaId, |
| Map<Account.Id, ObjectId> draftIds) { |
| List<Account.Id> accountIds = Lists.newArrayList(draftIds.keySet()); |
| Collections.sort(accountIds, ReviewDbUtil.intKeyOrdering()); |
| StringBuilder sb = new StringBuilder(changeMetaId.name()); |
| for (Account.Id id : accountIds) { |
| sb.append(',') |
| .append(id.get()) |
| .append('=') |
| .append(draftIds.get(id).name()); |
| } |
| return sb.toString(); |
| } |
| |
| private final Change.Id changeId; |
| private final ObjectId changeMetaId; |
| private final ImmutableMap<Account.Id, ObjectId> draftIds; |
| |
| public NoteDbChangeState(Change.Id changeId, ObjectId changeMetaId, |
| Map<Account.Id, ObjectId> draftIds) { |
| this.changeId = checkNotNull(changeId); |
| this.changeMetaId = checkNotNull(changeMetaId); |
| this.draftIds = ImmutableMap.copyOf(Maps.filterValues( |
| draftIds, Predicates.not(Predicates.equalTo(ObjectId.zeroId())))); |
| } |
| |
| public boolean isChangeUpToDate(RefCache changeRepoRefs) throws IOException { |
| Optional<ObjectId> id = changeRepoRefs.get(changeMetaRef(changeId)); |
| if (!id.isPresent()) { |
| return changeMetaId.equals(ObjectId.zeroId()); |
| } |
| return id.get().equals(changeMetaId); |
| } |
| |
| public boolean areDraftsUpToDate(RefCache draftsRepoRefs, Account.Id accountId) |
| throws IOException { |
| Optional<ObjectId> id = |
| draftsRepoRefs.get(refsDraftComments(changeId, accountId)); |
| if (!id.isPresent()) { |
| return !draftIds.containsKey(accountId); |
| } |
| return id.get().equals(draftIds.get(accountId)); |
| } |
| |
| boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs) |
| throws IOException { |
| if (!isChangeUpToDate(changeRepoRefs)) { |
| return false; |
| } |
| for (Account.Id accountId : draftIds.keySet()) { |
| if (!areDraftsUpToDate(draftsRepoRefs, accountId)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @VisibleForTesting |
| Change.Id getChangeId() { |
| return changeId; |
| } |
| |
| @VisibleForTesting |
| public ObjectId getChangeMetaId() { |
| return changeMetaId; |
| } |
| |
| @VisibleForTesting |
| ImmutableMap<Account.Id, ObjectId> getDraftIds() { |
| return draftIds; |
| } |
| |
| @Override |
| public String toString() { |
| return toString(changeMetaId, draftIds); |
| } |
| } |