From b63131c36ed257a33ead32a969b08992702fd999 Mon Sep 17 00:00:00 2001
From: guqing <38999863+guqing@users.noreply.github.com>
Date: Mon, 31 Oct 2022 15:44:16 +0800
Subject: [PATCH] feat: support obtaining the previous post and next post
according to post name (#2636)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
#### What type of PR is this?
/kind feature
/area core
/milestone 2.0
#### What this PR does / why we need it:
支持通过文章名称获取上一篇和下一篇文章数据
本 PR 通过实现一个固定大小的滑动窗口数据结构来通过传入的文章名称获取上一篇和下一篇文章
例如当具有一系列文章名为 [a, b, c, d, e, f, g]
查询文章名为 d 的文章的上一篇和下一篇则使用一个固定大小为 3 的滑动窗口, 示意图如下
通过窗口右移来调整当窗口圈住所需元素 D 时且 元素 D 的位置不是窗口的最后一个元素时得到一个想要的窗口
此时:
1. 如果 D 位于 window 中的第一个元素则 D 没有上一篇
2. 如果 D 位于 window 中间则 window 的第一个元素为 D 的上一篇,最后一个元素为下一篇
3. 如果 D 位于 window 的最后一个位置,则 D 没有下一篇
#### Which issue(s) this PR fixes:
Fixes #2635
#### Special notes for your reviewer:
how to test it?
1. 创建几篇文章然后发布
4. 能通过 `${postFinder.cursor(your-post-name)}` 来获取到带有上一页下一页的文章数据
5. 当文章处于第一条时没有上一页,当文章处于最后一页时没有下一页(可以通过`${postCursor.hasPrevious()}` 和 `${postCursor.hasNext()}` 判断按钮是否展示)
/cc @halo-dev/sig-halo
#### Does this PR introduce a user-facing change?
```release-note
支持通过文章名称获取上一篇和下一篇文章数据
```
---
.../halo/app/theme/finders/PostFinder.java | 3 +
.../theme/finders/impl/PostFinderImpl.java | 107 ++++++++++++++++++
.../theme/finders/vo/NavigationPostVo.java | 33 ++++++
.../finders/impl/PostFinderImplTest.java | 51 +++++++++
4 files changed, 194 insertions(+)
create mode 100644 src/main/java/run/halo/app/theme/finders/vo/NavigationPostVo.java
diff --git a/src/main/java/run/halo/app/theme/finders/PostFinder.java b/src/main/java/run/halo/app/theme/finders/PostFinder.java
index d7a35ecfc..4ed7fb0d7 100644
--- a/src/main/java/run/halo/app/theme/finders/PostFinder.java
+++ b/src/main/java/run/halo/app/theme/finders/PostFinder.java
@@ -4,6 +4,7 @@ import org.springframework.lang.Nullable;
import run.halo.app.core.extension.Post;
import run.halo.app.extension.ListResult;
import run.halo.app.theme.finders.vo.ContentVo;
+import run.halo.app.theme.finders.vo.NavigationPostVo;
import run.halo.app.theme.finders.vo.PostArchiveVo;
import run.halo.app.theme.finders.vo.PostVo;
@@ -19,6 +20,8 @@ public interface PostFinder {
ContentVo content(String postName);
+ NavigationPostVo cursor(String current);
+
ListResult list(@Nullable Integer page, @Nullable Integer size);
ListResult listByCategory(@Nullable Integer page, @Nullable Integer size,
diff --git a/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java b/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java
index e5a62cb46..334835d5c 100644
--- a/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java
+++ b/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java
@@ -1,7 +1,10 @@
package run.halo.app.theme.finders.impl;
import java.time.Instant;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
import java.util.Comparator;
+import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -10,8 +13,10 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
import org.springframework.lang.NonNull;
import org.springframework.util.comparator.Comparators;
+import reactor.util.function.Tuple2;
import run.halo.app.content.ContentService;
import run.halo.app.core.extension.Counter;
import run.halo.app.core.extension.Post;
@@ -28,6 +33,7 @@ import run.halo.app.theme.finders.TagFinder;
import run.halo.app.theme.finders.vo.CategoryVo;
import run.halo.app.theme.finders.vo.ContentVo;
import run.halo.app.theme.finders.vo.Contributor;
+import run.halo.app.theme.finders.vo.NavigationPostVo;
import run.halo.app.theme.finders.vo.PostArchiveVo;
import run.halo.app.theme.finders.vo.PostArchiveYearMonthVo;
import run.halo.app.theme.finders.vo.PostVo;
@@ -94,6 +100,107 @@ public class PostFinderImpl implements PostFinder {
.block();
}
+ @Override
+ public NavigationPostVo cursor(String currentName) {
+ // TODO Optimize the post names query here
+ List postNames = client.list(Post.class, FIXED_PREDICATE, defaultComparator())
+ .map(post -> post.getMetadata().getName())
+ .collectList()
+ .block();
+ if (postNames == null) {
+ return NavigationPostVo.empty();
+ }
+
+ NavigationPostVo.NavigationPostVoBuilder builder = NavigationPostVo.builder()
+ .current(getByName(currentName));
+
+ Pair previousNextPair = postPreviousNextPair(postNames, currentName);
+ String previousPostName = previousNextPair.getLeft();
+ String nextPostName = previousNextPair.getRight();
+
+ if (previousPostName != null) {
+ builder.previous(getByName(previousPostName));
+ }
+
+ if (nextPostName != null) {
+ builder.next(getByName(nextPostName));
+ }
+ return builder.build();
+ }
+
+ static Pair postPreviousNextPair(List postNames,
+ String currentName) {
+ FixedSizeSlidingWindow window = new FixedSizeSlidingWindow<>(3);
+ for (String postName : postNames) {
+ window.add(postName);
+ if (!window.isFull()) {
+ continue;
+ }
+ int index = window.indexOf(currentName);
+ if (index == -1) {
+ continue;
+ }
+ // got expected window
+ if (index < 2) {
+ break;
+ }
+ }
+
+ List elements = window.elements();
+ Tuple2 previousNext;
+ // current post index
+ int index = elements.indexOf(currentName);
+
+ String previousPostName = null;
+ if (index != 0) {
+ previousPostName = elements.get(index - 1);
+ }
+
+ String nextPostName = null;
+ if (elements.size() - 1 > index) {
+ nextPostName = elements.get(index + 1);
+ }
+ return Pair.of(previousPostName, nextPostName);
+ }
+
+ static class FixedSizeSlidingWindow {
+ Deque queue;
+ int size;
+
+ public FixedSizeSlidingWindow(int size) {
+ this.size = size;
+ // FIFO
+ queue = new ArrayDeque<>(size);
+ }
+
+ /**
+ * Add element to the window.
+ * The element added first will be deleted when the element in the collection exceeds
+ * {@code size}.
+ */
+ public void add(T t) {
+ if (queue.size() == size) {
+ // remove first
+ queue.poll();
+ }
+ // add to last
+ queue.add(t);
+ }
+
+ public int indexOf(T o) {
+ List elements = elements();
+ return elements.indexOf(o);
+ }
+
+ public List elements() {
+ return new ArrayList<>(queue);
+ }
+
+ public boolean isFull() {
+ return queue.size() == size;
+ }
+ }
+
@Override
public ListResult list(Integer page, Integer size) {
return listPost(page, size, null, defaultComparator());
diff --git a/src/main/java/run/halo/app/theme/finders/vo/NavigationPostVo.java b/src/main/java/run/halo/app/theme/finders/vo/NavigationPostVo.java
new file mode 100644
index 000000000..523b7e3e1
--- /dev/null
+++ b/src/main/java/run/halo/app/theme/finders/vo/NavigationPostVo.java
@@ -0,0 +1,33 @@
+package run.halo.app.theme.finders.vo;
+
+import lombok.Builder;
+import lombok.Value;
+
+/**
+ * Post navigation vo to hold previous and next item.
+ *
+ * @author guqing
+ * @since 2.0.0
+ */
+@Value
+@Builder
+public class NavigationPostVo {
+
+ PostVo previous;
+
+ PostVo current;
+
+ PostVo next;
+
+ public boolean hasNext() {
+ return next != null;
+ }
+
+ public boolean hasPrevious() {
+ return previous != null;
+ }
+
+ public static NavigationPostVo empty() {
+ return NavigationPostVo.builder().build();
+ }
+}
diff --git a/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java b/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java
index 4847f6f5f..03c93ec88 100644
--- a/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java
+++ b/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java
@@ -7,8 +7,11 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.logging.log4j.util.Strings;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -114,6 +117,54 @@ class PostFinderImplTest {
assertThat(items.get(1).getMonths().get(0).getMonth()).isEqualTo("01");
}
+ @Test
+ void fixedSizeSlidingWindow() {
+ PostFinderImpl.FixedSizeSlidingWindow
+ window = new PostFinderImpl.FixedSizeSlidingWindow<>(3);
+
+ List list = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ window.add(i);
+ list.add(Strings.join(window.elements(), ','));
+ }
+ assertThat(list).isEqualTo(
+ List.of("0", "0,1", "0,1,2", "1,2,3", "2,3,4", "3,4,5", "4,5,6", "5,6,7", "6,7,8",
+ "7,8,9")
+ );
+ }
+
+ @Test
+ void postPreviousNextPair() {
+ List postNames = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ postNames.add("post-" + i);
+ }
+
+ // post-0, post-1, post-2
+ Pair previousNextPair =
+ PostFinderImpl.postPreviousNextPair(postNames, "post-0");
+ assertThat(previousNextPair.getLeft()).isNull();
+ assertThat(previousNextPair.getRight()).isEqualTo("post-1");
+
+ previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-1");
+ assertThat(previousNextPair.getLeft()).isEqualTo("post-0");
+ assertThat(previousNextPair.getRight()).isEqualTo("post-2");
+
+ // post-1, post-2, post-3
+ previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-2");
+ assertThat(previousNextPair.getLeft()).isEqualTo("post-1");
+ assertThat(previousNextPair.getRight()).isEqualTo("post-3");
+
+ // post-7, post-8, post-9
+ previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-8");
+ assertThat(previousNextPair.getLeft()).isEqualTo("post-7");
+ assertThat(previousNextPair.getRight()).isEqualTo("post-9");
+
+ previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-9");
+ assertThat(previousNextPair.getLeft()).isEqualTo("post-8");
+ assertThat(previousNextPair.getRight()).isNull();
+ }
+
List postsForArchives() {
Post post1 = post(1);
post1.getSpec().setPublished(true);