com.google.gerrit.server.change.CommentThreads Maven / Gradle / Ivy
// 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.
 *
 * 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  type of comments in the threads. Can also be {@link Comment} if the threads mix
 *     comments of different types.
 */
public class CommentThreads {
  private final ImmutableMap commentPerUuid;
  private final Map> childrenPerParent;
  public CommentThreads(
      ImmutableMap commentPerUuid, Map> childrenPerParent) {
    this.commentPerUuid = commentPerUuid;
    this.childrenPerParent = childrenPerParent;
  }
  public static  CommentThreads forComments(Iterable comments) {
    ImmutableMap commentPerUuid =
        Streams.stream(comments)
            .distinct()
            .collect(ImmutableMap.toImmutableMap(comment -> comment.key.uuid, Function.identity()));
    Map> 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.
   *
   * Comments appear only once.
   */
  public ImmutableSet> getThreads() {
    ImmutableSet 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.
   *
   * 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> getThreadsForChildren(Iterable extends T> childComments) {
    ImmutableSet 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> buildThreadsOf(ImmutableSet roots) {
    return roots.stream()
        .map(root -> buildCommentThread(root, childrenPerParent))
        .collect(toImmutableSet());
  }
  private static  CommentThread buildCommentThread(
      T root, Map> childrenPerParent) {
    CommentThread.Builder 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 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 children =
          childrenPerParent.getOrDefault(nextComment.key.uuid, ImmutableSet.of());
      unvisited.addAll(children);
    }
    return commentThread.build();
  }
}