refactor: theme route pattern and permalink indexer (#2397)

#### What type of PR is this?
/kind improvement
/milestone 2.0
/area core

#### What this PR does / why we need it:
使用正则细化主题端路由并优化

如何测试:
1. 在 admin 系统设置中修改文章文章详情页访问规则
2. 根据规则访问文章详情页,如规则为:`/{year:\d{4}}/{month:\d{2}}/{slug}` 而存在文章 slug 为 fake-slug 且发布日期为 2022-09-08 则 /2022/09/fake-slug 能访问, /2022/9/fake-slug 则不能访问
使用规则 `/{year:\d{4}}/{month:\d{2}}/{day:\d{2}}/{slug}`时 /2022/09/08/fake-slug 能访问 /2022/09/8/fake-slug ,则不能访问
#### Which issue(s) this PR fixes:
Fixes #2396

#### Special notes for your reviewer:
/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?
```release-note
None
```
pull/2415/head
guqing 2022-09-13 11:58:10 +08:00 committed by GitHub
parent 9dc7bc1729
commit 069ff04c84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 186 additions and 183 deletions

View File

@ -58,8 +58,8 @@ public class CategoryPermalinkPolicy
@Override @Override
public void onPermalinkDelete(Category category) { public void onPermalinkDelete(Category category) {
applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, getLocator(category), applicationContext.publishEvent(
category.getStatusOrDefault().getPermalink())); new PermalinkIndexDeleteCommand(this, getLocator(category)));
} }
private ExtensionLocator getLocator(Category category) { private ExtensionLocator getLocator(Category category) {

View File

@ -11,6 +11,20 @@ import run.halo.app.extension.GroupVersionKind;
* @param slug extension slug * @param slug extension slug
*/ */
public record ExtensionLocator(GroupVersionKind gvk, String name, String slug) { public record ExtensionLocator(GroupVersionKind gvk, String name, String slug) {
/**
* Create a new {@link ExtensionLocator} instance.
*
* @param gvk group version kind
* @param name extension name
* @param slug extension slug
*/
public ExtensionLocator {
Objects.requireNonNull(gvk, "Group version kind must not be null");
Objects.requireNonNull(name, "Extension name must not be null");
Objects.requireNonNull(slug, "Extension slug must not be null");
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) { if (this == o) {

View File

@ -10,6 +10,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import run.halo.app.core.extension.Post; import run.halo.app.core.extension.Post;
import run.halo.app.extension.GroupVersionKind; import run.halo.app.extension.GroupVersionKind;
import run.halo.app.infra.utils.PathUtils;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.router.PermalinkIndexAddCommand; import run.halo.app.theme.router.PermalinkIndexAddCommand;
import run.halo.app.theme.router.PermalinkIndexDeleteCommand; import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
@ -23,7 +24,6 @@ import run.halo.app.theme.router.PermalinkWatch;
*/ */
@Component @Component
public class PostPermalinkPolicy implements PermalinkPolicy<Post>, PermalinkWatch<Post> { public class PostPermalinkPolicy implements PermalinkPolicy<Post>, PermalinkWatch<Post> {
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Post.class); private final GroupVersionKind gvk = GroupVersionKind.fromExtension(Post.class);
private static final NumberFormat NUMBER_FORMAT = new DecimalFormat("00"); private static final NumberFormat NUMBER_FORMAT = new DecimalFormat("00");
@ -65,8 +65,7 @@ public class PostPermalinkPolicy implements PermalinkPolicy<Post>, PermalinkWatc
@Override @Override
public void onPermalinkDelete(Post post) { public void onPermalinkDelete(Post post) {
applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, getLocator(post), applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, getLocator(post)));
post.getStatusOrDefault().getPermalink()));
} }
private ExtensionLocator getLocator(Post post) { private ExtensionLocator getLocator(Post post) {
@ -85,6 +84,8 @@ public class PostPermalinkPolicy implements PermalinkPolicy<Post>, PermalinkWatc
properties.put("year", String.valueOf(zonedDateTime.getYear())); properties.put("year", String.valueOf(zonedDateTime.getYear()));
properties.put("month", NUMBER_FORMAT.format(zonedDateTime.getMonthValue())); properties.put("month", NUMBER_FORMAT.format(zonedDateTime.getMonthValue()));
properties.put("day", NUMBER_FORMAT.format(zonedDateTime.getDayOfMonth())); properties.put("day", NUMBER_FORMAT.format(zonedDateTime.getDayOfMonth()));
return PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(pattern, properties);
String simplifiedPattern = PathUtils.simplifyPathPattern(pattern);
return PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(simplifiedPattern, properties);
} }
} }

View File

@ -57,8 +57,7 @@ public class TagPermalinkPolicy implements PermalinkPolicy<Tag>, PermalinkWatch<
@Override @Override
public void onPermalinkDelete(Tag tag) { public void onPermalinkDelete(Tag tag) {
applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, getLocator(tag), applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, getLocator(tag)));
tag.getStatusOrDefault().getPermalink()));
} }
private ExtensionLocator getLocator(Tag tag) { private ExtensionLocator getLocator(Tag tag) {

View File

@ -133,8 +133,7 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
.setPermalink(PathUtils.combinePath(singlePage.getSpec().getSlug())); .setPermalink(PathUtils.combinePath(singlePage.getSpec().getSlug()));
ExtensionLocator locator = new ExtensionLocator(GVK, singlePage.getMetadata().getName(), ExtensionLocator locator = new ExtensionLocator(GVK, singlePage.getMetadata().getName(),
singlePage.getSpec().getSlug()); singlePage.getSpec().getSlug());
applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, locator, applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, locator));
singlePage.getStatusOrDefault().getPermalink()));
templateRouteManager.changeTemplatePattern(DefaultTemplateEnum.SINGLE_PAGE.getValue()); templateRouteManager.changeTemplatePattern(DefaultTemplateEnum.SINGLE_PAGE.getValue());
} }

View File

@ -47,4 +47,35 @@ public class PathUtils {
public static String appendPathSeparatorIfMissing(String path) { public static String appendPathSeparatorIfMissing(String path) {
return StringUtils.appendIfMissing(path, "/", "/"); return StringUtils.appendIfMissing(path, "/", "/");
} }
/**
* <p>Remove the regex in the path pattern placeholder.</p>
* <p>For example: </p>
* <ul>
* <li>'{@code /{year:\d{4}}/{month:\d{2}}}' &rarr; '{@code /{year}/{month}}'</li>
* <li>'{@code /archives/{year:\d{4}}/{month:\d{2}}}' &rarr; '{@code /archives/{year}/{month}
* }'</li>
* <li>'{@code /archives/{year:\d{4}}/{slug}}' &rarr; '{@code /archives/{year}/{slug}}'</li>
* </ul>
*
* @param pattern path pattern
* @return Simplified path pattern
*/
public static String simplifyPathPattern(String pattern) {
if (StringUtils.isBlank(pattern)) {
return StringUtils.EMPTY;
}
String[] parts = StringUtils.split(pattern, '/');
for (int i = 0; i < parts.length; i++) {
String part = parts[i];
if (part.startsWith("{") && part.endsWith("}")) {
int colonIdx = part.indexOf(':');
if (colonIdx != -1) {
parts[i] = part.substring(0, colonIdx) + part.charAt(part.length() - 1);
}
}
}
return combinePath(parts);
}
} }

View File

@ -12,19 +12,13 @@ import run.halo.app.content.permalinks.ExtensionLocator;
*/ */
public class PermalinkIndexDeleteCommand extends ApplicationEvent { public class PermalinkIndexDeleteCommand extends ApplicationEvent {
private final ExtensionLocator locator; private final ExtensionLocator locator;
private final String permalink;
public PermalinkIndexDeleteCommand(Object source, ExtensionLocator locator, String permalink) { public PermalinkIndexDeleteCommand(Object source, ExtensionLocator locator) {
super(source); super(source);
this.locator = locator; this.locator = locator;
this.permalink = permalink;
} }
public ExtensionLocator getLocator() { public ExtensionLocator getLocator() {
return locator; return locator;
} }
public String getPermalink() {
return permalink;
}
} }

View File

@ -3,12 +3,11 @@ package run.halo.app.theme.router;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import run.halo.app.content.permalinks.ExtensionLocator; import run.halo.app.content.permalinks.ExtensionLocator;
import run.halo.app.extension.GroupVersionKind; import run.halo.app.extension.GroupVersionKind;
@ -21,9 +20,12 @@ import run.halo.app.extension.GroupVersionKind;
@Component @Component
public class PermalinkIndexer { public class PermalinkIndexer {
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final MultiValueMap<GroupVersionKind, String> permalinkLookup =
new LinkedMultiValueMap<>(); private final Map<GvkName, String> gvkNamePermalinkLookup = new HashMap<>();
private final Map<String, ExtensionLocator> permalinkLocatorMap = new HashMap<>(); private final Map<String, ExtensionLocator> permalinkLocatorLookup = new HashMap<>();
record GvkName(GroupVersionKind gvk, String name) {
}
/** /**
* Register extension and permalink mapping. * Register extension and permalink mapping.
@ -34,8 +36,9 @@ public class PermalinkIndexer {
public void register(ExtensionLocator locator, String permalink) { public void register(ExtensionLocator locator, String permalink) {
readWriteLock.writeLock().lock(); readWriteLock.writeLock().lock();
try { try {
permalinkLookup.add(locator.gvk(), permalink); GvkName gvkName = new GvkName(locator.gvk(), locator.name());
permalinkLocatorMap.put(permalink, locator); gvkNamePermalinkLookup.put(gvkName, permalink);
permalinkLocatorLookup.put(permalink, locator);
} finally { } finally {
readWriteLock.writeLock().unlock(); readWriteLock.writeLock().unlock();
} }
@ -44,110 +47,87 @@ public class PermalinkIndexer {
/** /**
* Remove extension and permalink mapping. * Remove extension and permalink mapping.
* *
* @param locator extension locator * @param locator extension info
* @param permalink extension permalink for theme template route
*/ */
public void remove(ExtensionLocator locator, String permalink) { public void remove(ExtensionLocator locator) {
readWriteLock.writeLock().lock(); readWriteLock.writeLock().lock();
try { try {
List<String> permalinks = permalinkLookup.get(locator.gvk()); String permalink =
if (permalinks != null) { gvkNamePermalinkLookup.remove(new GvkName(locator.gvk(), locator.name()));
permalinks.remove(permalink); if (permalink != null) {
if (permalinks.isEmpty()) { permalinkLocatorLookup.remove(permalink);
permalinkLookup.remove(locator.gvk());
} }
}
permalinkLocatorMap.remove(permalink);
} finally { } finally {
readWriteLock.writeLock().unlock(); readWriteLock.writeLock().unlock();
} }
} }
/** /**
* Lookup extension locator by permalink. * 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 * @param permalink extension permalink for theme template route
* @return extension locator * @return extension locator
*/ */
@Nullable
public ExtensionLocator lookup(String permalink) { public ExtensionLocator lookup(String permalink) {
readWriteLock.readLock().lock(); readWriteLock.readLock().lock();
try { try {
return permalinkLocatorMap.get(permalink); return permalinkLocatorLookup.get(permalink);
} finally { } finally {
readWriteLock.readLock().unlock(); readWriteLock.readLock().unlock();
} }
} }
/** /**
* Gets permalinks by extension's {@link GroupVersionKind}. * Lookup extension permalink by {@link GroupVersionKind} and {@code name}.
* *
* @param gvk extension's {@link GroupVersionKind} * @param gvk group version kind
* @return permalinks for extension's {@link GroupVersionKind} * @param name extension name
* @return {@code true} if contains, otherwise {@code false}
*/ */
public List<String> getPermalinks(GroupVersionKind gvk) { public boolean containsName(GroupVersionKind gvk, String name) {
readWriteLock.readLock().lock(); readWriteLock.readLock().lock();
try { try {
return Objects.requireNonNullElse(permalinkLookup.get(gvk), List.of()); return gvkNamePermalinkLookup.containsKey(new GvkName(gvk, name));
} finally { } finally {
readWriteLock.readLock().unlock(); readWriteLock.readLock().unlock();
} }
} }
/** /**
* Lookup extension resource names by {@link GroupVersionKind}. * Lookup extension permalink by {@link GroupVersionKind} and {@code slug}.
* *
* @param gvk extension's {@link GroupVersionKind} * @param gvk group version kind
* @return extension resource names * @param slug extension slug
* @return {@code true} if contains, otherwise {@code false}
*/ */
public List<String> getNames(GroupVersionKind gvk) { public boolean containsSlug(GroupVersionKind gvk, String slug) {
readWriteLock.readLock().lock(); readWriteLock.readLock().lock();
try { try {
return permalinkLocatorMap.values() return permalinkLocatorLookup.values()
.stream() .stream()
.filter(locator -> locator.gvk().equals(gvk)) .anyMatch(locator -> locator.gvk().equals(gvk)
.map(ExtensionLocator::name) && locator.slug().equals(slug));
.toList();
} finally {
readWriteLock.readLock().unlock();
}
}
/**
* Lookup extension resource slugs by {@link GroupVersionKind}.
*
* @param gvk extension's {@link GroupVersionKind}
* @return extension resource slugs
*/
public List<String> getSlugs(GroupVersionKind gvk) {
readWriteLock.readLock().lock();
try {
return permalinkLocatorMap.values()
.stream()
.filter(locator -> locator.gvk().equals(gvk))
.map(ExtensionLocator::slug)
.toList();
} finally {
readWriteLock.readLock().unlock();
}
}
/**
* Lookup extension slug by resource name.
*
* @param gvk extension's {@link GroupVersionKind}
* @param name extension resource name
* @return extension slug specified by resource name
*/
public String getSlugByName(GroupVersionKind gvk, String name) {
readWriteLock.readLock().lock();
try {
return permalinkLocatorMap.values()
.stream()
.filter(locator -> locator.gvk().equals(gvk))
.filter(locator -> locator.name().equals(name))
.findFirst()
.map(ExtensionLocator::slug)
.orElseThrow();
} finally { } finally {
readWriteLock.readLock().unlock(); readWriteLock.readLock().unlock();
} }
@ -163,10 +143,10 @@ public class PermalinkIndexer {
public String getNameBySlug(GroupVersionKind gvk, String slug) { public String getNameBySlug(GroupVersionKind gvk, String slug) {
readWriteLock.readLock().lock(); readWriteLock.readLock().lock();
try { try {
return permalinkLocatorMap.values() return permalinkLocatorLookup.values()
.stream() .stream()
.filter(locator -> locator.gvk().equals(gvk)) .filter(locator -> locator.gvk().equals(gvk)
.filter(locator -> locator.slug().equals(slug)) && locator.slug().equals(slug))
.findFirst() .findFirst()
.map(ExtensionLocator::name) .map(ExtensionLocator::name)
.orElseThrow(); .orElseThrow();
@ -180,8 +160,8 @@ public class PermalinkIndexer {
* *
* @return permalinkLookup map size * @return permalinkLookup map size
*/ */
protected long permalinkLookupSize() { protected long gvkNamePermalinkMapSize() {
return permalinkLookup.size(); return gvkNamePermalinkLookup.size();
} }
/** /**
@ -190,7 +170,7 @@ public class PermalinkIndexer {
* @return permalinkLocatorMap map size * @return permalinkLocatorMap map size
*/ */
protected long permalinkLocatorMapSize() { protected long permalinkLocatorMapSize() {
return permalinkLocatorMap.size(); return permalinkLocatorLookup.size();
} }
@EventListener(PermalinkIndexAddCommand.class) @EventListener(PermalinkIndexAddCommand.class)
@ -200,12 +180,12 @@ public class PermalinkIndexer {
@EventListener(PermalinkIndexDeleteCommand.class) @EventListener(PermalinkIndexDeleteCommand.class)
public void onPermalinkDelete(PermalinkIndexDeleteCommand deleteCommand) { public void onPermalinkDelete(PermalinkIndexDeleteCommand deleteCommand) {
remove(deleteCommand.getLocator(), deleteCommand.getPermalink()); remove(deleteCommand.getLocator());
} }
@EventListener(PermalinkIndexUpdateCommand.class) @EventListener(PermalinkIndexUpdateCommand.class)
public void onPermalinkUpdate(PermalinkIndexUpdateCommand updateCommand) { public void onPermalinkUpdate(PermalinkIndexUpdateCommand updateCommand) {
remove(updateCommand.getLocator(), updateCommand.getPermalink()); remove(updateCommand.getLocator());
register(updateCommand.getLocator(), updateCommand.getPermalink()); register(updateCommand.getLocator(), updateCommand.getPermalink());
} }
} }

View File

@ -40,9 +40,10 @@ public class ArchivesRouteStrategy implements TemplateRouterStrategy {
public RouterFunction<ServerResponse> getRouteFunction(String template, String prefix) { public RouterFunction<ServerResponse> getRouteFunction(String template, String prefix) {
return RouterFunctions return RouterFunctions
.route(GET(prefix) .route(GET(prefix)
.or(GET(PathUtils.combinePath(prefix, "/page/{page}"))) .or(GET(PathUtils.combinePath(prefix, "/page/{page:\\d+}")))
.or(GET(PathUtils.combinePath(prefix, "/{year}/{month}"))) .or(GET(PathUtils.combinePath(prefix, "/{year:\\d{4}}/{month:\\d{2}}")))
.or(GET(PathUtils.combinePath(prefix, "/{year}/{month}/page/{page}"))) .or(GET(PathUtils.combinePath(prefix,
"/{year:\\d{4}}/{month:\\d{2}}/page/{page:\\d+}")))
.and(accept(MediaType.TEXT_HTML)), .and(accept(MediaType.TEXT_HTML)),
request -> ServerResponse.ok() request -> ServerResponse.ok()
.render(DefaultTemplateEnum.ARCHIVES.getValue(), .render(DefaultTemplateEnum.ARCHIVES.getValue(),

View File

@ -5,7 +5,6 @@ import static org.springframework.web.reactive.function.server.RequestPredicates
import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.pageNum; import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.pageNum;
import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.totalPage; import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.totalPage;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -53,13 +52,12 @@ public class CategoryRouteStrategy implements TemplateRouterStrategy {
public RouterFunction<ServerResponse> getRouteFunction(String template, String prefix) { public RouterFunction<ServerResponse> getRouteFunction(String template, String prefix) {
return RouterFunctions return RouterFunctions
.route(GET(PathUtils.combinePath(prefix, "/{slug}")) .route(GET(PathUtils.combinePath(prefix, "/{slug}"))
.or(GET(PathUtils.combinePath(prefix, "/{slug}/page/{page}"))) .or(GET(PathUtils.combinePath(prefix, "/{slug}/page/{page:\\d+}")))
.and(accept(MediaType.TEXT_HTML)), .and(accept(MediaType.TEXT_HTML)),
request -> { request -> {
String slug = request.pathVariable("slug"); String slug = request.pathVariable("slug");
GroupVersionKind gvk = GroupVersionKind.fromExtension(Category.class); GroupVersionKind gvk = GroupVersionKind.fromExtension(Category.class);
List<String> slugs = permalinkIndexer.getSlugs(gvk); if (!permalinkIndexer.containsSlug(gvk, slug)) {
if (!slugs.contains(slug)) {
return ServerResponse.notFound().build(); return ServerResponse.notFound().build();
} }
String categoryName = permalinkIndexer.getNameBySlug(gvk, slug); String categoryName = permalinkIndexer.getNameBySlug(gvk, slug);

View File

@ -39,7 +39,7 @@ public class IndexRouteStrategy implements TemplateRouterStrategy {
@Override @Override
public RouterFunction<ServerResponse> getRouteFunction(String template, String pattern) { public RouterFunction<ServerResponse> getRouteFunction(String template, String pattern) {
return RouterFunctions return RouterFunctions
.route(GET("/").or(GET("/page/{page}")) .route(GET("/").or(GET("/page/{page:\\d+}"))
.and(accept(MediaType.TEXT_HTML)), .and(accept(MediaType.TEXT_HTML)),
request -> ServerResponse.ok() request -> ServerResponse.ok()
.render(DefaultTemplateEnum.INDEX.getValue(), .render(DefaultTemplateEnum.INDEX.getValue(),

View File

@ -4,7 +4,6 @@ import static org.springframework.web.reactive.function.server.RequestPredicates
import static org.springframework.web.reactive.function.server.RequestPredicates.accept; import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -75,32 +74,27 @@ public class PostRouteStrategy implements TemplateRouterStrategy {
} }
return ServerResponse.ok() return ServerResponse.ok()
.render(DefaultTemplateEnum.POST.getValue(), .render(DefaultTemplateEnum.POST.getValue(),
Map.of(PostRequestParamPredicate.NAME_PARAM, name) Map.of(PostRequestParamPredicate.NAME_PARAM, name,
"post", postByName(name))
); );
}); });
} }
return RouterFunctions return RouterFunctions
.route(GET(pattern).and(accept(MediaType.TEXT_HTML)), .route(GET(pattern).and(accept(MediaType.TEXT_HTML)),
request -> { request -> {
List<String> permalinks = ExtensionLocator locator = permalinkIndexer.lookup(request.path());
permalinkIndexer.getPermalinks( if (locator == null) {
PostRequestParamPredicate.GVK);
boolean contains = permalinks.contains(request.path());
if (!contains) {
return ServerResponse.notFound().build(); return ServerResponse.notFound().build();
} }
ExtensionLocator extensionLocator =
permalinkIndexer.lookup(request.path());
PathPattern parse = PathPatternParser.defaultInstance.parse(pattern); PathPattern parse = PathPatternParser.defaultInstance.parse(pattern);
PathPattern.PathMatchInfo pathMatchInfo = PathPattern.PathMatchInfo pathMatchInfo =
parse.matchAndExtract(PathContainer.parsePath(request.path())); parse.matchAndExtract(PathContainer.parsePath(request.path()));
Map<String, Object> model = new HashMap<>(); Map<String, Object> model = new HashMap<>();
model.put(PostRequestParamPredicate.NAME_PARAM, model.put(PostRequestParamPredicate.NAME_PARAM, locator.name());
extensionLocator.name());
if (pathMatchInfo != null) { if (pathMatchInfo != null) {
model.putAll(pathMatchInfo.getUriVariables()); model.putAll(pathMatchInfo.getUriVariables());
} }
model.put("post", postByName(extensionLocator.name())); model.put("post", postByName(locator.name()));
return ServerResponse.ok() return ServerResponse.ok()
.render(DefaultTemplateEnum.POST.getValue(), model); .render(DefaultTemplateEnum.POST.getValue(), model);
}); });
@ -139,17 +133,13 @@ public class PostRouteStrategy implements TemplateRouterStrategy {
} }
if (NAME_PARAM.equals(placeholderName)) { if (NAME_PARAM.equals(placeholderName)) {
return RequestPredicates.queryParam(paramName, name -> { return RequestPredicates.queryParam(paramName,
List<String> names = permalinkIndexer.getNames(GVK); name -> permalinkIndexer.containsName(GVK, name));
return names.contains(name);
});
} }
if (SLUG_PARAM.equals(placeholderName)) { if (SLUG_PARAM.equals(placeholderName)) {
return RequestPredicates.queryParam(paramName, slug -> { return RequestPredicates.queryParam(paramName,
List<String> slugs = permalinkIndexer.getSlugs(GVK); slug -> permalinkIndexer.containsSlug(GVK, slug));
return slugs.contains(slug);
});
} }
throw new IllegalArgumentException( throw new IllegalArgumentException(
String.format("Unknown param value placeholder [%s] in pattern [%s]", String.format("Unknown param value placeholder [%s] in pattern [%s]",

View File

@ -4,7 +4,6 @@ import static org.springframework.web.reactive.function.server.RequestPredicates
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -48,8 +47,7 @@ public class SinglePageRouteStrategy implements TemplateRouterStrategy {
RequestPredicate requestPredicate = request -> false; RequestPredicate requestPredicate = request -> false;
List<String> permalinks = List<String> permalinks = permalinkIndexer.getPermalinks(gvk);
Objects.requireNonNullElse(permalinkIndexer.getPermalinks(gvk), List.of());
for (String permalink : permalinks) { for (String permalink : permalinks) {
requestPredicate = requestPredicate.or(RequestPredicates.GET(permalink)); requestPredicate = requestPredicate.or(RequestPredicates.GET(permalink));
} }

View File

@ -5,7 +5,6 @@ import static org.springframework.web.reactive.function.server.RequestPredicates
import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.pageNum; import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.pageNum;
import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.totalPage; import static run.halo.app.theme.router.TemplateRouterStrategy.PageUrlUtils.totalPage;
import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -53,15 +52,15 @@ public class TagRouteStrategy implements TemplateRouterStrategy {
public RouterFunction<ServerResponse> getRouteFunction(String template, String prefix) { public RouterFunction<ServerResponse> getRouteFunction(String template, String prefix) {
return RouterFunctions return RouterFunctions
.route(GET(PathUtils.combinePath(prefix, "/{slug}")) .route(GET(PathUtils.combinePath(prefix, "/{slug}"))
.or(GET(PathUtils.combinePath(prefix, "/{slug}/page/{page}"))) .or(GET(PathUtils.combinePath(prefix, "/{slug}/page/{page:\\d+}")))
.and(accept(MediaType.TEXT_HTML)), .and(accept(MediaType.TEXT_HTML)),
request -> { request -> {
GroupVersionKind gvk = GroupVersionKind.fromExtension(Tag.class); GroupVersionKind gvk = GroupVersionKind.fromExtension(Tag.class);
List<String> slugs = permalinkIndexer.getSlugs(gvk);
String slug = request.pathVariable("slug"); String slug = request.pathVariable("slug");
if (!slugs.contains(slug)) { if (!permalinkIndexer.containsSlug(gvk, slug)) {
return ServerResponse.notFound().build(); return ServerResponse.notFound().build();
} }
String name = permalinkIndexer.getNameBySlug(gvk, slug); String name = permalinkIndexer.getNameBySlug(gvk, slug);
return ServerResponse.ok() return ServerResponse.ok()
.render(DefaultTemplateEnum.TAG.getValue(), .render(DefaultTemplateEnum.TAG.getValue(),

View File

@ -32,7 +32,7 @@ spec:
label: "默认角色" label: "默认角色"
name: defaultRole name: defaultRole
validation: required validation: required
- group: themeRules - group: routeRules
label: 主题模板路由设置 label: 主题模板路由设置
formSchema: formSchema:
- $formkit: text - $formkit: text
@ -58,9 +58,9 @@ spec:
- /archives/{name} - /archives/{name}
- /?p={name} - /?p={name}
- /?p={slug} - /?p={slug}
- /{year}/{slug} - /{year:\d{4}}/{slug}
- /{year}/{month}/{slug} - /{year:\d{4}}/{month:\d{2}}/{slug}
- /{year}/{month}/{day}/{slug} - /{year:\d{4}}/{month:\d{2}}/{day:\d{2}}/{slug}
name: post name: post
validation: required validation: required
- group: post - group: post

View File

@ -43,4 +43,17 @@ class PathUtilsTest {
s = PathUtils.appendPathSeparatorIfMissing(null); s = PathUtils.appendPathSeparatorIfMissing(null);
assertThat(s).isEqualTo(null); assertThat(s).isEqualTo(null);
} }
@Test
void simplifyPathPattern() {
assertThat(PathUtils.simplifyPathPattern("/a/b/c")).isEqualTo("/a/b/c");
assertThat(PathUtils.simplifyPathPattern("/a/{b}/c")).isEqualTo("/a/{b}/c");
assertThat(PathUtils.simplifyPathPattern("/a/{b}/*")).isEqualTo("/a/{b}/*");
assertThat(PathUtils.simplifyPathPattern("/archives/{year:\\d{4}}/{month:\\d{2}}"))
.isEqualTo("/archives/{year}/{month}");
assertThat(PathUtils.simplifyPathPattern("/archives/{year:\\d{4}}/{slug}"))
.isEqualTo("/archives/{year}/{slug}");
assertThat(PathUtils.simplifyPathPattern("/archives/{year:\\d{4}}/page/{page:\\d+}"))
.isEqualTo("/archives/{year}/page/{page}");
}
} }

View File

@ -49,7 +49,7 @@ class PermalinkIndexerTest {
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1); assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1);
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1); assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(1);
permalinkIndexer.remove(locator, "/fake-permalink"); permalinkIndexer.remove(locator);
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(0); assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(0);
assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(0); assertThat(permalinkIndexer.permalinkLocatorMapSize()).isEqualTo(0);
} }
@ -77,8 +77,8 @@ class PermalinkIndexerTest {
ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug"); ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug");
permalinkIndexer.register(locator, "/test-permalink"); permalinkIndexer.register(locator, "/test-permalink");
List<String> names = permalinkIndexer.getNames(gvk); assertThat(permalinkIndexer.containsName(gvk, "test-name")).isTrue();
assertThat(names).isEqualTo(List.of("fake-name", "test-name")); assertThat(permalinkIndexer.containsName(gvk, "nothing")).isFalse();
} }
@Test @Test
@ -86,24 +86,9 @@ class PermalinkIndexerTest {
ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug"); ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug");
permalinkIndexer.register(locator, "/test-permalink"); permalinkIndexer.register(locator, "/test-permalink");
List<String> slugs = permalinkIndexer.getSlugs(gvk); assertThat(permalinkIndexer.containsSlug(gvk, "fake-slug")).isTrue();
assertThat(slugs).isEqualTo(List.of("fake-slug", "test-slug")); assertThat(permalinkIndexer.containsSlug(gvk, "test-slug")).isTrue();
} assertThat(permalinkIndexer.containsSlug(gvk, "nothing")).isFalse();
@Test
void getSlugByName() {
ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug");
permalinkIndexer.register(locator, "/test-permalink");
String slugByName = permalinkIndexer.getSlugByName(gvk, "test-name");
assertThat(slugByName).isEqualTo("test-slug");
slugByName = permalinkIndexer.getSlugByName(gvk, "fake-name");
assertThat(slugByName).isEqualTo("fake-slug");
assertThatThrownBy(() -> {
permalinkIndexer.getSlugByName(gvk, "nothing");
}).isInstanceOf(NoSuchElementException.class);
} }
@Test @Test

View File

@ -82,14 +82,20 @@ class ArchivesRouteStrategyTest {
.expectStatus().isOk(); .expectStatus().isOk();
client.get() client.get()
.uri(prefix + "/year/month") .uri(prefix + "/2022/09")
.exchange() .exchange()
.expectStatus().isOk(); .expectStatus().isOk();
client.get() client.get()
.uri(prefix + "/year/month/page/1") .uri(prefix + "/2022/08/page/1")
.exchange() .exchange()
.expectStatus().isOk(); .expectStatus().isOk();
client.get()
.uri(prefix + "/2022/8/page/1")
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.NOT_FOUND);
} }
@Test @Test

View File

@ -20,6 +20,8 @@ import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.reactive.result.view.ViewResolver;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Category;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.ListResult; import run.halo.app.extension.ListResult;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.PostFinder;
@ -47,8 +49,11 @@ class CategoryRouteStrategyTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
when(permalinkIndexer.getSlugs(any())) GroupVersionKind gvk = GroupVersionKind.fromExtension(Category.class);
.thenReturn(List.of("category-slug-1", "category-slug-2")); when(permalinkIndexer.containsSlug(eq(gvk), eq("category-slug-1")))
.thenReturn(true);
when(permalinkIndexer.containsSlug(eq(gvk), eq("category-slug-2")))
.thenReturn(true);
when(permalinkIndexer.getNameBySlug(any(), eq("category-slug-1"))) when(permalinkIndexer.getNameBySlug(any(), eq("category-slug-1")))
.thenReturn("category-name-1"); .thenReturn("category-name-1");
when(permalinkIndexer.getNameBySlug(any(), eq("category-slug-2"))) when(permalinkIndexer.getNameBySlug(any(), eq("category-slug-2")))

View File

@ -5,7 +5,6 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.util.List;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -64,9 +63,6 @@ class PostRouteStrategyTest {
piling(); piling();
when(permalinkIndexer.getPermalinks(any()))
.thenReturn(List.of("/posts-test/fake-slug"));
client.get() client.get()
.uri("/posts-test/fake-slug") .uri("/posts-test/fake-slug")
.exchange() .exchange()
@ -84,9 +80,6 @@ class PostRouteStrategyTest {
piling(); piling();
when(permalinkIndexer.getPermalinks(any()))
.thenReturn(List.of("/posts-test/fake-name"));
client.get() client.get()
.uri("/posts-test/fake-name") .uri("/posts-test/fake-name")
.exchange() .exchange()
@ -104,9 +97,6 @@ class PostRouteStrategyTest {
piling(); piling();
when(permalinkIndexer.getPermalinks(any()))
.thenReturn(List.of("/2022/08/fake-slug"));
client.get() client.get()
.uri("/2022/08/fake-slug") .uri("/2022/08/fake-slug")
.exchange() .exchange()
@ -144,14 +134,11 @@ class PostRouteStrategyTest {
} }
private void piling() { private void piling() {
lenient().when(permalinkIndexer.getNames(any())) GroupVersionKind postGvk = GroupVersionKind.fromExtension(Post.class);
.thenReturn(List.of("fake-name")); lenient().when(permalinkIndexer.containsName(eq(postGvk), eq("fake-name")))
.thenReturn(true);
lenient().when(permalinkIndexer.getSlugs(any())) lenient().when(permalinkIndexer.containsSlug(eq(postGvk), eq("fake-slug")))
.thenReturn(List.of("fake-slug")); .thenReturn(true);
lenient().when(permalinkIndexer.getSlugs(any()))
.thenReturn(List.of("fake-slug"));
lenient().when(permalinkIndexer.getNameBySlug(any(), eq("fake-slug"))) lenient().when(permalinkIndexer.getNameBySlug(any(), eq("fake-slug")))
.thenReturn("fake-name"); .thenReturn("fake-name");

View File

@ -20,6 +20,8 @@ import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.reactive.result.view.ViewResolver;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Tag;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.ListResult; import run.halo.app.extension.ListResult;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.PostFinder;
@ -48,8 +50,9 @@ class TagRouteStrategyTest {
void setUp() { void setUp() {
lenient().when(postFinder.listByTag(anyInt(), anyInt(), any())) lenient().when(postFinder.listByTag(anyInt(), anyInt(), any()))
.thenReturn(new ListResult<>(1, 10, 0, List.of())); .thenReturn(new ListResult<>(1, 10, 0, List.of()));
when(permalinkIndexer.getSlugs(any())) GroupVersionKind gvk = GroupVersionKind.fromExtension(Tag.class);
.thenReturn(List.of("fake-slug")); when(permalinkIndexer.containsSlug(eq(gvk), eq("fake-slug")))
.thenReturn(true);
when(permalinkIndexer.getNameBySlug(any(), eq("fake-slug"))) when(permalinkIndexer.getNameBySlug(any(), eq("fake-slug")))
.thenReturn("fake-name"); .thenReturn("fake-name");
} }