diff --git a/application/src/main/java/run/halo/app/content/comment/CommentService.java b/application/src/main/java/run/halo/app/content/comment/CommentService.java index 157a03aac..c3a9012f9 100644 --- a/application/src/main/java/run/halo/app/content/comment/CommentService.java +++ b/application/src/main/java/run/halo/app/content/comment/CommentService.java @@ -1,8 +1,10 @@ package run.halo.app.content.comment; +import org.springframework.lang.NonNull; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Comment; import run.halo.app.extension.ListResult; +import run.halo.app.extension.Ref; /** * An application service for {@link Comment}. @@ -15,4 +17,6 @@ public interface CommentService { Mono> listComment(CommentQuery query); Mono create(Comment comment); + + Mono removeBySubject(@NonNull Ref subjectRef); } diff --git a/application/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java b/application/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java index 77e459560..dc2f33439 100644 --- a/application/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java +++ b/application/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java @@ -1,8 +1,14 @@ package run.halo.app.content.comment; +import static run.halo.app.extension.index.query.QueryFactory.and; +import static run.halo.app.extension.index.query.QueryFactory.equal; +import static run.halo.app.extension.index.query.QueryFactory.isNull; + import java.time.Instant; import java.util.function.Function; import org.apache.commons.lang3.BooleanUtils; +import org.springframework.data.domain.Sort; +import org.springframework.lang.NonNull; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.Assert; @@ -12,9 +18,13 @@ import run.halo.app.core.extension.User; import run.halo.app.core.extension.content.Comment; import run.halo.app.core.extension.service.UserService; import run.halo.app.extension.Extension; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.ListResult; +import run.halo.app.extension.PageRequest; +import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.Ref; +import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.exception.AccessDeniedException; import run.halo.app.metrics.CounterService; @@ -114,6 +124,31 @@ public class CommentServiceImpl implements CommentService { .flatMap(client::create); } + @Override + public Mono removeBySubject(@NonNull Ref subjectRef) { + Assert.notNull(subjectRef, "The subjectRef must not be null."); + // ascending order by creation time and name + var pageRequest = PageRequestImpl.of(1, 200, + Sort.by("metadata.creationTimestamp", "metadata.name")); + return Flux.defer(() -> listCommentsByRef(subjectRef, pageRequest)) + .expand(page -> page.hasNext() + ? listCommentsByRef(subjectRef, pageRequest) + : Mono.empty() + ) + .flatMap(page -> Flux.fromIterable(page.getItems())) + .flatMap(client::delete) + .then(); + } + + Mono> listCommentsByRef(Ref subjectRef, PageRequest pageRequest) { + var listOptions = new ListOptions(); + listOptions.setFieldSelector(FieldSelector.of( + and(equal("spec.subjectRef", Comment.toSubjectRefKey(subjectRef)), + isNull("metadata.deletionTimestamp")) + )); + return client.listBy(Comment.class, listOptions, pageRequest); + } + private boolean checkCommentOwner(Comment comment, Boolean onlySystemUser) { Comment.CommentOwner owner = comment.getSpec().getOwner(); if (Boolean.TRUE.equals(onlySystemUser)) { diff --git a/application/src/main/java/run/halo/app/content/comment/ReplyService.java b/application/src/main/java/run/halo/app/content/comment/ReplyService.java index 2010f229e..0c5f7c002 100644 --- a/application/src/main/java/run/halo/app/content/comment/ReplyService.java +++ b/application/src/main/java/run/halo/app/content/comment/ReplyService.java @@ -1,9 +1,5 @@ package run.halo.app.content.comment; -import java.time.Instant; -import java.util.Comparator; -import java.util.function.Function; -import org.springframework.util.comparator.Comparators; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Reply; import run.halo.app.extension.ListResult; @@ -20,19 +16,5 @@ public interface ReplyService { Mono> list(ReplyQuery query); - /** - * Ascending order by creation time. - * - * @return reply comparator - */ - static Comparator creationTimeAscComparator() { - Function creationTime = reply -> reply.getSpec().getCreationTime(); - Function metadataCreationTime = - reply -> reply.getMetadata().getCreationTimestamp(); - // ascending order by creation time - // asc nulls high will be placed at the end - return Comparator.comparing(creationTime, Comparators.nullsHigh()) - .thenComparing(metadataCreationTime) - .thenComparing(reply -> reply.getMetadata().getName()); - } + Mono removeAllByComment(String commentName); } diff --git a/application/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java b/application/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java index ba23b21a2..2594d5b99 100644 --- a/application/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java +++ b/application/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java @@ -1,5 +1,8 @@ package run.halo.app.content.comment; +import static run.halo.app.extension.index.query.QueryFactory.and; +import static run.halo.app.extension.index.query.QueryFactory.equal; +import static run.halo.app.extension.index.query.QueryFactory.isNull; import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate; import java.time.Instant; @@ -7,6 +10,7 @@ import java.util.function.Function; import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.BooleanUtils; +import org.springframework.data.domain.Sort; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.util.Assert; @@ -17,8 +21,12 @@ import run.halo.app.core.extension.content.Comment; import run.halo.app.core.extension.content.Reply; import run.halo.app.core.extension.service.UserService; import run.halo.app.extension.Extension; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.ListResult; +import run.halo.app.extension.PageRequest; +import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.metrics.CounterService; import run.halo.app.metrics.MeterUtils; @@ -89,6 +97,31 @@ public class ReplyServiceImpl implements ReplyService { ); } + @Override + public Mono removeAllByComment(String commentName) { + Assert.notNull(commentName, "The commentName must not be null."); + // ascending order by creation time and name + var pageRequest = PageRequestImpl.of(1, 200, + Sort.by("metadata.creationTimestamp", "metadata.name")); + return Flux.defer(() -> listRepliesByComment(commentName, pageRequest)) + .expand(page -> page.hasNext() + ? listRepliesByComment(commentName, pageRequest) + : Mono.empty() + ) + .flatMap(page -> Flux.fromIterable(page.getItems())) + .flatMap(client::delete) + .then(); + } + + Mono> listRepliesByComment(String commentName, PageRequest pageRequest) { + var listOptions = new ListOptions(); + listOptions.setFieldSelector(FieldSelector.of( + and(equal("spec.commentName", commentName), + isNull("metadata.deletionTimestamp")) + )); + return client.listBy(Reply.class, listOptions, pageRequest); + } + private Mono toListedReply(Reply reply) { ListedReply.ListedReplyBuilder builder = ListedReply.builder() .reply(reply); diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/CommentReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/CommentReconciler.java index 800c8ec84..05f5530dd 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/CommentReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/CommentReconciler.java @@ -2,33 +2,39 @@ package run.halo.app.core.extension.reconciler; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; import static run.halo.app.extension.ExtensionUtil.addFinalizers; +import static run.halo.app.extension.ExtensionUtil.isDeleted; +import static run.halo.app.extension.ExtensionUtil.removeFinalizers; +import static run.halo.app.extension.index.query.QueryFactory.and; +import static run.halo.app.extension.index.query.QueryFactory.equal; +import static run.halo.app.extension.index.query.QueryFactory.isNull; import java.time.Instant; -import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.BooleanUtils; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.lang.Nullable; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import run.halo.app.content.comment.ReplyNotificationSubscriptionHelper; import run.halo.app.content.comment.ReplyService; import run.halo.app.core.extension.Counter; import run.halo.app.core.extension.content.Comment; import run.halo.app.core.extension.content.Constant; -import run.halo.app.core.extension.content.Reply; import run.halo.app.event.post.CommentCreatedEvent; +import run.halo.app.event.post.CommentUnreadReplyCountChangedEvent; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.GroupVersionKind; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.MetadataUtil; +import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.Ref; import run.halo.app.extension.SchemeManager; import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.Reconciler; -import run.halo.app.infra.utils.JsonUtils; +import run.halo.app.extension.index.query.Query; +import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.metrics.MeterUtils; /** @@ -43,6 +49,7 @@ public class CommentReconciler implements Reconciler { public static final String FINALIZER_NAME = "comment-protection"; private final ExtensionClient client; private final SchemeManager schemeManager; + private final ReplyService replyService; private final ApplicationEventPublisher eventPublisher; private final ReplyNotificationSubscriptionHelper replyNotificationSubscriptionHelper; @@ -52,7 +59,10 @@ public class CommentReconciler implements Reconciler { client.fetch(Comment.class, request.name()) .ifPresent(comment -> { if (isDeleted(comment)) { - cleanUpResourcesAndRemoveFinalizer(request.name()); + if (removeFinalizers(comment.getMetadata(), Set.of(FINALIZER_NAME))) { + cleanUpResources(comment); + client.update(comment); + } return; } if (addFinalizers(comment.getMetadata(), Set.of(FINALIZER_NAME))) { @@ -62,9 +72,14 @@ public class CommentReconciler implements Reconciler { replyNotificationSubscriptionHelper.subscribeNewReplyReasonForComment(comment); - compatibleCreationTime(request.name()); - reconcileStatus(request.name()); - updateSameSubjectRefCommentCounter(comment.getSpec().getSubjectRef()); + compatibleCreationTime(comment); + Comment.CommentStatus status = comment.getStatusOrDefault(); + status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0); + + updateUnReplyCountIfNecessary(comment); + updateSameSubjectRefCommentCounter(comment); + + client.update(comment); }); return new Result(false, null); } @@ -79,39 +94,14 @@ public class CommentReconciler implements Reconciler { /** * If the comment creation time is null, set it to the approved time or the current time. * TODO remove this method in the future and fill in attributes in hook mode instead. - * - * @param name comment name */ - void compatibleCreationTime(String name) { - client.fetch(Comment.class, name).ifPresent(comment -> { - Instant creationTime = comment.getSpec().getCreationTime(); - Instant oldCreationTime = - creationTime == null ? null : Instant.ofEpochMilli(creationTime.toEpochMilli()); - if (creationTime == null) { - creationTime = defaultIfNull(comment.getSpec().getApprovedTime(), Instant.now()); - comment.getSpec().setCreationTime(creationTime); - } - - if (!Objects.equals(oldCreationTime, comment.getSpec().getCreationTime())) { - client.update(comment); - } - }); - } - - private boolean isDeleted(Comment comment) { - return comment.getMetadata().getDeletionTimestamp() != null; - } - - private void reconcileStatus(String name) { - client.fetch(Comment.class, name).ifPresent(comment -> { - Comment oldComment = JsonUtils.deepCopy(comment); - Comment.CommentStatus status = comment.getStatusOrDefault(); - status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0); - updateUnReplyCountIfNecessary(comment); - if (!oldComment.equals(comment)) { - client.update(comment); - } - }); + void compatibleCreationTime(Comment comment) { + var creationTime = comment.getSpec().getCreationTime(); + if (creationTime == null) { + creationTime = defaultIfNull(comment.getSpec().getApprovedTime(), + comment.getMetadata().getCreationTimestamp()); + } + comment.getSpec().setCreationTime(creationTime); } private void updateUnReplyCountIfNecessary(Comment comment) { @@ -121,80 +111,71 @@ public class CommentReconciler implements Reconciler { if (lastReadTime != null && lastReadTime.toString().equals(lastReadTimeAnno)) { return; } - // spec.lastReadTime is null or not equal to annotation.lastReadTime + // delegate to other handler though event String commentName = comment.getMetadata().getName(); - List replies = client.list(Reply.class, - reply -> commentName.equals(reply.getSpec().getCommentName()) - && reply.getMetadata().getDeletionTimestamp() == null, - ReplyService.creationTimeAscComparator()); - - // calculate unread reply count - comment.getStatusOrDefault() - .setUnreadReplyCount(Comment.getUnreadReplyCount(replies, lastReadTime)); + eventPublisher.publishEvent(new CommentUnreadReplyCountChangedEvent(this, commentName)); // handled flag if (lastReadTime != null) { annotations.put(Constant.LAST_READ_TIME_ANNO, lastReadTime.toString()); + } else { + annotations.remove(Constant.LAST_READ_TIME_ANNO); } } - private void updateSameSubjectRefCommentCounter(Ref commentSubjectRef) { - List comments = client.list(Comment.class, - comment -> !isDeleted(comment) - && commentSubjectRef.equals(comment.getSpec().getSubjectRef()), - null); - + private void updateSameSubjectRefCommentCounter(Comment comment) { + var commentSubjectRef = comment.getSpec().getSubjectRef(); GroupVersionKind groupVersionKind = groupVersionKind(commentSubjectRef); - if (groupVersionKind == null) { - return; - } - // approved total count - long approvedTotalCount = comments.stream() - .filter(comment -> BooleanUtils.isTrue(comment.getSpec().getApproved())) - .count(); - // total count - int totalCount = comments.size(); + var totalCount = countTotalComments(commentSubjectRef); + var approvedTotalCount = countApprovedComments(commentSubjectRef); schemeManager.fetch(groupVersionKind).ifPresent(scheme -> { String counterName = MeterUtils.nameOf(commentSubjectRef.getGroup(), scheme.plural(), commentSubjectRef.getName()); client.fetch(Counter.class, counterName).ifPresentOrElse(counter -> { counter.setTotalComment(totalCount); - counter.setApprovedComment((int) approvedTotalCount); + counter.setApprovedComment(approvedTotalCount); client.update(counter); }, () -> { Counter counter = Counter.emptyCounter(counterName); counter.setTotalComment(totalCount); - counter.setApprovedComment((int) approvedTotalCount); + counter.setApprovedComment(approvedTotalCount); client.create(counter); }); }); } - private void cleanUpResourcesAndRemoveFinalizer(String commentName) { - client.fetch(Comment.class, commentName).ifPresent(comment -> { - cleanUpResources(comment); - if (comment.getMetadata().getFinalizers() != null) { - comment.getMetadata().getFinalizers().remove(FINALIZER_NAME); - } - client.update(comment); - }); + int countTotalComments(Ref commentSubjectRef) { + var totalListOptions = new ListOptions(); + totalListOptions.setFieldSelector(FieldSelector.of(getBaseQuery(commentSubjectRef))); + return (int) client.listBy(Comment.class, totalListOptions, PageRequestImpl.ofSize(1)) + .getTotal(); + } + + int countApprovedComments(Ref commentSubjectRef) { + var approvedListOptions = new ListOptions(); + approvedListOptions.setFieldSelector(FieldSelector.of(and( + getBaseQuery(commentSubjectRef), + equal("spec.approved", BooleanUtils.TRUE) + ))); + return (int) client.listBy(Comment.class, approvedListOptions, PageRequestImpl.ofSize(1)) + .getTotal(); + } + + private static Query getBaseQuery(Ref commentSubjectRef) { + return and(equal("spec.subjectRef", Comment.toSubjectRefKey(commentSubjectRef)), + isNull("metadata.deletionTimestamp")); } private void cleanUpResources(Comment comment) { // delete all replies under current comment - client.list(Reply.class, reply -> comment.getMetadata().getName() - .equals(reply.getSpec().getCommentName()), - null) - .forEach(client::delete); + replyService.removeAllByComment(comment.getMetadata().getName()).block(); + // decrement total comment count - updateSameSubjectRefCommentCounter(comment.getSpec().getSubjectRef()); + updateSameSubjectRefCommentCounter(comment); } - @Nullable - private GroupVersionKind groupVersionKind(Ref ref) { - if (ref == null) { - return null; - } + @NonNull + private GroupVersionKind groupVersionKind(@NonNull Ref ref) { return new GroupVersionKind(ref.getGroup(), ref.getVersion(), ref.getKind()); } } diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java index 0a7eec281..90b01017a 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java @@ -25,8 +25,8 @@ import org.springframework.stereotype.Component; import run.halo.app.content.ContentWrapper; import run.halo.app.content.NotificationReasonConst; import run.halo.app.content.PostService; +import run.halo.app.content.comment.CommentService; import run.halo.app.content.permalinks.PostPermalinkPolicy; -import run.halo.app.core.extension.content.Comment; import run.halo.app.core.extension.content.Constant; import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post.PostPhase; @@ -75,6 +75,7 @@ public class PostReconciler implements Reconciler { private final PostService postService; private final PostPermalinkPolicy postPermalinkPolicy; private final CounterService counterService; + private final CommentService commentService; private final ApplicationEventPublisher eventPublisher; private final NotificationCenter notificationCenter; @@ -329,9 +330,7 @@ public class PostReconciler implements Reconciler { listSnapshots(ref).forEach(client::delete); // clean up comments - client.list(Comment.class, comment -> ref.equals(comment.getSpec().getSubjectRef()), - null) - .forEach(client::delete); + commentService.removeBySubject(ref).block(); // delete counter counterService.deleteByName(MeterUtils.nameOf(Post.class, post.getMetadata().getName())) diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java index 06eb01e52..4ceecac67 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java @@ -19,7 +19,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.Assert; import run.halo.app.content.NotificationReasonConst; import run.halo.app.content.SinglePageService; -import run.halo.app.core.extension.content.Comment; +import run.halo.app.content.comment.CommentService; import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.SinglePage; import run.halo.app.core.extension.content.Snapshot; @@ -64,6 +64,7 @@ public class SinglePageReconciler implements Reconciler { private final ExtensionClient client; private final SinglePageService singlePageService; private final CounterService counterService; + private final CommentService commentService; private final ExternalUrlSupplier externalUrlSupplier; @@ -250,9 +251,7 @@ public class SinglePageReconciler implements Reconciler { listSnapshots(ref).forEach(client::delete); // clean up comments - client.list(Comment.class, comment -> comment.getSpec().getSubjectRef().equals(ref), - null) - .forEach(client::delete); + commentService.removeBySubject(ref).block(); // delete counter for single page counterService.deleteByName( diff --git a/application/src/main/java/run/halo/app/event/post/CommentUnreadReplyCountChangedEvent.java b/application/src/main/java/run/halo/app/event/post/CommentUnreadReplyCountChangedEvent.java new file mode 100644 index 000000000..2e32514be --- /dev/null +++ b/application/src/main/java/run/halo/app/event/post/CommentUnreadReplyCountChangedEvent.java @@ -0,0 +1,22 @@ +package run.halo.app.event.post; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + *

This event will be triggered when the unread reply count of the comment is changed.

+ *

It is used to update the unread reply count of the comment,such as when the user reads the + * reply(lastReadTime changed in comment), the unread reply count will be updated.

+ * + * @author guqing + * @since 2.14.0 + */ +@Getter +public class CommentUnreadReplyCountChangedEvent extends ApplicationEvent { + private final String commentName; + + public CommentUnreadReplyCountChangedEvent(Object source, String commentName) { + super(source); + this.commentName = commentName; + } +} diff --git a/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java b/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java index c821e4491..25aa568cf 100644 --- a/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java +++ b/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java @@ -21,8 +21,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.util.Predicates; import org.springframework.stereotype.Component; @@ -34,7 +32,6 @@ import reactor.util.retry.Retry; import run.halo.app.extension.exception.ExtensionNotFoundException; import run.halo.app.extension.index.DefaultExtensionIterator; import run.halo.app.extension.index.ExtensionIterator; -import run.halo.app.extension.index.ExtensionPaginatedLister; import run.halo.app.extension.index.IndexedQueryEngine; import run.halo.app.extension.index.IndexerFactory; import run.halo.app.extension.store.ReactiveExtensionStoreClient; @@ -341,18 +338,13 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient { private ExtensionIterator createExtensionIterator(Scheme scheme) { var type = scheme.type(); var prefix = ExtensionStoreUtil.buildStoreNamePrefix(scheme); - var lister = new ExtensionPaginatedLister() { - @Override - @SuppressWarnings("unchecked") - public Page list(Pageable pageable) { - return client.listByNamePrefix(prefix, pageable) - .map(page -> page.map( - store -> (E) converter.convertFrom(type, store)) - ) - .block(); - } - }; - return new DefaultExtensionIterator<>(lister); + return new DefaultExtensionIterator<>(pageable -> + client.listByNamePrefix(prefix, pageable) + .map(page -> + page.map(store -> (Extension) converter.convertFrom(type, store)) + ) + .block() + ); } @EventListener(ContextRefreshedEvent.class) diff --git a/application/src/main/java/run/halo/app/extension/index/DefaultExtensionIterator.java b/application/src/main/java/run/halo/app/extension/index/DefaultExtensionIterator.java index fd752b4b2..a96b227cb 100644 --- a/application/src/main/java/run/halo/app/extension/index/DefaultExtensionIterator.java +++ b/application/src/main/java/run/halo/app/extension/index/DefaultExtensionIterator.java @@ -17,19 +17,23 @@ import run.halo.app.extension.Extension; */ public class DefaultExtensionIterator implements ExtensionIterator { static final int DEFAULT_PAGE_SIZE = 500; - private final ExtensionPaginatedLister lister; + private final ExtensionPaginatedLister lister; private Pageable currentPageable; private List currentData; private int currentIndex; + public DefaultExtensionIterator(ExtensionPaginatedLister lister) { + this(PageRequest.of(0, DEFAULT_PAGE_SIZE, Sort.by("name")), lister); + } + /** * Constructs a new DefaultExtensionIterator with the given lister. * * @param lister the lister to use to load data. */ - public DefaultExtensionIterator(ExtensionPaginatedLister lister) { + public DefaultExtensionIterator(Pageable initPageable, ExtensionPaginatedLister lister) { this.lister = lister; - this.currentPageable = PageRequest.of(0, DEFAULT_PAGE_SIZE, Sort.by("name")); + this.currentPageable = initPageable; this.currentData = loadData(); } diff --git a/application/src/main/java/run/halo/app/extension/index/ExtensionPaginatedLister.java b/application/src/main/java/run/halo/app/extension/index/ExtensionPaginatedLister.java index dbf406b12..155f954fa 100644 --- a/application/src/main/java/run/halo/app/extension/index/ExtensionPaginatedLister.java +++ b/application/src/main/java/run/halo/app/extension/index/ExtensionPaginatedLister.java @@ -10,14 +10,14 @@ import run.halo.app.extension.Extension; * @author guqing * @since 2.12.0 */ -public interface ExtensionPaginatedLister { +@FunctionalInterface +public interface ExtensionPaginatedLister { /** * List extensions with pagination. * * @param pageable pageable - * @param extension type * @return page of extensions */ - Page list(Pageable pageable); + Page list(Pageable pageable); } diff --git a/application/src/main/java/run/halo/app/metrics/ReplyEventReconciler.java b/application/src/main/java/run/halo/app/metrics/ReplyEventReconciler.java index 69f9c6734..9f0c5f462 100644 --- a/application/src/main/java/run/halo/app/metrics/ReplyEventReconciler.java +++ b/application/src/main/java/run/halo/app/metrics/ReplyEventReconciler.java @@ -14,10 +14,10 @@ import org.apache.commons.lang3.BooleanUtils; import org.springframework.context.SmartLifecycle; import org.springframework.context.event.EventListener; import org.springframework.data.domain.Sort; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import run.halo.app.core.extension.content.Comment; import run.halo.app.core.extension.content.Reply; +import run.halo.app.event.post.CommentUnreadReplyCountChangedEvent; import run.halo.app.event.post.ReplyEvent; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ListOptions; @@ -39,11 +39,12 @@ import run.halo.app.extension.router.selector.FieldSelector; */ @Slf4j @Component -public class ReplyEventReconciler implements Reconciler, SmartLifecycle { +public class ReplyEventReconciler + implements Reconciler, SmartLifecycle { private volatile boolean running = false; private final ExtensionClient client; - private final RequestQueue replyEventQueue; + private final RequestQueue replyEventQueue; private final Controller replyEventController; public ReplyEventReconciler(ExtensionClient client) { @@ -53,9 +54,8 @@ public class ReplyEventReconciler implements Reconciler, SmartLifecy } @Override - public Result reconcile(ReplyEvent request) { - Reply requestReply = request.getReply(); - String commentName = requestReply.getSpec().getCommentName(); + public Result reconcile(CommentName request) { + String commentName = request.name(); client.fetch(Comment.class, commentName) // if the comment has been deleted, then do nothing. @@ -110,6 +110,12 @@ public class ReplyEventReconciler implements Reconciler, SmartLifecy return new Result(false, null); } + public record CommentName(String name) { + public static CommentName of(String name) { + return new CommentName(name); + } + } + static ListOptions listOptionsWithFieldQuery(Query query) { var listOptions = new ListOptions(); listOptions.setFieldSelector(FieldSelector.of(query)); @@ -144,13 +150,14 @@ public class ReplyEventReconciler implements Reconciler, SmartLifecy return this.running; } - @Component - public class ReplyEventListener { + @EventListener(ReplyEvent.class) + public void onReplyEvent(ReplyEvent replyEvent) { + var commentName = replyEvent.getReply().getSpec().getCommentName(); + replyEventQueue.addImmediately(CommentName.of(commentName)); + } - @Async - @EventListener(ReplyEvent.class) - public void onReplyEvent(ReplyEvent replyEvent) { - replyEventQueue.addImmediately(replyEvent); - } + @EventListener(CommentUnreadReplyCountChangedEvent.class) + public void onUnreadReplyCountChangedEvent(CommentUnreadReplyCountChangedEvent event) { + replyEventQueue.addImmediately(CommentName.of(event.getCommentName())); } } diff --git a/application/src/test/java/run/halo/app/content/comment/ReplyServiceTest.java b/application/src/test/java/run/halo/app/content/comment/ReplyServiceTest.java deleted file mode 100644 index 7fea22487..000000000 --- a/application/src/test/java/run/halo/app/content/comment/ReplyServiceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package run.halo.app.content.comment; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.Instant; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.junit.jupiter.api.Test; -import run.halo.app.core.extension.content.Reply; -import run.halo.app.extension.Metadata; - -/** - * Tests for {@link ReplyService}. - * - * @author guqing - * @since 2.0.0 - */ -class ReplyServiceTest { - private final Instant now = Instant.now(); - - @Test - void creationTimeAscComparator() { - // creation time: - // 1. now + 5s, name: 1 - // 2. now + 3s, name: 2 - // 3. now + 3s, name: 3 - // 5. now + 1s, name: 4 - // 6. now - 1s, name: 5 - // 7. null, name: 6 - Reply reply1 = createReply("1", now.plusSeconds(5)); - Reply reply2 = createReply("2", now.plusSeconds(3)); - Reply reply3 = createReply("3", now.plusSeconds(3)); - Reply reply4 = createReply("4", now.plusSeconds(1)); - Reply reply5 = createReply("5", now.minusSeconds(1)); - Reply reply6 = createReply("6", null); - String result = Stream.of(reply1, reply2, reply3, reply4, reply5, reply6) - .sorted(ReplyService.creationTimeAscComparator()) - .map(reply -> reply.getMetadata().getName()) - .collect(Collectors.joining(", ")); - assertThat(result).isEqualTo("5, 4, 2, 3, 1, 6"); - } - - Reply createReply(String name, Instant creationTime) { - Reply reply = new Reply(); - reply.setMetadata(new Metadata()); - reply.getMetadata().setName(name); - reply.getMetadata().setCreationTimestamp(now); - reply.setSpec(new Reply.ReplySpec()); - reply.getSpec().setCreationTime(creationTime); - return reply; - } -} \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/CommentReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/CommentReconcilerTest.java index 0d647118d..1f6af9ec0 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/CommentReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/CommentReconcilerTest.java @@ -3,14 +3,13 @@ package run.halo.app.core.extension.reconciler; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.lenient; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.time.Instant; import java.util.HashSet; -import java.util.List; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.Test; @@ -19,10 +18,14 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; +import run.halo.app.content.comment.ReplyService; import run.halo.app.core.extension.content.Comment; -import run.halo.app.core.extension.content.Reply; import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ListOptions; +import run.halo.app.extension.ListResult; import run.halo.app.extension.Metadata; +import run.halo.app.extension.PageRequest; import run.halo.app.extension.Ref; import run.halo.app.extension.SchemeManager; import run.halo.app.extension.controller.Reconciler; @@ -42,6 +45,9 @@ class CommentReconcilerTest { @Mock SchemeManager schemeManager; + @Mock + ReplyService replyService; + @InjectMocks private CommentReconciler commentReconciler; @@ -57,23 +63,23 @@ class CommentReconcilerTest { finalizers.add(CommentReconciler.FINALIZER_NAME); comment.getMetadata().setFinalizers(finalizers); comment.setSpec(new Comment.CommentSpec()); + comment.getSpec().setSubjectRef(getRef()); comment.getSpec().setLastReadTime(now.plusSeconds(5)); comment.setStatus(new Comment.CommentStatus()); - lenient().when(client.list(eq(Reply.class), any(), any())) - .thenReturn(replyList()); - lenient().when(client.fetch(eq(Comment.class), eq("test"))) + when(client.fetch(eq(Comment.class), eq("test"))) .thenReturn(Optional.of(comment)); - lenient().when(client.list(eq(Reply.class), any(), any())) - .thenReturn(replyList()); + when(replyService.removeAllByComment(eq(comment.getMetadata().getName()))) + .thenReturn(Mono.empty()); + when(client.listBy(eq(Comment.class), any(ListOptions.class), isA(PageRequest.class))) + .thenReturn(ListResult.emptyResult()); Reconciler.Result reconcile = commentReconciler.reconcile(new Reconciler.Request("test")); assertThat(reconcile.reEnqueue()).isFalse(); assertThat(reconcile.retryAfter()).isNull(); - verify(client, times(1)).list(eq(Reply.class), any(), any()); - verify(client, times(3)).delete(any(Reply.class)); + verify(replyService).removeAllByComment(eq(comment.getMetadata().getName())); ArgumentCaptor captor = ArgumentCaptor.forClass(Comment.class); verify(client, times(1)).update(captor.capture()); @@ -90,19 +96,11 @@ class CommentReconcilerTest { comment.setSpec(new Comment.CommentSpec()); comment.getSpec().setApprovedTime(Instant.now()); comment.getSpec().setCreationTime(null); - when(client.fetch(eq(Comment.class), eq("fake-comment"))) - .thenReturn(Optional.of(comment)); - commentReconciler.compatibleCreationTime("fake-comment"); + commentReconciler.compatibleCreationTime(comment); - verify(client, times(1)).fetch(eq(Comment.class), eq("fake-comment")); - - ArgumentCaptor captor = ArgumentCaptor.forClass(Comment.class); - verify(client, times(1)).update(captor.capture()); - Comment updated = captor.getValue(); - assertThat(updated.getSpec().getCreationTime()).isNotNull(); - assertThat(updated.getSpec().getCreationTime()) - .isEqualTo(updated.getSpec().getApprovedTime()); + assertThat(comment.getSpec().getCreationTime()) + .isEqualTo(comment.getSpec().getApprovedTime()); } private static Ref getRef() { @@ -113,22 +111,4 @@ class CommentReconcilerTest { ref.setName("fake-post"); return ref; } - - List replyList() { - Reply replyA = new Reply(); - replyA.setMetadata(new Metadata()); - replyA.getMetadata().setName("reply-A"); - replyA.getMetadata().setCreationTimestamp(now.plusSeconds(6)); - - Reply replyB = new Reply(); - replyB.setMetadata(new Metadata()); - replyB.getMetadata().setName("reply-B"); - replyB.getMetadata().setCreationTimestamp(now.plusSeconds(5)); - - Reply replyC = new Reply(); - replyC.setMetadata(new Metadata()); - replyC.getMetadata().setName("reply-C"); - replyC.getMetadata().setCreationTimestamp(now.plusSeconds(4)); - return List.of(replyA, replyB, replyC); - } -} \ No newline at end of file +} diff --git a/application/src/test/java/run/halo/app/extension/index/DefaultExtensionIteratorTest.java b/application/src/test/java/run/halo/app/extension/index/DefaultExtensionIteratorTest.java index 87af097f4..d71d58023 100644 --- a/application/src/test/java/run/halo/app/extension/index/DefaultExtensionIteratorTest.java +++ b/application/src/test/java/run/halo/app/extension/index/DefaultExtensionIteratorTest.java @@ -28,7 +28,7 @@ import run.halo.app.extension.Extension; class DefaultExtensionIteratorTest { @Mock - private ExtensionPaginatedLister lister; + private ExtensionPaginatedLister lister; @Test @SuppressWarnings("unchecked")