mirror of https://github.com/halo-dev/halo
perf: cannot be accessed for a long time during startup (#3300)
#### What type of PR is this? /kind improvement /area core /milestone 2.3.x #### What this PR does / why we need it: 能通过注册 pattern 作为路由的就直接注册 pattern 以避免 Reconciler 还没结束而无法注册路由导致的访问问题。 1. 文章、标签、分类的 permalink 规则会记录在对应 extension 的 annotations 中为 `content.halo.run/permalink-pattern: some-pattern` ,当系统设置中路由规则改变时会刷一遍这些资源的 `content.halo.run/permalink-pattern` annotation。 3. 自定义页面的访问是通过 SinglePageRoute 控制,它会在 single page 添加、更新、删除时维护 quickRouteMap 的集合,在路由到它时直接通过 request path 查找 map key,找到则直接返回 HandleFunction。 4. 除了自定义页面外其他都是通过 ThemeCompositeRouterFunction 作为路由管理器,系统启动时文章等的 RouterFunction 会被生成并缓存到 cachedRouters 的一个 List 集合中,当路由规则改变会清理它重新赋值。 #### Which issue(s) this PR fixes: Fixes #3254 #### Special notes for your reviewer: how to test it? 1. 测试首页、文章、标签、分类、归档页、自定义页面和作者页等的访问。 6. 测试添加、删除资源和修改系统路由规则后上述资源的访问。 7. 测试分页路径如 /tags/slug/page/1 的访问。 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 优化启动时页面长时间无法访问的问题 ```pull/3363/head
parent
ce80ed4283
commit
9fff768134
|
@ -3,79 +3,46 @@ package run.halo.app.content.permalinks;
|
|||
import static org.springframework.web.util.UriUtils.encode;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.PermalinkIndexAddCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexUpdateCommand;
|
||||
import run.halo.app.theme.router.PermalinkPatternProvider;
|
||||
import run.halo.app.theme.router.PermalinkWatch;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class CategoryPermalinkPolicy
|
||||
implements PermalinkPolicy<Category>, PermalinkWatch<Category> {
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Category.class);
|
||||
private final ApplicationContext applicationContext;
|
||||
private final PermalinkPatternProvider permalinkPatternProvider;
|
||||
@RequiredArgsConstructor
|
||||
public class CategoryPermalinkPolicy implements PermalinkPolicy<Category> {
|
||||
public static final String DEFAULT_PERMALINK_PREFIX =
|
||||
SystemSetting.ThemeRouteRules.empty().getCategories();
|
||||
|
||||
private final ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
public CategoryPermalinkPolicy(ApplicationContext applicationContext,
|
||||
PermalinkPatternProvider permalinkPatternProvider,
|
||||
ExternalUrlSupplier externalUrlSupplier) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.permalinkPatternProvider = permalinkPatternProvider;
|
||||
this.externalUrlSupplier = externalUrlSupplier;
|
||||
}
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
@Override
|
||||
public String permalink(Category category) {
|
||||
Map<String, String> annotations = ExtensionUtil.nullSafeAnnotations(category);
|
||||
String permalinkPrefix =
|
||||
annotations.getOrDefault(Constant.PERMALINK_PATTERN_ANNO, DEFAULT_PERMALINK_PREFIX);
|
||||
String slug = encode(category.getSpec().getSlug(), StandardCharsets.UTF_8);
|
||||
String path = PathUtils.combinePath(pattern(), slug);
|
||||
String path = PathUtils.combinePath(permalinkPrefix, slug);
|
||||
return externalUrlSupplier.get()
|
||||
.resolve(path)
|
||||
.normalize().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String templateName() {
|
||||
return DefaultTemplateEnum.CATEGORY.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pattern() {
|
||||
return permalinkPatternProvider.getPattern(DefaultTemplateEnum.CATEGORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermalinkAdd(Category category) {
|
||||
applicationContext.publishEvent(new PermalinkIndexAddCommand(this, getLocator(category),
|
||||
category.getStatusOrDefault().getPermalink()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermalinkUpdate(Category category) {
|
||||
applicationContext.publishEvent(new PermalinkIndexUpdateCommand(this, getLocator(category),
|
||||
category.getStatusOrDefault().getPermalink()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermalinkDelete(Category category) {
|
||||
applicationContext.publishEvent(
|
||||
new PermalinkIndexDeleteCommand(this, getLocator(category)));
|
||||
}
|
||||
|
||||
private ExtensionLocator getLocator(Category category) {
|
||||
return new ExtensionLocator(gvk, category.getMetadata().getName(),
|
||||
category.getSpec().getSlug());
|
||||
return environmentFetcher.fetchRouteRules()
|
||||
.map(SystemSetting.ThemeRouteRules::getCategories)
|
||||
.blockOptional()
|
||||
.orElse(DEFAULT_PERMALINK_PREFIX);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,4 @@ public interface PermalinkPolicy<T extends AbstractExtension> {
|
|||
new PropertyPlaceholderHelper("{", "}");
|
||||
|
||||
String permalink(T extension);
|
||||
|
||||
String templateName();
|
||||
|
||||
String pattern();
|
||||
}
|
||||
|
|
|
@ -8,79 +8,45 @@ import java.text.NumberFormat;
|
|||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Objects;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.PermalinkIndexAddCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexUpdateCommand;
|
||||
import run.halo.app.theme.router.PermalinkPatternProvider;
|
||||
import run.halo.app.theme.router.PermalinkWatch;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class PostPermalinkPolicy implements PermalinkPolicy<Post>, PermalinkWatch<Post> {
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Post.class);
|
||||
@RequiredArgsConstructor
|
||||
public class PostPermalinkPolicy implements PermalinkPolicy<Post> {
|
||||
public static final String DEFAULT_PERMALINK_PATTERN =
|
||||
SystemSetting.ThemeRouteRules.empty().getPost();
|
||||
private static final NumberFormat NUMBER_FORMAT = new DecimalFormat("00");
|
||||
|
||||
private final PermalinkPatternProvider permalinkPatternProvider;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
private final ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
public PostPermalinkPolicy(PermalinkPatternProvider permalinkPatternProvider,
|
||||
ApplicationContext applicationContext, ExternalUrlSupplier externalUrlSupplier) {
|
||||
this.permalinkPatternProvider = permalinkPatternProvider;
|
||||
this.applicationContext = applicationContext;
|
||||
this.externalUrlSupplier = externalUrlSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String permalink(Post post) {
|
||||
return createPermalink(post, pattern());
|
||||
Map<String, String> annotations = ExtensionUtil.nullSafeAnnotations(post);
|
||||
String permalinkPattern =
|
||||
annotations.getOrDefault(Constant.PERMALINK_PATTERN_ANNO, DEFAULT_PERMALINK_PATTERN);
|
||||
return createPermalink(post, permalinkPattern);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String templateName() {
|
||||
return DefaultTemplateEnum.POST.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pattern() {
|
||||
return permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermalinkAdd(Post post) {
|
||||
if (!post.isPublished() || Objects.equals(true, post.getSpec().getDeleted())) {
|
||||
return;
|
||||
}
|
||||
// publish when post is published and not deleted
|
||||
applicationContext.publishEvent(new PermalinkIndexAddCommand(this, getLocator(post),
|
||||
post.getStatusOrDefault().getPermalink()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermalinkUpdate(Post post) {
|
||||
applicationContext.publishEvent(new PermalinkIndexUpdateCommand(this, getLocator(post),
|
||||
post.getStatusOrDefault().getPermalink()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermalinkDelete(Post post) {
|
||||
applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, getLocator(post)));
|
||||
}
|
||||
|
||||
private ExtensionLocator getLocator(Post post) {
|
||||
return new ExtensionLocator(gvk, post.getMetadata().getName(), post.getSpec().getSlug());
|
||||
return environmentFetcher.fetchRouteRules()
|
||||
.map(SystemSetting.ThemeRouteRules::getPost)
|
||||
.blockOptional()
|
||||
.orElse(DEFAULT_PERMALINK_PATTERN);
|
||||
}
|
||||
|
||||
private String createPermalink(Post post, String pattern) {
|
||||
|
|
|
@ -1,76 +1,47 @@
|
|||
package run.halo.app.content.permalinks;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.PermalinkIndexAddCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexUpdateCommand;
|
||||
import run.halo.app.theme.router.PermalinkPatternProvider;
|
||||
import run.halo.app.theme.router.PermalinkWatch;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class TagPermalinkPolicy implements PermalinkPolicy<Tag>, PermalinkWatch<Tag> {
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Tag.class);
|
||||
private final PermalinkPatternProvider permalinkPatternProvider;
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class TagPermalinkPolicy implements PermalinkPolicy<Tag> {
|
||||
public static final String DEFAULT_PERMALINK_PREFIX =
|
||||
SystemSetting.ThemeRouteRules.empty().getTags();
|
||||
private final ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
public TagPermalinkPolicy(PermalinkPatternProvider permalinkPatternProvider,
|
||||
ApplicationContext applicationContext, ExternalUrlSupplier externalUrlSupplier) {
|
||||
this.permalinkPatternProvider = permalinkPatternProvider;
|
||||
this.applicationContext = applicationContext;
|
||||
this.externalUrlSupplier = externalUrlSupplier;
|
||||
}
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
@Override
|
||||
public String permalink(Tag tag) {
|
||||
Map<String, String> annotations = ExtensionUtil.nullSafeAnnotations(tag);
|
||||
String permalinkPrefix =
|
||||
annotations.getOrDefault(Constant.PERMALINK_PATTERN_ANNO, DEFAULT_PERMALINK_PREFIX);
|
||||
|
||||
String slug = UriUtils.encode(tag.getSpec().getSlug(), StandardCharsets.UTF_8);
|
||||
String path = PathUtils.combinePath(pattern(), slug);
|
||||
String path = PathUtils.combinePath(permalinkPrefix, slug);
|
||||
return externalUrlSupplier.get()
|
||||
.resolve(path)
|
||||
.normalize().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String templateName() {
|
||||
return DefaultTemplateEnum.TAG.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pattern() {
|
||||
return permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermalinkAdd(Tag tag) {
|
||||
applicationContext.publishEvent(new PermalinkIndexAddCommand(this, getLocator(tag),
|
||||
tag.getStatusOrDefault().getPermalink()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermalinkUpdate(Tag tag) {
|
||||
applicationContext.publishEvent(new PermalinkIndexUpdateCommand(this, getLocator(tag),
|
||||
tag.getStatusOrDefault().getPermalink()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermalinkDelete(Tag tag) {
|
||||
applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, getLocator(tag)));
|
||||
}
|
||||
|
||||
private ExtensionLocator getLocator(Tag tag) {
|
||||
return new ExtensionLocator(gvk, tag.getMetadata().getName(), tag.getSpec().getSlug());
|
||||
return environmentFetcher.fetchRouteRules()
|
||||
.map(SystemSetting.ThemeRouteRules::getTags)
|
||||
.blockOptional()
|
||||
.orElse(DEFAULT_PERMALINK_PREFIX);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ public enum Constant {
|
|||
public static final String VERSION = "v1alpha1";
|
||||
|
||||
public static final String LAST_READ_TIME_ANNO = "content.halo.run/last-read-time";
|
||||
public static final String PERMALINK_PATTERN_ANNO = "content.halo.run/permalink-pattern";
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import static java.lang.Boolean.parseBoolean;
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -48,8 +49,9 @@ public class Post extends AbstractExtension {
|
|||
public static final String ARCHIVE_YEAR_LABEL = "content.halo.run/archive-year";
|
||||
|
||||
public static final String ARCHIVE_MONTH_LABEL = "content.halo.run/archive-month";
|
||||
public static final String ARCHIVE_DAY_LABEL = "content.halo.run/archive-day";
|
||||
|
||||
@Schema(required = true)
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED)
|
||||
private PostSpec spec;
|
||||
|
||||
@Schema
|
||||
|
@ -81,10 +83,10 @@ public class Post extends AbstractExtension {
|
|||
|
||||
@Data
|
||||
public static class PostSpec {
|
||||
@Schema(required = true, minLength = 1)
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED, minLength = 1)
|
||||
private String title;
|
||||
|
||||
@Schema(required = true, minLength = 1)
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED, minLength = 1)
|
||||
private String slug;
|
||||
|
||||
/**
|
||||
|
@ -102,27 +104,27 @@ public class Post extends AbstractExtension {
|
|||
|
||||
private String cover;
|
||||
|
||||
@Schema(required = true, defaultValue = "false")
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "false")
|
||||
private Boolean deleted;
|
||||
|
||||
@Schema(required = true, defaultValue = "false")
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "false")
|
||||
private Boolean publish;
|
||||
|
||||
private Instant publishTime;
|
||||
|
||||
@Schema(required = true, defaultValue = "false")
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "false")
|
||||
private Boolean pinned;
|
||||
|
||||
@Schema(required = true, defaultValue = "true")
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "true")
|
||||
private Boolean allowComment;
|
||||
|
||||
@Schema(required = true, defaultValue = "PUBLIC")
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "PUBLIC")
|
||||
private VisibleEnum visible;
|
||||
|
||||
@Schema(required = true, defaultValue = "0")
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "0")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(required = true)
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED)
|
||||
private Excerpt excerpt;
|
||||
|
||||
private List<String> categories;
|
||||
|
@ -134,7 +136,7 @@ public class Post extends AbstractExtension {
|
|||
|
||||
@Data
|
||||
public static class PostStatus {
|
||||
@Schema(required = true)
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED)
|
||||
private String phase;
|
||||
|
||||
@Schema
|
||||
|
@ -164,7 +166,7 @@ public class Post extends AbstractExtension {
|
|||
@Data
|
||||
public static class Excerpt {
|
||||
|
||||
@Schema(required = true, defaultValue = "true")
|
||||
@Schema(requiredMode = RequiredMode.REQUIRED, defaultValue = "true")
|
||||
private Boolean autoGenerate;
|
||||
|
||||
private String raw;
|
||||
|
|
|
@ -11,12 +11,16 @@ import java.util.Objects;
|
|||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
|
@ -29,17 +33,12 @@ import run.halo.app.infra.utils.JsonUtils;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
||||
private static final String FINALIZER_NAME = "category-protection";
|
||||
private final ExtensionClient client;
|
||||
private final CategoryPermalinkPolicy categoryPermalinkPolicy;
|
||||
|
||||
public CategoryReconciler(ExtensionClient client,
|
||||
CategoryPermalinkPolicy categoryPermalinkPolicy) {
|
||||
this.client = client;
|
||||
this.categoryPermalinkPolicy = categoryPermalinkPolicy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
return client.fetch(Category.class, request.name())
|
||||
|
@ -50,6 +49,8 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
addFinalizerIfNecessary(category);
|
||||
|
||||
reconcileMetadata(request.name());
|
||||
|
||||
reconcileStatusPermalink(request.name());
|
||||
|
||||
reconcileStatusPosts(request.name());
|
||||
|
@ -65,6 +66,20 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
|||
.build();
|
||||
}
|
||||
|
||||
void reconcileMetadata(String name) {
|
||||
client.fetch(Category.class, name).ifPresent(category -> {
|
||||
Map<String, String> annotations = ExtensionUtil.nullSafeAnnotations(category);
|
||||
String oldPermalinkPattern = annotations.get(Constant.PERMALINK_PATTERN_ANNO);
|
||||
|
||||
String newPattern = categoryPermalinkPolicy.pattern();
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern);
|
||||
|
||||
if (!StringUtils.equals(oldPermalinkPattern, newPattern)) {
|
||||
client.update(category);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addFinalizerIfNecessary(Category oldCategory) {
|
||||
Set<String> finalizers = oldCategory.getMetadata().getFinalizers();
|
||||
if (finalizers != null && finalizers.contains(FINALIZER_NAME)) {
|
||||
|
@ -82,14 +97,8 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
|||
});
|
||||
}
|
||||
|
||||
private void cleanUpResources(Category category) {
|
||||
// remove permalink from permalink indexer
|
||||
categoryPermalinkPolicy.onPermalinkDelete(category);
|
||||
}
|
||||
|
||||
private void cleanUpResourcesAndRemoveFinalizer(String categoryName) {
|
||||
client.fetch(Category.class, categoryName).ifPresent(category -> {
|
||||
cleanUpResources(category);
|
||||
if (category.getMetadata().getFinalizers() != null) {
|
||||
category.getMetadata().getFinalizers().remove(FINALIZER_NAME);
|
||||
}
|
||||
|
@ -101,11 +110,8 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
|||
client.fetch(Category.class, name)
|
||||
.ifPresent(category -> {
|
||||
Category oldCategory = JsonUtils.deepCopy(category);
|
||||
categoryPermalinkPolicy.onPermalinkDelete(oldCategory);
|
||||
|
||||
category.getStatusOrDefault()
|
||||
.setPermalink(categoryPermalinkPolicy.permalink(category));
|
||||
categoryPermalinkPolicy.onPermalinkAdd(category);
|
||||
|
||||
if (!oldCategory.equals(category)) {
|
||||
client.update(category);
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.springframework.util.Assert;
|
|||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.content.permalinks.PostPermalinkPolicy;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.Snapshot;
|
||||
import run.halo.app.event.post.PostPublishedEvent;
|
||||
|
@ -237,10 +238,16 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
if (publishTime != null) {
|
||||
labels.put(Post.ARCHIVE_YEAR_LABEL, HaloUtils.getYearText(publishTime));
|
||||
labels.put(Post.ARCHIVE_MONTH_LABEL, HaloUtils.getMonthText(publishTime));
|
||||
labels.put(Post.ARCHIVE_DAY_LABEL, HaloUtils.getDayText(publishTime));
|
||||
}
|
||||
if (!labels.containsKey(Post.PUBLISHED_LABEL)) {
|
||||
labels.put(Post.PUBLISHED_LABEL, Boolean.FALSE.toString());
|
||||
}
|
||||
|
||||
Map<String, String> annotations = ExtensionUtil.nullSafeAnnotations(post);
|
||||
String newPattern = postPermalinkPolicy.pattern();
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern);
|
||||
|
||||
if (!oldPost.equals(post)) {
|
||||
client.update(post);
|
||||
}
|
||||
|
@ -250,11 +257,9 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
private void reconcileStatus(String name) {
|
||||
client.fetch(Post.class, name).ifPresent(post -> {
|
||||
final Post oldPost = JsonUtils.deepCopy(post);
|
||||
postPermalinkPolicy.onPermalinkDelete(oldPost);
|
||||
|
||||
post.getStatusOrDefault()
|
||||
.setPermalink(postPermalinkPolicy.permalink(post));
|
||||
postPermalinkPolicy.onPermalinkAdd(post);
|
||||
|
||||
Post.PostStatus status = post.getStatusOrDefault();
|
||||
if (status.getPhase() == null) {
|
||||
|
@ -344,9 +349,6 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
|
||||
private void cleanUpResources(Post post) {
|
||||
// remove permalink from permalink indexer
|
||||
postPermalinkPolicy.onPermalinkDelete(post);
|
||||
|
||||
// clean up snapshots
|
||||
final Ref ref = Ref.of(post);
|
||||
client.list(Snapshot.class,
|
||||
|
|
|
@ -14,11 +14,9 @@ import lombok.AllArgsConstructor;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.content.SinglePageService;
|
||||
import run.halo.app.content.permalinks.ExtensionLocator;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
|
@ -26,7 +24,6 @@ import run.halo.app.core.extension.content.Snapshot;
|
|||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionOperator;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
|
@ -38,8 +35,6 @@ import run.halo.app.infra.ExternalUrlSupplier;
|
|||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
import run.halo.app.theme.router.PermalinkIndexAddCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
|
||||
|
||||
/**
|
||||
* <p>Reconciler for {@link SinglePage}.</p>
|
||||
|
@ -58,10 +53,8 @@ import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
|
|||
@Component
|
||||
public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
||||
private static final String FINALIZER_NAME = "single-page-protection";
|
||||
private static final GroupVersionKind GVK = GroupVersionKind.fromExtension(SinglePage.class);
|
||||
private final ExtensionClient client;
|
||||
private final SinglePageService singlePageService;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final CounterService counterService;
|
||||
|
||||
private final ExternalUrlSupplier externalUrlSupplier;
|
||||
|
@ -237,9 +230,6 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
|
||||
private void cleanUpResources(SinglePage singlePage) {
|
||||
// remove permalink from permalink indexer
|
||||
permalinkOnDelete(singlePage);
|
||||
|
||||
// clean up snapshot
|
||||
Ref ref = Ref.of(singlePage);
|
||||
client.list(Snapshot.class,
|
||||
|
@ -291,36 +281,18 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
});
|
||||
}
|
||||
|
||||
private void permalinkOnDelete(SinglePage singlePage) {
|
||||
ExtensionLocator locator = new ExtensionLocator(GVK, singlePage.getMetadata().getName(),
|
||||
singlePage.getSpec().getSlug());
|
||||
applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, locator));
|
||||
}
|
||||
|
||||
String createPermalink(SinglePage page) {
|
||||
var permalink = encodePath(page.getSpec().getSlug(), UTF_8);
|
||||
permalink = StringUtils.prependIfMissing(permalink, "/");
|
||||
return externalUrlSupplier.get().resolve(permalink).normalize().toString();
|
||||
}
|
||||
|
||||
private void permalinkOnAdd(SinglePage singlePage) {
|
||||
if (!singlePage.isPublished() || Objects.equals(true, singlePage.getSpec().getDeleted())) {
|
||||
return;
|
||||
}
|
||||
ExtensionLocator locator = new ExtensionLocator(GVK, singlePage.getMetadata().getName(),
|
||||
singlePage.getSpec().getSlug());
|
||||
applicationContext.publishEvent(new PermalinkIndexAddCommand(this, locator,
|
||||
singlePage.getStatusOrDefault().getPermalink()));
|
||||
}
|
||||
|
||||
private void reconcileStatus(String name) {
|
||||
client.fetch(SinglePage.class, name).ifPresent(singlePage -> {
|
||||
final SinglePage oldPage = JsonUtils.deepCopy(singlePage);
|
||||
permalinkOnDelete(oldPage);
|
||||
|
||||
singlePage.getStatusOrDefault()
|
||||
.setPermalink(createPermalink(singlePage));
|
||||
permalinkOnAdd(singlePage);
|
||||
|
||||
SinglePage.SinglePageSpec spec = singlePage.getSpec();
|
||||
SinglePage.SinglePageStatus status = singlePage.getStatusOrDefault();
|
||||
|
|
|
@ -2,13 +2,17 @@ package run.halo.app.core.extension.reconciler;
|
|||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.permalinks.TagPermalinkPolicy;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
|
@ -41,6 +45,10 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
addFinalizerIfNecessary(tag);
|
||||
|
||||
reconcileMetadata(request.name());
|
||||
|
||||
this.reconcileStatusPermalink(request.name());
|
||||
|
||||
reconcileStatusPosts(request.name());
|
||||
});
|
||||
return new Result(false, null);
|
||||
|
@ -54,9 +62,18 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
|
|||
.build();
|
||||
}
|
||||
|
||||
private void cleanUpResources(Tag tag) {
|
||||
// remove permalink from permalink indexer
|
||||
tagPermalinkPolicy.onPermalinkDelete(tag);
|
||||
void reconcileMetadata(String name) {
|
||||
client.fetch(Tag.class, name).ifPresent(tag -> {
|
||||
Map<String, String> annotations = ExtensionUtil.nullSafeAnnotations(tag);
|
||||
String oldPermalinkPattern = annotations.get(Constant.PERMALINK_PATTERN_ANNO);
|
||||
|
||||
String newPattern = tagPermalinkPolicy.pattern();
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern);
|
||||
|
||||
if (!StringUtils.equals(oldPermalinkPattern, newPattern)) {
|
||||
client.update(tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addFinalizerIfNecessary(Tag oldTag) {
|
||||
|
@ -78,7 +95,6 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
|
|||
|
||||
private void cleanUpResourcesAndRemoveFinalizer(String tagName) {
|
||||
client.fetch(Tag.class, tagName).ifPresent(tag -> {
|
||||
cleanUpResources(tag);
|
||||
if (tag.getMetadata().getFinalizers() != null) {
|
||||
tag.getMetadata().getFinalizers().remove(FINALIZER_NAME);
|
||||
}
|
||||
|
@ -86,6 +102,19 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
|
|||
});
|
||||
}
|
||||
|
||||
private void reconcileStatusPermalink(String tagName) {
|
||||
client.fetch(Tag.class, tagName)
|
||||
.ifPresent(tag -> {
|
||||
String oldPermalink = tag.getStatusOrDefault().getPermalink();
|
||||
String permalink = tagPermalinkPolicy.permalink(tag);
|
||||
tag.getStatusOrDefault().setPermalink(permalink);
|
||||
|
||||
if (!StringUtils.equals(permalink, oldPermalink)) {
|
||||
client.update(tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reconcileStatusPosts(String tagName) {
|
||||
client.fetch(Tag.class, tagName).ifPresent(tag -> {
|
||||
Tag oldTag = JsonUtils.deepCopy(tag);
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.permalinks.TagPermalinkPolicy;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TagRouteReconciler implements Reconciler<Reconciler.Request> {
|
||||
private final ExtensionClient client;
|
||||
private final TagPermalinkPolicy tagPermalinkPolicy;
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(Tag.class, request.name())
|
||||
.ifPresent(tag -> {
|
||||
if (tag.getMetadata().getDeletionTimestamp() != null) {
|
||||
// TagReconciler already did it, so there is no need to remove permalink
|
||||
return;
|
||||
}
|
||||
|
||||
reconcilePermalinkRoute(request.name());
|
||||
});
|
||||
return new Result(false, null);
|
||||
}
|
||||
|
||||
private void reconcilePermalinkRoute(String tagName) {
|
||||
client.fetch(Tag.class, tagName)
|
||||
.ifPresent(tag -> {
|
||||
final String oldPermalink = tag.getStatusOrDefault().getPermalink();
|
||||
|
||||
tagPermalinkPolicy.onPermalinkDelete(tag);
|
||||
|
||||
String permalink = tagPermalinkPolicy.permalink(tag);
|
||||
tag.getStatusOrDefault().setPermalink(permalink);
|
||||
tagPermalinkPolicy.onPermalinkAdd(tag);
|
||||
|
||||
if (!StringUtils.equals(permalink, oldPermalink)) {
|
||||
client.update(tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return builder
|
||||
.extension(new Tag())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -7,10 +7,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.permalinks.ExtensionLocator;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
|
@ -18,8 +16,6 @@ import run.halo.app.extension.controller.Reconciler.Request;
|
|||
import run.halo.app.infra.AnonymousUserConst;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexUpdateCommand;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
|
@ -58,21 +54,12 @@ public class UserReconciler implements Reconciler<Request> {
|
|||
|
||||
status.setPermalink(getUserPermalink(user));
|
||||
|
||||
ExtensionLocator extensionLocator = getExtensionLocator(name);
|
||||
eventPublisher.publishEvent(
|
||||
new PermalinkIndexUpdateCommand(this, extensionLocator, status.getPermalink()));
|
||||
|
||||
if (!StringUtils.equals(oldPermalink, status.getPermalink())) {
|
||||
client.update(user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static ExtensionLocator getExtensionLocator(String name) {
|
||||
return new ExtensionLocator(GroupVersionKind.fromExtension(User.class), name,
|
||||
name);
|
||||
}
|
||||
|
||||
private String getUserPermalink(User user) {
|
||||
return externalUrlSupplier.get()
|
||||
.resolve(PathUtils.combinePath("authors", user.getMetadata().getName()))
|
||||
|
@ -98,9 +85,6 @@ public class UserReconciler implements Reconciler<Request> {
|
|||
|
||||
private void cleanUpResourcesAndRemoveFinalizer(String userName) {
|
||||
client.fetch(User.class, userName).ifPresent(user -> {
|
||||
eventPublisher.publishEvent(
|
||||
new PermalinkIndexDeleteCommand(this, getExtensionLocator(userName)));
|
||||
|
||||
if (user.getMetadata().getFinalizers() != null) {
|
||||
user.getMetadata().getFinalizers().remove(FINALIZER_NAME);
|
||||
}
|
||||
|
|
|
@ -59,6 +59,10 @@ public class SystemConfigurableEnvironmentFetcher {
|
|||
.switchIfEmpty(Mono.just(new SystemSetting.Post()));
|
||||
}
|
||||
|
||||
public Mono<SystemSetting.ThemeRouteRules> fetchRouteRules() {
|
||||
return fetch(SystemSetting.ThemeRouteRules.GROUP, SystemSetting.ThemeRouteRules.class);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Mono<Map<String, String>> getValuesInternal() {
|
||||
return getConfigMap()
|
||||
|
|
|
@ -32,6 +32,15 @@ public class SystemSetting {
|
|||
private String archives;
|
||||
private String post;
|
||||
private String tags;
|
||||
|
||||
public static ThemeRouteRules empty() {
|
||||
ThemeRouteRules rules = new ThemeRouteRules();
|
||||
rules.setPost("/archives/{slug}");
|
||||
rules.setArchives("/archives");
|
||||
rules.setTags("/tags");
|
||||
rules.setCategories("/categories");
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
package run.halo.app.infra.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
/**
|
||||
* Not found exception.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class NotFoundException extends RuntimeException {
|
||||
public NotFoundException(String message) {
|
||||
super(message);
|
||||
public class NotFoundException extends ResponseStatusException {
|
||||
|
||||
public NotFoundException(@Nullable String reason) {
|
||||
this(reason, null);
|
||||
}
|
||||
|
||||
public NotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
public NotFoundException(@Nullable String reason,
|
||||
@Nullable Throwable cause) {
|
||||
super(HttpStatus.NOT_FOUND, reason, cause);
|
||||
}
|
||||
|
||||
public NotFoundException(Throwable cause) {
|
||||
super(cause);
|
||||
public NotFoundException(@Nullable Throwable cause) {
|
||||
this(cause == null ? "" : cause.getMessage(), cause);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,12 @@ public class HaloUtils {
|
|||
return StringUtils.defaultString(userAgent, "unknown");
|
||||
}
|
||||
|
||||
public static String getDayText(Instant instant) {
|
||||
Assert.notNull(instant, "Instant must not be null");
|
||||
int dayValue = instant.atZone(ZoneId.systemDefault()).getDayOfMonth();
|
||||
return StringUtils.leftPad(String.valueOf(dayValue), 2, '0');
|
||||
}
|
||||
|
||||
public static String getMonthText(Instant instant) {
|
||||
Assert.notNull(instant, "Instant must not be null");
|
||||
int monthValue = instant.atZone(ZoneId.systemDefault()).getMonthValue();
|
||||
|
|
|
@ -13,7 +13,7 @@ import reactor.core.publisher.Mono;
|
|||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.SinglePageFinder;
|
||||
import run.halo.app.theme.router.strategy.ModelConst;
|
||||
import run.halo.app.theme.router.factories.ModelConst;
|
||||
|
||||
/**
|
||||
* <p>The <code>head</code> html snippet injection processor for content template such as post
|
||||
|
|
|
@ -10,7 +10,7 @@ import reactor.core.publisher.Mono;
|
|||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.strategy.ModelConst;
|
||||
import run.halo.app.theme.router.factories.ModelConst;
|
||||
|
||||
/**
|
||||
* <p>Global custom head snippet injection for theme global setting.</p>
|
||||
|
|
|
@ -20,7 +20,6 @@ import org.springframework.util.CollectionUtils;
|
|||
import org.springframework.util.comparator.Comparators;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuple2;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ListResult;
|
||||
|
@ -135,7 +134,6 @@ public class PostFinderImpl implements PostFinder {
|
|||
}
|
||||
|
||||
List<String> elements = window.elements();
|
||||
Tuple2<String, String> previousNext;
|
||||
// current post index
|
||||
int index = elements.indexOf(currentName);
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
|||
@Override
|
||||
public Mono<SinglePageVo> getByName(String pageName) {
|
||||
return client.fetch(SinglePage.class, pageName)
|
||||
.filter(FIXED_PREDICATE)
|
||||
.map(page -> {
|
||||
SinglePageVo pageVo = SinglePageVo.from(page);
|
||||
pageVo.setContributors(List.of());
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.Extension;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
|
||||
/**
|
||||
* {@link ExtensionPermalinkPatternUpdater} to update the value of key
|
||||
* {@link Constant#PERMALINK_PATTERN_ANNO} in {@link MetadataOperator#getAnnotations()}
|
||||
* of {@link Extension} when the pattern changed.
|
||||
*
|
||||
* @author guqing
|
||||
* @see Post
|
||||
* @see Category
|
||||
* @see Tag
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ExtensionPermalinkPatternUpdater
|
||||
implements ApplicationListener<PermalinkRuleChangedEvent> {
|
||||
private final ExtensionClient client;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(@NonNull PermalinkRuleChangedEvent event) {
|
||||
DefaultTemplateEnum template = event.getTemplate();
|
||||
log.debug("Refresh permalink for template [{}]", template.getValue());
|
||||
String pattern = event.getRule();
|
||||
switch (template) {
|
||||
case POST -> updatePostPermalink(pattern);
|
||||
case CATEGORY -> updateCategoryPermalink(pattern);
|
||||
case TAG -> updateTagPermalink(pattern);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePostPermalink(String pattern) {
|
||||
log.debug("Update post permalink by new policy [{}]", pattern);
|
||||
client.list(Post.class, null, null)
|
||||
.forEach(post -> updateIfPermalinkPatternChanged(post, pattern));
|
||||
}
|
||||
|
||||
private void updateIfPermalinkPatternChanged(AbstractExtension extension, String pattern) {
|
||||
Map<String, String> annotations = ExtensionUtil.nullSafeAnnotations(extension);
|
||||
String oldPattern = annotations.get(Constant.PERMALINK_PATTERN_ANNO);
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, pattern);
|
||||
|
||||
if (StringUtils.equals(oldPattern, pattern) && StringUtils.isNotBlank(oldPattern)) {
|
||||
return;
|
||||
}
|
||||
// update permalink pattern annotation
|
||||
client.update(extension);
|
||||
}
|
||||
|
||||
private void updateCategoryPermalink(String pattern) {
|
||||
log.debug("Update category and categories permalink by new policy [{}]", pattern);
|
||||
client.list(Category.class, null, null)
|
||||
.forEach(category -> updateIfPermalinkPatternChanged(category, pattern));
|
||||
}
|
||||
|
||||
private void updateTagPermalink(String pattern) {
|
||||
log.debug("Update tag and tags permalink by new policy [{}]", pattern);
|
||||
client.list(Tag.class, null, null)
|
||||
.forEach(tag -> updateIfPermalinkPatternChanged(tag, pattern));
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
|
||||
/**
|
||||
* A record class for holding gvk and name.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public record GvkName(GroupVersionKind gvk, String name) {
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
import run.halo.app.core.extension.reconciler.SystemSettingReconciler;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.strategy.DetailsPageRouteHandlerStrategy;
|
||||
import run.halo.app.theme.router.strategy.IndexRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.ListPageRouteHandlerStrategy;
|
||||
|
||||
/**
|
||||
* Permalink router for http get method.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class PermalinkHttpGetRouter implements InitializingBean {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final RadixRouterTree routeTree = new RadixRouterTree();
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
private final ReactiveExtensionClient client;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
/**
|
||||
* Match permalink according to {@link ServerRequest}.
|
||||
*
|
||||
* @param request http request
|
||||
* @return a handler function if matched, otherwise null
|
||||
*/
|
||||
public HandlerFunction<ServerResponse> route(ServerRequest request) {
|
||||
return routeTree.match(request);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import run.halo.app.content.permalinks.ExtensionLocator;
|
||||
|
||||
/**
|
||||
* <p>Send a command to add a piece of data from {@link PermalinkIndexer}.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @see PermalinkIndexer
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class PermalinkIndexAddCommand extends ApplicationEvent {
|
||||
private final ExtensionLocator locator;
|
||||
private final String permalink;
|
||||
|
||||
public PermalinkIndexAddCommand(Object source, ExtensionLocator locator, String permalink) {
|
||||
super(source);
|
||||
this.locator = locator;
|
||||
this.permalink = permalink;
|
||||
}
|
||||
|
||||
public ExtensionLocator getLocator() {
|
||||
return locator;
|
||||
}
|
||||
|
||||
public String getPermalink() {
|
||||
return permalink;
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Permalink index changed event.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Getter
|
||||
public class PermalinkIndexChangedEvent extends ApplicationEvent {
|
||||
private final String oldPermalink;
|
||||
private final String permalink;
|
||||
|
||||
private final GvkName gvkName;
|
||||
|
||||
public PermalinkIndexChangedEvent(Object source,
|
||||
GvkName gvkName, String oldPermalink, String permalink) {
|
||||
super(source);
|
||||
Assert.notNull(gvkName, "The gvkName must not be null.");
|
||||
this.oldPermalink = oldPermalink;
|
||||
this.permalink = permalink;
|
||||
this.gvkName = gvkName;
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import run.halo.app.content.permalinks.ExtensionLocator;
|
||||
|
||||
/**
|
||||
* <p>Send a command to delete a piece of data from {@link PermalinkIndexer}.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @see PermalinkIndexer
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class PermalinkIndexDeleteCommand extends ApplicationEvent {
|
||||
private final ExtensionLocator locator;
|
||||
|
||||
public PermalinkIndexDeleteCommand(Object source, ExtensionLocator locator) {
|
||||
super(source);
|
||||
this.locator = locator;
|
||||
}
|
||||
|
||||
public ExtensionLocator getLocator() {
|
||||
return locator;
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import run.halo.app.content.permalinks.ExtensionLocator;
|
||||
|
||||
/**
|
||||
* <p>Send a command to update a piece of data from {@link PermalinkIndexer}.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @see PermalinkIndexer
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class PermalinkIndexUpdateCommand extends ApplicationEvent {
|
||||
private final ExtensionLocator locator;
|
||||
private final String permalink;
|
||||
|
||||
public PermalinkIndexUpdateCommand(Object source, ExtensionLocator locator, String permalink) {
|
||||
super(source);
|
||||
this.locator = locator;
|
||||
this.permalink = permalink;
|
||||
}
|
||||
|
||||
public ExtensionLocator getLocator() {
|
||||
return locator;
|
||||
}
|
||||
|
||||
public String getPermalink() {
|
||||
return permalink;
|
||||
}
|
||||
}
|
|
@ -1,251 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.permalinks.ExtensionLocator;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
|
||||
/**
|
||||
* <p>Permalink indexer for lookup extension's name and slug by permalink.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PermalinkIndexer {
|
||||
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
|
||||
|
||||
private final Map<GvkName, String> gvkNamePermalinkLookup = new HashMap<>();
|
||||
private final Map<String, ExtensionLocator> permalinkLocatorLookup = new HashMap<>();
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public PermalinkIndexer(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register extension and permalink mapping.
|
||||
*
|
||||
* @param locator extension locator to hold the gvk and name and slug
|
||||
* @param permalink extension permalink for template route
|
||||
*/
|
||||
public void register(ExtensionLocator locator, String permalink) {
|
||||
readWriteLock.writeLock().lock();
|
||||
try {
|
||||
GvkName gvkName = new GvkName(locator.gvk(), locator.name());
|
||||
gvkNamePermalinkLookup.put(gvkName, permalink);
|
||||
permalinkLocatorLookup.put(permalink, locator);
|
||||
publishEvent(gvkName, null, permalink);
|
||||
} finally {
|
||||
readWriteLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void publishEvent(GvkName gvkName, String oldPermalink, String newPermalink) {
|
||||
applicationContext.publishEvent(
|
||||
new PermalinkIndexChangedEvent(this, gvkName, oldPermalink, newPermalink));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove extension and permalink mapping.
|
||||
*
|
||||
* @param locator extension info
|
||||
*/
|
||||
public void remove(ExtensionLocator locator) {
|
||||
readWriteLock.writeLock().lock();
|
||||
try {
|
||||
GvkName gvkName = new GvkName(locator.gvk(), locator.name());
|
||||
String permalink = gvkNamePermalinkLookup.remove(gvkName);
|
||||
if (permalink != null) {
|
||||
permalinkLocatorLookup.remove(permalink);
|
||||
publishEvent(gvkName, permalink, null);
|
||||
}
|
||||
} finally {
|
||||
readWriteLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets permalink by {@link GroupVersionKind}.
|
||||
*
|
||||
* @param gvk group version kind
|
||||
* @return permalinks
|
||||
*/
|
||||
@NonNull
|
||||
public List<String> getPermalinks(GroupVersionKind gvk) {
|
||||
readWriteLock.readLock().lock();
|
||||
try {
|
||||
return gvkNamePermalinkLookup.entrySet()
|
||||
.stream()
|
||||
.filter(entry -> entry.getKey().gvk().equals(gvk))
|
||||
.map(Map.Entry::getValue)
|
||||
.toList();
|
||||
} finally {
|
||||
readWriteLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup extension info by permalink.
|
||||
*
|
||||
* @param permalink extension permalink for theme template route
|
||||
* @return extension locator
|
||||
*/
|
||||
@Nullable
|
||||
public ExtensionLocator lookup(String permalink) {
|
||||
readWriteLock.readLock().lock();
|
||||
try {
|
||||
return permalinkLocatorLookup.get(permalink);
|
||||
} finally {
|
||||
readWriteLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup extension permalink by {@link GroupVersionKind} and {@code name}.
|
||||
*
|
||||
* @param gvk group version kind
|
||||
* @param name extension name
|
||||
* @return {@code true} if contains, otherwise {@code false}
|
||||
*/
|
||||
public boolean containsName(GroupVersionKind gvk, String name) {
|
||||
readWriteLock.readLock().lock();
|
||||
try {
|
||||
return gvkNamePermalinkLookup.containsKey(new GvkName(gvk, name));
|
||||
} finally {
|
||||
readWriteLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup extension permalink by {@link GroupVersionKind} and {@code slug}.
|
||||
*
|
||||
* @param gvk group version kind
|
||||
* @param slug extension slug
|
||||
* @return {@code true} if contains, otherwise {@code false}
|
||||
*/
|
||||
public boolean containsSlug(GroupVersionKind gvk, String slug) {
|
||||
readWriteLock.readLock().lock();
|
||||
try {
|
||||
return permalinkLocatorLookup.values()
|
||||
.stream()
|
||||
.anyMatch(locator -> locator.gvk().equals(gvk)
|
||||
&& locator.slug().equals(slug));
|
||||
} finally {
|
||||
readWriteLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup extension name by resource slug.
|
||||
*
|
||||
* @param gvk extension's {@link GroupVersionKind}
|
||||
* @param slug extension resource slug
|
||||
* @return extension resource name specified by resource slug
|
||||
*/
|
||||
public String getNameBySlug(GroupVersionKind gvk, String slug) {
|
||||
readWriteLock.readLock().lock();
|
||||
try {
|
||||
return permalinkLocatorLookup.values()
|
||||
.stream()
|
||||
.filter(locator -> locator.gvk().equals(gvk)
|
||||
&& locator.slug().equals(slug))
|
||||
.findFirst()
|
||||
.map(ExtensionLocator::name)
|
||||
.orElseThrow();
|
||||
} finally {
|
||||
readWriteLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extension name by permalink.
|
||||
*
|
||||
* @param gvk is GroupVersionKind of extension
|
||||
* @param permalink is encoded permalink
|
||||
* @return extension name or null
|
||||
*/
|
||||
@Nullable
|
||||
public String getNameByPermalink(GroupVersionKind gvk, String permalink) {
|
||||
readWriteLock.readLock().lock();
|
||||
try {
|
||||
var locator = permalinkLocatorLookup.get(permalink);
|
||||
return locator == null ? null : locator.name();
|
||||
} finally {
|
||||
readWriteLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only for test.
|
||||
*
|
||||
* @return permalinkLookup map size
|
||||
*/
|
||||
protected long gvkNamePermalinkMapSize() {
|
||||
return gvkNamePermalinkLookup.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Only for test.
|
||||
*
|
||||
* @return permalinkLocatorMap map size
|
||||
*/
|
||||
protected long permalinkLocatorMapSize() {
|
||||
return permalinkLocatorLookup.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a record to the {@link PermalinkIndexer}.
|
||||
* If permalink already exists, it will not be added to indexer
|
||||
*
|
||||
* @param addCommand a command to add a record to {@link PermalinkIndexer}
|
||||
*/
|
||||
@EventListener(PermalinkIndexAddCommand.class)
|
||||
public void onPermalinkAdd(PermalinkIndexAddCommand addCommand) {
|
||||
if (checkPermalinkExists(addCommand.getLocator(), addCommand.getPermalink())) {
|
||||
// TODO send an extension event to log this error
|
||||
log.error("Permalink [{}] already exists, you can try to change the slug [{}].",
|
||||
addCommand.getPermalink(), addCommand.getLocator());
|
||||
return;
|
||||
}
|
||||
register(addCommand.getLocator(), addCommand.getPermalink());
|
||||
}
|
||||
|
||||
@EventListener(PermalinkIndexDeleteCommand.class)
|
||||
public void onPermalinkDelete(PermalinkIndexDeleteCommand deleteCommand) {
|
||||
remove(deleteCommand.getLocator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a {@link PermalinkIndexer} record by {@link ExtensionLocator} and permalink.
|
||||
* If permalink already exists, it will not be updated
|
||||
*
|
||||
* @param updateCommand a command to update an indexer record
|
||||
*/
|
||||
@EventListener(PermalinkIndexUpdateCommand.class)
|
||||
public void onPermalinkUpdate(PermalinkIndexUpdateCommand updateCommand) {
|
||||
if (checkPermalinkExists(updateCommand.getLocator(), updateCommand.getPermalink())) {
|
||||
// TODO send an extension event to log this error
|
||||
log.error("Permalink [{}] already exists, you can try to change the slug [{}].",
|
||||
updateCommand.getPermalink(), updateCommand.getLocator());
|
||||
return;
|
||||
}
|
||||
remove(updateCommand.getLocator());
|
||||
register(updateCommand.getLocator(), updateCommand.getPermalink());
|
||||
}
|
||||
|
||||
private boolean checkPermalinkExists(ExtensionLocator locator, String permalink) {
|
||||
ExtensionLocator lookup = lookup(permalink);
|
||||
return lookup != null && !lookup.equals(locator);
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
|
||||
/**
|
||||
* <p>The {@link PermalinkPatternProvider} used to obtain permalink rules according to specific
|
||||
* template names.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class PermalinkPatternProvider {
|
||||
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
public PermalinkPatternProvider(SystemConfigurableEnvironmentFetcher environmentFetcher) {
|
||||
this.environmentFetcher = environmentFetcher;
|
||||
}
|
||||
|
||||
private SystemSetting.ThemeRouteRules getPermalinkRules() {
|
||||
return environmentFetcher.getConfigMapBlocking()
|
||||
.map(configMap -> {
|
||||
Map<String, String> data = configMap.getData();
|
||||
return data.get(SystemSetting.ThemeRouteRules.GROUP);
|
||||
})
|
||||
.map(routeRulesJson -> JsonUtils.jsonToObject(routeRulesJson,
|
||||
SystemSetting.ThemeRouteRules.class))
|
||||
.orElseGet(() -> {
|
||||
SystemSetting.ThemeRouteRules themeRouteRules = new SystemSetting.ThemeRouteRules();
|
||||
themeRouteRules.setArchives("archives");
|
||||
themeRouteRules.setPost("/archives/{slug}");
|
||||
themeRouteRules.setTags("tags");
|
||||
themeRouteRules.setCategories("categories");
|
||||
return themeRouteRules;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permalink pattern by template name.
|
||||
*
|
||||
* @param defaultTemplateEnum default templates
|
||||
* @return a pattern specified by the template name
|
||||
*/
|
||||
public String getPattern(DefaultTemplateEnum defaultTemplateEnum) {
|
||||
SystemSetting.ThemeRouteRules permalinkRules = getPermalinkRules();
|
||||
return switch (defaultTemplateEnum) {
|
||||
case INDEX, SINGLE_PAGE, AUTHOR -> null;
|
||||
case POST -> permalinkRules.getPost();
|
||||
case ARCHIVES -> permalinkRules.getArchives();
|
||||
case CATEGORY, CATEGORIES -> permalinkRules.getCategories();
|
||||
case TAG, TAGS -> permalinkRules.getTags();
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
|
||||
import run.halo.app.content.permalinks.PostPermalinkPolicy;
|
||||
import run.halo.app.content.permalinks.TagPermalinkPolicy;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
|
||||
/**
|
||||
* Permalink refresh handler.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PermalinkRefreshHandler implements ApplicationListener<PermalinkRuleChangedEvent> {
|
||||
private final ExtensionClient client;
|
||||
private final PostPermalinkPolicy postPermalinkPolicy;
|
||||
private final TagPermalinkPolicy tagPermalinkPolicy;
|
||||
private final CategoryPermalinkPolicy categoryPermalinkPolicy;
|
||||
|
||||
public PermalinkRefreshHandler(ExtensionClient client,
|
||||
PostPermalinkPolicy postPermalinkPolicy,
|
||||
TagPermalinkPolicy tagPermalinkPolicy,
|
||||
CategoryPermalinkPolicy categoryPermalinkPolicy) {
|
||||
this.client = client;
|
||||
this.postPermalinkPolicy = postPermalinkPolicy;
|
||||
this.tagPermalinkPolicy = tagPermalinkPolicy;
|
||||
this.categoryPermalinkPolicy = categoryPermalinkPolicy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(@NonNull PermalinkRuleChangedEvent event) {
|
||||
DefaultTemplateEnum template = event.getTemplate();
|
||||
log.debug("Refresh permalink for template [{}]", template.getValue());
|
||||
switch (template) {
|
||||
case POST -> updatePostPermalink();
|
||||
case CATEGORIES, CATEGORY -> updateCategoryPermalink();
|
||||
case TAGS, TAG -> updateTagPermalink();
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePostPermalink() {
|
||||
String pattern = postPermalinkPolicy.pattern();
|
||||
log.debug("Update post permalink by new policy [{}]", pattern);
|
||||
client.list(Post.class, null, null)
|
||||
.forEach(post -> {
|
||||
String oldPermalink = post.getStatusOrDefault().getPermalink();
|
||||
String permalink = postPermalinkPolicy.permalink(post);
|
||||
post.getStatusOrDefault().setPermalink(permalink);
|
||||
if (StringUtils.equals(oldPermalink, permalink)) {
|
||||
return;
|
||||
}
|
||||
// update permalink
|
||||
client.update(post);
|
||||
|
||||
postPermalinkPolicy.onPermalinkUpdate(post);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateCategoryPermalink() {
|
||||
String pattern = categoryPermalinkPolicy.pattern();
|
||||
log.debug("Update category and categories permalink by new policy [{}]", pattern);
|
||||
client.list(Category.class, null, null)
|
||||
.forEach(category -> {
|
||||
String oldPermalink = category.getStatusOrDefault().getPermalink();
|
||||
String permalink = categoryPermalinkPolicy.permalink(category);
|
||||
category.getStatusOrDefault().setPermalink(permalink);
|
||||
if (StringUtils.equals(oldPermalink, permalink)) {
|
||||
return;
|
||||
}
|
||||
// update permalink
|
||||
client.update(category);
|
||||
|
||||
categoryPermalinkPolicy.onPermalinkUpdate(category);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateTagPermalink() {
|
||||
String pattern = tagPermalinkPolicy.pattern();
|
||||
log.debug("Update tag and tags permalink by new policy [{}]", pattern);
|
||||
client.list(Tag.class, null, null)
|
||||
.forEach(tag -> {
|
||||
String oldPermalink = tag.getStatusOrDefault().getPermalink();
|
||||
String permalink = tagPermalinkPolicy.permalink(tag);
|
||||
tag.getStatusOrDefault().setPermalink(permalink);
|
||||
if (StringUtils.equals(oldPermalink, permalink)) {
|
||||
return;
|
||||
}
|
||||
// update permalink
|
||||
client.update(tag);
|
||||
|
||||
tagPermalinkPolicy.onPermalinkUpdate(tag);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,224 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* A router tree implementation based on radix tree.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class RadixRouterTree extends RadixTree<HandlerFunction<ServerResponse>> {
|
||||
|
||||
@Override
|
||||
public void insert(String key, HandlerFunction<ServerResponse> value)
|
||||
throws IllegalArgumentException {
|
||||
super.insert(key, value);
|
||||
if (log.isDebugEnabled()) {
|
||||
checkIndices();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String key) {
|
||||
boolean result = super.delete(key);
|
||||
if (log.isDebugEnabled()) {
|
||||
checkIndices();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* <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 request server request
|
||||
* @return a handler function if matched, otherwise null
|
||||
*/
|
||||
public HandlerFunction<ServerResponse> match(ServerRequest request) {
|
||||
String path = pathToFind(request);
|
||||
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 + "}");
|
||||
}
|
||||
}
|
||||
PathPattern.PathMatchInfo info =
|
||||
bestMatch.matchAndExtract(request.requestPath().pathWithinApplication());
|
||||
if (info != null) {
|
||||
mergeAttributes(request, info.getUriVariables(), bestMatch);
|
||||
}
|
||||
return find(bestMatch.getPatternString());
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO Optimize parameter route matching query.
|
||||
* Router 仅匹配请求方法和请求的 URL 路径, 形如 /?p=post-name 是 URL query,而不是 URL 路径的一部分。
|
||||
*/
|
||||
static String pathToFind(ServerRequest request) {
|
||||
String requestPath = processRequestPath(request.path());
|
||||
MultiValueMap<String, String> queryParams = request.queryParams();
|
||||
// 文章的 permalink 规则需要对 p 参数规则特殊处理
|
||||
if (requestPath.equals("/") && queryParams.containsKey("p")) {
|
||||
// post special route path
|
||||
String postSlug = queryParams.getFirst("p");
|
||||
requestPath = requestPath + "?p=" + postSlug;
|
||||
}
|
||||
// /categories/{slug}/page/{page} 和 /tags/{slug}/page/{page} 需要去掉 page 部分
|
||||
if (PageUrlUtils.isPageUrl(requestPath)) {
|
||||
int i = requestPath.lastIndexOf("/page/");
|
||||
if (i != -1) {
|
||||
requestPath = requestPath.substring(0, i);
|
||||
}
|
||||
}
|
||||
requestPath = StringUtils.removeEnd(requestPath, "/");
|
||||
return StringUtils.prependIfMissing(requestPath, "/");
|
||||
}
|
||||
|
||||
private static void mergeAttributes(ServerRequest request, Map<String, String> variables,
|
||||
PathPattern pattern) {
|
||||
Map<String, String> pathVariables = mergePathVariables(request.pathVariables(), variables);
|
||||
request.attributes().put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
|
||||
Collections.unmodifiableMap(pathVariables));
|
||||
|
||||
pattern = mergePatterns(
|
||||
(PathPattern) request.attributes().get(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE),
|
||||
pattern);
|
||||
request.attributes().put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, pattern);
|
||||
}
|
||||
|
||||
private static PathPattern mergePatterns(@Nullable PathPattern oldPattern,
|
||||
PathPattern newPattern) {
|
||||
if (oldPattern != null) {
|
||||
return oldPattern.combine(newPattern);
|
||||
} else {
|
||||
return newPattern;
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> mergePathVariables(Map<String, String> oldVariables,
|
||||
Map<String, String> newVariables) {
|
||||
|
||||
if (!newVariables.isEmpty()) {
|
||||
Map<String, String> mergedVariables = new LinkedHashMap<>(oldVariables);
|
||||
mergedVariables.putAll(newVariables);
|
||||
return mergedVariables;
|
||||
} else {
|
||||
return oldVariables;
|
||||
}
|
||||
}
|
||||
|
||||
private static String processRequestPath(String requestPath) {
|
||||
String path = StringUtils.prependIfMissing(requestPath, "/");
|
||||
return UriUtils.decode(path, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public boolean hasPatternSyntax(String pathPattern) {
|
||||
return pathPattern.indexOf('{') != -1 || pathPattern.indexOf(':') != -1
|
||||
|| pathPattern.indexOf('*') != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keys(paths) in trie, call recursion function
|
||||
* Time O(n), Space O(n), n is number of nodes in trie.
|
||||
*/
|
||||
public List<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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,360 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Implementation for {@link RadixTree Radix tree}.
|
||||
*
|
||||
* @author guqing
|
||||
*/
|
||||
@Data
|
||||
public class RadixTree<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);
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* Represents a node of a Radix tree {@link RadixTree}.
|
||||
*
|
||||
* @param <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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.util.UriUtils.encodePath;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RequestPredicates;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionOperator;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.Scheme;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.SinglePageFinder;
|
||||
import run.halo.app.theme.router.factories.ModelConst;
|
||||
|
||||
/**
|
||||
* The {@link SinglePageRoute} for route request to specific template <code>page.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SinglePageRoute
|
||||
implements RouterFunction<ServerResponse>, Reconciler<Reconciler.Request>, DisposableBean {
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class);
|
||||
|
||||
private final Map<NameSlugPair, HandlerFunction<ServerResponse>> quickRouteMap =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
private final SinglePageFinder singlePageFinder;
|
||||
|
||||
private final ViewNameResolver viewNameResolver;
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
|
||||
return Flux.fromIterable(routerFunctions())
|
||||
.concatMap(routerFunction -> routerFunction.route(request))
|
||||
.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(@NonNull RouterFunctions.Visitor visitor) {
|
||||
routerFunctions().forEach(routerFunction -> routerFunction.accept(visitor));
|
||||
}
|
||||
|
||||
private List<RouterFunction<ServerResponse>> routerFunctions() {
|
||||
return quickRouteMap.keySet().stream()
|
||||
.map(nameSlugPair -> {
|
||||
String routePath = singlePageRoute(nameSlugPair.slug());
|
||||
return RouterFunctions.route(GET(routePath)
|
||||
.and(RequestPredicates.accept(MediaType.TEXT_HTML)),
|
||||
handlerFunction(nameSlugPair.name()));
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(SinglePage.class, request.name())
|
||||
.ifPresent(page -> {
|
||||
if (ExtensionOperator.isDeleted(page)
|
||||
|| BooleanUtils.isTrue(page.getSpec().getDeleted())) {
|
||||
quickRouteMap.remove(NameSlugPair.from(page));
|
||||
return;
|
||||
}
|
||||
// put new one
|
||||
quickRouteMap.entrySet()
|
||||
.removeIf(entry -> entry.getKey().name().equals(request.name()));
|
||||
quickRouteMap.put(NameSlugPair.from(page), handlerFunction(request.name()));
|
||||
});
|
||||
return new Result(false, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return builder
|
||||
.extension(new SinglePage())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
quickRouteMap.clear();
|
||||
}
|
||||
|
||||
record NameSlugPair(String name, String slug) {
|
||||
public static NameSlugPair from(SinglePage page) {
|
||||
return new NameSlugPair(page.getMetadata().getName(), page.getSpec().getSlug());
|
||||
}
|
||||
}
|
||||
|
||||
String singlePageRoute(String slug) {
|
||||
var permalink = encodePath(slug, UTF_8);
|
||||
return StringUtils.prependIfMissing(permalink, "/");
|
||||
}
|
||||
|
||||
HandlerFunction<ServerResponse> handlerFunction(String name) {
|
||||
return request -> singlePageFinder.getByName(name)
|
||||
.flatMap(singlePageVo -> {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("groupVersionKind", gvk);
|
||||
model.put("plural", getPlural());
|
||||
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue());
|
||||
model.put("singlePage", singlePageVo);
|
||||
String template = singlePageVo.getSpec().getTemplate();
|
||||
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||
DefaultTemplateEnum.SINGLE_PAGE.getValue())
|
||||
.flatMap(viewName -> ServerResponse.ok().render(viewName, model));
|
||||
})
|
||||
.switchIfEmpty(
|
||||
Mono.error(new NotFoundException("Single page not found"))
|
||||
);
|
||||
}
|
||||
|
||||
private String getPlural() {
|
||||
GVK gvk = Scheme.getGvkFromType(SinglePage.class);
|
||||
return gvk.plural();
|
||||
}
|
||||
}
|
|
@ -1,40 +1,132 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SchemeInitializedEvent;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.factories.ArchiveRouteFactory;
|
||||
import run.halo.app.theme.router.factories.AuthorPostsRouteFactory;
|
||||
import run.halo.app.theme.router.factories.CategoriesRouteFactory;
|
||||
import run.halo.app.theme.router.factories.CategoryPostRouteFactory;
|
||||
import run.halo.app.theme.router.factories.IndexRouteFactory;
|
||||
import run.halo.app.theme.router.factories.PostRouteFactory;
|
||||
import run.halo.app.theme.router.factories.TagPostRouteFactory;
|
||||
import run.halo.app.theme.router.factories.TagsRouteFactory;
|
||||
|
||||
/**
|
||||
* <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 PermalinkHttpGetRouter}.
|
||||
* <p>The combination router of theme templates is used to render theme templates, but does not
|
||||
* include <code>page.html</code> templates which is processed separately.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @see PermalinkHttpGetRouter
|
||||
* @see SinglePageRoute
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class ThemeCompositeRouterFunction implements
|
||||
RouterFunction<ServerResponse> {
|
||||
@RequiredArgsConstructor
|
||||
public class ThemeCompositeRouterFunction implements RouterFunction<ServerResponse> {
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
private final PermalinkHttpGetRouter permalinkHttpGetRouter;
|
||||
private final ArchiveRouteFactory archiveRouteFactory;
|
||||
private final PostRouteFactory postRouteFactory;
|
||||
private final CategoriesRouteFactory categoriesRouteFactory;
|
||||
private final CategoryPostRouteFactory categoryPostRouteFactory;
|
||||
private final TagPostRouteFactory tagPostRouteFactory;
|
||||
private final TagsRouteFactory tagsRouteFactory;
|
||||
private final AuthorPostsRouteFactory authorPostsRouteFactory;
|
||||
private final IndexRouteFactory indexRouteFactory;
|
||||
|
||||
public ThemeCompositeRouterFunction(PermalinkHttpGetRouter permalinkHttpGetRouter) {
|
||||
this.permalinkHttpGetRouter = permalinkHttpGetRouter;
|
||||
}
|
||||
private List<RouterFunction<ServerResponse>> cachedRouters = List.of();
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
|
||||
// this router function only supports GET method
|
||||
if (!request.method().equals(HttpMethod.GET)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return Mono.justOrEmpty(permalinkHttpGetRouter.route(request));
|
||||
return Flux.fromIterable(cachedRouters)
|
||||
.concatMap(routerFunction -> routerFunction.route(request))
|
||||
.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(@NonNull RouterFunctions.Visitor visitor) {
|
||||
cachedRouters.forEach(routerFunction -> routerFunction.accept(visitor));
|
||||
}
|
||||
|
||||
List<RouterFunction<ServerResponse>> routerFunctions() {
|
||||
return transformedPatterns()
|
||||
.stream()
|
||||
.map(this::createRouterFunction)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private RouterFunction<ServerResponse> createRouterFunction(RoutePattern routePattern) {
|
||||
return switch (routePattern.identifier()) {
|
||||
case POST -> postRouteFactory.create(routePattern.pattern());
|
||||
case ARCHIVES -> archiveRouteFactory.create(routePattern.pattern());
|
||||
case CATEGORIES -> categoriesRouteFactory.create(routePattern.pattern());
|
||||
case CATEGORY -> categoryPostRouteFactory.create(routePattern.pattern());
|
||||
case TAGS -> tagsRouteFactory.create(routePattern.pattern());
|
||||
case TAG -> tagPostRouteFactory.create(routePattern.pattern());
|
||||
case AUTHOR -> authorPostsRouteFactory.create(routePattern.pattern());
|
||||
case INDEX -> indexRouteFactory.create(routePattern.pattern());
|
||||
default ->
|
||||
throw new IllegalStateException("Unexpected value: " + routePattern.identifier());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the {@link #cachedRouters} when the permalink rule is changed.
|
||||
*
|
||||
* @param event {@link SchemeInitializedEvent} or {@link PermalinkRuleChangedEvent}
|
||||
*/
|
||||
@EventListener({SchemeInitializedEvent.class, PermalinkRuleChangedEvent.class})
|
||||
public void onSchemeInitializedEvent(@NonNull ApplicationEvent event) {
|
||||
this.cachedRouters = routerFunctions();
|
||||
}
|
||||
|
||||
record RoutePattern(DefaultTemplateEnum identifier, String pattern) {
|
||||
}
|
||||
|
||||
private List<RoutePattern> transformedPatterns() {
|
||||
List<RoutePattern> routePatterns = new ArrayList<>();
|
||||
|
||||
SystemSetting.ThemeRouteRules rules =
|
||||
environmentFetcher.fetch(SystemSetting.ThemeRouteRules.GROUP,
|
||||
SystemSetting.ThemeRouteRules.class)
|
||||
.blockOptional()
|
||||
.orElse(SystemSetting.ThemeRouteRules.empty());
|
||||
String post = rules.getPost();
|
||||
routePatterns.add(new RoutePattern(DefaultTemplateEnum.POST, post));
|
||||
|
||||
String archives = rules.getArchives();
|
||||
routePatterns.add(
|
||||
new RoutePattern(DefaultTemplateEnum.ARCHIVES, archives));
|
||||
|
||||
String categories = rules.getCategories();
|
||||
routePatterns.add(
|
||||
new RoutePattern(DefaultTemplateEnum.CATEGORIES, categories));
|
||||
routePatterns.add(
|
||||
new RoutePattern(DefaultTemplateEnum.CATEGORY, categories));
|
||||
|
||||
String tags = rules.getTags();
|
||||
routePatterns.add(new RoutePattern(DefaultTemplateEnum.TAGS, tags));
|
||||
routePatterns.add(new RoutePattern(DefaultTemplateEnum.TAG, tags));
|
||||
|
||||
// Add the index route to the end to prevent conflict with the queryParam rule of the post
|
||||
routePatterns.add(new RoutePattern(DefaultTemplateEnum.INDEX, "/"));
|
||||
return routePatterns;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RequestPredicate;
|
||||
import org.springframework.web.reactive.function.server.RequestPredicates;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.vo.PostArchiveVo;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
/**
|
||||
* The {@link ArchiveRouteFactory} for generate {@link RouterFunction} specific to the template
|
||||
* <code>posts.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class ArchiveRouteFactory implements RouteFactory {
|
||||
|
||||
private final PostFinder postFinder;
|
||||
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> create(String prefix) {
|
||||
RequestPredicate requestPredicate = patterns(prefix).stream()
|
||||
.map(RequestPredicates::GET)
|
||||
.reduce(req -> false, RequestPredicate::or)
|
||||
.and(accept(MediaType.TEXT_HTML));
|
||||
return RouterFunctions.route(requestPredicate, handlerFunction());
|
||||
}
|
||||
|
||||
HandlerFunction<ServerResponse> handlerFunction() {
|
||||
return request -> {
|
||||
String templateName = DefaultTemplateEnum.ARCHIVES.getValue();
|
||||
return ServerResponse.ok()
|
||||
.render(templateName,
|
||||
Map.of("archives", archivePosts(request),
|
||||
ModelConst.TEMPLATE_ID, templateName)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private List<String> patterns(String prefix) {
|
||||
return List.of(
|
||||
StringUtils.prependIfMissing(prefix, "/"),
|
||||
PathUtils.combinePath(prefix, "/page/{page:\\d+}"),
|
||||
PathUtils.combinePath(prefix, "/{year:\\d{4}}"),
|
||||
PathUtils.combinePath(prefix, "/{year:\\d{4}}/page/{page:\\d+}"),
|
||||
PathUtils.combinePath(prefix, "/{year:\\d{4}}/{month:\\d{2}}"),
|
||||
PathUtils.combinePath(prefix,
|
||||
"/{year:\\d{4}}/{month:\\d{2}}/page/{page:\\d+}")
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<PostArchiveVo>> archivePosts(ServerRequest request) {
|
||||
ArchivePathVariables variables = ArchivePathVariables.from(request);
|
||||
int pageNum = pageNumInPathVariable(request);
|
||||
String requestPath = request.path();
|
||||
return configuredPageSize(environmentFetcher)
|
||||
.flatMap(pageSize -> postFinder.archives(pageNum, pageSize, variables.getYear(),
|
||||
variables.getMonth()))
|
||||
.map(list -> new UrlContextListResult.Builder<PostArchiveVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(PageUrlUtils.nextPageUrl(requestPath, totalPage(list)))
|
||||
.prevUrl(PageUrlUtils.prevPageUrl(requestPath))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Data
|
||||
static class ArchivePathVariables {
|
||||
String year;
|
||||
String month;
|
||||
String page;
|
||||
|
||||
static ArchivePathVariables from(ServerRequest request) {
|
||||
Map<String, String> variables = request.pathVariables();
|
||||
return JsonUtils.mapToObject(variables, ArchivePathVariables.class);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
import run.halo.app.theme.finders.vo.UserVo;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
/**
|
||||
* The {@link AuthorPostsRouteFactory} for generate {@link RouterFunction} specific to the template
|
||||
* <code>index.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class AuthorPostsRouteFactory implements RouteFactory {
|
||||
|
||||
private final PostFinder postFinder;
|
||||
private final ReactiveExtensionClient client;
|
||||
private SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> create(String pattern) {
|
||||
return RouterFunctions
|
||||
.route(GET("/authors/{name}").or(GET("/authors/{name}/page/{page}"))
|
||||
.and(accept(MediaType.TEXT_HTML)), handlerFunction());
|
||||
}
|
||||
|
||||
HandlerFunction<ServerResponse> handlerFunction() {
|
||||
return request -> {
|
||||
String name = request.pathVariable("name");
|
||||
return ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.AUTHOR.getValue(),
|
||||
Map.of("author", getByName(name),
|
||||
"posts", postList(request, name),
|
||||
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.AUTHOR.getValue()
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<ListedPostVo>> postList(ServerRequest request, String name) {
|
||||
String path = request.path();
|
||||
int pageNum = pageNumInPathVariable(request);
|
||||
return configuredPageSize(environmentFetcher)
|
||||
.flatMap(pageSize -> postFinder.listByOwner(pageNum, pageSize, name))
|
||||
.map(list -> new UrlContextListResult.Builder<ListedPostVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
|
||||
.prevUrl(PageUrlUtils.prevPageUrl(path))
|
||||
.build());
|
||||
}
|
||||
|
||||
private Mono<UserVo> getByName(String name) {
|
||||
return client.get(User.class, name)
|
||||
.map(UserVo::from);
|
||||
}
|
||||
}
|
|
@ -1,17 +1,21 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
|
||||
/**
|
||||
* Categories router strategy for generate {@link HandlerFunction} specific to the template
|
||||
* The {@link CategoriesRouteFactory} for generate {@link RouterFunction} specific to the
|
||||
* template
|
||||
* <code>categories.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
|
@ -19,24 +23,20 @@ import run.halo.app.theme.finders.CategoryFinder;
|
|||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class CategoriesRouteStrategy implements ListPageRouteHandlerStrategy {
|
||||
public class CategoriesRouteFactory implements RouteFactory {
|
||||
|
||||
private final CategoryFinder categoryFinder;
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler() {
|
||||
public RouterFunction<ServerResponse> create(String prefix) {
|
||||
return RouterFunctions.route(GET(StringUtils.prependIfMissing(prefix, "/")),
|
||||
handlerFunction());
|
||||
}
|
||||
|
||||
HandlerFunction<ServerResponse> handlerFunction() {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.CATEGORIES.getValue(),
|
||||
Map.of("categories", categoryFinder.listAsTree(),
|
||||
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORIES.getValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRouterPaths(String prefix) {
|
||||
return List.of(StringUtils.prependIfMissing(prefix, "/"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(DefaultTemplateEnum template) {
|
||||
return DefaultTemplateEnum.CATEGORIES.equals(template);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.vo.CategoryVo;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
import run.halo.app.theme.router.ViewNameResolver;
|
||||
|
||||
/**
|
||||
* The {@link CategoryPostRouteFactory} for generate {@link RouterFunction} specific to the template
|
||||
* <code>category.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class CategoryPostRouteFactory implements RouteFactory {
|
||||
|
||||
private final PostFinder postFinder;
|
||||
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
private final ReactiveExtensionClient client;
|
||||
private final ViewNameResolver viewNameResolver;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> create(String prefix) {
|
||||
return RouterFunctions.route(GET(PathUtils.combinePath(prefix, "/{slug}"))
|
||||
.or(GET(PathUtils.combinePath(prefix, "/{slug}/page/{page:\\d+}")))
|
||||
.and(accept(MediaType.TEXT_HTML)), handlerFunction());
|
||||
}
|
||||
|
||||
HandlerFunction<ServerResponse> handlerFunction() {
|
||||
return request -> {
|
||||
String slug = request.pathVariable("slug");
|
||||
return fetchBySlug(slug)
|
||||
.flatMap(categoryVo -> {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORY.getValue());
|
||||
model.put("posts",
|
||||
postListByCategoryName(categoryVo.getMetadata().getName(), request));
|
||||
model.put("category", categoryVo);
|
||||
String template = categoryVo.getSpec().getTemplate();
|
||||
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||
DefaultTemplateEnum.CATEGORY.getValue())
|
||||
.flatMap(viewName -> ServerResponse.ok().render(viewName, model));
|
||||
})
|
||||
.switchIfEmpty(
|
||||
Mono.error(new NotFoundException("Category not found with slug: " + slug)));
|
||||
};
|
||||
}
|
||||
|
||||
Mono<CategoryVo> fetchBySlug(String slug) {
|
||||
return client.list(Category.class, category -> category.getSpec().getSlug().equals(slug)
|
||||
&& category.getMetadata().getDeletionTimestamp() == null, null)
|
||||
.next()
|
||||
.map(CategoryVo::from);
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<ListedPostVo>> postListByCategoryName(String name,
|
||||
ServerRequest request) {
|
||||
String path = request.path();
|
||||
int pageNum = pageNumInPathVariable(request);
|
||||
return configuredPageSize(environmentFetcher)
|
||||
.flatMap(pageSize -> postFinder.listByCategory(pageNum, pageSize, name))
|
||||
.map(list -> new UrlContextListResult.Builder<ListedPostVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
|
||||
.prevUrl(PageUrlUtils.prevPageUrl(path))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.pageNum;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
import static run.halo.app.theme.router.strategy.ModelConst.DEFAULT_PAGE_SIZE;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
@ -21,7 +22,7 @@ import run.halo.app.theme.router.PageUrlUtils;
|
|||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
/**
|
||||
* The {@link IndexRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* The {@link IndexRouteFactory} for generate {@link RouterFunction} specific to the template
|
||||
* <code>index.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
|
@ -29,38 +30,35 @@ import run.halo.app.theme.router.UrlContextListResult;
|
|||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class IndexRouteStrategy implements ListPageRouteHandlerStrategy {
|
||||
public class IndexRouteFactory implements RouteFactory {
|
||||
|
||||
private final PostFinder postFinder;
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
private Mono<UrlContextListResult<ListedPostVo>> postList(ServerRequest request) {
|
||||
String path = request.path();
|
||||
return environmentFetcher.fetchPost()
|
||||
.map(p -> defaultIfNull(p.getPostPageSize(), DEFAULT_PAGE_SIZE))
|
||||
.flatMap(pageSize -> postFinder.list(pageNum(request), pageSize))
|
||||
.map(list -> new UrlContextListResult.Builder<ListedPostVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
|
||||
.prevUrl(PageUrlUtils.prevPageUrl(path))
|
||||
.build());
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> create(String pattern) {
|
||||
return RouterFunctions
|
||||
.route(GET("/").or(GET("/page/{page}")
|
||||
.or(GET("/index")).or(GET("/index/page/{page}"))
|
||||
.and(accept(MediaType.TEXT_HTML))), handlerFunction());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler() {
|
||||
HandlerFunction<ServerResponse> handlerFunction() {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.INDEX.getValue(),
|
||||
Map.of("posts", postList(request),
|
||||
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.INDEX.getValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRouterPaths(String pattern) {
|
||||
return List.of("/", "/index");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(DefaultTemplateEnum template) {
|
||||
return DefaultTemplateEnum.INDEX.equals(template);
|
||||
private Mono<UrlContextListResult<ListedPostVo>> postList(ServerRequest request) {
|
||||
String path = request.path();
|
||||
return configuredPageSize(environmentFetcher)
|
||||
.flatMap(pageSize -> postFinder.list(pageNumInPathVariable(request), pageSize))
|
||||
.map(list -> new UrlContextListResult.Builder<ListedPostVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
|
||||
.prevUrl(PageUrlUtils.prevPageUrl(path))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
package run.halo.app.theme.router.factories;
|
||||
|
||||
/**
|
||||
* Static variable keys for view model.
|
|
@ -0,0 +1,236 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RequestPredicate;
|
||||
import org.springframework.web.reactive.function.server.RequestPredicates;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.impl.PostFinderImpl;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
import run.halo.app.theme.router.ViewNameResolver;
|
||||
|
||||
/**
|
||||
* The {@link PostRouteFactory} for generate {@link RouterFunction} specific to the template
|
||||
* <code>post.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class PostRouteFactory implements RouteFactory {
|
||||
|
||||
private final PostFinder postFinder;
|
||||
|
||||
private final ViewNameResolver viewNameResolver;
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> create(String pattern) {
|
||||
PatternParser postParamPredicate =
|
||||
new PatternParser(pattern);
|
||||
if (postParamPredicate.isQueryParamPattern()) {
|
||||
RequestPredicate requestPredicate = postParamPredicate.toRequestPredicate();
|
||||
return RouterFunctions.route(GET("/")
|
||||
.and(requestPredicate), queryParamHandlerFunction(postParamPredicate));
|
||||
}
|
||||
return RouterFunctions
|
||||
.route(GET(pattern).and(accept(MediaType.TEXT_HTML)), handlerFunction());
|
||||
}
|
||||
|
||||
HandlerFunction<ServerResponse> queryParamHandlerFunction(PatternParser paramPredicate) {
|
||||
return request -> {
|
||||
Map<String, String> variables = mergedVariables(request);
|
||||
PostPatternVariable patternVariable = new PostPatternVariable();
|
||||
Optional.ofNullable(variables.get(paramPredicate.getQueryParamName()))
|
||||
.ifPresent(value -> {
|
||||
switch (paramPredicate.getPlaceholderName()) {
|
||||
case "name" -> patternVariable.setName(value);
|
||||
case "slug" -> patternVariable.setSlug(value);
|
||||
default ->
|
||||
throw new IllegalArgumentException("Unsupported query param predicate");
|
||||
}
|
||||
});
|
||||
return postResponse(request, patternVariable);
|
||||
};
|
||||
}
|
||||
|
||||
HandlerFunction<ServerResponse> handlerFunction() {
|
||||
return request -> {
|
||||
PostPatternVariable patternVariable = PostPatternVariable.from(request);
|
||||
return postResponse(request, patternVariable);
|
||||
};
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Mono<ServerResponse> postResponse(ServerRequest request,
|
||||
PostPatternVariable patternVariable) {
|
||||
Mono<PostVo> postVoMono = bestMatchPost(patternVariable);
|
||||
return postVoMono
|
||||
.flatMap(postVo -> {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("groupVersionKind", GroupVersionKind.fromExtension(Post.class));
|
||||
GVK gvk = Post.class.getAnnotation(GVK.class);
|
||||
model.put("plural", gvk.plural());
|
||||
model.put("post", postVo);
|
||||
|
||||
String template = postVo.getSpec().getTemplate();
|
||||
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||
DefaultTemplateEnum.POST.getValue())
|
||||
.flatMap(templateName -> ServerResponse.ok().render(templateName, model));
|
||||
});
|
||||
}
|
||||
|
||||
Mono<PostVo> bestMatchPost(PostPatternVariable variable) {
|
||||
return postsByPredicates(variable)
|
||||
.filter(post -> {
|
||||
Map<String, String> labels = ExtensionUtil.nullSafeLabels(post);
|
||||
return matchIfPresent(variable.getName(), post.getMetadata().getName())
|
||||
&& matchIfPresent(variable.getSlug(), post.getSpec().getSlug())
|
||||
&& matchIfPresent(variable.getYear(), labels.get(Post.ARCHIVE_YEAR_LABEL))
|
||||
&& matchIfPresent(variable.getMonth(), labels.get(Post.ARCHIVE_MONTH_LABEL))
|
||||
&& matchIfPresent(variable.getDay(), labels.get(Post.ARCHIVE_DAY_LABEL));
|
||||
})
|
||||
.next()
|
||||
.flatMap(post -> postFinder.getByName(post.getMetadata().getName()))
|
||||
.switchIfEmpty(Mono.error(new NotFoundException("Post not found")));
|
||||
}
|
||||
|
||||
Flux<Post> postsByPredicates(PostPatternVariable patternVariable) {
|
||||
if (StringUtils.isNotBlank(patternVariable.getName())) {
|
||||
return fetchPostsByName(patternVariable.getName());
|
||||
}
|
||||
if (StringUtils.isNotBlank(patternVariable.getSlug())) {
|
||||
return fetchPostsBySlug(patternVariable.getSlug());
|
||||
}
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
private Flux<Post> fetchPostsByName(String name) {
|
||||
return client.fetch(Post.class, name)
|
||||
.filter(PostFinderImpl.FIXED_PREDICATE)
|
||||
.flux();
|
||||
}
|
||||
|
||||
private Flux<Post> fetchPostsBySlug(String slug) {
|
||||
return client.list(Post.class,
|
||||
post -> PostFinderImpl.FIXED_PREDICATE.test(post)
|
||||
&& matchIfPresent(slug, post.getSpec().getSlug()),
|
||||
null);
|
||||
}
|
||||
|
||||
private boolean matchIfPresent(String variable, String target) {
|
||||
return StringUtils.isBlank(variable) || StringUtils.equals(target, variable);
|
||||
}
|
||||
|
||||
@Data
|
||||
static class PostPatternVariable {
|
||||
String name;
|
||||
String slug;
|
||||
String year;
|
||||
String month;
|
||||
String day;
|
||||
|
||||
static PostPatternVariable from(ServerRequest request) {
|
||||
Map<String, String> variables = mergedVariables(request);
|
||||
return JsonUtils.mapToObject(variables, PostPatternVariable.class);
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, String> mergedVariables(ServerRequest request) {
|
||||
Map<String, String> pathVariables = request.pathVariables();
|
||||
MultiValueMap<String, String> queryParams = request.queryParams();
|
||||
Map<String, String> mergedVariables = new LinkedHashMap<>();
|
||||
for (String paramKey : queryParams.keySet()) {
|
||||
mergedVariables.put(paramKey, queryParams.getFirst(paramKey));
|
||||
}
|
||||
// path variables higher priority will override query params
|
||||
mergedVariables.putAll(pathVariables);
|
||||
return mergedVariables;
|
||||
}
|
||||
|
||||
static class PatternParser {
|
||||
private static final Pattern PATTERN_COMPILE = Pattern.compile("([^&?]*)=\\{(.*?)\\}(&|$)");
|
||||
private static final Cache<String, Matcher> MATCHER_CACHE = CacheBuilder.newBuilder()
|
||||
.maximumSize(5)
|
||||
.build();
|
||||
|
||||
private final String pattern;
|
||||
private String paramName;
|
||||
private String placeholderName;
|
||||
private final boolean isQueryParamPattern;
|
||||
|
||||
PatternParser(String pattern) {
|
||||
this.pattern = pattern;
|
||||
Matcher matcher = patternToMatcher(pattern);
|
||||
if (matcher.find()) {
|
||||
this.paramName = matcher.group(1);
|
||||
this.placeholderName = matcher.group(2);
|
||||
this.isQueryParamPattern = true;
|
||||
} else {
|
||||
this.isQueryParamPattern = false;
|
||||
}
|
||||
}
|
||||
|
||||
Matcher patternToMatcher(String pattern) {
|
||||
try {
|
||||
return MATCHER_CACHE.get(pattern, () -> PATTERN_COMPILE.matcher(pattern));
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
RequestPredicate toRequestPredicate() {
|
||||
if (!this.isQueryParamPattern) {
|
||||
throw new IllegalStateException("Not a query param pattern: " + pattern);
|
||||
}
|
||||
|
||||
return RequestPredicates.queryParam(paramName, value -> true);
|
||||
}
|
||||
|
||||
public String getPlaceholderName() {
|
||||
return this.placeholderName;
|
||||
}
|
||||
|
||||
public String getQueryParamName() {
|
||||
return this.paramName;
|
||||
}
|
||||
|
||||
public boolean isQueryParamPattern() {
|
||||
return isQueryParamPattern;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface RouteFactory {
|
||||
RouterFunction<ServerResponse> create(String pattern);
|
||||
|
||||
default Mono<Integer> configuredPageSize(
|
||||
SystemConfigurableEnvironmentFetcher environmentFetcher) {
|
||||
return environmentFetcher.fetchPost()
|
||||
.map(p -> defaultIfNull(p.getTagPageSize(), ModelConst.DEFAULT_PAGE_SIZE));
|
||||
}
|
||||
|
||||
default int pageNumInPathVariable(ServerRequest request) {
|
||||
String page = request.pathVariables().get("page");
|
||||
return NumberUtils.toInt(page, 1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
import run.halo.app.theme.finders.vo.TagVo;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
/**
|
||||
* The {@link TagPostRouteFactory} for generate {@link RouterFunction} specific to the template
|
||||
* <code>tag.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class TagPostRouteFactory implements RouteFactory {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
private final TagFinder tagFinder;
|
||||
private final PostFinder postFinder;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> create(String prefix) {
|
||||
return RouterFunctions
|
||||
.route(GET(PathUtils.combinePath(prefix, "/{slug}"))
|
||||
.or(GET(PathUtils.combinePath(prefix, "/{slug}/page/{page:\\d+}")))
|
||||
.and(accept(MediaType.TEXT_HTML)), handlerFunction());
|
||||
}
|
||||
|
||||
private HandlerFunction<ServerResponse> handlerFunction() {
|
||||
return request -> tagBySlug(request.pathVariable("slug"))
|
||||
.flatMap(tagVo -> {
|
||||
int pageNum = pageNumInPathVariable(request);
|
||||
String path = request.path();
|
||||
var postList = postList(tagVo.getMetadata().getName(), pageNum, path);
|
||||
return ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.TAG.getValue(),
|
||||
Map.of("name", tagVo.getMetadata().getName(),
|
||||
"posts", postList,
|
||||
"tag", tagVo)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<ListedPostVo>> postList(String name, Integer page,
|
||||
String requestPath) {
|
||||
return configuredPageSize(environmentFetcher)
|
||||
.flatMap(pageSize -> postFinder.listByTag(page, pageSize, name))
|
||||
.map(list -> new UrlContextListResult.Builder<ListedPostVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(PageUrlUtils.nextPageUrl(requestPath, totalPage(list)))
|
||||
.prevUrl(PageUrlUtils.prevPageUrl(requestPath))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<TagVo> tagBySlug(String slug) {
|
||||
return client.list(Tag.class, tag -> tag.getSpec().getSlug().equals(slug)
|
||||
&& tag.getMetadata().getDeletionTimestamp() == null, null)
|
||||
.next()
|
||||
.flatMap(tag -> tagFinder.getByName(tag.getMetadata().getName()))
|
||||
.switchIfEmpty(
|
||||
Mono.error(new NotFoundException("Tag not found with slug: " + slug)));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
|
||||
/**
|
||||
* The {@link TagsRouteFactory} for generate {@link RouterFunction} specific to the template
|
||||
* <code>tags.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class TagsRouteFactory implements RouteFactory {
|
||||
|
||||
private final TagFinder tagFinder;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> create(String prefix) {
|
||||
return RouterFunctions
|
||||
.route(GET(StringUtils.prependIfMissing(prefix, "/"))
|
||||
.and(accept(MediaType.TEXT_HTML)), handlerFunction());
|
||||
}
|
||||
|
||||
private HandlerFunction<ServerResponse> handlerFunction() {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.TAGS.getValue(),
|
||||
Map.of("tags", tagFinder.listAll(),
|
||||
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.TAGS.getValue()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
import static run.halo.app.theme.router.strategy.ModelConst.DEFAULT_PAGE_SIZE;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.vo.PostArchiveVo;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
/**
|
||||
* The {@link ArchivesRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* <code>posts.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class ArchivesRouteStrategy implements ListPageRouteHandlerStrategy {
|
||||
private final PostFinder postFinder;
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
public ArchivesRouteStrategy(PostFinder postFinder,
|
||||
SystemConfigurableEnvironmentFetcher environmentFetcher) {
|
||||
this.postFinder = postFinder;
|
||||
this.environmentFetcher = environmentFetcher;
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<PostArchiveVo>> postList(ServerRequest request) {
|
||||
String year = pathVariable(request, "year");
|
||||
String month = pathVariable(request, "month");
|
||||
String path = request.path();
|
||||
return environmentFetcher.fetchPost()
|
||||
.map(postSetting -> defaultIfNull(postSetting.getArchivePageSize(), DEFAULT_PAGE_SIZE))
|
||||
.flatMap(pageSize -> postFinder.archives(pageNum(request), pageSize, year, month))
|
||||
.map(list -> new UrlContextListResult.Builder<PostArchiveVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
|
||||
.prevUrl(PageUrlUtils.prevPageUrl(path))
|
||||
.build());
|
||||
}
|
||||
|
||||
private String pathVariable(ServerRequest request, String name) {
|
||||
Map<String, String> pathVariables = request.pathVariables();
|
||||
if (pathVariables.containsKey(name)) {
|
||||
return pathVariables.get(name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler() {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.ARCHIVES.getValue(),
|
||||
Map.of("archives", postList(request)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRouterPaths(String prefix) {
|
||||
return List.of(
|
||||
StringUtils.prependIfMissing(prefix, "/"),
|
||||
PathUtils.combinePath(prefix, "/page/{page:\\d+}"),
|
||||
PathUtils.combinePath(prefix, "/{year:\\d{4}}"),
|
||||
PathUtils.combinePath(prefix, "/{year:\\d{4}}/page/{page:\\d+}"),
|
||||
PathUtils.combinePath(prefix, "/{year:\\d{4}}/{month:\\d{2}}"),
|
||||
PathUtils.combinePath(prefix,
|
||||
"/{year:\\d{4}}/{month:\\d{2}}/page/{page:\\d+}")
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(DefaultTemplateEnum template) {
|
||||
return DefaultTemplateEnum.ARCHIVES.equals(template);
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
import run.halo.app.theme.finders.vo.UserVo;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
/**
|
||||
* Author route strategy.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.1
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class AuthorRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final PostFinder postFinder;
|
||||
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
||||
String name) {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.AUTHOR.getValue(),
|
||||
Map.of("name", name,
|
||||
"author", getByName(name),
|
||||
"posts", postList(request, name),
|
||||
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.AUTHOR.getValue()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<UrlContextListResult<ListedPostVo>> postList(ServerRequest request, String name) {
|
||||
String path = request.path();
|
||||
return environmentFetcher.fetchPost()
|
||||
.map(p -> defaultIfNull(p.getPostPageSize(), ModelConst.DEFAULT_PAGE_SIZE))
|
||||
.flatMap(pageSize -> postFinder.listByOwner(pageNum(request), pageSize, name))
|
||||
.map(list -> new UrlContextListResult.Builder<ListedPostVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
|
||||
.prevUrl(PageUrlUtils.prevPageUrl(path))
|
||||
.build());
|
||||
}
|
||||
|
||||
private Mono<UserVo> getByName(String name) {
|
||||
return client.fetch(User.class, name)
|
||||
.map(UserVo::from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GroupVersionKind gvk) {
|
||||
return GroupVersionKind.fromExtension(User.class).equals(gvk);
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
import run.halo.app.theme.router.ViewNameResolver;
|
||||
|
||||
/**
|
||||
* The {@link CategoryRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* <code>category.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class CategoryRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Category.class);
|
||||
private final PostFinder postFinder;
|
||||
|
||||
private final CategoryFinder categoryFinder;
|
||||
|
||||
private final ViewNameResolver viewNameResolver;
|
||||
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
private Mono<UrlContextListResult<ListedPostVo>> postListByCategoryName(String name,
|
||||
ServerRequest request) {
|
||||
String path = request.path();
|
||||
return environmentFetcher.fetchPost()
|
||||
.map(post -> defaultIfNull(post.getCategoryPageSize(), ModelConst.DEFAULT_PAGE_SIZE))
|
||||
.flatMap(
|
||||
pageSize -> postFinder.listByCategory(pageNum(request), pageSize, name))
|
||||
.map(list -> new UrlContextListResult.Builder<ListedPostVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
|
||||
.prevUrl(PageUrlUtils.prevPageUrl(path))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
||||
String name) {
|
||||
return request -> {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("name", name);
|
||||
model.put("posts", postListByCategoryName(name, request));
|
||||
|
||||
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORY.getValue());
|
||||
return categoryFinder.getByName(name).flatMap(categoryVo -> {
|
||||
model.put("category", categoryVo);
|
||||
String template = categoryVo.getSpec().getTemplate();
|
||||
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||
DefaultTemplateEnum.CATEGORY.getValue())
|
||||
.flatMap(viewName -> ServerResponse.ok().render(viewName, model));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GroupVersionKind gvk) {
|
||||
return this.gvk.equals(gvk);
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
|
||||
/**
|
||||
* The {@link DetailsPageRouteHandlerStrategy} for generate {@link HandlerFunction} specific to the
|
||||
* template.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface DetailsPageRouteHandlerStrategy {
|
||||
|
||||
HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
||||
String name);
|
||||
|
||||
boolean supports(GroupVersionKind gvk);
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
|
||||
/**
|
||||
* The {@link ListPageRouteHandlerStrategy} for generate {@link HandlerFunction} specific to the
|
||||
* template.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface ListPageRouteHandlerStrategy {
|
||||
|
||||
HandlerFunction<ServerResponse> getHandler();
|
||||
|
||||
List<String> getRouterPaths(String pattern);
|
||||
|
||||
boolean supports(DefaultTemplateEnum template);
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.router.ViewNameResolver;
|
||||
|
||||
/**
|
||||
* The {@link PostRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* <code>post.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class PostRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||
static final String NAME_PARAM = "name";
|
||||
private final GroupVersionKind groupVersionKind = GroupVersionKind.fromExtension(Post.class);
|
||||
private final PostFinder postFinder;
|
||||
private final ViewNameResolver viewNameResolver;
|
||||
|
||||
public PostRouteStrategy(PostFinder postFinder, ViewNameResolver viewNameResolver) {
|
||||
this.postFinder = postFinder;
|
||||
this.viewNameResolver = viewNameResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<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(NAME_PARAM, name);
|
||||
if (pathMatchInfo != null) {
|
||||
model.putAll(pathMatchInfo.getUriVariables());
|
||||
}
|
||||
// used by HaloTrackerProcessor
|
||||
model.put("groupVersionKind", groupVersionKind);
|
||||
model.put("plural", gvk.plural());
|
||||
// used by TemplateGlobalHeadProcessor and PostTemplateHeadProcessor
|
||||
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue());
|
||||
return postFinder.getByName(name)
|
||||
.flatMap(postVo -> {
|
||||
model.put("post", postVo);
|
||||
String template = postVo.getSpec().getTemplate();
|
||||
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||
DefaultTemplateEnum.POST.getValue())
|
||||
.flatMap(templateName -> ServerResponse.ok().render(templateName, model));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GroupVersionKind gvk) {
|
||||
return groupVersionKind.equals(gvk);
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.SinglePageFinder;
|
||||
import run.halo.app.theme.router.ViewNameResolver;
|
||||
|
||||
/**
|
||||
* The {@link SinglePageRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* <code>page.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class SinglePageRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class);
|
||||
private final SinglePageFinder singlePageFinder;
|
||||
private final ViewNameResolver viewNameResolver;
|
||||
|
||||
public SinglePageRouteStrategy(SinglePageFinder singlePageFinder,
|
||||
ViewNameResolver viewNameResolver) {
|
||||
this.singlePageFinder = singlePageFinder;
|
||||
this.viewNameResolver = viewNameResolver;
|
||||
}
|
||||
|
||||
private String getPlural() {
|
||||
GVK annotation = SinglePage.class.getAnnotation(GVK.class);
|
||||
return annotation.plural();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
|
||||
String name) {
|
||||
return request -> {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("name", name);
|
||||
model.put("groupVersionKind", gvk);
|
||||
model.put("plural", getPlural());
|
||||
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue());
|
||||
|
||||
return singlePageFinder.getByName(name).flatMap(singlePageVo -> {
|
||||
model.put("singlePage", singlePageVo);
|
||||
String template = singlePageVo.getSpec().getTemplate();
|
||||
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||
DefaultTemplateEnum.SINGLE_PAGE.getValue())
|
||||
.flatMap(viewName -> ServerResponse.ok().render(viewName, model));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GroupVersionKind gvk) {
|
||||
return this.gvk.equals(gvk);
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.pageNum;
|
||||
import static run.halo.app.theme.router.PageUrlUtils.totalPage;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
import run.halo.app.theme.router.PageUrlUtils;
|
||||
import run.halo.app.theme.router.UrlContextListResult;
|
||||
|
||||
/**
|
||||
* The {@link TagRouteStrategy} for generate {@link RouterFunction} specific to the template
|
||||
* <code>tag.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class TagRouteStrategy implements DetailsPageRouteHandlerStrategy {
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Tag.class);
|
||||
private final PostFinder postFinder;
|
||||
|
||||
private final TagFinder tagFinder;
|
||||
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
private Mono<UrlContextListResult<ListedPostVo>> postList(ServerRequest request, String name) {
|
||||
String path = request.path();
|
||||
return environmentFetcher.fetchPost()
|
||||
.map(p -> defaultIfNull(p.getTagPageSize(), ModelConst.DEFAULT_PAGE_SIZE))
|
||||
.flatMap(pageSize -> postFinder.listByTag(pageNum(request), pageSize, name))
|
||||
.map(list -> new UrlContextListResult.Builder<ListedPostVo>()
|
||||
.listResult(list)
|
||||
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
|
||||
.prevUrl(PageUrlUtils.prevPageUrl(path))
|
||||
.build());
|
||||
}
|
||||
|
||||
@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", tagFinder.getByName(name),
|
||||
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.TAG.getValue()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GroupVersionKind gvk) {
|
||||
return this.gvk.equals(gvk);
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
|
||||
/**
|
||||
* The {@link TagsRouteStrategy} for generate {@link HandlerFunction} specific to the template
|
||||
* <code>tags.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class TagsRouteStrategy implements ListPageRouteHandlerStrategy {
|
||||
|
||||
private final TagFinder tagFinder;
|
||||
|
||||
public TagsRouteStrategy(TagFinder tagFinder) {
|
||||
this.tagFinder = tagFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerFunction<ServerResponse> getHandler() {
|
||||
return request -> ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.TAGS.getValue(),
|
||||
Map.of("tags", tagFinder.listAll(),
|
||||
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.TAGS.getValue()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRouterPaths(String prefix) {
|
||||
return List.of(StringUtils.prependIfMissing(prefix, "/"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(DefaultTemplateEnum template) {
|
||||
return DefaultTemplateEnum.TAGS.equals(template);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package run.halo.app.content.permalinks;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.net.URI;
|
||||
|
@ -10,12 +9,10 @@ import org.junit.jupiter.api.Test;
|
|||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.PermalinkPatternProvider;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
|
||||
/**
|
||||
* Tests for {@link CategoryPermalinkPolicy}.
|
||||
|
@ -26,28 +23,22 @@ import run.halo.app.theme.router.PermalinkPatternProvider;
|
|||
@ExtendWith(MockitoExtension.class)
|
||||
class CategoryPermalinkPolicyTest {
|
||||
|
||||
@Mock
|
||||
private PermalinkPatternProvider permalinkPatternProvider;
|
||||
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Mock
|
||||
private ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
@Mock
|
||||
private SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
private CategoryPermalinkPolicy categoryPermalinkPolicy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
categoryPermalinkPolicy =
|
||||
new CategoryPermalinkPolicy(applicationContext, permalinkPatternProvider,
|
||||
externalUrlSupplier);
|
||||
new CategoryPermalinkPolicy(externalUrlSupplier, environmentFetcher);
|
||||
}
|
||||
|
||||
@Test
|
||||
void permalink() {
|
||||
when(permalinkPatternProvider.getPattern(eq(DefaultTemplateEnum.CATEGORY)))
|
||||
.thenReturn("categories");
|
||||
Category category = new Category();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("category-test");
|
||||
|
@ -70,18 +61,4 @@ class CategoryPermalinkPolicyTest {
|
|||
permalink = categoryPermalinkPolicy.permalink(category);
|
||||
assertThat(permalink).isEqualTo("http://exmaple.com/categories/%E4%B8%AD%E6%96%87%20slug");
|
||||
}
|
||||
|
||||
@Test
|
||||
void templateName() {
|
||||
String s = categoryPermalinkPolicy.templateName();
|
||||
assertThat(s).isEqualTo(DefaultTemplateEnum.CATEGORY.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pattern() {
|
||||
when(permalinkPatternProvider.getPattern(eq(DefaultTemplateEnum.CATEGORY)))
|
||||
.thenReturn("categories");
|
||||
String pattern = categoryPermalinkPolicy.pattern();
|
||||
assertThat(pattern).isEqualTo("categories");
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import java.text.NumberFormat;
|
|||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
@ -17,11 +18,12 @@ import org.mockito.Mock;
|
|||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import run.halo.app.content.TestPost;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.PermalinkPatternProvider;
|
||||
|
||||
/**
|
||||
* Tests for {@link PostPermalinkPolicy}.
|
||||
|
@ -33,27 +35,28 @@ import run.halo.app.theme.router.PermalinkPatternProvider;
|
|||
class PostPermalinkPolicyTest {
|
||||
private static final NumberFormat NUMBER_FORMAT = new DecimalFormat("00");
|
||||
|
||||
@Mock
|
||||
private PermalinkPatternProvider permalinkPatternProvider;
|
||||
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Mock
|
||||
private ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
@Mock
|
||||
private SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
private PostPermalinkPolicy postPermalinkPolicy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(externalUrlSupplier.get()).thenReturn(URI.create(""));
|
||||
postPermalinkPolicy = new PostPermalinkPolicy(permalinkPatternProvider, applicationContext,
|
||||
externalUrlSupplier);
|
||||
postPermalinkPolicy = new PostPermalinkPolicy(environmentFetcher, externalUrlSupplier);
|
||||
}
|
||||
|
||||
@Test
|
||||
void permalink() {
|
||||
Post post = TestPost.postV1();
|
||||
Map<String, String> annotations = ExtensionUtil.nullSafeAnnotations(post);
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/{year}/{month}/{day}/{slug}");
|
||||
post.getMetadata().setName("test-post");
|
||||
post.getSpec().setSlug("test-post-slug");
|
||||
Instant now = Instant.now();
|
||||
|
@ -64,35 +67,28 @@ class PostPermalinkPolicyTest {
|
|||
String month = NUMBER_FORMAT.format(zonedDateTime.getMonthValue());
|
||||
String day = NUMBER_FORMAT.format(zonedDateTime.getDayOfMonth());
|
||||
|
||||
// pattern /{year}/{month}/{day}/{slug}
|
||||
when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST))
|
||||
.thenReturn("/{year}/{month}/{day}/{slug}");
|
||||
String permalink = postPermalinkPolicy.permalink(post);
|
||||
assertThat(permalink)
|
||||
.isEqualTo(PathUtils.combinePath(year, month, day, post.getSpec().getSlug()));
|
||||
|
||||
// pattern {month}/{day}/{slug}
|
||||
when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST))
|
||||
.thenReturn("/{month}/{day}/{slug}");
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/{month}/{day}/{slug}");
|
||||
permalink = postPermalinkPolicy.permalink(post);
|
||||
assertThat(permalink)
|
||||
.isEqualTo(PathUtils.combinePath(month, day, post.getSpec().getSlug()));
|
||||
|
||||
// pattern /?p={name}
|
||||
when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST))
|
||||
.thenReturn("/?p={name}");
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/?p={name}");
|
||||
permalink = postPermalinkPolicy.permalink(post);
|
||||
assertThat(permalink).isEqualTo("/?p=test-post");
|
||||
|
||||
// pattern /posts/{slug}
|
||||
when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST))
|
||||
.thenReturn("/posts/{slug}");
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/posts/{slug}");
|
||||
permalink = postPermalinkPolicy.permalink(post);
|
||||
assertThat(permalink).isEqualTo("/posts/test-post-slug");
|
||||
|
||||
// pattern /posts/{name}
|
||||
when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST))
|
||||
.thenReturn("/posts/{name}");
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/posts/{name}");
|
||||
permalink = postPermalinkPolicy.permalink(post);
|
||||
assertThat(permalink).isEqualTo("/posts/test-post");
|
||||
}
|
||||
|
@ -100,6 +96,8 @@ class PostPermalinkPolicyTest {
|
|||
@Test
|
||||
void permalinkWithExternalUrl() {
|
||||
Post post = TestPost.postV1();
|
||||
Map<String, String> annotations = ExtensionUtil.nullSafeAnnotations(post);
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/{year}/{month}/{day}/{slug}");
|
||||
post.getMetadata().setName("test-post");
|
||||
post.getSpec().setSlug("test-post-slug");
|
||||
Instant now = Instant.parse("2022-11-01T02:40:06.806310Z");
|
||||
|
@ -107,8 +105,6 @@ class PostPermalinkPolicyTest {
|
|||
|
||||
when(externalUrlSupplier.get()).thenReturn(URI.create("http://example.com"));
|
||||
|
||||
when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST))
|
||||
.thenReturn("/{year}/{month}/{day}/{slug}");
|
||||
String permalink = postPermalinkPolicy.permalink(post);
|
||||
assertThat(permalink).isEqualTo("http://example.com/2022/11/01/test-post-slug");
|
||||
|
||||
|
@ -116,17 +112,4 @@ class PostPermalinkPolicyTest {
|
|||
permalink = postPermalinkPolicy.permalink(post);
|
||||
assertThat(permalink).isEqualTo("http://example.com/2022/11/01/%E4%B8%AD%E6%96%87%20slug");
|
||||
}
|
||||
|
||||
@Test
|
||||
void templateName() {
|
||||
String s = postPermalinkPolicy.templateName();
|
||||
assertThat(s).isEqualTo(DefaultTemplateEnum.POST.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pattern() {
|
||||
when(permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST))
|
||||
.thenReturn("/{year}/{month}/{day}/{slug}");
|
||||
assertThat(postPermalinkPolicy.pattern()).isEqualTo("/{year}/{month}/{day}/{slug}");
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package run.halo.app.content.permalinks;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.net.URI;
|
||||
|
@ -14,8 +13,7 @@ import org.springframework.context.ApplicationContext;
|
|||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.PermalinkPatternProvider;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
|
||||
/**
|
||||
* Tests for {@link TagPermalinkPolicy}.
|
||||
|
@ -26,28 +24,24 @@ import run.halo.app.theme.router.PermalinkPatternProvider;
|
|||
@ExtendWith(MockitoExtension.class)
|
||||
class TagPermalinkPolicyTest {
|
||||
|
||||
@Mock
|
||||
private PermalinkPatternProvider permalinkPatternProvider;
|
||||
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Mock
|
||||
private ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
@Mock
|
||||
private SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
private TagPermalinkPolicy tagPermalinkPolicy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
tagPermalinkPolicy = new TagPermalinkPolicy(permalinkPatternProvider, applicationContext,
|
||||
externalUrlSupplier);
|
||||
tagPermalinkPolicy = new TagPermalinkPolicy(externalUrlSupplier, environmentFetcher);
|
||||
}
|
||||
|
||||
@Test
|
||||
void permalink() {
|
||||
when(permalinkPatternProvider.getPattern(eq(DefaultTemplateEnum.TAG)))
|
||||
.thenReturn("tags");
|
||||
|
||||
Tag tag = new Tag();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("test-tag");
|
||||
|
@ -70,17 +64,4 @@ class TagPermalinkPolicyTest {
|
|||
permalink = tagPermalinkPolicy.permalink(tag);
|
||||
assertThat(permalink).isEqualTo("http://example.com/tags/%E4%B8%AD%E6%96%87slug");
|
||||
}
|
||||
|
||||
@Test
|
||||
void templateName() {
|
||||
String s = tagPermalinkPolicy.templateName();
|
||||
assertThat(s).isEqualTo(DefaultTemplateEnum.TAG.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pattern() {
|
||||
when(permalinkPatternProvider.getPattern(eq(DefaultTemplateEnum.TAG)))
|
||||
.thenReturn("tags");
|
||||
assertThat(tagPermalinkPolicy.pattern()).isEqualTo("tags");
|
||||
}
|
||||
}
|
|
@ -47,7 +47,7 @@ class CategoryReconcilerTest {
|
|||
reconcileStatusPostPilling("category-A");
|
||||
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client, times(2)).update(captor.capture());
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
assertThat(captor.getAllValues().get(1).getStatusOrDefault().getPostCount()).isEqualTo(4);
|
||||
assertThat(
|
||||
captor.getAllValues().get(1).getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
|
||||
|
@ -57,7 +57,7 @@ class CategoryReconcilerTest {
|
|||
void reconcileStatusPostForCategoryB() throws JSONException {
|
||||
reconcileStatusPostPilling("category-B");
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client, times(2)).update(captor.capture());
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
Category category = captor.getAllValues().get(1);
|
||||
assertThat(category.getStatusOrDefault().getPostCount()).isEqualTo(3);
|
||||
assertThat(category.getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
|
||||
|
@ -67,7 +67,7 @@ class CategoryReconcilerTest {
|
|||
void reconcileStatusPostForCategoryC() throws JSONException {
|
||||
reconcileStatusPostPilling("category-C");
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client, times(2)).update(captor.capture());
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
assertThat(captor.getAllValues().get(1).getStatusOrDefault().getPostCount()).isEqualTo(2);
|
||||
assertThat(
|
||||
captor.getAllValues().get(1).getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
|
||||
|
@ -77,7 +77,7 @@ class CategoryReconcilerTest {
|
|||
void reconcileStatusPostForCategoryD() throws JSONException {
|
||||
reconcileStatusPostPilling("category-D");
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client, times(2)).update(captor.capture());
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
assertThat(captor.getAllValues().get(1).getStatusOrDefault().postCount).isEqualTo(1);
|
||||
assertThat(captor.getAllValues().get(1).getStatusOrDefault().visiblePostCount).isEqualTo(0);
|
||||
}
|
||||
|
|
|
@ -80,9 +80,6 @@ class PostReconcilerTest {
|
|||
verify(client, times(3)).update(captor.capture());
|
||||
|
||||
verify(postPermalinkPolicy, times(1)).permalink(any());
|
||||
verify(postPermalinkPolicy, times(1)).onPermalinkAdd(any());
|
||||
verify(postPermalinkPolicy, times(1)).onPermalinkDelete(any());
|
||||
verify(postPermalinkPolicy, times(0)).onPermalinkUpdate(any());
|
||||
|
||||
Post value = captor.getValue();
|
||||
assertThat(value.getStatus().getExcerpt()).isNull();
|
||||
|
|
|
@ -3,7 +3,6 @@ package run.halo.app.core.extension.reconciler;
|
|||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
@ -34,9 +33,6 @@ import run.halo.app.extension.Metadata;
|
|||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.theme.router.PermalinkIndexAddCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexUpdateCommand;
|
||||
|
||||
/**
|
||||
* Tests for {@link SinglePageReconciler}.
|
||||
|
@ -97,10 +93,6 @@ class SinglePageReconcilerTest {
|
|||
SinglePage value = captor.getValue();
|
||||
assertThat(value.getStatus().getExcerpt()).isEqualTo("hello world");
|
||||
assertThat(value.getStatus().getContributors()).isEqualTo(List.of("guqing", "zhangsan"));
|
||||
|
||||
verify(applicationContext, times(0)).publishEvent(isA(PermalinkIndexAddCommand.class));
|
||||
verify(applicationContext, times(1)).publishEvent(isA(PermalinkIndexDeleteCommand.class));
|
||||
verify(applicationContext, times(0)).publishEvent(isA(PermalinkIndexUpdateCommand.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -40,6 +40,28 @@ class TagReconcilerTest {
|
|||
@InjectMocks
|
||||
private TagReconciler tagReconciler;
|
||||
|
||||
@Test
|
||||
void reconcile() {
|
||||
Tag tag = tag();
|
||||
when(client.fetch(eq(Tag.class), eq("fake-tag")))
|
||||
.thenReturn(Optional.of(tag));
|
||||
when(tagPermalinkPolicy.permalink(any()))
|
||||
.thenAnswer(arg -> "/tags/" + tag.getSpec().getSlug());
|
||||
ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class);
|
||||
|
||||
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
|
||||
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
Tag capture = captor.getValue();
|
||||
assertThat(capture.getStatus().getPermalink()).isEqualTo("/tags/fake-slug");
|
||||
|
||||
// change slug
|
||||
tag.getSpec().setSlug("new-slug");
|
||||
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
|
||||
verify(client, times(5)).update(captor.capture());
|
||||
assertThat(capture.getStatus().getPermalink()).isEqualTo("/tags/new-slug");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcileDelete() {
|
||||
Tag tag = tag();
|
||||
|
@ -50,8 +72,6 @@ class TagReconcilerTest {
|
|||
|
||||
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
|
||||
verify(client, times(1)).update(captor.capture());
|
||||
verify(tagPermalinkPolicy, times(0)).onPermalinkAdd(any());
|
||||
verify(tagPermalinkPolicy, times(1)).onPermalinkDelete(any());
|
||||
verify(tagPermalinkPolicy, times(0)).permalink(any());
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ import run.halo.app.extension.Metadata;
|
|||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.AnonymousUserConst;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.theme.router.PermalinkIndexUpdateCommand;
|
||||
|
||||
/**
|
||||
* Tests for {@link UserReconciler}.
|
||||
|
@ -54,7 +53,6 @@ class UserReconcilerTest {
|
|||
.thenReturn(Optional.of(user("fake-user")));
|
||||
userReconciler.reconcile(new Reconciler.Request("fake-user"));
|
||||
verify(client, times(1)).update(any(User.class));
|
||||
verify(eventPublisher, times(1)).publishEvent(any(PermalinkIndexUpdateCommand.class));
|
||||
|
||||
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
|
||||
verify(client, times(1)).update(captor.capture());
|
||||
|
@ -68,7 +66,6 @@ class UserReconcilerTest {
|
|||
.thenReturn(Optional.of(user(AnonymousUserConst.PRINCIPAL)));
|
||||
userReconciler.reconcile(new Reconciler.Request(AnonymousUserConst.PRINCIPAL));
|
||||
verify(client, times(0)).update(any(User.class));
|
||||
verify(eventPublisher, times(0)).publishEvent(any(PermalinkIndexUpdateCommand.class));
|
||||
}
|
||||
|
||||
User user(String name) {
|
||||
|
|
|
@ -38,7 +38,7 @@ import run.halo.app.theme.finders.PostFinder;
|
|||
import run.halo.app.theme.finders.SinglePageFinder;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
import run.halo.app.theme.finders.vo.UserVo;
|
||||
import run.halo.app.theme.router.strategy.ModelConst;
|
||||
import run.halo.app.theme.router.factories.ModelConst;
|
||||
|
||||
/**
|
||||
* Tests for {@link HaloProcessorDialect}.
|
||||
|
|
|
@ -7,6 +7,7 @@ import static org.mockito.Mockito.times;
|
|||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
@ -15,6 +16,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
|||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.content.SinglePageService;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
@ -52,9 +54,13 @@ class SinglePageFinderImplTest {
|
|||
SinglePage singlePage = new SinglePage();
|
||||
singlePage.setMetadata(new Metadata());
|
||||
singlePage.getMetadata().setName(fakePageName);
|
||||
singlePage.getMetadata().setLabels(Map.of(SinglePage.PUBLISHED_LABEL, "true"));
|
||||
singlePage.setSpec(new SinglePage.SinglePageSpec());
|
||||
singlePage.getSpec().setOwner("fake-owner");
|
||||
singlePage.getSpec().setReleaseSnapshot("fake-release");
|
||||
singlePage.getSpec().setPublish(true);
|
||||
singlePage.getSpec().setDeleted(false);
|
||||
singlePage.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||
singlePage.setStatus(new SinglePage.SinglePageStatus());
|
||||
when(client.fetch(eq(SinglePage.class), eq(fakePageName)))
|
||||
.thenReturn(Mono.just(singlePage));
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
package run.halo.app.theme.router;
|
||||
|
||||
import java.util.Map;
|
||||
import org.springframework.http.MediaType;
|
|
@ -1,136 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import run.halo.app.content.permalinks.ExtensionLocator;
|
||||
import run.halo.app.extension.FakeExtension;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
|
||||
/**
|
||||
* Tests for {@link PermalinkIndexer}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PermalinkIndexerTest {
|
||||
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(FakeExtension.class);
|
||||
|
||||
private PermalinkIndexer permalinkIndexer;
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
permalinkIndexer = new PermalinkIndexer(applicationContext);
|
||||
|
||||
ExtensionLocator locator = new ExtensionLocator(gvk, "fake-name", "fake-slug");
|
||||
permalinkIndexer.register(locator, "/fake-permalink");
|
||||
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1);
|
||||
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void register() {
|
||||
ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug");
|
||||
permalinkIndexer.register(locator, "/test-permalink");
|
||||
verify(applicationContext, times(2)).publishEvent(any(PermalinkIndexChangedEvent.class));
|
||||
|
||||
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(2);
|
||||
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void remove() {
|
||||
ExtensionLocator locator = new ExtensionLocator(gvk, "fake-name", "fake-slug");
|
||||
|
||||
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1);
|
||||
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1);
|
||||
|
||||
permalinkIndexer.remove(locator);
|
||||
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(0);
|
||||
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(0);
|
||||
|
||||
verify(applicationContext, times(2)).publishEvent(any(PermalinkIndexChangedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lookup() {
|
||||
ExtensionLocator lookup = permalinkIndexer.lookup("/fake-permalink");
|
||||
assertThat(lookup).isEqualTo(new ExtensionLocator(gvk, "fake-name", "fake-slug"));
|
||||
|
||||
lookup = permalinkIndexer.lookup("/nothing");
|
||||
assertThat(lookup).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPermalinks() {
|
||||
ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug");
|
||||
permalinkIndexer.register(locator, "/test-permalink");
|
||||
|
||||
List<String> permalinks = permalinkIndexer.getPermalinks(gvk);
|
||||
assertThat(permalinks).isEqualTo(List.of("/fake-permalink", "/test-permalink"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNames() {
|
||||
ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug");
|
||||
permalinkIndexer.register(locator, "/test-permalink");
|
||||
|
||||
assertThat(permalinkIndexer.containsName(gvk, "test-name")).isTrue();
|
||||
assertThat(permalinkIndexer.containsName(gvk, "nothing")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSlugs() {
|
||||
ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug");
|
||||
permalinkIndexer.register(locator, "/test-permalink");
|
||||
|
||||
assertThat(permalinkIndexer.containsSlug(gvk, "fake-slug")).isTrue();
|
||||
assertThat(permalinkIndexer.containsSlug(gvk, "test-slug")).isTrue();
|
||||
assertThat(permalinkIndexer.containsSlug(gvk, "nothing")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNameBySlug() {
|
||||
ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug");
|
||||
permalinkIndexer.register(locator, "/test-permalink");
|
||||
|
||||
String nameBySlug = permalinkIndexer.getNameBySlug(gvk, "test-slug");
|
||||
assertThat(nameBySlug).isEqualTo("test-name");
|
||||
|
||||
nameBySlug = permalinkIndexer.getNameBySlug(gvk, "fake-slug");
|
||||
assertThat(nameBySlug).isEqualTo("fake-name");
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
permalinkIndexer.getNameBySlug(gvk, "nothing");
|
||||
}).isInstanceOf(NoSuchElementException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNameByPermalink() {
|
||||
ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug");
|
||||
permalinkIndexer.register(locator, "/test-permalink");
|
||||
|
||||
var name = permalinkIndexer.getNameByPermalink(gvk, "/test-permalink");
|
||||
assertEquals("test-name", name);
|
||||
|
||||
name = permalinkIndexer.getNameByPermalink(gvk, "/invalid-permalink");
|
||||
assertNull(name);
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import run.halo.app.extension.ConfigMap;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
|
||||
/**
|
||||
* Tests for {@link PermalinkPatternProvider}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PermalinkPatternProviderTest {
|
||||
|
||||
@Mock
|
||||
private SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
@InjectMocks
|
||||
private PermalinkPatternProvider permalinkPatternProvider;
|
||||
|
||||
@Test
|
||||
void getPatternThenDefault() {
|
||||
when(environmentFetcher.getConfigMapBlocking())
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
String pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST);
|
||||
assertThat(pattern).isEqualTo("/archives/{slug}");
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAG);
|
||||
assertThat(pattern).isEqualTo("tags");
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAGS);
|
||||
assertThat(pattern).isEqualTo("tags");
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.CATEGORY);
|
||||
assertThat(pattern).isEqualTo("categories");
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.CATEGORIES);
|
||||
assertThat(pattern).isEqualTo("categories");
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.ARCHIVES);
|
||||
assertThat(pattern).isEqualTo("archives");
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.INDEX);
|
||||
assertThat(pattern).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPattern() {
|
||||
ConfigMap configMap = new ConfigMap();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("system");
|
||||
configMap.setMetadata(metadata);
|
||||
|
||||
SystemSetting.ThemeRouteRules themeRouteRules = new SystemSetting.ThemeRouteRules();
|
||||
themeRouteRules.setPost("/posts/{slug}");
|
||||
themeRouteRules.setCategories("c");
|
||||
themeRouteRules.setTags("t");
|
||||
themeRouteRules.setArchives("a");
|
||||
|
||||
configMap.setData(Map.of("routeRules", JsonUtils.objectToJson(themeRouteRules)));
|
||||
|
||||
when(environmentFetcher.getConfigMapBlocking())
|
||||
.thenReturn(Optional.of(configMap));
|
||||
|
||||
String pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.POST);
|
||||
assertThat(pattern).isEqualTo(themeRouteRules.getPost());
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAG);
|
||||
assertThat(pattern).isEqualTo(themeRouteRules.getTags());
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.TAGS);
|
||||
assertThat(pattern).isEqualTo(themeRouteRules.getTags());
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.CATEGORY);
|
||||
assertThat(pattern).isEqualTo(themeRouteRules.getCategories());
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.CATEGORIES);
|
||||
assertThat(pattern).isEqualTo(themeRouteRules.getCategories());
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.ARCHIVES);
|
||||
assertThat(pattern).isEqualTo(themeRouteRules.getArchives());
|
||||
|
||||
pattern = permalinkPatternProvider.getPattern(DefaultTemplateEnum.INDEX);
|
||||
assertThat(pattern).isNull();
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
/**
|
||||
* Tests for {@link RadixRouterTree}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class RadixRouterTreeTest {
|
||||
|
||||
@Test
|
||||
void pathToFind() throws URISyntaxException {
|
||||
MockServerRequest request =
|
||||
MockServerRequest.builder().uri(new URI("/archives"))
|
||||
.method(HttpMethod.GET).build();
|
||||
String path = RadixRouterTree.pathToFind(request);
|
||||
assertThat(path).isEqualTo("/archives");
|
||||
|
||||
request = MockServerRequest.builder().uri(new URI("/archives/"))
|
||||
.method(HttpMethod.GET).build();
|
||||
assertThat(RadixRouterTree.pathToFind(request)).isEqualTo("/archives");
|
||||
|
||||
request = MockServerRequest.builder().uri(new URI("/archives/page/1"))
|
||||
.method(HttpMethod.GET).build();
|
||||
assertThat(RadixRouterTree.pathToFind(request)).isEqualTo("/archives");
|
||||
|
||||
request = MockServerRequest.builder().uri(new URI("/"))
|
||||
.method(HttpMethod.GET).build();
|
||||
assertThat(RadixRouterTree.pathToFind(request)).isEqualTo("/");
|
||||
|
||||
request = MockServerRequest.builder().uri(new URI("/"))
|
||||
.queryParam("p", "fake-post")
|
||||
.method(HttpMethod.GET).build();
|
||||
assertThat(RadixRouterTree.pathToFind(request)).isEqualTo("/?p=fake-post");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInsertKeyWithPercentSign() {
|
||||
var tree = new RadixRouterTree();
|
||||
tree.insert("/1%1", request -> ServerResponse.ok().build());
|
||||
}
|
||||
}
|
|
@ -1,267 +0,0 @@
|
|||
package run.halo.app.theme.router;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link RadixTree}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class RadixTreeTest {
|
||||
|
||||
@Test
|
||||
void insert() {
|
||||
RadixTree<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"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -22,7 +22,6 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.theme.HaloViewResolver;
|
||||
import run.halo.app.theme.router.strategy.EmptyView;
|
||||
|
||||
/**
|
||||
* Tests for {@link ViewNameResolver}.
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
|
||||
/**
|
||||
* Tests for {@link ArchiveRouteFactory}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ArchiveRouteFactoryTest extends RouteFactoryTestSuite {
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
||||
@InjectMocks
|
||||
private ArchiveRouteFactory archiveRouteFactory;
|
||||
|
||||
@Test
|
||||
void create() {
|
||||
String prefix = "/new-archives";
|
||||
RouterFunction<ServerResponse> routerFunction = archiveRouteFactory.create(prefix);
|
||||
WebTestClient client = getWebTestClient(routerFunction);
|
||||
|
||||
client.get()
|
||||
.uri(prefix)
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
client.get()
|
||||
.uri(prefix + "/page/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
client.get()
|
||||
.uri(prefix + "/2022/09")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
client.get()
|
||||
.uri(prefix + "/2022/08/page/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
client.get()
|
||||
.uri(prefix + "/2022/8/page/1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
|
||||
client.get()
|
||||
.uri("/nothing")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
||||
/**
|
||||
* Tests for {@link AuthorPostsRouteFactory}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuthorPostsRouteFactoryTest extends RouteFactoryTestSuite {
|
||||
@Mock
|
||||
ReactiveExtensionClient client;
|
||||
@InjectMocks
|
||||
AuthorPostsRouteFactory authorPostsRouteFactory;
|
||||
|
||||
@Test
|
||||
void create() {
|
||||
RouterFunction<ServerResponse> routerFunction = authorPostsRouteFactory.create(null);
|
||||
WebTestClient webClient = getWebTestClient(routerFunction);
|
||||
|
||||
when(client.get(eq(User.class), eq("fake-user")))
|
||||
.thenReturn(Mono.just(new User()));
|
||||
webClient.get()
|
||||
.uri("/authors/fake-user")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
|
||||
/**
|
||||
* Tests for {@link CategoriesRouteFactory}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class CategoriesRouteFactoryTest extends RouteFactoryTestSuite {
|
||||
|
||||
@Mock
|
||||
private CategoryFinder categoryFinder;
|
||||
|
||||
@InjectMocks
|
||||
private CategoriesRouteFactory categoriesRouteFactory;
|
||||
|
||||
@Test
|
||||
void create() {
|
||||
String prefix = "/topics";
|
||||
RouterFunction<ServerResponse> routerFunction = categoriesRouteFactory.create(prefix);
|
||||
WebTestClient webClient = getWebTestClient(routerFunction);
|
||||
|
||||
when(categoryFinder.listAsTree())
|
||||
.thenReturn(Flux.empty());
|
||||
webClient.get()
|
||||
.uri(prefix)
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
|
||||
/**
|
||||
* Tests for {@link IndexRouteFactory}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IndexRouteFactoryTest extends RouteFactoryTestSuite {
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
||||
@InjectMocks
|
||||
private IndexRouteFactory indexRouteFactory;
|
||||
|
||||
@Test
|
||||
void create() {
|
||||
RouterFunction<ServerResponse> routerFunction = indexRouteFactory.create("/");
|
||||
WebTestClient webTestClient = getWebTestClient(routerFunction);
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/page/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
}
|
||||
}
|
|
@ -1,17 +1,14 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
|
@ -20,38 +17,27 @@ import org.springframework.web.reactive.result.view.ViewResolver;
|
|||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.theme.router.PermalinkHttpGetRouter;
|
||||
import run.halo.app.theme.router.EmptyView;
|
||||
|
||||
/**
|
||||
* Abstract test for {@link DetailsPageRouteHandlerStrategy} and
|
||||
* {@link ListPageRouteHandlerStrategy}.
|
||||
* Abstract test for {@link RouteFactory}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
abstract class RouterStrategyTestSuite {
|
||||
abstract class RouteFactoryTestSuite {
|
||||
@Mock
|
||||
protected SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
@Mock
|
||||
protected ApplicationContext applicationContext;
|
||||
@Mock
|
||||
protected HaloProperties haloProperties;
|
||||
|
||||
@Mock
|
||||
protected ViewResolver viewResolver;
|
||||
|
||||
@InjectMocks
|
||||
protected PermalinkHttpGetRouter permalinkHttpGetRouter;
|
||||
|
||||
@BeforeEach
|
||||
final void setUpParent() throws URISyntaxException {
|
||||
lenient().when(environmentFetcher.fetchPost())
|
||||
.thenReturn(Mono.just(new SystemSetting.Post()));
|
||||
lenient().when(environmentFetcher.fetch(eq(SystemSetting.ThemeRouteRules.GROUP),
|
||||
eq(SystemSetting.ThemeRouteRules.class))).thenReturn(Mono.just(getThemeRouteRules()));
|
||||
lenient().when(haloProperties.getExternalUrl()).thenReturn(new URI("http://example.com"));
|
||||
lenient().when(viewResolver.resolveViewName(any(), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
setUp();
|
||||
|
@ -77,8 +63,4 @@ abstract class RouterStrategyTestSuite {
|
|||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
public RouterFunction<ServerResponse> getRouterFunction() {
|
||||
return request -> Mono.justOrEmpty(permalinkHttpGetRouter.route(request));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package run.halo.app.theme.router.factories;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
import run.halo.app.theme.finders.vo.TagVo;
|
||||
|
||||
/**
|
||||
* Tests for @link TagPostRouteFactory}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TagPostRouteFactoryTest extends RouteFactoryTestSuite {
|
||||
@Mock
|
||||
private ReactiveExtensionClient client;
|
||||
@Mock
|
||||
private TagFinder tagFinder;
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
||||
@InjectMocks
|
||||
TagPostRouteFactory tagPostRouteFactory;
|
||||
|
||||
@Test
|
||||
void create() {
|
||||
when(client.list(eq(Tag.class), any(), eq(null))).thenReturn(Flux.empty());
|
||||
WebTestClient webTestClient = getWebTestClient(tagPostRouteFactory.create("/new-tags"));
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/new-tags/tag-slug-1")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound();
|
||||
|
||||
Tag tag = new Tag();
|
||||
tag.setMetadata(new Metadata());
|
||||
tag.getMetadata().setName("fake-tag-name");
|
||||
tag.setSpec(new Tag.TagSpec());
|
||||
tag.getSpec().setSlug("tag-slug-2");
|
||||
when(client.list(eq(Tag.class), any(), eq(null))).thenReturn(Flux.just(tag));
|
||||
when(tagFinder.getByName(eq(tag.getMetadata().getName())))
|
||||
.thenReturn(Mono.just(TagVo.from(tag)));
|
||||
webTestClient.get()
|
||||
.uri("/new-tags/tag-slug-2")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/new-tags/tag-slug-2/page/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
|
||||
/**
|
||||
* Tests for {@link ArchivesRouteStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ArchivesRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
||||
@InjectMocks
|
||||
private ArchivesRouteStrategy archivesRouteStrategy;
|
||||
|
||||
@Test
|
||||
void getRouteFunctionWhenDefaultPattern() {
|
||||
HandlerFunction<ServerResponse> handler = archivesRouteStrategy.getHandler();
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
|
||||
WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
List<String> routerPaths = archivesRouteStrategy.getRouterPaths("/archives");
|
||||
for (String routerPath : routerPaths) {
|
||||
permalinkHttpGetRouter.insert(routerPath, handler);
|
||||
}
|
||||
|
||||
fixedAssertion(client, "/archives");
|
||||
|
||||
client.get()
|
||||
.uri("/nothing")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
private static void fixedAssertion(WebTestClient client, String prefix) {
|
||||
client.get()
|
||||
.uri(prefix)
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
client.get()
|
||||
.uri(prefix + "/page/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
client.get()
|
||||
.uri(prefix + "/2022/09")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
client.get()
|
||||
.uri(prefix + "/2022/08/page/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
client.get()
|
||||
.uri(prefix + "/2022/8/page/1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunctionWhenOtherPattern() {
|
||||
HandlerFunction<ServerResponse> handler = archivesRouteStrategy.getHandler();
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
|
||||
final WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
List<String> routerPaths = archivesRouteStrategy.getRouterPaths("/archives-test");
|
||||
for (String routerPath : routerPaths) {
|
||||
permalinkHttpGetRouter.insert(routerPath, handler);
|
||||
}
|
||||
|
||||
fixedAssertion(client, "/archives-test");
|
||||
|
||||
client.get()
|
||||
.uri("/archives")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
|
||||
/**
|
||||
* Tests for {@link AuthorRouteStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.1
|
||||
*/
|
||||
class AuthorRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
|
||||
@Mock
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
@InjectMocks
|
||||
private AuthorRouteStrategy strategy;
|
||||
|
||||
@Test
|
||||
void handlerTest() {
|
||||
User user = new User();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("fake-user");
|
||||
user.setMetadata(metadata);
|
||||
user.setSpec(new User.UserSpec());
|
||||
|
||||
when(client.fetch(eq(User.class), eq("fake-user"))).thenReturn(Mono.just(user));
|
||||
permalinkHttpGetRouter.insert("/authors/fake-user",
|
||||
strategy.getHandler(getThemeRouteRules(), "fake-user"));
|
||||
|
||||
when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.AUTHOR.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView() {
|
||||
@Override
|
||||
public Mono<Void> render(Map<String, ?> model, MediaType contentType,
|
||||
ServerWebExchange exchange) {
|
||||
assertThat(model.get("name")).isEqualTo("fake-user");
|
||||
assertThat(model.get("_templateId"))
|
||||
.isEqualTo(DefaultTemplateEnum.AUTHOR.getValue());
|
||||
assertThat(model.get("author")).isNotNull();
|
||||
assertThat(model.get("posts")).isNotNull();
|
||||
return Mono.empty();
|
||||
}
|
||||
}));
|
||||
|
||||
WebTestClient webTestClient = getWebTestClient(getRouterFunction());
|
||||
webTestClient.get()
|
||||
.uri("/authors/fake-user")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
|
||||
/**
|
||||
* Tests for {@link CategoriesRouteStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CategoriesRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
@Mock
|
||||
private CategoryFinder categoryFinder;
|
||||
|
||||
@InjectMocks
|
||||
private CategoriesRouteStrategy categoriesRouteStrategy;
|
||||
|
||||
@Test
|
||||
void getRouteFunction() {
|
||||
HandlerFunction<ServerResponse> handler = categoriesRouteStrategy.getHandler();
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
|
||||
WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
List<String> routerPaths = categoriesRouteStrategy.getRouterPaths("/categories-test");
|
||||
for (String routerPath : routerPaths) {
|
||||
permalinkHttpGetRouter.insert(routerPath, handler);
|
||||
}
|
||||
|
||||
when(categoryFinder.listAsTree()).thenReturn(Flux.empty());
|
||||
client.get()
|
||||
.uri("/categories-test")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
client.get()
|
||||
.uri("/nothing")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
|
||||
/**
|
||||
* Tests for {@link CategoryRouteStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CategoryRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
||||
@Mock
|
||||
private CategoryFinder categoryFinder;
|
||||
|
||||
@InjectMocks
|
||||
private CategoryRouteStrategy categoryRouteStrategy;
|
||||
|
||||
@Test
|
||||
void getRouteFunction() {
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
final WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
permalinkHttpGetRouter.insert("/categories-test/category-slug-1",
|
||||
categoryRouteStrategy.getHandler(null, "category-slug-1"));
|
||||
permalinkHttpGetRouter.insert("/categories-test/category-slug-2",
|
||||
categoryRouteStrategy.getHandler(null, "category-slug-2"));
|
||||
|
||||
when(categoryFinder.getByName(any())).thenReturn(Mono.empty());
|
||||
|
||||
// /{prefix}/{slug}
|
||||
client.get()
|
||||
.uri("/categories-test/category-slug-1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
// /{prefix}/{slug}
|
||||
client.get()
|
||||
.uri("/categories-test/category-slug-2")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
// /{prefix}/{slug}/page/{page}
|
||||
client.get()
|
||||
.uri("/categories-test/category-slug-2/page/1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
client.get()
|
||||
.uri("/categories-test/not-exist-slug")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
|
||||
client.get()
|
||||
.uri("/nothing")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
|
||||
/**
|
||||
* Tests for {@link IndexRouteStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IndexRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
||||
@InjectMocks
|
||||
private IndexRouteStrategy indexRouteStrategy;
|
||||
|
||||
@Test
|
||||
void getRouteFunction() {
|
||||
HandlerFunction<ServerResponse> handler = indexRouteStrategy.getHandler();
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
|
||||
List<String> routerPaths = indexRouteStrategy.getRouterPaths("/");
|
||||
for (String routerPath : routerPaths) {
|
||||
permalinkHttpGetRouter.insert(routerPath, handler);
|
||||
}
|
||||
|
||||
final WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
client.get()
|
||||
.uri("/")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
client.get()
|
||||
.uri("/page/1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
client.get()
|
||||
.uri("/nothing")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.TestPost;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
import run.halo.app.theme.router.ViewNameResolver;
|
||||
|
||||
/**
|
||||
* Tests for {@link PostRouteStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PostRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
|
||||
@Mock
|
||||
private ViewNameResolver viewNameResolver;
|
||||
|
||||
@InjectMocks
|
||||
private PostRouteStrategy postRouteStrategy;
|
||||
|
||||
@Override
|
||||
public void setUp() {
|
||||
lenient().when(viewNameResolver.resolveViewNameOrDefault(any(), any(), any()))
|
||||
.thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue()));
|
||||
lenient().when(postFinder.getByName(any()))
|
||||
.thenReturn(Mono.just(PostVo.from(TestPost.postV1())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunctionWhenSlugPathVariable() {
|
||||
RouterFunction<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);
|
||||
|
||||
client.get()
|
||||
.uri("/posts-test/fake-slug")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunctionWhenNamePathVariable() {
|
||||
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);
|
||||
|
||||
client.get()
|
||||
.uri("/posts-test/fake-name")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunctionWhenYearMonthSlugPathVariable() {
|
||||
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);
|
||||
|
||||
client.get()
|
||||
.uri("/2022/08/fake-slug")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunctionWhenQueryParam() {
|
||||
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);
|
||||
|
||||
client.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/")
|
||||
.queryParam("p", "fake-name")
|
||||
.build()
|
||||
)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
client.get()
|
||||
.uri(uriBuilder -> uriBuilder.path("/")
|
||||
.queryParam("p", "nothing")
|
||||
.build()
|
||||
)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static run.halo.app.theme.DefaultTemplateEnum.SINGLE_PAGE;
|
||||
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.theme.finders.SinglePageFinder;
|
||||
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||
import run.halo.app.theme.router.ViewNameResolver;
|
||||
|
||||
/**
|
||||
* Tests for {@link SinglePageRouteStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SinglePageRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
@Mock
|
||||
private SinglePageFinder singlePageFinder;
|
||||
|
||||
@Mock
|
||||
private ViewNameResolver viewNameResolver;
|
||||
|
||||
@InjectMocks
|
||||
private SinglePageRouteStrategy strategy;
|
||||
|
||||
|
||||
@Override
|
||||
public void setUp() {
|
||||
lenient().when(viewResolver.resolveViewName(eq(SINGLE_PAGE.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResponse404IfNoPermalinkFound() {
|
||||
createClient().get()
|
||||
.uri("/nothing")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResponse200IfPermalinkFound() {
|
||||
permalinkHttpGetRouter.insert("/fake-slug",
|
||||
strategy.getHandler(getThemeRouteRules(), "fake-name"));
|
||||
when(singlePageFinder.getByName(any())).thenReturn(Mono.empty());
|
||||
createClient().get()
|
||||
.uri("/fake-slug")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResponse200IfSlugNameContainsSpecialChars() {
|
||||
permalinkHttpGetRouter.insert("/fake / slug",
|
||||
strategy.getHandler(getThemeRouteRules(), "fake-name"));
|
||||
|
||||
when(singlePageFinder.getByName(any())).thenReturn(Mono.empty());
|
||||
createClient().get()
|
||||
.uri("/fake / slug")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResponse200IfSlugNameContainsChineseChars() {
|
||||
permalinkHttpGetRouter.insert("/中文",
|
||||
strategy.getHandler(getThemeRouteRules(), "fake-name"));
|
||||
|
||||
when(singlePageFinder.getByName(any())).thenReturn(Mono.empty());
|
||||
|
||||
createClient().get()
|
||||
.uri("/中文")
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureModel() {
|
||||
// fix gh-2912
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("fake-name");
|
||||
SinglePageVo singlePageVo = SinglePageVo.builder()
|
||||
.metadata(metadata)
|
||||
.spec(new SinglePage.SinglePageSpec())
|
||||
.build();
|
||||
|
||||
when(singlePageFinder.getByName(eq("fake-name"))).thenReturn(Mono.just(singlePageVo));
|
||||
permalinkHttpGetRouter.insert("/fake-slug",
|
||||
strategy.getHandler(getThemeRouteRules(), "fake-name"));
|
||||
|
||||
when(viewNameResolver.resolveViewNameOrDefault(any(), any(), any()))
|
||||
.thenReturn(Mono.just("page"));
|
||||
when(viewResolver.resolveViewName(eq(SINGLE_PAGE.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView() {
|
||||
@Override
|
||||
public Mono<Void> render(Map<String, ?> model, MediaType contentType,
|
||||
ServerWebExchange exchange) {
|
||||
assertThat(model.get("name")).isEqualTo("fake-name");
|
||||
assertThat(model.get("plural")).isEqualTo("singlepages");
|
||||
assertThat(model.get("_templateId")).isEqualTo("page");
|
||||
assertThat(model.get("singlePage")).isEqualTo(singlePageVo);
|
||||
assertThat(model.get("groupVersionKind")).isEqualTo(
|
||||
GroupVersionKind.fromExtension(SinglePage.class));
|
||||
return Mono.empty();
|
||||
}
|
||||
}));
|
||||
createClient().get()
|
||||
.uri("/fake-slug")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody();
|
||||
}
|
||||
|
||||
WebTestClient createClient() {
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
return getWebTestClient(routeFunction);
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
|
||||
/**
|
||||
* Tests for {@link TagRouteStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TagRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
|
||||
@Mock
|
||||
private PostFinder postFinder;
|
||||
@Mock
|
||||
private TagFinder tagFinder;
|
||||
|
||||
@InjectMocks
|
||||
private TagRouteStrategy tagRouteStrategy;
|
||||
|
||||
@Test
|
||||
void getRouteFunction() {
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
permalinkHttpGetRouter.insert("/tags-test/fake-slug",
|
||||
tagRouteStrategy.getHandler(getThemeRouteRules(), "fake-name"));
|
||||
when(tagFinder.getByName(any())).thenReturn(Mono.empty());
|
||||
|
||||
client.get()
|
||||
.uri("/tags-test/fake-slug")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
client.get()
|
||||
.uri("/tags-test/fake-slug/page/1")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
client.get()
|
||||
.uri("/nothing")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
|
||||
/**
|
||||
* Tests for {@link TagsRouteStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TagsRouteStrategyTest extends RouterStrategyTestSuite {
|
||||
|
||||
@Mock
|
||||
private TagFinder tagFinder;
|
||||
|
||||
@InjectMocks
|
||||
private TagsRouteStrategy tagsRouteStrategy;
|
||||
|
||||
@Test
|
||||
void getRouteFunction() {
|
||||
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
|
||||
WebTestClient client = getWebTestClient(routeFunction);
|
||||
|
||||
List<String> routerPaths = tagsRouteStrategy.getRouterPaths("/tags-test");
|
||||
HandlerFunction<ServerResponse> handler = tagsRouteStrategy.getHandler();
|
||||
for (String routerPath : routerPaths) {
|
||||
permalinkHttpGetRouter.insert(routerPath, handler);
|
||||
}
|
||||
when(tagFinder.listAll()).thenReturn(Flux.empty());
|
||||
|
||||
client.get()
|
||||
.uri("/tags-test")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
client.get()
|
||||
.uri("/nothing")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue