diff --git a/api/src/main/java/run/halo/app/core/extension/content/Post.java b/api/src/main/java/run/halo/app/core/extension/content/Post.java
index 6d51ba0ed..861a99fa9 100644
--- a/api/src/main/java/run/halo/app/core/extension/content/Post.java
+++ b/api/src/main/java/run/halo/app/core/extension/content/Post.java
@@ -222,54 +222,4 @@ public class Post extends AbstractExtension {
return null;
}
}
-
- @Data
- public static class CompactPost {
- private String name;
-
- private VisibleEnum visible;
-
- private Boolean published;
-
- public static Builder builder() {
- return new Builder();
- }
-
- /**
- *
Compact post builder.
- * Can not replace with lombok builder.
- * The class used by subclasses of {@link AbstractExtension} must have a no-args
- * constructor.
- */
- public static class Builder {
- private String name;
-
- private VisibleEnum visible;
-
- private Boolean published;
-
- public Builder name(String name) {
- this.name = name;
- return this;
- }
-
- public Builder visible(VisibleEnum visible) {
- this.visible = visible;
- return this;
- }
-
- public Builder published(Boolean published) {
- this.published = published;
- return this;
- }
-
- public CompactPost build() {
- CompactPost compactPost = new CompactPost();
- compactPost.setName(name);
- compactPost.setVisible(visible);
- compactPost.setPublished(published);
- return compactPost;
- }
- }
- }
}
diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java
index 26b138b87..5432e5614 100644
--- a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java
+++ b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java
@@ -2,6 +2,7 @@ package run.halo.app.core.extension.reconciler;
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.in;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
import java.time.Duration;
@@ -11,14 +12,13 @@ import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
+import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
-import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
import run.halo.app.core.extension.content.Category;
@@ -27,10 +27,13 @@ import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.MetadataUtil;
+import run.halo.app.extension.PageRequestImpl;
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.extension.index.query.Query;
import run.halo.app.extension.router.selector.FieldSelector;
+import run.halo.app.extension.router.selector.LabelSelector;
import run.halo.app.infra.utils.JsonUtils;
/**
@@ -42,7 +45,7 @@ import run.halo.app.infra.utils.JsonUtils;
@Component
@AllArgsConstructor
public class CategoryReconciler implements Reconciler {
- private static final String FINALIZER_NAME = "category-protection";
+ static final String FINALIZER_NAME = "category-protection";
private final ExtensionClient client;
private final CategoryPermalinkPolicy categoryPermalinkPolicy;
@@ -140,53 +143,46 @@ public class CategoryReconciler implements Reconciler {
private void populatePosts(Category category) {
String name = category.getMetadata().getName();
- List categoryNames = listChildrenByName(name)
+ Set categoryNames = listChildrenByName(name)
.stream()
.map(item -> item.getMetadata().getName())
- .toList();
+ .collect(Collectors.toSet());
- var postListOptions = new ListOptions();
- postListOptions.setFieldSelector(FieldSelector.of(
- and(isNull("metadata.deletionTimestamp"),
- equal("spec.deleted", "false")))
- );
- var posts = client.listAll(Post.class, postListOptions, Sort.unsorted());
+ var totalPostCount = countTotalPosts(categoryNames);
+ category.getStatusOrDefault().setPostCount(totalPostCount);
- // populate post to status
- List compactPosts = posts.stream()
- .filter(post -> includes(post.getSpec().getCategories(), categoryNames))
- .map(post -> Post.CompactPost.builder()
- .name(post.getMetadata().getName())
- .visible(post.getSpec().getVisible())
- .published(post.isPublished())
- .build())
- .toList();
- category.getStatusOrDefault().setPostCount(compactPosts.size());
-
- long visiblePostCount = compactPosts.stream()
- .filter(post -> Objects.equals(true, post.getPublished())
- && Post.VisibleEnum.PUBLIC.equals(post.getVisible()))
- .count();
- category.getStatusOrDefault().setVisiblePostCount((int) visiblePostCount);
+ var visiblePostCount = countVisiblePosts(categoryNames);
+ category.getStatusOrDefault().setVisiblePostCount(visiblePostCount);
}
- /**
- * whether {@code categoryRefs} contains elements in {@code categoryNames}.
- *
- * @param categoryRefs category left to judge
- * @param categoryNames category right to judge
- * @return true if {@code categoryRefs} contains elements in {@code categoryNames}.
- */
- private boolean includes(@Nullable List categoryRefs, List categoryNames) {
- if (categoryRefs == null || categoryNames == null) {
- return false;
- }
- for (String categoryRef : categoryRefs) {
- if (categoryNames.contains(categoryRef)) {
- return true;
- }
- }
- return false;
+ int countTotalPosts(Set categoryNames) {
+ var postListOptions = new ListOptions();
+ postListOptions.setFieldSelector(FieldSelector.of(
+ basePostQuery(categoryNames)
+ ));
+ return (int) client.listBy(Post.class, postListOptions, PageRequestImpl.ofSize(1))
+ .getTotal();
+ }
+
+ int countVisiblePosts(Set categoryNames) {
+ var postListOptions = new ListOptions();
+ var fieldQuery = and(basePostQuery(categoryNames),
+ equal("spec.visible", Post.VisibleEnum.PUBLIC.name())
+ );
+ var labelSelector = LabelSelector.builder()
+ .eq(Post.PUBLISHED_LABEL, BooleanUtils.TRUE)
+ .build();
+ postListOptions.setFieldSelector(FieldSelector.of(fieldQuery));
+ postListOptions.setLabelSelector(labelSelector);
+ return (int) client.listBy(Post.class, postListOptions, PageRequestImpl.ofSize(1))
+ .getTotal();
+ }
+
+ private static Query basePostQuery(Set categoryNames) {
+ return and(isNull("metadata.deletionTimestamp"),
+ equal("spec.deleted", BooleanUtils.FALSE),
+ in("spec.categories", categoryNames)
+ );
}
private List listChildrenByName(String name) {
diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerIntegrationTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerIntegrationTest.java
new file mode 100644
index 000000000..248d658d6
--- /dev/null
+++ b/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerIntegrationTest.java
@@ -0,0 +1,230 @@
+package run.halo.app.core.extension.reconciler;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.test.annotation.DirtiesContext;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import run.halo.app.content.TestPost;
+import run.halo.app.core.extension.content.Category;
+import run.halo.app.core.extension.content.Post;
+import run.halo.app.extension.Extension;
+import run.halo.app.extension.ExtensionClient;
+import run.halo.app.extension.ExtensionStoreUtil;
+import run.halo.app.extension.Metadata;
+import run.halo.app.extension.ReactiveExtensionClient;
+import run.halo.app.extension.SchemeManager;
+import run.halo.app.extension.controller.Reconciler;
+import run.halo.app.extension.index.IndexerFactory;
+import run.halo.app.extension.store.ReactiveExtensionStoreClient;
+
+/**
+ * Tests for {@link CategoryReconciler}.
+ *
+ * @author guqing
+ * @since 2.0.0
+ */
+@DirtiesContext
+@SpringBootTest
+class CategoryReconcilerIntegrationTest {
+ private final List storedPosts = posts();
+ private final List storedCategories = categories();
+
+ @Autowired
+ private SchemeManager schemeManager;
+
+ @SpyBean
+ private ExtensionClient client;
+
+ @Autowired
+ private ReactiveExtensionClient reactiveClient;
+
+ @Autowired
+ private ReactiveExtensionStoreClient storeClient;
+
+ @Autowired
+ private IndexerFactory indexerFactory;
+
+ @Autowired
+ private CategoryReconciler categoryReconciler;
+
+ Mono deleteImmediately(Extension extension) {
+ var name = extension.getMetadata().getName();
+ var scheme = schemeManager.get(extension.getClass());
+ // un-index
+ var indexer = indexerFactory.getIndexer(extension.groupVersionKind());
+ indexer.unIndexRecord(extension.getMetadata().getName());
+
+ // delete from db
+ var storeName = ExtensionStoreUtil.buildStoreName(scheme, name);
+ return storeClient.delete(storeName, extension.getMetadata().getVersion())
+ .thenReturn(extension);
+ }
+
+ @BeforeEach
+ void setUp() {
+ Flux.fromIterable(storedPosts)
+ .flatMap(post -> reactiveClient.create(post))
+ .as(StepVerifier::create)
+ .expectNextCount(storedPosts.size())
+ .verifyComplete();
+
+ Flux.fromIterable(storedCategories)
+ .flatMap(category -> reactiveClient.create(category))
+ .as(StepVerifier::create)
+ .expectNextCount(storedCategories.size())
+ .verifyComplete();
+ }
+
+ @AfterEach
+ void tearDown() {
+ Flux.fromIterable(storedPosts)
+ .flatMap(this::deleteImmediately)
+ .as(StepVerifier::create)
+ .expectNextCount(storedPosts.size())
+ .verifyComplete();
+
+ Flux.fromIterable(storedCategories)
+ .flatMap(this::deleteImmediately)
+ .as(StepVerifier::create)
+ .expectNextCount(storedCategories.size())
+ .verifyComplete();
+ }
+
+ @Test
+ void reconcileStatusPostForCategoryA() {
+ reconcileStatusPostPilling("category-A");
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class);
+ verify(client, times(3)).update(captor.capture());
+ var values = captor.getAllValues();
+ assertThat(values.get(2).getStatusOrDefault().getPostCount()).isEqualTo(4);
+ assertThat(
+ captor.getAllValues().get(2).getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
+ }
+
+ @Test
+ void reconcileStatusPostForCategoryB() {
+ reconcileStatusPostPilling("category-B");
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class);
+ verify(client, times(3)).update(captor.capture());
+ Category category = captor.getAllValues().get(2);
+ assertThat(category.getStatusOrDefault().getPostCount()).isEqualTo(3);
+ assertThat(category.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
+ }
+
+ @Test
+ void reconcileStatusPostForCategoryC() {
+ reconcileStatusPostPilling("category-C");
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class);
+ verify(client, times(3)).update(captor.capture());
+ var value = captor.getAllValues().get(2);
+ assertThat(value.getStatusOrDefault().getPostCount()).isEqualTo(2);
+ assertThat(value.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
+ }
+
+ @Test
+ void reconcileStatusPostForCategoryD() {
+ reconcileStatusPostPilling("category-D");
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class);
+ verify(client, times(3)).update(captor.capture());
+ var value = captor.getAllValues().get(2);
+ assertThat(value.getStatusOrDefault().postCount).isEqualTo(1);
+ assertThat(value.getStatusOrDefault().visiblePostCount).isEqualTo(0);
+ }
+
+ private void reconcileStatusPostPilling(String reconcileCategoryName) {
+ Reconciler.Result result =
+ categoryReconciler.reconcile(new Reconciler.Request(reconcileCategoryName));
+ assertThat(result.reEnqueue()).isTrue();
+ assertThat(result.retryAfter()).isEqualTo(Duration.ofMinutes(1));
+ }
+
+ private List categories() {
+ /*
+ * |-A(post-4)
+ * |-B(post-3)
+ * |-|-C(post-2,post-1)
+ * |-D(post-1)
+ */
+ Category categoryA = category("category-A");
+ categoryA.getSpec().setChildren(List.of("category-B", "category-D"));
+
+ Category categoryB = category("category-B");
+ categoryB.getSpec().setChildren(List.of("category-C"));
+
+ Category categoryC = category("category-C");
+ Category categoryD = category("category-D");
+ return List.of(categoryA, categoryB, categoryC, categoryD);
+ }
+
+ private Category category(String name) {
+ Category category = new Category();
+ Metadata metadata = new Metadata();
+ metadata.setName(name);
+ category.setMetadata(metadata);
+ category.getMetadata().setFinalizers(Set.of(CategoryReconciler.FINALIZER_NAME));
+ category.setSpec(new Category.CategorySpec());
+ category.setStatus(new Category.CategoryStatus());
+
+ category.getSpec().setDisplayName("display-name");
+ category.getSpec().setSlug("slug");
+ category.getSpec().setPriority(0);
+ return category;
+ }
+
+ private List posts() {
+ /*
+ * |-A(post-4)
+ * |-B(post-3)
+ * |-|-C(post-2,post-1)
+ * |-D(post-1)
+ */
+ Post post1 = fakePost();
+ post1.getMetadata().setName("post-1");
+ post1.getSpec().setCategories(List.of("category-D", "category-C"));
+ post1.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
+
+ Post post2 = fakePost();
+ post2.getMetadata().setName("post-2");
+ post2.getSpec().setCategories(List.of("category-C"));
+ post2.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
+
+ Post post3 = fakePost();
+ post3.getMetadata().setName("post-3");
+ post3.getSpec().setCategories(List.of("category-B"));
+ post3.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
+
+ Post post4 = fakePost();
+ post4.getMetadata().setName("post-4");
+ post4.getSpec().setCategories(List.of("category-A"));
+ post4.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
+ return List.of(post1, post2, post3, post4);
+ }
+
+ Post fakePost() {
+ var post = TestPost.postV1();
+ post.getSpec().setAllowComment(true);
+ post.getSpec().setDeleted(false);
+ post.getSpec().setExcerpt(new Post.Excerpt());
+ post.getSpec().getExcerpt().setAutoGenerate(false);
+ post.getSpec().setPinned(false);
+ post.getSpec().setPriority(0);
+ post.getSpec().setPublish(false);
+ post.getSpec().setSlug("fake-post");
+ return post;
+ }
+}
\ No newline at end of file
diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java
deleted file mode 100644
index e6d16c47b..000000000
--- a/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java
+++ /dev/null
@@ -1,160 +0,0 @@
-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.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Optional;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.data.domain.Sort;
-import run.halo.app.content.TestPost;
-import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
-import run.halo.app.core.extension.content.Category;
-import run.halo.app.core.extension.content.Post;
-import run.halo.app.extension.ExtensionClient;
-import run.halo.app.extension.ListOptions;
-import run.halo.app.extension.Metadata;
-import run.halo.app.extension.controller.Reconciler;
-
-/**
- * Tests for {@link CategoryReconciler}.
- *
- * @author guqing
- * @since 2.0.0
- */
-@ExtendWith(MockitoExtension.class)
-class CategoryReconcilerTest {
- @Mock
- private ExtensionClient client;
-
- @Mock
- private CategoryPermalinkPolicy categoryPermalinkPolicy;
-
- @InjectMocks
- private CategoryReconciler categoryReconciler;
-
- @Test
- void reconcileStatusPostForCategoryA() {
- reconcileStatusPostPilling("category-A");
-
- ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class);
- verify(client, times(3)).update(captor.capture());
- assertThat(captor.getAllValues().get(1).getStatusOrDefault().getPostCount()).isEqualTo(4);
- assertThat(
- captor.getAllValues().get(1).getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
- }
-
- @Test
- void reconcileStatusPostForCategoryB() {
- reconcileStatusPostPilling("category-B");
- ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class);
- verify(client, times(3)).update(captor.capture());
- Category category = captor.getAllValues().get(1);
- assertThat(category.getStatusOrDefault().getPostCount()).isEqualTo(3);
- assertThat(category.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
- }
-
- @Test
- void reconcileStatusPostForCategoryC() {
- reconcileStatusPostPilling("category-C");
- ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class);
- verify(client, times(3)).update(captor.capture());
- assertThat(captor.getAllValues().get(1).getStatusOrDefault().getPostCount()).isEqualTo(2);
- assertThat(
- captor.getAllValues().get(1).getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
- }
-
- @Test
- void reconcileStatusPostForCategoryD() {
- reconcileStatusPostPilling("category-D");
- ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class);
- verify(client, times(3)).update(captor.capture());
- assertThat(captor.getAllValues().get(1).getStatusOrDefault().postCount).isEqualTo(1);
- assertThat(captor.getAllValues().get(1).getStatusOrDefault().visiblePostCount).isEqualTo(0);
- }
-
-
- private void reconcileStatusPostPilling(String reconcileCategoryName) {
- categories().forEach(category -> {
- lenient().when(client.fetch(eq(Category.class), eq(category.getMetadata().getName())))
- .thenReturn(Optional.of(category));
- });
-
- lenient().when(client.listAll(eq(Post.class), any(ListOptions.class), any(Sort.class)))
- .thenReturn(posts());
- lenient().when(client.listAll(eq(Category.class), any(), any()))
- .thenReturn(categories());
-
- Reconciler.Result result =
- categoryReconciler.reconcile(new Reconciler.Request(reconcileCategoryName));
- assertThat(result.reEnqueue()).isTrue();
- assertThat(result.retryAfter()).isEqualTo(Duration.ofMinutes(1));
- }
-
- private List categories() {
- /*
- * |-A(post-4)
- * |-B(post-3)
- * |-|-C(post-2,post-1)
- * |-D(post-1)
- */
- Category categoryA = category("category-A");
- categoryA.getSpec().setChildren(List.of("category-B", "category-D"));
-
- Category categoryB = category("category-B");
- categoryB.getSpec().setChildren(List.of("category-C"));
-
- Category categoryC = category("category-C");
- Category categoryD = category("category-D");
- return List.of(categoryA, categoryB, categoryC, categoryD);
- }
-
- private Category category(String name) {
- Category category = new Category();
- Metadata metadata = new Metadata();
- metadata.setName(name);
- category.setMetadata(metadata);
- category.setSpec(new Category.CategorySpec());
- category.setStatus(new Category.CategoryStatus());
- return category;
- }
-
- private List posts() {
- /*
- * |-A(post-4)
- * |-B(post-3)
- * |-|-C(post-2,post-1)
- * |-D(post-1)
- */
- Post post1 = TestPost.postV1();
- post1.getMetadata().setName("post-1");
- post1.getSpec().setCategories(List.of("category-D", "category-C"));
- post1.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
-
- Post post2 = TestPost.postV1();
- post2.getMetadata().setName("post-2");
- post2.getSpec().setCategories(List.of("category-C"));
- post2.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
-
- Post post3 = TestPost.postV1();
- post3.getMetadata().setName("post-3");
- post3.getSpec().setCategories(List.of("category-B"));
- post3.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
-
- Post post4 = TestPost.postV1();
- post4.getMetadata().setName("post-4");
- post4.getSpec().setCategories(List.of("category-A"));
- post4.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
- return List.of(post1, post2, post3, post4);
- }
-}
\ No newline at end of file