diff options
Diffstat (limited to 'java/com/google/gerrit/server/schema/GroupBundle.java')
-rw-r--r-- | java/com/google/gerrit/server/schema/GroupBundle.java | 768 |
1 files changed, 768 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/schema/GroupBundle.java b/java/com/google/gerrit/server/schema/GroupBundle.java new file mode 100644 index 0000000000..4b8bdcc032 --- /dev/null +++ b/java/com/google/gerrit/server/schema/GroupBundle.java @@ -0,0 +1,768 @@ +// 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.schema; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.gerrit.reviewdb.server.ReviewDbUtil.checkColumns; +import static java.util.Comparator.naturalOrder; +import static java.util.Comparator.nullsLast; +import static java.util.stream.Collectors.toList; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Multimaps; +import com.google.common.collect.Streams; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.AccountGroupById; +import com.google.gerrit.reviewdb.client.AccountGroupByIdAud; +import com.google.gerrit.reviewdb.client.AccountGroupMember; +import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.reviewdb.server.ReviewDbWrapper; +import com.google.gerrit.server.group.InternalGroup; +import com.google.gerrit.server.group.db.AuditLogReader; +import com.google.gerrit.server.group.db.GroupConfig; +import com.google.gerrit.server.util.time.TimeUtil; +import com.google.gwtorm.jdbc.JdbcSchema; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Repository; + +/** + * A bundle of all entities rooted at a single {@link AccountGroup} entity. + * + * <p>Used primarily during the migration process. Most callers should prefer {@link InternalGroup} + * instead. + */ +@AutoValue +abstract class GroupBundle { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + static { + // Initialization-time checks that the column set hasn't changed since the + // last time this file was updated. + checkColumns(AccountGroup.NameKey.class, 1); + checkColumns(AccountGroup.UUID.class, 1); + checkColumns(AccountGroup.Id.class, 1); + checkColumns(AccountGroup.class, 1, 2, 4, 7, 9, 10, 11); + + checkColumns(AccountGroupById.Key.class, 1, 2); + checkColumns(AccountGroupById.class, 1); + + checkColumns(AccountGroupByIdAud.Key.class, 1, 2, 3); + checkColumns(AccountGroupByIdAud.class, 1, 2, 3, 4); + + checkColumns(AccountGroupMember.Key.class, 1, 2); + checkColumns(AccountGroupMember.class, 1); + + checkColumns(AccountGroupMemberAudit.Key.class, 1, 2, 3); + checkColumns(AccountGroupMemberAudit.class, 1, 2, 3, 4); + } + + public enum Source { + REVIEW_DB("ReviewDb"), + NOTE_DB("NoteDb"); + + private final String name; + + private Source(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + @Singleton + public static class Factory { + private final AuditLogReader auditLogReader; + + @Inject + Factory(AuditLogReader auditLogReader) { + this.auditLogReader = auditLogReader; + } + + public GroupBundle fromNoteDb( + Project.NameKey projectName, Repository repo, AccountGroup.UUID uuid) + throws ConfigInvalidException, IOException { + GroupConfig groupConfig = GroupConfig.loadForGroup(projectName, repo, uuid); + InternalGroup internalGroup = groupConfig.getLoadedGroup().get(); + AccountGroup.Id groupId = internalGroup.getId(); + + AccountGroup accountGroup = + new AccountGroup( + internalGroup.getNameKey(), + internalGroup.getId(), + internalGroup.getGroupUUID(), + internalGroup.getCreatedOn()); + accountGroup.setDescription(internalGroup.getDescription()); + accountGroup.setOwnerGroupUUID(internalGroup.getOwnerGroupUUID()); + accountGroup.setVisibleToAll(internalGroup.isVisibleToAll()); + + return create( + Source.NOTE_DB, + accountGroup, + internalGroup.getMembers().stream() + .map( + accountId -> + new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId))) + .collect(toImmutableSet()), + auditLogReader.getMembersAudit(repo, uuid), + internalGroup.getSubgroups().stream() + .map( + subgroupUuid -> + new AccountGroupById(new AccountGroupById.Key(groupId, subgroupUuid))) + .collect(toImmutableSet()), + auditLogReader.getSubgroupsAudit(repo, uuid)); + } + + public static GroupBundle fromReviewDb(ReviewDb db, AccountGroup.UUID groupUuid) + throws OrmException { + JdbcSchema jdbcSchema = ReviewDbWrapper.unwrapJbdcSchema(db); + AccountGroup group = readAccountGroupFromReviewDb(jdbcSchema, groupUuid); + AccountGroup.Id groupId = group.getId(); + + return create( + Source.REVIEW_DB, + group, + readAccountGroupMembersFromReviewDb(jdbcSchema, groupId), + readAccountGroupMemberAuditsFromReviewDb(jdbcSchema, groupId), + readAccountGroupSubgroupsFromReviewDb(jdbcSchema, groupId), + readAccountGroupSubgroupAuditsFromReviewDb(jdbcSchema, groupId)); + } + + private static AccountGroup readAccountGroupFromReviewDb( + JdbcSchema jdbcSchema, AccountGroup.UUID groupUuid) throws OrmException { + try (Statement stmt = jdbcSchema.getConnection().createStatement(); + ResultSet rs = + stmt.executeQuery( + "SELECT group_id," + + " name," + + " created_on," + + " description," + + " owner_group_uuid," + + " visible_to_all" + + " FROM account_groups" + + " WHERE group_uuid = '" + + groupUuid.get() + + "'")) { + if (!rs.next()) { + throw new OrmException(String.format("Group %s not found", groupUuid)); + } + + AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1)); + AccountGroup.NameKey groupName = new AccountGroup.NameKey(rs.getString(2)); + Timestamp createdOn = rs.getTimestamp(3); + String description = rs.getString(4); + AccountGroup.UUID ownerGroupUuid = new AccountGroup.UUID(rs.getString(5)); + boolean visibleToAll = "Y".equals(rs.getString(6)); + + AccountGroup group = new AccountGroup(groupName, groupId, groupUuid, createdOn); + group.setDescription(description); + group.setOwnerGroupUUID(ownerGroupUuid); + group.setVisibleToAll(visibleToAll); + + if (rs.next()) { + throw new OrmException(String.format("Group UUID %s is ambiguous", groupUuid)); + } + + return group; + } catch (SQLException e) { + throw new OrmException( + String.format("Failed to read account group %s from ReviewDb", groupUuid.get()), e); + } + } + + private static List<AccountGroupMember> readAccountGroupMembersFromReviewDb( + JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException { + try (Statement stmt = jdbcSchema.getConnection().createStatement(); + ResultSet rs = + stmt.executeQuery( + "SELECT account_id" + + " FROM account_group_members" + + " WHERE group_id = '" + + groupId.get() + + "'")) { + List<AccountGroupMember> members = new ArrayList<>(); + while (rs.next()) { + Account.Id accountId = new Account.Id(rs.getInt(1)); + members.add(new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId))); + } + return members; + } catch (SQLException e) { + throw new OrmException( + String.format( + "Failed to read members of account group %s from ReviewDb", groupId.get()), + e); + } + } + + private static List<AccountGroupMemberAudit> readAccountGroupMemberAuditsFromReviewDb( + JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException { + try (Statement stmt = jdbcSchema.getConnection().createStatement(); + ResultSet rs = + stmt.executeQuery( + "SELECT account_id, added_by, added_on, removed_by, removed_on" + + " FROM account_group_members_audit" + + " WHERE group_id = '" + + groupId.get() + + "'")) { + List<AccountGroupMemberAudit> audits = new ArrayList<>(); + while (rs.next()) { + Account.Id accountId = new Account.Id(rs.getInt(1)); + + Account.Id addedBy = new Account.Id(rs.getInt(2)); + Timestamp addedOn = rs.getTimestamp(3); + + Timestamp removedOn = rs.getTimestamp(5); + Account.Id removedBy = removedOn != null ? new Account.Id(rs.getInt(4)) : null; + + AccountGroupMemberAudit.Key key = + new AccountGroupMemberAudit.Key(accountId, groupId, addedOn); + AccountGroupMemberAudit audit = new AccountGroupMemberAudit(key, addedBy); + audit.removed(removedBy, removedOn); + audits.add(audit); + } + return audits; + } catch (SQLException e) { + throw new OrmException( + String.format( + "Failed to read member audits of account group %s from ReviewDb", groupId.get()), + e); + } + } + + private static List<AccountGroupById> readAccountGroupSubgroupsFromReviewDb( + JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException { + try (Statement stmt = jdbcSchema.getConnection().createStatement(); + ResultSet rs = + stmt.executeQuery( + "SELECT include_uuid" + + " FROM account_group_by_id" + + " WHERE group_id = '" + + groupId.get() + + "'")) { + List<AccountGroupById> subgroups = new ArrayList<>(); + while (rs.next()) { + AccountGroup.UUID includedGroupUuid = new AccountGroup.UUID(rs.getString(1)); + subgroups.add(new AccountGroupById(new AccountGroupById.Key(groupId, includedGroupUuid))); + } + return subgroups; + } catch (SQLException e) { + throw new OrmException( + String.format( + "Failed to read subgroups of account group %s from ReviewDb", groupId.get()), + e); + } + } + + private static List<AccountGroupByIdAud> readAccountGroupSubgroupAuditsFromReviewDb( + JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException { + try (Statement stmt = jdbcSchema.getConnection().createStatement(); + ResultSet rs = + stmt.executeQuery( + "SELECT include_uuid, added_by, added_on, removed_by, removed_on" + + " FROM account_group_by_id_aud" + + " WHERE group_id = '" + + groupId.get() + + "'")) { + List<AccountGroupByIdAud> audits = new ArrayList<>(); + while (rs.next()) { + AccountGroup.UUID includedGroupUuid = new AccountGroup.UUID(rs.getString(1)); + + Account.Id addedBy = new Account.Id(rs.getInt(2)); + Timestamp addedOn = rs.getTimestamp(3); + + Timestamp removedOn = rs.getTimestamp(5); + Account.Id removedBy = removedOn != null ? new Account.Id(rs.getInt(4)) : null; + + AccountGroupByIdAud.Key key = + new AccountGroupByIdAud.Key(groupId, includedGroupUuid, addedOn); + AccountGroupByIdAud audit = new AccountGroupByIdAud(key, addedBy); + audit.removed(removedBy, removedOn); + audits.add(audit); + } + return audits; + } catch (SQLException e) { + throw new OrmException( + String.format( + "Failed to read subgroup audits of account group %s from ReviewDb", groupId.get()), + e); + } + } + } + + private static final Comparator<AccountGroupMember> ACCOUNT_GROUP_MEMBER_COMPARATOR = + Comparator.comparingInt((AccountGroupMember m) -> m.getAccountGroupId().get()) + .thenComparingInt(m -> m.getAccountId().get()); + + private static final Comparator<AccountGroupMemberAudit> ACCOUNT_GROUP_MEMBER_AUDIT_COMPARATOR = + Comparator.comparingInt((AccountGroupMemberAudit a) -> a.getGroupId().get()) + .thenComparing(AccountGroupMemberAudit::getAddedOn) + .thenComparingInt(a -> a.getAddedBy().get()) + .thenComparingInt(a -> a.getMemberId().get()) + .thenComparing( + a -> a.getRemovedBy() != null ? a.getRemovedBy().get() : null, + nullsLast(naturalOrder())) + .thenComparing(AccountGroupMemberAudit::getRemovedOn, nullsLast(naturalOrder())); + + private static final Comparator<AccountGroupById> ACCOUNT_GROUP_BY_ID_COMPARATOR = + Comparator.comparingInt((AccountGroupById m) -> m.getGroupId().get()) + .thenComparing(AccountGroupById::getIncludeUUID); + + private static final Comparator<AccountGroupByIdAud> ACCOUNT_GROUP_BY_ID_AUD_COMPARATOR = + Comparator.comparingInt((AccountGroupByIdAud a) -> a.getGroupId().get()) + .thenComparing(AccountGroupByIdAud::getAddedOn) + .thenComparingInt(a -> a.getAddedBy().get()) + .thenComparing(AccountGroupByIdAud::getIncludeUUID) + .thenComparing( + a -> a.getRemovedBy() != null ? a.getRemovedBy().get() : null, + nullsLast(naturalOrder())) + .thenComparing(AccountGroupByIdAud::getRemovedOn, nullsLast(naturalOrder())); + + private static final Comparator<AuditEntry> AUDIT_ENTRY_COMPARATOR = + Comparator.comparing(AuditEntry::getTimestamp) + .thenComparing(AuditEntry::getAction, Comparator.comparingInt(Action::getOrder)); + + public static GroupBundle create( + Source source, + AccountGroup group, + Iterable<AccountGroupMember> members, + Iterable<AccountGroupMemberAudit> memberAudit, + Iterable<AccountGroupById> byId, + Iterable<AccountGroupByIdAud> byIdAudit) { + AccountGroup.UUID uuid = group.getGroupUUID(); + return new AutoValue_GroupBundle.Builder() + .source(source) + .group(group) + .members( + logIfNotUnique( + source, uuid, members, ACCOUNT_GROUP_MEMBER_COMPARATOR, AccountGroupMember.class)) + .memberAudit( + logIfNotUnique( + source, + uuid, + memberAudit, + ACCOUNT_GROUP_MEMBER_AUDIT_COMPARATOR, + AccountGroupMemberAudit.class)) + .byId( + logIfNotUnique( + source, uuid, byId, ACCOUNT_GROUP_BY_ID_COMPARATOR, AccountGroupById.class)) + .byIdAudit( + logIfNotUnique( + source, + uuid, + byIdAudit, + ACCOUNT_GROUP_BY_ID_AUD_COMPARATOR, + AccountGroupByIdAud.class)) + .build(); + } + + private static <T> ImmutableSet<T> logIfNotUnique( + Source source, + AccountGroup.UUID uuid, + Iterable<T> iterable, + Comparator<T> comparator, + Class<T> clazz) { + List<T> list = Streams.stream(iterable).sorted(comparator).collect(toList()); + ImmutableSet<T> set = ImmutableSet.copyOf(list); + if (set.size() != list.size()) { + // One way this can happen is that distinct audit entities can compare equal, because + // AccountGroup{MemberAudit,ByIdAud}.Key does not include the addedOn timestamp in its + // members() list. However, this particular issue only applies to pure adds, since removedOn + // *is* included in equality. As a result, if this happens, it means the audit log is already + // corrupt, and it's not clear if we can programmatically repair it. For migrating to NoteDb, + // we'll try our best to recreate it, but no guarantees it will match the real sequence of + // attempted operations, which is in any case lost in the mists of time. + logger.atWarning().log( + "group %s in %s has duplicate %s entities: %s", + uuid, source, clazz.getSimpleName(), iterable); + } + return set; + } + + static Builder builder() { + return new AutoValue_GroupBundle.Builder().members().memberAudit().byId().byIdAudit(); + } + + public static ImmutableList<String> compareWithAudits( + GroupBundle reviewDbBundle, GroupBundle noteDbBundle) { + return compare(reviewDbBundle, noteDbBundle, true); + } + + public static ImmutableList<String> compareWithoutAudits( + GroupBundle reviewDbBundle, GroupBundle noteDbBundle) { + return compare(reviewDbBundle, noteDbBundle, false); + } + + private static ImmutableList<String> compare( + GroupBundle reviewDbBundle, GroupBundle noteDbBundle, boolean compareAudits) { + // Normalize the ReviewDb bundle to what we expect in NoteDb. This means that values in error + // messages will not reflect the actual data in ReviewDb, but it will make it easier for humans + // to see the difference. + reviewDbBundle = reviewDbBundle.truncateToSecond(); + AccountGroup reviewDbGroup = new AccountGroup(reviewDbBundle.group()); + reviewDbGroup.setDescription(Strings.emptyToNull(reviewDbGroup.getDescription())); + reviewDbBundle = reviewDbBundle.toBuilder().group(reviewDbGroup).build(); + + checkArgument( + reviewDbBundle.source() == Source.REVIEW_DB, + "first bundle's source must be %s: %s", + Source.REVIEW_DB, + reviewDbBundle); + checkArgument( + noteDbBundle.source() == Source.NOTE_DB, + "second bundle's source must be %s: %s", + Source.NOTE_DB, + noteDbBundle); + + ImmutableList.Builder<String> result = ImmutableList.builder(); + if (!reviewDbBundle.group().equals(noteDbBundle.group())) { + result.add( + "AccountGroups differ\n" + + ("ReviewDb: " + reviewDbBundle.group() + "\n") + + ("NoteDb : " + noteDbBundle.group())); + } + if (!reviewDbBundle.members().equals(noteDbBundle.members())) { + result.add( + "AccountGroupMembers differ\n" + + ("ReviewDb: " + reviewDbBundle.members() + "\n") + + ("NoteDb : " + noteDbBundle.members())); + } + if (compareAudits + && !areMemberAuditsConsideredEqual( + reviewDbBundle.memberAudit(), noteDbBundle.memberAudit())) { + result.add( + "AccountGroupMemberAudits differ\n" + + ("ReviewDb: " + reviewDbBundle.memberAudit() + "\n") + + ("NoteDb : " + noteDbBundle.memberAudit())); + } + if (!reviewDbBundle.byId().equals(noteDbBundle.byId())) { + result.add( + "AccountGroupByIds differ\n" + + ("ReviewDb: " + reviewDbBundle.byId() + "\n") + + ("NoteDb : " + noteDbBundle.byId())); + } + if (compareAudits + && !areByIdAuditsConsideredEqual(reviewDbBundle.byIdAudit(), noteDbBundle.byIdAudit())) { + result.add( + "AccountGroupByIdAudits differ\n" + + ("ReviewDb: " + reviewDbBundle.byIdAudit() + "\n") + + ("NoteDb : " + noteDbBundle.byIdAudit())); + } + return result.build(); + } + + private static boolean areMemberAuditsConsideredEqual( + ImmutableSet<AccountGroupMemberAudit> reviewDbMemberAudits, + ImmutableSet<AccountGroupMemberAudit> noteDbMemberAudits) { + ListMultimap<String, AuditEntry> reviewDbMemberAuditsByMemberId = + toMemberAuditEntriesByMemberId(reviewDbMemberAudits); + ListMultimap<String, AuditEntry> noteDbMemberAuditsByMemberId = + toMemberAuditEntriesByMemberId(noteDbMemberAudits); + + return areConsideredEqual(reviewDbMemberAuditsByMemberId, noteDbMemberAuditsByMemberId); + } + + private static boolean areByIdAuditsConsideredEqual( + ImmutableSet<AccountGroupByIdAud> reviewDbByIdAudits, + ImmutableSet<AccountGroupByIdAud> noteDbByIdAudits) { + ListMultimap<String, AuditEntry> reviewDbByIdAuditsById = + toByIdAuditEntriesById(reviewDbByIdAudits); + ListMultimap<String, AuditEntry> noteDbByIdAuditsById = + toByIdAuditEntriesById(noteDbByIdAudits); + + return areConsideredEqual(reviewDbByIdAuditsById, noteDbByIdAuditsById); + } + + private static ListMultimap<String, AuditEntry> toMemberAuditEntriesByMemberId( + ImmutableSet<AccountGroupMemberAudit> memberAudits) { + return memberAudits.stream() + .flatMap(GroupBundle::toAuditEntries) + .collect( + Multimaps.toMultimap( + AuditEntry::getTarget, + Function.identity(), + MultimapBuilder.hashKeys().arrayListValues()::build)); + } + + private static Stream<AuditEntry> toAuditEntries(AccountGroupMemberAudit memberAudit) { + AuditEntry additionAuditEntry = + AuditEntry.create( + Action.ADD, + memberAudit.getAddedBy(), + memberAudit.getMemberId(), + memberAudit.getAddedOn()); + if (memberAudit.isActive()) { + return Stream.of(additionAuditEntry); + } + + AuditEntry removalAuditEntry = + AuditEntry.create( + Action.REMOVE, + memberAudit.getRemovedBy(), + memberAudit.getMemberId(), + memberAudit.getRemovedOn()); + return Stream.of(additionAuditEntry, removalAuditEntry); + } + + private static ListMultimap<String, AuditEntry> toByIdAuditEntriesById( + ImmutableSet<AccountGroupByIdAud> byIdAudits) { + return byIdAudits.stream() + .flatMap(GroupBundle::toAuditEntries) + .collect( + Multimaps.toMultimap( + AuditEntry::getTarget, + Function.identity(), + MultimapBuilder.hashKeys().arrayListValues()::build)); + } + + private static Stream<AuditEntry> toAuditEntries(AccountGroupByIdAud byIdAudit) { + AuditEntry additionAuditEntry = + AuditEntry.create( + Action.ADD, byIdAudit.getAddedBy(), byIdAudit.getIncludeUUID(), byIdAudit.getAddedOn()); + if (byIdAudit.isActive()) { + return Stream.of(additionAuditEntry); + } + + AuditEntry removalAuditEntry = + AuditEntry.create( + Action.REMOVE, + byIdAudit.getRemovedBy(), + byIdAudit.getIncludeUUID(), + byIdAudit.getRemovedOn()); + return Stream.of(additionAuditEntry, removalAuditEntry); + } + + /** + * Determines whether the audit log entries are equal except for redundant entries. Entries of the + * same type (addition/removal) which follow directly on each other according to their timestamp + * are considered redundant. + */ + private static boolean areConsideredEqual( + ListMultimap<String, AuditEntry> reviewDbMemberAuditsByTarget, + ListMultimap<String, AuditEntry> noteDbMemberAuditsByTarget) { + for (String target : reviewDbMemberAuditsByTarget.keySet()) { + ImmutableList<AuditEntry> reviewDbAuditEntries = + reviewDbMemberAuditsByTarget.get(target).stream() + .sorted(AUDIT_ENTRY_COMPARATOR) + .collect(toImmutableList()); + ImmutableSet<AuditEntry> noteDbAuditEntries = + noteDbMemberAuditsByTarget.get(target).stream() + .sorted(AUDIT_ENTRY_COMPARATOR) + .collect(toImmutableSet()); + + int reviewDbIndex = 0; + for (AuditEntry noteDbAuditEntry : noteDbAuditEntries) { + Set<AuditEntry> redundantReviewDbAuditEntries = new HashSet<>(); + while (reviewDbIndex < reviewDbAuditEntries.size()) { + AuditEntry reviewDbAuditEntry = reviewDbAuditEntries.get(reviewDbIndex); + if (!reviewDbAuditEntry.getAction().equals(noteDbAuditEntry.getAction())) { + break; + } + redundantReviewDbAuditEntries.add(reviewDbAuditEntry); + reviewDbIndex++; + } + + // The order of the entries is not perfect as ReviewDb included milliseconds for timestamps + // and we cut off everything below seconds due to NoteDb/git. Consequently, we don't have a + // way to know in this method in which exact order additions/removals within the same second + // happened. The best we can do is to group all additions within the same second as + // redundant entries and the removals afterward. To compensate that we possibly group + // non-redundant additions/removals, we also accept NoteDb audit entries which just occur + // anywhere as ReviewDb audit entries. + if (!redundantReviewDbAuditEntries.contains(noteDbAuditEntry) + && !reviewDbAuditEntries.contains(noteDbAuditEntry)) { + return false; + } + } + + if (reviewDbIndex < reviewDbAuditEntries.size()) { + // Some of the ReviewDb audit log entries aren't matched by NoteDb audit log entries. + return false; + } + } + return true; + } + + public AccountGroup.Id id() { + return group().getId(); + } + + public AccountGroup.UUID uuid() { + return group().getGroupUUID(); + } + + public abstract Source source(); + + public abstract AccountGroup group(); + + public abstract ImmutableSet<AccountGroupMember> members(); + + public abstract ImmutableSet<AccountGroupMemberAudit> memberAudit(); + + public abstract ImmutableSet<AccountGroupById> byId(); + + public abstract ImmutableSet<AccountGroupByIdAud> byIdAudit(); + + public abstract Builder toBuilder(); + + public GroupBundle truncateToSecond() { + AccountGroup newGroup = new AccountGroup(group()); + if (newGroup.getCreatedOn() != null) { + newGroup.setCreatedOn(TimeUtil.truncateToSecond(newGroup.getCreatedOn())); + } + return toBuilder() + .group(newGroup) + .memberAudit( + memberAudit().stream().map(GroupBundle::truncateToSecond).collect(toImmutableSet())) + .byIdAudit( + byIdAudit().stream().map(GroupBundle::truncateToSecond).collect(toImmutableSet())) + .build(); + } + + private static AccountGroupMemberAudit truncateToSecond(AccountGroupMemberAudit a) { + AccountGroupMemberAudit result = + new AccountGroupMemberAudit( + new AccountGroupMemberAudit.Key( + a.getKey().getParentKey(), + a.getKey().getGroupId(), + TimeUtil.truncateToSecond(a.getKey().getAddedOn())), + a.getAddedBy()); + if (a.getRemovedOn() != null) { + result.removed(a.getRemovedBy(), TimeUtil.truncateToSecond(a.getRemovedOn())); + } + return result; + } + + private static AccountGroupByIdAud truncateToSecond(AccountGroupByIdAud a) { + AccountGroupByIdAud result = + new AccountGroupByIdAud( + new AccountGroupByIdAud.Key( + a.getKey().getParentKey(), + a.getKey().getIncludeUUID(), + TimeUtil.truncateToSecond(a.getKey().getAddedOn())), + a.getAddedBy()); + if (a.getRemovedOn() != null) { + result.removed(a.getRemovedBy(), TimeUtil.truncateToSecond(a.getRemovedOn())); + } + return result; + } + + public InternalGroup toInternalGroup() { + return InternalGroup.create( + group(), + members().stream().map(AccountGroupMember::getAccountId).collect(toImmutableSet()), + byId().stream().map(AccountGroupById::getIncludeUUID).collect(toImmutableSet())); + } + + @Override + public int hashCode() { + throw new UnsupportedOperationException( + "hashCode is not supported because equals is not supported"); + } + + @Override + public boolean equals(Object o) { + throw new UnsupportedOperationException("Use GroupBundle.compare(a, b) instead of equals"); + } + + @AutoValue + abstract static class AuditEntry { + private static AuditEntry create( + Action action, Account.Id userId, Account.Id memberId, Timestamp timestamp) { + return new AutoValue_GroupBundle_AuditEntry( + action, userId, String.valueOf(memberId.get()), timestamp); + } + + private static AuditEntry create( + Action action, Account.Id userId, AccountGroup.UUID subgroupId, Timestamp timestamp) { + return new AutoValue_GroupBundle_AuditEntry(action, userId, subgroupId.get(), timestamp); + } + + abstract Action getAction(); + + abstract Account.Id getUserId(); + + abstract String getTarget(); + + abstract Timestamp getTimestamp(); + } + + enum Action { + ADD(1), + REMOVE(2); + + private final int order; + + Action(int order) { + this.order = order; + } + + public int getOrder() { + return order; + } + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder source(Source source); + + abstract Builder group(AccountGroup group); + + abstract Builder members(AccountGroupMember... member); + + abstract Builder members(Iterable<AccountGroupMember> member); + + abstract Builder memberAudit(AccountGroupMemberAudit... audit); + + abstract Builder memberAudit(Iterable<AccountGroupMemberAudit> audit); + + abstract Builder byId(AccountGroupById... byId); + + abstract Builder byId(Iterable<AccountGroupById> byId); + + abstract Builder byIdAudit(AccountGroupByIdAud... audit); + + abstract Builder byIdAudit(Iterable<AccountGroupByIdAud> audit); + + abstract GroupBundle build(); + } +} |