Skip to content

Commit ffcbea5

Browse files
committed
feat: support route pattern /{categorySlug}/{postSlug} for post access
1 parent 629a0f8 commit ffcbea5

File tree

8 files changed

+121
-5
lines changed

8 files changed

+121
-5
lines changed

application/src/main/java/run/halo/app/content/PostService.java

+4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package run.halo.app.content;
22

3+
import java.util.List;
34
import org.springframework.lang.NonNull;
45
import reactor.core.publisher.Flux;
56
import reactor.core.publisher.Mono;
7+
import run.halo.app.core.extension.content.Category;
68
import run.halo.app.core.extension.content.Post;
79
import run.halo.app.extension.ListResult;
810

@@ -52,4 +54,6 @@ public interface PostService {
5254
Mono<ContentWrapper> deleteContent(String postName, String snapshotName);
5355

5456
Mono<Post> recycleBy(String postName, String username);
57+
58+
Mono<Category> getFirstPresentCategory(List<String> categories);
5559
}

application/src/main/java/run/halo/app/content/impl/PostServiceImpl.java

+15
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import java.time.Duration;
66
import java.time.Instant;
7+
import java.util.Comparator;
78
import java.util.List;
89
import java.util.Objects;
910
import java.util.function.Function;
11+
import java.util.function.ToIntFunction;
1012
import java.util.function.UnaryOperator;
1113
import lombok.extern.slf4j.Slf4j;
1214
import org.apache.commons.lang3.StringUtils;
@@ -15,6 +17,7 @@
1517
import org.springframework.lang.NonNull;
1618
import org.springframework.stereotype.Component;
1719
import org.springframework.util.Assert;
20+
import org.springframework.util.CollectionUtils;
1821
import org.springframework.web.server.ServerWebInputException;
1922
import reactor.core.publisher.Flux;
2023
import reactor.core.publisher.Mono;
@@ -170,6 +173,18 @@ private Flux<Category> listCategories(List<String> categoryNames) {
170173
return client.listAll(Category.class, listOptions, Sort.by("metadata.creationTimestamp"));
171174
}
172175

176+
@Override
177+
public Mono<Category> getFirstPresentCategory(List<String> categoryNames) {
178+
if (CollectionUtils.isEmpty(categoryNames)) {
179+
return Mono.empty();
180+
}
181+
ToIntFunction<Category> comparator =
182+
category -> categoryNames.indexOf(category.getMetadata().getName());
183+
return this.listCategories(categoryNames)
184+
.sort(Comparator.comparingInt(comparator))
185+
.next();
186+
}
187+
173188
private Flux<Contributor> listContributors(List<String> usernames) {
174189
if (usernames == null) {
175190
return Flux.empty();

application/src/main/java/run/halo/app/content/permalinks/PostPermalinkPolicy.java

+9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.Properties;
1313
import lombok.RequiredArgsConstructor;
1414
import org.springframework.stereotype.Component;
15+
import run.halo.app.content.PostService;
1516
import run.halo.app.core.extension.content.Constant;
1617
import run.halo.app.core.extension.content.Post;
1718
import run.halo.app.extension.MetadataUtil;
@@ -27,12 +28,14 @@
2728
@Component
2829
@RequiredArgsConstructor
2930
public class PostPermalinkPolicy implements PermalinkPolicy<Post> {
31+
public static final String DEFAULT_CATEGORY = "default";
3032
public static final String DEFAULT_PERMALINK_PATTERN =
3133
SystemSetting.ThemeRouteRules.empty().getPost();
3234
private static final NumberFormat NUMBER_FORMAT = new DecimalFormat("00");
3335

3436
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
3537
private final ExternalUrlSupplier externalUrlSupplier;
38+
private final PostService postService;
3639

3740
@Override
3841
public String permalink(Post post) {
@@ -62,6 +65,12 @@ private String createPermalink(Post post, String pattern) {
6265
properties.put("month", NUMBER_FORMAT.format(zonedDateTime.getMonthValue()));
6366
properties.put("day", NUMBER_FORMAT.format(zonedDateTime.getDayOfMonth()));
6467

68+
var categorySlug = postService.getFirstPresentCategory(post.getSpec().getCategories())
69+
.blockOptional()
70+
.map(category -> category.getSpec().getSlug())
71+
.orElse(DEFAULT_CATEGORY);
72+
properties.put("categorySlug", categorySlug);
73+
6574
String simplifiedPattern = PathUtils.simplifyPathPattern(pattern);
6675
String permalink =
6776
PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(simplifiedPattern, properties);

application/src/main/java/run/halo/app/core/reconciler/CategoryReconciler.java

+19
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,22 @@
88
import java.util.Set;
99
import lombok.AllArgsConstructor;
1010
import org.springframework.context.ApplicationEventPublisher;
11+
import org.springframework.data.domain.Sort;
1112
import org.springframework.stereotype.Component;
1213
import org.springframework.util.CollectionUtils;
1314
import run.halo.app.content.CategoryService;
1415
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
1516
import run.halo.app.core.extension.content.Category;
1617
import run.halo.app.core.extension.content.Constant;
18+
import run.halo.app.core.extension.content.Post;
1719
import run.halo.app.event.post.CategoryHiddenStateChangeEvent;
1820
import run.halo.app.extension.ExtensionClient;
1921
import run.halo.app.extension.ExtensionUtil;
22+
import run.halo.app.extension.ListOptions;
2023
import run.halo.app.extension.controller.Controller;
2124
import run.halo.app.extension.controller.ControllerBuilder;
2225
import run.halo.app.extension.controller.Reconciler;
26+
import run.halo.app.extension.index.query.QueryFactory;
2327

2428
/**
2529
* Reconciler for {@link Category}.
@@ -43,6 +47,7 @@ public Result reconcile(Request request) {
4347
if (ExtensionUtil.isDeleted(category)) {
4448
if (removeFinalizers(category.getMetadata(), Set.of(FINALIZER_NAME))) {
4549
refreshHiddenState(category, false);
50+
updateCategoryForPost(category.getMetadata().getName());
4651
client.update(category);
4752
}
4853
return;
@@ -118,4 +123,18 @@ void populatePermalink(Category category) {
118123
category.getStatusOrDefault()
119124
.setPermalink(categoryPermalinkPolicy.permalink(category));
120125
}
126+
127+
private void updateCategoryForPost(String categoryName) {
128+
var posts = client.listAll(Post.class, ListOptions.builder()
129+
.fieldQuery(QueryFactory.equal("spec.categories", categoryName))
130+
.build(), Sort.by("metadata.creationTimestamp", "metadata.name")
131+
);
132+
for (Post post : posts) {
133+
var categoryNames = post.getSpec().getCategories();
134+
if (!CollectionUtils.isEmpty(categoryNames)) {
135+
categoryNames.remove(categoryName);
136+
}
137+
client.update(post);
138+
}
139+
}
121140
}

application/src/main/java/run/halo/app/theme/router/ThemeCompositeRouterFunction.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ public class ThemeCompositeRouterFunction implements RouterFunction<ServerRespon
5555
@NonNull
5656
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
5757
return Flux.fromIterable(cachedRouters)
58-
.concatMap(routerFunction -> routerFunction.route(request))
58+
.concatMap(routerFunction -> routerFunction.route(request)
59+
.filterWhen(handle -> handle.handle(request).hasElement())
60+
)
5961
.next();
6062
}
6163

application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java

+23-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.apache.commons.lang3.StringUtils.isNotBlank;
55
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
66
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
7+
import static run.halo.app.content.permalinks.PostPermalinkPolicy.DEFAULT_CATEGORY;
78

89
import com.google.common.cache.Cache;
910
import com.google.common.cache.CacheBuilder;
@@ -33,11 +34,11 @@
3334
import org.springframework.web.server.i18n.LocaleContextResolver;
3435
import reactor.core.publisher.Flux;
3536
import reactor.core.publisher.Mono;
37+
import run.halo.app.content.PostService;
3638
import run.halo.app.core.extension.content.Post;
3739
import run.halo.app.extension.MetadataUtil;
3840
import run.halo.app.extension.ReactiveExtensionClient;
3941
import run.halo.app.extension.index.query.QueryFactory;
40-
import run.halo.app.infra.exception.NotFoundException;
4142
import run.halo.app.infra.utils.JsonUtils;
4243
import run.halo.app.theme.DefaultTemplateEnum;
4344
import run.halo.app.theme.ViewNameResolver;
@@ -69,6 +70,7 @@ public class PostRouteFactory implements RouteFactory {
6970
private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator;
7071

7172
private final LocaleContextResolver localeContextResolver;
73+
private final PostService postService;
7274

7375
@Override
7476
public RouterFunction<ServerResponse> create(String pattern) {
@@ -151,9 +153,26 @@ && matchIfPresent(variable.getYear(), labels.get(Post.ARCHIVE_YEAR_LABEL))
151153
&& matchIfPresent(variable.getMonth(), labels.get(Post.ARCHIVE_MONTH_LABEL))
152154
&& matchIfPresent(variable.getDay(), labels.get(Post.ARCHIVE_DAY_LABEL));
153155
})
156+
.filterWhen(post -> {
157+
if (isNotBlank(variable.getCategorySlug())) {
158+
var categoryNames = post.getSpec().getCategories();
159+
return postService.getFirstPresentCategory(categoryNames)
160+
.filter(category -> category.getSpec().getSlug()
161+
.equals(variable.getCategorySlug())
162+
)
163+
.map(category -> category.getSpec().getSlug())
164+
.switchIfEmpty(Mono.fromSupplier(() -> {
165+
if (DEFAULT_CATEGORY.equals(variable.getCategorySlug())) {
166+
return DEFAULT_CATEGORY;
167+
}
168+
return null;
169+
}))
170+
.hasElement();
171+
}
172+
return Mono.just(true);
173+
})
154174
.next()
155-
.flatMap(post -> postFinder.getByName(post.getMetadata().getName()))
156-
.switchIfEmpty(Mono.error(new NotFoundException("Post not found")));
175+
.flatMap(post -> postFinder.getByName(post.getMetadata().getName()));
157176
}
158177

159178
Flux<Post> postsByPredicates(PostPatternVariable patternVariable) {
@@ -196,6 +215,7 @@ static class PostPatternVariable {
196215
String year;
197216
String month;
198217
String day;
218+
String categorySlug;
199219

200220
static PostPatternVariable from(ServerRequest request) {
201221
Map<String, String> variables = mergedVariables(request);

application/src/main/resources/extensions/system-setting.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ spec:
180180
value: '/{year:\d{4}}/{month:\d{2}}/{slug}'
181181
- label: '/{year}/{month}/{day}/{slug}'
182182
value: '/{year:\d{4}}/{month:\d{2}}/{day:\d{2}}/{slug}'
183+
- label: '/{categorySlug}/{slug}'
184+
value: '/{categorySlug}/{slug}'
185+
- label: '/categories/{categorySlug}/{slug}'
186+
value: '/categories/{categorySlug}/{slug}'
183187
name: post
184188
validation: required
185189
- group: codeInjection

application/src/test/java/run/halo/app/content/permalinks/PostPermalinkPolicyTest.java

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package run.halo.app.content.permalinks;
22

33
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.mockito.ArgumentMatchers.any;
45
import static org.mockito.Mockito.lenient;
56
import static org.mockito.Mockito.when;
67

@@ -10,16 +11,21 @@
1011
import java.time.Instant;
1112
import java.time.ZoneId;
1213
import java.time.ZonedDateTime;
14+
import java.util.List;
1315
import java.util.Map;
1416
import org.junit.jupiter.api.BeforeEach;
1517
import org.junit.jupiter.api.Test;
1618
import org.junit.jupiter.api.extension.ExtendWith;
1719
import org.mockito.Mock;
1820
import org.mockito.junit.jupiter.MockitoExtension;
1921
import org.springframework.context.ApplicationContext;
22+
import reactor.core.publisher.Mono;
23+
import run.halo.app.content.PostService;
2024
import run.halo.app.content.TestPost;
25+
import run.halo.app.core.extension.content.Category;
2126
import run.halo.app.core.extension.content.Constant;
2227
import run.halo.app.core.extension.content.Post;
28+
import run.halo.app.extension.Metadata;
2329
import run.halo.app.extension.MetadataUtil;
2430
import run.halo.app.infra.ExternalUrlSupplier;
2531
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
@@ -44,12 +50,17 @@ class PostPermalinkPolicyTest {
4450
@Mock
4551
private SystemConfigurableEnvironmentFetcher environmentFetcher;
4652

53+
@Mock
54+
private PostService postService;
55+
4756
private PostPermalinkPolicy postPermalinkPolicy;
4857

4958
@BeforeEach
5059
void setUp() {
5160
lenient().when(externalUrlSupplier.get()).thenReturn(URI.create(""));
52-
postPermalinkPolicy = new PostPermalinkPolicy(environmentFetcher, externalUrlSupplier);
61+
lenient().when(postService.getFirstPresentCategory(any())).thenReturn(Mono.empty());
62+
postPermalinkPolicy =
63+
new PostPermalinkPolicy(environmentFetcher, externalUrlSupplier, postService);
5364
}
5465

5566
@Test
@@ -93,6 +104,24 @@ void permalink() {
93104
assertThat(permalink).isEqualTo("/posts/test-post");
94105
}
95106

107+
@Test
108+
void permalinkForCategory() {
109+
Post post = TestPost.postV1();
110+
post.getSpec().setCategories(List.of("test-category"));
111+
Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(post);
112+
annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/{categorySlug}/{slug}");
113+
post.getMetadata().setName("test-post");
114+
post.getSpec().setSlug("test-post-slug");
115+
Instant now = Instant.now();
116+
post.getSpec().setPublishTime(now);
117+
118+
var category = createCategory("test-category", "test-category-slug");
119+
when(postService.getFirstPresentCategory(post.getSpec().getCategories()))
120+
.thenReturn(Mono.just(category));
121+
var permalink = postPermalinkPolicy.permalink(post);
122+
assertThat(permalink).isEqualTo("/test-category-slug/test-post-slug");
123+
}
124+
96125
@Test
97126
void permalinkWithExternalUrl() {
98127
Post post = TestPost.postV1();
@@ -112,4 +141,18 @@ void permalinkWithExternalUrl() {
112141
permalink = postPermalinkPolicy.permalink(post);
113142
assertThat(permalink).isEqualTo("http://example.com/2022/11/01/%E4%B8%AD%E6%96%87%20slug");
114143
}
144+
145+
private Category createCategory(String name, String slug) {
146+
Category category = new Category();
147+
Metadata metadata = new Metadata();
148+
metadata.setName(name);
149+
category.setMetadata(metadata);
150+
category.setSpec(new Category.CategorySpec());
151+
category.setStatus(new Category.CategoryStatus());
152+
153+
category.getSpec().setDisplayName("display-name");
154+
category.getSpec().setSlug(slug);
155+
category.getSpec().setPriority(0);
156+
return category;
157+
}
115158
}

0 commit comments

Comments
 (0)