blob: ad57e2421507992e00382fa48824dda76ba18371 [file] [log] [blame]
// 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.common.base.Preconditions.checkState;
import static com.google.gerrit.common.TimeUtil.roundToSecond;
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
import com.google.common.base.Joiner;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.PatchLineCommentsUtil;
import com.google.gwtorm.client.Column;
import com.google.gwtorm.server.OrmException;
import java.lang.reflect.Field;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* A bundle of all entities rooted at a single {@link Change} entity.
* <p>
* See the {@link Change} Javadoc for a depiction of this tree. Bundles may be
* compared using {@link #differencesFrom(ChangeBundle)}, which normalizes out
* the minor implementation differences between ReviewDb and NoteDb.
*/
public class ChangeBundle {
public enum Source {
REVIEW_DB, NOTE_DB;
}
public static ChangeBundle fromReviewDb(ReviewDb db, Change.Id id)
throws OrmException {
db.changes().beginTransaction(id);
try {
return new ChangeBundle(
db.changes().get(id),
db.changeMessages().byChange(id),
db.patchSets().byChange(id),
db.patchSetApprovals().byChange(id),
db.patchComments().byChange(id),
Source.REVIEW_DB);
} finally {
db.rollback();
}
}
public static ChangeBundle fromNotes(PatchLineCommentsUtil plcUtil,
ChangeNotes notes) throws OrmException {
return new ChangeBundle(
notes.getChange(),
notes.getChangeMessages(),
notes.getPatchSets().values(),
notes.getApprovals().values(),
Iterables.concat(
plcUtil.draftByChange(null, notes),
plcUtil.publishedByChange(null, notes)),
Source.NOTE_DB);
}
private static Map<ChangeMessage.Key, ChangeMessage> changeMessageMap(
Iterable<ChangeMessage> in) {
Map<ChangeMessage.Key, ChangeMessage> out = new TreeMap<>(
new Comparator<ChangeMessage.Key>() {
@Override
public int compare(ChangeMessage.Key a, ChangeMessage.Key b) {
return ComparisonChain.start()
.compare(a.getParentKey().get(), b.getParentKey().get())
.compare(a.get(), b.get())
.result();
}
});
for (ChangeMessage cm : in) {
out.put(cm.getKey(), cm);
}
return out;
}
private static ImmutableList<ChangeMessage> changeMessageList(
Iterable<ChangeMessage> in) {
// Unlike the *Map comparators, which are intended to make key lists
// diffable, this comparator sorts first on timestamp, then on every other
// field.
final Ordering<Comparable<?>> nullsFirst = Ordering.natural().nullsFirst();
return new Ordering<ChangeMessage>() {
@Override
public int compare(ChangeMessage a, ChangeMessage b) {
return ComparisonChain.start()
.compare(roundToSecond(a.getWrittenOn()),
roundToSecond(b.getWrittenOn()))
.compare(a.getKey().getParentKey().get(),
b.getKey().getParentKey().get())
.compare(psId(a), psId(b), nullsFirst)
.compare(a.getAuthor(), b.getAuthor(), intKeyOrdering())
.compare(a.getMessage(), b.getMessage(), nullsFirst)
.result();
}
private Integer psId(ChangeMessage m) {
return m.getPatchSetId() != null ? m.getPatchSetId().get() : null;
}
}.immutableSortedCopy(in);
}
private static Map<PatchSet.Id, PatchSet> patchSetMap(Iterable<PatchSet> in) {
Map<PatchSet.Id, PatchSet> out = new TreeMap<>(
new Comparator<PatchSet.Id>() {
@Override
public int compare(PatchSet.Id a, PatchSet.Id b) {
return patchSetIdChain(a, b).result();
}
});
for (PatchSet ps : in) {
out.put(ps.getId(), ps);
}
return out;
}
private static Map<PatchSetApproval.Key, PatchSetApproval>
patchSetApprovalMap(Iterable<PatchSetApproval> in) {
Map<PatchSetApproval.Key, PatchSetApproval> out = new TreeMap<>(
new Comparator<PatchSetApproval.Key>() {
@Override
public int compare(PatchSetApproval.Key a, PatchSetApproval.Key b) {
return patchSetIdChain(a.getParentKey(), b.getParentKey())
.compare(a.getAccountId().get(), b.getAccountId().get())
.compare(a.getLabelId(), b.getLabelId())
.result();
}
});
for (PatchSetApproval psa : in) {
out.put(psa.getKey(), psa);
}
return out;
}
private static Map<PatchLineComment.Key, PatchLineComment>
patchLineCommentMap(Iterable<PatchLineComment> in) {
Map<PatchLineComment.Key, PatchLineComment> out = new TreeMap<>(
new Comparator<PatchLineComment.Key>() {
@Override
public int compare(PatchLineComment.Key a, PatchLineComment.Key b) {
Patch.Key pka = a.getParentKey();
Patch.Key pkb = b.getParentKey();
return patchSetIdChain(pka.getParentKey(), pkb.getParentKey())
.compare(pka.get(), pkb.get())
.compare(a.get(), b.get())
.result();
}
});
for (PatchLineComment plc : in) {
out.put(plc.getKey(), plc);
}
return out;
}
private static ComparisonChain patchSetIdChain(PatchSet.Id a, PatchSet.Id b) {
return ComparisonChain.start()
.compare(a.getParentKey().get(), b.getParentKey().get())
.compare(a.get(), b.get());
}
private static void checkColumns(Class<?> clazz, Integer... expected) {
Set<Integer> ids = new TreeSet<>();
for (Field f : clazz.getDeclaredFields()) {
Column col = f.getAnnotation(Column.class);
if (col != null) {
ids.add(col.id());
}
}
Set<Integer> expectedIds = Sets.newTreeSet(Arrays.asList(expected));
checkState(ids.equals(expectedIds),
"Unexpected column set for %s: %s != %s",
clazz.getSimpleName(), ids, expectedIds);
}
static {
// Initialization-time checks that the column set hasn't changed since the
// last time this file was updated.
checkColumns(Change.Id.class, 1);
checkColumns(Change.class,
1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18,
// TODO(dborowitz): It's potentially possible to compare noteDbState in
// the Change with the state implied by a ChangeNotes.
101);
checkColumns(ChangeMessage.Key.class, 1, 2);
checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5, 6);
checkColumns(PatchSet.Id.class, 1, 2);
checkColumns(PatchSet.class, 1, 2, 3, 4, 5, 6, 8);
checkColumns(PatchSetApproval.Key.class, 1, 2, 3);
checkColumns(PatchSetApproval.class, 1, 2, 3, 6);
checkColumns(PatchLineComment.Key.class, 1, 2);
checkColumns(PatchLineComment.class, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
}
private final Change change;
private final ImmutableList<ChangeMessage> changeMessages;
private final ImmutableMap<PatchSet.Id, PatchSet> patchSets;
private final ImmutableMap<PatchSetApproval.Key, PatchSetApproval>
patchSetApprovals;
private final ImmutableMap<PatchLineComment.Key, PatchLineComment>
patchLineComments;
private final Source source;
public ChangeBundle(
Change change,
Iterable<ChangeMessage> changeMessages,
Iterable<PatchSet> patchSets,
Iterable<PatchSetApproval> patchSetApprovals,
Iterable<PatchLineComment> patchLineComments,
Source source) {
this.change = checkNotNull(change);
this.changeMessages = changeMessageList(changeMessages);
this.patchSets = ImmutableMap.copyOf(patchSetMap(patchSets));
this.patchSetApprovals =
ImmutableMap.copyOf(patchSetApprovalMap(patchSetApprovals));
this.patchLineComments =
ImmutableMap.copyOf(patchLineCommentMap(patchLineComments));
this.source = checkNotNull(source);
for (ChangeMessage m : this.changeMessages) {
checkArgument(m.getKey().getParentKey().equals(change.getId()));
}
for (PatchSet.Id id : this.patchSets.keySet()) {
checkArgument(id.getParentKey().equals(change.getId()));
}
for (PatchSetApproval.Key k : this.patchSetApprovals.keySet()) {
checkArgument(k.getParentKey().getParentKey().equals(change.getId()));
}
for (PatchLineComment.Key k : this.patchLineComments.keySet()) {
checkArgument(k.getParentKey().getParentKey().getParentKey()
.equals(change.getId()));
}
}
public Change getChange() {
return change;
}
public ImmutableCollection<ChangeMessage> getChangeMessages() {
return changeMessages;
}
public ImmutableCollection<PatchSet> getPatchSets() {
return patchSets.values();
}
public ImmutableCollection<PatchSetApproval> getPatchSetApprovals() {
return patchSetApprovals.values();
}
public ImmutableCollection<PatchLineComment> getPatchLineComments() {
return patchLineComments.values();
}
public Source getSource() {
return source;
}
public ImmutableList<String> differencesFrom(ChangeBundle o) {
List<String> diffs = new ArrayList<>();
diffChanges(diffs, this, o);
diffChangeMessages(diffs, this, o);
diffPatchSets(diffs, this, o);
diffPatchSetApprovals(diffs, this, o);
diffPatchLineComments(diffs, this, o);
return ImmutableList.copyOf(diffs);
}
private static void diffChanges(List<String> diffs, ChangeBundle bundleA,
ChangeBundle bundleB) {
Change a = bundleA.change;
Change b = bundleB.change;
String desc = a.getId().equals(b.getId()) ? describe(a.getId()) : "Changes";
diffColumnsExcluding(diffs, Change.class, desc, bundleA, a, bundleB, b,
"rowVersion", "noteDbState");
}
private static void diffChangeMessages(List<String> diffs,
ChangeBundle bundleA, ChangeBundle bundleB) {
if (bundleA.source == REVIEW_DB && bundleB.source == REVIEW_DB) {
// Both came from ReviewDb: check all fields exactly.
Map<ChangeMessage.Key, ChangeMessage> as =
changeMessageMap(bundleA.changeMessages);
Map<ChangeMessage.Key, ChangeMessage> bs =
changeMessageMap(bundleB.changeMessages);
for (ChangeMessage.Key k : diffKeySets(diffs, as, bs)) {
ChangeMessage a = as.get(k);
ChangeMessage b = bs.get(k);
String desc = describe(k);
diffColumns(diffs, ChangeMessage.class, desc, bundleA, a, bundleB, b);
}
return;
}
// At least one is from NoteDb, so we need to ignore UUIDs for both, and
// allow timestamp slop if the sources differ.
Change.Id id = bundleA.getChange().getId();
checkArgument(id.equals(bundleB.getChange().getId()));
List<ChangeMessage> as = bundleA.changeMessages;
List<ChangeMessage> bs = bundleB.changeMessages;
if (as.size() != bs.size()) {
Joiner j = Joiner.on("\n");
diffs.add("Differing numbers of ChangeMessages for Change.Id " + id
+ ":\n" + j.join(as) + "\n--- vs. ---\n" + j.join(bs));
return;
}
for (int i = 0; i < as.size(); i++) {
ChangeMessage a = as.get(i);
ChangeMessage b = bs.get(i);
String desc = "ChangeMessage on " + id + " at index " + i;
diffColumnsExcluding(diffs, ChangeMessage.class, desc, bundleA, a,
bundleB, b, "key");
}
}
private static void diffPatchSets(List<String> diffs, ChangeBundle bundleA,
ChangeBundle bundleB) {
Map<PatchSet.Id, PatchSet> as = bundleA.patchSets;
Map<PatchSet.Id, PatchSet> bs = bundleB.patchSets;
for (PatchSet.Id id : diffKeySets(diffs, as, bs)) {
PatchSet a = as.get(id);
PatchSet b = bs.get(id);
String desc = describe(id);
diffColumns(diffs, PatchSet.class, desc, bundleA, a, bundleB, b);
}
}
private static void diffPatchSetApprovals(List<String> diffs,
ChangeBundle bundleA, ChangeBundle bundleB) {
Map<PatchSetApproval.Key, PatchSetApproval> as = bundleA.patchSetApprovals;
Map<PatchSetApproval.Key, PatchSetApproval> bs = bundleB.patchSetApprovals;
for (PatchSetApproval.Key k : diffKeySets(diffs, as, bs)) {
PatchSetApproval a = as.get(k);
PatchSetApproval b = bs.get(k);
String desc = describe(k);
diffColumns(diffs, PatchSetApproval.class, desc, bundleA, a, bundleB, b);
}
}
private static void diffPatchLineComments(List<String> diffs,
ChangeBundle bundleA, ChangeBundle bundleB) {
Map<PatchLineComment.Key, PatchLineComment> as = bundleA.patchLineComments;
Map<PatchLineComment.Key, PatchLineComment> bs = bundleB.patchLineComments;
for (PatchLineComment.Key k : diffKeySets(diffs, as, bs)) {
PatchLineComment a = as.get(k);
PatchLineComment b = bs.get(k);
String desc = describe(k);
diffColumns(diffs, PatchLineComment.class, desc, bundleA, a, bundleB, b);
}
}
private static <T> Set<T> diffKeySets(List<String> diffs, Map<T, ?> a,
Map<T, ?> b) {
Set<T> as = a.keySet();
Set<T> bs = b.keySet();
if (as.isEmpty() && bs.isEmpty()) {
return as;
}
String clazz = keyClass((!as.isEmpty() ? as : bs).iterator().next());
Set<T> aNotB = Sets.difference(as, bs);
Set<T> bNotA = Sets.difference(bs, as);
if (aNotB.isEmpty() && bNotA.isEmpty()) {
return as;
}
diffs.add(clazz + " sets differ: " + aNotB + " only in A; "
+ bNotA + " only in B");
return Sets.intersection(as, bs);
}
private static <T> void diffColumns(List<String> diffs, Class<T> clazz,
String desc, ChangeBundle bundleA, T a, ChangeBundle bundleB, T b) {
diffColumnsExcluding(diffs, clazz, desc, bundleA, a, bundleB, b);
}
private static <T> void diffColumnsExcluding(List<String> diffs,
Class<T> clazz, String desc, ChangeBundle bundleA, T a,
ChangeBundle bundleB, T b, String... exclude) {
Set<String> toExclude = Sets.newLinkedHashSet(Arrays.asList(exclude));
for (Field f : clazz.getDeclaredFields()) {
Column col = f.getAnnotation(Column.class);
if (col == null) {
continue;
} else if (toExclude.remove(f.getName())) {
continue;
}
f.setAccessible(true);
try {
if (Timestamp.class.isAssignableFrom(f.getType())) {
diffTimestamps(diffs, desc, bundleA, a, bundleB, b, f.getName());
} else {
diffValues(diffs, desc, f.get(a), f.get(b), f.getName());
}
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
}
checkArgument(toExclude.isEmpty(),
"requested columns to exclude not present in %s: %s",
clazz.getSimpleName(), toExclude);
}
private static void diffTimestamps(List<String> diffs, String desc,
ChangeBundle bundleA, Object a, ChangeBundle bundleB, Object b,
String field) {
checkArgument(a.getClass() == b.getClass());
Class<?> clazz = a.getClass();
Timestamp ta;
Timestamp tb;
try {
Field f = clazz.getDeclaredField(field);
checkArgument(f.getAnnotation(Column.class) != null);
f.setAccessible(true);
ta = (Timestamp) f.get(a);
tb = (Timestamp) f.get(b);
} catch (IllegalAccessException | NoSuchFieldException
| SecurityException e) {
throw new IllegalArgumentException(e);
}
if (bundleA.source == bundleB.source || ta == null || tb == null) {
diffValues(diffs, desc, ta, tb, field);
} else if (bundleA.source == NOTE_DB) {
diffTimestamps(diffs, desc, ta, tb, field);
} else {
diffTimestamps(diffs, desc, tb, ta, field);
}
}
private static void diffTimestamps(List<String> diffs, String desc,
Timestamp tsFromNoteDb, Timestamp tsFromReviewDb, String field) {
// Because ChangeRebuilder may batch events together that are several
// seconds apart, the timestamp in NoteDb may actually be several seconds
// *earlier* than the timestamp in ReviewDb that it was converted from.
checkArgument(tsFromNoteDb.equals(roundToSecond(tsFromNoteDb)),
"%s from NoteDb has non-rounded %s timestamp: %s",
desc, field, tsFromNoteDb);
long delta = tsFromReviewDb.getTime() - tsFromNoteDb.getTime();
long max = ChangeRebuilderImpl.MAX_WINDOW_MS;
if (delta < 0 || delta > max) {
diffs.add(
field + " differs for " + desc + " in NoteDb vs. ReviewDb:"
+ " {" + tsFromNoteDb + "} != {" + tsFromReviewDb + "}");
}
}
private static void diffValues(List<String> diffs, String desc, Object va,
Object vb, String name) {
if (!Objects.equals(va, vb)) {
diffs.add(
name + " differs for " + desc + ": {" + va + "} != {" + vb + "}");
}
}
private static String describe(Object key) {
return keyClass(key) + " " + key;
}
private static String keyClass(Object obj) {
Class<?> clazz = obj.getClass();
String name = clazz.getSimpleName();
checkArgument(name.equals("Key") || name.equals("Id"),
"not an Id/Key class: %s", name);
return clazz.getEnclosingClass().getSimpleName() + "." + name;
}
@Override
public String toString() {
return getClass().getSimpleName() + "{id=" + change.getId()
+ ", ChangeMessage[" + changeMessages.size() + "]"
+ ", PatchSet[" + patchSets.size() + "]"
+ ", PatchSetApproval[" + patchSetApprovals.size() + "]"
+ ", PatchLineComment[" + patchLineComments.size() + "]"
+ "}";
}
}