blob: b948737bee38aaf3f8cc3a3aeb60ba59cd69fe84 [file] [log] [blame]
// Copyright (C) 2020 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.change;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.util.stream.Collectors.groupingBy;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.gerrit.entities.Comment;
import java.util.Comparator;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.function.Function;
/**
* Identifier of comment threads.
*
* <p>Comments are ordered into threads according to their parent relationship indicated via {@link
* Comment#parentUuid}. It's possible that two comments refer to the same parent, which especially
* happens when two persons reply in parallel. If such branches exist, we merge them into a flat
* list taking the comment creation date ({@link Comment#writtenOn} into account (but still
* preserving the general parent order). Remaining ties are resolved by using the natural order of
* the comment UUID, which is unique.
*
* @param <T> type of comments in the threads. Can also be {@link Comment} if the threads mix
* comments of different types.
*/
public class CommentThreads<T extends Comment> {
private final ImmutableMap<String, T> commentPerUuid;
private final Map<String, ImmutableSet<T>> childrenPerParent;
public CommentThreads(
ImmutableMap<String, T> commentPerUuid, Map<String, ImmutableSet<T>> childrenPerParent) {
this.commentPerUuid = commentPerUuid;
this.childrenPerParent = childrenPerParent;
}
public static <T extends Comment> CommentThreads<T> forComments(Iterable<T> comments) {
ImmutableMap<String, T> commentPerUuid =
Streams.stream(comments)
.distinct()
.collect(ImmutableMap.toImmutableMap(comment -> comment.key.uuid, Function.identity()));
Map<String, ImmutableSet<T>> childrenPerParent =
commentPerUuid.values().stream()
.filter(comment -> comment.parentUuid != null)
.collect(groupingBy(comment -> comment.parentUuid, toImmutableSet()));
return new CommentThreads<>(commentPerUuid, childrenPerParent);
}
/**
* Returns all comments organized into threads.
*
* <p>Comments appear only once.
*/
public ImmutableSet<CommentThread<T>> getThreads() {
ImmutableSet<T> roots =
commentPerUuid.values().stream().filter(this::isRoot).collect(toImmutableSet());
return buildThreadsOf(roots);
}
/**
* Returns only the comment threads to which the specified comments are a reply.
*
* <p>If the specified child comments are part of the comments originally provided to {@link
* CommentThreads#forComments(Iterable)}, they will also appear in the returned comment threads.
* They don't need to be part of the originally provided comments, though, but should refer to one
* of these comments via their {@link Comment#parentUuid}. Child comments not referring to any
* known comments will be ignored.
*
* @param childComments comments for which the matching threads should be determined
* @return threads to which the provided child comments are a reply
*/
public ImmutableSet<CommentThread<T>> getThreadsForChildren(Iterable<? extends T> childComments) {
ImmutableSet<T> relevantRoots =
Streams.stream(childComments)
.map(this::findRoot)
.filter(root -> commentPerUuid.containsKey(root.key.uuid))
.collect(toImmutableSet());
return buildThreadsOf(relevantRoots);
}
private T findRoot(T comment) {
T current = comment;
while (!isRoot(current)) {
current = commentPerUuid.get(current.parentUuid);
}
return current;
}
private boolean isRoot(T current) {
return current.parentUuid == null || !commentPerUuid.containsKey(current.parentUuid);
}
private ImmutableSet<CommentThread<T>> buildThreadsOf(ImmutableSet<T> roots) {
return roots.stream()
.map(root -> buildCommentThread(root, childrenPerParent))
.collect(toImmutableSet());
}
private static <T extends Comment> CommentThread<T> buildCommentThread(
T root, Map<String, ImmutableSet<T>> childrenPerParent) {
CommentThread.Builder<T> commentThread = CommentThread.builder();
// Expand comments gradually from the root. If there is more than one child per level, place the
// earlier-created child earlier in the thread. Break ties with the UUID to be deterministic.
Queue<T> unvisited =
new PriorityQueue<>(
Comparator.comparing((T comment) -> comment.writtenOn)
.thenComparing(comment -> comment.key.uuid));
unvisited.add(root);
while (!unvisited.isEmpty()) {
T nextComment = unvisited.remove();
commentThread.addComment(nextComment);
ImmutableSet<T> children =
childrenPerParent.getOrDefault(nextComment.key.uuid, ImmutableSet.of());
unvisited.addAll(children);
}
return commentThread.build();
}
}