mirror of https://github.com/halo-dev/halo
feat: add single page extension module (#2381)
<!-- Thanks for sending a pull request! Here are some tips for you: 1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>。 1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>. 2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。 2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. 3. 请确保你已经添加并运行了适当的测试。 3. Ensure you have added or ran the appropriate tests for your PR. --> #### What type of PR is this? /kind feature /milestone 2.0 /area core <!-- 添加其中一个类别: Add one of the following kinds: /kind bug /kind cleanup /kind documentation /kind feature /kind improvement 适当添加其中一个或多个类别(可选): Optionally add one or more of the following kinds if applicable: /kind api-change /kind deprecation /kind failing-test /kind flake /kind regression --> #### What this PR does / why we need it: 新增自定义页面模块 #### Which issue(s) this PR fixes: <!-- PR 合并时自动关闭 issue。 Automatically closes linked issue when PR is merged. 用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)` Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`. --> Fixes #2379 #### Special notes for your reviewer: /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? <!-- 如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`。 否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新(Break Change), Release Note 需要以 `action required` 开头。 If no, just write "NONE" in the release-note block below. If yes, a release note is required: Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required". --> ```release-note None ```pull/2382/head^2
parent
53ee2ea547
commit
969fcde641
|
@ -18,6 +18,7 @@ import run.halo.app.core.extension.Plugin;
|
|||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.RoleBinding;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.core.extension.Tag;
|
||||
import run.halo.app.core.extension.Theme;
|
||||
import run.halo.app.core.extension.User;
|
||||
|
@ -29,6 +30,7 @@ import run.halo.app.core.extension.reconciler.PluginReconciler;
|
|||
import run.halo.app.core.extension.reconciler.PostReconciler;
|
||||
import run.halo.app.core.extension.reconciler.RoleBindingReconciler;
|
||||
import run.halo.app.core.extension.reconciler.RoleReconciler;
|
||||
import run.halo.app.core.extension.reconciler.SinglePageReconciler;
|
||||
import run.halo.app.core.extension.reconciler.SystemSettingReconciler;
|
||||
import run.halo.app.core.extension.reconciler.TagReconciler;
|
||||
import run.halo.app.core.extension.reconciler.ThemeReconciler;
|
||||
|
@ -50,6 +52,7 @@ import run.halo.app.infra.ExternalUrlSupplier;
|
|||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
||||
import run.halo.app.theme.router.TemplateRouteManager;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ExtensionConfiguration {
|
||||
|
@ -181,6 +184,17 @@ public class ExtensionConfiguration {
|
|||
.extension(new Attachment())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Controller singlePageController(ExtensionClient client, ContentService contentService,
|
||||
ApplicationContext applicationContext, TemplateRouteManager templateRouteManager) {
|
||||
return new ControllerBuilder("single-page-controller", client)
|
||||
.reconciler(new SinglePageReconciler(client, contentService,
|
||||
applicationContext, templateRouteManager)
|
||||
)
|
||||
.extension(new SinglePage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
|
||||
|
||||
/**
|
||||
* An aggregate object of {@link SinglePage} and {@link Contributor} single page list.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
public class ListedSinglePage {
|
||||
|
||||
@Schema(required = true)
|
||||
private SinglePage page;
|
||||
|
||||
@Schema(required = true)
|
||||
private List<Contributor> contributors;
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.extension.router.ListRequest;
|
||||
import run.halo.app.extension.router.IListRequest;
|
||||
|
||||
/**
|
||||
* A query object for {@link Post} list.
|
||||
|
@ -12,14 +13,29 @@ import run.halo.app.extension.router.ListRequest;
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PostQuery extends ListRequest {
|
||||
public class PostQuery extends IListRequest.QueryListRequest {
|
||||
|
||||
private Set<String> contributors;
|
||||
public PostQuery(MultiValueMap<String, String> queryParams) {
|
||||
super(queryParams);
|
||||
}
|
||||
|
||||
private Set<String> categories;
|
||||
@Nullable
|
||||
public Set<String> getContributors() {
|
||||
return listToSet(queryParams.get("contributor"));
|
||||
}
|
||||
|
||||
private Set<String> tags;
|
||||
// TODO add more query fields
|
||||
@Nullable
|
||||
public Set<String> getCategories() {
|
||||
return listToSet(queryParams.get("category"));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Set<String> getTags() {
|
||||
return listToSet(queryParams.get("tag"));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Set<String> listToSet(List<String> param) {
|
||||
return param == null ? null : Set.copyOf(param);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.extension.router.IListRequest;
|
||||
|
||||
/**
|
||||
* Query parameter for {@link SinglePage} list.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class SinglePageQuery extends IListRequest.QueryListRequest {
|
||||
|
||||
public SinglePageQuery(MultiValueMap<String, String> queryParams) {
|
||||
super(queryParams);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Set<String> getContributors() {
|
||||
List<String> contributorList = queryParams.get("contributor");
|
||||
return contributorList == null ? null : Set.copyOf(contributorList);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
|
||||
/**
|
||||
* A request parameter for {@link SinglePage}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public record SinglePageRequest(@Schema(required = true) SinglePage page,
|
||||
@Schema(required = true) Content content) {
|
||||
|
||||
public ContentRequest contentRequest() {
|
||||
Snapshot.SubjectRef subjectRef =
|
||||
Snapshot.SubjectRef.of(SinglePage.KIND, page.getMetadata().getName());
|
||||
return new ContentRequest(subjectRef, page.getSpec().getHeadSnapshot(), content.raw,
|
||||
content.content, content.rawType);
|
||||
}
|
||||
|
||||
public record Content(String raw, String content, String rawType) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.extension.ListResult;
|
||||
|
||||
/**
|
||||
* Single page service.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface SinglePageService {
|
||||
|
||||
Mono<ListResult<ListedSinglePage>> list(SinglePageQuery listRequest);
|
||||
|
||||
Mono<SinglePage> draft(SinglePageRequest pageRequest);
|
||||
|
||||
Mono<SinglePage> update(SinglePageRequest pageRequest);
|
||||
|
||||
Mono<SinglePage> publish(String name);
|
||||
}
|
|
@ -0,0 +1,200 @@
|
|||
package run.halo.app.content.impl;
|
||||
|
||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.ContentService;
|
||||
import run.halo.app.content.Contributor;
|
||||
import run.halo.app.content.ListedSinglePage;
|
||||
import run.halo.app.content.SinglePageQuery;
|
||||
import run.halo.app.content.SinglePageRequest;
|
||||
import run.halo.app.content.SinglePageService;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.Condition;
|
||||
import run.halo.app.infra.ConditionStatus;
|
||||
|
||||
/**
|
||||
* Single page service implementation.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Service
|
||||
public class SinglePageServiceImpl implements SinglePageService {
|
||||
private static final Comparator<SinglePage> DEFAULT_PAGE_COMPARATOR =
|
||||
Comparator.comparing(page -> page.getMetadata().getCreationTimestamp());
|
||||
|
||||
private final ContentService contentService;
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public SinglePageServiceImpl(ContentService contentService, ReactiveExtensionClient client) {
|
||||
this.contentService = contentService;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<ListedSinglePage>> list(SinglePageQuery query) {
|
||||
return client.list(SinglePage.class, pageListPredicate(query),
|
||||
DEFAULT_PAGE_COMPARATOR.reversed(), query.getPage(), query.getSize())
|
||||
.flatMap(listResult -> Flux.fromStream(
|
||||
listResult.get().map(this::getListedSinglePage)
|
||||
)
|
||||
.flatMap(Function.identity())
|
||||
.collectList()
|
||||
.map(listedSinglePages -> new ListResult<>(listResult.getPage(),
|
||||
listResult.getSize(),
|
||||
listResult.getTotal(), listedSinglePages)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SinglePage> draft(SinglePageRequest pageRequest) {
|
||||
return contentService.draftContent(pageRequest.contentRequest())
|
||||
.flatMap(contentWrapper -> getContextUsername()
|
||||
.flatMap(username -> {
|
||||
SinglePage page = pageRequest.page();
|
||||
page.getSpec().setBaseSnapshot(contentWrapper.snapshotName());
|
||||
page.getSpec().setHeadSnapshot(contentWrapper.snapshotName());
|
||||
page.getSpec().setOwner(username);
|
||||
appendPublishedCondition(page, Post.PostPhase.DRAFT);
|
||||
return client.create(page)
|
||||
.then(Mono.defer(() ->
|
||||
client.fetch(SinglePage.class,
|
||||
pageRequest.page().getMetadata().getName())));
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SinglePage> update(SinglePageRequest pageRequest) {
|
||||
SinglePage page = pageRequest.page();
|
||||
return contentService.updateContent(pageRequest.contentRequest())
|
||||
.flatMap(contentWrapper -> {
|
||||
page.getSpec().setHeadSnapshot(contentWrapper.snapshotName());
|
||||
return client.update(page);
|
||||
})
|
||||
.then(Mono.defer(() -> client.fetch(SinglePage.class, page.getMetadata().getName())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SinglePage> publish(String name) {
|
||||
return client.fetch(SinglePage.class, name)
|
||||
.flatMap(page -> {
|
||||
SinglePage.SinglePageSpec spec = page.getSpec();
|
||||
if (Objects.equals(true, spec.getPublished())) {
|
||||
// has been published before
|
||||
spec.setVersion(spec.getVersion() + 1);
|
||||
} else {
|
||||
spec.setPublished(true);
|
||||
}
|
||||
|
||||
if (spec.getPublishTime() == null) {
|
||||
spec.setPublishTime(Instant.now());
|
||||
}
|
||||
|
||||
Snapshot.SubjectRef subjectRef =
|
||||
Snapshot.SubjectRef.of(SinglePage.KIND, page.getMetadata().getName());
|
||||
return contentService.publish(spec.getHeadSnapshot(), subjectRef)
|
||||
.flatMap(contentWrapper -> {
|
||||
// update release snapshot name and condition
|
||||
appendPublishedCondition(page, Post.PostPhase.PUBLISHED);
|
||||
page.getSpec().setReleaseSnapshot(contentWrapper.snapshotName());
|
||||
return client.update(page);
|
||||
})
|
||||
.then(Mono.defer(() -> client.fetch(SinglePage.class, name)));
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<String> getContextUsername() {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Principal::getName);
|
||||
}
|
||||
|
||||
Predicate<SinglePage> pageListPredicate(SinglePageQuery query) {
|
||||
Predicate<SinglePage> paramPredicate = singlePage -> contains(query.getContributors(),
|
||||
singlePage.getStatusOrDefault().getContributors());
|
||||
Predicate<SinglePage> predicate = labelAndFieldSelectorToPredicate(query.getLabelSelector(),
|
||||
query.getFieldSelector());
|
||||
return predicate.and(paramPredicate);
|
||||
}
|
||||
|
||||
private Mono<ListedSinglePage> getListedSinglePage(SinglePage singlePage) {
|
||||
Assert.notNull(singlePage, "The singlePage must not be null.");
|
||||
ListedSinglePage listedSinglePage = new ListedSinglePage();
|
||||
listedSinglePage.setPage(singlePage);
|
||||
return Mono.just(listedSinglePage)
|
||||
.flatMap(page -> listContributors(singlePage.getStatusOrDefault().getContributors())
|
||||
.map(contributors -> {
|
||||
page.setContributors(contributors);
|
||||
return page;
|
||||
}));
|
||||
}
|
||||
|
||||
private Mono<List<Contributor>> listContributors(List<String> usernames) {
|
||||
if (usernames == null) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return Flux.fromIterable(usernames)
|
||||
.map(username -> client.fetch(User.class, username)
|
||||
.map(user -> {
|
||||
Contributor contributor = new Contributor();
|
||||
contributor.setName(username);
|
||||
contributor.setDisplayName(user.getSpec().getDisplayName());
|
||||
contributor.setAvatar(user.getSpec().getAvatar());
|
||||
return contributor;
|
||||
})
|
||||
)
|
||||
.flatMap(Function.identity())
|
||||
.collectList();
|
||||
}
|
||||
|
||||
boolean contains(Collection<String> left, List<String> right) {
|
||||
// parameter is null, it means that ignore this condition
|
||||
if (left == null) {
|
||||
return true;
|
||||
}
|
||||
// else, it means that right is empty
|
||||
if (left.isEmpty()) {
|
||||
return right.isEmpty();
|
||||
}
|
||||
if (right == null) {
|
||||
return false;
|
||||
}
|
||||
return right.stream().anyMatch(left::contains);
|
||||
}
|
||||
|
||||
void appendPublishedCondition(SinglePage page, Post.PostPhase phase) {
|
||||
Assert.notNull(page, "The singlePage must not be null.");
|
||||
SinglePage.SinglePageStatus status = page.getStatusOrDefault();
|
||||
status.setPhase(phase.name());
|
||||
List<Condition> conditions = status.getConditionsOrDefault();
|
||||
Condition condition = new Condition();
|
||||
conditions.add(condition);
|
||||
|
||||
condition.setType(phase.name());
|
||||
condition.setReason(phase.name());
|
||||
condition.setMessage("");
|
||||
condition.setStatus(ConditionStatus.TRUE);
|
||||
condition.setLastTransitionTime(Instant.now());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package run.halo.app.core.extension;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
/**
|
||||
* <p>Single page extension.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@ToString(callSuper = true)
|
||||
@GVK(group = "content.halo.run", version = "v1alpha1", kind = SinglePage.KIND,
|
||||
plural = "singlepages", singular = "singlepage")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SinglePage extends AbstractExtension {
|
||||
public static final String KIND = "SinglePage";
|
||||
public static final String DELETED_LABEL = "content.halo.run/deleted";
|
||||
public static final String OWNER_LABEL = "content.halo.run/owner";
|
||||
public static final String VISIBLE_LABEL = "content.halo.run/visible";
|
||||
public static final String PHASE_LABEL = "content.halo.run/phase";
|
||||
|
||||
@Schema(required = true)
|
||||
private SinglePageSpec spec;
|
||||
|
||||
@Schema
|
||||
private SinglePageStatus status;
|
||||
|
||||
@JsonIgnore
|
||||
public SinglePageStatus getStatusOrDefault() {
|
||||
if (this.status == null) {
|
||||
this.status = new SinglePageStatus();
|
||||
}
|
||||
return this.status;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SinglePageSpec {
|
||||
@Schema(required = true, minLength = 1)
|
||||
private String title;
|
||||
|
||||
@Schema(required = true, minLength = 1)
|
||||
private String slug;
|
||||
|
||||
/**
|
||||
* 引用到的已发布的内容,用于主题端显示.
|
||||
*/
|
||||
private String releaseSnapshot;
|
||||
|
||||
private String headSnapshot;
|
||||
|
||||
private String baseSnapshot;
|
||||
|
||||
private String owner;
|
||||
|
||||
private String template;
|
||||
|
||||
private String cover;
|
||||
|
||||
@Schema(required = true, defaultValue = "false")
|
||||
private Boolean deleted;
|
||||
|
||||
@Schema(required = true, defaultValue = "false")
|
||||
private Boolean published;
|
||||
|
||||
private Instant publishTime;
|
||||
|
||||
@Schema(required = true, defaultValue = "false")
|
||||
private Boolean pinned;
|
||||
|
||||
@Schema(required = true, defaultValue = "true")
|
||||
private Boolean allowComment;
|
||||
|
||||
@Schema(required = true, defaultValue = "PUBLIC")
|
||||
private Post.VisibleEnum visible;
|
||||
|
||||
@Schema(required = true, defaultValue = "1")
|
||||
private Integer version;
|
||||
|
||||
@Schema(required = true, defaultValue = "0")
|
||||
private Integer priority;
|
||||
|
||||
@Schema(required = true)
|
||||
private Post.Excerpt excerpt;
|
||||
|
||||
private List<Map<String, String>> htmlMetas;
|
||||
}
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public static class SinglePageStatus extends Post.PostStatus {
|
||||
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuil
|
|||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import org.springdoc.core.fn.builders.schema.Builder;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.boot.convert.ApplicationConversionService;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
|
@ -117,23 +116,7 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
}
|
||||
|
||||
Mono<ServerResponse> listPost(ServerRequest request) {
|
||||
var conversionService = ApplicationConversionService.getSharedInstance();
|
||||
var page =
|
||||
request.queryParam("page")
|
||||
.map(pageString -> conversionService.convert(pageString, Integer.class))
|
||||
.orElse(0);
|
||||
|
||||
var size = request.queryParam("size")
|
||||
.map(sizeString -> conversionService.convert(sizeString, Integer.class))
|
||||
.orElse(0);
|
||||
|
||||
var labelSelectors = request.queryParams().get("labelSelector");
|
||||
var fieldSelectors = request.queryParams().get("fieldSelector");
|
||||
PostQuery postQuery = new PostQuery();
|
||||
postQuery.setPage(page);
|
||||
postQuery.setSize(size);
|
||||
postQuery.setLabelSelector(labelSelectors);
|
||||
postQuery.setFieldSelector(fieldSelectors);
|
||||
PostQuery postQuery = new PostQuery(request.queryParams());
|
||||
return postService.listPost(postQuery)
|
||||
.flatMap(listedPosts -> ServerResponse.ok().bodyValue(listedPosts));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
package run.halo.app.core.extension.endpoint;
|
||||
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import org.springdoc.core.fn.builders.schema.Builder;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.ListedSinglePage;
|
||||
import run.halo.app.content.SinglePageQuery;
|
||||
import run.halo.app.content.SinglePageRequest;
|
||||
import run.halo.app.content.SinglePageService;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.router.QueryParamBuildUtil;
|
||||
|
||||
/**
|
||||
* Endpoint for managing {@link SinglePage}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class SinglePageEndpoint implements CustomEndpoint {
|
||||
|
||||
private final SinglePageService singlePageService;
|
||||
|
||||
public SinglePageEndpoint(SinglePageService singlePageService) {
|
||||
this.singlePageService = singlePageService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
final var tag = "api.halo.run/v1alpha1/SinglePage";
|
||||
return SpringdocRouteBuilder.route()
|
||||
.GET("singlepages", this::listSinglePage, builder -> {
|
||||
builder.operationId("ListSinglePages")
|
||||
.description("List single pages.")
|
||||
.tag(tag)
|
||||
.response(responseBuilder()
|
||||
.implementation(ListResult.generateGenericClass(ListedSinglePage.class))
|
||||
);
|
||||
QueryParamBuildUtil.buildParametersFromType(builder, SinglePageQuery.class);
|
||||
}
|
||||
)
|
||||
.POST("singlepages", this::draftSinglePage,
|
||||
builder -> builder.operationId("DraftSinglePage")
|
||||
.description("Draft a single page.")
|
||||
.tag(tag)
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(SinglePageRequest.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(SinglePage.class))
|
||||
)
|
||||
.PUT("singlepages/{name}", this::updateSinglePage,
|
||||
builder -> builder.operationId("UpdateDraftSinglePage")
|
||||
.description("Update a single page.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(SinglePageRequest.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(SinglePage.class))
|
||||
)
|
||||
.PUT("singlepages/{name}/publish", this::publishSinglePage,
|
||||
builder -> builder.operationId("PublishSinglePage")
|
||||
.description("Publish a single page.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.response(responseBuilder()
|
||||
.implementation(SinglePage.class))
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
Mono<ServerResponse> draftSinglePage(ServerRequest request) {
|
||||
return request.bodyToMono(SinglePageRequest.class)
|
||||
.flatMap(singlePageService::draft)
|
||||
.flatMap(singlePage -> ServerResponse.ok().bodyValue(singlePage));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> updateSinglePage(ServerRequest request) {
|
||||
return request.bodyToMono(SinglePageRequest.class)
|
||||
.flatMap(singlePageService::update)
|
||||
.flatMap(page -> ServerResponse.ok().bodyValue(page));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> publishSinglePage(ServerRequest request) {
|
||||
String name = request.pathVariable("name");
|
||||
return singlePageService.publish(name)
|
||||
.flatMap(singlePage -> ServerResponse.ok().bodyValue(singlePage));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> listSinglePage(ServerRequest request) {
|
||||
var listRequest = new SinglePageQuery(request.queryParams());
|
||||
return singlePageService.list(listRequest)
|
||||
.flatMap(listedPages -> ServerResponse.ok().bodyValue(listedPages));
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ import run.halo.app.extension.controller.Reconciler;
|
|||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Reconciler for {@link Category}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
@ -36,10 +38,8 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
|
||||
private void reconcilePermalink(Category category) {
|
||||
if (category.getStatusOrDefault().getPermalink() == null) {
|
||||
category.getStatusOrDefault()
|
||||
.setPermalink(categoryPermalinkPolicy.permalink(category));
|
||||
}
|
||||
category.getStatusOrDefault()
|
||||
.setPermalink(categoryPermalinkPolicy.permalink(category));
|
||||
|
||||
if (category.getMetadata().getDeletionTimestamp() != null) {
|
||||
categoryPermalinkPolicy.onPermalinkDelete(category);
|
||||
|
|
|
@ -61,10 +61,8 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
|
||||
private void permalinkReconcile(Post post) {
|
||||
if (post.getStatusOrDefault().getPermalink() == null) {
|
||||
post.getStatusOrDefault()
|
||||
.setPermalink(postPermalinkPolicy.permalink(post));
|
||||
}
|
||||
post.getStatusOrDefault()
|
||||
.setPermalink(postPermalinkPolicy.permalink(post));
|
||||
|
||||
if (Objects.equals(true, post.getSpec().getDeleted())
|
||||
|| post.getMetadata().getDeletionTimestamp() != null
|
||||
|
@ -143,7 +141,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
|
||||
// handle logic delete
|
||||
Map<String, String> labels = getLabelsOrDefault(post);
|
||||
if (Objects.equals(spec.getDeleted(), true)) {
|
||||
if (isDeleted(post)) {
|
||||
labels.put(Post.DELETED_LABEL, Boolean.TRUE.toString());
|
||||
// TODO do more about logic delete such as remove router
|
||||
} else {
|
||||
|
@ -176,4 +174,9 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
private boolean isPublished(Snapshot snapshot) {
|
||||
return snapshot.getSpec().getPublishTime() != null;
|
||||
}
|
||||
|
||||
private boolean isDeleted(Post post) {
|
||||
return Objects.equals(true, post.getSpec().getDeleted())
|
||||
|| post.getMetadata().getDeletionTimestamp() != null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.content.ContentService;
|
||||
import run.halo.app.content.permalinks.ExtensionLocator;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.Condition;
|
||||
import run.halo.app.infra.ConditionStatus;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.PermalinkIndexAddCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
|
||||
import run.halo.app.theme.router.TemplateRouteManager;
|
||||
|
||||
/**
|
||||
* <p>Reconciler for {@link SinglePage}.</p>
|
||||
*
|
||||
* <p>things to do:</p>
|
||||
* <ul>
|
||||
* 1. generate permalink
|
||||
* 2. generate excerpt if auto generate is enabled
|
||||
* </ul>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
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 ContentService contentService;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final TemplateRouteManager templateRouteManager;
|
||||
|
||||
public SinglePageReconciler(ExtensionClient client, ContentService contentService,
|
||||
ApplicationContext applicationContext, TemplateRouteManager templateRouteManager) {
|
||||
this.client = client;
|
||||
this.contentService = contentService;
|
||||
this.applicationContext = applicationContext;
|
||||
this.templateRouteManager = templateRouteManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(SinglePage.class, request.name())
|
||||
.ifPresent(singlePage -> {
|
||||
SinglePage oldPage = JsonUtils.deepCopy(singlePage);
|
||||
if (isDeleted(oldPage)) {
|
||||
cleanUpResourcesAndRemoveFinalizer(request.name());
|
||||
return;
|
||||
}
|
||||
addFinalizerIfNecessary(oldPage);
|
||||
|
||||
reconcileStatus(request.name());
|
||||
reconcileMetadata(request.name());
|
||||
});
|
||||
return new Result(false, null);
|
||||
}
|
||||
|
||||
private void addFinalizerIfNecessary(SinglePage oldSinglePage) {
|
||||
Set<String> finalizers = oldSinglePage.getMetadata().getFinalizers();
|
||||
if (finalizers != null && finalizers.contains(FINALIZER_NAME)) {
|
||||
return;
|
||||
}
|
||||
client.fetch(SinglePage.class, oldSinglePage.getMetadata().getName())
|
||||
.ifPresent(singlePage -> {
|
||||
Set<String> newFinalizers = singlePage.getMetadata().getFinalizers();
|
||||
if (newFinalizers == null) {
|
||||
newFinalizers = new HashSet<>();
|
||||
singlePage.getMetadata().setFinalizers(newFinalizers);
|
||||
}
|
||||
newFinalizers.add(FINALIZER_NAME);
|
||||
client.update(singlePage);
|
||||
});
|
||||
}
|
||||
|
||||
private void cleanUpResources(SinglePage singlePage) {
|
||||
// remove permalink from permalink indexer
|
||||
permalinkOnDelete(singlePage);
|
||||
}
|
||||
|
||||
private void cleanUpResourcesAndRemoveFinalizer(String pageName) {
|
||||
client.fetch(SinglePage.class, pageName).ifPresent(singlePage -> {
|
||||
cleanUpResources(singlePage);
|
||||
if (singlePage.getMetadata().getFinalizers() != null) {
|
||||
singlePage.getMetadata().getFinalizers().remove(FINALIZER_NAME);
|
||||
}
|
||||
client.update(singlePage);
|
||||
});
|
||||
}
|
||||
|
||||
private void reconcileMetadata(String name) {
|
||||
client.fetch(SinglePage.class, name).ifPresent(singlePage -> {
|
||||
final SinglePage oldPage = JsonUtils.deepCopy(singlePage);
|
||||
|
||||
SinglePage.SinglePageSpec spec = singlePage.getSpec();
|
||||
// handle logic delete
|
||||
Map<String, String> labels = getLabelsOrDefault(singlePage);
|
||||
if (isDeleted(singlePage)) {
|
||||
labels.put(SinglePage.DELETED_LABEL, Boolean.TRUE.toString());
|
||||
} else {
|
||||
labels.put(SinglePage.DELETED_LABEL, Boolean.FALSE.toString());
|
||||
}
|
||||
// synchronize some fields to labels to query
|
||||
labels.put(SinglePage.PHASE_LABEL, singlePage.getStatusOrDefault().getPhase());
|
||||
labels.put(SinglePage.VISIBLE_LABEL,
|
||||
Objects.requireNonNullElse(spec.getVisible(), Post.VisibleEnum.PUBLIC).name());
|
||||
labels.put(SinglePage.OWNER_LABEL, spec.getOwner());
|
||||
if (!oldPage.equals(singlePage)) {
|
||||
client.update(singlePage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void permalinkOnDelete(SinglePage singlePage) {
|
||||
singlePage.getStatusOrDefault()
|
||||
.setPermalink(PathUtils.combinePath(singlePage.getSpec().getSlug()));
|
||||
ExtensionLocator locator = new ExtensionLocator(GVK, singlePage.getMetadata().getName(),
|
||||
singlePage.getSpec().getSlug());
|
||||
applicationContext.publishEvent(new PermalinkIndexDeleteCommand(this, locator,
|
||||
singlePage.getStatusOrDefault().getPermalink()));
|
||||
templateRouteManager.changeTemplatePattern(DefaultTemplateEnum.SINGLE_PAGE.getValue());
|
||||
}
|
||||
|
||||
private void permalinkOnAdd(SinglePage singlePage) {
|
||||
ExtensionLocator locator = new ExtensionLocator(GVK, singlePage.getMetadata().getName(),
|
||||
singlePage.getSpec().getSlug());
|
||||
applicationContext.publishEvent(new PermalinkIndexAddCommand(this, locator,
|
||||
singlePage.getStatusOrDefault().getPermalink()));
|
||||
templateRouteManager.changeTemplatePattern(DefaultTemplateEnum.SINGLE_PAGE.getValue());
|
||||
}
|
||||
|
||||
private void reconcileStatus(String name) {
|
||||
client.fetch(SinglePage.class, name).ifPresent(singlePage -> {
|
||||
final SinglePage oldPage = JsonUtils.deepCopy(singlePage);
|
||||
permalinkOnDelete(oldPage);
|
||||
|
||||
singlePage.getStatusOrDefault()
|
||||
.setPermalink(PathUtils.combinePath(singlePage.getSpec().getSlug()));
|
||||
if (isPublished(singlePage)) {
|
||||
permalinkOnAdd(singlePage);
|
||||
}
|
||||
|
||||
SinglePage.SinglePageSpec spec = singlePage.getSpec();
|
||||
SinglePage.SinglePageStatus status = singlePage.getStatusOrDefault();
|
||||
if (status.getPhase() == null) {
|
||||
status.setPhase(Post.PostPhase.DRAFT.name());
|
||||
}
|
||||
|
||||
// handle excerpt
|
||||
Post.Excerpt excerpt = spec.getExcerpt();
|
||||
if (excerpt == null) {
|
||||
excerpt = new Post.Excerpt();
|
||||
excerpt.setAutoGenerate(true);
|
||||
spec.setExcerpt(excerpt);
|
||||
}
|
||||
|
||||
if (excerpt.getAutoGenerate()) {
|
||||
contentService.getContent(spec.getHeadSnapshot())
|
||||
.subscribe(content -> {
|
||||
String contentRevised = content.content();
|
||||
status.setExcerpt(getExcerpt(contentRevised));
|
||||
});
|
||||
} else {
|
||||
status.setExcerpt(excerpt.getRaw());
|
||||
}
|
||||
|
||||
// handle contributors
|
||||
String headSnapshot = singlePage.getSpec().getHeadSnapshot();
|
||||
contentService.listSnapshots(Snapshot.SubjectRef.of(SinglePage.KIND, name))
|
||||
.collectList()
|
||||
.subscribe(snapshots -> {
|
||||
List<String> contributors = snapshots.stream()
|
||||
.map(snapshot -> {
|
||||
Set<String> usernames = snapshot.getSpec().getContributors();
|
||||
return Objects.requireNonNullElseGet(usernames,
|
||||
() -> new HashSet<String>());
|
||||
})
|
||||
.flatMap(Set::stream)
|
||||
.distinct()
|
||||
.sorted()
|
||||
.toList();
|
||||
status.setContributors(contributors);
|
||||
|
||||
// update in progress status
|
||||
snapshots.stream()
|
||||
.filter(snapshot -> snapshot.getMetadata().getName().equals(headSnapshot))
|
||||
.findAny()
|
||||
.ifPresent(snapshot -> {
|
||||
status.setInProgress(!isPublished(snapshot));
|
||||
});
|
||||
});
|
||||
|
||||
// handle cancel publish,has released version and published is false and not handled
|
||||
if (StringUtils.isNotBlank(spec.getReleaseSnapshot())
|
||||
&& Objects.equals(false, spec.getPublished())
|
||||
&& !StringUtils.equals(status.getPhase(), Post.PostPhase.DRAFT.name())) {
|
||||
Condition condition = new Condition();
|
||||
condition.setType("CancelledPublish");
|
||||
condition.setStatus(ConditionStatus.TRUE);
|
||||
condition.setReason(condition.getType());
|
||||
condition.setMessage(StringUtils.EMPTY);
|
||||
condition.setLastTransitionTime(Instant.now());
|
||||
status.getConditionsOrDefault().add(condition);
|
||||
status.setPhase(Post.PostPhase.DRAFT.name());
|
||||
}
|
||||
|
||||
if (!oldPage.equals(singlePage)) {
|
||||
client.update(singlePage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, String> getLabelsOrDefault(SinglePage singlePage) {
|
||||
Assert.notNull(singlePage, "The singlePage must not be null.");
|
||||
Map<String, String> labels = singlePage.getMetadata().getLabels();
|
||||
if (labels == null) {
|
||||
labels = new LinkedHashMap<>();
|
||||
singlePage.getMetadata().setLabels(labels);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
private String getExcerpt(String htmlContent) {
|
||||
String shortHtmlContent = StringUtils.substring(htmlContent, 0, 500);
|
||||
String text = Jsoup.parse(shortHtmlContent).text();
|
||||
// TODO The default capture 150 words as excerpt
|
||||
return StringUtils.substring(text, 0, 150);
|
||||
}
|
||||
|
||||
private boolean isPublished(Snapshot snapshot) {
|
||||
return snapshot.getSpec().getPublishTime() != null;
|
||||
}
|
||||
|
||||
private boolean isPublished(SinglePage singlePage) {
|
||||
return Objects.equals(true, singlePage.getSpec().getPublished());
|
||||
}
|
||||
|
||||
private boolean isDeleted(SinglePage singlePage) {
|
||||
return Objects.equals(true, singlePage.getSpec().getDeleted())
|
||||
|| singlePage.getMetadata().getDeletionTimestamp() != null;
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ import run.halo.app.extension.controller.Reconciler;
|
|||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Reconciler for {@link Tag}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
@ -35,10 +37,8 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
|
||||
private void reconcilePermalink(Tag tag) {
|
||||
if (tag.getStatusOrDefault().getPermalink() == null) {
|
||||
tag.getStatusOrDefault()
|
||||
.setPermalink(tagPermalinkPolicy.permalink(tag));
|
||||
}
|
||||
tag.getStatusOrDefault()
|
||||
.setPermalink(tagPermalinkPolicy.permalink(tag));
|
||||
|
||||
if (tag.getMetadata().getDeletionTimestamp() != null) {
|
||||
tagPermalinkPolicy.onPermalinkDelete(tag);
|
||||
|
|
|
@ -15,6 +15,7 @@ import run.halo.app.core.extension.ReverseProxy;
|
|||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.RoleBinding;
|
||||
import run.halo.app.core.extension.Setting;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
import run.halo.app.core.extension.Tag;
|
||||
import run.halo.app.core.extension.Theme;
|
||||
|
@ -55,6 +56,7 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
|
|||
schemeManager.register(Snapshot.class);
|
||||
schemeManager.register(Comment.class);
|
||||
schemeManager.register(Reply.class);
|
||||
schemeManager.register(SinglePage.class);
|
||||
// storage.halo.run
|
||||
schemeManager.register(Group.class);
|
||||
schemeManager.register(Policy.class);
|
||||
|
|
|
@ -17,7 +17,9 @@ public enum DefaultTemplateEnum {
|
|||
|
||||
TAG("tag"),
|
||||
|
||||
TAGS("tags");
|
||||
TAGS("tags"),
|
||||
|
||||
SINGLE_PAGE("page");
|
||||
|
||||
private final String value;
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||
|
||||
/**
|
||||
* A finder for {@link SinglePage}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface SinglePageFinder {
|
||||
|
||||
SinglePageVo getByName(String pageName);
|
||||
|
||||
ContentVo content(String pageName);
|
||||
|
||||
ListResult<SinglePageVo> list(@Nullable Integer page, @Nullable Integer size);
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import run.halo.app.content.ContentService;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.ContributorFinder;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.SinglePageFinder;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.Contributor;
|
||||
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link SinglePage}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Finder("singlePageFinder")
|
||||
public class SinglePageFinderImpl implements SinglePageFinder {
|
||||
|
||||
public static final Predicate<SinglePage> FIXED_PREDICATE = page ->
|
||||
Objects.equals(false, page.getSpec().getDeleted())
|
||||
&& Objects.equals(true, page.getSpec().getPublished())
|
||||
&& Post.VisibleEnum.PUBLIC.equals(page.getSpec().getVisible());
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final ContentService contentService;
|
||||
|
||||
private final ContributorFinder contributorFinder;
|
||||
|
||||
public SinglePageFinderImpl(ReactiveExtensionClient client, ContentService contentService,
|
||||
ContributorFinder contributorFinder) {
|
||||
this.client = client;
|
||||
this.contentService = contentService;
|
||||
this.contributorFinder = contributorFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SinglePageVo getByName(String pageName) {
|
||||
SinglePage page = client.fetch(SinglePage.class, pageName)
|
||||
.block();
|
||||
if (page == null) {
|
||||
return null;
|
||||
}
|
||||
List<Contributor> contributors =
|
||||
contributorFinder.getContributors(page.getStatus().getContributors());
|
||||
SinglePageVo pageVo = SinglePageVo.from(page);
|
||||
pageVo.setContributors(contributors);
|
||||
return pageVo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentVo content(String pageName) {
|
||||
return client.fetch(SinglePage.class, pageName)
|
||||
.map(page -> page.getSpec().getReleaseSnapshot())
|
||||
.flatMap(contentService::getContent)
|
||||
.map(wrapper -> ContentVo.builder().content(wrapper.content())
|
||||
.raw(wrapper.raw()).build())
|
||||
.block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListResult<SinglePageVo> list(Integer page, Integer size) {
|
||||
ListResult<SinglePage> list = client.list(SinglePage.class, FIXED_PREDICATE,
|
||||
defaultComparator(), pageNullSafe(page), sizeNullSafe(size))
|
||||
.block();
|
||||
if (list == null) {
|
||||
return new ListResult<>(0, 0, 0, List.of());
|
||||
}
|
||||
List<SinglePageVo> postVos = list.get()
|
||||
.map(sp -> {
|
||||
List<Contributor> contributors =
|
||||
contributorFinder.getContributors(sp.getStatus().getContributors());
|
||||
SinglePageVo pageVo = SinglePageVo.from(sp);
|
||||
pageVo.setContributors(contributors);
|
||||
return pageVo;
|
||||
})
|
||||
.toList();
|
||||
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), postVos);
|
||||
}
|
||||
|
||||
static Comparator<SinglePage> defaultComparator() {
|
||||
Function<SinglePage, Boolean> pinned =
|
||||
page -> Objects.requireNonNullElse(page.getSpec().getPinned(), false);
|
||||
Function<SinglePage, Integer> priority =
|
||||
page -> Objects.requireNonNullElse(page.getSpec().getPriority(), 0);
|
||||
Function<SinglePage, Instant> creationTimestamp =
|
||||
page -> page.getMetadata().getCreationTimestamp();
|
||||
Function<SinglePage, String> name = page -> page.getMetadata().getName();
|
||||
return Comparator.comparing(pinned)
|
||||
.thenComparing(priority)
|
||||
.thenComparing(creationTimestamp)
|
||||
.thenComparing(name)
|
||||
.reversed();
|
||||
}
|
||||
|
||||
int pageNullSafe(Integer page) {
|
||||
return ObjectUtils.defaultIfNull(page, 1);
|
||||
}
|
||||
|
||||
int sizeNullSafe(Integer size) {
|
||||
return ObjectUtils.defaultIfNull(size, 10);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import run.halo.app.core.extension.Post;
|
||||
|
||||
/**
|
||||
* a base entity for post.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@ToString
|
||||
@SuperBuilder
|
||||
public abstract class BasePostVo {
|
||||
String name;
|
||||
|
||||
String title;
|
||||
|
||||
String slug;
|
||||
|
||||
String owner;
|
||||
|
||||
String template;
|
||||
|
||||
String cover;
|
||||
|
||||
Boolean published;
|
||||
|
||||
Instant publishTime;
|
||||
|
||||
Boolean pinned;
|
||||
|
||||
Boolean allowComment;
|
||||
|
||||
Post.VisibleEnum visible;
|
||||
|
||||
Integer version;
|
||||
|
||||
Integer priority;
|
||||
|
||||
String excerpt;
|
||||
|
||||
List<Map<String, String>> htmlMetas;
|
||||
|
||||
String permalink;
|
||||
|
||||
List<Contributor> contributors;
|
||||
|
||||
Map<String, String> annotations;
|
||||
|
||||
static <T> List<T> nullSafe(List<T> t) {
|
||||
return Objects.requireNonNullElse(t, List.of());
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.core.extension.Post;
|
||||
|
||||
|
@ -16,49 +14,14 @@ import run.halo.app.core.extension.Post;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class PostVo {
|
||||
|
||||
String name;
|
||||
|
||||
String title;
|
||||
|
||||
String slug;
|
||||
|
||||
String owner;
|
||||
|
||||
String template;
|
||||
|
||||
String cover;
|
||||
|
||||
Boolean published;
|
||||
|
||||
Instant publishTime;
|
||||
|
||||
Boolean pinned;
|
||||
|
||||
Boolean allowComment;
|
||||
|
||||
Post.VisibleEnum visible;
|
||||
|
||||
Integer version;
|
||||
|
||||
Integer priority;
|
||||
|
||||
String excerpt;
|
||||
@SuperBuilder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PostVo extends BasePostVo {
|
||||
|
||||
List<CategoryVo> categories;
|
||||
|
||||
List<TagVo> tags;
|
||||
|
||||
List<Map<String, String>> htmlMetas;
|
||||
|
||||
String permalink;
|
||||
|
||||
List<Contributor> contributors;
|
||||
|
||||
Map<String, String> annotations;
|
||||
|
||||
/**
|
||||
* Convert {@link Post} to {@link PostVo}.
|
||||
*
|
||||
|
@ -92,8 +55,4 @@ public class PostVo {
|
|||
.contributors(List.of())
|
||||
.build();
|
||||
}
|
||||
|
||||
static <T> List<T> nullSafe(List<T> t) {
|
||||
return Objects.requireNonNullElse(t, List.of());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
|
||||
/**
|
||||
* A value object for {@link SinglePage}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@ToString(callSuper = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SinglePageVo extends BasePostVo {
|
||||
|
||||
/**
|
||||
* Convert {@link SinglePage} to {@link SinglePageVo}.
|
||||
*
|
||||
* @param singlePage single page extension
|
||||
* @return special page value object
|
||||
*/
|
||||
public static SinglePageVo from(SinglePage singlePage) {
|
||||
Assert.notNull(singlePage, "The singlePage must not be null.");
|
||||
SinglePage.SinglePageSpec spec = singlePage.getSpec();
|
||||
SinglePage.SinglePageStatus pageStatus = singlePage.getStatus();
|
||||
return SinglePageVo.builder()
|
||||
.name(singlePage.getMetadata().getName())
|
||||
.annotations(singlePage.getMetadata().getAnnotations())
|
||||
.title(spec.getTitle())
|
||||
.cover(spec.getCover())
|
||||
.allowComment(spec.getAllowComment())
|
||||
.owner(spec.getOwner())
|
||||
.pinned(spec.getPinned())
|
||||
.slug(spec.getSlug())
|
||||
.htmlMetas(nullSafe(spec.getHtmlMetas()))
|
||||
.published(spec.getPublished())
|
||||
.publishTime(spec.getPublishTime())
|
||||
.priority(spec.getPriority())
|
||||
.version(spec.getVersion())
|
||||
.visible(spec.getVisible())
|
||||
.template(spec.getTemplate())
|
||||
.permalink(pageStatus.getPermalink())
|
||||
.excerpt(pageStatus.getExcerpt())
|
||||
.contributors(List.of())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -51,7 +51,7 @@ public class PermalinkPatternProvider {
|
|||
public String getPattern(DefaultTemplateEnum defaultTemplateEnum) {
|
||||
SystemSetting.ThemeRouteRules permalinkRules = getPermalinkRules();
|
||||
return switch (defaultTemplateEnum) {
|
||||
case INDEX -> null;
|
||||
case INDEX, SINGLE_PAGE -> null;
|
||||
case POST -> permalinkRules.getPost();
|
||||
case ARCHIVES -> permalinkRules.getArchives();
|
||||
case CATEGORY, CATEGORIES -> permalinkRules.getCategories();
|
||||
|
|
|
@ -16,6 +16,7 @@ import run.halo.app.theme.router.strategy.CategoriesRouteStrategy;
|
|||
import run.halo.app.theme.router.strategy.CategoryRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.IndexRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.PostRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.SinglePageRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.TagRouteStrategy;
|
||||
import run.halo.app.theme.router.strategy.TagsRouteStrategy;
|
||||
|
||||
|
@ -113,6 +114,7 @@ public class TemplateRouteManager implements ApplicationListener<ApplicationRead
|
|||
case TAG -> new TagRouteStrategy(permalinkIndexer);
|
||||
case CATEGORIES -> new CategoriesRouteStrategy();
|
||||
case CATEGORY -> new CategoryRouteStrategy(permalinkIndexer);
|
||||
case SINGLE_PAGE -> new SinglePageRouteStrategy(permalinkIndexer);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
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.ServerResponse;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.PermalinkIndexer;
|
||||
import run.halo.app.theme.router.TemplateRouterStrategy;
|
||||
|
||||
/**
|
||||
* The {@link SinglePageRouteStrategy} for generate {@link RouterFunction} specific to the template
|
||||
* <code>page.html</code>.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class SinglePageRouteStrategy implements TemplateRouterStrategy {
|
||||
|
||||
private final PermalinkIndexer permalinkIndexer;
|
||||
|
||||
public SinglePageRouteStrategy(PermalinkIndexer permalinkIndexer) {
|
||||
this.permalinkIndexer = permalinkIndexer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> getRouteFunction(String template, String pattern) {
|
||||
GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class);
|
||||
|
||||
RequestPredicate requestPredicate = request -> false;
|
||||
|
||||
List<String> permalinks =
|
||||
Objects.requireNonNullElse(permalinkIndexer.getPermalinks(gvk), List.of());
|
||||
for (String permalink : permalinks) {
|
||||
requestPredicate = requestPredicate.or(RequestPredicates.GET(permalink));
|
||||
}
|
||||
|
||||
return RouterFunctions
|
||||
.route(requestPredicate.and(accept(MediaType.TEXT_HTML)), request -> {
|
||||
String slug = StringUtils.removeStart(request.path(), "/");
|
||||
String name = permalinkIndexer.getNameBySlug(gvk, slug);
|
||||
if (name == null) {
|
||||
return ServerResponse.notFound().build();
|
||||
}
|
||||
return ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.SINGLE_PAGE.getValue(), Map.of("name", name));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -5,12 +5,13 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
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.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import run.halo.app.content.ContentService;
|
||||
import run.halo.app.content.PostQuery;
|
||||
import run.halo.app.content.TestPost;
|
||||
|
@ -40,8 +41,9 @@ class PostServiceImplTest {
|
|||
|
||||
@Test
|
||||
void listPredicate() {
|
||||
PostQuery postQuery = new PostQuery();
|
||||
postQuery.setCategories(Set.of("category1", "category2"));
|
||||
MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
|
||||
multiValueMap.put("category", List.of("category1", "category2"));
|
||||
PostQuery postQuery = new PostQuery(multiValueMap);
|
||||
|
||||
Post post = TestPost.postV1();
|
||||
post.getSpec().setTags(null);
|
||||
|
@ -55,21 +57,23 @@ class PostServiceImplTest {
|
|||
assertThat(test).isTrue();
|
||||
|
||||
// Do not include tags
|
||||
postQuery.setTags(Set.of("tag2"));
|
||||
multiValueMap.put("tag", List.of("tag2"));
|
||||
post.getSpec().setTags(List.of("tag1"));
|
||||
post.getSpec().setCategories(null);
|
||||
test = postService.postListPredicate(postQuery).test(post);
|
||||
assertThat(test).isFalse();
|
||||
|
||||
postQuery.setTags(Set.of());
|
||||
multiValueMap.put("tag", List.of());
|
||||
multiValueMap.remove("category");
|
||||
postQuery = new PostQuery(multiValueMap);
|
||||
post.getSpec().setTags(List.of());
|
||||
test = postService.postListPredicate(postQuery).test(post);
|
||||
assertThat(test).isTrue();
|
||||
|
||||
postQuery.setLabelSelector(List.of("hello"));
|
||||
multiValueMap.put("labelSelector", List.of("hello"));
|
||||
test = postService.postListPredicate(postQuery).test(post);
|
||||
assertThat(test).isFalse();
|
||||
|
||||
postQuery.setLabelSelector(List.of("hello"));
|
||||
post.getMetadata().setLabels(Map.of("hello", "world"));
|
||||
test = postService.postListPredicate(postQuery).test(post);
|
||||
assertThat(test).isTrue();
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
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;
|
||||
import static run.halo.app.content.TestPost.snapshotV1;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.ContentService;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.TestPost;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.PermalinkIndexAddCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
|
||||
import run.halo.app.theme.router.PermalinkIndexUpdateCommand;
|
||||
import run.halo.app.theme.router.TemplateRouteManager;
|
||||
|
||||
/**
|
||||
* Tests for {@link SinglePageReconciler}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SinglePageReconcilerTest {
|
||||
@Mock
|
||||
private ExtensionClient client;
|
||||
@Mock
|
||||
private ContentService contentService;
|
||||
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Mock
|
||||
private TemplateRouteManager templateRouteManager;
|
||||
|
||||
private SinglePageReconciler singlePageReconciler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
singlePageReconciler = new SinglePageReconciler(client, contentService, applicationContext,
|
||||
templateRouteManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcile() {
|
||||
String name = "page-A";
|
||||
SinglePage page = pageV1();
|
||||
page.getSpec().setHeadSnapshot("page-A-head-snapshot");
|
||||
when(client.fetch(eq(SinglePage.class), eq(name)))
|
||||
.thenReturn(Optional.of(page));
|
||||
when(contentService.getContent(eq(page.getSpec().getHeadSnapshot())))
|
||||
.thenReturn(Mono.just(
|
||||
new ContentWrapper(page.getSpec().getHeadSnapshot(), "hello world",
|
||||
"<p>hello world</p>", "markdown")));
|
||||
|
||||
Snapshot snapshotV1 = snapshotV1();
|
||||
Snapshot snapshotV2 = TestPost.snapshotV2();
|
||||
snapshotV1.getSpec().setContributors(Set.of("guqing"));
|
||||
snapshotV2.getSpec().setContributors(Set.of("guqing", "zhangsan"));
|
||||
when(contentService.listSnapshots(any()))
|
||||
.thenReturn(Flux.just(snapshotV1, snapshotV2));
|
||||
|
||||
ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class);
|
||||
singlePageReconciler.reconcile(new Reconciler.Request(name));
|
||||
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
|
||||
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));
|
||||
verify(templateRouteManager, times(1))
|
||||
.changeTemplatePattern(eq(DefaultTemplateEnum.SINGLE_PAGE.getValue()));
|
||||
}
|
||||
|
||||
public static SinglePage pageV1() {
|
||||
SinglePage page = new SinglePage();
|
||||
page.setKind(Post.KIND);
|
||||
|
||||
page.setApiVersion("content.halo.run/v1alpha1");
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("page-A");
|
||||
page.setMetadata(metadata);
|
||||
|
||||
SinglePage.SinglePageSpec spec = new SinglePage.SinglePageSpec();
|
||||
page.setSpec(spec);
|
||||
|
||||
spec.setTitle("page-A");
|
||||
spec.setSlug("page-slug");
|
||||
spec.setVersion(1);
|
||||
spec.setBaseSnapshot(snapshotV1().getMetadata().getName());
|
||||
spec.setHeadSnapshot("base-snapshot");
|
||||
spec.setReleaseSnapshot(null);
|
||||
|
||||
return page;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package run.halo.app.theme.router.strategy;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.router.PermalinkIndexer;
|
||||
|
||||
/**
|
||||
* Tests for {@link SinglePageRouteStrategy}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SinglePageRouteStrategyTest {
|
||||
|
||||
@Mock
|
||||
private PermalinkIndexer permalinkIndexer;
|
||||
|
||||
@Mock
|
||||
private ViewResolver viewResolver;
|
||||
|
||||
private SinglePageRouteStrategy strategy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
strategy = new SinglePageRouteStrategy(permalinkIndexer);
|
||||
when(permalinkIndexer.getPermalinks(any()))
|
||||
.thenReturn(List.of("/fake-slug"));
|
||||
when(permalinkIndexer.getNameBySlug(any(), eq("fake-slug")))
|
||||
.thenReturn("fake-name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRouteFunction() {
|
||||
RouterFunction<ServerResponse> routeFunction =
|
||||
strategy.getRouteFunction(DefaultTemplateEnum.SINGLE_PAGE.getValue(),
|
||||
null);
|
||||
|
||||
WebTestClient client = WebTestClient.bindToRouterFunction(routeFunction)
|
||||
.handlerStrategies(HandlerStrategies.builder()
|
||||
.viewResolver(viewResolver)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.SINGLE_PAGE.getValue()), any()))
|
||||
.thenReturn(Mono.just(new EmptyView()));
|
||||
|
||||
client.get()
|
||||
.uri("/fake-slug")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
client.get()
|
||||
.uri("/nothing")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue