From 9fff76813450ae3a92dafc8be939c703746fd179 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Fri, 24 Feb 2023 17:38:44 +0800 Subject: [PATCH] perf: cannot be accessed for a long time during startup (#3300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /area core /milestone 2.3.x #### What this PR does / why we need it: 能通过注册 pattern 作为路由的就直接注册 pattern 以避免 Reconciler 还没结束而无法注册路由导致的访问问题。 1. 文章、标签、分类的 permalink 规则会记录在对应 extension 的 annotations 中为 `content.halo.run/permalink-pattern: some-pattern` ,当系统设置中路由规则改变时会刷一遍这些资源的 `content.halo.run/permalink-pattern` annotation。 3. 自定义页面的访问是通过 SinglePageRoute 控制,它会在 single page 添加、更新、删除时维护 quickRouteMap 的集合,在路由到它时直接通过 request path 查找 map key,找到则直接返回 HandleFunction。 4. 除了自定义页面外其他都是通过 ThemeCompositeRouterFunction 作为路由管理器,系统启动时文章等的 RouterFunction 会被生成并缓存到 cachedRouters 的一个 List 集合中,当路由规则改变会清理它重新赋值。 #### Which issue(s) this PR fixes: Fixes #3254 #### Special notes for your reviewer: how to test it? 1. 测试首页、文章、标签、分类、归档页、自定义页面和作者页等的访问。 6. 测试添加、删除资源和修改系统路由规则后上述资源的访问。 7. 测试分页路径如 /tags/slug/page/1 的访问。 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 优化启动时页面长时间无法访问的问题 ``` --- .../permalinks/CategoryPermalinkPolicy.java | 71 +--- .../content/permalinks/PermalinkPolicy.java | 4 - .../permalinks/PostPermalinkPolicy.java | 72 +--- .../permalinks/TagPermalinkPolicy.java | 69 +--- .../app/core/extension/content/Constant.java | 1 + .../halo/app/core/extension/content/Post.java | 26 +- .../reconciler/CategoryReconciler.java | 36 +- .../extension/reconciler/PostReconciler.java | 12 +- .../reconciler/SinglePageReconciler.java | 28 -- .../extension/reconciler/TagReconciler.java | 37 +- .../reconciler/TagRouteReconciler.java | 56 --- .../extension/reconciler/UserReconciler.java | 16 - .../SystemConfigurableEnvironmentFetcher.java | 4 + .../run/halo/app/infra/SystemSetting.java | 9 + .../infra/exception/NotFoundException.java | 20 +- .../run/halo/app/infra/utils/HaloUtils.java | 6 + .../dialect/ContentTemplateHeadProcessor.java | 2 +- .../dialect/TemplateGlobalHeadProcessor.java | 2 +- .../theme/finders/impl/PostFinderImpl.java | 2 - .../finders/impl/SinglePageFinderImpl.java | 1 + .../ExtensionPermalinkPatternUpdater.java | 82 ++++ .../run/halo/app/theme/router/GvkName.java | 12 - .../theme/router/PermalinkHttpGetRouter.java | 212 ----------- .../router/PermalinkIndexAddCommand.java | 30 -- .../router/PermalinkIndexChangedEvent.java | 28 -- .../router/PermalinkIndexDeleteCommand.java | 24 -- .../router/PermalinkIndexUpdateCommand.java | 30 -- .../app/theme/router/PermalinkIndexer.java | 251 ------------ .../router/PermalinkPatternProvider.java | 60 --- .../theme/router/PermalinkRefreshHandler.java | 107 ------ .../app/theme/router/RadixRouterTree.java | 224 ----------- .../run/halo/app/theme/router/RadixTree.java | 360 ------------------ .../halo/app/theme/router/RadixTreeNode.java | 85 ----- .../app/theme/router/SinglePageRoute.java | 148 +++++++ .../router/ThemeCompositeRouterFunction.java | 124 +++++- .../router/factories/ArchiveRouteFactory.java | 102 +++++ .../factories/AuthorPostsRouteFactory.java | 78 ++++ .../CategoriesRouteFactory.java} | 30 +- .../factories/CategoryPostRouteFactory.java | 95 +++++ .../IndexRouteFactory.java} | 52 ++- .../{strategy => factories}/ModelConst.java | 2 +- .../router/factories/PostRouteFactory.java | 236 ++++++++++++ .../theme/router/factories/RouteFactory.java | 29 ++ .../router/factories/TagPostRouteFactory.java | 89 +++++ .../router/factories/TagsRouteFactory.java | 46 +++ .../strategy/ArchivesRouteStrategy.java | 88 ----- .../router/strategy/AuthorRouteStrategy.java | 76 ---- .../strategy/CategoryRouteStrategy.java | 83 ---- .../DetailsPageRouteHandlerStrategy.java | 21 - .../ListPageRouteHandlerStrategy.java | 22 -- .../router/strategy/PostRouteStrategy.java | 72 ---- .../strategy/SinglePageRouteStrategy.java | 64 ---- .../router/strategy/TagRouteStrategy.java | 72 ---- .../router/strategy/TagsRouteStrategy.java | 47 --- .../CategoryPermalinkPolicyTest.java | 33 +- .../permalinks/PostPermalinkPolicyTest.java | 49 +-- .../permalinks/TagPermalinkPolicyTest.java | 29 +- .../reconciler/CategoryReconcilerTest.java | 8 +- .../reconciler/PostReconcilerTest.java | 3 - .../reconciler/SinglePageReconcilerTest.java | 8 - .../reconciler/TagReconcilerTest.java | 24 +- .../reconciler/UserReconcilerTest.java | 3 - .../dialect/HaloProcessorDialectTest.java | 2 +- .../impl/SinglePageFinderImplTest.java | 6 + .../router/{strategy => }/EmptyView.java | 2 +- .../theme/router/PermalinkIndexerTest.java | 136 ------- .../router/PermalinkPatternProviderTest.java | 101 ----- .../app/theme/router/RadixRouterTreeTest.java | 51 --- .../halo/app/theme/router/RadixTreeTest.java | 267 ------------- .../theme/router/ViewNameResolverTest.java | 1 - .../factories/ArchiveRouteFactoryTest.java | 66 ++++ .../AuthorPostsRouteFactoryTest.java | 43 +++ .../factories/CategoriesRouteFactoryTest.java | 41 ++ .../factories/IndexRouteFactoryTest.java | 42 ++ .../RouteFactoryTestSuite.java} | 26 +- .../factories/TagPostRouteFactoryTest.java | 68 ++++ .../strategy/ArchivesRouteStrategyTest.java | 99 ----- .../strategy/AuthorRouteStrategyTest.java | 68 ---- .../strategy/CategoriesRouteStrategyTest.java | 58 --- .../strategy/CategoryRouteStrategyTest.java | 82 ---- .../strategy/IndexRouteStrategyTest.java | 60 --- .../strategy/PostRouteStrategyTest.java | 132 ------- .../strategy/SinglePageRouteStrategyTest.java | 139 ------- .../router/strategy/TagRouteStrategyTest.java | 63 --- .../strategy/TagsRouteStrategyTest.java | 58 --- 85 files changed, 1547 insertions(+), 3776 deletions(-) delete mode 100644 src/main/java/run/halo/app/core/extension/reconciler/TagRouteReconciler.java create mode 100644 src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java delete mode 100644 src/main/java/run/halo/app/theme/router/GvkName.java delete mode 100644 src/main/java/run/halo/app/theme/router/PermalinkHttpGetRouter.java delete mode 100644 src/main/java/run/halo/app/theme/router/PermalinkIndexAddCommand.java delete mode 100644 src/main/java/run/halo/app/theme/router/PermalinkIndexChangedEvent.java delete mode 100644 src/main/java/run/halo/app/theme/router/PermalinkIndexDeleteCommand.java delete mode 100644 src/main/java/run/halo/app/theme/router/PermalinkIndexUpdateCommand.java delete mode 100644 src/main/java/run/halo/app/theme/router/PermalinkIndexer.java delete mode 100644 src/main/java/run/halo/app/theme/router/PermalinkPatternProvider.java delete mode 100644 src/main/java/run/halo/app/theme/router/PermalinkRefreshHandler.java delete mode 100644 src/main/java/run/halo/app/theme/router/RadixRouterTree.java delete mode 100644 src/main/java/run/halo/app/theme/router/RadixTree.java delete mode 100644 src/main/java/run/halo/app/theme/router/RadixTreeNode.java create mode 100644 src/main/java/run/halo/app/theme/router/SinglePageRoute.java create mode 100644 src/main/java/run/halo/app/theme/router/factories/ArchiveRouteFactory.java create mode 100644 src/main/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactory.java rename src/main/java/run/halo/app/theme/router/{strategy/CategoriesRouteStrategy.java => factories/CategoriesRouteFactory.java} (55%) create mode 100644 src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java rename src/main/java/run/halo/app/theme/router/{strategy/IndexRouteStrategy.java => factories/IndexRouteFactory.java} (59%) rename src/main/java/run/halo/app/theme/router/{strategy => factories}/ModelConst.java (83%) create mode 100644 src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java create mode 100644 src/main/java/run/halo/app/theme/router/factories/RouteFactory.java create mode 100644 src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java create mode 100644 src/main/java/run/halo/app/theme/router/factories/TagsRouteFactory.java delete mode 100644 src/main/java/run/halo/app/theme/router/strategy/ArchivesRouteStrategy.java delete mode 100644 src/main/java/run/halo/app/theme/router/strategy/AuthorRouteStrategy.java delete mode 100644 src/main/java/run/halo/app/theme/router/strategy/CategoryRouteStrategy.java delete mode 100644 src/main/java/run/halo/app/theme/router/strategy/DetailsPageRouteHandlerStrategy.java delete mode 100644 src/main/java/run/halo/app/theme/router/strategy/ListPageRouteHandlerStrategy.java delete mode 100644 src/main/java/run/halo/app/theme/router/strategy/PostRouteStrategy.java delete mode 100644 src/main/java/run/halo/app/theme/router/strategy/SinglePageRouteStrategy.java delete mode 100644 src/main/java/run/halo/app/theme/router/strategy/TagRouteStrategy.java delete mode 100644 src/main/java/run/halo/app/theme/router/strategy/TagsRouteStrategy.java rename src/test/java/run/halo/app/theme/router/{strategy => }/EmptyView.java (92%) delete mode 100644 src/test/java/run/halo/app/theme/router/PermalinkIndexerTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/PermalinkPatternProviderTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/RadixRouterTreeTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/RadixTreeTest.java create mode 100644 src/test/java/run/halo/app/theme/router/factories/ArchiveRouteFactoryTest.java create mode 100644 src/test/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactoryTest.java create mode 100644 src/test/java/run/halo/app/theme/router/factories/CategoriesRouteFactoryTest.java create mode 100644 src/test/java/run/halo/app/theme/router/factories/IndexRouteFactoryTest.java rename src/test/java/run/halo/app/theme/router/{strategy/RouterStrategyTestSuite.java => factories/RouteFactoryTestSuite.java} (72%) create mode 100644 src/test/java/run/halo/app/theme/router/factories/TagPostRouteFactoryTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/strategy/ArchivesRouteStrategyTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/strategy/AuthorRouteStrategyTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/strategy/CategoriesRouteStrategyTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/strategy/CategoryRouteStrategyTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/strategy/IndexRouteStrategyTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/strategy/PostRouteStrategyTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/strategy/SinglePageRouteStrategyTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/strategy/TagRouteStrategyTest.java delete mode 100644 src/test/java/run/halo/app/theme/router/strategy/TagsRouteStrategyTest.java diff --git a/src/main/java/run/halo/app/content/permalinks/CategoryPermalinkPolicy.java b/src/main/java/run/halo/app/content/permalinks/CategoryPermalinkPolicy.java index bfc83f744..8ce95e151 100644 --- a/src/main/java/run/halo/app/content/permalinks/CategoryPermalinkPolicy.java +++ b/src/main/java/run/halo/app/content/permalinks/CategoryPermalinkPolicy.java @@ -3,79 +3,46 @@ package run.halo.app.content.permalinks; import static org.springframework.web.util.UriUtils.encode; import java.nio.charset.StandardCharsets; -import org.springframework.context.ApplicationContext; +import java.util.Map; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import run.halo.app.core.extension.content.Category; -import run.halo.app.extension.GroupVersionKind; +import run.halo.app.core.extension.content.Constant; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.infra.ExternalUrlSupplier; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.SystemSetting; import run.halo.app.infra.utils.PathUtils; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.router.PermalinkIndexAddCommand; -import run.halo.app.theme.router.PermalinkIndexDeleteCommand; -import run.halo.app.theme.router.PermalinkIndexUpdateCommand; -import run.halo.app.theme.router.PermalinkPatternProvider; -import run.halo.app.theme.router.PermalinkWatch; /** * @author guqing * @since 2.0.0 */ @Component -public class CategoryPermalinkPolicy - implements PermalinkPolicy, PermalinkWatch { - private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Category.class); - private final ApplicationContext applicationContext; - private final PermalinkPatternProvider permalinkPatternProvider; +@RequiredArgsConstructor +public class CategoryPermalinkPolicy implements PermalinkPolicy { + public static final String DEFAULT_PERMALINK_PREFIX = + SystemSetting.ThemeRouteRules.empty().getCategories(); private final ExternalUrlSupplier externalUrlSupplier; - - public CategoryPermalinkPolicy(ApplicationContext applicationContext, - PermalinkPatternProvider permalinkPatternProvider, - ExternalUrlSupplier externalUrlSupplier) { - this.applicationContext = applicationContext; - this.permalinkPatternProvider = permalinkPatternProvider; - this.externalUrlSupplier = externalUrlSupplier; - } + private final SystemConfigurableEnvironmentFetcher environmentFetcher; @Override public String permalink(Category category) { + Map annotations = ExtensionUtil.nullSafeAnnotations(category); + String permalinkPrefix = + annotations.getOrDefault(Constant.PERMALINK_PATTERN_ANNO, DEFAULT_PERMALINK_PREFIX); String slug = encode(category.getSpec().getSlug(), StandardCharsets.UTF_8); - String path = PathUtils.combinePath(pattern(), slug); + String path = PathUtils.combinePath(permalinkPrefix, slug); return externalUrlSupplier.get() .resolve(path) .normalize().toString(); } - @Override - public String templateName() { - return DefaultTemplateEnum.CATEGORY.getValue(); - } - - @Override public String pattern() { - return permalinkPatternProvider.getPattern(DefaultTemplateEnum.CATEGORY); - } - - @Override - public void onPermalinkAdd(Category category) { - applicationContext.publishEvent(new PermalinkIndexAddCommand(this, getLocator(category), - category.getStatusOrDefault().getPermalink())); - } - - @Override - public void onPermalinkUpdate(Category category) { - applicationContext.publishEvent(new PermalinkIndexUpdateCommand(this, getLocator(category), - category.getStatusOrDefault().getPermalink())); - } - - @Override - public void onPermalinkDelete(Category category) { - applicationContext.publishEvent( - new PermalinkIndexDeleteCommand(this, getLocator(category))); - } - - private ExtensionLocator getLocator(Category category) { - return new ExtensionLocator(gvk, category.getMetadata().getName(), - category.getSpec().getSlug()); + return environmentFetcher.fetchRouteRules() + .map(SystemSetting.ThemeRouteRules::getCategories) + .blockOptional() + .orElse(DEFAULT_PERMALINK_PREFIX); } } diff --git a/src/main/java/run/halo/app/content/permalinks/PermalinkPolicy.java b/src/main/java/run/halo/app/content/permalinks/PermalinkPolicy.java index bbc75ada4..03ec67ae7 100644 --- a/src/main/java/run/halo/app/content/permalinks/PermalinkPolicy.java +++ b/src/main/java/run/halo/app/content/permalinks/PermalinkPolicy.java @@ -13,8 +13,4 @@ public interface PermalinkPolicy { new PropertyPlaceholderHelper("{", "}"); String permalink(T extension); - - String templateName(); - - String pattern(); } diff --git a/src/main/java/run/halo/app/content/permalinks/PostPermalinkPolicy.java b/src/main/java/run/halo/app/content/permalinks/PostPermalinkPolicy.java index c21f46ada..c4c9e3cdc 100644 --- a/src/main/java/run/halo/app/content/permalinks/PostPermalinkPolicy.java +++ b/src/main/java/run/halo/app/content/permalinks/PostPermalinkPolicy.java @@ -8,79 +8,45 @@ import java.text.NumberFormat; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.Objects; +import java.util.Map; import java.util.Properties; -import org.springframework.context.ApplicationContext; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import run.halo.app.core.extension.content.Constant; import run.halo.app.core.extension.content.Post; -import run.halo.app.extension.GroupVersionKind; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.infra.ExternalUrlSupplier; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.SystemSetting; import run.halo.app.infra.utils.PathUtils; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.router.PermalinkIndexAddCommand; -import run.halo.app.theme.router.PermalinkIndexDeleteCommand; -import run.halo.app.theme.router.PermalinkIndexUpdateCommand; -import run.halo.app.theme.router.PermalinkPatternProvider; -import run.halo.app.theme.router.PermalinkWatch; /** * @author guqing * @since 2.0.0 */ @Component -public class PostPermalinkPolicy implements PermalinkPolicy, PermalinkWatch { - private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Post.class); +@RequiredArgsConstructor +public class PostPermalinkPolicy implements PermalinkPolicy { + public static final String DEFAULT_PERMALINK_PATTERN = + SystemSetting.ThemeRouteRules.empty().getPost(); private static final NumberFormat NUMBER_FORMAT = new DecimalFormat("00"); - private final PermalinkPatternProvider permalinkPatternProvider; - private final ApplicationContext applicationContext; + private final SystemConfigurableEnvironmentFetcher environmentFetcher; private final ExternalUrlSupplier externalUrlSupplier; - public PostPermalinkPolicy(PermalinkPatternProvider permalinkPatternProvider, - ApplicationContext applicationContext, ExternalUrlSupplier externalUrlSupplier) { - this.permalinkPatternProvider = permalinkPatternProvider; - this.applicationContext = applicationContext; - this.externalUrlSupplier = externalUrlSupplier; - } - @Override public String permalink(Post post) { - return createPermalink(post, pattern()); + Map annotations = ExtensionUtil.nullSafeAnnotations(post); + String permalinkPattern = + annotations.getOrDefault(Constant.PERMALINK_PATTERN_ANNO, DEFAULT_PERMALINK_PATTERN); + return createPermalink(post, permalinkPattern); } - @Override - public String templateName() { - return DefaultTemplateEnum.POST.getValue(); - } - - @Override public String pattern() { - return permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST); - } - - @Override - public void onPermalinkAdd(Post post) { - if (!post.isPublished() || Objects.equals(true, post.getSpec().getDeleted())) { - return; - } - // publish when post is published and not deleted - applicationContext.publishEvent(new PermalinkIndexAddCommand(this, getLocator(post), - post.getStatusOrDefault().getPermalink())); - } - - @Override - public void onPermalinkUpdate(Post post) { - applicationContext.publishEvent(new PermalinkIndexUpdateCommand(this, getLocator(post), - post.getStatusOrDefault().getPermalink())); - } - - @Override - public void onPermalinkDelete(Post post) { - applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, getLocator(post))); - } - - private ExtensionLocator getLocator(Post post) { - return new ExtensionLocator(gvk, post.getMetadata().getName(), post.getSpec().getSlug()); + return environmentFetcher.fetchRouteRules() + .map(SystemSetting.ThemeRouteRules::getPost) + .blockOptional() + .orElse(DEFAULT_PERMALINK_PATTERN); } private String createPermalink(Post post, String pattern) { diff --git a/src/main/java/run/halo/app/content/permalinks/TagPermalinkPolicy.java b/src/main/java/run/halo/app/content/permalinks/TagPermalinkPolicy.java index 8000e9862..bb86ab5c5 100644 --- a/src/main/java/run/halo/app/content/permalinks/TagPermalinkPolicy.java +++ b/src/main/java/run/halo/app/content/permalinks/TagPermalinkPolicy.java @@ -1,76 +1,47 @@ package run.halo.app.content.permalinks; import java.nio.charset.StandardCharsets; -import org.springframework.context.ApplicationContext; +import java.util.Map; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.web.util.UriUtils; +import run.halo.app.core.extension.content.Constant; import run.halo.app.core.extension.content.Tag; -import run.halo.app.extension.GroupVersionKind; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.infra.ExternalUrlSupplier; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.SystemSetting; import run.halo.app.infra.utils.PathUtils; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.router.PermalinkIndexAddCommand; -import run.halo.app.theme.router.PermalinkIndexDeleteCommand; -import run.halo.app.theme.router.PermalinkIndexUpdateCommand; -import run.halo.app.theme.router.PermalinkPatternProvider; -import run.halo.app.theme.router.PermalinkWatch; /** * @author guqing * @since 2.0.0 */ @Component -public class TagPermalinkPolicy implements PermalinkPolicy, PermalinkWatch { - private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Tag.class); - private final PermalinkPatternProvider permalinkPatternProvider; - private final ApplicationContext applicationContext; - +@RequiredArgsConstructor +public class TagPermalinkPolicy implements PermalinkPolicy { + public static final String DEFAULT_PERMALINK_PREFIX = + SystemSetting.ThemeRouteRules.empty().getTags(); private final ExternalUrlSupplier externalUrlSupplier; - - public TagPermalinkPolicy(PermalinkPatternProvider permalinkPatternProvider, - ApplicationContext applicationContext, ExternalUrlSupplier externalUrlSupplier) { - this.permalinkPatternProvider = permalinkPatternProvider; - this.applicationContext = applicationContext; - this.externalUrlSupplier = externalUrlSupplier; - } + private final SystemConfigurableEnvironmentFetcher environmentFetcher; @Override public String permalink(Tag tag) { + Map annotations = ExtensionUtil.nullSafeAnnotations(tag); + String permalinkPrefix = + annotations.getOrDefault(Constant.PERMALINK_PATTERN_ANNO, DEFAULT_PERMALINK_PREFIX); + String slug = UriUtils.encode(tag.getSpec().getSlug(), StandardCharsets.UTF_8); - String path = PathUtils.combinePath(pattern(), slug); + String path = PathUtils.combinePath(permalinkPrefix, slug); return externalUrlSupplier.get() .resolve(path) .normalize().toString(); } - @Override - public String templateName() { - return DefaultTemplateEnum.TAG.getValue(); - } - - @Override public String pattern() { - return permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAG); - } - - @Override - public void onPermalinkAdd(Tag tag) { - applicationContext.publishEvent(new PermalinkIndexAddCommand(this, getLocator(tag), - tag.getStatusOrDefault().getPermalink())); - } - - @Override - public void onPermalinkUpdate(Tag tag) { - applicationContext.publishEvent(new PermalinkIndexUpdateCommand(this, getLocator(tag), - tag.getStatusOrDefault().getPermalink())); - } - - @Override - public void onPermalinkDelete(Tag tag) { - applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, getLocator(tag))); - } - - private ExtensionLocator getLocator(Tag tag) { - return new ExtensionLocator(gvk, tag.getMetadata().getName(), tag.getSpec().getSlug()); + return environmentFetcher.fetchRouteRules() + .map(SystemSetting.ThemeRouteRules::getTags) + .blockOptional() + .orElse(DEFAULT_PERMALINK_PREFIX); } } diff --git a/src/main/java/run/halo/app/core/extension/content/Constant.java b/src/main/java/run/halo/app/core/extension/content/Constant.java index d4e28d922..93b4ba1c6 100644 --- a/src/main/java/run/halo/app/core/extension/content/Constant.java +++ b/src/main/java/run/halo/app/core/extension/content/Constant.java @@ -7,4 +7,5 @@ public enum Constant { public static final String VERSION = "v1alpha1"; public static final String LAST_READ_TIME_ANNO = "content.halo.run/last-read-time"; + public static final String PERMALINK_PATTERN_ANNO = "content.halo.run/permalink-pattern"; } diff --git a/src/main/java/run/halo/app/core/extension/content/Post.java b/src/main/java/run/halo/app/core/extension/content/Post.java index a057c506c..8982fb654 100644 --- a/src/main/java/run/halo/app/core/extension/content/Post.java +++ b/src/main/java/run/halo/app/core/extension/content/Post.java @@ -4,6 +4,7 @@ import static java.lang.Boolean.parseBoolean; import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.media.Schema.RequiredMode; import java.time.Instant; import java.util.List; import java.util.Map; @@ -48,8 +49,9 @@ public class Post extends AbstractExtension { public static final String ARCHIVE_YEAR_LABEL = "content.halo.run/archive-year"; public static final String ARCHIVE_MONTH_LABEL = "content.halo.run/archive-month"; + public static final String ARCHIVE_DAY_LABEL = "content.halo.run/archive-day"; - @Schema(required = true) + @Schema(requiredMode = RequiredMode.REQUIRED) private PostSpec spec; @Schema @@ -81,10 +83,10 @@ public class Post extends AbstractExtension { @Data public static class PostSpec { - @Schema(required = true, minLength = 1) + @Schema(requiredMode = RequiredMode.REQUIRED, minLength = 1) private String title; - @Schema(required = true, minLength = 1) + @Schema(requiredMode = RequiredMode.REQUIRED, minLength = 1) private String slug; /** @@ -102,27 +104,27 @@ public class Post extends AbstractExtension { private String cover; - @Schema(required = true, defaultValue = "false") + @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "false") private Boolean deleted; - @Schema(required = true, defaultValue = "false") + @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "false") private Boolean publish; private Instant publishTime; - @Schema(required = true, defaultValue = "false") + @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "false") private Boolean pinned; - @Schema(required = true, defaultValue = "true") + @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "true") private Boolean allowComment; - @Schema(required = true, defaultValue = "PUBLIC") + @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "PUBLIC") private VisibleEnum visible; - @Schema(required = true, defaultValue = "0") + @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "0") private Integer priority; - @Schema(required = true) + @Schema(requiredMode = RequiredMode.REQUIRED) private Excerpt excerpt; private List categories; @@ -134,7 +136,7 @@ public class Post extends AbstractExtension { @Data public static class PostStatus { - @Schema(required = true) + @Schema(requiredMode = RequiredMode.REQUIRED) private String phase; @Schema @@ -164,7 +166,7 @@ public class Post extends AbstractExtension { @Data public static class Excerpt { - @Schema(required = true, defaultValue = "true") + @Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "true") private Boolean autoGenerate; private String raw; diff --git a/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java index b03a2fcdf..67bdf64fd 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java @@ -11,12 +11,16 @@ 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.StringUtils; 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; +import run.halo.app.core.extension.content.Constant; import run.halo.app.core.extension.content.Post; import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.Reconciler; @@ -29,17 +33,12 @@ import run.halo.app.infra.utils.JsonUtils; * @since 2.0.0 */ @Component +@AllArgsConstructor public class CategoryReconciler implements Reconciler { private static final String FINALIZER_NAME = "category-protection"; private final ExtensionClient client; private final CategoryPermalinkPolicy categoryPermalinkPolicy; - public CategoryReconciler(ExtensionClient client, - CategoryPermalinkPolicy categoryPermalinkPolicy) { - this.client = client; - this.categoryPermalinkPolicy = categoryPermalinkPolicy; - } - @Override public Result reconcile(Request request) { return client.fetch(Category.class, request.name()) @@ -50,6 +49,8 @@ public class CategoryReconciler implements Reconciler { } addFinalizerIfNecessary(category); + reconcileMetadata(request.name()); + reconcileStatusPermalink(request.name()); reconcileStatusPosts(request.name()); @@ -65,6 +66,20 @@ public class CategoryReconciler implements Reconciler { .build(); } + void reconcileMetadata(String name) { + client.fetch(Category.class, name).ifPresent(category -> { + Map annotations = ExtensionUtil.nullSafeAnnotations(category); + String oldPermalinkPattern = annotations.get(Constant.PERMALINK_PATTERN_ANNO); + + String newPattern = categoryPermalinkPolicy.pattern(); + annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern); + + if (!StringUtils.equals(oldPermalinkPattern, newPattern)) { + client.update(category); + } + }); + } + private void addFinalizerIfNecessary(Category oldCategory) { Set finalizers = oldCategory.getMetadata().getFinalizers(); if (finalizers != null && finalizers.contains(FINALIZER_NAME)) { @@ -82,14 +97,8 @@ public class CategoryReconciler implements Reconciler { }); } - private void cleanUpResources(Category category) { - // remove permalink from permalink indexer - categoryPermalinkPolicy.onPermalinkDelete(category); - } - private void cleanUpResourcesAndRemoveFinalizer(String categoryName) { client.fetch(Category.class, categoryName).ifPresent(category -> { - cleanUpResources(category); if (category.getMetadata().getFinalizers() != null) { category.getMetadata().getFinalizers().remove(FINALIZER_NAME); } @@ -101,11 +110,8 @@ public class CategoryReconciler implements Reconciler { client.fetch(Category.class, name) .ifPresent(category -> { Category oldCategory = JsonUtils.deepCopy(category); - categoryPermalinkPolicy.onPermalinkDelete(oldCategory); - category.getStatusOrDefault() .setPermalink(categoryPermalinkPolicy.permalink(category)); - categoryPermalinkPolicy.onPermalinkAdd(category); if (!oldCategory.equals(category)) { client.update(category); diff --git a/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java index dfd3d8091..4985e0798 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java @@ -16,6 +16,7 @@ import org.springframework.util.Assert; import run.halo.app.content.PostService; 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.Snapshot; import run.halo.app.event.post.PostPublishedEvent; @@ -237,10 +238,16 @@ public class PostReconciler implements Reconciler { if (publishTime != null) { labels.put(Post.ARCHIVE_YEAR_LABEL, HaloUtils.getYearText(publishTime)); labels.put(Post.ARCHIVE_MONTH_LABEL, HaloUtils.getMonthText(publishTime)); + labels.put(Post.ARCHIVE_DAY_LABEL, HaloUtils.getDayText(publishTime)); } if (!labels.containsKey(Post.PUBLISHED_LABEL)) { labels.put(Post.PUBLISHED_LABEL, Boolean.FALSE.toString()); } + + Map annotations = ExtensionUtil.nullSafeAnnotations(post); + String newPattern = postPermalinkPolicy.pattern(); + annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern); + if (!oldPost.equals(post)) { client.update(post); } @@ -250,11 +257,9 @@ public class PostReconciler implements Reconciler { private void reconcileStatus(String name) { client.fetch(Post.class, name).ifPresent(post -> { final Post oldPost = JsonUtils.deepCopy(post); - postPermalinkPolicy.onPermalinkDelete(oldPost); post.getStatusOrDefault() .setPermalink(postPermalinkPolicy.permalink(post)); - postPermalinkPolicy.onPermalinkAdd(post); Post.PostStatus status = post.getStatusOrDefault(); if (status.getPhase() == null) { @@ -344,9 +349,6 @@ public class PostReconciler implements Reconciler { } private void cleanUpResources(Post post) { - // remove permalink from permalink indexer - postPermalinkPolicy.onPermalinkDelete(post); - // clean up snapshots final Ref ref = Ref.of(post); client.list(Snapshot.class, diff --git a/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java index 981fd107e..73f922127 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java @@ -14,11 +14,9 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jsoup.Jsoup; -import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import run.halo.app.content.SinglePageService; -import run.halo.app.content.permalinks.ExtensionLocator; import run.halo.app.core.extension.content.Comment; import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.SinglePage; @@ -26,7 +24,6 @@ import run.halo.app.core.extension.content.Snapshot; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionOperator; import run.halo.app.extension.ExtensionUtil; -import run.halo.app.extension.GroupVersionKind; import run.halo.app.extension.Ref; import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; @@ -38,8 +35,6 @@ import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.infra.utils.JsonUtils; import run.halo.app.metrics.CounterService; import run.halo.app.metrics.MeterUtils; -import run.halo.app.theme.router.PermalinkIndexAddCommand; -import run.halo.app.theme.router.PermalinkIndexDeleteCommand; /** *

Reconciler for {@link SinglePage}.

@@ -58,10 +53,8 @@ import run.halo.app.theme.router.PermalinkIndexDeleteCommand; @Component public class SinglePageReconciler implements Reconciler { private static final String FINALIZER_NAME = "single-page-protection"; - private static final GroupVersionKind GVK = GroupVersionKind.fromExtension(SinglePage.class); private final ExtensionClient client; private final SinglePageService singlePageService; - private final ApplicationContext applicationContext; private final CounterService counterService; private final ExternalUrlSupplier externalUrlSupplier; @@ -237,9 +230,6 @@ public class SinglePageReconciler implements Reconciler { } private void cleanUpResources(SinglePage singlePage) { - // remove permalink from permalink indexer - permalinkOnDelete(singlePage); - // clean up snapshot Ref ref = Ref.of(singlePage); client.list(Snapshot.class, @@ -291,36 +281,18 @@ public class SinglePageReconciler implements Reconciler { }); } - private void permalinkOnDelete(SinglePage singlePage) { - ExtensionLocator locator = new ExtensionLocator(GVK, singlePage.getMetadata().getName(), - singlePage.getSpec().getSlug()); - applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, locator)); - } - String createPermalink(SinglePage page) { var permalink = encodePath(page.getSpec().getSlug(), UTF_8); permalink = StringUtils.prependIfMissing(permalink, "/"); return externalUrlSupplier.get().resolve(permalink).normalize().toString(); } - private void permalinkOnAdd(SinglePage singlePage) { - if (!singlePage.isPublished() || Objects.equals(true, singlePage.getSpec().getDeleted())) { - return; - } - ExtensionLocator locator = new ExtensionLocator(GVK, singlePage.getMetadata().getName(), - singlePage.getSpec().getSlug()); - applicationContext.publishEvent(new PermalinkIndexAddCommand(this, locator, - singlePage.getStatusOrDefault().getPermalink())); - } - private void reconcileStatus(String name) { client.fetch(SinglePage.class, name).ifPresent(singlePage -> { final SinglePage oldPage = JsonUtils.deepCopy(singlePage); - permalinkOnDelete(oldPage); singlePage.getStatusOrDefault() .setPermalink(createPermalink(singlePage)); - permalinkOnAdd(singlePage); SinglePage.SinglePageSpec spec = singlePage.getSpec(); SinglePage.SinglePageStatus status = singlePage.getStatusOrDefault(); diff --git a/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java index 4a17cc2b9..6b4733768 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java @@ -2,13 +2,17 @@ package run.halo.app.core.extension.reconciler; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; +import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import run.halo.app.content.permalinks.TagPermalinkPolicy; +import run.halo.app.core.extension.content.Constant; import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Tag; import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.Reconciler; @@ -41,6 +45,10 @@ public class TagReconciler implements Reconciler { } addFinalizerIfNecessary(tag); + reconcileMetadata(request.name()); + + this.reconcileStatusPermalink(request.name()); + reconcileStatusPosts(request.name()); }); return new Result(false, null); @@ -54,9 +62,18 @@ public class TagReconciler implements Reconciler { .build(); } - private void cleanUpResources(Tag tag) { - // remove permalink from permalink indexer - tagPermalinkPolicy.onPermalinkDelete(tag); + void reconcileMetadata(String name) { + client.fetch(Tag.class, name).ifPresent(tag -> { + Map annotations = ExtensionUtil.nullSafeAnnotations(tag); + String oldPermalinkPattern = annotations.get(Constant.PERMALINK_PATTERN_ANNO); + + String newPattern = tagPermalinkPolicy.pattern(); + annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern); + + if (!StringUtils.equals(oldPermalinkPattern, newPattern)) { + client.update(tag); + } + }); } private void addFinalizerIfNecessary(Tag oldTag) { @@ -78,7 +95,6 @@ public class TagReconciler implements Reconciler { private void cleanUpResourcesAndRemoveFinalizer(String tagName) { client.fetch(Tag.class, tagName).ifPresent(tag -> { - cleanUpResources(tag); if (tag.getMetadata().getFinalizers() != null) { tag.getMetadata().getFinalizers().remove(FINALIZER_NAME); } @@ -86,6 +102,19 @@ public class TagReconciler implements Reconciler { }); } + private void reconcileStatusPermalink(String tagName) { + client.fetch(Tag.class, tagName) + .ifPresent(tag -> { + String oldPermalink = tag.getStatusOrDefault().getPermalink(); + String permalink = tagPermalinkPolicy.permalink(tag); + tag.getStatusOrDefault().setPermalink(permalink); + + if (!StringUtils.equals(permalink, oldPermalink)) { + client.update(tag); + } + }); + } + private void reconcileStatusPosts(String tagName) { client.fetch(Tag.class, tagName).ifPresent(tag -> { Tag oldTag = JsonUtils.deepCopy(tag); diff --git a/src/main/java/run/halo/app/core/extension/reconciler/TagRouteReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/TagRouteReconciler.java deleted file mode 100644 index 5996af2b5..000000000 --- a/src/main/java/run/halo/app/core/extension/reconciler/TagRouteReconciler.java +++ /dev/null @@ -1,56 +0,0 @@ -package run.halo.app.core.extension.reconciler; - -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; -import run.halo.app.content.permalinks.TagPermalinkPolicy; -import run.halo.app.core.extension.content.Tag; -import run.halo.app.extension.ExtensionClient; -import run.halo.app.extension.controller.Controller; -import run.halo.app.extension.controller.ControllerBuilder; -import run.halo.app.extension.controller.Reconciler; - -@Component -@RequiredArgsConstructor -public class TagRouteReconciler implements Reconciler { - private final ExtensionClient client; - private final TagPermalinkPolicy tagPermalinkPolicy; - - @Override - public Result reconcile(Request request) { - client.fetch(Tag.class, request.name()) - .ifPresent(tag -> { - if (tag.getMetadata().getDeletionTimestamp() != null) { - // TagReconciler already did it, so there is no need to remove permalink - return; - } - - reconcilePermalinkRoute(request.name()); - }); - return new Result(false, null); - } - - private void reconcilePermalinkRoute(String tagName) { - client.fetch(Tag.class, tagName) - .ifPresent(tag -> { - final String oldPermalink = tag.getStatusOrDefault().getPermalink(); - - tagPermalinkPolicy.onPermalinkDelete(tag); - - String permalink = tagPermalinkPolicy.permalink(tag); - tag.getStatusOrDefault().setPermalink(permalink); - tagPermalinkPolicy.onPermalinkAdd(tag); - - if (!StringUtils.equals(permalink, oldPermalink)) { - client.update(tag); - } - }); - } - - @Override - public Controller setupWith(ControllerBuilder builder) { - return builder - .extension(new Tag()) - .build(); - } -} diff --git a/src/main/java/run/halo/app/core/extension/reconciler/UserReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/UserReconciler.java index bdcbf0a74..f54ad2ae8 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/UserReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/UserReconciler.java @@ -7,10 +7,8 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; -import run.halo.app.content.permalinks.ExtensionLocator; import run.halo.app.core.extension.User; import run.halo.app.extension.ExtensionClient; -import run.halo.app.extension.GroupVersionKind; import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.Reconciler; @@ -18,8 +16,6 @@ import run.halo.app.extension.controller.Reconciler.Request; import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.infra.utils.PathUtils; -import run.halo.app.theme.router.PermalinkIndexDeleteCommand; -import run.halo.app.theme.router.PermalinkIndexUpdateCommand; @Slf4j @Component @@ -58,21 +54,12 @@ public class UserReconciler implements Reconciler { status.setPermalink(getUserPermalink(user)); - ExtensionLocator extensionLocator = getExtensionLocator(name); - eventPublisher.publishEvent( - new PermalinkIndexUpdateCommand(this, extensionLocator, status.getPermalink())); - if (!StringUtils.equals(oldPermalink, status.getPermalink())) { client.update(user); } }); } - private static ExtensionLocator getExtensionLocator(String name) { - return new ExtensionLocator(GroupVersionKind.fromExtension(User.class), name, - name); - } - private String getUserPermalink(User user) { return externalUrlSupplier.get() .resolve(PathUtils.combinePath("authors", user.getMetadata().getName())) @@ -98,9 +85,6 @@ public class UserReconciler implements Reconciler { private void cleanUpResourcesAndRemoveFinalizer(String userName) { client.fetch(User.class, userName).ifPresent(user -> { - eventPublisher.publishEvent( - new PermalinkIndexDeleteCommand(this, getExtensionLocator(userName))); - if (user.getMetadata().getFinalizers() != null) { user.getMetadata().getFinalizers().remove(FINALIZER_NAME); } diff --git a/src/main/java/run/halo/app/infra/SystemConfigurableEnvironmentFetcher.java b/src/main/java/run/halo/app/infra/SystemConfigurableEnvironmentFetcher.java index 21afdd1ab..2f3f87ea9 100644 --- a/src/main/java/run/halo/app/infra/SystemConfigurableEnvironmentFetcher.java +++ b/src/main/java/run/halo/app/infra/SystemConfigurableEnvironmentFetcher.java @@ -59,6 +59,10 @@ public class SystemConfigurableEnvironmentFetcher { .switchIfEmpty(Mono.just(new SystemSetting.Post())); } + public Mono fetchRouteRules() { + return fetch(SystemSetting.ThemeRouteRules.GROUP, SystemSetting.ThemeRouteRules.class); + } + @NonNull private Mono> getValuesInternal() { return getConfigMap() diff --git a/src/main/java/run/halo/app/infra/SystemSetting.java b/src/main/java/run/halo/app/infra/SystemSetting.java index d2e186dd2..a8f258053 100644 --- a/src/main/java/run/halo/app/infra/SystemSetting.java +++ b/src/main/java/run/halo/app/infra/SystemSetting.java @@ -32,6 +32,15 @@ public class SystemSetting { private String archives; private String post; private String tags; + + public static ThemeRouteRules empty() { + ThemeRouteRules rules = new ThemeRouteRules(); + rules.setPost("/archives/{slug}"); + rules.setArchives("/archives"); + rules.setTags("/tags"); + rules.setCategories("/categories"); + return rules; + } } @Data diff --git a/src/main/java/run/halo/app/infra/exception/NotFoundException.java b/src/main/java/run/halo/app/infra/exception/NotFoundException.java index b7ff8c5eb..f15716617 100644 --- a/src/main/java/run/halo/app/infra/exception/NotFoundException.java +++ b/src/main/java/run/halo/app/infra/exception/NotFoundException.java @@ -1,21 +1,27 @@ package run.halo.app.infra.exception; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import org.springframework.web.server.ResponseStatusException; + /** * Not found exception. * * @author guqing * @since 2.0.0 */ -public class NotFoundException extends RuntimeException { - public NotFoundException(String message) { - super(message); +public class NotFoundException extends ResponseStatusException { + + public NotFoundException(@Nullable String reason) { + this(reason, null); } - public NotFoundException(String message, Throwable cause) { - super(message, cause); + public NotFoundException(@Nullable String reason, + @Nullable Throwable cause) { + super(HttpStatus.NOT_FOUND, reason, cause); } - public NotFoundException(Throwable cause) { - super(cause); + public NotFoundException(@Nullable Throwable cause) { + this(cause == null ? "" : cause.getMessage(), cause); } } diff --git a/src/main/java/run/halo/app/infra/utils/HaloUtils.java b/src/main/java/run/halo/app/infra/utils/HaloUtils.java index caaefbe67..08d9e2cc0 100644 --- a/src/main/java/run/halo/app/infra/utils/HaloUtils.java +++ b/src/main/java/run/halo/app/infra/utils/HaloUtils.java @@ -54,6 +54,12 @@ public class HaloUtils { return StringUtils.defaultString(userAgent, "unknown"); } + public static String getDayText(Instant instant) { + Assert.notNull(instant, "Instant must not be null"); + int dayValue = instant.atZone(ZoneId.systemDefault()).getDayOfMonth(); + return StringUtils.leftPad(String.valueOf(dayValue), 2, '0'); + } + public static String getMonthText(Instant instant) { Assert.notNull(instant, "Instant must not be null"); int monthValue = instant.atZone(ZoneId.systemDefault()).getMonthValue(); diff --git a/src/main/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessor.java b/src/main/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessor.java index 294a80f42..02af63841 100644 --- a/src/main/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessor.java +++ b/src/main/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessor.java @@ -13,7 +13,7 @@ import reactor.core.publisher.Mono; import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.SinglePageFinder; -import run.halo.app.theme.router.strategy.ModelConst; +import run.halo.app.theme.router.factories.ModelConst; /** *

The head html snippet injection processor for content template such as post diff --git a/src/main/java/run/halo/app/theme/dialect/TemplateGlobalHeadProcessor.java b/src/main/java/run/halo/app/theme/dialect/TemplateGlobalHeadProcessor.java index 3162ce43a..39bb457f8 100644 --- a/src/main/java/run/halo/app/theme/dialect/TemplateGlobalHeadProcessor.java +++ b/src/main/java/run/halo/app/theme/dialect/TemplateGlobalHeadProcessor.java @@ -10,7 +10,7 @@ import reactor.core.publisher.Mono; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.router.strategy.ModelConst; +import run.halo.app.theme.router.factories.ModelConst; /** *

Global custom head snippet injection for theme global setting.

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 99c596a56..f2dc17b43 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 @@ -20,7 +20,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.comparator.Comparators; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.util.function.Tuple2; import run.halo.app.content.PostService; import run.halo.app.core.extension.content.Post; import run.halo.app.extension.ListResult; @@ -135,7 +134,6 @@ public class PostFinderImpl implements PostFinder { } List elements = window.elements(); - Tuple2 previousNext; // current post index int index = elements.indexOf(currentName); diff --git a/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java b/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java index 1a8724017..3433936bb 100644 --- a/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java +++ b/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java @@ -51,6 +51,7 @@ public class SinglePageFinderImpl implements SinglePageFinder { @Override public Mono getByName(String pageName) { return client.fetch(SinglePage.class, pageName) + .filter(FIXED_PREDICATE) .map(page -> { SinglePageVo pageVo = SinglePageVo.from(page); pageVo.setContributors(List.of()); diff --git a/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java b/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java new file mode 100644 index 000000000..06ba27743 --- /dev/null +++ b/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java @@ -0,0 +1,82 @@ +package run.halo.app.theme.router; + +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationListener; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import run.halo.app.core.extension.content.Category; +import run.halo.app.core.extension.content.Constant; +import run.halo.app.core.extension.content.Post; +import run.halo.app.core.extension.content.Tag; +import run.halo.app.extension.AbstractExtension; +import run.halo.app.extension.Extension; +import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ExtensionUtil; +import run.halo.app.extension.MetadataOperator; +import run.halo.app.theme.DefaultTemplateEnum; + +/** + * {@link ExtensionPermalinkPatternUpdater} to update the value of key + * {@link Constant#PERMALINK_PATTERN_ANNO} in {@link MetadataOperator#getAnnotations()} + * of {@link Extension} when the pattern changed. + * + * @author guqing + * @see Post + * @see Category + * @see Tag + * @since 2.0.0 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ExtensionPermalinkPatternUpdater + implements ApplicationListener { + private final ExtensionClient client; + + @Override + public void onApplicationEvent(@NonNull PermalinkRuleChangedEvent event) { + DefaultTemplateEnum template = event.getTemplate(); + log.debug("Refresh permalink for template [{}]", template.getValue()); + String pattern = event.getRule(); + switch (template) { + case POST -> updatePostPermalink(pattern); + case CATEGORY -> updateCategoryPermalink(pattern); + case TAG -> updateTagPermalink(pattern); + default -> { + } + } + } + + private void updatePostPermalink(String pattern) { + log.debug("Update post permalink by new policy [{}]", pattern); + client.list(Post.class, null, null) + .forEach(post -> updateIfPermalinkPatternChanged(post, pattern)); + } + + private void updateIfPermalinkPatternChanged(AbstractExtension extension, String pattern) { + Map annotations = ExtensionUtil.nullSafeAnnotations(extension); + String oldPattern = annotations.get(Constant.PERMALINK_PATTERN_ANNO); + annotations.put(Constant.PERMALINK_PATTERN_ANNO, pattern); + + if (StringUtils.equals(oldPattern, pattern) && StringUtils.isNotBlank(oldPattern)) { + return; + } + // update permalink pattern annotation + client.update(extension); + } + + private void updateCategoryPermalink(String pattern) { + log.debug("Update category and categories permalink by new policy [{}]", pattern); + client.list(Category.class, null, null) + .forEach(category -> updateIfPermalinkPatternChanged(category, pattern)); + } + + private void updateTagPermalink(String pattern) { + log.debug("Update tag and tags permalink by new policy [{}]", pattern); + client.list(Tag.class, null, null) + .forEach(tag -> updateIfPermalinkPatternChanged(tag, pattern)); + } +} diff --git a/src/main/java/run/halo/app/theme/router/GvkName.java b/src/main/java/run/halo/app/theme/router/GvkName.java deleted file mode 100644 index b67973c7d..000000000 --- a/src/main/java/run/halo/app/theme/router/GvkName.java +++ /dev/null @@ -1,12 +0,0 @@ -package run.halo.app.theme.router; - -import run.halo.app.extension.GroupVersionKind; - -/** - * A record class for holding gvk and name. - * - * @author guqing - * @since 2.0.0 - */ -public record GvkName(GroupVersionKind gvk, String name) { -} diff --git a/src/main/java/run/halo/app/theme/router/PermalinkHttpGetRouter.java b/src/main/java/run/halo/app/theme/router/PermalinkHttpGetRouter.java deleted file mode 100644 index e56d71b69..000000000 --- a/src/main/java/run/halo/app/theme/router/PermalinkHttpGetRouter.java +++ /dev/null @@ -1,212 +0,0 @@ -package run.halo.app.theme.router; - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Map; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Consumer; -import lombok.AllArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.ApplicationContext; -import org.springframework.context.event.EventListener; -import org.springframework.lang.Nullable; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; -import org.springframework.web.util.UriUtils; -import run.halo.app.core.extension.reconciler.SystemSettingReconciler; -import run.halo.app.extension.GroupVersionKind; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.infra.ExternalUrlSupplier; -import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; -import run.halo.app.infra.SystemSetting; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.router.strategy.DetailsPageRouteHandlerStrategy; -import run.halo.app.theme.router.strategy.IndexRouteStrategy; -import run.halo.app.theme.router.strategy.ListPageRouteHandlerStrategy; - -/** - * Permalink router for http get method. - * - * @author guqing - * @since 2.0.0 - */ -@Component -@AllArgsConstructor -public class PermalinkHttpGetRouter implements InitializingBean { - private final ReentrantLock lock = new ReentrantLock(); - private final RadixRouterTree routeTree = new RadixRouterTree(); - private final SystemConfigurableEnvironmentFetcher environmentFetcher; - private final ReactiveExtensionClient client; - private final ApplicationContext applicationContext; - private final ExternalUrlSupplier externalUrlSupplier; - - /** - * Match permalink according to {@link ServerRequest}. - * - * @param request http request - * @return a handler function if matched, otherwise null - */ - public HandlerFunction route(ServerRequest request) { - return routeTree.match(request); - } - - public void insert(String key, HandlerFunction handlerFunction) { - routeTree.insert(key, handlerFunction); - } - - /** - * Watch permalink changed event to refresh route tree. - * - * @param event permalink changed event - */ - @EventListener(PermalinkIndexChangedEvent.class) - public void onPermalinkChanged(PermalinkIndexChangedEvent event) { - String oldPath = getPath(event.getOldPermalink()); - String path = getPath(event.getPermalink()); - GvkName gvkName = event.getGvkName(); - lock.lock(); - try { - if (oldPath == null && path != null) { - onPermalinkAdded(gvkName, path); - return; - } - - if (oldPath != null) { - if (path == null) { - onPermalinkDeleted(oldPath); - } else { - onPermalinkUpdated(gvkName, oldPath, path); - } - } - } finally { - lock.unlock(); - } - } - - /** - * Watch permalink rule changed event to refresh route tree for list style templates. - * - * @param event permalink changed event - */ - @EventListener(PermalinkRuleChangedEvent.class) - public void onPermalinkRuleChanged(PermalinkRuleChangedEvent event) { - final String rule = event.getRule(); - final String oldRule = event.getOldRule(); - lock.lock(); - try { - if (StringUtils.isNotBlank(oldRule)) { - routeTree.delete(oldRule); - } - registerByTemplate(event.getTemplate(), rule); - } finally { - lock.unlock(); - } - } - - /** - * delete theme route old rules to trigger register. - */ - @EventListener(ApplicationReadyEvent.class) - public void onApplicationReady() { - environmentFetcher.getConfigMap().flatMap(configMap -> { - Map annotations = configMap.getMetadata().getAnnotations(); - if (annotations != null) { - annotations.remove(SystemSettingReconciler.OLD_THEME_ROUTE_RULES); - } - return client.update(configMap); - }).block(); - } - - private void registerByTemplate(DefaultTemplateEnum template, String rule) { - ListPageRouteHandlerStrategy routeStrategy = getRouteStrategy(template); - if (routeStrategy == null) { - return; - } - List routerPaths = routeStrategy.getRouterPaths(rule); - routeTreeBatchOperation(routerPaths, routeTree::delete); - if (StringUtils.isNotBlank(rule)) { - routeTreeBatchOperation(routeStrategy.getRouterPaths(rule), - path -> routeTree.insert(path, routeStrategy.getHandler())); - } - } - - void init() { - // Index route need to be added first - IndexRouteStrategy indexRouteStrategy = - applicationContext.getBean(IndexRouteStrategy.class); - List routerPaths = indexRouteStrategy.getRouterPaths("/"); - routeTreeBatchOperation(routerPaths, - path -> routeTree.insert(path, indexRouteStrategy.getHandler())); - } - - private void routeTreeBatchOperation(List paths, - Consumer templateFunction) { - if (paths == null) { - return; - } - paths.forEach(templateFunction); - } - - private void onPermalinkAdded(GvkName gvkName, String path) { - routeTree.insert(path, getRouteHandler(gvkName)); - } - - private void onPermalinkUpdated(GvkName gvkName, String oldPath, String path) { - routeTree.delete(oldPath); - routeTree.insert(path, getRouteHandler(gvkName)); - - } - - private void onPermalinkDeleted(String path) { - routeTree.delete(path); - } - - private String getPath(@Nullable String permalink) { - if (permalink == null) { - return null; - } - String decode = UriUtils.decode(permalink, StandardCharsets.UTF_8); - URI externalUrl = externalUrlSupplier.get(); - if (externalUrl != null) { - String externalAsciiUrl = externalUrl.toASCIIString(); - return StringUtils.prependIfMissing( - StringUtils.removeStart(decode, externalAsciiUrl), "/"); - } - return decode; - } - - private HandlerFunction getRouteHandler(GvkName gvkName) { - GroupVersionKind gvk = gvkName.gvk(); - return applicationContext.getBeansOfType(DetailsPageRouteHandlerStrategy.class) - .values() - .stream() - .filter(strategy -> strategy.supports(gvk)) - .findFirst() - .map(strategy -> strategy.getHandler(getThemeRouteRules(), gvkName.name())) - .orElse(null); - } - - private ListPageRouteHandlerStrategy getRouteStrategy(DefaultTemplateEnum template) { - return applicationContext.getBeansOfType(ListPageRouteHandlerStrategy.class) - .values() - .stream() - .filter(strategy -> strategy.supports(template)) - .findFirst() - .orElse(null); - } - - public SystemSetting.ThemeRouteRules getThemeRouteRules() { - return environmentFetcher.fetch(SystemSetting.ThemeRouteRules.GROUP, - SystemSetting.ThemeRouteRules.class).block(); - } - - @Override - public void afterPropertiesSet() throws Exception { - init(); - } -} diff --git a/src/main/java/run/halo/app/theme/router/PermalinkIndexAddCommand.java b/src/main/java/run/halo/app/theme/router/PermalinkIndexAddCommand.java deleted file mode 100644 index 3ee8a2ea1..000000000 --- a/src/main/java/run/halo/app/theme/router/PermalinkIndexAddCommand.java +++ /dev/null @@ -1,30 +0,0 @@ -package run.halo.app.theme.router; - -import org.springframework.context.ApplicationEvent; -import run.halo.app.content.permalinks.ExtensionLocator; - -/** - *

Send a command to add a piece of data from {@link PermalinkIndexer}.

- * - * @author guqing - * @see PermalinkIndexer - * @since 2.0.0 - */ -public class PermalinkIndexAddCommand extends ApplicationEvent { - private final ExtensionLocator locator; - private final String permalink; - - public PermalinkIndexAddCommand(Object source, ExtensionLocator locator, String permalink) { - super(source); - this.locator = locator; - this.permalink = permalink; - } - - public ExtensionLocator getLocator() { - return locator; - } - - public String getPermalink() { - return permalink; - } -} diff --git a/src/main/java/run/halo/app/theme/router/PermalinkIndexChangedEvent.java b/src/main/java/run/halo/app/theme/router/PermalinkIndexChangedEvent.java deleted file mode 100644 index e99ecbf06..000000000 --- a/src/main/java/run/halo/app/theme/router/PermalinkIndexChangedEvent.java +++ /dev/null @@ -1,28 +0,0 @@ -package run.halo.app.theme.router; - -import lombok.Getter; -import org.springframework.context.ApplicationEvent; -import org.springframework.util.Assert; - -/** - * Permalink index changed event. - * - * @author guqing - * @since 2.0.0 - */ -@Getter -public class PermalinkIndexChangedEvent extends ApplicationEvent { - private final String oldPermalink; - private final String permalink; - - private final GvkName gvkName; - - public PermalinkIndexChangedEvent(Object source, - GvkName gvkName, String oldPermalink, String permalink) { - super(source); - Assert.notNull(gvkName, "The gvkName must not be null."); - this.oldPermalink = oldPermalink; - this.permalink = permalink; - this.gvkName = gvkName; - } -} diff --git a/src/main/java/run/halo/app/theme/router/PermalinkIndexDeleteCommand.java b/src/main/java/run/halo/app/theme/router/PermalinkIndexDeleteCommand.java deleted file mode 100644 index d5bd880f3..000000000 --- a/src/main/java/run/halo/app/theme/router/PermalinkIndexDeleteCommand.java +++ /dev/null @@ -1,24 +0,0 @@ -package run.halo.app.theme.router; - -import org.springframework.context.ApplicationEvent; -import run.halo.app.content.permalinks.ExtensionLocator; - -/** - *

Send a command to delete a piece of data from {@link PermalinkIndexer}.

- * - * @author guqing - * @see PermalinkIndexer - * @since 2.0.0 - */ -public class PermalinkIndexDeleteCommand extends ApplicationEvent { - private final ExtensionLocator locator; - - public PermalinkIndexDeleteCommand(Object source, ExtensionLocator locator) { - super(source); - this.locator = locator; - } - - public ExtensionLocator getLocator() { - return locator; - } -} diff --git a/src/main/java/run/halo/app/theme/router/PermalinkIndexUpdateCommand.java b/src/main/java/run/halo/app/theme/router/PermalinkIndexUpdateCommand.java deleted file mode 100644 index 3ba417355..000000000 --- a/src/main/java/run/halo/app/theme/router/PermalinkIndexUpdateCommand.java +++ /dev/null @@ -1,30 +0,0 @@ -package run.halo.app.theme.router; - -import org.springframework.context.ApplicationEvent; -import run.halo.app.content.permalinks.ExtensionLocator; - -/** - *

Send a command to update a piece of data from {@link PermalinkIndexer}.

- * - * @author guqing - * @see PermalinkIndexer - * @since 2.0.0 - */ -public class PermalinkIndexUpdateCommand extends ApplicationEvent { - private final ExtensionLocator locator; - private final String permalink; - - public PermalinkIndexUpdateCommand(Object source, ExtensionLocator locator, String permalink) { - super(source); - this.locator = locator; - this.permalink = permalink; - } - - public ExtensionLocator getLocator() { - return locator; - } - - public String getPermalink() { - return permalink; - } -} diff --git a/src/main/java/run/halo/app/theme/router/PermalinkIndexer.java b/src/main/java/run/halo/app/theme/router/PermalinkIndexer.java deleted file mode 100644 index 4657a6fee..000000000 --- a/src/main/java/run/halo/app/theme/router/PermalinkIndexer.java +++ /dev/null @@ -1,251 +0,0 @@ -package run.halo.app.theme.router; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.ApplicationContext; -import org.springframework.context.event.EventListener; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; -import org.springframework.stereotype.Component; -import run.halo.app.content.permalinks.ExtensionLocator; -import run.halo.app.extension.GroupVersionKind; - -/** - *

Permalink indexer for lookup extension's name and slug by permalink.

- * - * @author guqing - * @since 2.0.0 - */ -@Slf4j -@Component -public class PermalinkIndexer { - private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - - private final Map gvkNamePermalinkLookup = new HashMap<>(); - private final Map permalinkLocatorLookup = new HashMap<>(); - - private final ApplicationContext applicationContext; - - public PermalinkIndexer(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - - /** - * Register extension and permalink mapping. - * - * @param locator extension locator to hold the gvk and name and slug - * @param permalink extension permalink for template route - */ - public void register(ExtensionLocator locator, String permalink) { - readWriteLock.writeLock().lock(); - try { - GvkName gvkName = new GvkName(locator.gvk(), locator.name()); - gvkNamePermalinkLookup.put(gvkName, permalink); - permalinkLocatorLookup.put(permalink, locator); - publishEvent(gvkName, null, permalink); - } finally { - readWriteLock.writeLock().unlock(); - } - } - - private void publishEvent(GvkName gvkName, String oldPermalink, String newPermalink) { - applicationContext.publishEvent( - new PermalinkIndexChangedEvent(this, gvkName, oldPermalink, newPermalink)); - } - - /** - * Remove extension and permalink mapping. - * - * @param locator extension info - */ - public void remove(ExtensionLocator locator) { - readWriteLock.writeLock().lock(); - try { - GvkName gvkName = new GvkName(locator.gvk(), locator.name()); - String permalink = gvkNamePermalinkLookup.remove(gvkName); - if (permalink != null) { - permalinkLocatorLookup.remove(permalink); - publishEvent(gvkName, permalink, null); - } - } finally { - readWriteLock.writeLock().unlock(); - } - } - - /** - * Gets permalink by {@link GroupVersionKind}. - * - * @param gvk group version kind - * @return permalinks - */ - @NonNull - public List getPermalinks(GroupVersionKind gvk) { - readWriteLock.readLock().lock(); - try { - return gvkNamePermalinkLookup.entrySet() - .stream() - .filter(entry -> entry.getKey().gvk().equals(gvk)) - .map(Map.Entry::getValue) - .toList(); - } finally { - readWriteLock.readLock().unlock(); - } - } - - /** - * Lookup extension info by permalink. - * - * @param permalink extension permalink for theme template route - * @return extension locator - */ - @Nullable - public ExtensionLocator lookup(String permalink) { - readWriteLock.readLock().lock(); - try { - return permalinkLocatorLookup.get(permalink); - } finally { - readWriteLock.readLock().unlock(); - } - } - - /** - * Lookup extension permalink by {@link GroupVersionKind} and {@code name}. - * - * @param gvk group version kind - * @param name extension name - * @return {@code true} if contains, otherwise {@code false} - */ - public boolean containsName(GroupVersionKind gvk, String name) { - readWriteLock.readLock().lock(); - try { - return gvkNamePermalinkLookup.containsKey(new GvkName(gvk, name)); - } finally { - readWriteLock.readLock().unlock(); - } - } - - /** - * Lookup extension permalink by {@link GroupVersionKind} and {@code slug}. - * - * @param gvk group version kind - * @param slug extension slug - * @return {@code true} if contains, otherwise {@code false} - */ - public boolean containsSlug(GroupVersionKind gvk, String slug) { - readWriteLock.readLock().lock(); - try { - return permalinkLocatorLookup.values() - .stream() - .anyMatch(locator -> locator.gvk().equals(gvk) - && locator.slug().equals(slug)); - } finally { - readWriteLock.readLock().unlock(); - } - } - - /** - * Lookup extension name by resource slug. - * - * @param gvk extension's {@link GroupVersionKind} - * @param slug extension resource slug - * @return extension resource name specified by resource slug - */ - public String getNameBySlug(GroupVersionKind gvk, String slug) { - readWriteLock.readLock().lock(); - try { - return permalinkLocatorLookup.values() - .stream() - .filter(locator -> locator.gvk().equals(gvk) - && locator.slug().equals(slug)) - .findFirst() - .map(ExtensionLocator::name) - .orElseThrow(); - } finally { - readWriteLock.readLock().unlock(); - } - } - - /** - * Get extension name by permalink. - * - * @param gvk is GroupVersionKind of extension - * @param permalink is encoded permalink - * @return extension name or null - */ - @Nullable - public String getNameByPermalink(GroupVersionKind gvk, String permalink) { - readWriteLock.readLock().lock(); - try { - var locator = permalinkLocatorLookup.get(permalink); - return locator == null ? null : locator.name(); - } finally { - readWriteLock.readLock().unlock(); - } - } - - /** - * Only for test. - * - * @return permalinkLookup map size - */ - protected long gvkNamePermalinkMapSize() { - return gvkNamePermalinkLookup.size(); - } - - /** - * Only for test. - * - * @return permalinkLocatorMap map size - */ - protected long permalinkLocatorMapSize() { - return permalinkLocatorLookup.size(); - } - - /** - * Add a record to the {@link PermalinkIndexer}. - * If permalink already exists, it will not be added to indexer - * - * @param addCommand a command to add a record to {@link PermalinkIndexer} - */ - @EventListener(PermalinkIndexAddCommand.class) - public void onPermalinkAdd(PermalinkIndexAddCommand addCommand) { - if (checkPermalinkExists(addCommand.getLocator(), addCommand.getPermalink())) { - // TODO send an extension event to log this error - log.error("Permalink [{}] already exists, you can try to change the slug [{}].", - addCommand.getPermalink(), addCommand.getLocator()); - return; - } - register(addCommand.getLocator(), addCommand.getPermalink()); - } - - @EventListener(PermalinkIndexDeleteCommand.class) - public void onPermalinkDelete(PermalinkIndexDeleteCommand deleteCommand) { - remove(deleteCommand.getLocator()); - } - - /** - * Update a {@link PermalinkIndexer} record by {@link ExtensionLocator} and permalink. - * If permalink already exists, it will not be updated - * - * @param updateCommand a command to update an indexer record - */ - @EventListener(PermalinkIndexUpdateCommand.class) - public void onPermalinkUpdate(PermalinkIndexUpdateCommand updateCommand) { - if (checkPermalinkExists(updateCommand.getLocator(), updateCommand.getPermalink())) { - // TODO send an extension event to log this error - log.error("Permalink [{}] already exists, you can try to change the slug [{}].", - updateCommand.getPermalink(), updateCommand.getLocator()); - return; - } - remove(updateCommand.getLocator()); - register(updateCommand.getLocator(), updateCommand.getPermalink()); - } - - private boolean checkPermalinkExists(ExtensionLocator locator, String permalink) { - ExtensionLocator lookup = lookup(permalink); - return lookup != null && !lookup.equals(locator); - } -} diff --git a/src/main/java/run/halo/app/theme/router/PermalinkPatternProvider.java b/src/main/java/run/halo/app/theme/router/PermalinkPatternProvider.java deleted file mode 100644 index 4e731915e..000000000 --- a/src/main/java/run/halo/app/theme/router/PermalinkPatternProvider.java +++ /dev/null @@ -1,60 +0,0 @@ -package run.halo.app.theme.router; - -import java.util.Map; -import org.springframework.stereotype.Component; -import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; -import run.halo.app.infra.SystemSetting; -import run.halo.app.infra.utils.JsonUtils; -import run.halo.app.theme.DefaultTemplateEnum; - -/** - *

The {@link PermalinkPatternProvider} used to obtain permalink rules according to specific - * template names.

- * - * @author guqing - * @since 2.0.0 - */ -@Component -public class PermalinkPatternProvider { - - private final SystemConfigurableEnvironmentFetcher environmentFetcher; - - public PermalinkPatternProvider(SystemConfigurableEnvironmentFetcher environmentFetcher) { - this.environmentFetcher = environmentFetcher; - } - - private SystemSetting.ThemeRouteRules getPermalinkRules() { - return environmentFetcher.getConfigMapBlocking() - .map(configMap -> { - Map data = configMap.getData(); - return data.get(SystemSetting.ThemeRouteRules.GROUP); - }) - .map(routeRulesJson -> JsonUtils.jsonToObject(routeRulesJson, - SystemSetting.ThemeRouteRules.class)) - .orElseGet(() -> { - SystemSetting.ThemeRouteRules themeRouteRules = new SystemSetting.ThemeRouteRules(); - themeRouteRules.setArchives("archives"); - themeRouteRules.setPost("/archives/{slug}"); - themeRouteRules.setTags("tags"); - themeRouteRules.setCategories("categories"); - return themeRouteRules; - }); - } - - /** - * Get permalink pattern by template name. - * - * @param defaultTemplateEnum default templates - * @return a pattern specified by the template name - */ - public String getPattern(DefaultTemplateEnum defaultTemplateEnum) { - SystemSetting.ThemeRouteRules permalinkRules = getPermalinkRules(); - return switch (defaultTemplateEnum) { - case INDEX, SINGLE_PAGE, AUTHOR -> null; - case POST -> permalinkRules.getPost(); - case ARCHIVES -> permalinkRules.getArchives(); - case CATEGORY, CATEGORIES -> permalinkRules.getCategories(); - case TAG, TAGS -> permalinkRules.getTags(); - }; - } -} diff --git a/src/main/java/run/halo/app/theme/router/PermalinkRefreshHandler.java b/src/main/java/run/halo/app/theme/router/PermalinkRefreshHandler.java deleted file mode 100644 index 141d4dfad..000000000 --- a/src/main/java/run/halo/app/theme/router/PermalinkRefreshHandler.java +++ /dev/null @@ -1,107 +0,0 @@ -package run.halo.app.theme.router; - -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.context.ApplicationListener; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; -import run.halo.app.content.permalinks.CategoryPermalinkPolicy; -import run.halo.app.content.permalinks.PostPermalinkPolicy; -import run.halo.app.content.permalinks.TagPermalinkPolicy; -import run.halo.app.core.extension.content.Category; -import run.halo.app.core.extension.content.Post; -import run.halo.app.core.extension.content.Tag; -import run.halo.app.extension.ExtensionClient; -import run.halo.app.theme.DefaultTemplateEnum; - -/** - * Permalink refresh handler. - * - * @author guqing - * @since 2.0.0 - */ -@Slf4j -@Component -public class PermalinkRefreshHandler implements ApplicationListener { - private final ExtensionClient client; - private final PostPermalinkPolicy postPermalinkPolicy; - private final TagPermalinkPolicy tagPermalinkPolicy; - private final CategoryPermalinkPolicy categoryPermalinkPolicy; - - public PermalinkRefreshHandler(ExtensionClient client, - PostPermalinkPolicy postPermalinkPolicy, - TagPermalinkPolicy tagPermalinkPolicy, - CategoryPermalinkPolicy categoryPermalinkPolicy) { - this.client = client; - this.postPermalinkPolicy = postPermalinkPolicy; - this.tagPermalinkPolicy = tagPermalinkPolicy; - this.categoryPermalinkPolicy = categoryPermalinkPolicy; - } - - @Override - public void onApplicationEvent(@NonNull PermalinkRuleChangedEvent event) { - DefaultTemplateEnum template = event.getTemplate(); - log.debug("Refresh permalink for template [{}]", template.getValue()); - switch (template) { - case POST -> updatePostPermalink(); - case CATEGORIES, CATEGORY -> updateCategoryPermalink(); - case TAGS, TAG -> updateTagPermalink(); - default -> { - } - } - } - - private void updatePostPermalink() { - String pattern = postPermalinkPolicy.pattern(); - log.debug("Update post permalink by new policy [{}]", pattern); - client.list(Post.class, null, null) - .forEach(post -> { - String oldPermalink = post.getStatusOrDefault().getPermalink(); - String permalink = postPermalinkPolicy.permalink(post); - post.getStatusOrDefault().setPermalink(permalink); - if (StringUtils.equals(oldPermalink, permalink)) { - return; - } - // update permalink - client.update(post); - - postPermalinkPolicy.onPermalinkUpdate(post); - }); - } - - private void updateCategoryPermalink() { - String pattern = categoryPermalinkPolicy.pattern(); - log.debug("Update category and categories permalink by new policy [{}]", pattern); - client.list(Category.class, null, null) - .forEach(category -> { - String oldPermalink = category.getStatusOrDefault().getPermalink(); - String permalink = categoryPermalinkPolicy.permalink(category); - category.getStatusOrDefault().setPermalink(permalink); - if (StringUtils.equals(oldPermalink, permalink)) { - return; - } - // update permalink - client.update(category); - - categoryPermalinkPolicy.onPermalinkUpdate(category); - }); - } - - private void updateTagPermalink() { - String pattern = tagPermalinkPolicy.pattern(); - log.debug("Update tag and tags permalink by new policy [{}]", pattern); - client.list(Tag.class, null, null) - .forEach(tag -> { - String oldPermalink = tag.getStatusOrDefault().getPermalink(); - String permalink = tagPermalinkPolicy.permalink(tag); - tag.getStatusOrDefault().setPermalink(permalink); - if (StringUtils.equals(oldPermalink, permalink)) { - return; - } - // update permalink - client.update(tag); - - tagPermalinkPolicy.onPermalinkUpdate(tag); - }); - } -} diff --git a/src/main/java/run/halo/app/theme/router/RadixRouterTree.java b/src/main/java/run/halo/app/theme/router/RadixRouterTree.java deleted file mode 100644 index e6c181361..000000000 --- a/src/main/java/run/halo/app/theme/router/RadixRouterTree.java +++ /dev/null @@ -1,224 +0,0 @@ -package run.halo.app.theme.router; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.server.PathContainer; -import org.springframework.lang.Nullable; -import org.springframework.util.MultiValueMap; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.RouterFunctions; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; -import org.springframework.web.util.UriUtils; -import org.springframework.web.util.pattern.PathPattern; -import org.springframework.web.util.pattern.PathPatternParser; - -/** - * A router tree implementation based on radix tree. - * - * @author guqing - * @since 2.0.0 - */ -@Slf4j -public class RadixRouterTree extends RadixTree> { - - @Override - public void insert(String key, HandlerFunction value) - throws IllegalArgumentException { - super.insert(key, value); - if (log.isDebugEnabled()) { - checkIndices(); - } - } - - @Override - public boolean delete(String key) { - boolean result = super.delete(key); - if (log.isDebugEnabled()) { - checkIndices(); - } - return result; - } - - /** - *

Find the tree node according to the request path.

- * There are the following situations: - *
    - *
  • If found, return to the {@link HandlerFunction} directly.
  • - *
  • If the request path is not found in tree, it may be this request path corresponds - * to a pattern,so all the patterns will be iterated to match the current request path. If - * they match, the handler will be returned. - * Otherwise, null will be returned.
  • - *
- * for example, there is a tree as follows: - *
-     * / [indices=tca, priority=4]
-     * ├── tags/ [indices=h{, priority=2]
-     * │   ├── halo [value=tags-halo, priority=1]*
-     * │   └── {slug}/page/{page} [value=post by tags, priority=1]*
-     * ├── categories/default [value=categories-default, priority=1]*
-     * └── about [value=about, priority=1]*
-     * 
- *

1. find the request path "/categories/default" in tree, return the handler directly.

- *

2. but find the request path "/categories/default/page/1", it will be iterated to - * match

- * TODO Optimize matching algorithm to improve efficiency and try your best to get results - * through one search - * - * @param request server request - * @return a handler function if matched, otherwise null - */ - public HandlerFunction match(ServerRequest request) { - String path = pathToFind(request); - HandlerFunction result = find(path); - if (result != null) { - return result; - } - - PathContainer pathContainer = PathContainer.parsePath(path); - - List matches = new ArrayList<>(); - for (String pathPattern : getKeys()) { - if (!hasPatternSyntax(pathPattern)) { - continue; - } - log.trace("PathPatternParser handle pathPattern [{}]", pathPattern); - PathPattern parse = PathPatternParser.defaultInstance.parse(pathPattern); - if (parse.matches(pathContainer)) { - matches.add(parse); - } - } - - if (matches.isEmpty()) { - return null; - } - matches.sort(PathPattern.SPECIFICITY_COMPARATOR); - PathPattern bestMatch = matches.get(0); - if (matches.size() > 1) { - if (log.isTraceEnabled()) { - log.trace("request [GET {}] matching mappings: [{}]", path, matches); - } - PathPattern secondBestMatch = matches.get(1); - if (PathPattern.SPECIFICITY_COMPARATOR.compare(bestMatch, secondBestMatch) == 0) { - throw new IllegalStateException( - "Ambiguous mapping mapped for '" + path + "': {" + bestMatch + ", " - + secondBestMatch + "}"); - } - } - PathPattern.PathMatchInfo info = - bestMatch.matchAndExtract(request.requestPath().pathWithinApplication()); - if (info != null) { - mergeAttributes(request, info.getUriVariables(), bestMatch); - } - return find(bestMatch.getPatternString()); - } - - /** - * TODO Optimize parameter route matching query. - * Router 仅匹配请求方法和请求的 URL 路径, 形如 /?p=post-name 是 URL query,而不是 URL 路径的一部分。 - */ - static String pathToFind(ServerRequest request) { - String requestPath = processRequestPath(request.path()); - MultiValueMap queryParams = request.queryParams(); - // 文章的 permalink 规则需要对 p 参数规则特殊处理 - if (requestPath.equals("/") && queryParams.containsKey("p")) { - // post special route path - String postSlug = queryParams.getFirst("p"); - requestPath = requestPath + "?p=" + postSlug; - } - // /categories/{slug}/page/{page} 和 /tags/{slug}/page/{page} 需要去掉 page 部分 - if (PageUrlUtils.isPageUrl(requestPath)) { - int i = requestPath.lastIndexOf("/page/"); - if (i != -1) { - requestPath = requestPath.substring(0, i); - } - } - requestPath = StringUtils.removeEnd(requestPath, "/"); - return StringUtils.prependIfMissing(requestPath, "/"); - } - - private static void mergeAttributes(ServerRequest request, Map variables, - PathPattern pattern) { - Map pathVariables = mergePathVariables(request.pathVariables(), variables); - request.attributes().put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, - Collections.unmodifiableMap(pathVariables)); - - pattern = mergePatterns( - (PathPattern) request.attributes().get(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE), - pattern); - request.attributes().put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, pattern); - } - - private static PathPattern mergePatterns(@Nullable PathPattern oldPattern, - PathPattern newPattern) { - if (oldPattern != null) { - return oldPattern.combine(newPattern); - } else { - return newPattern; - } - } - - private static Map mergePathVariables(Map oldVariables, - Map newVariables) { - - if (!newVariables.isEmpty()) { - Map mergedVariables = new LinkedHashMap<>(oldVariables); - mergedVariables.putAll(newVariables); - return mergedVariables; - } else { - return oldVariables; - } - } - - private static String processRequestPath(String requestPath) { - String path = StringUtils.prependIfMissing(requestPath, "/"); - return UriUtils.decode(path, StandardCharsets.UTF_8); - } - - public boolean hasPatternSyntax(String pathPattern) { - return pathPattern.indexOf('{') != -1 || pathPattern.indexOf(':') != -1 - || pathPattern.indexOf('*') != -1; - } - - /** - * Get all keys(paths) in trie, call recursion function - * Time O(n), Space O(n), n is number of nodes in trie. - */ - public List getKeys() { - List res = new ArrayList<>(); - keysHelper(root, res, ""); - return res; - } - - /** - * Similar to pre-order (DFS, depth first search) of the tree, - * recursion is used to traverse all nodes in trie. When visiting the node, - * the method concatenates characters from previously visited nodes with - * the character of the current node. When the node's isReal is true, - * the recursion reaches the last character of the path. - * Add the path to the result list. - * recursion function, Time O(n), Space O(n), n is number of nodes in trie - */ - void keysHelper(RadixTreeNode> node, List res, - String prefix) { - if (node == null) { - //base condition - return; - } - - if (node.isReal()) { - String path = prefix + node.getKey(); - res.add(path); - } - for (RadixTreeNode> child : node.getChildren()) { - keysHelper(child, res, prefix + node.getKey()); - } - } - -} diff --git a/src/main/java/run/halo/app/theme/router/RadixTree.java b/src/main/java/run/halo/app/theme/router/RadixTree.java deleted file mode 100644 index ee9b80fa9..000000000 --- a/src/main/java/run/halo/app/theme/router/RadixTree.java +++ /dev/null @@ -1,360 +0,0 @@ -package run.halo.app.theme.router; - -import java.util.ArrayList; -import java.util.Iterator; -import lombok.Data; - -/** - * Implementation for {@link RadixTree Radix tree}. - * - * @author guqing - */ -@Data -public class RadixTree { - protected RadixTreeNode root; - protected long size; - - /** - * Create a Radix Tree with only the default node root. - */ - public RadixTree() { - root = new RadixTreeNode(); - root.setKey("/"); - root.setIndices(""); - size = 0; - } - - /** - * Find the node value with the given key. - * - * @param key the key to search - * @return value of the node with the given key if found, otherwise null - */ - public T find(String key) { - Visitor visitor = new Visitor<>() { - public void visit(String key, RadixTreeNode parent, - RadixTreeNode node) { - if (node.isReal()) { - result = node.getValue(); - } - } - }; - visit(key, visitor); - return visitor.getResult(); - } - - /** - * Replace value by the given key. - * - * @param key the key to search - * @param value the value to replace - * @return {@code true} if replaced, otherwise {@code false} - */ - public boolean replace(String key, final T value) { - Visitor visitor = new Visitor<>() { - public void visit(String key, RadixTreeNode parent, RadixTreeNode node) { - if (node.isReal()) { - node.setValue(value); - result = value; - } else { - result = null; - } - } - }; - visit(key, visitor); - return visitor.getResult() != null; - } - - /** - * Delete the tree node with the given key. - * - * @param key the key to delete - * @return @{code true} if deleted, otherwise {@code false} - */ - public boolean delete(String key) { - Visitor visitor = new Visitor<>(Boolean.FALSE) { - public void visit(String key, RadixTreeNode parent, - RadixTreeNode node) { - result = node.isReal(); - // if it is a real node - if (result) { - // If there are no children of the node we need to - // delete it from the parent children list - if (node.getChildren().size() == 0) { - Iterator> it = parent.getChildren() - .iterator(); - for (int index = 0; it.hasNext(); index++) { - if (it.next().getKey().equals(node.getKey())) { - // delete node - it.remove(); - - // update indices - StringBuilder indices = new StringBuilder(parent.getIndices()); - indices.deleteCharAt(index); - parent.setIndices(indices.toString()); - break; - } - } - - // if parent is not real node and has only one child - // then they need to be merged. - if (parent.getChildren().size() == 1 - && !parent.isReal()) { - mergeNodes(parent, parent.getChildren().get(0)); - } - } else if (node.getChildren().size() == 1) { - // we need to merge the only child of this node with - // itself - mergeNodes(node, node.getChildren().get(0)); - } else { // we jus need to mark the node as non-real. - node.setReal(false); - } - } - } - - /** - * Merge a child into its parent node. Operation only valid if it is - * only child of the parent node and parent node is not a real node. - * - * @param parent The parent Node - * @param child The child Node - */ - private void mergeNodes(RadixTreeNode parent, - RadixTreeNode child) { - parent.setKey(parent.getKey() + child.getKey()); - parent.setReal(child.isReal()); - parent.setValue(child.getValue()); - parent.setChildren(child.getChildren()); - parent.setIndices(child.getIndices()); - } - }; - visit(key, visitor); - if (visitor.getResult()) { - size--; - } - return visitor.getResult(); - } - - /** - * Recursively insert the key in the radix tree. - * - * @see #insert(String, Object) - */ - public void insert(String key, T value) throws IllegalArgumentException { - try { - insert(key, root, value); - } catch (IllegalArgumentException e) { - // re-throw the exception with 'key' in the message - throw new IllegalArgumentException("A handle is already registered for key:" + key); - } - size++; - } - - /** - * Recursively insert the key in the radix tree. - * - * @param key The key to be inserted - * @param node The current node - * @param value The value associated with the key - * @throws IllegalArgumentException If the key already exists in the database. - */ - private void insert(String key, RadixTreeNode node, T value) - throws IllegalArgumentException { - int numberOfMatchingCharacters = node.getNumberOfMatchingCharacters(key); - // we are either at the root node - // or we need to go down the tree - if (node.getKey().equals("") || numberOfMatchingCharacters == 0 - || (numberOfMatchingCharacters < key.length() - && numberOfMatchingCharacters >= node.getKey().length())) { - boolean flag = false; - String newText = key.substring(numberOfMatchingCharacters); - - // 递归查找插入位置 - char idxc = newText.charAt(0); - for (int i = 0; i < node.getIndices().length(); i++) { - if (node.getIndices().charAt(i) == idxc) { - RadixTreeNode child = node.getChildren().get(i); - flag = true; - insert(newText, child, value); - break; - } - } - // just add the node as the child of the current node - if (!flag) { - RadixTreeNode n = new RadixTreeNode(); - n.setKey(newText); - n.setReal(true); - n.setValue(value); - // 往后追加与child对于的首字母到 indices - node.setIndices(node.getIndices() + idxc); - node.getChildren().add(n); - } - } else if (numberOfMatchingCharacters == key.length() - && numberOfMatchingCharacters == node.getKey().length()) { - // there is an exact match just make the current node as data node - if (node.isReal()) { - throw new IllegalArgumentException("Duplicate key."); - } - node.setReal(true); - node.setValue(value); - } else if (numberOfMatchingCharacters > 0 && numberOfMatchingCharacters < node.getKey() - .length()) { - // This node need to be split as the key to be inserted - // is a prefix of the current node key - RadixTreeNode n1 = new RadixTreeNode<>(); - n1.setKey(node.getKey().substring(numberOfMatchingCharacters)); - n1.setReal(node.isReal()); - n1.setValue(node.getValue()); - n1.setIndices(node.getIndices()); - n1.setChildren(node.getChildren()); - - node.setKey(key.substring(0, numberOfMatchingCharacters)); - node.setReal(false); - node.setChildren(new ArrayList<>()); - node.getChildren().add(n1); - node.setIndices(""); - // 往后追加与child对于的首字母到 indices - node.setIndices(node.getIndices() + n1.getKey().charAt(0)); - // 新公共前缀比原公共前缀短,需要将当前的节点按公共前缀分开 - if (numberOfMatchingCharacters < key.length()) { - RadixTreeNode n2 = new RadixTreeNode<>(); - n2.setKey(key.substring(numberOfMatchingCharacters)); - n2.setReal(true); - n2.setValue(value); - - node.getChildren().add(n2); - node.setIndices(node.getIndices() + n2.getKey().charAt(0)); - } else { - node.setValue(value); - node.setReal(true); - } - } else { - // this key need to be added as the child of the current node - RadixTreeNode n = new RadixTreeNode(); - n.setKey(node.getKey().substring(numberOfMatchingCharacters)); - n.setChildren(node.getChildren()); - n.setReal(node.isReal()); - n.setValue(node.getValue()); - - node.setKey(key); - node.setReal(true); - node.setValue(value); - node.getChildren().add(n); - char idxc = node.getKey().charAt(0); - // 往后追加与child对于的首字母到 indices - n.setIndices(n.getIndices() + idxc); - } - } - - /** - * The tree contains the key. - * - * @param key the key to search - * @return {@code true} if the tree contains the key, otherwise {@code false} - */ - public boolean contains(String key) { - Visitor visitor = new Visitor<>(Boolean.FALSE) { - public void visit(String key, RadixTreeNode parent, - RadixTreeNode node) { - result = node.isReal(); - } - }; - visit(key, visitor); - return visitor.getResult(); - } - - /** - * visit the node those key matches the given key. - * - * @param key The key that need to be visited - * @param visitor The visitor object - */ - public void visit(String key, Visitor visitor) { - if (root != null) { - visit(key, visitor, null, root); - } - } - - /** - * recursively visit the tree based on the supplied "key". calls the Visitor - * for the node those key matches the given prefix. - * - * @param prefix The key of prefix to search in the tree - * @param visitor The Visitor that will be called if a node with "key" as its key is found - * @param node The Node from where onward to search - */ - void visit(String prefix, Visitor visitor, - RadixTreeNode parent, RadixTreeNode node) { - int numberOfMatchingCharacters = node.getNumberOfMatchingCharacters(prefix); - // if the node key and prefix match, we found a match! - if (numberOfMatchingCharacters == prefix.length() - && numberOfMatchingCharacters == node.getKey().length()) { - visitor.visit(prefix, parent, node); - } else if (node.getKey().equals("") // either we are at the - // root - || (numberOfMatchingCharacters < prefix.length() - && numberOfMatchingCharacters >= node.getKey().length())) { - // OR we need to traverse the children - String newText = prefix.substring(numberOfMatchingCharacters); - for (RadixTreeNode child : node.getChildren()) { - // recursively search the child nodes - if (child.getKey().startsWith(newText.charAt(0) + "")) { - visit(newText, visitor, node, child); - break; - } - } - } - } - - public long getSize() { - return size; - } - - /** - *

Display the Trie on console.

- * WARNING! Do not use this for a large Trie, it's for testing purpose only. - */ - @Deprecated - public String display() { - StringBuilder buffer = new StringBuilder(); - root.print(buffer, "", ""); - return buffer.toString(); - } - - /** - * Only used for testing purpose. - */ - public void checkIndices() { - this.checkIndices(root); - } - - void checkIndices(RadixTreeNode node) { - if (node == null) { - //base condition - return; - } - node.checkIndices(); - for (RadixTreeNode child : node.getChildren()) { - checkIndices(child); - } - } - - public abstract static class Visitor { - - protected R result; - - public Visitor() { - this.result = null; - } - - public Visitor(R initialValue) { - this.result = initialValue; - } - - public R getResult() { - return result; - } - - public abstract void visit(String key, RadixTreeNode parent, RadixTreeNode node); - } -} diff --git a/src/main/java/run/halo/app/theme/router/RadixTreeNode.java b/src/main/java/run/halo/app/theme/router/RadixTreeNode.java deleted file mode 100644 index 33512e4db..000000000 --- a/src/main/java/run/halo/app/theme/router/RadixTreeNode.java +++ /dev/null @@ -1,85 +0,0 @@ -package run.halo.app.theme.router; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import lombok.Data; -import org.apache.commons.lang3.StringUtils; - -/** - * Represents a node of a Radix tree {@link RadixTree}. - * - * @param value type - * @author guqing - */ -@Data -public class RadixTreeNode { - private String key; - private List> children; - private boolean real; - private T value; - - protected String indices; - - /** - * intailize the fields with default values to avoid null reference checks - * all over the places. - */ - public RadixTreeNode() { - key = ""; - children = new ArrayList<>(); - real = false; - indices = ""; - } - - protected int getNumberOfMatchingCharacters(String key) { - int numberOfMatchingCharacters = 0; - while (numberOfMatchingCharacters < key.length() - && numberOfMatchingCharacters < this.getKey().length()) { - if (key.charAt(numberOfMatchingCharacters) != this.getKey() - .charAt(numberOfMatchingCharacters)) { - break; - } - numberOfMatchingCharacters++; - } - return numberOfMatchingCharacters; - } - - void print(StringBuilder buffer, String prefix, String childrenPrefix) { - buffer.append(prefix); - buffer.append(printNode()); - buffer.append('\n'); - for (Iterator> it = children.iterator(); it.hasNext(); ) { - RadixTreeNode next = it.next(); - if (it.hasNext()) { - next.print(buffer, childrenPrefix + "├── ", childrenPrefix + "│ "); - } else { - next.print(buffer, childrenPrefix + "└── ", childrenPrefix + " "); - } - } - } - - String printNode() { - if (isReal()) { - return String.format("%s [value=%s]*", getKey(), getValue()); - } else { - return String.format("%s [indices=%s]", getKey(), getIndices()); - } - } - - /** - * Check whether {@link #indices} matches the {@link #children} items prefix. - */ - public void checkIndices() { - StringBuilder indices = new StringBuilder(); - for (RadixTreeNode child : this.getChildren()) { - indices.append(child.getKey().charAt(0)); - } - - if (!StringUtils.equals(this.getIndices(), indices.toString())) { - throw new IllegalStateException( - String.format("indices mismatch for node '%s': is %s, should be %s", this.getKey(), - this.getIndices(), indices)); - } - } -} diff --git a/src/main/java/run/halo/app/theme/router/SinglePageRoute.java b/src/main/java/run/halo/app/theme/router/SinglePageRoute.java new file mode 100644 index 000000000..7e611b604 --- /dev/null +++ b/src/main/java/run/halo/app/theme/router/SinglePageRoute.java @@ -0,0 +1,148 @@ +package run.halo.app.theme.router; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.util.UriUtils.encodePath; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.content.SinglePage; +import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ExtensionOperator; +import run.halo.app.extension.GVK; +import run.halo.app.extension.GroupVersionKind; +import run.halo.app.extension.Scheme; +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.exception.NotFoundException; +import run.halo.app.theme.DefaultTemplateEnum; +import run.halo.app.theme.finders.SinglePageFinder; +import run.halo.app.theme.router.factories.ModelConst; + +/** + * The {@link SinglePageRoute} for route request to specific template page.html. + * + * @author guqing + * @since 2.0.0 + */ +@Component +@RequiredArgsConstructor +public class SinglePageRoute + implements RouterFunction, Reconciler, DisposableBean { + private final GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class); + + private final Map> quickRouteMap = + new ConcurrentHashMap<>(); + + private final ExtensionClient client; + + private final SinglePageFinder singlePageFinder; + + private final ViewNameResolver viewNameResolver; + + @Override + @NonNull + public Mono> route(@NonNull ServerRequest request) { + return Flux.fromIterable(routerFunctions()) + .concatMap(routerFunction -> routerFunction.route(request)) + .next(); + } + + @Override + public void accept(@NonNull RouterFunctions.Visitor visitor) { + routerFunctions().forEach(routerFunction -> routerFunction.accept(visitor)); + } + + private List> routerFunctions() { + return quickRouteMap.keySet().stream() + .map(nameSlugPair -> { + String routePath = singlePageRoute(nameSlugPair.slug()); + return RouterFunctions.route(GET(routePath) + .and(RequestPredicates.accept(MediaType.TEXT_HTML)), + handlerFunction(nameSlugPair.name())); + }) + .collect(Collectors.toList()); + } + + @Override + public Result reconcile(Request request) { + client.fetch(SinglePage.class, request.name()) + .ifPresent(page -> { + if (ExtensionOperator.isDeleted(page) + || BooleanUtils.isTrue(page.getSpec().getDeleted())) { + quickRouteMap.remove(NameSlugPair.from(page)); + return; + } + // put new one + quickRouteMap.entrySet() + .removeIf(entry -> entry.getKey().name().equals(request.name())); + quickRouteMap.put(NameSlugPair.from(page), handlerFunction(request.name())); + }); + return new Result(false, null); + } + + @Override + public Controller setupWith(ControllerBuilder builder) { + return builder + .extension(new SinglePage()) + .build(); + } + + @Override + public void destroy() throws Exception { + quickRouteMap.clear(); + } + + record NameSlugPair(String name, String slug) { + public static NameSlugPair from(SinglePage page) { + return new NameSlugPair(page.getMetadata().getName(), page.getSpec().getSlug()); + } + } + + String singlePageRoute(String slug) { + var permalink = encodePath(slug, UTF_8); + return StringUtils.prependIfMissing(permalink, "/"); + } + + HandlerFunction handlerFunction(String name) { + return request -> singlePageFinder.getByName(name) + .flatMap(singlePageVo -> { + Map model = new HashMap<>(); + model.put("groupVersionKind", gvk); + model.put("plural", getPlural()); + model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue()); + model.put("singlePage", singlePageVo); + String template = singlePageVo.getSpec().getTemplate(); + return viewNameResolver.resolveViewNameOrDefault(request, template, + DefaultTemplateEnum.SINGLE_PAGE.getValue()) + .flatMap(viewName -> ServerResponse.ok().render(viewName, model)); + }) + .switchIfEmpty( + Mono.error(new NotFoundException("Single page not found")) + ); + } + + private String getPlural() { + GVK gvk = Scheme.getGvkFromType(SinglePage.class); + return gvk.plural(); + } +} diff --git a/src/main/java/run/halo/app/theme/router/ThemeCompositeRouterFunction.java b/src/main/java/run/halo/app/theme/router/ThemeCompositeRouterFunction.java index 3548f979e..a837c5e55 100644 --- a/src/main/java/run/halo/app/theme/router/ThemeCompositeRouterFunction.java +++ b/src/main/java/run/halo/app/theme/router/ThemeCompositeRouterFunction.java @@ -1,40 +1,132 @@ package run.halo.app.theme.router; -import org.springframework.http.HttpMethod; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.EventListener; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import run.halo.app.infra.SchemeInitializedEvent; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.SystemSetting; +import run.halo.app.theme.DefaultTemplateEnum; +import run.halo.app.theme.router.factories.ArchiveRouteFactory; +import run.halo.app.theme.router.factories.AuthorPostsRouteFactory; +import run.halo.app.theme.router.factories.CategoriesRouteFactory; +import run.halo.app.theme.router.factories.CategoryPostRouteFactory; +import run.halo.app.theme.router.factories.IndexRouteFactory; +import run.halo.app.theme.router.factories.PostRouteFactory; +import run.halo.app.theme.router.factories.TagPostRouteFactory; +import run.halo.app.theme.router.factories.TagsRouteFactory; /** - *

Theme template composite {@link RouterFunction} for manage routers for default templates.

- * It routes specific requests to the {@link RouterFunction} maintained by the - * {@link PermalinkHttpGetRouter}. + *

The combination router of theme templates is used to render theme templates, but does not + * include page.html templates which is processed separately.

* * @author guqing - * @see PermalinkHttpGetRouter + * @see SinglePageRoute * @since 2.0.0 */ @Component -public class ThemeCompositeRouterFunction implements - RouterFunction { +@RequiredArgsConstructor +public class ThemeCompositeRouterFunction implements RouterFunction { + private final SystemConfigurableEnvironmentFetcher environmentFetcher; - private final PermalinkHttpGetRouter permalinkHttpGetRouter; + private final ArchiveRouteFactory archiveRouteFactory; + private final PostRouteFactory postRouteFactory; + private final CategoriesRouteFactory categoriesRouteFactory; + private final CategoryPostRouteFactory categoryPostRouteFactory; + private final TagPostRouteFactory tagPostRouteFactory; + private final TagsRouteFactory tagsRouteFactory; + private final AuthorPostsRouteFactory authorPostsRouteFactory; + private final IndexRouteFactory indexRouteFactory; - public ThemeCompositeRouterFunction(PermalinkHttpGetRouter permalinkHttpGetRouter) { - this.permalinkHttpGetRouter = permalinkHttpGetRouter; - } + private List> cachedRouters = List.of(); @Override @NonNull public Mono> route(@NonNull ServerRequest request) { - // this router function only supports GET method - if (!request.method().equals(HttpMethod.GET)) { - return Mono.empty(); - } - return Mono.justOrEmpty(permalinkHttpGetRouter.route(request)); + return Flux.fromIterable(cachedRouters) + .concatMap(routerFunction -> routerFunction.route(request)) + .next(); + } + + @Override + public void accept(@NonNull RouterFunctions.Visitor visitor) { + cachedRouters.forEach(routerFunction -> routerFunction.accept(visitor)); + } + + List> routerFunctions() { + return transformedPatterns() + .stream() + .map(this::createRouterFunction) + .collect(Collectors.toList()); + } + + private RouterFunction createRouterFunction(RoutePattern routePattern) { + return switch (routePattern.identifier()) { + case POST -> postRouteFactory.create(routePattern.pattern()); + case ARCHIVES -> archiveRouteFactory.create(routePattern.pattern()); + case CATEGORIES -> categoriesRouteFactory.create(routePattern.pattern()); + case CATEGORY -> categoryPostRouteFactory.create(routePattern.pattern()); + case TAGS -> tagsRouteFactory.create(routePattern.pattern()); + case TAG -> tagPostRouteFactory.create(routePattern.pattern()); + case AUTHOR -> authorPostsRouteFactory.create(routePattern.pattern()); + case INDEX -> indexRouteFactory.create(routePattern.pattern()); + default -> + throw new IllegalStateException("Unexpected value: " + routePattern.identifier()); + }; + } + + /** + * Refresh the {@link #cachedRouters} when the permalink rule is changed. + * + * @param event {@link SchemeInitializedEvent} or {@link PermalinkRuleChangedEvent} + */ + @EventListener({SchemeInitializedEvent.class, PermalinkRuleChangedEvent.class}) + public void onSchemeInitializedEvent(@NonNull ApplicationEvent event) { + this.cachedRouters = routerFunctions(); + } + + record RoutePattern(DefaultTemplateEnum identifier, String pattern) { + } + + private List transformedPatterns() { + List routePatterns = new ArrayList<>(); + + SystemSetting.ThemeRouteRules rules = + environmentFetcher.fetch(SystemSetting.ThemeRouteRules.GROUP, + SystemSetting.ThemeRouteRules.class) + .blockOptional() + .orElse(SystemSetting.ThemeRouteRules.empty()); + String post = rules.getPost(); + routePatterns.add(new RoutePattern(DefaultTemplateEnum.POST, post)); + + String archives = rules.getArchives(); + routePatterns.add( + new RoutePattern(DefaultTemplateEnum.ARCHIVES, archives)); + + String categories = rules.getCategories(); + routePatterns.add( + new RoutePattern(DefaultTemplateEnum.CATEGORIES, categories)); + routePatterns.add( + new RoutePattern(DefaultTemplateEnum.CATEGORY, categories)); + + String tags = rules.getTags(); + routePatterns.add(new RoutePattern(DefaultTemplateEnum.TAGS, tags)); + routePatterns.add(new RoutePattern(DefaultTemplateEnum.TAG, tags)); + + // Add the index route to the end to prevent conflict with the queryParam rule of the post + routePatterns.add(new RoutePattern(DefaultTemplateEnum.INDEX, "/")); + return routePatterns; } } diff --git a/src/main/java/run/halo/app/theme/router/factories/ArchiveRouteFactory.java b/src/main/java/run/halo/app/theme/router/factories/ArchiveRouteFactory.java new file mode 100644 index 000000000..62f4bf0d3 --- /dev/null +++ b/src/main/java/run/halo/app/theme/router/factories/ArchiveRouteFactory.java @@ -0,0 +1,102 @@ +package run.halo.app.theme.router.factories; + +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static run.halo.app.theme.router.PageUrlUtils.totalPage; + +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RequestPredicate; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.utils.JsonUtils; +import run.halo.app.infra.utils.PathUtils; +import run.halo.app.theme.DefaultTemplateEnum; +import run.halo.app.theme.finders.PostFinder; +import run.halo.app.theme.finders.vo.PostArchiveVo; +import run.halo.app.theme.router.PageUrlUtils; +import run.halo.app.theme.router.UrlContextListResult; + +/** + * The {@link ArchiveRouteFactory} for generate {@link RouterFunction} specific to the template + * posts.html. + * + * @author guqing + * @since 2.0.0 + */ +@Component +@AllArgsConstructor +public class ArchiveRouteFactory implements RouteFactory { + + private final PostFinder postFinder; + + private final SystemConfigurableEnvironmentFetcher environmentFetcher; + + @Override + public RouterFunction create(String prefix) { + RequestPredicate requestPredicate = patterns(prefix).stream() + .map(RequestPredicates::GET) + .reduce(req -> false, RequestPredicate::or) + .and(accept(MediaType.TEXT_HTML)); + return RouterFunctions.route(requestPredicate, handlerFunction()); + } + + HandlerFunction handlerFunction() { + return request -> { + String templateName = DefaultTemplateEnum.ARCHIVES.getValue(); + return ServerResponse.ok() + .render(templateName, + Map.of("archives", archivePosts(request), + ModelConst.TEMPLATE_ID, templateName) + ); + }; + } + + private List patterns(String prefix) { + return List.of( + StringUtils.prependIfMissing(prefix, "/"), + PathUtils.combinePath(prefix, "/page/{page:\\d+}"), + PathUtils.combinePath(prefix, "/{year:\\d{4}}"), + PathUtils.combinePath(prefix, "/{year:\\d{4}}/page/{page:\\d+}"), + PathUtils.combinePath(prefix, "/{year:\\d{4}}/{month:\\d{2}}"), + PathUtils.combinePath(prefix, + "/{year:\\d{4}}/{month:\\d{2}}/page/{page:\\d+}") + ); + } + + private Mono> archivePosts(ServerRequest request) { + ArchivePathVariables variables = ArchivePathVariables.from(request); + int pageNum = pageNumInPathVariable(request); + String requestPath = request.path(); + return configuredPageSize(environmentFetcher) + .flatMap(pageSize -> postFinder.archives(pageNum, pageSize, variables.getYear(), + variables.getMonth())) + .map(list -> new UrlContextListResult.Builder() + .listResult(list) + .nextUrl(PageUrlUtils.nextPageUrl(requestPath, totalPage(list))) + .prevUrl(PageUrlUtils.prevPageUrl(requestPath)) + .build()); + } + + @Data + static class ArchivePathVariables { + String year; + String month; + String page; + + static ArchivePathVariables from(ServerRequest request) { + Map variables = request.pathVariables(); + return JsonUtils.mapToObject(variables, ArchivePathVariables.class); + } + } +} diff --git a/src/main/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactory.java b/src/main/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactory.java new file mode 100644 index 000000000..553c6b54d --- /dev/null +++ b/src/main/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactory.java @@ -0,0 +1,78 @@ +package run.halo.app.theme.router.factories; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static run.halo.app.theme.router.PageUrlUtils.totalPage; + +import java.util.Map; +import lombok.AllArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.User; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.theme.DefaultTemplateEnum; +import run.halo.app.theme.finders.PostFinder; +import run.halo.app.theme.finders.vo.ListedPostVo; +import run.halo.app.theme.finders.vo.UserVo; +import run.halo.app.theme.router.PageUrlUtils; +import run.halo.app.theme.router.UrlContextListResult; + +/** + * The {@link AuthorPostsRouteFactory} for generate {@link RouterFunction} specific to the template + * index.html. + * + * @author guqing + * @since 2.0.0 + */ +@Component +@AllArgsConstructor +public class AuthorPostsRouteFactory implements RouteFactory { + + private final PostFinder postFinder; + private final ReactiveExtensionClient client; + private SystemConfigurableEnvironmentFetcher environmentFetcher; + + @Override + public RouterFunction create(String pattern) { + return RouterFunctions + .route(GET("/authors/{name}").or(GET("/authors/{name}/page/{page}")) + .and(accept(MediaType.TEXT_HTML)), handlerFunction()); + } + + HandlerFunction handlerFunction() { + return request -> { + String name = request.pathVariable("name"); + return ServerResponse.ok() + .render(DefaultTemplateEnum.AUTHOR.getValue(), + Map.of("author", getByName(name), + "posts", postList(request, name), + ModelConst.TEMPLATE_ID, DefaultTemplateEnum.AUTHOR.getValue() + ) + ); + }; + } + + private Mono> postList(ServerRequest request, String name) { + String path = request.path(); + int pageNum = pageNumInPathVariable(request); + return configuredPageSize(environmentFetcher) + .flatMap(pageSize -> postFinder.listByOwner(pageNum, pageSize, name)) + .map(list -> new UrlContextListResult.Builder() + .listResult(list) + .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) + .prevUrl(PageUrlUtils.prevPageUrl(path)) + .build()); + } + + private Mono getByName(String name) { + return client.get(User.class, name) + .map(UserVo::from); + } +} diff --git a/src/main/java/run/halo/app/theme/router/strategy/CategoriesRouteStrategy.java b/src/main/java/run/halo/app/theme/router/factories/CategoriesRouteFactory.java similarity index 55% rename from src/main/java/run/halo/app/theme/router/strategy/CategoriesRouteStrategy.java rename to src/main/java/run/halo/app/theme/router/factories/CategoriesRouteFactory.java index 084860ea6..c16f2976e 100644 --- a/src/main/java/run/halo/app/theme/router/strategy/CategoriesRouteStrategy.java +++ b/src/main/java/run/halo/app/theme/router/factories/CategoriesRouteFactory.java @@ -1,17 +1,21 @@ -package run.halo.app.theme.router.strategy; +package run.halo.app.theme.router.factories; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; -import java.util.List; import java.util.Map; import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.finders.CategoryFinder; /** - * Categories router strategy for generate {@link HandlerFunction} specific to the template + * The {@link CategoriesRouteFactory} for generate {@link RouterFunction} specific to the + * template * categories.html. * * @author guqing @@ -19,24 +23,20 @@ import run.halo.app.theme.finders.CategoryFinder; */ @Component @AllArgsConstructor -public class CategoriesRouteStrategy implements ListPageRouteHandlerStrategy { +public class CategoriesRouteFactory implements RouteFactory { + private final CategoryFinder categoryFinder; @Override - public HandlerFunction getHandler() { + public RouterFunction create(String prefix) { + return RouterFunctions.route(GET(StringUtils.prependIfMissing(prefix, "/")), + handlerFunction()); + } + + HandlerFunction handlerFunction() { return request -> ServerResponse.ok() .render(DefaultTemplateEnum.CATEGORIES.getValue(), Map.of("categories", categoryFinder.listAsTree(), ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORIES.getValue())); } - - @Override - public List getRouterPaths(String prefix) { - return List.of(StringUtils.prependIfMissing(prefix, "/")); - } - - @Override - public boolean supports(DefaultTemplateEnum template) { - return DefaultTemplateEnum.CATEGORIES.equals(template); - } } diff --git a/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java b/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java new file mode 100644 index 000000000..871ab9879 --- /dev/null +++ b/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java @@ -0,0 +1,95 @@ +package run.halo.app.theme.router.factories; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static run.halo.app.theme.router.PageUrlUtils.totalPage; + +import java.util.HashMap; +import java.util.Map; +import lombok.AllArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.content.Category; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.exception.NotFoundException; +import run.halo.app.infra.utils.PathUtils; +import run.halo.app.theme.DefaultTemplateEnum; +import run.halo.app.theme.finders.PostFinder; +import run.halo.app.theme.finders.vo.CategoryVo; +import run.halo.app.theme.finders.vo.ListedPostVo; +import run.halo.app.theme.router.PageUrlUtils; +import run.halo.app.theme.router.UrlContextListResult; +import run.halo.app.theme.router.ViewNameResolver; + +/** + * The {@link CategoryPostRouteFactory} for generate {@link RouterFunction} specific to the template + * category.html. + * + * @author guqing + * @since 2.0.0 + */ +@Component +@AllArgsConstructor +public class CategoryPostRouteFactory implements RouteFactory { + + private final PostFinder postFinder; + + private final SystemConfigurableEnvironmentFetcher environmentFetcher; + private final ReactiveExtensionClient client; + private final ViewNameResolver viewNameResolver; + + @Override + public RouterFunction create(String prefix) { + return RouterFunctions.route(GET(PathUtils.combinePath(prefix, "/{slug}")) + .or(GET(PathUtils.combinePath(prefix, "/{slug}/page/{page:\\d+}"))) + .and(accept(MediaType.TEXT_HTML)), handlerFunction()); + } + + HandlerFunction handlerFunction() { + return request -> { + String slug = request.pathVariable("slug"); + return fetchBySlug(slug) + .flatMap(categoryVo -> { + Map model = new HashMap<>(); + model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORY.getValue()); + model.put("posts", + postListByCategoryName(categoryVo.getMetadata().getName(), request)); + model.put("category", categoryVo); + String template = categoryVo.getSpec().getTemplate(); + return viewNameResolver.resolveViewNameOrDefault(request, template, + DefaultTemplateEnum.CATEGORY.getValue()) + .flatMap(viewName -> ServerResponse.ok().render(viewName, model)); + }) + .switchIfEmpty( + Mono.error(new NotFoundException("Category not found with slug: " + slug))); + }; + } + + Mono fetchBySlug(String slug) { + return client.list(Category.class, category -> category.getSpec().getSlug().equals(slug) + && category.getMetadata().getDeletionTimestamp() == null, null) + .next() + .map(CategoryVo::from); + } + + private Mono> postListByCategoryName(String name, + ServerRequest request) { + String path = request.path(); + int pageNum = pageNumInPathVariable(request); + return configuredPageSize(environmentFetcher) + .flatMap(pageSize -> postFinder.listByCategory(pageNum, pageSize, name)) + .map(list -> new UrlContextListResult.Builder() + .listResult(list) + .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) + .prevUrl(PageUrlUtils.prevPageUrl(path)) + .build() + ); + } +} diff --git a/src/main/java/run/halo/app/theme/router/strategy/IndexRouteStrategy.java b/src/main/java/run/halo/app/theme/router/factories/IndexRouteFactory.java similarity index 59% rename from src/main/java/run/halo/app/theme/router/strategy/IndexRouteStrategy.java rename to src/main/java/run/halo/app/theme/router/factories/IndexRouteFactory.java index c89d01e67..125d878b2 100644 --- a/src/main/java/run/halo/app/theme/router/strategy/IndexRouteStrategy.java +++ b/src/main/java/run/halo/app/theme/router/factories/IndexRouteFactory.java @@ -1,15 +1,16 @@ -package run.halo.app.theme.router.strategy; +package run.halo.app.theme.router.factories; -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static run.halo.app.theme.router.PageUrlUtils.pageNum; +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; import static run.halo.app.theme.router.PageUrlUtils.totalPage; -import static run.halo.app.theme.router.strategy.ModelConst.DEFAULT_PAGE_SIZE; -import java.util.List; import java.util.Map; import lombok.AllArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; @@ -21,7 +22,7 @@ import run.halo.app.theme.router.PageUrlUtils; import run.halo.app.theme.router.UrlContextListResult; /** - * The {@link IndexRouteStrategy} for generate {@link HandlerFunction} specific to the template + * The {@link IndexRouteFactory} for generate {@link RouterFunction} specific to the template * index.html. * * @author guqing @@ -29,38 +30,35 @@ import run.halo.app.theme.router.UrlContextListResult; */ @Component @AllArgsConstructor -public class IndexRouteStrategy implements ListPageRouteHandlerStrategy { +public class IndexRouteFactory implements RouteFactory { private final PostFinder postFinder; private final SystemConfigurableEnvironmentFetcher environmentFetcher; - private Mono> postList(ServerRequest request) { - String path = request.path(); - return environmentFetcher.fetchPost() - .map(p -> defaultIfNull(p.getPostPageSize(), DEFAULT_PAGE_SIZE)) - .flatMap(pageSize -> postFinder.list(pageNum(request), pageSize)) - .map(list -> new UrlContextListResult.Builder() - .listResult(list) - .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) - .prevUrl(PageUrlUtils.prevPageUrl(path)) - .build()); + @Override + public RouterFunction create(String pattern) { + return RouterFunctions + .route(GET("/").or(GET("/page/{page}") + .or(GET("/index")).or(GET("/index/page/{page}")) + .and(accept(MediaType.TEXT_HTML))), handlerFunction()); } - @Override - public HandlerFunction getHandler() { + HandlerFunction handlerFunction() { return request -> ServerResponse.ok() .render(DefaultTemplateEnum.INDEX.getValue(), Map.of("posts", postList(request), ModelConst.TEMPLATE_ID, DefaultTemplateEnum.INDEX.getValue())); } - @Override - public List getRouterPaths(String pattern) { - return List.of("/", "/index"); - } - - @Override - public boolean supports(DefaultTemplateEnum template) { - return DefaultTemplateEnum.INDEX.equals(template); + private Mono> postList(ServerRequest request) { + String path = request.path(); + return configuredPageSize(environmentFetcher) + .flatMap(pageSize -> postFinder.list(pageNumInPathVariable(request), pageSize)) + .map(list -> new UrlContextListResult.Builder() + .listResult(list) + .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) + .prevUrl(PageUrlUtils.prevPageUrl(path)) + .build() + ); } } diff --git a/src/main/java/run/halo/app/theme/router/strategy/ModelConst.java b/src/main/java/run/halo/app/theme/router/factories/ModelConst.java similarity index 83% rename from src/main/java/run/halo/app/theme/router/strategy/ModelConst.java rename to src/main/java/run/halo/app/theme/router/factories/ModelConst.java index 090a812f4..67985ab0f 100644 --- a/src/main/java/run/halo/app/theme/router/strategy/ModelConst.java +++ b/src/main/java/run/halo/app/theme/router/factories/ModelConst.java @@ -1,4 +1,4 @@ -package run.halo.app.theme.router.strategy; +package run.halo.app.theme.router.factories; /** * Static variable keys for view model. diff --git a/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java b/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java new file mode 100644 index 000000000..af7c1fd58 --- /dev/null +++ b/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java @@ -0,0 +1,236 @@ +package run.halo.app.theme.router.factories; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.MediaType; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RequestPredicate; +import org.springframework.web.reactive.function.server.RequestPredicates; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.content.Post; +import run.halo.app.extension.ExtensionUtil; +import run.halo.app.extension.GVK; +import run.halo.app.extension.GroupVersionKind; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.exception.NotFoundException; +import run.halo.app.infra.utils.JsonUtils; +import run.halo.app.theme.DefaultTemplateEnum; +import run.halo.app.theme.finders.PostFinder; +import run.halo.app.theme.finders.impl.PostFinderImpl; +import run.halo.app.theme.finders.vo.PostVo; +import run.halo.app.theme.router.ViewNameResolver; + +/** + * The {@link PostRouteFactory} for generate {@link RouterFunction} specific to the template + * post.html. + * + * @author guqing + * @since 2.0.0 + */ +@Component +@AllArgsConstructor +public class PostRouteFactory implements RouteFactory { + + private final PostFinder postFinder; + + private final ViewNameResolver viewNameResolver; + + private final ReactiveExtensionClient client; + + @Override + public RouterFunction create(String pattern) { + PatternParser postParamPredicate = + new PatternParser(pattern); + if (postParamPredicate.isQueryParamPattern()) { + RequestPredicate requestPredicate = postParamPredicate.toRequestPredicate(); + return RouterFunctions.route(GET("/") + .and(requestPredicate), queryParamHandlerFunction(postParamPredicate)); + } + return RouterFunctions + .route(GET(pattern).and(accept(MediaType.TEXT_HTML)), handlerFunction()); + } + + HandlerFunction queryParamHandlerFunction(PatternParser paramPredicate) { + return request -> { + Map variables = mergedVariables(request); + PostPatternVariable patternVariable = new PostPatternVariable(); + Optional.ofNullable(variables.get(paramPredicate.getQueryParamName())) + .ifPresent(value -> { + switch (paramPredicate.getPlaceholderName()) { + case "name" -> patternVariable.setName(value); + case "slug" -> patternVariable.setSlug(value); + default -> + throw new IllegalArgumentException("Unsupported query param predicate"); + } + }); + return postResponse(request, patternVariable); + }; + } + + HandlerFunction handlerFunction() { + return request -> { + PostPatternVariable patternVariable = PostPatternVariable.from(request); + return postResponse(request, patternVariable); + }; + } + + @NonNull + private Mono postResponse(ServerRequest request, + PostPatternVariable patternVariable) { + Mono postVoMono = bestMatchPost(patternVariable); + return postVoMono + .flatMap(postVo -> { + Map model = new HashMap<>(); + model.put("groupVersionKind", GroupVersionKind.fromExtension(Post.class)); + GVK gvk = Post.class.getAnnotation(GVK.class); + model.put("plural", gvk.plural()); + model.put("post", postVo); + + String template = postVo.getSpec().getTemplate(); + return viewNameResolver.resolveViewNameOrDefault(request, template, + DefaultTemplateEnum.POST.getValue()) + .flatMap(templateName -> ServerResponse.ok().render(templateName, model)); + }); + } + + Mono bestMatchPost(PostPatternVariable variable) { + return postsByPredicates(variable) + .filter(post -> { + Map labels = ExtensionUtil.nullSafeLabels(post); + return matchIfPresent(variable.getName(), post.getMetadata().getName()) + && matchIfPresent(variable.getSlug(), post.getSpec().getSlug()) + && matchIfPresent(variable.getYear(), labels.get(Post.ARCHIVE_YEAR_LABEL)) + && matchIfPresent(variable.getMonth(), labels.get(Post.ARCHIVE_MONTH_LABEL)) + && matchIfPresent(variable.getDay(), labels.get(Post.ARCHIVE_DAY_LABEL)); + }) + .next() + .flatMap(post -> postFinder.getByName(post.getMetadata().getName())) + .switchIfEmpty(Mono.error(new NotFoundException("Post not found"))); + } + + Flux postsByPredicates(PostPatternVariable patternVariable) { + if (StringUtils.isNotBlank(patternVariable.getName())) { + return fetchPostsByName(patternVariable.getName()); + } + if (StringUtils.isNotBlank(patternVariable.getSlug())) { + return fetchPostsBySlug(patternVariable.getSlug()); + } + return Flux.empty(); + } + + private Flux fetchPostsByName(String name) { + return client.fetch(Post.class, name) + .filter(PostFinderImpl.FIXED_PREDICATE) + .flux(); + } + + private Flux fetchPostsBySlug(String slug) { + return client.list(Post.class, + post -> PostFinderImpl.FIXED_PREDICATE.test(post) + && matchIfPresent(slug, post.getSpec().getSlug()), + null); + } + + private boolean matchIfPresent(String variable, String target) { + return StringUtils.isBlank(variable) || StringUtils.equals(target, variable); + } + + @Data + static class PostPatternVariable { + String name; + String slug; + String year; + String month; + String day; + + static PostPatternVariable from(ServerRequest request) { + Map variables = mergedVariables(request); + return JsonUtils.mapToObject(variables, PostPatternVariable.class); + } + } + + static Map mergedVariables(ServerRequest request) { + Map pathVariables = request.pathVariables(); + MultiValueMap queryParams = request.queryParams(); + Map mergedVariables = new LinkedHashMap<>(); + for (String paramKey : queryParams.keySet()) { + mergedVariables.put(paramKey, queryParams.getFirst(paramKey)); + } + // path variables higher priority will override query params + mergedVariables.putAll(pathVariables); + return mergedVariables; + } + + static class PatternParser { + private static final Pattern PATTERN_COMPILE = Pattern.compile("([^&?]*)=\\{(.*?)\\}(&|$)"); + private static final Cache MATCHER_CACHE = CacheBuilder.newBuilder() + .maximumSize(5) + .build(); + + private final String pattern; + private String paramName; + private String placeholderName; + private final boolean isQueryParamPattern; + + PatternParser(String pattern) { + this.pattern = pattern; + Matcher matcher = patternToMatcher(pattern); + if (matcher.find()) { + this.paramName = matcher.group(1); + this.placeholderName = matcher.group(2); + this.isQueryParamPattern = true; + } else { + this.isQueryParamPattern = false; + } + } + + Matcher patternToMatcher(String pattern) { + try { + return MATCHER_CACHE.get(pattern, () -> PATTERN_COMPILE.matcher(pattern)); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + RequestPredicate toRequestPredicate() { + if (!this.isQueryParamPattern) { + throw new IllegalStateException("Not a query param pattern: " + pattern); + } + + return RequestPredicates.queryParam(paramName, value -> true); + } + + public String getPlaceholderName() { + return this.placeholderName; + } + + public String getQueryParamName() { + return this.paramName; + } + + public boolean isQueryParamPattern() { + return isQueryParamPattern; + } + } +} diff --git a/src/main/java/run/halo/app/theme/router/factories/RouteFactory.java b/src/main/java/run/halo/app/theme/router/factories/RouteFactory.java new file mode 100644 index 000000000..94c03a9cb --- /dev/null +++ b/src/main/java/run/halo/app/theme/router/factories/RouteFactory.java @@ -0,0 +1,29 @@ +package run.halo.app.theme.router.factories; + +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; + +import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; + +/** + * @author guqing + * @since 2.0.0 + */ +public interface RouteFactory { + RouterFunction create(String pattern); + + default Mono configuredPageSize( + SystemConfigurableEnvironmentFetcher environmentFetcher) { + return environmentFetcher.fetchPost() + .map(p -> defaultIfNull(p.getTagPageSize(), ModelConst.DEFAULT_PAGE_SIZE)); + } + + default int pageNumInPathVariable(ServerRequest request) { + String page = request.pathVariables().get("page"); + return NumberUtils.toInt(page, 1); + } +} diff --git a/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java b/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java new file mode 100644 index 000000000..15d5eb8d7 --- /dev/null +++ b/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java @@ -0,0 +1,89 @@ +package run.halo.app.theme.router.factories; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static run.halo.app.theme.router.PageUrlUtils.totalPage; + +import java.util.Map; +import lombok.AllArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.content.Tag; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.exception.NotFoundException; +import run.halo.app.infra.utils.PathUtils; +import run.halo.app.theme.DefaultTemplateEnum; +import run.halo.app.theme.finders.PostFinder; +import run.halo.app.theme.finders.TagFinder; +import run.halo.app.theme.finders.vo.ListedPostVo; +import run.halo.app.theme.finders.vo.TagVo; +import run.halo.app.theme.router.PageUrlUtils; +import run.halo.app.theme.router.UrlContextListResult; + +/** + * The {@link TagPostRouteFactory} for generate {@link RouterFunction} specific to the template + * tag.html. + * + * @author guqing + * @since 2.0.0 + */ +@Component +@AllArgsConstructor +public class TagPostRouteFactory implements RouteFactory { + + private final ReactiveExtensionClient client; + private final SystemConfigurableEnvironmentFetcher environmentFetcher; + private final TagFinder tagFinder; + private final PostFinder postFinder; + + @Override + public RouterFunction create(String prefix) { + return RouterFunctions + .route(GET(PathUtils.combinePath(prefix, "/{slug}")) + .or(GET(PathUtils.combinePath(prefix, "/{slug}/page/{page:\\d+}"))) + .and(accept(MediaType.TEXT_HTML)), handlerFunction()); + } + + private HandlerFunction handlerFunction() { + return request -> tagBySlug(request.pathVariable("slug")) + .flatMap(tagVo -> { + int pageNum = pageNumInPathVariable(request); + String path = request.path(); + var postList = postList(tagVo.getMetadata().getName(), pageNum, path); + return ServerResponse.ok() + .render(DefaultTemplateEnum.TAG.getValue(), + Map.of("name", tagVo.getMetadata().getName(), + "posts", postList, + "tag", tagVo) + ); + }); + } + + private Mono> postList(String name, Integer page, + String requestPath) { + return configuredPageSize(environmentFetcher) + .flatMap(pageSize -> postFinder.listByTag(page, pageSize, name)) + .map(list -> new UrlContextListResult.Builder() + .listResult(list) + .nextUrl(PageUrlUtils.nextPageUrl(requestPath, totalPage(list))) + .prevUrl(PageUrlUtils.prevPageUrl(requestPath)) + .build() + ); + } + + private Mono tagBySlug(String slug) { + return client.list(Tag.class, tag -> tag.getSpec().getSlug().equals(slug) + && tag.getMetadata().getDeletionTimestamp() == null, null) + .next() + .flatMap(tag -> tagFinder.getByName(tag.getMetadata().getName())) + .switchIfEmpty( + Mono.error(new NotFoundException("Tag not found with slug: " + slug))); + } + +} diff --git a/src/main/java/run/halo/app/theme/router/factories/TagsRouteFactory.java b/src/main/java/run/halo/app/theme/router/factories/TagsRouteFactory.java new file mode 100644 index 000000000..32b5f43dc --- /dev/null +++ b/src/main/java/run/halo/app/theme/router/factories/TagsRouteFactory.java @@ -0,0 +1,46 @@ +package run.halo.app.theme.router.factories; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; + +import java.util.Map; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import run.halo.app.theme.DefaultTemplateEnum; +import run.halo.app.theme.finders.TagFinder; + +/** + * The {@link TagsRouteFactory} for generate {@link RouterFunction} specific to the template + * tags.html. + * + * @author guqing + * @since 2.0.0 + */ +@Component +@AllArgsConstructor +public class TagsRouteFactory implements RouteFactory { + + private final TagFinder tagFinder; + + @Override + public RouterFunction create(String prefix) { + return RouterFunctions + .route(GET(StringUtils.prependIfMissing(prefix, "/")) + .and(accept(MediaType.TEXT_HTML)), handlerFunction()); + } + + private HandlerFunction handlerFunction() { + return request -> ServerResponse.ok() + .render(DefaultTemplateEnum.TAGS.getValue(), + Map.of("tags", tagFinder.listAll(), + ModelConst.TEMPLATE_ID, DefaultTemplateEnum.TAGS.getValue() + ) + ); + } +} diff --git a/src/main/java/run/halo/app/theme/router/strategy/ArchivesRouteStrategy.java b/src/main/java/run/halo/app/theme/router/strategy/ArchivesRouteStrategy.java deleted file mode 100644 index 42d182209..000000000 --- a/src/main/java/run/halo/app/theme/router/strategy/ArchivesRouteStrategy.java +++ /dev/null @@ -1,88 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static run.halo.app.theme.router.PageUrlUtils.pageNum; -import static run.halo.app.theme.router.PageUrlUtils.totalPage; -import static run.halo.app.theme.router.strategy.ModelConst.DEFAULT_PAGE_SIZE; - -import java.util.List; -import java.util.Map; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Mono; -import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; -import run.halo.app.infra.utils.PathUtils; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.finders.PostFinder; -import run.halo.app.theme.finders.vo.PostArchiveVo; -import run.halo.app.theme.router.PageUrlUtils; -import run.halo.app.theme.router.UrlContextListResult; - -/** - * The {@link ArchivesRouteStrategy} for generate {@link HandlerFunction} specific to the template - * posts.html. - * - * @author guqing - * @since 2.0.0 - */ -@Component -public class ArchivesRouteStrategy implements ListPageRouteHandlerStrategy { - private final PostFinder postFinder; - private final SystemConfigurableEnvironmentFetcher environmentFetcher; - - public ArchivesRouteStrategy(PostFinder postFinder, - SystemConfigurableEnvironmentFetcher environmentFetcher) { - this.postFinder = postFinder; - this.environmentFetcher = environmentFetcher; - } - - private Mono> postList(ServerRequest request) { - String year = pathVariable(request, "year"); - String month = pathVariable(request, "month"); - String path = request.path(); - return environmentFetcher.fetchPost() - .map(postSetting -> defaultIfNull(postSetting.getArchivePageSize(), DEFAULT_PAGE_SIZE)) - .flatMap(pageSize -> postFinder.archives(pageNum(request), pageSize, year, month)) - .map(list -> new UrlContextListResult.Builder() - .listResult(list) - .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) - .prevUrl(PageUrlUtils.prevPageUrl(path)) - .build()); - } - - private String pathVariable(ServerRequest request, String name) { - Map pathVariables = request.pathVariables(); - if (pathVariables.containsKey(name)) { - return pathVariables.get(name); - } - return null; - } - - @Override - public HandlerFunction getHandler() { - return request -> ServerResponse.ok() - .render(DefaultTemplateEnum.ARCHIVES.getValue(), - Map.of("archives", postList(request))); - } - - @Override - public List getRouterPaths(String prefix) { - return List.of( - StringUtils.prependIfMissing(prefix, "/"), - PathUtils.combinePath(prefix, "/page/{page:\\d+}"), - PathUtils.combinePath(prefix, "/{year:\\d{4}}"), - PathUtils.combinePath(prefix, "/{year:\\d{4}}/page/{page:\\d+}"), - PathUtils.combinePath(prefix, "/{year:\\d{4}}/{month:\\d{2}}"), - PathUtils.combinePath(prefix, - "/{year:\\d{4}}/{month:\\d{2}}/page/{page:\\d+}") - ); - } - - @Override - public boolean supports(DefaultTemplateEnum template) { - return DefaultTemplateEnum.ARCHIVES.equals(template); - } -} diff --git a/src/main/java/run/halo/app/theme/router/strategy/AuthorRouteStrategy.java b/src/main/java/run/halo/app/theme/router/strategy/AuthorRouteStrategy.java deleted file mode 100644 index 0c243a499..000000000 --- a/src/main/java/run/halo/app/theme/router/strategy/AuthorRouteStrategy.java +++ /dev/null @@ -1,76 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static run.halo.app.theme.router.PageUrlUtils.pageNum; -import static run.halo.app.theme.router.PageUrlUtils.totalPage; - -import java.util.Map; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Mono; -import run.halo.app.core.extension.User; -import run.halo.app.extension.GroupVersionKind; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; -import run.halo.app.infra.SystemSetting; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.finders.PostFinder; -import run.halo.app.theme.finders.vo.ListedPostVo; -import run.halo.app.theme.finders.vo.UserVo; -import run.halo.app.theme.router.PageUrlUtils; -import run.halo.app.theme.router.UrlContextListResult; - -/** - * Author route strategy. - * - * @author guqing - * @since 2.0.1 - */ -@Component -@AllArgsConstructor -public class AuthorRouteStrategy implements DetailsPageRouteHandlerStrategy { - - private final ReactiveExtensionClient client; - - private final PostFinder postFinder; - - private final SystemConfigurableEnvironmentFetcher environmentFetcher; - - @Override - public HandlerFunction getHandler(SystemSetting.ThemeRouteRules routeRules, - String name) { - return request -> ServerResponse.ok() - .render(DefaultTemplateEnum.AUTHOR.getValue(), - Map.of("name", name, - "author", getByName(name), - "posts", postList(request, name), - ModelConst.TEMPLATE_ID, DefaultTemplateEnum.AUTHOR.getValue() - ) - ); - } - - private Mono> postList(ServerRequest request, String name) { - String path = request.path(); - return environmentFetcher.fetchPost() - .map(p -> defaultIfNull(p.getPostPageSize(), ModelConst.DEFAULT_PAGE_SIZE)) - .flatMap(pageSize -> postFinder.listByOwner(pageNum(request), pageSize, name)) - .map(list -> new UrlContextListResult.Builder() - .listResult(list) - .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) - .prevUrl(PageUrlUtils.prevPageUrl(path)) - .build()); - } - - private Mono getByName(String name) { - return client.fetch(User.class, name) - .map(UserVo::from); - } - - @Override - public boolean supports(GroupVersionKind gvk) { - return GroupVersionKind.fromExtension(User.class).equals(gvk); - } -} diff --git a/src/main/java/run/halo/app/theme/router/strategy/CategoryRouteStrategy.java b/src/main/java/run/halo/app/theme/router/strategy/CategoryRouteStrategy.java deleted file mode 100644 index 2bda1ee13..000000000 --- a/src/main/java/run/halo/app/theme/router/strategy/CategoryRouteStrategy.java +++ /dev/null @@ -1,83 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static run.halo.app.theme.router.PageUrlUtils.pageNum; -import static run.halo.app.theme.router.PageUrlUtils.totalPage; - -import java.util.HashMap; -import java.util.Map; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Mono; -import run.halo.app.core.extension.content.Category; -import run.halo.app.extension.GroupVersionKind; -import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; -import run.halo.app.infra.SystemSetting; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.finders.CategoryFinder; -import run.halo.app.theme.finders.PostFinder; -import run.halo.app.theme.finders.vo.ListedPostVo; -import run.halo.app.theme.router.PageUrlUtils; -import run.halo.app.theme.router.UrlContextListResult; -import run.halo.app.theme.router.ViewNameResolver; - -/** - * The {@link CategoryRouteStrategy} for generate {@link HandlerFunction} specific to the template - * category.html. - * - * @author guqing - * @since 2.0.0 - */ -@Component -@AllArgsConstructor -public class CategoryRouteStrategy implements DetailsPageRouteHandlerStrategy { - private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Category.class); - private final PostFinder postFinder; - - private final CategoryFinder categoryFinder; - - private final ViewNameResolver viewNameResolver; - - private final SystemConfigurableEnvironmentFetcher environmentFetcher; - - private Mono> postListByCategoryName(String name, - ServerRequest request) { - String path = request.path(); - return environmentFetcher.fetchPost() - .map(post -> defaultIfNull(post.getCategoryPageSize(), ModelConst.DEFAULT_PAGE_SIZE)) - .flatMap( - pageSize -> postFinder.listByCategory(pageNum(request), pageSize, name)) - .map(list -> new UrlContextListResult.Builder() - .listResult(list) - .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) - .prevUrl(PageUrlUtils.prevPageUrl(path)) - .build()); - } - - @Override - public HandlerFunction getHandler(SystemSetting.ThemeRouteRules routeRules, - String name) { - return request -> { - Map model = new HashMap<>(); - model.put("name", name); - model.put("posts", postListByCategoryName(name, request)); - - model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORY.getValue()); - return categoryFinder.getByName(name).flatMap(categoryVo -> { - model.put("category", categoryVo); - String template = categoryVo.getSpec().getTemplate(); - return viewNameResolver.resolveViewNameOrDefault(request, template, - DefaultTemplateEnum.CATEGORY.getValue()) - .flatMap(viewName -> ServerResponse.ok().render(viewName, model)); - }); - }; - } - - @Override - public boolean supports(GroupVersionKind gvk) { - return this.gvk.equals(gvk); - } -} diff --git a/src/main/java/run/halo/app/theme/router/strategy/DetailsPageRouteHandlerStrategy.java b/src/main/java/run/halo/app/theme/router/strategy/DetailsPageRouteHandlerStrategy.java deleted file mode 100644 index ba4bd692b..000000000 --- a/src/main/java/run/halo/app/theme/router/strategy/DetailsPageRouteHandlerStrategy.java +++ /dev/null @@ -1,21 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import run.halo.app.extension.GroupVersionKind; -import run.halo.app.infra.SystemSetting; - -/** - * The {@link DetailsPageRouteHandlerStrategy} for generate {@link HandlerFunction} specific to the - * template. - * - * @author guqing - * @since 2.0.0 - */ -public interface DetailsPageRouteHandlerStrategy { - - HandlerFunction getHandler(SystemSetting.ThemeRouteRules routeRules, - String name); - - boolean supports(GroupVersionKind gvk); -} diff --git a/src/main/java/run/halo/app/theme/router/strategy/ListPageRouteHandlerStrategy.java b/src/main/java/run/halo/app/theme/router/strategy/ListPageRouteHandlerStrategy.java deleted file mode 100644 index 96a6f18a0..000000000 --- a/src/main/java/run/halo/app/theme/router/strategy/ListPageRouteHandlerStrategy.java +++ /dev/null @@ -1,22 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import java.util.List; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import run.halo.app.theme.DefaultTemplateEnum; - -/** - * The {@link ListPageRouteHandlerStrategy} for generate {@link HandlerFunction} specific to the - * template. - * - * @author guqing - * @since 2.0.0 - */ -public interface ListPageRouteHandlerStrategy { - - HandlerFunction getHandler(); - - List getRouterPaths(String pattern); - - boolean supports(DefaultTemplateEnum template); -} diff --git a/src/main/java/run/halo/app/theme/router/strategy/PostRouteStrategy.java b/src/main/java/run/halo/app/theme/router/strategy/PostRouteStrategy.java deleted file mode 100644 index 5440f4a06..000000000 --- a/src/main/java/run/halo/app/theme/router/strategy/PostRouteStrategy.java +++ /dev/null @@ -1,72 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import java.util.HashMap; -import java.util.Map; -import org.springframework.http.server.PathContainer; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import org.springframework.web.util.pattern.PathPattern; -import org.springframework.web.util.pattern.PathPatternParser; -import run.halo.app.core.extension.content.Post; -import run.halo.app.extension.GVK; -import run.halo.app.extension.GroupVersionKind; -import run.halo.app.infra.SystemSetting; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.finders.PostFinder; -import run.halo.app.theme.router.ViewNameResolver; - -/** - * The {@link PostRouteStrategy} for generate {@link HandlerFunction} specific to the template - * post.html. - * - * @author guqing - * @since 2.0.0 - */ -@Component -public class PostRouteStrategy implements DetailsPageRouteHandlerStrategy { - static final String NAME_PARAM = "name"; - private final GroupVersionKind groupVersionKind = GroupVersionKind.fromExtension(Post.class); - private final PostFinder postFinder; - private final ViewNameResolver viewNameResolver; - - public PostRouteStrategy(PostFinder postFinder, ViewNameResolver viewNameResolver) { - this.postFinder = postFinder; - this.viewNameResolver = viewNameResolver; - } - - @Override - public HandlerFunction getHandler(SystemSetting.ThemeRouteRules routeRules, - final String name) { - return request -> { - String pattern = routeRules.getPost(); - final GVK gvk = Post.class.getAnnotation(GVK.class); - PathPattern parse = PathPatternParser.defaultInstance.parse(pattern); - PathPattern.PathMatchInfo pathMatchInfo = - parse.matchAndExtract(PathContainer.parsePath(request.path())); - Map model = new HashMap<>(); - model.put(NAME_PARAM, name); - if (pathMatchInfo != null) { - model.putAll(pathMatchInfo.getUriVariables()); - } - // used by HaloTrackerProcessor - model.put("groupVersionKind", groupVersionKind); - model.put("plural", gvk.plural()); - // used by TemplateGlobalHeadProcessor and PostTemplateHeadProcessor - model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue()); - return postFinder.getByName(name) - .flatMap(postVo -> { - model.put("post", postVo); - String template = postVo.getSpec().getTemplate(); - return viewNameResolver.resolveViewNameOrDefault(request, template, - DefaultTemplateEnum.POST.getValue()) - .flatMap(templateName -> ServerResponse.ok().render(templateName, model)); - }); - }; - } - - @Override - public boolean supports(GroupVersionKind gvk) { - return groupVersionKind.equals(gvk); - } -} diff --git a/src/main/java/run/halo/app/theme/router/strategy/SinglePageRouteStrategy.java b/src/main/java/run/halo/app/theme/router/strategy/SinglePageRouteStrategy.java deleted file mode 100644 index 3085e789f..000000000 --- a/src/main/java/run/halo/app/theme/router/strategy/SinglePageRouteStrategy.java +++ /dev/null @@ -1,64 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import java.util.HashMap; -import java.util.Map; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import run.halo.app.core.extension.content.SinglePage; -import run.halo.app.extension.GVK; -import run.halo.app.extension.GroupVersionKind; -import run.halo.app.infra.SystemSetting; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.finders.SinglePageFinder; -import run.halo.app.theme.router.ViewNameResolver; - -/** - * The {@link SinglePageRouteStrategy} for generate {@link HandlerFunction} specific to the template - * page.html. - * - * @author guqing - * @since 2.0.0 - */ -@Component -public class SinglePageRouteStrategy implements DetailsPageRouteHandlerStrategy { - private final GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class); - private final SinglePageFinder singlePageFinder; - private final ViewNameResolver viewNameResolver; - - public SinglePageRouteStrategy(SinglePageFinder singlePageFinder, - ViewNameResolver viewNameResolver) { - this.singlePageFinder = singlePageFinder; - this.viewNameResolver = viewNameResolver; - } - - private String getPlural() { - GVK annotation = SinglePage.class.getAnnotation(GVK.class); - return annotation.plural(); - } - - @Override - public HandlerFunction getHandler(SystemSetting.ThemeRouteRules routeRules, - String name) { - return request -> { - Map model = new HashMap<>(); - model.put("name", name); - model.put("groupVersionKind", gvk); - model.put("plural", getPlural()); - model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue()); - - return singlePageFinder.getByName(name).flatMap(singlePageVo -> { - model.put("singlePage", singlePageVo); - String template = singlePageVo.getSpec().getTemplate(); - return viewNameResolver.resolveViewNameOrDefault(request, template, - DefaultTemplateEnum.SINGLE_PAGE.getValue()) - .flatMap(viewName -> ServerResponse.ok().render(viewName, model)); - }); - }; - } - - @Override - public boolean supports(GroupVersionKind gvk) { - return this.gvk.equals(gvk); - } -} diff --git a/src/main/java/run/halo/app/theme/router/strategy/TagRouteStrategy.java b/src/main/java/run/halo/app/theme/router/strategy/TagRouteStrategy.java deleted file mode 100644 index c0572ceef..000000000 --- a/src/main/java/run/halo/app/theme/router/strategy/TagRouteStrategy.java +++ /dev/null @@ -1,72 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static run.halo.app.theme.router.PageUrlUtils.pageNum; -import static run.halo.app.theme.router.PageUrlUtils.totalPage; - -import java.util.Map; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Mono; -import run.halo.app.core.extension.content.Tag; -import run.halo.app.extension.GroupVersionKind; -import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; -import run.halo.app.infra.SystemSetting; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.finders.PostFinder; -import run.halo.app.theme.finders.TagFinder; -import run.halo.app.theme.finders.vo.ListedPostVo; -import run.halo.app.theme.router.PageUrlUtils; -import run.halo.app.theme.router.UrlContextListResult; - -/** - * The {@link TagRouteStrategy} for generate {@link RouterFunction} specific to the template - * tag.html. - * - * @author guqing - * @since 2.0.0 - */ -@Component -@AllArgsConstructor -public class TagRouteStrategy implements DetailsPageRouteHandlerStrategy { - private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Tag.class); - private final PostFinder postFinder; - - private final TagFinder tagFinder; - - private final SystemConfigurableEnvironmentFetcher environmentFetcher; - - private Mono> postList(ServerRequest request, String name) { - String path = request.path(); - return environmentFetcher.fetchPost() - .map(p -> defaultIfNull(p.getTagPageSize(), ModelConst.DEFAULT_PAGE_SIZE)) - .flatMap(pageSize -> postFinder.listByTag(pageNum(request), pageSize, name)) - .map(list -> new UrlContextListResult.Builder() - .listResult(list) - .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) - .prevUrl(PageUrlUtils.prevPageUrl(path)) - .build()); - } - - @Override - public HandlerFunction getHandler(SystemSetting.ThemeRouteRules routeRules, - String name) { - return request -> ServerResponse.ok() - .render(DefaultTemplateEnum.TAG.getValue(), - Map.of("name", name, - "posts", postList(request, name), - "tag", tagFinder.getByName(name), - ModelConst.TEMPLATE_ID, DefaultTemplateEnum.TAG.getValue() - ) - ); - } - - @Override - public boolean supports(GroupVersionKind gvk) { - return this.gvk.equals(gvk); - } -} diff --git a/src/main/java/run/halo/app/theme/router/strategy/TagsRouteStrategy.java b/src/main/java/run/halo/app/theme/router/strategy/TagsRouteStrategy.java deleted file mode 100644 index d669a238b..000000000 --- a/src/main/java/run/halo/app/theme/router/strategy/TagsRouteStrategy.java +++ /dev/null @@ -1,47 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import java.util.List; -import java.util.Map; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.finders.TagFinder; - -/** - * The {@link TagsRouteStrategy} for generate {@link HandlerFunction} specific to the template - * tags.html. - * - * @author guqing - * @since 2.0.0 - */ -@Component -public class TagsRouteStrategy implements ListPageRouteHandlerStrategy { - - private final TagFinder tagFinder; - - public TagsRouteStrategy(TagFinder tagFinder) { - this.tagFinder = tagFinder; - } - - @Override - public HandlerFunction getHandler() { - return request -> ServerResponse.ok() - .render(DefaultTemplateEnum.TAGS.getValue(), - Map.of("tags", tagFinder.listAll(), - ModelConst.TEMPLATE_ID, DefaultTemplateEnum.TAGS.getValue() - ) - ); - } - - @Override - public List getRouterPaths(String prefix) { - return List.of(StringUtils.prependIfMissing(prefix, "/")); - } - - @Override - public boolean supports(DefaultTemplateEnum template) { - return DefaultTemplateEnum.TAGS.equals(template); - } -} diff --git a/src/test/java/run/halo/app/content/permalinks/CategoryPermalinkPolicyTest.java b/src/test/java/run/halo/app/content/permalinks/CategoryPermalinkPolicyTest.java index 645f90629..fbd1e1ac7 100644 --- a/src/test/java/run/halo/app/content/permalinks/CategoryPermalinkPolicyTest.java +++ b/src/test/java/run/halo/app/content/permalinks/CategoryPermalinkPolicyTest.java @@ -1,7 +1,6 @@ package run.halo.app.content.permalinks; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import java.net.URI; @@ -10,12 +9,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.context.ApplicationContext; import run.halo.app.core.extension.content.Category; import run.halo.app.extension.Metadata; import run.halo.app.infra.ExternalUrlSupplier; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.router.PermalinkPatternProvider; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; /** * Tests for {@link CategoryPermalinkPolicy}. @@ -26,28 +23,22 @@ import run.halo.app.theme.router.PermalinkPatternProvider; @ExtendWith(MockitoExtension.class) class CategoryPermalinkPolicyTest { - @Mock - private PermalinkPatternProvider permalinkPatternProvider; - - @Mock - private ApplicationContext applicationContext; - @Mock private ExternalUrlSupplier externalUrlSupplier; + @Mock + private SystemConfigurableEnvironmentFetcher environmentFetcher; + private CategoryPermalinkPolicy categoryPermalinkPolicy; @BeforeEach void setUp() { categoryPermalinkPolicy = - new CategoryPermalinkPolicy(applicationContext, permalinkPatternProvider, - externalUrlSupplier); + new CategoryPermalinkPolicy(externalUrlSupplier, environmentFetcher); } @Test void permalink() { - when(permalinkPatternProvider.getPattern(eq(DefaultTemplateEnum.CATEGORY))) - .thenReturn("categories"); Category category = new Category(); Metadata metadata = new Metadata(); metadata.setName("category-test"); @@ -70,18 +61,4 @@ class CategoryPermalinkPolicyTest { permalink = categoryPermalinkPolicy.permalink(category); assertThat(permalink).isEqualTo("http://exmaple.com/categories/%E4%B8%AD%E6%96%87%20slug"); } - - @Test - void templateName() { - String s = categoryPermalinkPolicy.templateName(); - assertThat(s).isEqualTo(DefaultTemplateEnum.CATEGORY.getValue()); - } - - @Test - void pattern() { - when(permalinkPatternProvider.getPattern(eq(DefaultTemplateEnum.CATEGORY))) - .thenReturn("categories"); - String pattern = categoryPermalinkPolicy.pattern(); - assertThat(pattern).isEqualTo("categories"); - } } \ No newline at end of file diff --git a/src/test/java/run/halo/app/content/permalinks/PostPermalinkPolicyTest.java b/src/test/java/run/halo/app/content/permalinks/PostPermalinkPolicyTest.java index 43cd41173..d3ca4cef9 100644 --- a/src/test/java/run/halo/app/content/permalinks/PostPermalinkPolicyTest.java +++ b/src/test/java/run/halo/app/content/permalinks/PostPermalinkPolicyTest.java @@ -10,6 +10,7 @@ import java.text.NumberFormat; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -17,11 +18,12 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationContext; import run.halo.app.content.TestPost; +import run.halo.app.core.extension.content.Constant; import run.halo.app.core.extension.content.Post; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.infra.ExternalUrlSupplier; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.utils.PathUtils; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.router.PermalinkPatternProvider; /** * Tests for {@link PostPermalinkPolicy}. @@ -33,27 +35,28 @@ import run.halo.app.theme.router.PermalinkPatternProvider; class PostPermalinkPolicyTest { private static final NumberFormat NUMBER_FORMAT = new DecimalFormat("00"); - @Mock - private PermalinkPatternProvider permalinkPatternProvider; - @Mock private ApplicationContext applicationContext; @Mock private ExternalUrlSupplier externalUrlSupplier; + @Mock + private SystemConfigurableEnvironmentFetcher environmentFetcher; + private PostPermalinkPolicy postPermalinkPolicy; @BeforeEach void setUp() { lenient().when(externalUrlSupplier.get()).thenReturn(URI.create("")); - postPermalinkPolicy = new PostPermalinkPolicy(permalinkPatternProvider, applicationContext, - externalUrlSupplier); + postPermalinkPolicy = new PostPermalinkPolicy(environmentFetcher, externalUrlSupplier); } @Test void permalink() { Post post = TestPost.postV1(); + Map annotations = ExtensionUtil.nullSafeAnnotations(post); + annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/{year}/{month}/{day}/{slug}"); post.getMetadata().setName("test-post"); post.getSpec().setSlug("test-post-slug"); Instant now = Instant.now(); @@ -64,35 +67,28 @@ class PostPermalinkPolicyTest { String month = NUMBER_FORMAT.format(zonedDateTime.getMonthValue()); String day = NUMBER_FORMAT.format(zonedDateTime.getDayOfMonth()); - // pattern /{year}/{month}/{day}/{slug} - when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST)) - .thenReturn("/{year}/{month}/{day}/{slug}"); String permalink = postPermalinkPolicy.permalink(post); assertThat(permalink) .isEqualTo(PathUtils.combinePath(year, month, day, post.getSpec().getSlug())); // pattern {month}/{day}/{slug} - when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST)) - .thenReturn("/{month}/{day}/{slug}"); + annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/{month}/{day}/{slug}"); permalink = postPermalinkPolicy.permalink(post); assertThat(permalink) .isEqualTo(PathUtils.combinePath(month, day, post.getSpec().getSlug())); // pattern /?p={name} - when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST)) - .thenReturn("/?p={name}"); + annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/?p={name}"); permalink = postPermalinkPolicy.permalink(post); assertThat(permalink).isEqualTo("/?p=test-post"); // pattern /posts/{slug} - when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST)) - .thenReturn("/posts/{slug}"); + annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/posts/{slug}"); permalink = postPermalinkPolicy.permalink(post); assertThat(permalink).isEqualTo("/posts/test-post-slug"); // pattern /posts/{name} - when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST)) - .thenReturn("/posts/{name}"); + annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/posts/{name}"); permalink = postPermalinkPolicy.permalink(post); assertThat(permalink).isEqualTo("/posts/test-post"); } @@ -100,6 +96,8 @@ class PostPermalinkPolicyTest { @Test void permalinkWithExternalUrl() { Post post = TestPost.postV1(); + Map annotations = ExtensionUtil.nullSafeAnnotations(post); + annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/{year}/{month}/{day}/{slug}"); post.getMetadata().setName("test-post"); post.getSpec().setSlug("test-post-slug"); Instant now = Instant.parse("2022-11-01T02:40:06.806310Z"); @@ -107,8 +105,6 @@ class PostPermalinkPolicyTest { when(externalUrlSupplier.get()).thenReturn(URI.create("http://example.com")); - when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST)) - .thenReturn("/{year}/{month}/{day}/{slug}"); String permalink = postPermalinkPolicy.permalink(post); assertThat(permalink).isEqualTo("http://example.com/2022/11/01/test-post-slug"); @@ -116,17 +112,4 @@ class PostPermalinkPolicyTest { permalink = postPermalinkPolicy.permalink(post); assertThat(permalink).isEqualTo("http://example.com/2022/11/01/%E4%B8%AD%E6%96%87%20slug"); } - - @Test - void templateName() { - String s = postPermalinkPolicy.templateName(); - assertThat(s).isEqualTo(DefaultTemplateEnum.POST.getValue()); - } - - @Test - void pattern() { - when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST)) - .thenReturn("/{year}/{month}/{day}/{slug}"); - assertThat(postPermalinkPolicy.pattern()).isEqualTo("/{year}/{month}/{day}/{slug}"); - } } \ No newline at end of file diff --git a/src/test/java/run/halo/app/content/permalinks/TagPermalinkPolicyTest.java b/src/test/java/run/halo/app/content/permalinks/TagPermalinkPolicyTest.java index c31751b0d..cd40a75de 100644 --- a/src/test/java/run/halo/app/content/permalinks/TagPermalinkPolicyTest.java +++ b/src/test/java/run/halo/app/content/permalinks/TagPermalinkPolicyTest.java @@ -1,7 +1,6 @@ package run.halo.app.content.permalinks; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import java.net.URI; @@ -14,8 +13,7 @@ import org.springframework.context.ApplicationContext; import run.halo.app.core.extension.content.Tag; import run.halo.app.extension.Metadata; import run.halo.app.infra.ExternalUrlSupplier; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.router.PermalinkPatternProvider; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; /** * Tests for {@link TagPermalinkPolicy}. @@ -26,28 +24,24 @@ import run.halo.app.theme.router.PermalinkPatternProvider; @ExtendWith(MockitoExtension.class) class TagPermalinkPolicyTest { - @Mock - private PermalinkPatternProvider permalinkPatternProvider; - @Mock private ApplicationContext applicationContext; @Mock private ExternalUrlSupplier externalUrlSupplier; + @Mock + private SystemConfigurableEnvironmentFetcher environmentFetcher; + private TagPermalinkPolicy tagPermalinkPolicy; @BeforeEach void setUp() { - tagPermalinkPolicy = new TagPermalinkPolicy(permalinkPatternProvider, applicationContext, - externalUrlSupplier); + tagPermalinkPolicy = new TagPermalinkPolicy(externalUrlSupplier, environmentFetcher); } @Test void permalink() { - when(permalinkPatternProvider.getPattern(eq(DefaultTemplateEnum.TAG))) - .thenReturn("tags"); - Tag tag = new Tag(); Metadata metadata = new Metadata(); metadata.setName("test-tag"); @@ -70,17 +64,4 @@ class TagPermalinkPolicyTest { permalink = tagPermalinkPolicy.permalink(tag); assertThat(permalink).isEqualTo("http://example.com/tags/%E4%B8%AD%E6%96%87slug"); } - - @Test - void templateName() { - String s = tagPermalinkPolicy.templateName(); - assertThat(s).isEqualTo(DefaultTemplateEnum.TAG.getValue()); - } - - @Test - void pattern() { - when(permalinkPatternProvider.getPattern(eq(DefaultTemplateEnum.TAG))) - .thenReturn("tags"); - assertThat(tagPermalinkPolicy.pattern()).isEqualTo("tags"); - } } \ No newline at end of file diff --git a/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java index ad4321d59..edfd2854d 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java @@ -47,7 +47,7 @@ class CategoryReconcilerTest { reconcileStatusPostPilling("category-A"); ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class); - verify(client, times(2)).update(captor.capture()); + 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); @@ -57,7 +57,7 @@ class CategoryReconcilerTest { void reconcileStatusPostForCategoryB() throws JSONException { reconcileStatusPostPilling("category-B"); ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class); - verify(client, times(2)).update(captor.capture()); + 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); @@ -67,7 +67,7 @@ class CategoryReconcilerTest { void reconcileStatusPostForCategoryC() throws JSONException { reconcileStatusPostPilling("category-C"); ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class); - verify(client, times(2)).update(captor.capture()); + 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); @@ -77,7 +77,7 @@ class CategoryReconcilerTest { void reconcileStatusPostForCategoryD() throws JSONException { reconcileStatusPostPilling("category-D"); ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class); - verify(client, times(2)).update(captor.capture()); + 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); } diff --git a/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java index c5e13bf4c..01e0ef3f4 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java @@ -80,9 +80,6 @@ class PostReconcilerTest { verify(client, times(3)).update(captor.capture()); verify(postPermalinkPolicy, times(1)).permalink(any()); - verify(postPermalinkPolicy, times(1)).onPermalinkAdd(any()); - verify(postPermalinkPolicy, times(1)).onPermalinkDelete(any()); - verify(postPermalinkPolicy, times(0)).onPermalinkUpdate(any()); Post value = captor.getValue(); assertThat(value.getStatus().getExcerpt()).isNull(); diff --git a/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java index 6d58cc73a..68258e017 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java @@ -3,7 +3,6 @@ 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.ArgumentMatchers.isA; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -34,9 +33,6 @@ import run.halo.app.extension.Metadata; import run.halo.app.extension.controller.Reconciler; import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.metrics.CounterService; -import run.halo.app.theme.router.PermalinkIndexAddCommand; -import run.halo.app.theme.router.PermalinkIndexDeleteCommand; -import run.halo.app.theme.router.PermalinkIndexUpdateCommand; /** * Tests for {@link SinglePageReconciler}. @@ -97,10 +93,6 @@ class SinglePageReconcilerTest { SinglePage value = captor.getValue(); assertThat(value.getStatus().getExcerpt()).isEqualTo("hello world"); assertThat(value.getStatus().getContributors()).isEqualTo(List.of("guqing", "zhangsan")); - - verify(applicationContext, times(0)).publishEvent(isA(PermalinkIndexAddCommand.class)); - verify(applicationContext, times(1)).publishEvent(isA(PermalinkIndexDeleteCommand.class)); - verify(applicationContext, times(0)).publishEvent(isA(PermalinkIndexUpdateCommand.class)); } @Test diff --git a/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java index 2edaa3ddd..a7211e76b 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java @@ -40,6 +40,28 @@ class TagReconcilerTest { @InjectMocks private TagReconciler tagReconciler; + @Test + void reconcile() { + Tag tag = tag(); + when(client.fetch(eq(Tag.class), eq("fake-tag"))) + .thenReturn(Optional.of(tag)); + when(tagPermalinkPolicy.permalink(any())) + .thenAnswer(arg -> "/tags/" + tag.getSpec().getSlug()); + ArgumentCaptor captor = ArgumentCaptor.forClass(Tag.class); + + tagReconciler.reconcile(new TagReconciler.Request("fake-tag")); + + verify(client, times(3)).update(captor.capture()); + Tag capture = captor.getValue(); + assertThat(capture.getStatus().getPermalink()).isEqualTo("/tags/fake-slug"); + + // change slug + tag.getSpec().setSlug("new-slug"); + tagReconciler.reconcile(new TagReconciler.Request("fake-tag")); + verify(client, times(5)).update(captor.capture()); + assertThat(capture.getStatus().getPermalink()).isEqualTo("/tags/new-slug"); + } + @Test void reconcileDelete() { Tag tag = tag(); @@ -50,8 +72,6 @@ class TagReconcilerTest { tagReconciler.reconcile(new TagReconciler.Request("fake-tag")); verify(client, times(1)).update(captor.capture()); - verify(tagPermalinkPolicy, times(0)).onPermalinkAdd(any()); - verify(tagPermalinkPolicy, times(1)).onPermalinkDelete(any()); verify(tagPermalinkPolicy, times(0)).permalink(any()); } diff --git a/src/test/java/run/halo/app/core/extension/reconciler/UserReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/UserReconcilerTest.java index fc11fbe46..57d58f5ff 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/UserReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/UserReconcilerTest.java @@ -24,7 +24,6 @@ import run.halo.app.extension.Metadata; import run.halo.app.extension.controller.Reconciler; import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.ExternalUrlSupplier; -import run.halo.app.theme.router.PermalinkIndexUpdateCommand; /** * Tests for {@link UserReconciler}. @@ -54,7 +53,6 @@ class UserReconcilerTest { .thenReturn(Optional.of(user("fake-user"))); userReconciler.reconcile(new Reconciler.Request("fake-user")); verify(client, times(1)).update(any(User.class)); - verify(eventPublisher, times(1)).publishEvent(any(PermalinkIndexUpdateCommand.class)); ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); verify(client, times(1)).update(captor.capture()); @@ -68,7 +66,6 @@ class UserReconcilerTest { .thenReturn(Optional.of(user(AnonymousUserConst.PRINCIPAL))); userReconciler.reconcile(new Reconciler.Request(AnonymousUserConst.PRINCIPAL)); verify(client, times(0)).update(any(User.class)); - verify(eventPublisher, times(0)).publishEvent(any(PermalinkIndexUpdateCommand.class)); } User user(String name) { diff --git a/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java b/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java index adc1c04d0..30ea61275 100644 --- a/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java +++ b/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java @@ -38,7 +38,7 @@ import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.SinglePageFinder; import run.halo.app.theme.finders.vo.PostVo; import run.halo.app.theme.finders.vo.UserVo; -import run.halo.app.theme.router.strategy.ModelConst; +import run.halo.app.theme.router.factories.ModelConst; /** * Tests for {@link HaloProcessorDialect}. diff --git a/src/test/java/run/halo/app/theme/finders/impl/SinglePageFinderImplTest.java b/src/test/java/run/halo/app/theme/finders/impl/SinglePageFinderImplTest.java index 3ad3a3e01..daab2ca40 100644 --- a/src/test/java/run/halo/app/theme/finders/impl/SinglePageFinderImplTest.java +++ b/src/test/java/run/halo/app/theme/finders/impl/SinglePageFinderImplTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -15,6 +16,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import run.halo.app.content.SinglePageService; +import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.SinglePage; import run.halo.app.extension.Metadata; import run.halo.app.extension.ReactiveExtensionClient; @@ -52,9 +54,13 @@ class SinglePageFinderImplTest { SinglePage singlePage = new SinglePage(); singlePage.setMetadata(new Metadata()); singlePage.getMetadata().setName(fakePageName); + singlePage.getMetadata().setLabels(Map.of(SinglePage.PUBLISHED_LABEL, "true")); singlePage.setSpec(new SinglePage.SinglePageSpec()); singlePage.getSpec().setOwner("fake-owner"); singlePage.getSpec().setReleaseSnapshot("fake-release"); + singlePage.getSpec().setPublish(true); + singlePage.getSpec().setDeleted(false); + singlePage.getSpec().setVisible(Post.VisibleEnum.PUBLIC); singlePage.setStatus(new SinglePage.SinglePageStatus()); when(client.fetch(eq(SinglePage.class), eq(fakePageName))) .thenReturn(Mono.just(singlePage)); diff --git a/src/test/java/run/halo/app/theme/router/strategy/EmptyView.java b/src/test/java/run/halo/app/theme/router/EmptyView.java similarity index 92% rename from src/test/java/run/halo/app/theme/router/strategy/EmptyView.java rename to src/test/java/run/halo/app/theme/router/EmptyView.java index 837ffb1fc..ba4a0bfc2 100644 --- a/src/test/java/run/halo/app/theme/router/strategy/EmptyView.java +++ b/src/test/java/run/halo/app/theme/router/EmptyView.java @@ -1,4 +1,4 @@ -package run.halo.app.theme.router.strategy; +package run.halo.app.theme.router; import java.util.Map; import org.springframework.http.MediaType; diff --git a/src/test/java/run/halo/app/theme/router/PermalinkIndexerTest.java b/src/test/java/run/halo/app/theme/router/PermalinkIndexerTest.java deleted file mode 100644 index cbe9ed3dc..000000000 --- a/src/test/java/run/halo/app/theme/router/PermalinkIndexerTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package run.halo.app.theme.router; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import java.util.List; -import java.util.NoSuchElementException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.context.ApplicationContext; -import run.halo.app.content.permalinks.ExtensionLocator; -import run.halo.app.extension.FakeExtension; -import run.halo.app.extension.GroupVersionKind; - -/** - * Tests for {@link PermalinkIndexer}. - * - * @author guqing - * @since 2.0.0 - */ -@ExtendWith(MockitoExtension.class) -class PermalinkIndexerTest { - - private final GroupVersionKind gvk = GroupVersionKind.fromExtension(FakeExtension.class); - - private PermalinkIndexer permalinkIndexer; - @Mock - private ApplicationContext applicationContext; - - @BeforeEach - void setUp() { - permalinkIndexer = new PermalinkIndexer(applicationContext); - - ExtensionLocator locator = new ExtensionLocator(gvk, "fake-name", "fake-slug"); - permalinkIndexer.register(locator, "/fake-permalink"); - assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1); - assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1); - } - - @Test - void register() { - ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug"); - permalinkIndexer.register(locator, "/test-permalink"); - verify(applicationContext, times(2)).publishEvent(any(PermalinkIndexChangedEvent.class)); - - assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(2); - assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(2); - } - - @Test - void remove() { - ExtensionLocator locator = new ExtensionLocator(gvk, "fake-name", "fake-slug"); - - assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1); - assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1); - - permalinkIndexer.remove(locator); - assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(0); - assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(0); - - verify(applicationContext, times(2)).publishEvent(any(PermalinkIndexChangedEvent.class)); - } - - @Test - void lookup() { - ExtensionLocator lookup = permalinkIndexer.lookup("/fake-permalink"); - assertThat(lookup).isEqualTo(new ExtensionLocator(gvk, "fake-name", "fake-slug")); - - lookup = permalinkIndexer.lookup("/nothing"); - assertThat(lookup).isNull(); - } - - @Test - void getPermalinks() { - ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug"); - permalinkIndexer.register(locator, "/test-permalink"); - - List permalinks = permalinkIndexer.getPermalinks(gvk); - assertThat(permalinks).isEqualTo(List.of("/fake-permalink", "/test-permalink")); - } - - @Test - void getNames() { - ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug"); - permalinkIndexer.register(locator, "/test-permalink"); - - assertThat(permalinkIndexer.containsName(gvk, "test-name")).isTrue(); - assertThat(permalinkIndexer.containsName(gvk, "nothing")).isFalse(); - } - - @Test - void getSlugs() { - ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug"); - permalinkIndexer.register(locator, "/test-permalink"); - - assertThat(permalinkIndexer.containsSlug(gvk, "fake-slug")).isTrue(); - assertThat(permalinkIndexer.containsSlug(gvk, "test-slug")).isTrue(); - assertThat(permalinkIndexer.containsSlug(gvk, "nothing")).isFalse(); - } - - @Test - void getNameBySlug() { - ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug"); - permalinkIndexer.register(locator, "/test-permalink"); - - String nameBySlug = permalinkIndexer.getNameBySlug(gvk, "test-slug"); - assertThat(nameBySlug).isEqualTo("test-name"); - - nameBySlug = permalinkIndexer.getNameBySlug(gvk, "fake-slug"); - assertThat(nameBySlug).isEqualTo("fake-name"); - - assertThatThrownBy(() -> { - permalinkIndexer.getNameBySlug(gvk, "nothing"); - }).isInstanceOf(NoSuchElementException.class); - } - - @Test - void getNameByPermalink() { - ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug"); - permalinkIndexer.register(locator, "/test-permalink"); - - var name = permalinkIndexer.getNameByPermalink(gvk, "/test-permalink"); - assertEquals("test-name", name); - - name = permalinkIndexer.getNameByPermalink(gvk, "/invalid-permalink"); - assertNull(name); - } -} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/PermalinkPatternProviderTest.java b/src/test/java/run/halo/app/theme/router/PermalinkPatternProviderTest.java deleted file mode 100644 index 0cbb6b9e7..000000000 --- a/src/test/java/run/halo/app/theme/router/PermalinkPatternProviderTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package run.halo.app.theme.router; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -import java.util.Map; -import java.util.Optional; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import run.halo.app.extension.ConfigMap; -import run.halo.app.extension.Metadata; -import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; -import run.halo.app.infra.SystemSetting; -import run.halo.app.infra.utils.JsonUtils; -import run.halo.app.theme.DefaultTemplateEnum; - -/** - * Tests for {@link PermalinkPatternProvider}. - * - * @author guqing - * @since 2.0.0 - */ -@ExtendWith(MockitoExtension.class) -class PermalinkPatternProviderTest { - - @Mock - private SystemConfigurableEnvironmentFetcher environmentFetcher; - - @InjectMocks - private PermalinkPatternProvider permalinkPatternProvider; - - @Test - void getPatternThenDefault() { - when(environmentFetcher.getConfigMapBlocking()) - .thenReturn(Optional.empty()); - - String pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST); - assertThat(pattern).isEqualTo("/archives/{slug}"); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAG); - assertThat(pattern).isEqualTo("tags"); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAGS); - assertThat(pattern).isEqualTo("tags"); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.CATEGORY); - assertThat(pattern).isEqualTo("categories"); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.CATEGORIES); - assertThat(pattern).isEqualTo("categories"); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.ARCHIVES); - assertThat(pattern).isEqualTo("archives"); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.INDEX); - assertThat(pattern).isNull(); - } - - @Test - void getPattern() { - ConfigMap configMap = new ConfigMap(); - Metadata metadata = new Metadata(); - metadata.setName("system"); - configMap.setMetadata(metadata); - - SystemSetting.ThemeRouteRules themeRouteRules = new SystemSetting.ThemeRouteRules(); - themeRouteRules.setPost("/posts/{slug}"); - themeRouteRules.setCategories("c"); - themeRouteRules.setTags("t"); - themeRouteRules.setArchives("a"); - - configMap.setData(Map.of("routeRules", JsonUtils.objectToJson(themeRouteRules))); - - when(environmentFetcher.getConfigMapBlocking()) - .thenReturn(Optional.of(configMap)); - - String pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST); - assertThat(pattern).isEqualTo(themeRouteRules.getPost()); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAG); - assertThat(pattern).isEqualTo(themeRouteRules.getTags()); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAGS); - assertThat(pattern).isEqualTo(themeRouteRules.getTags()); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.CATEGORY); - assertThat(pattern).isEqualTo(themeRouteRules.getCategories()); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.CATEGORIES); - assertThat(pattern).isEqualTo(themeRouteRules.getCategories()); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.ARCHIVES); - assertThat(pattern).isEqualTo(themeRouteRules.getArchives()); - - pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.INDEX); - assertThat(pattern).isNull(); - } -} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/RadixRouterTreeTest.java b/src/test/java/run/halo/app/theme/router/RadixRouterTreeTest.java deleted file mode 100644 index a13c767e7..000000000 --- a/src/test/java/run/halo/app/theme/router/RadixRouterTreeTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package run.halo.app.theme.router; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.net.URI; -import java.net.URISyntaxException; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpMethod; -import org.springframework.mock.web.reactive.function.server.MockServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; - -/** - * Tests for {@link RadixRouterTree}. - * - * @author guqing - * @since 2.0.0 - */ -class RadixRouterTreeTest { - - @Test - void pathToFind() throws URISyntaxException { - MockServerRequest request = - MockServerRequest.builder().uri(new URI("/archives")) - .method(HttpMethod.GET).build(); - String path = RadixRouterTree.pathToFind(request); - assertThat(path).isEqualTo("/archives"); - - request = MockServerRequest.builder().uri(new URI("/archives/")) - .method(HttpMethod.GET).build(); - assertThat(RadixRouterTree.pathToFind(request)).isEqualTo("/archives"); - - request = MockServerRequest.builder().uri(new URI("/archives/page/1")) - .method(HttpMethod.GET).build(); - assertThat(RadixRouterTree.pathToFind(request)).isEqualTo("/archives"); - - request = MockServerRequest.builder().uri(new URI("/")) - .method(HttpMethod.GET).build(); - assertThat(RadixRouterTree.pathToFind(request)).isEqualTo("/"); - - request = MockServerRequest.builder().uri(new URI("/")) - .queryParam("p", "fake-post") - .method(HttpMethod.GET).build(); - assertThat(RadixRouterTree.pathToFind(request)).isEqualTo("/?p=fake-post"); - } - - @Test - void shouldInsertKeyWithPercentSign() { - var tree = new RadixRouterTree(); - tree.insert("/1%1", request -> ServerResponse.ok().build()); - } -} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/RadixTreeTest.java b/src/test/java/run/halo/app/theme/router/RadixTreeTest.java deleted file mode 100644 index e4bf19f9a..000000000 --- a/src/test/java/run/halo/app/theme/router/RadixTreeTest.java +++ /dev/null @@ -1,267 +0,0 @@ -package run.halo.app.theme.router; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link RadixTree}. - * - * @author guqing - * @since 2.0.0 - */ -class RadixTreeTest { - - @Test - void insert() { - RadixTree radixTree = new RadixTree<>(); - radixTree.insert("/users", "users"); - radixTree.insert("/users/a", "users-a"); - radixTree.insert("/users/a/b/c", "users-a-b-c"); - radixTree.insert("/users/a/b/c/d", "users-a-b-c-d"); - radixTree.insert("/users/a/b/e/f", "users-a-b-c-d"); - radixTree.insert("/users/b/d", "users-b-d"); - radixTree.insert("/users/b/d/e/f", "users-b-d-e-f"); - radixTree.insert("/users/b/d/g/h", "users-b-d-g-h"); - radixTree.insert("/users/b/f/g/h", "users-b-f-g-h"); - radixTree.insert("/users/c/d/g/h", "users-c-d-g-h"); - radixTree.insert("/users/c/f/g/h", "users-c-f-g-h"); - radixTree.insert("/test/hello", "test-hello"); - radixTree.insert("/test/中文/abc", "test-中文-abc"); - radixTree.insert("/test/中/test", "test-中-test"); - - radixTree.checkIndices(); - String display = radixTree.display(); - assertThat(display).isEqualTo(""" - / [indices=ut] - ├── users [value=users]* - │ └── / [indices=abc] - │ ├── a [value=users-a]* - │ │ └── /b/ [indices=ce] - │ │ ├── c [value=users-a-b-c]* - │ │ │ └── /d [value=users-a-b-c-d]* - │ │ └── e/f [value=users-a-b-c-d]* - │ ├── b/ [indices=df] - │ │ ├── d [value=users-b-d]* - │ │ │ └── / [indices=eg] - │ │ │ ├── e/f [value=users-b-d-e-f]* - │ │ │ └── g/h [value=users-b-d-g-h]* - │ │ └── f/g/h [value=users-b-f-g-h]* - │ └── c/ [indices=df] - │ ├── d/g/h [value=users-c-d-g-h]* - │ └── f/g/h [value=users-c-f-g-h]* - └── test/ [indices=h中] - ├── hello [value=test-hello]* - └── 中 [indices=文/] - ├── 文/abc [value=test-中文-abc]* - └── /test [value=test-中-test]* - """); - } - - @Test - void delete() { - RadixTree radixTree = new RadixTree<>(); - radixTree.insert("/", "index"); - radixTree.insert("/categories/default", "categories-default"); - radixTree.insert("/tags/halo", "tags-halo"); - radixTree.insert("/archives/hello-halo", "archives-hello-halo"); - radixTree.insert("/about", "about"); - radixTree.delete("/tags/halo"); - radixTree.delete("/archives/hello-halo"); - radixTree.insert("/tags/halo", "tags-halo"); - radixTree.delete("/"); - - String display = radixTree.display(); - assertThat(display).isEqualTo(""" - / [indices=cat] - ├── categories/default [value=categories-default]* - ├── about [value=about]* - └── tags/halo [value=tags-halo]* - """); - - radixTree.checkIndices(); - } - - @Test - void getSize() { - RadixTree radixTree = new RadixTree<>(); - radixTree.insert("/", "index"); - radixTree.insert("/categories/default", "categories-default"); - radixTree.insert("/tags/halo", "tags-halo"); - radixTree.insert("/archives/hello-halo", "archives-hello-halo"); - - assertThat(radixTree.getSize()).isEqualTo(4); - - radixTree.insert("/about", "about"); - radixTree.delete("/tags/halo"); - assertThat(radixTree.getSize()).isEqualTo(4); - - radixTree.delete("/archives/hello-halo"); - radixTree.insert("/tags/halo", "tags-halo"); - radixTree.delete("/"); - assertThat(radixTree.getSize()).isEqualTo(3); - } - - @Test - void contains() { - RadixTree radixTree = new RadixTree<>(); - radixTree.insert("/", "index"); - radixTree.insert("/categories/default", "categories-default"); - radixTree.insert("/tags/halo", "tags-halo"); - radixTree.insert("/archives/hello-halo", "archives-hello-halo"); - - assertThat(radixTree.contains("/tags/halo")).isTrue(); - assertThat(radixTree.contains("/archives/hello-halo")).isTrue(); - assertThat(radixTree.contains("/categories/default")).isTrue(); - - assertThat(radixTree.contains("/tags/test")).isFalse(); - assertThat(radixTree.contains("/tags/abc")).isFalse(); - assertThat(radixTree.contains("/archives/abc")).isFalse(); - assertThat(radixTree.contains("/archives")).isFalse(); - } - - @Test - void replace() { - RadixTree radixTree = new RadixTree<>(); - radixTree.insert("/", "index"); - radixTree.insert("/categories/default", "categories-default"); - - boolean replaced = radixTree.replace("/categories/default", "categories-new"); - assertThat(replaced).isTrue(); - assertThat(radixTree.find("/categories/default")).isEqualTo("categories-new"); - } - - @Test - void find() { - RadixTree radixTree = new RadixTree<>(); - for (String testCase : testCases()) { - radixTree.insert(testCase, testCase); - } - - for (String testCase : testCases()) { - String s = radixTree.find(testCase); - assertThat(s).isEqualTo(testCase); - } - } - - @Test - void visitTimes() { - AtomicInteger visitCount = new AtomicInteger(0); - RadixTree radixTree = new RadixTree<>() { - @Override - protected void visit(String prefix, Visitor visitor, - RadixTreeNode parent, RadixTreeNode node) { - visitCount.getAndIncrement(); - super.visit(prefix, visitor, parent, node); - } - }; - - for (String testCase : testCases()) { - radixTree.insert(testCase, testCase); - } - - /* - * / [indices=hbAscxy01adnΠuvw] 1 - * ├── s [indices=er] 2 - │ ├── earch/query [value=/search/query]* 3 - │ └── rc/*filepath [value=/src/*filepath]* 3 - * ├── u [indices=/s] 2 - * │ └── sers/a/b/c [indices=/c] 3 - * │ ├── /d [value=/users/a/b/c/d]* 4 - * │ └── c/d [value=/users/a/b/cc/d]* 4 - * //... - */ - - String key = "/users/a/b/c/d"; - AtomicInteger resultVisitorCount = new AtomicInteger(0); - RadixTree.Visitor visitor = new RadixTree.Visitor<>() { - public void visit(String key, RadixTreeNode parent, - RadixTreeNode node) { - resultVisitorCount.getAndIncrement(); - if (node.isReal()) { - result = node.getValue(); - } - } - }; - - RadixTreeNode root = radixTree.getRoot(); - radixTree.visit(key, visitor, null, root); - assertThat(resultVisitorCount.get()).isEqualTo(1); - assertThat(visitor.result).isEqualTo(key); - assertThat(visitCount.get()).isEqualTo(4); - - // clear counter - visitCount.set(0); - resultVisitorCount.set(0); - visitor.result = null; - key = "/search/query"; - radixTree.visit(key, visitor, null, root); - assertThat(resultVisitorCount.get()).isEqualTo(1); - assertThat(visitCount.get()).isEqualTo(3); - assertThat(visitor.getResult()).isEqualTo(key); - - // clear counter - visitCount.set(0); - resultVisitorCount.set(0); - visitor.result = null; - // not exists key - key = "/search"; - radixTree.visit(key, visitor, null, root); - assertThat(resultVisitorCount.get()).isEqualTo(0); - assertThat(visitCount.get()).isEqualTo(3); - assertThat(visitor.getResult()).isEqualTo(null); - - // clear counter - visitCount.set(0); - resultVisitorCount.set(0); - visitor.result = null; - // not exists key - key = "/s"; - radixTree.visit(key, visitor, null, root); - assertThat(resultVisitorCount.get()).isEqualTo(1); - assertThat(visitCount.get()).isEqualTo(2); - assertThat(visitor.getResult()).isEqualTo(null); - } - - private List testCases() { - return Arrays.asList( - "/hi", - "/b/", - "/ABC/", - "/search/query", - "/cmd/tool/", - "/src/*filepath", - "/x", - "/x/y", - "/y/", - "/y/z", - "/0/id", - "/0/id/1", - "/1/id/", - "/1/id/2", - "/aa", - "/a/", - "/doc", - "/doc/go_faq.html", - "/doc/go1.html", - "/doc/go/away", - "/no/a", - "/no/b", - "/Π", - "/u/apfêl/", - "/u/äpfêl/", - "/u/öpfêl", - "/v/Äpfêl/", - "/v/Öpfêl", - "/w/♬", - "/w/♭/", - "/w/𠜎", - "/w/𠜏/", - "/users/a/b/c/d", - "/users/a/b/cc/d" - ); - } -} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/ViewNameResolverTest.java b/src/test/java/run/halo/app/theme/router/ViewNameResolverTest.java index 749097383..f069e7dfb 100644 --- a/src/test/java/run/halo/app/theme/router/ViewNameResolverTest.java +++ b/src/test/java/run/halo/app/theme/router/ViewNameResolverTest.java @@ -22,7 +22,6 @@ import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import run.halo.app.theme.HaloViewResolver; -import run.halo.app.theme.router.strategy.EmptyView; /** * Tests for {@link ViewNameResolver}. diff --git a/src/test/java/run/halo/app/theme/router/factories/ArchiveRouteFactoryTest.java b/src/test/java/run/halo/app/theme/router/factories/ArchiveRouteFactoryTest.java new file mode 100644 index 000000000..81d99e1d5 --- /dev/null +++ b/src/test/java/run/halo/app/theme/router/factories/ArchiveRouteFactoryTest.java @@ -0,0 +1,66 @@ +package run.halo.app.theme.router.factories; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import run.halo.app.theme.finders.PostFinder; + +/** + * Tests for {@link ArchiveRouteFactory}. + * + * @author guqing + * @since 2.0.0 + */ +@ExtendWith(MockitoExtension.class) +class ArchiveRouteFactoryTest extends RouteFactoryTestSuite { + @Mock + private PostFinder postFinder; + + @InjectMocks + private ArchiveRouteFactory archiveRouteFactory; + + @Test + void create() { + String prefix = "/new-archives"; + RouterFunction routerFunction = archiveRouteFactory.create(prefix); + WebTestClient client = getWebTestClient(routerFunction); + + client.get() + .uri(prefix) + .exchange() + .expectStatus().isOk(); + + client.get() + .uri(prefix + "/page/1") + .exchange() + .expectStatus().isOk(); + + client.get() + .uri(prefix + "/2022/09") + .exchange() + .expectStatus().isOk(); + + client.get() + .uri(prefix + "/2022/08/page/1") + .exchange() + .expectStatus().isOk(); + + client.get() + .uri(prefix + "/2022/8/page/1") + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.NOT_FOUND); + + client.get() + .uri("/nothing") + .exchange() + .expectStatus() + .isEqualTo(HttpStatus.NOT_FOUND); + } +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactoryTest.java b/src/test/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactoryTest.java new file mode 100644 index 000000000..cd2b10773 --- /dev/null +++ b/src/test/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactoryTest.java @@ -0,0 +1,43 @@ +package run.halo.app.theme.router.factories; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.User; +import run.halo.app.extension.ReactiveExtensionClient; + +/** + * Tests for {@link AuthorPostsRouteFactory}. + * + * @author guqing + * @since 2.0.0 + */ +@ExtendWith(MockitoExtension.class) +class AuthorPostsRouteFactoryTest extends RouteFactoryTestSuite { + @Mock + ReactiveExtensionClient client; + @InjectMocks + AuthorPostsRouteFactory authorPostsRouteFactory; + + @Test + void create() { + RouterFunction routerFunction = authorPostsRouteFactory.create(null); + WebTestClient webClient = getWebTestClient(routerFunction); + + when(client.get(eq(User.class), eq("fake-user"))) + .thenReturn(Mono.just(new User())); + webClient.get() + .uri("/authors/fake-user") + .exchange() + .expectStatus().isOk(); + } +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/factories/CategoriesRouteFactoryTest.java b/src/test/java/run/halo/app/theme/router/factories/CategoriesRouteFactoryTest.java new file mode 100644 index 000000000..8b2066d11 --- /dev/null +++ b/src/test/java/run/halo/app/theme/router/factories/CategoriesRouteFactoryTest.java @@ -0,0 +1,41 @@ +package run.halo.app.theme.router.factories; + +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Flux; +import run.halo.app.theme.finders.CategoryFinder; + +/** + * Tests for {@link CategoriesRouteFactory}. + * + * @author guqing + * @since 2.0.0 + */ +class CategoriesRouteFactoryTest extends RouteFactoryTestSuite { + + @Mock + private CategoryFinder categoryFinder; + + @InjectMocks + private CategoriesRouteFactory categoriesRouteFactory; + + @Test + void create() { + String prefix = "/topics"; + RouterFunction routerFunction = categoriesRouteFactory.create(prefix); + WebTestClient webClient = getWebTestClient(routerFunction); + + when(categoryFinder.listAsTree()) + .thenReturn(Flux.empty()); + webClient.get() + .uri(prefix) + .exchange() + .expectStatus().isOk(); + } +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/factories/IndexRouteFactoryTest.java b/src/test/java/run/halo/app/theme/router/factories/IndexRouteFactoryTest.java new file mode 100644 index 000000000..a7983d0dc --- /dev/null +++ b/src/test/java/run/halo/app/theme/router/factories/IndexRouteFactoryTest.java @@ -0,0 +1,42 @@ +package run.halo.app.theme.router.factories; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import run.halo.app.theme.finders.PostFinder; + +/** + * Tests for {@link IndexRouteFactory}. + * + * @author guqing + * @since 2.0.0 + */ +@ExtendWith(MockitoExtension.class) +class IndexRouteFactoryTest extends RouteFactoryTestSuite { + @Mock + private PostFinder postFinder; + + @InjectMocks + private IndexRouteFactory indexRouteFactory; + + @Test + void create() { + RouterFunction routerFunction = indexRouteFactory.create("/"); + WebTestClient webTestClient = getWebTestClient(routerFunction); + + webTestClient.get() + .uri("/") + .exchange() + .expectStatus().isOk(); + + webTestClient.get() + .uri("/page/1") + .exchange() + .expectStatus().isOk(); + } +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/strategy/RouterStrategyTestSuite.java b/src/test/java/run/halo/app/theme/router/factories/RouteFactoryTestSuite.java similarity index 72% rename from src/test/java/run/halo/app/theme/router/strategy/RouterStrategyTestSuite.java rename to src/test/java/run/halo/app/theme/router/factories/RouteFactoryTestSuite.java index 9f1959f02..a08dabffd 100644 --- a/src/test/java/run/halo/app/theme/router/strategy/RouterStrategyTestSuite.java +++ b/src/test/java/run/halo/app/theme/router/factories/RouteFactoryTestSuite.java @@ -1,17 +1,14 @@ -package run.halo.app.theme.router.strategy; +package run.halo.app.theme.router.factories; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; -import java.net.URI; import java.net.URISyntaxException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.context.ApplicationContext; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.function.server.RouterFunction; @@ -20,38 +17,27 @@ import org.springframework.web.reactive.result.view.ViewResolver; import reactor.core.publisher.Mono; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; -import run.halo.app.infra.properties.HaloProperties; -import run.halo.app.theme.router.PermalinkHttpGetRouter; +import run.halo.app.theme.router.EmptyView; /** - * Abstract test for {@link DetailsPageRouteHandlerStrategy} and - * {@link ListPageRouteHandlerStrategy}. + * Abstract test for {@link RouteFactory}. * * @author guqing * @since 2.0.0 */ @ExtendWith(MockitoExtension.class) -abstract class RouterStrategyTestSuite { +abstract class RouteFactoryTestSuite { @Mock protected SystemConfigurableEnvironmentFetcher environmentFetcher; - @Mock - protected ApplicationContext applicationContext; - @Mock - protected HaloProperties haloProperties; - @Mock protected ViewResolver viewResolver; - @InjectMocks - protected PermalinkHttpGetRouter permalinkHttpGetRouter; - @BeforeEach final void setUpParent() throws URISyntaxException { lenient().when(environmentFetcher.fetchPost()) .thenReturn(Mono.just(new SystemSetting.Post())); lenient().when(environmentFetcher.fetch(eq(SystemSetting.ThemeRouteRules.GROUP), eq(SystemSetting.ThemeRouteRules.class))).thenReturn(Mono.just(getThemeRouteRules())); - lenient().when(haloProperties.getExternalUrl()).thenReturn(new URI("http://example.com")); lenient().when(viewResolver.resolveViewName(any(), any())) .thenReturn(Mono.just(new EmptyView())); setUp(); @@ -77,8 +63,4 @@ abstract class RouterStrategyTestSuite { .build()) .build(); } - - public RouterFunction getRouterFunction() { - return request -> Mono.justOrEmpty(permalinkHttpGetRouter.route(request)); - } } diff --git a/src/test/java/run/halo/app/theme/router/factories/TagPostRouteFactoryTest.java b/src/test/java/run/halo/app/theme/router/factories/TagPostRouteFactoryTest.java new file mode 100644 index 000000000..1110880bd --- /dev/null +++ b/src/test/java/run/halo/app/theme/router/factories/TagPostRouteFactoryTest.java @@ -0,0 +1,68 @@ +package run.halo.app.theme.router.factories; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.reactive.server.WebTestClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.content.Tag; +import run.halo.app.extension.Metadata; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.theme.finders.PostFinder; +import run.halo.app.theme.finders.TagFinder; +import run.halo.app.theme.finders.vo.TagVo; + +/** + * Tests for @link TagPostRouteFactory}. + * + * @author guqing + * @since 2.0.0 + */ +@ExtendWith(MockitoExtension.class) +class TagPostRouteFactoryTest extends RouteFactoryTestSuite { + @Mock + private ReactiveExtensionClient client; + @Mock + private TagFinder tagFinder; + @Mock + private PostFinder postFinder; + + @InjectMocks + TagPostRouteFactory tagPostRouteFactory; + + @Test + void create() { + when(client.list(eq(Tag.class), any(), eq(null))).thenReturn(Flux.empty()); + WebTestClient webTestClient = getWebTestClient(tagPostRouteFactory.create("/new-tags")); + + webTestClient.get() + .uri("/new-tags/tag-slug-1") + .exchange() + .expectStatus().isNotFound(); + + Tag tag = new Tag(); + tag.setMetadata(new Metadata()); + tag.getMetadata().setName("fake-tag-name"); + tag.setSpec(new Tag.TagSpec()); + tag.getSpec().setSlug("tag-slug-2"); + when(client.list(eq(Tag.class), any(), eq(null))).thenReturn(Flux.just(tag)); + when(tagFinder.getByName(eq(tag.getMetadata().getName()))) + .thenReturn(Mono.just(TagVo.from(tag))); + webTestClient.get() + .uri("/new-tags/tag-slug-2") + .exchange() + .expectStatus().isOk(); + + webTestClient.get() + .uri("/new-tags/tag-slug-2/page/1") + .exchange() + .expectStatus().isOk(); + } +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/strategy/ArchivesRouteStrategyTest.java b/src/test/java/run/halo/app/theme/router/strategy/ArchivesRouteStrategyTest.java deleted file mode 100644 index 854991657..000000000 --- a/src/test/java/run/halo/app/theme/router/strategy/ArchivesRouteStrategyTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import run.halo.app.theme.finders.PostFinder; - -/** - * Tests for {@link ArchivesRouteStrategy}. - * - * @author guqing - * @since 2.0.0 - */ -@ExtendWith(MockitoExtension.class) -class ArchivesRouteStrategyTest extends RouterStrategyTestSuite { - @Mock - private PostFinder postFinder; - - @InjectMocks - private ArchivesRouteStrategy archivesRouteStrategy; - - @Test - void getRouteFunctionWhenDefaultPattern() { - HandlerFunction handler = archivesRouteStrategy.getHandler(); - RouterFunction routeFunction = getRouterFunction(); - - WebTestClient client = getWebTestClient(routeFunction); - - List routerPaths = archivesRouteStrategy.getRouterPaths("/archives"); - for (String routerPath : routerPaths) { - permalinkHttpGetRouter.insert(routerPath, handler); - } - - fixedAssertion(client, "/archives"); - - client.get() - .uri("/nothing") - .exchange() - .expectStatus() - .isEqualTo(HttpStatus.NOT_FOUND); - } - - private static void fixedAssertion(WebTestClient client, String prefix) { - client.get() - .uri(prefix) - .exchange() - .expectStatus().isOk(); - - client.get() - .uri(prefix + "/page/1") - .exchange() - .expectStatus().isOk(); - - client.get() - .uri(prefix + "/2022/09") - .exchange() - .expectStatus().isOk(); - - client.get() - .uri(prefix + "/2022/08/page/1") - .exchange() - .expectStatus().isOk(); - - client.get() - .uri(prefix + "/2022/8/page/1") - .exchange() - .expectStatus() - .isEqualTo(HttpStatus.NOT_FOUND); - } - - @Test - void getRouteFunctionWhenOtherPattern() { - HandlerFunction handler = archivesRouteStrategy.getHandler(); - RouterFunction routeFunction = getRouterFunction(); - - final WebTestClient client = getWebTestClient(routeFunction); - - List routerPaths = archivesRouteStrategy.getRouterPaths("/archives-test"); - for (String routerPath : routerPaths) { - permalinkHttpGetRouter.insert(routerPath, handler); - } - - fixedAssertion(client, "/archives-test"); - - client.get() - .uri("/archives") - .exchange() - .expectStatus() - .isEqualTo(HttpStatus.NOT_FOUND); - } -} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/strategy/AuthorRouteStrategyTest.java b/src/test/java/run/halo/app/theme/router/strategy/AuthorRouteStrategyTest.java deleted file mode 100644 index 7426d85fc..000000000 --- a/src/test/java/run/halo/app/theme/router/strategy/AuthorRouteStrategyTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package run.halo.app.theme.router.strategy; - -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.when; - -import java.util.Map; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.springframework.http.MediaType; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; -import run.halo.app.core.extension.User; -import run.halo.app.extension.Metadata; -import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.theme.DefaultTemplateEnum; - -/** - * Tests for {@link AuthorRouteStrategy}. - * - * @author guqing - * @since 2.0.1 - */ -class AuthorRouteStrategyTest extends RouterStrategyTestSuite { - - @Mock - private ReactiveExtensionClient client; - - @InjectMocks - private AuthorRouteStrategy strategy; - - @Test - void handlerTest() { - User user = new User(); - Metadata metadata = new Metadata(); - metadata.setName("fake-user"); - user.setMetadata(metadata); - user.setSpec(new User.UserSpec()); - - when(client.fetch(eq(User.class), eq("fake-user"))).thenReturn(Mono.just(user)); - permalinkHttpGetRouter.insert("/authors/fake-user", - strategy.getHandler(getThemeRouteRules(), "fake-user")); - - when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.AUTHOR.getValue()), any())) - .thenReturn(Mono.just(new EmptyView() { - @Override - public Mono render(Map model, MediaType contentType, - ServerWebExchange exchange) { - assertThat(model.get("name")).isEqualTo("fake-user"); - assertThat(model.get("_templateId")) - .isEqualTo(DefaultTemplateEnum.AUTHOR.getValue()); - assertThat(model.get("author")).isNotNull(); - assertThat(model.get("posts")).isNotNull(); - return Mono.empty(); - } - })); - - WebTestClient webTestClient = getWebTestClient(getRouterFunction()); - webTestClient.get() - .uri("/authors/fake-user") - .exchange() - .expectStatus() - .isOk(); - } -} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/strategy/CategoriesRouteStrategyTest.java b/src/test/java/run/halo/app/theme/router/strategy/CategoriesRouteStrategyTest.java deleted file mode 100644 index eae360e26..000000000 --- a/src/test/java/run/halo/app/theme/router/strategy/CategoriesRouteStrategyTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import static org.mockito.Mockito.when; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Flux; -import run.halo.app.theme.finders.CategoryFinder; - -/** - * Tests for {@link CategoriesRouteStrategy}. - * - * @author guqing - * @since 2.0.0 - */ -@ExtendWith(MockitoExtension.class) -class CategoriesRouteStrategyTest extends RouterStrategyTestSuite { - @Mock - private CategoryFinder categoryFinder; - - @InjectMocks - private CategoriesRouteStrategy categoriesRouteStrategy; - - @Test - void getRouteFunction() { - HandlerFunction handler = categoriesRouteStrategy.getHandler(); - RouterFunction routeFunction = getRouterFunction(); - - WebTestClient client = getWebTestClient(routeFunction); - - List routerPaths = categoriesRouteStrategy.getRouterPaths("/categories-test"); - for (String routerPath : routerPaths) { - permalinkHttpGetRouter.insert(routerPath, handler); - } - - when(categoryFinder.listAsTree()).thenReturn(Flux.empty()); - client.get() - .uri("/categories-test") - .exchange() - .expectStatus() - .isOk(); - - client.get() - .uri("/nothing") - .exchange() - .expectStatus() - .isEqualTo(HttpStatus.NOT_FOUND); - } -} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/strategy/CategoryRouteStrategyTest.java b/src/test/java/run/halo/app/theme/router/strategy/CategoryRouteStrategyTest.java deleted file mode 100644 index a07946e6b..000000000 --- a/src/test/java/run/halo/app/theme/router/strategy/CategoryRouteStrategyTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Mono; -import run.halo.app.theme.finders.CategoryFinder; -import run.halo.app.theme.finders.PostFinder; - -/** - * Tests for {@link CategoryRouteStrategy}. - * - * @author guqing - * @since 2.0.0 - */ -@ExtendWith(MockitoExtension.class) -class CategoryRouteStrategyTest extends RouterStrategyTestSuite { - - @Mock - private PostFinder postFinder; - - @Mock - private CategoryFinder categoryFinder; - - @InjectMocks - private CategoryRouteStrategy categoryRouteStrategy; - - @Test - void getRouteFunction() { - RouterFunction routeFunction = getRouterFunction(); - final WebTestClient client = getWebTestClient(routeFunction); - - permalinkHttpGetRouter.insert("/categories-test/category-slug-1", - categoryRouteStrategy.getHandler(null, "category-slug-1")); - permalinkHttpGetRouter.insert("/categories-test/category-slug-2", - categoryRouteStrategy.getHandler(null, "category-slug-2")); - - when(categoryFinder.getByName(any())).thenReturn(Mono.empty()); - - // /{prefix}/{slug} - client.get() - .uri("/categories-test/category-slug-1") - .exchange() - .expectStatus() - .isOk(); - - // /{prefix}/{slug} - client.get() - .uri("/categories-test/category-slug-2") - .exchange() - .expectStatus() - .isOk(); - - // /{prefix}/{slug}/page/{page} - client.get() - .uri("/categories-test/category-slug-2/page/1") - .exchange() - .expectStatus() - .isOk(); - - client.get() - .uri("/categories-test/not-exist-slug") - .exchange() - .expectStatus() - .isEqualTo(HttpStatus.NOT_FOUND); - - client.get() - .uri("/nothing") - .exchange() - .expectStatus() - .isEqualTo(HttpStatus.NOT_FOUND); - } -} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/strategy/IndexRouteStrategyTest.java b/src/test/java/run/halo/app/theme/router/strategy/IndexRouteStrategyTest.java deleted file mode 100644 index 1ae853c43..000000000 --- a/src/test/java/run/halo/app/theme/router/strategy/IndexRouteStrategyTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import run.halo.app.theme.finders.PostFinder; - -/** - * Tests for {@link IndexRouteStrategy}. - * - * @author guqing - * @since 2.0.0 - */ -@ExtendWith(MockitoExtension.class) -class IndexRouteStrategyTest extends RouterStrategyTestSuite { - @Mock - private PostFinder postFinder; - - @InjectMocks - private IndexRouteStrategy indexRouteStrategy; - - @Test - void getRouteFunction() { - HandlerFunction handler = indexRouteStrategy.getHandler(); - RouterFunction routeFunction = getRouterFunction(); - - List routerPaths = indexRouteStrategy.getRouterPaths("/"); - for (String routerPath : routerPaths) { - permalinkHttpGetRouter.insert(routerPath, handler); - } - - final WebTestClient client = getWebTestClient(routeFunction); - - client.get() - .uri("/") - .exchange() - .expectStatus() - .isOk(); - - client.get() - .uri("/page/1") - .exchange() - .expectStatus() - .isOk(); - - client.get() - .uri("/nothing") - .exchange() - .expectStatus() - .isEqualTo(HttpStatus.NOT_FOUND); - } -} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/strategy/PostRouteStrategyTest.java b/src/test/java/run/halo/app/theme/router/strategy/PostRouteStrategyTest.java deleted file mode 100644 index 454f26e20..000000000 --- a/src/test/java/run/halo/app/theme/router/strategy/PostRouteStrategyTest.java +++ /dev/null @@ -1,132 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.lenient; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Mono; -import run.halo.app.content.TestPost; -import run.halo.app.infra.SystemSetting; -import run.halo.app.theme.DefaultTemplateEnum; -import run.halo.app.theme.finders.PostFinder; -import run.halo.app.theme.finders.vo.PostVo; -import run.halo.app.theme.router.ViewNameResolver; - -/** - * Tests for {@link PostRouteStrategy}. - * - * @author guqing - * @since 2.0.0 - */ -@ExtendWith(MockitoExtension.class) -class PostRouteStrategyTest extends RouterStrategyTestSuite { - - @Mock - private PostFinder postFinder; - - @Mock - private ViewNameResolver viewNameResolver; - - @InjectMocks - private PostRouteStrategy postRouteStrategy; - - @Override - public void setUp() { - lenient().when(viewNameResolver.resolveViewNameOrDefault(any(), any(), any())) - .thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue())); - lenient().when(postFinder.getByName(any())) - .thenReturn(Mono.just(PostVo.from(TestPost.postV1()))); - } - - @Test - void getRouteFunctionWhenSlugPathVariable() { - RouterFunction routeFunction = getRouterFunction(); - - SystemSetting.ThemeRouteRules themeRouteRules = getThemeRouteRules(); - themeRouteRules.setPost("/posts-test/{slug}"); - permalinkHttpGetRouter.insert("/posts-test/fake-slug", - postRouteStrategy.getHandler(themeRouteRules, "fake-slug")); - - WebTestClient client = getWebTestClient(routeFunction); - - client.get() - .uri("/posts-test/fake-slug") - .exchange() - .expectStatus() - .isOk(); - } - - @Test - void getRouteFunctionWhenNamePathVariable() { - RouterFunction routeFunction = getRouterFunction(); - - SystemSetting.ThemeRouteRules themeRouteRules = getThemeRouteRules(); - themeRouteRules.setPost("/posts-test/{slug}"); - permalinkHttpGetRouter.insert("/posts-test/fake-name", - postRouteStrategy.getHandler(themeRouteRules, "fake-name")); - - WebTestClient client = getWebTestClient(routeFunction); - - client.get() - .uri("/posts-test/fake-name") - .exchange() - .expectStatus() - .isOk(); - } - - @Test - void getRouteFunctionWhenYearMonthSlugPathVariable() { - RouterFunction routeFunction = getRouterFunction(); - - SystemSetting.ThemeRouteRules themeRouteRules = getThemeRouteRules(); - themeRouteRules.setPost("/{year}/{month}/{slug}"); - permalinkHttpGetRouter.insert("/{year}/{month}/{slug}", - postRouteStrategy.getHandler(themeRouteRules, "fake-name")); - - WebTestClient client = getWebTestClient(routeFunction); - - client.get() - .uri("/2022/08/fake-slug") - .exchange() - .expectStatus() - .isOk(); - } - - @Test - void getRouteFunctionWhenQueryParam() { - RouterFunction routeFunction = getRouterFunction(); - - SystemSetting.ThemeRouteRules themeRouteRules = getThemeRouteRules(); - themeRouteRules.setPost("/?p={slug}"); - permalinkHttpGetRouter.insert("/?p=fake-name", - postRouteStrategy.getHandler(themeRouteRules, "fake-name")); - - WebTestClient client = getWebTestClient(routeFunction); - - client.get() - .uri(uriBuilder -> uriBuilder.path("/") - .queryParam("p", "fake-name") - .build() - ) - .exchange() - .expectStatus() - .isOk(); - - client.get() - .uri(uriBuilder -> uriBuilder.path("/") - .queryParam("p", "nothing") - .build() - ) - .exchange() - .expectStatus() - .isEqualTo(HttpStatus.NOT_FOUND); - } -} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/strategy/SinglePageRouteStrategyTest.java b/src/test/java/run/halo/app/theme/router/strategy/SinglePageRouteStrategyTest.java deleted file mode 100644 index 6dd836186..000000000 --- a/src/test/java/run/halo/app/theme/router/strategy/SinglePageRouteStrategyTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package run.halo.app.theme.router.strategy; - -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.when; -import static run.halo.app.theme.DefaultTemplateEnum.SINGLE_PAGE; - -import java.util.Map; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.MediaType; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; -import run.halo.app.core.extension.content.SinglePage; -import run.halo.app.extension.GroupVersionKind; -import run.halo.app.extension.Metadata; -import run.halo.app.theme.finders.SinglePageFinder; -import run.halo.app.theme.finders.vo.SinglePageVo; -import run.halo.app.theme.router.ViewNameResolver; - -/** - * Tests for {@link SinglePageRouteStrategy}. - * - * @author guqing - * @since 2.0.0 - */ -@ExtendWith(MockitoExtension.class) -class SinglePageRouteStrategyTest extends RouterStrategyTestSuite { - @Mock - private SinglePageFinder singlePageFinder; - - @Mock - private ViewNameResolver viewNameResolver; - - @InjectMocks - private SinglePageRouteStrategy strategy; - - - @Override - public void setUp() { - lenient().when(viewResolver.resolveViewName(eq(SINGLE_PAGE.getValue()), any())) - .thenReturn(Mono.just(new EmptyView())); - } - - @Test - void shouldResponse404IfNoPermalinkFound() { - createClient().get() - .uri("/nothing") - .exchange() - .expectStatus().isNotFound(); - } - - @Test - void shouldResponse200IfPermalinkFound() { - permalinkHttpGetRouter.insert("/fake-slug", - strategy.getHandler(getThemeRouteRules(), "fake-name")); - when(singlePageFinder.getByName(any())).thenReturn(Mono.empty()); - createClient().get() - .uri("/fake-slug") - .exchange() - .expectStatus() - .isOk(); - } - - @Test - void shouldResponse200IfSlugNameContainsSpecialChars() { - permalinkHttpGetRouter.insert("/fake / slug", - strategy.getHandler(getThemeRouteRules(), "fake-name")); - - when(singlePageFinder.getByName(any())).thenReturn(Mono.empty()); - createClient().get() - .uri("/fake / slug") - .exchange() - .expectStatus().isOk(); - } - - @Test - void shouldResponse200IfSlugNameContainsChineseChars() { - permalinkHttpGetRouter.insert("/中文", - strategy.getHandler(getThemeRouteRules(), "fake-name")); - - when(singlePageFinder.getByName(any())).thenReturn(Mono.empty()); - - createClient().get() - .uri("/中文") - .exchange() - .expectStatus().isOk(); - } - - @Test - void ensureModel() { - // fix gh-2912 - Metadata metadata = new Metadata(); - metadata.setName("fake-name"); - SinglePageVo singlePageVo = SinglePageVo.builder() - .metadata(metadata) - .spec(new SinglePage.SinglePageSpec()) - .build(); - - when(singlePageFinder.getByName(eq("fake-name"))).thenReturn(Mono.just(singlePageVo)); - permalinkHttpGetRouter.insert("/fake-slug", - strategy.getHandler(getThemeRouteRules(), "fake-name")); - - when(viewNameResolver.resolveViewNameOrDefault(any(), any(), any())) - .thenReturn(Mono.just("page")); - when(viewResolver.resolveViewName(eq(SINGLE_PAGE.getValue()), any())) - .thenReturn(Mono.just(new EmptyView() { - @Override - public Mono render(Map model, MediaType contentType, - ServerWebExchange exchange) { - assertThat(model.get("name")).isEqualTo("fake-name"); - assertThat(model.get("plural")).isEqualTo("singlepages"); - assertThat(model.get("_templateId")).isEqualTo("page"); - assertThat(model.get("singlePage")).isEqualTo(singlePageVo); - assertThat(model.get("groupVersionKind")).isEqualTo( - GroupVersionKind.fromExtension(SinglePage.class)); - return Mono.empty(); - } - })); - createClient().get() - .uri("/fake-slug") - .exchange() - .expectStatus().isOk() - .expectBody(); - } - - WebTestClient createClient() { - RouterFunction routeFunction = getRouterFunction(); - return getWebTestClient(routeFunction); - } -} diff --git a/src/test/java/run/halo/app/theme/router/strategy/TagRouteStrategyTest.java b/src/test/java/run/halo/app/theme/router/strategy/TagRouteStrategyTest.java deleted file mode 100644 index ddb8d37a8..000000000 --- a/src/test/java/run/halo/app/theme/router/strategy/TagRouteStrategyTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Mono; -import run.halo.app.theme.finders.PostFinder; -import run.halo.app.theme.finders.TagFinder; - -/** - * Tests for {@link TagRouteStrategy}. - * - * @author guqing - * @since 2.0.0 - */ -@ExtendWith(MockitoExtension.class) -class TagRouteStrategyTest extends RouterStrategyTestSuite { - - @Mock - private PostFinder postFinder; - @Mock - private TagFinder tagFinder; - - @InjectMocks - private TagRouteStrategy tagRouteStrategy; - - @Test - void getRouteFunction() { - RouterFunction routeFunction = getRouterFunction(); - WebTestClient client = getWebTestClient(routeFunction); - - permalinkHttpGetRouter.insert("/tags-test/fake-slug", - tagRouteStrategy.getHandler(getThemeRouteRules(), "fake-name")); - when(tagFinder.getByName(any())).thenReturn(Mono.empty()); - - client.get() - .uri("/tags-test/fake-slug") - .exchange() - .expectStatus() - .isOk(); - - client.get() - .uri("/tags-test/fake-slug/page/1") - .exchange() - .expectStatus() - .isOk(); - - client.get() - .uri("/nothing") - .exchange() - .expectStatus() - .isEqualTo(HttpStatus.NOT_FOUND); - } -} \ No newline at end of file diff --git a/src/test/java/run/halo/app/theme/router/strategy/TagsRouteStrategyTest.java b/src/test/java/run/halo/app/theme/router/strategy/TagsRouteStrategyTest.java deleted file mode 100644 index a30b4fd34..000000000 --- a/src/test/java/run/halo/app/theme/router/strategy/TagsRouteStrategyTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package run.halo.app.theme.router.strategy; - -import static org.mockito.Mockito.when; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatus; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.server.HandlerFunction; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerResponse; -import reactor.core.publisher.Flux; -import run.halo.app.theme.finders.TagFinder; - -/** - * Tests for {@link TagsRouteStrategy}. - * - * @author guqing - * @since 2.0.0 - */ -@ExtendWith(MockitoExtension.class) -class TagsRouteStrategyTest extends RouterStrategyTestSuite { - - @Mock - private TagFinder tagFinder; - - @InjectMocks - private TagsRouteStrategy tagsRouteStrategy; - - @Test - void getRouteFunction() { - RouterFunction routeFunction = getRouterFunction(); - WebTestClient client = getWebTestClient(routeFunction); - - List routerPaths = tagsRouteStrategy.getRouterPaths("/tags-test"); - HandlerFunction handler = tagsRouteStrategy.getHandler(); - for (String routerPath : routerPaths) { - permalinkHttpGetRouter.insert(routerPath, handler); - } - when(tagFinder.listAll()).thenReturn(Flux.empty()); - - client.get() - .uri("/tags-test") - .exchange() - .expectStatus() - .isOk(); - - client.get() - .uri("/nothing") - .exchange() - .expectStatus() - .isEqualTo(HttpStatus.NOT_FOUND); - } -} \ No newline at end of file