mirror of https://github.com/halo-dev/halo
refactor: content permalink routing using radix tree (#2547)
#### What type of PR is this? /area core /milestone 2.0 /kind improvement #### What this PR does / why we need it: 由于之前的实现方式在某些场景下遇到瓶颈,例如 permalink 含有中文或者 /{year}/{month}/{slug} 这样的 pattern 容易出现路由冲突,所以提出此 PR 以改进之前的路由方式: [Radix tree wiki](https://en.wikipedia.org/wiki/Radix_tree) 本 PR 通过变种的 RadixTree 来作为存储 permalink 的数据结构有以下原因: 1. 内容模块的 permalink 都具有大部分相同的前缀,例如分类是 以 /categories 前缀开头,文章的 /{year}/{month}/ 前缀等,使用 RadixTree 前缀树来存储将更节约内存 2. 具有分页规则例如根据分类查询文章列表的路由,/categories/fake-slug/page/{page},需要 Router 在匹配时能支持动态参数 3. 最坏时间复杂度为 O(n) 当插入以下几个路由 ``` /categories/default /categories/hello /archives/test /about /tags/halo ``` 存储结构将如下(带 * 的表示它为实际 path 节点) ``` / [indices=act, priority=6] ├── a [indices=rb, priority=3] │ ├── rchives/test [value=archives-test, priority=1]* │ └── bout [value=about, priority=1]* ├── categories/ [indices=dh, priority=2] │ ├── default [value=categories-default, priority=1]* │ └── hello [value=categories-hello, priority=1]* └── tags/halo [value=tags-halo, priority=1]* ``` 通过在 Node 添加 indices 字段来对应 children 的首字符,当查询时便可直接判断 key 的首字符选择对应下标的 children 进行深度查找(这得益于每个 node 都是与插入 key part 取最大公共字串,所以每个 node 的 indices 都不存在重复的字符。) 例如当查询 /categories/default 时 1. 首先将 `/categories/default` 与 root node 的 key `/` 取最长公共字串为 `/`,`/categories/default` 剩下的子串为 `categories/default` 首字符为 `c`, 通过获取 root node 的 `indices` 在 `indexOf('c')` 得到该进入哪个子分支 2. 如果 index 为 -1 则直接返回 null 表示不存在 3. 得到 index 为 1,则 node.getChildren(1), 得到 key 为 `categories` 的 node 4. 重复步骤1,取公共最长字串的剩余部分得到 `default`,获取该 node 的 indices 检查`default` 的首字符 是否存在:`indexOf('d')`,得到 index=0,取 node 的 children.get(0) 得到 key 为`default` 的 node,取最长公共字串没有剩余部分且当前node的 isReal(是否是一个 path) 为 true 查询结束得到结果 使用 indices 的改进 radix tree 的思路来自于 [julienschmidt/httprouter](https://github.com/julienschmidt/httprouter/blob/master/tree.go) - [ ] 带参数的 node 节点和带统配符的 node 节点有待优化查询效率,尽量做到一次查询得到结果 #### Which issue(s) this PR fixes: Fixes #2539 #2473 #### Special notes for your reviewer: how to test it? 1. 创建文章、分类、标签、自定义页面等多条数据 2. 通过反复删除和创建来测试这些资源的 permalink 是否还能被访问到 3. 该 PR 已经支持 permalink 中带有中文和特殊字符 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 使用 RadixTree 的变种数据结构改进 permalink Router ```pull/2598/head
parent
ec7dc8445f
commit
6b2aea9301
|
@ -58,7 +58,6 @@ import run.halo.app.infra.properties.HaloProperties;
|
|||
import run.halo.app.plugin.ExtensionComponentsFinder;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionRegistry;
|
||||
import run.halo.app.theme.router.TemplateRouteManager;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ExtensionConfiguration {
|
||||
|
@ -196,10 +195,10 @@ public class ExtensionConfiguration {
|
|||
|
||||
@Bean
|
||||
Controller singlePageController(ExtensionClient client, ContentService contentService,
|
||||
ApplicationContext applicationContext, TemplateRouteManager templateRouteManager) {
|
||||
ApplicationContext applicationContext) {
|
||||
return new ControllerBuilder("single-page-controller", client)
|
||||
.reconciler(new SinglePageReconciler(client, contentService,
|
||||
applicationContext, templateRouteManager)
|
||||
applicationContext)
|
||||
)
|
||||
.extension(new SinglePage())
|
||||
.build();
|
||||
|
|
|
@ -26,10 +26,8 @@ import run.halo.app.infra.Condition;
|
|||
import run.halo.app.infra.ConditionStatus;
|
||||
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.router.PermalinkIndexAddCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
|
||||
import run.halo.app.theme.router.TemplateRouteManager;
|
||||
|
||||
/**
|
||||
* <p>Reconciler for {@link SinglePage}.</p>
|
||||
|
@ -49,14 +47,12 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
private final ExtensionClient client;
|
||||
private final ContentService contentService;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final TemplateRouteManager templateRouteManager;
|
||||
|
||||
public SinglePageReconciler(ExtensionClient client, ContentService contentService,
|
||||
ApplicationContext applicationContext, TemplateRouteManager templateRouteManager) {
|
||||
ApplicationContext applicationContext) {
|
||||
this.client = client;
|
||||
this.contentService = contentService;
|
||||
this.applicationContext = applicationContext;
|
||||
this.templateRouteManager = templateRouteManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -137,7 +133,6 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
ExtensionLocator locator = new ExtensionLocator(GVK, singlePage.getMetadata().getName(),
|
||||
singlePage.getSpec().getSlug());
|
||||
applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, locator));
|
||||
templateRouteManager.changeTemplatePattern(DefaultTemplateEnum.SINGLE_PAGE.getValue());
|
||||
}
|
||||
|
||||
private void permalinkOnAdd(SinglePage singlePage) {
|
||||
|
@ -145,7 +140,6 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
singlePage.getSpec().getSlug());
|
||||
applicationContext.publishEvent(new PermalinkIndexAddCommand(this, locator,
|
||||
singlePage.getStatusOrDefault().getPermalink()));
|
||||
templateRouteManager.changeTemplatePattern(DefaultTemplateEnum.SINGLE_PAGE.getValue());
|
||||
}
|
||||
|
||||
private void reconcileStatus(String name) {
|
||||
|
|
|
@ -144,7 +144,7 @@ public class SystemSettingReconciler implements Reconciler<Reconciler.Request> {
|
|||
newRules.getArchives());
|
||||
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
||||
DefaultTemplateEnum.ARCHIVES,
|
||||
newRules.getArchives()));
|
||||
oldArchivesPrefix, newRules.getArchives()));
|
||||
}
|
||||
|
||||
if (postPatternChanged) {
|
||||
|
@ -152,7 +152,7 @@ public class SystemSettingReconciler implements Reconciler<Reconciler.Request> {
|
|||
newRules.getPost());
|
||||
// post rule changed
|
||||
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
||||
DefaultTemplateEnum.POST, newRules.getPost()));
|
||||
DefaultTemplateEnum.POST, oldPostPattern, newRules.getPost()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -181,7 +181,7 @@ public class SystemSettingReconciler implements Reconciler<Reconciler.Request> {
|
|||
// then publish event
|
||||
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
||||
DefaultTemplateEnum.TAGS,
|
||||
newRules.getTags()));
|
||||
oldTagsPrefix, newRules.getTags()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -200,7 +200,7 @@ public class SystemSettingReconciler implements Reconciler<Reconciler.Request> {
|
|||
// categories rule changed
|
||||
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
||||
DefaultTemplateEnum.CATEGORIES,
|
||||
newRules.getCategories()));
|
||||
oldCategoriesPrefix, newRules.getCategories()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -215,12 +215,12 @@ public class SystemSettingReconciler implements Reconciler<Reconciler.Request> {
|
|||
oldRules.setPost(newRules.getPost());
|
||||
updateNewRuleToConfigMap(configMap, oldRules, newRules);
|
||||
|
||||
log.debug("Categories prefix changed from [{}] to [{}].", oldPostPattern,
|
||||
log.debug("Post pattern changed from [{}] to [{}].", oldPostPattern,
|
||||
newRules.getPost());
|
||||
// post rule changed
|
||||
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
||||
DefaultTemplateEnum.POST,
|
||||
newRules.getPost()));
|
||||
oldPostPattern, newRules.getPost()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -68,8 +68,12 @@ public class SystemConfigurableEnvironmentFetcher {
|
|||
* @return a new {@link ConfigMap} named <code>system</code> by json merge patch.
|
||||
*/
|
||||
public Mono<ConfigMap> getConfigMap() {
|
||||
return extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT)
|
||||
.flatMap(systemDefault ->
|
||||
Mono<ConfigMap> mapMono =
|
||||
extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT);
|
||||
if (mapMono == null) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return mapMono.flatMap(systemDefault ->
|
||||
extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
|
||||
.map(system -> {
|
||||
Map<String, String> defaultData = systemDefault.getData();
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
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) {
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
|
||||
/**
|
||||
* A utility class for template page url.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class PageUrlUtils {
|
||||
public static final String PAGE_PART = "page";
|
||||
|
||||
public static int pageNum(ServerRequest request) {
|
||||
if (isPageUrl(request.path())) {
|
||||
String pageNum = StringUtils.substringAfterLast(request.path(), "/page/");
|
||||
return NumberUtils.toInt(pageNum, 1);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public static boolean isPageUrl(String path) {
|
||||
String[] split = StringUtils.split(path, "/");
|
||||
if (split.length > 1) {
|
||||
return PAGE_PART.equals(split[split.length - 2])
|
||||
&& NumberUtils.isDigits(split[split.length - 1]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static long totalPage(ListResult<?> list) {
|
||||
return (list.getTotal() - 1) / list.getSize() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets next page url with path.
|
||||
*
|
||||
* @param path request path
|
||||
* @return request path with next page part
|
||||
*/
|
||||
public static String nextPageUrl(String path, long total) {
|
||||
String[] segments = StringUtils.split(path, "/");
|
||||
long defaultPage = Math.min(2, Math.max(total, 1));
|
||||
if (segments.length > 1) {
|
||||
String pagePart = segments[segments.length - 2];
|
||||
if (PAGE_PART.equals(pagePart)) {
|
||||
int pageNumIndex = segments.length - 1;
|
||||
String pageNum = segments[pageNumIndex];
|
||||
segments[pageNumIndex] = toNextPage(pageNum, total);
|
||||
return PathUtils.combinePath(segments);
|
||||
}
|
||||
return appendPagePart(PathUtils.combinePath(segments), defaultPage);
|
||||
}
|
||||
return appendPagePart(PathUtils.combinePath(segments), defaultPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets previous page url with path.
|
||||
*
|
||||
* @param path request path
|
||||
* @return request path with previous page part
|
||||
*/
|
||||
public static String prevPageUrl(String path) {
|
||||
String[] segments = StringUtils.split(path, "/");
|
||||
if (segments.length > 1) {
|
||||
String pagePart = segments[segments.length - 2];
|
||||
if (PAGE_PART.equals(pagePart)) {
|
||||
int pageNumIndex = segments.length - 1;
|
||||
String pageNum = segments[pageNumIndex];
|
||||
int prevPage = toPrevPage(pageNum);
|
||||
segments[pageNumIndex] = String.valueOf(prevPage);
|
||||
if (prevPage == 1) {
|
||||
segments = ArrayUtils.subarray(segments, 0, pageNumIndex - 1);
|
||||
}
|
||||
if (segments.length == 0) {
|
||||
return "/";
|
||||
}
|
||||
return PathUtils.combinePath(segments);
|
||||
}
|
||||
}
|
||||
return StringUtils.defaultString(path, "/");
|
||||
}
|
||||
|
||||
private static String appendPagePart(String path, long page) {
|
||||
return PathUtils.combinePath(path, PAGE_PART, String.valueOf(page));
|
||||
}
|
||||
|
||||
private static String toNextPage(String pageStr, long total) {
|
||||
long page = Math.min(parseInt(pageStr) + 1, Math.max(total, 1));
|
||||
return String.valueOf(page);
|
||||
}
|
||||
|
||||
private static int toPrevPage(String pageStr) {
|
||||
return Math.max(parseInt(pageStr) - 1, 1);
|
||||
}
|
||||
|
||||
private static int parseInt(String pageStr) {
|
||||
if (!NumberUtils.isParsable(pageStr)) {
|
||||
throw new IllegalArgumentException("Page number must be a number");
|
||||
}
|
||||
return NumberUtils.toInt(pageStr, 1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
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.util.MultiValueMap;
|
||||
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<ServerResponse> route(ServerRequest request) {
|
||||
MultiValueMap<String, String> queryParams = request.queryParams();
|
||||
String requestPath = request.path();
|
||||
// 文章的 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);
|
||||
}
|
||||
}
|
||||
return routeTree.match(requestPath);
|
||||
}
|
||||
|
||||
public void insert(String key, HandlerFunction<ServerResponse> 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<String, String> 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<String> 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<String> routerPaths = indexRouteStrategy.getRouterPaths("/");
|
||||
routeTreeBatchOperation(routerPaths,
|
||||
path -> routeTree.insert(path, indexRouteStrategy.getHandler()));
|
||||
}
|
||||
|
||||
private void routeTreeBatchOperation(List<String> paths,
|
||||
Consumer<String> 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<ServerResponse> 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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ 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;
|
||||
|
@ -26,7 +27,10 @@ public class PermalinkIndexer {
|
|||
private final Map<GvkName, String> gvkNamePermalinkLookup = new HashMap<>();
|
||||
private final Map<String, ExtensionLocator> permalinkLocatorLookup = new HashMap<>();
|
||||
|
||||
record GvkName(GroupVersionKind gvk, String name) {
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public PermalinkIndexer(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -41,11 +45,17 @@ public class PermalinkIndexer {
|
|||
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.
|
||||
*
|
||||
|
@ -54,10 +64,11 @@ public class PermalinkIndexer {
|
|||
public void remove(ExtensionLocator locator) {
|
||||
readWriteLock.writeLock().lock();
|
||||
try {
|
||||
String permalink =
|
||||
gvkNamePermalinkLookup.remove(new GvkName(locator.gvk(), locator.name()));
|
||||
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();
|
||||
|
@ -76,7 +87,7 @@ public class PermalinkIndexer {
|
|||
try {
|
||||
return gvkNamePermalinkLookup.entrySet()
|
||||
.stream()
|
||||
.filter(entry -> entry.getKey().gvk.equals(gvk))
|
||||
.filter(entry -> entry.getKey().gvk().equals(gvk))
|
||||
.map(Map.Entry::getValue)
|
||||
.toList();
|
||||
} finally {
|
||||
|
|
|
@ -24,18 +24,15 @@ import run.halo.app.theme.DefaultTemplateEnum;
|
|||
@Component
|
||||
public class PermalinkRefreshHandler implements ApplicationListener<PermalinkRuleChangedEvent> {
|
||||
private final ExtensionClient client;
|
||||
private final TemplateRouteManager templateRouterManager;
|
||||
private final PostPermalinkPolicy postPermalinkPolicy;
|
||||
private final TagPermalinkPolicy tagPermalinkPolicy;
|
||||
private final CategoryPermalinkPolicy categoryPermalinkPolicy;
|
||||
|
||||
public PermalinkRefreshHandler(ExtensionClient client,
|
||||
TemplateRouteManager templateRouterManager,
|
||||
PostPermalinkPolicy postPermalinkPolicy,
|
||||
TagPermalinkPolicy tagPermalinkPolicy,
|
||||
CategoryPermalinkPolicy categoryPermalinkPolicy) {
|
||||
this.client = client;
|
||||
this.templateRouterManager = templateRouterManager;
|
||||
this.postPermalinkPolicy = postPermalinkPolicy;
|
||||
this.tagPermalinkPolicy = tagPermalinkPolicy;
|
||||
this.categoryPermalinkPolicy = categoryPermalinkPolicy;
|
||||
|
@ -47,7 +44,6 @@ public class PermalinkRefreshHandler implements ApplicationListener<PermalinkRul
|
|||
log.debug("Refresh permalink for template [{}]", template.getValue());
|
||||
switch (template) {
|
||||
case POST -> updatePostPermalink();
|
||||
case ARCHIVES -> templateRouterManager.changeTemplatePattern(template.getValue());
|
||||
case CATEGORIES, CATEGORY -> updateCategoryPermalink();
|
||||
case TAGS, TAG -> updateTagPermalink();
|
||||
default -> {
|
||||
|
@ -70,14 +66,12 @@ public class PermalinkRefreshHandler implements ApplicationListener<PermalinkRul
|
|||
client.update(post);
|
||||
|
||||
postPermalinkPolicy.onPermalinkUpdate(post);
|
||||
templateRouterManager.changeTemplatePattern(postPermalinkPolicy.templateName());
|
||||
});
|
||||
}
|
||||
|
||||
private void updateCategoryPermalink() {
|
||||
String pattern = categoryPermalinkPolicy.pattern();
|
||||
log.debug("Update category and categories permalink by new policy [{}]", pattern);
|
||||
templateRouterManager.changeTemplatePattern(DefaultTemplateEnum.CATEGORIES.getValue());
|
||||
client.list(Category.class, null, null)
|
||||
.forEach(category -> {
|
||||
String oldPermalink = category.getStatusOrDefault().getPermalink();
|
||||
|
@ -90,15 +84,12 @@ public class PermalinkRefreshHandler implements ApplicationListener<PermalinkRul
|
|||
client.update(category);
|
||||
|
||||
categoryPermalinkPolicy.onPermalinkUpdate(category);
|
||||
templateRouterManager.changeTemplatePattern(
|
||||
categoryPermalinkPolicy.templateName());
|
||||
});
|
||||
}
|
||||
|
||||
private void updateTagPermalink() {
|
||||
String pattern = tagPermalinkPolicy.pattern();
|
||||
log.debug("Update tag and tags permalink by new policy [{}]", pattern);
|
||||
templateRouterManager.changeTemplatePattern(DefaultTemplateEnum.TAGS.getValue());
|
||||
client.list(Tag.class, null, null)
|
||||
.forEach(tag -> {
|
||||
String oldPermalink = tag.getStatusOrDefault().getPermalink();
|
||||
|
@ -111,7 +102,6 @@ public class PermalinkRefreshHandler implements ApplicationListener<PermalinkRul
|
|||
client.update(tag);
|
||||
|
||||
tagPermalinkPolicy.onPermalinkUpdate(tag);
|
||||
templateRouterManager.changeTemplatePattern(tagPermalinkPolicy.templateName());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,14 @@ import run.halo.app.theme.DefaultTemplateEnum;
|
|||
|
||||
public class PermalinkRuleChangedEvent extends ApplicationEvent {
|
||||
private final DefaultTemplateEnum template;
|
||||
private final String oldRule;
|
||||
private final String rule;
|
||||
|
||||
public PermalinkRuleChangedEvent(Object source, DefaultTemplateEnum template,
|
||||
String rule) {
|
||||
String oldRule, String rule) {
|
||||
super(source);
|
||||
this.template = template;
|
||||
this.oldRule = oldRule;
|
||||
this.rule = rule;
|
||||
}
|
||||
|
||||
|
@ -18,6 +20,10 @@ public class PermalinkRuleChangedEvent extends ApplicationEvent {
|
|||
return template;
|
||||
}
|
||||
|
||||
public String getOldRule() {
|
||||
return oldRule;
|
||||
}
|
||||
|
||||
public String getRule() {
|
||||
return rule;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
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<HandlerFunction<ServerResponse>> {
|
||||
|
||||
@Override
|
||||
public void insert(String key, HandlerFunction<ServerResponse> value)
|
||||
throws IllegalArgumentException {
|
||||
// uri decode key to insert
|
||||
String decodedKey = UriUtils.decode(key, StandardCharsets.UTF_8);
|
||||
super.insert(decodedKey, value);
|
||||
if (log.isDebugEnabled()) {
|
||||
checkIndices();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String key) {
|
||||
boolean result = super.delete(key);
|
||||
if (log.isDebugEnabled()) {
|
||||
checkIndices();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Find the tree node according to the request path.</p>
|
||||
* There are the following situations:
|
||||
* <ul>
|
||||
* <li>If found, return to the {@link HandlerFunction} directly.</li>
|
||||
* <li>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.</li>
|
||||
* </ul>
|
||||
* for example, there is a tree as follows:
|
||||
* <pre>
|
||||
* / [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]*
|
||||
* </pre>
|
||||
* <p>1. find the request path "/categories/default" in tree, return the handler directly.</p>
|
||||
* <p>2. but find the request path "/categories/default/page/1", it will be iterated to
|
||||
* match</p>
|
||||
* TODO Optimize matching algorithm to improve efficiency and try your best to get results
|
||||
* through one search
|
||||
*
|
||||
* @param requestPath request path
|
||||
* @return a handler function if matched, otherwise null
|
||||
*/
|
||||
public HandlerFunction<ServerResponse> match(String requestPath) {
|
||||
String path = processRequestPath(requestPath);
|
||||
HandlerFunction<ServerResponse> result = find(path);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
PathContainer pathContainer = PathContainer.parsePath(path);
|
||||
|
||||
List<PathPattern> 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 + "}");
|
||||
}
|
||||
}
|
||||
|
||||
return find(bestMatch.getPatternString());
|
||||
}
|
||||
|
||||
private 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<String> getKeys() {
|
||||
List<String> 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 <code>path</code>.
|
||||
* Add the <code>path</code> to the result list.
|
||||
* recursion function, Time O(n), Space O(n), n is number of nodes in trie
|
||||
*/
|
||||
void keysHelper(RadixTreeNode<HandlerFunction<ServerResponse>> node, List<String> res,
|
||||
String prefix) {
|
||||
if (node == null) {
|
||||
//base condition
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.isReal()) {
|
||||
String path = prefix + node.getKey();
|
||||
res.add(path);
|
||||
}
|
||||
for (RadixTreeNode<HandlerFunction<ServerResponse>> child : node.getChildren()) {
|
||||
keysHelper(child, res, prefix + node.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,360 @@
|
|||
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<T> {
|
||||
protected RadixTreeNode<T> root;
|
||||
protected long size;
|
||||
|
||||
/**
|
||||
* Create a Radix Tree with only the default node root.
|
||||
*/
|
||||
public RadixTree() {
|
||||
root = new RadixTreeNode<T>();
|
||||
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<T, T> visitor = new Visitor<>() {
|
||||
public void visit(String key, RadixTreeNode<T> parent,
|
||||
RadixTreeNode<T> 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<T, T> visitor = new Visitor<>() {
|
||||
public void visit(String key, RadixTreeNode<T> parent, RadixTreeNode<T> 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<T, Boolean> visitor = new Visitor<>(Boolean.FALSE) {
|
||||
public void visit(String key, RadixTreeNode<T> parent,
|
||||
RadixTreeNode<T> 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<RadixTreeNode<T>> 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<T> parent,
|
||||
RadixTreeNode<T> 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<T> 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<T> 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<T> n = new RadixTreeNode<T>();
|
||||
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<T> 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<T> 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<T> n = new RadixTreeNode<T>();
|
||||
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<T, Boolean> visitor = new Visitor<>(Boolean.FALSE) {
|
||||
public void visit(String key, RadixTreeNode<T> parent,
|
||||
RadixTreeNode<T> 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 <R> void visit(String key, Visitor<T, R> 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
|
||||
*/
|
||||
<R> void visit(String prefix, Visitor<T, R> visitor,
|
||||
RadixTreeNode<T> parent, RadixTreeNode<T> 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<T> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Display the Trie on console.</p>
|
||||
* 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<T> node) {
|
||||
if (node == null) {
|
||||
//base condition
|
||||
return;
|
||||
}
|
||||
node.checkIndices();
|
||||
for (RadixTreeNode<T> child : node.getChildren()) {
|
||||
checkIndices(child);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class Visitor<T, R> {
|
||||
|
||||
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<T> parent, RadixTreeNode<T> node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
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 <T> value type
|
||||
* @author guqing
|
||||
*/
|
||||
@Data
|
||||
public class RadixTreeNode<T> {
|
||||
private String key;
|
||||
private List<RadixTreeNode<T>> 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<RadixTreeNode<T>> it = children.iterator(); it.hasNext(); ) {
|
||||
RadixTreeNode<T> 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<T> 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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.strategy.ArchivesRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.CategoriesRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.CategoryRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.IndexRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.PostRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.SinglePageRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.TagRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.TagsRouteStrategy;
|
||||
|
||||
/**
|
||||
* Theme template router mapping manager.
|
||||
*
|
||||
* @author guqing
|
||||
* @see PermalinkIndexer
|
||||
* @see PermalinkPatternProvider
|
||||
* @see TemplateRouterStrategy
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class TemplateRouteManager implements ApplicationListener<ApplicationReadyEvent> {
|
||||
|
||||
private final ReentrantReadWriteLock.WriteLock writeLock =
|
||||
new ReentrantReadWriteLock().writeLock();
|
||||
private final Map<String, RouterFunction<ServerResponse>> routerFunctionMap = new HashMap<>();
|
||||
private final PermalinkPatternProvider permalinkPatternProvider;
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public TemplateRouteManager(PermalinkPatternProvider permalinkPatternProvider,
|
||||
ApplicationContext applicationContext) {
|
||||
this.permalinkPatternProvider = permalinkPatternProvider;
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public Map<String, RouterFunction<ServerResponse>> getRouterFunctionMap() {
|
||||
return Map.copyOf(this.routerFunctionMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the template router function.
|
||||
*
|
||||
* @param templateName template name
|
||||
* @return a new {@link RouterFunction} generated by the template router strategy
|
||||
*/
|
||||
public RouterFunction<ServerResponse> changeTemplatePattern(String templateName) {
|
||||
String pattern = getPatternByTemplateName(templateName);
|
||||
RouterFunction<ServerResponse> routeFunction = templateRouterStrategy(templateName)
|
||||
.getRouteFunction(templateName, pattern);
|
||||
writeLock.lock();
|
||||
try {
|
||||
routerFunctionMap.remove(templateName);
|
||||
routerFunctionMap.put(templateName, routeFunction);
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
}
|
||||
return routeFunction;
|
||||
}
|
||||
|
||||
private String getPatternByTemplateName(String templateName) {
|
||||
DefaultTemplateEnum templateEnum = DefaultTemplateEnum.convertFrom(templateName);
|
||||
if (templateEnum == null) {
|
||||
throw new IllegalArgumentException("Template name is not valid");
|
||||
}
|
||||
return permalinkPatternProvider.getPattern(templateEnum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register router mapping by template name.
|
||||
*
|
||||
* @param templateName template name
|
||||
*/
|
||||
public void register(String templateName) {
|
||||
String pattern = getPatternByTemplateName(templateName);
|
||||
RouterFunction<ServerResponse> routeFunction = templateRouterStrategy(templateName)
|
||||
.getRouteFunction(templateName, pattern);
|
||||
if (routeFunction == null) {
|
||||
throw new IllegalStateException("Router function must not be null");
|
||||
}
|
||||
writeLock.lock();
|
||||
try {
|
||||
routerFunctionMap.put(templateName, routeFunction);
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void initRouterMapping() {
|
||||
for (DefaultTemplateEnum templateEnum : DefaultTemplateEnum.values()) {
|
||||
String templateName = templateEnum.getValue();
|
||||
register(templateName);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TemplateRouterStrategy templateRouterStrategy(String template) {
|
||||
DefaultTemplateEnum value = DefaultTemplateEnum.convertFrom(template);
|
||||
if (value == null) {
|
||||
throw new NotFoundException("Unknown template: " + template);
|
||||
}
|
||||
|
||||
return switch (value) {
|
||||
case INDEX -> applicationContext.getBean(IndexRouteStrategy.class);
|
||||
case POST -> applicationContext.getBean(PostRouteStrategy.class);
|
||||
case ARCHIVES -> applicationContext.getBean(ArchivesRouteStrategy.class);
|
||||
case TAGS -> applicationContext.getBean(TagsRouteStrategy.class);
|
||||
case TAG -> applicationContext.getBean(TagRouteStrategy.class);
|
||||
case CATEGORIES -> applicationContext.getBean(CategoriesRouteStrategy.class);
|
||||
case CATEGORY -> applicationContext.getBean(CategoryRouteStrategy.class);
|
||||
case SINGLE_PAGE -> applicationContext.getBean(SinglePageRouteStrategy.class);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
|
||||
initRouterMapping();
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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 run.halo.app.extension.ListResult;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
|
||||
/**
|
||||
* The {@link TemplateRouterStrategy} for generate {@link RouterFunction} specific to the template.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TemplateRouterStrategy {
|
||||
|
||||
RouterFunction<ServerResponse> getRouteFunction(String template, String pattern);
|
||||
|
||||
class PageUrlUtils {
|
||||
public static final String PAGE_PART = "page";
|
||||
|
||||
public static int pageNum(ServerRequest request) {
|
||||
String pageNum = request.pathVariables().get(PageUrlUtils.PAGE_PART);
|
||||
return NumberUtils.toInt(pageNum, 1);
|
||||
}
|
||||
|
||||
public static long totalPage(ListResult<?> list) {
|
||||
return (list.getTotal() - 1) / list.getSize() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets next page url with path.
|
||||
*
|
||||
* @param path request path
|
||||
* @return request path with next page part
|
||||
*/
|
||||
public static String nextPageUrl(String path, long total) {
|
||||
String[] segments = StringUtils.split(path, "/");
|
||||
long defaultPage = Math.min(2, Math.max(total, 1));
|
||||
if (segments.length > 1) {
|
||||
String pagePart = segments[segments.length - 2];
|
||||
if (PAGE_PART.equals(pagePart)) {
|
||||
int pageNumIndex = segments.length - 1;
|
||||
String pageNum = segments[pageNumIndex];
|
||||
segments[pageNumIndex] = toNextPage(pageNum, total);
|
||||
return PathUtils.combinePath(segments);
|
||||
}
|
||||
return appendPagePart(PathUtils.combinePath(segments), defaultPage);
|
||||
}
|
||||
return appendPagePart(PathUtils.combinePath(segments), defaultPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets previous page url with path.
|
||||
*
|
||||
* @param path request path
|
||||
* @return request path with previous page part
|
||||
*/
|
||||
public static String prevPageUrl(String path) {
|
||||
String[] segments = StringUtils.split(path, "/");
|
||||
if (segments.length > 1) {
|
||||
String pagePart = segments[segments.length - 2];
|
||||
if (PAGE_PART.equals(pagePart)) {
|
||||
int pageNumIndex = segments.length - 1;
|
||||
String pageNum = segments[pageNumIndex];
|
||||
int prevPage = toPrevPage(pageNum);
|
||||
segments[pageNumIndex] = String.valueOf(prevPage);
|
||||
if (prevPage == 1) {
|
||||
segments = ArrayUtils.subarray(segments, 0, pageNumIndex - 1);
|
||||
}
|
||||
if (segments.length == 0) {
|
||||
return "/";
|
||||
}
|
||||
return PathUtils.combinePath(segments);
|
||||
}
|
||||
}
|
||||
return StringUtils.defaultString(path, "/");
|
||||
}
|
||||
|
||||
private static String appendPagePart(String path, long page) {
|
||||
return PathUtils.combinePath(path, PAGE_PART, String.valueOf(page));
|
||||
}
|
||||
|
||||
private static String toNextPage(String pageStr, long total) {
|
||||
long page = Math.min(parseInt(pageStr) + 1, Math.max(total, 1));
|
||||
return String.valueOf(page);
|
||||
}
|
||||
|
||||
private static int toPrevPage(String pageStr) {
|
||||
return Math.max(parseInt(pageStr) - 1, 1);
|
||||
}
|
||||
|
||||
private static int parseInt(String pageStr) {
|
||||
if (!NumberUtils.isParsable(pageStr)) {
|
||||
throw new IllegalArgumentException("Page number must be a number");
|
||||
}
|
||||
return NumberUtils.toInt(pageStr, 1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,49 +1,40 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpMethod;
|
||||
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;
|
||||
|
||||
/**
|
||||
* <p>Theme template composite {@link RouterFunction} for manage routers for default templates.</p>
|
||||
* It routes specific requests to the {@link RouterFunction} maintained by the
|
||||
* {@link TemplateRouteManager}.
|
||||
* {@link PermalinkHttpGetRouter}.
|
||||
*
|
||||
* @author guqing
|
||||
* @see TemplateRouteManager
|
||||
* @see PermalinkHttpGetRouter
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class ThemeCompositeRouterFunction implements
|
||||
RouterFunction<ServerResponse> {
|
||||
|
||||
private final TemplateRouteManager templateRouterManager;
|
||||
private final PermalinkHttpGetRouter permalinkHttpGetRouter;
|
||||
|
||||
public ThemeCompositeRouterFunction(TemplateRouteManager templateRouterManager) {
|
||||
this.templateRouterManager = templateRouterManager;
|
||||
public ThemeCompositeRouterFunction(PermalinkHttpGetRouter permalinkHttpGetRouter) {
|
||||
this.permalinkHttpGetRouter = permalinkHttpGetRouter;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
|
||||
return Flux.fromIterable(getRouterFunctions())
|
||||
.concatMap(routerFunction -> routerFunction.route(request))
|
||||
.next();
|
||||
// this router function only supports GET method
|
||||
if (!request.method().equals(HttpMethod.GET)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(@NonNull RouterFunctions.Visitor visitor) {
|
||||
getRouterFunctions().forEach(routerFunction -> routerFunction.accept(visitor));
|
||||
}
|
||||
|
||||
private List<RouterFunction<ServerResponse>> getRouterFunctions() {
|
||||
return List.copyOf(templateRouterManager.getRouterFunctionMap().values());
|
||||
return Mono.justOrEmpty(permalinkHttpGetRouter.route(request));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
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.TemplateRouterStrategy.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.totalPage;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
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;
|
||||
|
@ -18,38 +15,24 @@ 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.PostVo;
|
||||
import run.halo.app.theme.router.TemplateRouterStrategy;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
/**
|
||||
* The {@link ArchivesRouteStrategy} for generate {@link RouterFunction} specific to the template
|
||||
* The {@link ArchivesRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* <code>posts.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class ArchivesRouteStrategy implements TemplateRouterStrategy {
|
||||
public class ArchivesRouteStrategy implements ListPageRouteHandlerStrategy {
|
||||
private final PostFinder postFinder;
|
||||
|
||||
public ArchivesRouteStrategy(PostFinder postFinder) {
|
||||
this.postFinder = postFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> getRouteFunction(String template, String prefix) {
|
||||
return RouterFunctions
|
||||
.route(GET(prefix)
|
||||
.or(GET(PathUtils.combinePath(prefix, "/page/{page:\\d+}")))
|
||||
.or(GET(PathUtils.combinePath(prefix, "/{year:\\d{4}}/{month:\\d{2}}")))
|
||||
.or(GET(PathUtils.combinePath(prefix,
|
||||
"/{year:\\d{4}}/{month:\\d{2}}/page/{page:\\d+}")))
|
||||
.and(accept(MediaType.TEXT_HTML)),
|
||||
request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.ARCHIVES.getValue(),
|
||||
Map.of("posts", postList(request))));
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<PostVo>> postList(ServerRequest request) {
|
||||
String path = request.path();
|
||||
return Mono.defer(() -> Mono.just(postFinder.list(pageNum(request), 10)))
|
||||
|
@ -60,4 +43,27 @@ public class ArchivesRouteStrategy implements TemplateRouterStrategy {
|
|||
.prevUrl(PageUrlUtils.prevPageUrl(path))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler() {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.ARCHIVES.getValue(),
|
||||
Map.of("posts", postList(request)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRouterPaths(String prefix) {
|
||||
return List.of(
|
||||
prefix,
|
||||
PathUtils.combinePath(prefix, "/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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +1,51 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
import run.halo.app.theme.finders.vo.CategoryTreeVo;
|
||||
import run.halo.app.theme.router.TemplateRouterStrategy;
|
||||
|
||||
/**
|
||||
* Categories router strategy for generate {@link RouterFunction} specific to the template
|
||||
* Categories router strategy for generate {@link HandlerFunction} specific to the template
|
||||
* <code>categories.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class CategoriesRouteStrategy implements TemplateRouterStrategy {
|
||||
public class CategoriesRouteStrategy implements ListPageRouteHandlerStrategy {
|
||||
private final CategoryFinder categoryFinder;
|
||||
|
||||
public CategoriesRouteStrategy(CategoryFinder categoryFinder) {
|
||||
this.categoryFinder = categoryFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> getRouteFunction(String template, String prefix) {
|
||||
return RouterFunctions
|
||||
.route(GET(PathUtils.combinePath(prefix))
|
||||
.and(accept(MediaType.TEXT_HTML)),
|
||||
request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.CATEGORIES.getValue(),
|
||||
Map.of("categories", categories())));
|
||||
}
|
||||
|
||||
private Mono<List<CategoryTreeVo>> categories() {
|
||||
return Mono.defer(() -> Mono.just(categoryFinder.listAsTree()))
|
||||
.publishOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler() {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.CATEGORIES.getValue(),
|
||||
Map.of("categories", categories()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRouterPaths(String prefix) {
|
||||
return List.of(StringUtils.prependIfMissing(prefix, "/"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(DefaultTemplateEnum template) {
|
||||
return DefaultTemplateEnum.CATEGORIES.equals(template);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,74 +1,46 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
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.TemplateRouterStrategy.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.totalPage;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.Map;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
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 reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.core.extension.Category;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
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.CategoryVo;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
import run.halo.app.theme.router.PermalinkIndexer;
|
||||
import run.halo.app.theme.router.TemplateRouterStrategy;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
/**
|
||||
* The {@link CategoryRouteStrategy} for generate {@link RouterFunction} specific to the template
|
||||
* The {@link CategoryRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* <code>category.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class CategoryRouteStrategy implements TemplateRouterStrategy {
|
||||
|
||||
private final PermalinkIndexer permalinkIndexer;
|
||||
public class CategoryRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Category.class);
|
||||
private final PostFinder postFinder;
|
||||
|
||||
private final CategoryFinder categoryFinder;
|
||||
|
||||
public CategoryRouteStrategy(PermalinkIndexer permalinkIndexer, PostFinder postFinder,
|
||||
public CategoryRouteStrategy(PostFinder postFinder,
|
||||
CategoryFinder categoryFinder) {
|
||||
this.permalinkIndexer = permalinkIndexer;
|
||||
this.postFinder = postFinder;
|
||||
this.categoryFinder = categoryFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> getRouteFunction(String template, String prefix) {
|
||||
return RouterFunctions
|
||||
.route(GET(PathUtils.combinePath(prefix, "/{slug}"))
|
||||
.or(GET(PathUtils.combinePath(prefix, "/{slug}/page/{page:\\d+}")))
|
||||
.and(accept(MediaType.TEXT_HTML)),
|
||||
request -> {
|
||||
String slug = request.pathVariable("slug");
|
||||
GroupVersionKind gvk = GroupVersionKind.fromExtension(Category.class);
|
||||
if (!permalinkIndexer.containsSlug(gvk, slug)) {
|
||||
return ServerResponse.notFound().build();
|
||||
}
|
||||
String categoryName = permalinkIndexer.getNameBySlug(gvk, slug);
|
||||
return ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.CATEGORY.getValue(),
|
||||
Map.of("name", categoryName,
|
||||
"posts", postListByCategoryName(categoryName, request),
|
||||
"category", categoryByName(categoryName)));
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<PostVo>> postListByCategoryName(String name,
|
||||
ServerRequest request) {
|
||||
String path = request.path();
|
||||
|
@ -85,4 +57,19 @@ public class CategoryRouteStrategy implements TemplateRouterStrategy {
|
|||
return Mono.defer(() -> Mono.just(categoryFinder.getByName(name)))
|
||||
.publishOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
||||
String name) {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.CATEGORY.getValue(),
|
||||
Map.of("name", name,
|
||||
"posts", postListByCategoryName(name, request),
|
||||
"category", categoryByName(name)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GroupVersionKind gvk) {
|
||||
return this.gvk.equals(gvk);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
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<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
||||
String name);
|
||||
|
||||
boolean supports(GroupVersionKind gvk);
|
||||
}
|
|
@ -1,15 +1,12 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
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.TemplateRouterStrategy.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.totalPage;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
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;
|
||||
|
@ -17,18 +14,18 @@ import reactor.core.scheduler.Schedulers;
|
|||
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.TemplateRouterStrategy;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
/**
|
||||
* The {@link IndexRouteStrategy} for generate {@link RouterFunction} specific to the template
|
||||
* The {@link IndexRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* <code>index.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class IndexRouteStrategy implements TemplateRouterStrategy {
|
||||
public class IndexRouteStrategy implements ListPageRouteHandlerStrategy {
|
||||
|
||||
private final PostFinder postFinder;
|
||||
|
||||
|
@ -36,16 +33,6 @@ public class IndexRouteStrategy implements TemplateRouterStrategy {
|
|||
this.postFinder = postFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> getRouteFunction(String template, String pattern) {
|
||||
return RouterFunctions
|
||||
.route(GET("/").or(GET("/page/{page:\\d+}"))
|
||||
.and(accept(MediaType.TEXT_HTML)),
|
||||
request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.INDEX.getValue(),
|
||||
Map.of("posts", postList(request))));
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<PostVo>> postList(ServerRequest request) {
|
||||
String path = request.path();
|
||||
return Mono.defer(() -> Mono.just(postFinder.list(pageNum(request), 10)))
|
||||
|
@ -56,4 +43,21 @@ public class IndexRouteStrategy implements TemplateRouterStrategy {
|
|||
.prevUrl(PageUrlUtils.prevPageUrl(path))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler() {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.INDEX.getValue(),
|
||||
Map.of("posts", postList(request)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRouterPaths(String pattern) {
|
||||
return List.of("/", "/index");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(DefaultTemplateEnum template) {
|
||||
return DefaultTemplateEnum.INDEX.equals(template);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
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<ServerResponse> getHandler();
|
||||
|
||||
List<String> getRouterPaths(String pattern);
|
||||
|
||||
boolean supports(DefaultTemplateEnum template);
|
||||
}
|
|
@ -1,177 +1,69 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.stereotype.Component;
|
||||
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.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.content.permalinks.ExtensionLocator;
|
||||
import run.halo.app.core.extension.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.finders.vo.PostVo;
|
||||
import run.halo.app.theme.router.PermalinkIndexer;
|
||||
import run.halo.app.theme.router.TemplateRouterStrategy;
|
||||
|
||||
/**
|
||||
* The {@link PostRouteStrategy} for generate {@link RouterFunction} specific to the template
|
||||
* The {@link PostRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* <code>post.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class PostRouteStrategy implements TemplateRouterStrategy {
|
||||
private final PermalinkIndexer permalinkIndexer;
|
||||
public class PostRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||
static final String NAME_PARAM = "name";
|
||||
private final GroupVersionKind groupVersionKind = GroupVersionKind.fromExtension(Post.class);
|
||||
private final PostFinder postFinder;
|
||||
|
||||
public PostRouteStrategy(PermalinkIndexer permalinkIndexer, PostFinder postFinder) {
|
||||
this.permalinkIndexer = permalinkIndexer;
|
||||
public PostRouteStrategy(PostFinder postFinder) {
|
||||
this.postFinder = postFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> getRouteFunction(String template, String pattern) {
|
||||
PostRequestParamPredicate postParamPredicate =
|
||||
new PostRequestParamPredicate(pattern);
|
||||
GVK gvk = Post.class.getAnnotation(GVK.class);
|
||||
if (postParamPredicate.isQueryParamPattern()) {
|
||||
String paramName = postParamPredicate.getParamName();
|
||||
String placeholderName = postParamPredicate.getPlaceholderName();
|
||||
RequestPredicate requestPredicate = postParamPredicate.requestPredicate();
|
||||
return RouterFunctions.route(requestPredicate,
|
||||
request -> {
|
||||
String name = null;
|
||||
if (PostRequestParamPredicate.NAME_PARAM.equals(
|
||||
placeholderName)) {
|
||||
name = request.queryParam(paramName).orElseThrow();
|
||||
}
|
||||
|
||||
if (PostRequestParamPredicate.SLUG_PARAM.equals(
|
||||
placeholderName)) {
|
||||
name = permalinkIndexer.getNameBySlug(
|
||||
PostRequestParamPredicate.GVK,
|
||||
placeholderName);
|
||||
}
|
||||
|
||||
if (name == null) {
|
||||
return ServerResponse.notFound().build();
|
||||
}
|
||||
GroupVersionKind groupVersionKind =
|
||||
new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind());
|
||||
return ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.POST.getValue(),
|
||||
Map.of(PostRequestParamPredicate.NAME_PARAM, name,
|
||||
"post", postByName(name),
|
||||
"groupVersionKind", groupVersionKind,
|
||||
"plural", gvk.plural())
|
||||
);
|
||||
});
|
||||
}
|
||||
return RouterFunctions
|
||||
.route(GET(pattern).and(accept(MediaType.TEXT_HTML)),
|
||||
request -> {
|
||||
ExtensionLocator locator = permalinkIndexer.lookup(request.path());
|
||||
if (locator == null) {
|
||||
return ServerResponse.notFound().build();
|
||||
}
|
||||
public HandlerFunction<ServerResponse> 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<String, Object> model = new HashMap<>();
|
||||
model.put(PostRequestParamPredicate.NAME_PARAM, locator.name());
|
||||
model.put(NAME_PARAM, name);
|
||||
if (pathMatchInfo != null) {
|
||||
model.putAll(pathMatchInfo.getUriVariables());
|
||||
}
|
||||
model.put("post", postByName(locator.name()));
|
||||
model.put("groupVersionKind", locator.gvk());
|
||||
model.put("post", postByName(name));
|
||||
model.put("groupVersionKind", groupVersionKind);
|
||||
model.put("plural", gvk.plural());
|
||||
return ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.POST.getValue(), model);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GroupVersionKind gvk) {
|
||||
return groupVersionKind.equals(gvk);
|
||||
}
|
||||
|
||||
private Mono<PostVo> postByName(String name) {
|
||||
return Mono.defer(() -> Mono.just(postFinder.getByName(name)))
|
||||
.publishOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
class PostRequestParamPredicate {
|
||||
static final String NAME_PARAM = "name";
|
||||
static final String SLUG_PARAM = "slug";
|
||||
static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Post.class);
|
||||
private final String pattern;
|
||||
private String paramName;
|
||||
private String placeholderName;
|
||||
|
||||
private final boolean isQueryParamPattern;
|
||||
|
||||
PostRequestParamPredicate(String pattern) {
|
||||
this.pattern = pattern;
|
||||
Matcher matcher = matchUrlParam(pattern);
|
||||
if (matcher != null) {
|
||||
this.paramName = matcher.group(1);
|
||||
this.placeholderName = matcher.group(2);
|
||||
this.isQueryParamPattern = true;
|
||||
} else {
|
||||
this.isQueryParamPattern = false;
|
||||
}
|
||||
}
|
||||
|
||||
RequestPredicate requestPredicate() {
|
||||
if (!this.isQueryParamPattern) {
|
||||
throw new IllegalStateException("Not a query param pattern: " + pattern);
|
||||
}
|
||||
|
||||
if (NAME_PARAM.equals(placeholderName)) {
|
||||
return RequestPredicates.queryParam(paramName,
|
||||
name -> permalinkIndexer.containsName(GVK, name));
|
||||
}
|
||||
|
||||
if (SLUG_PARAM.equals(placeholderName)) {
|
||||
return RequestPredicates.queryParam(paramName,
|
||||
slug -> permalinkIndexer.containsSlug(GVK, slug));
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Unknown param value placeholder [%s] in pattern [%s]",
|
||||
placeholderName, pattern));
|
||||
}
|
||||
|
||||
Matcher matchUrlParam(String patternSequence) {
|
||||
Pattern compile = Pattern.compile("([^&?]*)=\\{(.*?)\\}(&|$)");
|
||||
Matcher matcher = compile.matcher(patternSequence);
|
||||
if (matcher.find()) {
|
||||
return matcher;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getParamName() {
|
||||
return this.paramName;
|
||||
}
|
||||
|
||||
public String getPlaceholderName() {
|
||||
return placeholderName;
|
||||
}
|
||||
|
||||
public boolean isQueryParamPattern() {
|
||||
return isQueryParamPattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,72 +1,35 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
import static run.halo.app.theme.router.strategy.PermalinkPredicates.get;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RequestPredicate;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.core.extension.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.finders.vo.SinglePageVo;
|
||||
import run.halo.app.theme.router.PermalinkIndexer;
|
||||
import run.halo.app.theme.router.TemplateRouterStrategy;
|
||||
|
||||
/**
|
||||
* The {@link SinglePageRouteStrategy} for generate {@link RouterFunction} specific to the template
|
||||
* The {@link SinglePageRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* <code>page.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class SinglePageRouteStrategy implements TemplateRouterStrategy {
|
||||
|
||||
private final PermalinkIndexer permalinkIndexer;
|
||||
public class SinglePageRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class);
|
||||
private final SinglePageFinder singlePageFinder;
|
||||
|
||||
public SinglePageRouteStrategy(PermalinkIndexer permalinkIndexer,
|
||||
SinglePageFinder singlePageFinder) {
|
||||
this.permalinkIndexer = permalinkIndexer;
|
||||
public SinglePageRouteStrategy(SinglePageFinder singlePageFinder) {
|
||||
this.singlePageFinder = singlePageFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> getRouteFunction(String template, String pattern) {
|
||||
GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class);
|
||||
|
||||
RequestPredicate requestPredicate = request -> false;
|
||||
|
||||
List<String> permalinks = permalinkIndexer.getPermalinks(gvk);
|
||||
for (String permalink : permalinks) {
|
||||
requestPredicate = requestPredicate.or(get(permalink));
|
||||
}
|
||||
|
||||
return RouterFunctions
|
||||
.route(requestPredicate.and(accept(MediaType.TEXT_HTML)), request -> {
|
||||
var name = permalinkIndexer.getNameByPermalink(gvk, request.path());
|
||||
if (name == null) {
|
||||
return ServerResponse.notFound().build();
|
||||
}
|
||||
return ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.SINGLE_PAGE.getValue(),
|
||||
Map.of("name", name,
|
||||
"groupVersionKind", gvk,
|
||||
"plural", getPlural(),
|
||||
"singlePage", singlePageByName(name)));
|
||||
});
|
||||
}
|
||||
|
||||
private String getPlural() {
|
||||
GVK annotation = SinglePage.class.getAnnotation(GVK.class);
|
||||
return annotation.plural();
|
||||
|
@ -76,4 +39,20 @@ public class SinglePageRouteStrategy implements TemplateRouterStrategy {
|
|||
return Mono.defer(() -> Mono.just(singlePageFinder.getByName(name)))
|
||||
.publishOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
||||
String name) {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.SINGLE_PAGE.getValue(),
|
||||
Map.of("name", name,
|
||||
"groupVersionKind", gvk,
|
||||
"plural", getPlural(),
|
||||
"singlePage", singlePageByName(name)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GroupVersionKind gvk) {
|
||||
return this.gvk.equals(gvk);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,25 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
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.TemplateRouterStrategy.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.totalPage;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.Map;
|
||||
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 reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.core.extension.Tag;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
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.PostVo;
|
||||
import run.halo.app.theme.finders.vo.TagVo;
|
||||
import run.halo.app.theme.router.PermalinkIndexer;
|
||||
import run.halo.app.theme.router.TemplateRouterStrategy;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
/**
|
||||
|
@ -34,43 +30,18 @@ import run.halo.app.theme.router.UrlContextListResult;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class TagRouteStrategy implements TemplateRouterStrategy {
|
||||
|
||||
private final PermalinkIndexer permalinkIndexer;
|
||||
public class TagRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Tag.class);
|
||||
private final PostFinder postFinder;
|
||||
|
||||
private final TagFinder tagFinder;
|
||||
|
||||
public TagRouteStrategy(PermalinkIndexer permalinkIndexer, PostFinder postFinder,
|
||||
public TagRouteStrategy(PostFinder postFinder,
|
||||
TagFinder tagFinder) {
|
||||
this.permalinkIndexer = permalinkIndexer;
|
||||
this.postFinder = postFinder;
|
||||
this.tagFinder = tagFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> getRouteFunction(String template, String prefix) {
|
||||
return RouterFunctions
|
||||
.route(GET(PathUtils.combinePath(prefix, "/{slug}"))
|
||||
.or(GET(PathUtils.combinePath(prefix, "/{slug}/page/{page:\\d+}")))
|
||||
.and(accept(MediaType.TEXT_HTML)),
|
||||
request -> {
|
||||
GroupVersionKind gvk = GroupVersionKind.fromExtension(Tag.class);
|
||||
String slug = request.pathVariable("slug");
|
||||
if (!permalinkIndexer.containsSlug(gvk, slug)) {
|
||||
return ServerResponse.notFound().build();
|
||||
}
|
||||
|
||||
String name = permalinkIndexer.getNameBySlug(gvk, slug);
|
||||
return ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.TAG.getValue(),
|
||||
Map.of("name", name,
|
||||
"posts", postList(request, name),
|
||||
"tag", tagByName(name))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<PostVo>> postList(ServerRequest request, String name) {
|
||||
String path = request.path();
|
||||
return Mono.defer(() -> Mono.just(postFinder.listByTag(pageNum(request), 10, name)))
|
||||
|
@ -86,4 +57,20 @@ public class TagRouteStrategy implements TemplateRouterStrategy {
|
|||
return Mono.defer(() -> Mono.just(tagFinder.getByName(name)))
|
||||
.publishOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
||||
String name) {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.TAG.getValue(),
|
||||
Map.of("name", name,
|
||||
"posts", postList(request, name),
|
||||
"tag", tagByName(name))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GroupVersionKind gvk) {
|
||||
return this.gvk.equals(gvk);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,26 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
import run.halo.app.theme.finders.vo.TagVo;
|
||||
import run.halo.app.theme.router.TemplateRouterStrategy;
|
||||
|
||||
/**
|
||||
* The {@link TagsRouteStrategy} for generate {@link RouterFunction} specific to the template
|
||||
* The {@link TagsRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* <code>tags.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class TagsRouteStrategy implements TemplateRouterStrategy {
|
||||
public class TagsRouteStrategy implements ListPageRouteHandlerStrategy {
|
||||
|
||||
private final TagFinder tagFinder;
|
||||
|
||||
|
@ -34,20 +28,25 @@ public class TagsRouteStrategy implements TemplateRouterStrategy {
|
|||
this.tagFinder = tagFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> getRouteFunction(String template, String prefix) {
|
||||
String pattern = PathUtils.combinePath(prefix);
|
||||
return RouterFunctions
|
||||
.route(GET(pattern)
|
||||
.and(accept(MediaType.TEXT_HTML)),
|
||||
request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.TAGS.getValue(),
|
||||
Map.of("tags", tags()))
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<List<TagVo>> tags() {
|
||||
return Mono.defer(() -> Mono.just(tagFinder.listAll()))
|
||||
.publishOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler() {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.TAGS.getValue(),
|
||||
Map.of("tags", tags()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRouterPaths(String prefix) {
|
||||
return List.of(StringUtils.prependIfMissing(prefix, "/"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(DefaultTemplateEnum template) {
|
||||
return DefaultTemplateEnum.TAGS.equals(template);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ import run.halo.app.core.extension.service.UserService;
|
|||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.metrics.CounterMeterHandler;
|
||||
|
||||
|
@ -56,9 +55,6 @@ class UserEndpointTest {
|
|||
@MockBean
|
||||
CounterMeterHandler counterMeterHandler;
|
||||
|
||||
@MockBean
|
||||
SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// disable authorization
|
||||
|
|
|
@ -30,11 +30,9 @@ import run.halo.app.core.extension.Snapshot;
|
|||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
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.TemplateRouteManager;
|
||||
|
||||
/**
|
||||
* Tests for {@link SinglePageReconciler}.
|
||||
|
@ -52,15 +50,11 @@ class SinglePageReconcilerTest {
|
|||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Mock
|
||||
private TemplateRouteManager templateRouteManager;
|
||||
|
||||
private SinglePageReconciler singlePageReconciler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
singlePageReconciler = new SinglePageReconciler(client, contentService, applicationContext,
|
||||
templateRouteManager);
|
||||
singlePageReconciler = new SinglePageReconciler(client, contentService, applicationContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -94,8 +88,6 @@ class SinglePageReconcilerTest {
|
|||
verify(applicationContext, times(0)).publishEvent(isA(PermalinkIndexAddCommand.class));
|
||||
verify(applicationContext, times(1)).publishEvent(isA(PermalinkIndexDeleteCommand.class));
|
||||
verify(applicationContext, times(0)).publishEvent(isA(PermalinkIndexUpdateCommand.class));
|
||||
verify(templateRouteManager, times(1))
|
||||
.changeTemplatePattern(eq(DefaultTemplateEnum.SINGLE_PAGE.getValue()));
|
||||
}
|
||||
|
||||
public static SinglePage pageV1() {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link PageUrlUtils}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class PageUrlUtilsTest {
|
||||
static String s = "/tags";
|
||||
static String s1 = "/tags/page/1";
|
||||
static String s2 = "/tags/page/2";
|
||||
static String s3 = "/tags/y/m/page/2";
|
||||
static String s4 = "/tags/y/m";
|
||||
static String s5 = "/tags/y/m/page/3";
|
||||
|
||||
@Test
|
||||
void nextPageUrl() {
|
||||
long totalPage = 10;
|
||||
assertThat(PageUrlUtils.nextPageUrl(s, totalPage))
|
||||
.isEqualTo("/tags/page/2");
|
||||
assertThat(PageUrlUtils.nextPageUrl(s2, totalPage))
|
||||
.isEqualTo("/tags/page/3");
|
||||
assertThat(PageUrlUtils.nextPageUrl(s3, totalPage))
|
||||
.isEqualTo("/tags/y/m/page/3");
|
||||
assertThat(PageUrlUtils.nextPageUrl(s4, totalPage))
|
||||
.isEqualTo("/tags/y/m/page/2");
|
||||
assertThat(PageUrlUtils.nextPageUrl(s5, totalPage))
|
||||
.isEqualTo("/tags/y/m/page/4");
|
||||
|
||||
// The number of pages does not exceed the total number of pages
|
||||
totalPage = 1;
|
||||
assertThat(PageUrlUtils.nextPageUrl("/tags/page/1", totalPage))
|
||||
.isEqualTo("/tags/page/1");
|
||||
|
||||
totalPage = 0;
|
||||
assertThat(PageUrlUtils.nextPageUrl("/tags", totalPage))
|
||||
.isEqualTo("/tags/page/1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void prevPageUrl() {
|
||||
assertThat(PageUrlUtils.prevPageUrl(s))
|
||||
.isEqualTo("/tags");
|
||||
assertThat(PageUrlUtils.prevPageUrl(s1))
|
||||
.isEqualTo("/tags");
|
||||
assertThat(PageUrlUtils.prevPageUrl(s2))
|
||||
.isEqualTo("/tags");
|
||||
assertThat(PageUrlUtils.prevPageUrl(s3))
|
||||
.isEqualTo("/tags/y/m");
|
||||
assertThat(PageUrlUtils.prevPageUrl(s4))
|
||||
.isEqualTo("/tags/y/m");
|
||||
assertThat(PageUrlUtils.prevPageUrl(s5))
|
||||
.isEqualTo("/tags/y/m/page/2");
|
||||
|
||||
assertThat(PageUrlUtils.prevPageUrl("/page/2"))
|
||||
.isEqualTo("/");
|
||||
}
|
||||
}
|
|
@ -4,11 +4,18 @@ 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;
|
||||
|
@ -19,15 +26,18 @@ import run.halo.app.extension.GroupVersionKind;
|
|||
* @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();
|
||||
permalinkIndexer = new PermalinkIndexer(applicationContext);
|
||||
|
||||
ExtensionLocator locator = new ExtensionLocator(gvk, "fake-name", "fake-slug");
|
||||
permalinkIndexer.register(locator, "/fake-permalink");
|
||||
|
@ -39,6 +49,7 @@ class PermalinkIndexerTest {
|
|||
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);
|
||||
|
@ -54,6 +65,8 @@ class PermalinkIndexerTest {
|
|||
permalinkIndexer.remove(locator);
|
||||
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(0);
|
||||
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(0);
|
||||
|
||||
verify(applicationContext, times(2)).publishEvent(any(PermalinkIndexChangedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,267 @@
|
|||
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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> radixTree = new RadixTree<>() {
|
||||
@Override
|
||||
protected <R> void visit(String prefix, Visitor<String, R> visitor,
|
||||
RadixTreeNode<String> parent, RadixTreeNode<String> 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<String, String> visitor = new RadixTree.Visitor<>() {
|
||||
public void visit(String key, RadixTreeNode<String> parent,
|
||||
RadixTreeNode<String> node) {
|
||||
resultVisitorCount.getAndIncrement();
|
||||
if (node.isReal()) {
|
||||
result = node.getValue();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
RadixTreeNode<String> 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<String> 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"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Map;
|
||||
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.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
import run.halo.app.theme.router.strategy.TagsRouteStrategy;
|
||||
|
||||
/**
|
||||
* Tests for {@link TemplateRouteManager}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TemplateRouteManagerTest {
|
||||
|
||||
@Mock
|
||||
private PermalinkPatternProvider permalinkPatternProvider;
|
||||
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private TemplateRouteManager templateRouteManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
templateRouteManager = new TemplateRouteManager(permalinkPatternProvider,
|
||||
applicationContext);
|
||||
|
||||
when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAGS))
|
||||
.thenReturn("/tags");
|
||||
|
||||
TagFinder tagFinder = Mockito.mock(TagFinder.class);
|
||||
when(applicationContext.getBean(eq(TagsRouteStrategy.class)))
|
||||
.thenReturn(new TagsRouteStrategy(tagFinder));
|
||||
|
||||
templateRouteManager.register(DefaultTemplateEnum.TAGS.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouterFunctionMap() {
|
||||
Map<String, RouterFunction<ServerResponse>> routerFunctionMap =
|
||||
templateRouteManager.getRouterFunctionMap();
|
||||
assertThat(routerFunctionMap.size()).isEqualTo(1);
|
||||
assertThat(routerFunctionMap).containsKey(DefaultTemplateEnum.TAGS.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeTemplatePattern() {
|
||||
Map<String, RouterFunction<ServerResponse>> routerFunctionMap =
|
||||
templateRouteManager.getRouterFunctionMap();
|
||||
|
||||
RouterFunction<ServerResponse> routerFunction =
|
||||
routerFunctionMap.get(DefaultTemplateEnum.TAGS.getValue());
|
||||
|
||||
when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAGS))
|
||||
.thenReturn("/t");
|
||||
RouterFunction<ServerResponse> newRouterFunction =
|
||||
templateRouteManager.changeTemplatePattern(DefaultTemplateEnum.TAGS.getValue());
|
||||
|
||||
assertThat(newRouterFunction).isNotEqualTo(routerFunction);
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link TemplateRouterStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class TemplateRouterStrategyTest {
|
||||
|
||||
@Nested
|
||||
class PageUrlUtils {
|
||||
static String s = "/tags";
|
||||
static String s1 = "/tags/page/1";
|
||||
static String s2 = "/tags/page/2";
|
||||
static String s3 = "/tags/y/m/page/2";
|
||||
static String s4 = "/tags/y/m";
|
||||
static String s5 = "/tags/y/m/page/3";
|
||||
|
||||
@Test
|
||||
void nextPageUrl() {
|
||||
long totalPage = 10;
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.nextPageUrl(s, totalPage))
|
||||
.isEqualTo("/tags/page/2");
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.nextPageUrl(s2, totalPage))
|
||||
.isEqualTo("/tags/page/3");
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.nextPageUrl(s3, totalPage))
|
||||
.isEqualTo("/tags/y/m/page/3");
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.nextPageUrl(s4, totalPage))
|
||||
.isEqualTo("/tags/y/m/page/2");
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.nextPageUrl(s5, totalPage))
|
||||
.isEqualTo("/tags/y/m/page/4");
|
||||
|
||||
// The number of pages does not exceed the total number of pages
|
||||
totalPage = 1;
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.nextPageUrl("/tags/page/1", totalPage))
|
||||
.isEqualTo("/tags/page/1");
|
||||
|
||||
totalPage = 0;
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.nextPageUrl("/tags", totalPage))
|
||||
.isEqualTo("/tags/page/1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void prevPageUrl() {
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.prevPageUrl(s))
|
||||
.isEqualTo("/tags");
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.prevPageUrl(s1))
|
||||
.isEqualTo("/tags");
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.prevPageUrl(s2))
|
||||
.isEqualTo("/tags");
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.prevPageUrl(s3))
|
||||
.isEqualTo("/tags/y/m");
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.prevPageUrl(s4))
|
||||
.isEqualTo("/tags/y/m");
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.prevPageUrl(s5))
|
||||
.isEqualTo("/tags/y/m/page/2");
|
||||
|
||||
assertThat(TemplateRouterStrategy.PageUrlUtils.prevPageUrl("/page/2"))
|
||||
.isEqualTo("/");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,9 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
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 java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
@ -14,12 +11,9 @@ 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.HandlerStrategies;
|
||||
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 org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
|
@ -30,36 +24,30 @@ import run.halo.app.theme.router.UrlContextListResult;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ArchivesRouteStrategyTest {
|
||||
|
||||
@Mock
|
||||
private ViewResolver viewResolver;
|
||||
class ArchivesRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
||||
@InjectMocks
|
||||
private ArchivesRouteStrategy archivesRouteStrategy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@Override
|
||||
public void setUp() {
|
||||
lenient().when(postFinder.list(any(), any())).thenReturn(
|
||||
new UrlContextListResult<>(1, 10, 1, List.of(), null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunctionWhenDefaultPattern() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
archivesRouteStrategy.getRouteFunction(DefaultTemplateEnum.ARCHIVES.getValue(),
|
||||
"/archives");
|
||||
HandlerFunction<ServerResponse> handler = archivesRouteStrategy.getHandler();
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
|
||||
WebTestClient client = WebTestClient.bindToRouterFunction(routeFunction)
|
||||
.handlerStrategies(HandlerStrategies.builder()
|
||||
.viewResolver(viewResolver)
|
||||
.build())
|
||||
.build();
|
||||
WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.ARCHIVES.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
List<String> routerPaths = archivesRouteStrategy.getRouterPaths("/archives");
|
||||
for (String routerPath : routerPaths) {
|
||||
permalinkHttpGetRouter.insert(routerPath, handler);
|
||||
}
|
||||
|
||||
fixedAssertion(client, "/archives");
|
||||
|
||||
|
@ -100,18 +88,15 @@ class ArchivesRouteStrategyTest {
|
|||
|
||||
@Test
|
||||
void getRouteFunctionWhenOtherPattern() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
archivesRouteStrategy.getRouteFunction(DefaultTemplateEnum.ARCHIVES.getValue(),
|
||||
"/archives-test");
|
||||
HandlerFunction<ServerResponse> handler = archivesRouteStrategy.getHandler();
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
|
||||
when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.ARCHIVES.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
final WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
WebTestClient client = WebTestClient.bindToRouterFunction(routeFunction)
|
||||
.handlerStrategies(HandlerStrategies.builder()
|
||||
.viewResolver(viewResolver)
|
||||
.build())
|
||||
.build();
|
||||
List<String> routerPaths = archivesRouteStrategy.getRouterPaths("/archives-test");
|
||||
for (String routerPath : routerPaths) {
|
||||
permalinkHttpGetRouter.insert(routerPath, handler);
|
||||
}
|
||||
|
||||
fixedAssertion(client, "/archives-test");
|
||||
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
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 java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
@ -14,12 +10,9 @@ 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.HandlerStrategies;
|
||||
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 org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
|
||||
/**
|
||||
|
@ -29,36 +22,30 @@ import run.halo.app.theme.finders.CategoryFinder;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CategoriesRouteStrategyTest {
|
||||
@Mock
|
||||
private ViewResolver viewResolver;
|
||||
|
||||
class CategoriesRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
@Mock
|
||||
private CategoryFinder categoryFinder;
|
||||
|
||||
@InjectMocks
|
||||
private CategoriesRouteStrategy categoriesRouteStrategy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@Override
|
||||
public void setUp() {
|
||||
lenient().when(categoryFinder.listAsTree())
|
||||
.thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunction() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
categoriesRouteStrategy.getRouteFunction(DefaultTemplateEnum.ARCHIVES.getValue(),
|
||||
"/categories-test");
|
||||
HandlerFunction<ServerResponse> handler = categoriesRouteStrategy.getHandler();
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
|
||||
WebTestClient client = WebTestClient.bindToRouterFunction(routeFunction)
|
||||
.handlerStrategies(HandlerStrategies.builder()
|
||||
.viewResolver(viewResolver)
|
||||
.build())
|
||||
.build();
|
||||
WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.CATEGORIES.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
List<String> routerPaths = categoriesRouteStrategy.getRouterPaths("/categories-test");
|
||||
for (String routerPath : routerPaths) {
|
||||
permalinkHttpGetRouter.insert(routerPath, handler);
|
||||
}
|
||||
|
||||
client.get()
|
||||
.uri("/categories-test")
|
||||
|
|
|
@ -2,12 +2,9 @@ package run.halo.app.theme.router.strategy;
|
|||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
@ -15,17 +12,10 @@ 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.HandlerStrategies;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Category;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.router.PermalinkIndexer;
|
||||
|
||||
/**
|
||||
* Tests for {@link CategoryRouteStrategy}.
|
||||
|
@ -34,12 +24,7 @@ import run.halo.app.theme.router.PermalinkIndexer;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CategoryRouteStrategyTest {
|
||||
@Mock
|
||||
private PermalinkIndexer permalinkIndexer;
|
||||
|
||||
@Mock
|
||||
private ViewResolver viewResolver;
|
||||
class CategoryRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
@ -47,36 +32,21 @@ class CategoryRouteStrategyTest {
|
|||
@InjectMocks
|
||||
private CategoryRouteStrategy categoryRouteStrategy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
GroupVersionKind gvk = GroupVersionKind.fromExtension(Category.class);
|
||||
when(permalinkIndexer.containsSlug(eq(gvk), eq("category-slug-1")))
|
||||
.thenReturn(true);
|
||||
when(permalinkIndexer.containsSlug(eq(gvk), eq("category-slug-2")))
|
||||
.thenReturn(true);
|
||||
when(permalinkIndexer.getNameBySlug(any(), eq("category-slug-1")))
|
||||
.thenReturn("category-name-1");
|
||||
when(permalinkIndexer.getNameBySlug(any(), eq("category-slug-2")))
|
||||
.thenReturn("category-name-2");
|
||||
|
||||
@Override
|
||||
public void setUp() {
|
||||
lenient().when(postFinder.listByCategory(anyInt(), anyInt(), any()))
|
||||
.thenReturn(new ListResult<>(1, 10, 0, List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunction() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
categoryRouteStrategy.getRouteFunction(DefaultTemplateEnum.CATEGORY.getValue(),
|
||||
"/categories-test");
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
final WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
WebTestClient client = WebTestClient.bindToRouterFunction(routeFunction)
|
||||
.handlerStrategies(HandlerStrategies.builder()
|
||||
.viewResolver(viewResolver)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.CATEGORY.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
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"));
|
||||
|
||||
// /{prefix}/{slug}
|
||||
client.get()
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
@ -15,13 +11,10 @@ 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.HandlerStrategies;
|
||||
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 org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
|
||||
/**
|
||||
|
@ -31,36 +24,30 @@ import run.halo.app.theme.finders.PostFinder;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IndexRouteStrategyTest {
|
||||
@Mock
|
||||
private ViewResolver viewResolver;
|
||||
|
||||
class IndexRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
||||
@InjectMocks
|
||||
private IndexRouteStrategy indexRouteStrategy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@Override
|
||||
public void setUp() {
|
||||
lenient().when(postFinder.list(anyInt(), anyInt()))
|
||||
.thenReturn(new ListResult<>(1, 10, 0, List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunction() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
indexRouteStrategy.getRouteFunction(DefaultTemplateEnum.INDEX.getValue(),
|
||||
null);
|
||||
HandlerFunction<ServerResponse> handler = indexRouteStrategy.getHandler();
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
|
||||
WebTestClient client = WebTestClient.bindToRouterFunction(routeFunction)
|
||||
.handlerStrategies(HandlerStrategies.builder()
|
||||
.viewResolver(viewResolver)
|
||||
.build())
|
||||
.build();
|
||||
List<String> routerPaths = indexRouteStrategy.getRouterPaths("/");
|
||||
for (String routerPath : routerPaths) {
|
||||
permalinkHttpGetRouter.insert(routerPath, handler);
|
||||
}
|
||||
|
||||
when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.INDEX.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
final WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
client.get()
|
||||
.uri("/")
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
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 org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
@ -13,19 +10,12 @@ 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.HandlerStrategies;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.TestPost;
|
||||
import run.halo.app.content.permalinks.ExtensionLocator;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
import run.halo.app.theme.router.PermalinkIndexer;
|
||||
|
||||
/**
|
||||
* Tests for {@link PostRouteStrategy}.
|
||||
|
@ -34,13 +24,7 @@ import run.halo.app.theme.router.PermalinkIndexer;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PostRouteStrategyTest {
|
||||
|
||||
@Mock
|
||||
private ViewResolver viewResolver;
|
||||
|
||||
@Mock
|
||||
private PermalinkIndexer permalinkIndexer;
|
||||
class PostRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
@ -48,21 +32,22 @@ class PostRouteStrategyTest {
|
|||
@InjectMocks
|
||||
private PostRouteStrategy postRouteStrategy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@Override
|
||||
public void setUp() {
|
||||
lenient().when(postFinder.getByName(any())).thenReturn(PostVo.from(TestPost.postV1()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunctionWhenSlugPathVariable() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
postRouteStrategy.getRouteFunction(DefaultTemplateEnum.POST.getValue(),
|
||||
"/posts-test/{slug}");
|
||||
RouterFunction<ServerResponse> 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);
|
||||
|
||||
piling();
|
||||
|
||||
client.get()
|
||||
.uri("/posts-test/fake-slug")
|
||||
.exchange()
|
||||
|
@ -72,14 +57,15 @@ class PostRouteStrategyTest {
|
|||
|
||||
@Test
|
||||
void getRouteFunctionWhenNamePathVariable() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
postRouteStrategy.getRouteFunction(DefaultTemplateEnum.POST.getValue(),
|
||||
"/posts-test/{name}");
|
||||
RouterFunction<ServerResponse> 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);
|
||||
|
||||
piling();
|
||||
|
||||
client.get()
|
||||
.uri("/posts-test/fake-name")
|
||||
.exchange()
|
||||
|
@ -89,14 +75,15 @@ class PostRouteStrategyTest {
|
|||
|
||||
@Test
|
||||
void getRouteFunctionWhenYearMonthSlugPathVariable() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
postRouteStrategy.getRouteFunction(DefaultTemplateEnum.POST.getValue(),
|
||||
"/{year}/{month}/{slug}");
|
||||
RouterFunction<ServerResponse> 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);
|
||||
|
||||
piling();
|
||||
|
||||
client.get()
|
||||
.uri("/2022/08/fake-slug")
|
||||
.exchange()
|
||||
|
@ -106,17 +93,18 @@ class PostRouteStrategyTest {
|
|||
|
||||
@Test
|
||||
void getRouteFunctionWhenQueryParam() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
postRouteStrategy.getRouteFunction(DefaultTemplateEnum.POST.getValue(),
|
||||
"/?pp={name}");
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
|
||||
SystemSetting.ThemeRouteRules themeRouteRules = getThemeRouteRules();
|
||||
themeRouteRules.setPost("/?p={slug}");
|
||||
permalinkHttpGetRouter.insert("/?p=fake-name",
|
||||
postRouteStrategy.getHandler(themeRouteRules, "fake-name"));
|
||||
|
||||
WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
piling();
|
||||
|
||||
client.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/")
|
||||
.queryParam("pp", "fake-name")
|
||||
.queryParam("p", "fake-name")
|
||||
.build()
|
||||
)
|
||||
.exchange()
|
||||
|
@ -125,41 +113,11 @@ class PostRouteStrategyTest {
|
|||
|
||||
client.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/")
|
||||
.queryParam("pp", "nothing")
|
||||
.queryParam("p", "nothing")
|
||||
.build()
|
||||
)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
private void piling() {
|
||||
GroupVersionKind postGvk = GroupVersionKind.fromExtension(Post.class);
|
||||
lenient().when(permalinkIndexer.containsName(eq(postGvk), eq("fake-name")))
|
||||
.thenReturn(true);
|
||||
lenient().when(permalinkIndexer.containsSlug(eq(postGvk), eq("fake-slug")))
|
||||
.thenReturn(true);
|
||||
|
||||
lenient().when(permalinkIndexer.getNameBySlug(any(), eq("fake-slug")))
|
||||
.thenReturn("fake-name");
|
||||
|
||||
ExtensionLocator extensionLocator =
|
||||
new ExtensionLocator(GroupVersionKind.fromExtension(Post.class), "fake-name",
|
||||
"fake-slug");
|
||||
lenient().when(permalinkIndexer.lookup(any()))
|
||||
.thenReturn(extensionLocator);
|
||||
|
||||
}
|
||||
|
||||
private WebTestClient getWebTestClient(RouterFunction<ServerResponse> routeFunction) {
|
||||
WebTestClient client = WebTestClient.bindToRouterFunction(routeFunction)
|
||||
.handlerStrategies(HandlerStrategies.builder()
|
||||
.viewResolver(viewResolver)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.POST.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
return client;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
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 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;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Abstract test for {@link DetailsPageRouteHandlerStrategy} and
|
||||
* {@link ListPageRouteHandlerStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
abstract class RouterStrategyTestSuite {
|
||||
@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.fetch(eq(SystemSetting.ThemeRouteRules.GROUP),
|
||||
eq(SystemSetting.ThemeRouteRules.class))).thenReturn(Mono.just(getThemeRouteRules()));
|
||||
lenient().when(haloProperties.getExternalUrl()).thenReturn(new URI("http://example.com"));
|
||||
when(viewResolver.resolveViewName(any(), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
setUp();
|
||||
}
|
||||
|
||||
public void setUp() {
|
||||
|
||||
}
|
||||
|
||||
public SystemSetting.ThemeRouteRules getThemeRouteRules() {
|
||||
SystemSetting.ThemeRouteRules themeRouteRules = new SystemSetting.ThemeRouteRules();
|
||||
themeRouteRules.setArchives("archives");
|
||||
themeRouteRules.setPost("/archives/{slug}");
|
||||
themeRouteRules.setTags("tags");
|
||||
themeRouteRules.setCategories("categories");
|
||||
return themeRouteRules;
|
||||
}
|
||||
|
||||
public WebTestClient getWebTestClient(RouterFunction<ServerResponse> routeFunction) {
|
||||
return WebTestClient.bindToRouterFunction(routeFunction)
|
||||
.handlerStrategies(HandlerStrategies.builder()
|
||||
.viewResolver(viewResolver)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
public RouterFunction<ServerResponse> getRouterFunction() {
|
||||
return request -> Mono.justOrEmpty(permalinkHttpGetRouter.route(request));
|
||||
}
|
||||
}
|
|
@ -4,24 +4,20 @@ import static org.mockito.ArgumentMatchers.any;
|
|||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static run.halo.app.theme.DefaultTemplateEnum.SINGLE_PAGE;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.HandlerStrategies;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
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.extension.ListResult;
|
||||
import run.halo.app.theme.finders.SinglePageFinder;
|
||||
import run.halo.app.theme.router.PermalinkIndexer;
|
||||
|
||||
/**
|
||||
* Tests for {@link SinglePageRouteStrategy}.
|
||||
|
@ -30,22 +26,15 @@ import run.halo.app.theme.router.PermalinkIndexer;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SinglePageRouteStrategyTest {
|
||||
|
||||
@Mock
|
||||
private PermalinkIndexer permalinkIndexer;
|
||||
|
||||
@Mock
|
||||
private ViewResolver viewResolver;
|
||||
|
||||
class SinglePageRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
@Mock
|
||||
private SinglePageFinder singlePageFinder;
|
||||
|
||||
@InjectMocks
|
||||
private SinglePageRouteStrategy strategy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@Override
|
||||
public void setUp() {
|
||||
lenient().when(singlePageFinder.list(anyInt(), anyInt()))
|
||||
.thenReturn(new ListResult<>(1, 10, 0, List.of()));
|
||||
lenient().when(viewResolver.resolveViewName(eq(SINGLE_PAGE.getValue()), any()))
|
||||
|
@ -62,52 +51,37 @@ class SinglePageRouteStrategyTest {
|
|||
|
||||
@Test
|
||||
void shouldResponse200IfPermalinkFound() {
|
||||
when(permalinkIndexer.getPermalinks(any()))
|
||||
.thenReturn(List.of("/fake-slug"));
|
||||
when(permalinkIndexer.getNameByPermalink(any(), eq("/fake-slug")))
|
||||
.thenReturn("fake-name");
|
||||
permalinkHttpGetRouter.insert("/fake-slug",
|
||||
strategy.getHandler(getThemeRouteRules(), "fake-name"));
|
||||
createClient().get()
|
||||
.uri("/fake-slug")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
verify(permalinkIndexer).getNameByPermalink(any(), eq("/fake-slug"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResponse200IfSlugNameContainsSpecialChars() {
|
||||
when(permalinkIndexer.getPermalinks(any()))
|
||||
.thenReturn(List.of("/fake%20/%20slug"));
|
||||
when(permalinkIndexer.getNameByPermalink(any(), eq("/fake%20/%20slug")))
|
||||
.thenReturn("fake-name");
|
||||
permalinkHttpGetRouter.insert("/fake / slug",
|
||||
strategy.getHandler(getThemeRouteRules(), "fake-name"));
|
||||
createClient().get()
|
||||
.uri("/fake / slug")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
verify(permalinkIndexer).getNameByPermalink(any(), eq("/fake%20/%20slug"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResponse200IfSlugNameContainsChineseChars() {
|
||||
when(permalinkIndexer.getPermalinks(any()))
|
||||
.thenReturn(List.of("/%E4%B8%AD%E6%96%87"));
|
||||
when(permalinkIndexer.getNameByPermalink(any(), eq("/%E4%B8%AD%E6%96%87")))
|
||||
.thenReturn("fake-name");
|
||||
permalinkHttpGetRouter.insert("/中文",
|
||||
strategy.getHandler(getThemeRouteRules(), "fake-name"));
|
||||
createClient().get()
|
||||
.uri("/中文")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
verify(permalinkIndexer).getNameByPermalink(any(), eq("/%E4%B8%AD%E6%96%87"));
|
||||
}
|
||||
|
||||
WebTestClient createClient() {
|
||||
var routeFunction = strategy.getRouteFunction(SINGLE_PAGE.getValue(), null);
|
||||
|
||||
return WebTestClient.bindToRouterFunction(routeFunction)
|
||||
.handlerStrategies(HandlerStrategies.builder()
|
||||
.viewResolver(viewResolver)
|
||||
.build())
|
||||
.build();
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
return getWebTestClient(routeFunction);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,9 @@ package run.halo.app.theme.router.strategy;
|
|||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
@ -15,17 +12,10 @@ 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.HandlerStrategies;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Tag;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.router.PermalinkIndexer;
|
||||
|
||||
/**
|
||||
* Tests for {@link TagRouteStrategy}.
|
||||
|
@ -34,43 +24,27 @@ import run.halo.app.theme.router.PermalinkIndexer;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TagRouteStrategyTest {
|
||||
class TagRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
|
||||
@Mock
|
||||
private PermalinkIndexer permalinkIndexer;
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
@Mock
|
||||
private ViewResolver viewResolver;
|
||||
|
||||
@InjectMocks
|
||||
private TagRouteStrategy tagRouteStrategy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@Override
|
||||
public void setUp() {
|
||||
lenient().when(postFinder.listByTag(anyInt(), anyInt(), any()))
|
||||
.thenReturn(new ListResult<>(1, 10, 0, List.of()));
|
||||
GroupVersionKind gvk = GroupVersionKind.fromExtension(Tag.class);
|
||||
when(permalinkIndexer.containsSlug(eq(gvk), eq("fake-slug")))
|
||||
.thenReturn(true);
|
||||
when(permalinkIndexer.getNameBySlug(any(), eq("fake-slug")))
|
||||
.thenReturn("fake-name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunction() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
tagRouteStrategy.getRouteFunction(DefaultTemplateEnum.TAG.getValue(),
|
||||
"/tags-test");
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
WebTestClient client = WebTestClient.bindToRouterFunction(routeFunction)
|
||||
.handlerStrategies(HandlerStrategies.builder()
|
||||
.viewResolver(viewResolver)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.TAG.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
permalinkHttpGetRouter.insert("/tags-test/fake-slug",
|
||||
tagRouteStrategy.getHandler(getThemeRouteRules(), "fake-name"));
|
||||
|
||||
client.get()
|
||||
.uri("/tags-test/fake-slug")
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
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 java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
@ -14,12 +10,9 @@ 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.HandlerStrategies;
|
||||
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 org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
|
||||
/**
|
||||
|
@ -29,10 +22,7 @@ import run.halo.app.theme.finders.TagFinder;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TagsRouteStrategyTest {
|
||||
|
||||
@Mock
|
||||
private ViewResolver viewResolver;
|
||||
class TagsRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
|
||||
@Mock
|
||||
private TagFinder tagFinder;
|
||||
|
@ -40,25 +30,21 @@ class TagsRouteStrategyTest {
|
|||
@InjectMocks
|
||||
private TagsRouteStrategy tagsRouteStrategy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@Override
|
||||
public void setUp() {
|
||||
lenient().when(tagFinder.listAll()).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunction() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
tagsRouteStrategy.getRouteFunction(DefaultTemplateEnum.TAGS.getValue(),
|
||||
"/tags-test");
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
WebTestClient client = WebTestClient.bindToRouterFunction(routeFunction)
|
||||
.handlerStrategies(HandlerStrategies.builder()
|
||||
.viewResolver(viewResolver)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.TAGS.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
List<String> routerPaths = tagsRouteStrategy.getRouterPaths("/tags-test");
|
||||
HandlerFunction<ServerResponse> handler = tagsRouteStrategy.getHandler();
|
||||
for (String routerPath : routerPaths) {
|
||||
permalinkHttpGetRouter.insert(routerPath, handler);
|
||||
}
|
||||
|
||||
client.get()
|
||||
.uri("/tags-test")
|
||||
|
|
Loading…
Reference in New Issue