From 3adf5b8a950d0f1d10d13a26153b7e7828e670d5 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Thu, 10 Nov 2022 22:44:10 +0800 Subject: [PATCH] refactor: change from manual publishing to automatic publishing (#2659) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /milestone 2.0 /area core /kind api-change #### What this PR does / why we need it: - 通过修改 `spec.publish=true` 且 `spec.releasedSnapshot = spec.headSnapshot` 来实现通过 reconciler 异步发布 - Snapshot 中的 subjectRef 类型改为了 Ref 类型,因为之前的 Snapshot.SubjectRef 只包含了 kind 和 name 可能会冲突 #### Which issue(s) this PR fixes: Fixes #2650 #### Special notes for your reviewer: how to test it? 1. 通过 `POST /apis/console.halo.run/v1alpha1/posts` 创建文章并将 `spec.publish=true`,创建后查询可以看到 `spec.baseSnapshot`、`spec.headSnapshot`、`spec.releasedSnapshot` 三个值都相等,且 `status.phase=PUBLISHED`(此过程相当于创建即发布没有保存过程) 2. 先通过 `POST /apis/console.halo.run/v1alpha1/posts` 创建一篇草稿(`spec.publish=false`),在获取它并设置 `spec.publish=true` ,更新后期望文章为发布状态 `spec.headSnapshot`, `spec.releasedSnapshot` 都不等于空且等于 `spec.baseSnapshot`),且 `spec.version=1`(此过程相当于先保存后发布且之前从未发布过) 3. 在步骤2的基础上修改`spec.releasedSnapshot=spec.headSnapshot`并更新,期望 `spec.version=2`且`spec.releasedSnapshot` 对应的 Snapshot 数据具有 publishTime(此过程相当于发布后编辑内容在发布的场景) 4. 自定义页面亦如是 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 重构文章发布以解决创建与发布 API 几乎同时调用时无法成功发布文章的问题 ``` --- .../app/config/ExtensionConfiguration.java | 12 +- .../run/halo/app/content/ContentRequest.java | 10 +- .../run/halo/app/content/ContentService.java | 11 +- .../run/halo/app/content/ContentWrapper.java | 15 +- .../java/run/halo/app/content/ListedPost.java | 3 + .../halo/app/content/ListedSinglePage.java | 3 + .../run/halo/app/content/PostRequest.java | 5 +- .../halo/app/content/SinglePageRequest.java | 5 +- .../app/content/impl/ContentServiceImpl.java | 34 ++-- .../app/content/impl/PostServiceImpl.java | 100 +++++++----- .../content/impl/SinglePageServiceImpl.java | 89 +++++++---- .../run/halo/app/core/extension/Post.java | 18 ++- .../halo/app/core/extension/SinglePage.java | 14 +- .../run/halo/app/core/extension/Snapshot.java | 59 +++---- .../extension/endpoint/ContentEndpoint.java | 64 ++++---- .../core/extension/endpoint/PostEndpoint.java | 17 +- .../endpoint/SinglePageEndpoint.java | 19 ++- .../extension/reconciler/PostReconciler.java | 138 ++++++++++++----- .../reconciler/SinglePageReconciler.java | 146 ++++++++++++------ .../run/halo/app/extension/ExtensionUtil.java | 20 +++ .../run/halo/app/infra/utils/PathUtils.java | 6 +- .../theme/finders/impl/PostFinderImpl.java | 10 +- .../finders/impl/SinglePageFinderImpl.java | 12 +- .../run/halo/app/theme/finders/vo/PostVo.java | 2 + .../app/theme/finders/vo/SinglePageVo.java | 2 + .../system-configurable-configmap.yaml | 4 + .../resources/extensions/system-setting.yaml | 7 +- .../halo/app/content/ContentRequestTest.java | 9 +- .../halo/app/content/ContentServiceTest.java | 144 ++++++++++------- .../app/content/impl/PostServiceImplTest.java | 136 ++++++++++++++++ .../extension/endpoint/PostEndpointTest.java | 19 ++- .../reconciler/PostReconcilerTest.java | 54 ++++++- .../reconciler/SinglePageReconcilerTest.java | 24 +-- .../finders/impl/PostFinderImplTest.java | 25 ++- 34 files changed, 861 insertions(+), 375 deletions(-) diff --git a/src/main/java/run/halo/app/config/ExtensionConfiguration.java b/src/main/java/run/halo/app/config/ExtensionConfiguration.java index e0c3bdd49..dc78c8547 100644 --- a/src/main/java/run/halo/app/config/ExtensionConfiguration.java +++ b/src/main/java/run/halo/app/config/ExtensionConfiguration.java @@ -8,6 +8,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; import run.halo.app.content.ContentService; +import run.halo.app.content.PostService; +import run.halo.app.content.SinglePageService; import run.halo.app.content.permalinks.CategoryPermalinkPolicy; import run.halo.app.content.permalinks.PostPermalinkPolicy; import run.halo.app.content.permalinks.TagPermalinkPolicy; @@ -148,9 +150,11 @@ public class ExtensionConfiguration { @Bean Controller postController(ExtensionClient client, ContentService contentService, - PostPermalinkPolicy postPermalinkPolicy, CounterService counterService) { + PostPermalinkPolicy postPermalinkPolicy, CounterService counterService, + PostService postService) { return new ControllerBuilder("post-controller", client) - .reconciler(new PostReconciler(client, contentService, postPermalinkPolicy, + .reconciler(new PostReconciler(client, contentService, postService, + postPermalinkPolicy, counterService)) .extension(new Post()) .build(); @@ -198,10 +202,10 @@ public class ExtensionConfiguration { @Bean Controller singlePageController(ExtensionClient client, ContentService contentService, ApplicationContext applicationContext, CounterService counterService, - ExternalUrlSupplier externalUrlSupplier) { + SinglePageService singlePageService, ExternalUrlSupplier externalUrlSupplier) { return new ControllerBuilder("single-page-controller", client) .reconciler(new SinglePageReconciler(client, contentService, - applicationContext, counterService, externalUrlSupplier) + applicationContext, singlePageService, counterService, externalUrlSupplier) ) .extension(new SinglePage()) .build(); diff --git a/src/main/java/run/halo/app/content/ContentRequest.java b/src/main/java/run/halo/app/content/ContentRequest.java index 4dfee93e9..328341f56 100644 --- a/src/main/java/run/halo/app/content/ContentRequest.java +++ b/src/main/java/run/halo/app/content/ContentRequest.java @@ -1,14 +1,16 @@ package run.halo.app.content; import io.swagger.v3.oas.annotations.media.Schema; +import org.apache.commons.lang3.StringUtils; import run.halo.app.core.extension.Snapshot; import run.halo.app.extension.Metadata; +import run.halo.app.extension.Ref; /** * @author guqing * @since 2.0.0 */ -public record ContentRequest(@Schema(required = true) Snapshot.SubjectRef subjectRef, +public record ContentRequest(@Schema(required = true) Ref subjectRef, String headSnapshotName, @Schema(required = true) String raw, @Schema(required = true) String content, @@ -25,8 +27,8 @@ public record ContentRequest(@Schema(required = true) Snapshot.SubjectRef subjec snapShotSpec.setSubjectRef(subjectRef); snapShotSpec.setVersion(1); snapShotSpec.setRawType(rawType); - snapShotSpec.setRawPatch(raw); - snapShotSpec.setContentPatch(content); + snapShotSpec.setRawPatch(StringUtils.defaultString(raw())); + snapShotSpec.setContentPatch(StringUtils.defaultString(content())); String displayVersion = Snapshot.displayVersionFrom(snapShotSpec.getVersion()); snapShotSpec.setDisplayVersion(displayVersion); @@ -34,7 +36,7 @@ public record ContentRequest(@Schema(required = true) Snapshot.SubjectRef subjec return snapshot; } - private String defaultName(Snapshot.SubjectRef subjectRef) { + private String defaultName(Ref subjectRef) { // example: Post-apost-v1-snapshot return String.join("-", subjectRef.getKind(), subjectRef.getName(), "v1", "snapshot"); diff --git a/src/main/java/run/halo/app/content/ContentService.java b/src/main/java/run/halo/app/content/ContentService.java index 93187544c..64d3fecb4 100644 --- a/src/main/java/run/halo/app/content/ContentService.java +++ b/src/main/java/run/halo/app/content/ContentService.java @@ -3,6 +3,7 @@ package run.halo.app.content; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.Snapshot; +import run.halo.app.extension.Ref; /** * Content service for {@link Snapshot}. @@ -18,13 +19,13 @@ public interface ContentService { Mono updateContent(ContentRequest content); - Mono publish(String headSnapshotName, Snapshot.SubjectRef subjectRef); + Mono publish(String headSnapshotName, Ref subjectRef); - Mono getBaseSnapshot(Snapshot.SubjectRef subjectRef); + Mono getBaseSnapshot(Ref subjectRef); - Mono latestSnapshotVersion(Snapshot.SubjectRef subjectRef); + Mono latestSnapshotVersion(Ref subjectRef); - Mono latestPublishedSnapshot(Snapshot.SubjectRef subjectRef); + Mono latestPublishedSnapshot(Ref subjectRef); - Flux listSnapshots(Snapshot.SubjectRef subjectRef); + Flux listSnapshots(Ref subjectRef); } diff --git a/src/main/java/run/halo/app/content/ContentWrapper.java b/src/main/java/run/halo/app/content/ContentWrapper.java index d264af887..6e7823022 100644 --- a/src/main/java/run/halo/app/content/ContentWrapper.java +++ b/src/main/java/run/halo/app/content/ContentWrapper.java @@ -1,13 +1,18 @@ package run.halo.app.content; -import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; /** * @author guqing * @since 2.0.0 */ -public record ContentWrapper(@Schema(required = true) String snapshotName, - @Schema(required = true) String raw, - @Schema(required = true) String content, - @Schema(required = true) String rawType) { +@Data +@Builder +public class ContentWrapper { + private String snapshotName; + private Integer version; + private String raw; + private String content; + private String rawType; } diff --git a/src/main/java/run/halo/app/content/ListedPost.java b/src/main/java/run/halo/app/content/ListedPost.java index aedd10c4d..618d2f4bc 100644 --- a/src/main/java/run/halo/app/content/ListedPost.java +++ b/src/main/java/run/halo/app/content/ListedPost.java @@ -29,6 +29,9 @@ public class ListedPost { @Schema(required = true) private List contributors; + @Schema(required = true) + private Contributor owner; + @Schema(required = true) private Stats stats; } diff --git a/src/main/java/run/halo/app/content/ListedSinglePage.java b/src/main/java/run/halo/app/content/ListedSinglePage.java index 4b3e78c12..eb1ffca8a 100644 --- a/src/main/java/run/halo/app/content/ListedSinglePage.java +++ b/src/main/java/run/halo/app/content/ListedSinglePage.java @@ -21,6 +21,9 @@ public class ListedSinglePage { @Schema(required = true) private List contributors; + @Schema(required = true) + private Contributor owner; + @Schema(required = true) private Stats stats; } diff --git a/src/main/java/run/halo/app/content/PostRequest.java b/src/main/java/run/halo/app/content/PostRequest.java index 60fc63291..01a7dcef8 100644 --- a/src/main/java/run/halo/app/content/PostRequest.java +++ b/src/main/java/run/halo/app/content/PostRequest.java @@ -2,7 +2,7 @@ package run.halo.app.content; import io.swagger.v3.oas.annotations.media.Schema; import run.halo.app.core.extension.Post; -import run.halo.app.core.extension.Snapshot; +import run.halo.app.extension.Ref; /** * @author guqing @@ -12,8 +12,7 @@ public record PostRequest(@Schema(required = true) Post post, @Schema(required = true) Content content) { public ContentRequest contentRequest() { - Snapshot.SubjectRef subjectRef = - Snapshot.SubjectRef.of(Post.KIND, post.getMetadata().getName()); + Ref subjectRef = Ref.of(post); return new ContentRequest(subjectRef, post.getSpec().getHeadSnapshot(), content.raw, content.content, content.rawType); } diff --git a/src/main/java/run/halo/app/content/SinglePageRequest.java b/src/main/java/run/halo/app/content/SinglePageRequest.java index ea1314d82..93e195a9c 100644 --- a/src/main/java/run/halo/app/content/SinglePageRequest.java +++ b/src/main/java/run/halo/app/content/SinglePageRequest.java @@ -2,7 +2,7 @@ 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; +import run.halo.app.extension.Ref; /** * A request parameter for {@link SinglePage}. @@ -14,8 +14,7 @@ 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()); + Ref subjectRef = Ref.of(page); return new ContentRequest(subjectRef, page.getSpec().getHeadSnapshot(), content.raw, content.content, content.rawType); } diff --git a/src/main/java/run/halo/app/content/impl/ContentServiceImpl.java b/src/main/java/run/halo/app/content/impl/ContentServiceImpl.java index d59129ac6..d1268c72d 100644 --- a/src/main/java/run/halo/app/content/impl/ContentServiceImpl.java +++ b/src/main/java/run/halo/app/content/impl/ContentServiceImpl.java @@ -3,6 +3,7 @@ package run.halo.app.content.impl; import java.security.Principal; import java.time.Instant; import java.util.Comparator; +import java.util.Map; import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.springframework.security.core.context.ReactiveSecurityContextHolder; @@ -15,7 +16,9 @@ import run.halo.app.content.ContentRequest; import run.halo.app.content.ContentService; import run.halo.app.content.ContentWrapper; import run.halo.app.core.extension.Snapshot; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.extension.Ref; /** * A default implementation of {@link ContentService}. @@ -65,15 +68,11 @@ public class ContentServiceImpl implements ContentService { Snapshot headSnapShot = tuple.getT2(); return handleSnapshot(headSnapShot, contentRequest, username); }) - .flatMap(snapshot -> restoredContent(snapshot) - .map(content -> new ContentWrapper(snapshot.getMetadata().getName(), - content.raw(), content.content(), content.rawType()) - ) - ); + .flatMap(this::restoredContent); } @Override - public Mono publish(String headSnapshotName, Snapshot.SubjectRef subjectRef) { + public Mono publish(String headSnapshotName, Ref subjectRef) { Assert.notNull(headSnapshotName, "The headSnapshotName must not be null"); return client.fetch(Snapshot.class, headSnapshotName) .flatMap(snapshot -> { @@ -82,7 +81,8 @@ public class ContentServiceImpl implements ContentService { return restoredContent(snapshot.getMetadata().getName(), subjectRef); } - + Map labels = ExtensionUtil.nullSafeLabels(snapshot); + Snapshot.putPublishedLabel(labels); Snapshot.SnapShotSpec snapshotSpec = snapshot.getSpec(); snapshotSpec.setPublishTime(Instant.now()); snapshotSpec.setDisplayVersion( @@ -95,7 +95,7 @@ public class ContentServiceImpl implements ContentService { } private Mono restoredContent(String snapshotName, - Snapshot.SubjectRef subjectRef) { + Ref subjectRef) { return getBaseSnapshot(subjectRef) .flatMap(baseSnapshot -> client.fetch(Snapshot.class, snapshotName) .map(snapshot -> snapshot.applyPatch(baseSnapshot))); @@ -107,7 +107,7 @@ public class ContentServiceImpl implements ContentService { } @Override - public Mono getBaseSnapshot(Snapshot.SubjectRef subjectRef) { + public Mono getBaseSnapshot(Ref subjectRef) { return listSnapshots(subjectRef) .filter(snapshot -> snapshot.getSpec().getVersion() == 1) .next(); @@ -115,7 +115,7 @@ public class ContentServiceImpl implements ContentService { private Mono handleSnapshot(Snapshot headSnapshot, ContentRequest contentRequest, String username) { - Snapshot.SubjectRef subjectRef = contentRequest.subjectRef(); + Ref subjectRef = contentRequest.subjectRef(); return getBaseSnapshot(subjectRef).flatMap(baseSnapshot -> { String baseSnapshotName = baseSnapshot.getMetadata().getName(); return latestPublishedSnapshot(subjectRef) @@ -142,6 +142,14 @@ public class ContentServiceImpl implements ContentService { latestReleasedSnapshot.getMetadata().getName(); if (headSnapshot.isPublished() || StringUtils.equals(headSnapshotName, releasedSnapshotName)) { + String latestSnapshotName = latestSnapshot.getMetadata().getName(); + if (!headSnapshotName.equals(latestSnapshotName) + && !latestSnapshot.isPublished()) { + // publish it then create new one + return publish(latestSnapshotName, subjectRef) + .then(createNewSnapshot(newSnapshot, baseSnapshotName, + contentRequest)); + } // create a new snapshot,done return createNewSnapshot(newSnapshot, baseSnapshotName, contentRequest); @@ -160,7 +168,7 @@ public class ContentServiceImpl implements ContentService { } @Override - public Mono latestSnapshotVersion(Snapshot.SubjectRef subjectRef) { + public Mono latestSnapshotVersion(Ref subjectRef) { Assert.notNull(subjectRef, "The subjectRef must not be null."); return listSnapshots(subjectRef) .sort(LATEST_SNAPSHOT_COMPARATOR) @@ -168,7 +176,7 @@ public class ContentServiceImpl implements ContentService { } @Override - public Mono latestPublishedSnapshot(Snapshot.SubjectRef subjectRef) { + public Mono latestPublishedSnapshot(Ref subjectRef) { Assert.notNull(subjectRef, "The subjectRef must not be null."); return listSnapshots(subjectRef) .filter(Snapshot::isPublished) @@ -177,7 +185,7 @@ public class ContentServiceImpl implements ContentService { } @Override - public Flux listSnapshots(Snapshot.SubjectRef subjectRef) { + public Flux listSnapshots(Ref subjectRef) { Assert.notNull(subjectRef, "The subjectRef must not be null."); return client.list(Snapshot.class, snapshot -> subjectRef.equals(snapshot.getSpec() .getSubjectRef()), null); diff --git a/src/main/java/run/halo/app/content/impl/PostServiceImpl.java b/src/main/java/run/halo/app/content/impl/PostServiceImpl.java index 16b07db47..81957acdd 100644 --- a/src/main/java/run/halo/app/content/impl/PostServiceImpl.java +++ b/src/main/java/run/halo/app/content/impl/PostServiceImpl.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.security.core.context.ReactiveSecurityContextHolder; @@ -36,8 +37,11 @@ import run.halo.app.core.extension.Tag; import run.halo.app.core.extension.User; import run.halo.app.extension.ListResult; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.extension.Ref; import run.halo.app.infra.Condition; import run.halo.app.infra.ConditionStatus; +import run.halo.app.infra.exception.NotFoundException; +import run.halo.app.infra.utils.JsonUtils; import run.halo.app.metrics.CounterService; import run.halo.app.metrics.MeterUtils; @@ -47,6 +51,7 @@ import run.halo.app.metrics.MeterUtils; * @author guqing * @since 2.0.0 */ +@Slf4j @Component public class PostServiceImpl implements PostService { private final ContentService contentService; @@ -160,7 +165,8 @@ public class PostServiceImpl implements PostService { }) .flatMap(lp -> setTags(post.getSpec().getTags(), lp)) .flatMap(lp -> setCategories(post.getSpec().getCategories(), lp)) - .flatMap(lp -> setContributors(post.getStatus().getContributors(), lp)); + .flatMap(lp -> setContributors(post.getStatus().getContributors(), lp)) + .flatMap(lp -> setOwner(post.getSpec().getOwner(), lp)); } private Mono setTags(List tagNames, ListedPost post) { @@ -187,6 +193,19 @@ public class PostServiceImpl implements PostService { .switchIfEmpty(Mono.defer(() -> Mono.just(post))); } + private Mono setOwner(String ownerName, ListedPost post) { + return client.fetch(User.class, ownerName) + .map(user -> { + Contributor contributor = new Contributor(); + contributor.setName(user.getMetadata().getName()); + contributor.setDisplayName(user.getSpec().getDisplayName()); + contributor.setAvatar(user.getSpec().getAvatar()); + return contributor; + }) + .doOnNext(post::setOwner) + .thenReturn(post); + } + private Flux listTags(List tagNames) { if (tagNames == null) { return Flux.empty(); @@ -224,8 +243,8 @@ public class PostServiceImpl implements PostService { .flatMap(contentWrapper -> getContextUsername() .flatMap(username -> { Post post = postRequest.post(); - post.getSpec().setBaseSnapshot(contentWrapper.snapshotName()); - post.getSpec().setHeadSnapshot(contentWrapper.snapshotName()); + post.getSpec().setBaseSnapshot(contentWrapper.getSnapshotName()); + post.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName()); post.getSpec().setOwner(username); appendPublishedCondition(post, Post.PostPhase.DRAFT); return client.create(post) @@ -239,7 +258,7 @@ public class PostServiceImpl implements PostService { Post post = postRequest.post(); return contentService.updateContent(postRequest.contentRequest()) .flatMap(contentWrapper -> { - post.getSpec().setHeadSnapshot(contentWrapper.snapshotName()); + post.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName()); return client.update(post); }) .retryWhen(Retry.backoff(5, Duration.ofMillis(100)) @@ -256,43 +275,45 @@ public class PostServiceImpl implements PostService { @Override public Mono publishPost(String postName) { return client.fetch(Post.class, postName) + .filter(post -> Objects.equals(true, post.getSpec().getPublish())) .flatMap(post -> { - Post.PostSpec spec = post.getSpec(); - // publish snapshot - return Mono.zip(Mono.just(post), - client.fetch(Snapshot.class, spec.getHeadSnapshot())); - }) - .flatMap(tuple -> { - Post post = tuple.getT1(); - Snapshot snapshot = tuple.getT2(); - - Post.PostSpec postSpec = post.getSpec(); - if (Objects.equals(true, postSpec.getPublished())) { - // has been published before - postSpec.setVersion(postSpec.getVersion() + 1); - } else { - postSpec.setPublished(true); + final Post oldPost = JsonUtils.deepCopy(post); + final Post.PostSpec postSpec = post.getSpec(); + // if it's published state but releaseSnapshot is null, it means that need to + // publish headSnapshot + // if releaseSnapshot is draft and publish state is true, it means that need to + // publish releaseSnapshot + if (StringUtils.isBlank(postSpec.getHeadSnapshot())) { + postSpec.setHeadSnapshot(postSpec.getBaseSnapshot()); } - if (postSpec.getPublishTime() == null) { - postSpec.setPublishTime(Instant.now()); + if (StringUtils.isBlank(postSpec.getReleaseSnapshot())) { + postSpec.setReleaseSnapshot(postSpec.getHeadSnapshot()); + postSpec.setVersion(0); } - - // update release snapshot name and condition - postSpec.setReleaseSnapshot(snapshot.getMetadata().getName()); - appendPublishedCondition(post, Post.PostPhase.PUBLISHED); - - Snapshot.SubjectRef subjectRef = - Snapshot.SubjectRef.of(Post.KIND, post.getMetadata().getName()); - return contentService.publish(snapshot.getMetadata().getName(), subjectRef) - .flatMap(contentWrapper -> { - post.getSpec().setReleaseSnapshot(contentWrapper.snapshotName()); - return client.update(post); + return client.fetch(Snapshot.class, postSpec.getReleaseSnapshot()) + .flatMap(releasedSnapshot -> { + Ref ref = Ref.of(post); + // not published state, need to publish + return contentService.publish(releasedSnapshot.getMetadata().getName(), + ref) + .flatMap(contentWrapper -> { + appendPublishedCondition(post, Post.PostPhase.PUBLISHED); + postSpec.setVersion(contentWrapper.getVersion()); + Post.changePublishedState(post, true); + if (postSpec.getPublishTime() == null) { + postSpec.setPublishTime(Instant.now()); + } + if (!oldPost.equals(post)) { + return client.update(post); + } + return Mono.just(post); + }); }) - .then(Mono.defer(() -> client.fetch(Post.class, postName))); - }) - .retryWhen(Retry.backoff(5, Duration.ofMillis(100)) - .filter(throwable -> throwable instanceof OptimisticLockingFailureException)); + .switchIfEmpty(Mono.defer(() -> Mono.error(new NotFoundException( + String.format("Snapshot [%s] not found", postSpec.getReleaseSnapshot())))) + ); + }); } void appendPublishedCondition(Post post, Post.PostPhase phase) { @@ -300,13 +321,16 @@ public class PostServiceImpl implements PostService { Post.PostStatus status = post.getStatusOrDefault(); status.setPhase(phase.name()); List conditions = status.getConditionsOrDefault(); - Condition condition = new Condition(); - conditions.add(condition); + conditions.add(createCondition(phase)); + } + Condition createCondition(Post.PostPhase phase) { + Condition condition = new Condition(); condition.setType(phase.name()); condition.setReason(phase.name()); condition.setMessage(""); condition.setStatus(ConditionStatus.TRUE); condition.setLastTransitionTime(Instant.now()); + return condition; } } diff --git a/src/main/java/run/halo/app/content/impl/SinglePageServiceImpl.java b/src/main/java/run/halo/app/content/impl/SinglePageServiceImpl.java index 199a61cff..ba6b24943 100644 --- a/src/main/java/run/halo/app/content/impl/SinglePageServiceImpl.java +++ b/src/main/java/run/halo/app/content/impl/SinglePageServiceImpl.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.security.core.context.ReactiveSecurityContextHolder; @@ -35,8 +36,11 @@ 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.extension.Ref; import run.halo.app.infra.Condition; import run.halo.app.infra.ConditionStatus; +import run.halo.app.infra.exception.NotFoundException; +import run.halo.app.infra.utils.JsonUtils; import run.halo.app.metrics.CounterService; import run.halo.app.metrics.MeterUtils; @@ -46,6 +50,7 @@ import run.halo.app.metrics.MeterUtils; * @author guqing * @since 2.0.0 */ +@Slf4j @Service public class SinglePageServiceImpl implements SinglePageService { private final ContentService contentService; @@ -85,8 +90,8 @@ public class SinglePageServiceImpl implements SinglePageService { .flatMap(contentWrapper -> getContextUsername() .flatMap(username -> { SinglePage page = pageRequest.page(); - page.getSpec().setBaseSnapshot(contentWrapper.snapshotName()); - page.getSpec().setHeadSnapshot(contentWrapper.snapshotName()); + page.getSpec().setBaseSnapshot(contentWrapper.getSnapshotName()); + page.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName()); page.getSpec().setOwner(username); appendPublishedCondition(page, Post.PostPhase.DRAFT); return client.create(page) @@ -101,7 +106,7 @@ public class SinglePageServiceImpl implements SinglePageService { SinglePage page = pageRequest.page(); return contentService.updateContent(pageRequest.contentRequest()) .flatMap(contentWrapper -> { - page.getSpec().setHeadSnapshot(contentWrapper.snapshotName()); + page.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName()); return client.update(page); }) .retryWhen(Retry.backoff(5, Duration.ofMillis(100)) @@ -112,32 +117,46 @@ public class SinglePageServiceImpl implements SinglePageService { @Override public Mono publish(String name) { return client.fetch(SinglePage.class, name) + .filter(page -> Objects.equals(true, page.getSpec().getPublish())) .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); + final SinglePage oldPage = JsonUtils.deepCopy(page); + final SinglePage.SinglePageSpec spec = page.getSpec(); + // if it's published state but releaseSnapshot is null, it means that need to + // publish headSnapshot + // if releaseSnapshot is draft and publish state is true, it means that need to + // publish releaseSnapshot + if (StringUtils.isBlank(spec.getHeadSnapshot())) { + spec.setHeadSnapshot(spec.getBaseSnapshot()); } - if (spec.getPublishTime() == null) { - spec.setPublishTime(Instant.now()); + if (StringUtils.isBlank(spec.getReleaseSnapshot())) { + spec.setReleaseSnapshot(spec.getHeadSnapshot()); + // first-time to publish reset version to 0 + spec.setVersion(0); } - - 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); + return client.fetch(Snapshot.class, spec.getReleaseSnapshot()) + .flatMap(releasedSnapshot -> { + Ref ref = Ref.of(page); + // not published state, need to publish + return contentService.publish(releasedSnapshot.getMetadata().getName(), + ref) + .flatMap(contentWrapper -> { + appendPublishedCondition(page, Post.PostPhase.PUBLISHED); + spec.setVersion(contentWrapper.getVersion()); + SinglePage.changePublishedState(page, true); + if (spec.getPublishTime() == null) { + spec.setPublishTime(Instant.now()); + } + if (!oldPage.equals(page)) { + return client.update(page); + } + return Mono.just(page); + }); }) - .then(Mono.defer(() -> client.fetch(SinglePage.class, name))); - }) - .retryWhen(Retry.backoff(5, Duration.ofMillis(100)) - .filter(throwable -> throwable instanceof OptimisticLockingFailureException)); + .switchIfEmpty(Mono.defer(() -> Mono.error(new NotFoundException( + String.format("Snapshot [%s] not found", spec.getReleaseSnapshot())))) + ); + }); } private Mono getContextUsername() { @@ -198,7 +217,8 @@ public class SinglePageServiceImpl implements SinglePageService { return listedSinglePage; }) .flatMap(lsp -> - setContributors(singlePage.getStatusOrDefault().getContributors(), lsp)); + setContributors(singlePage.getStatusOrDefault().getContributors(), lsp)) + .flatMap(lsp -> setOwner(singlePage.getSpec().getOwner(), lsp)); } private Mono setContributors(List usernames, @@ -210,6 +230,19 @@ public class SinglePageServiceImpl implements SinglePageService { .defaultIfEmpty(singlePage); } + private Mono setOwner(String ownerName, ListedSinglePage page) { + return client.fetch(User.class, ownerName) + .map(user -> { + Contributor contributor = new Contributor(); + contributor.setName(user.getMetadata().getName()); + contributor.setDisplayName(user.getSpec().getDisplayName()); + contributor.setAvatar(user.getSpec().getAvatar()); + return contributor; + }) + .doOnNext(page::setOwner) + .thenReturn(page); + } + Stats fetchStats(SinglePage singlePage) { Assert.notNull(singlePage, "The singlePage must not be null."); String name = singlePage.getMetadata().getName(); @@ -259,12 +292,16 @@ public class SinglePageServiceImpl implements SinglePageService { status.setPhase(phase.name()); List conditions = status.getConditionsOrDefault(); Condition condition = new Condition(); - conditions.add(condition); + conditions.add(createCondition(phase)); + } + Condition createCondition(Post.PostPhase phase) { + Condition condition = new Condition(); condition.setType(phase.name()); condition.setReason(phase.name()); condition.setMessage(""); condition.setStatus(ConditionStatus.TRUE); condition.setLastTransitionTime(Instant.now()); + return condition; } } diff --git a/src/main/java/run/halo/app/core/extension/Post.java b/src/main/java/run/halo/app/core/extension/Post.java index ebaed8760..810de45c7 100644 --- a/src/main/java/run/halo/app/core/extension/Post.java +++ b/src/main/java/run/halo/app/core/extension/Post.java @@ -11,6 +11,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import run.halo.app.extension.AbstractExtension; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.GVK; import run.halo.app.infra.Condition; @@ -31,9 +32,9 @@ public class Post extends AbstractExtension { public static final String CATEGORIES_ANNO = "content.halo.run/categories"; public static final String TAGS_ANNO = "content.halo.run/tags"; public static final String DELETED_LABEL = "content.halo.run/deleted"; + public static final String PUBLISHED_LABEL = "content.halo.run/published"; 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"; public static final String ARCHIVE_YEAR_LABEL = "content.halo.run/archive-year"; @@ -61,7 +62,8 @@ public class Post extends AbstractExtension { @JsonIgnore public boolean isPublished() { - return Objects.equals(true, spec.getPublished()); + Map labels = getMetadata().getLabels(); + return labels != null && labels.getOrDefault(PUBLISHED_LABEL, "false").equals("true"); } @Data @@ -91,7 +93,7 @@ public class Post extends AbstractExtension { private Boolean deleted; @Schema(required = true, defaultValue = "false") - private Boolean published; + private Boolean publish; private Instant publishTime; @@ -138,6 +140,8 @@ public class Post extends AbstractExtension { private List contributors; + private List releasedSnapshots; + @JsonIgnore public List getConditionsOrDefault() { if (this.conditions == null) { @@ -159,7 +163,8 @@ public class Post extends AbstractExtension { public enum PostPhase { DRAFT, PENDING_APPROVAL, - PUBLISHED; + PUBLISHED, + FAILED; /** * Convert string value to {@link PostPhase}. @@ -252,4 +257,9 @@ public class Post extends AbstractExtension { } } } + + public static void changePublishedState(Post post, boolean value) { + Map labels = ExtensionUtil.nullSafeLabels(post); + labels.put(PUBLISHED_LABEL, String.valueOf(value)); + } } diff --git a/src/main/java/run/halo/app/core/extension/SinglePage.java b/src/main/java/run/halo/app/core/extension/SinglePage.java index 944f20bb2..09b318c7c 100644 --- a/src/main/java/run/halo/app/core/extension/SinglePage.java +++ b/src/main/java/run/halo/app/core/extension/SinglePage.java @@ -5,11 +5,11 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.Instant; import java.util.List; import java.util.Map; -import java.util.Objects; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import run.halo.app.extension.AbstractExtension; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.GVK; /** @@ -26,9 +26,9 @@ import run.halo.app.extension.GVK; 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 PUBLISHED_LABEL = "content.halo.run/published"; 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; @@ -46,7 +46,8 @@ public class SinglePage extends AbstractExtension { @JsonIgnore public boolean isPublished() { - return Objects.equals(true, spec.getPublished()); + Map labels = getMetadata().getLabels(); + return labels != null && labels.getOrDefault(PUBLISHED_LABEL, "false").equals("true"); } @Data @@ -76,7 +77,7 @@ public class SinglePage extends AbstractExtension { private Boolean deleted; @Schema(required = true, defaultValue = "false") - private Boolean published; + private Boolean publish; private Instant publishTime; @@ -106,4 +107,9 @@ public class SinglePage extends AbstractExtension { public static class SinglePageStatus extends Post.PostStatus { } + + public static void changePublishedState(SinglePage page, boolean value) { + Map labels = ExtensionUtil.nullSafeLabels(page); + labels.put(PUBLISHED_LABEL, String.valueOf(value)); + } } diff --git a/src/main/java/run/halo/app/core/extension/Snapshot.java b/src/main/java/run/halo/app/core/extension/Snapshot.java index 073ffd31d..6dcf67be1 100644 --- a/src/main/java/run/halo/app/core/extension/Snapshot.java +++ b/src/main/java/run/halo/app/core/extension/Snapshot.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import java.time.Instant; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,6 +14,7 @@ import run.halo.app.content.ContentWrapper; import run.halo.app.content.PatchUtils; import run.halo.app.extension.AbstractExtension; import run.halo.app.extension.GVK; +import run.halo.app.extension.Ref; /** * @author guqing @@ -26,6 +28,7 @@ import run.halo.app.extension.GVK; @EqualsAndHashCode(callSuper = true) public class Snapshot extends AbstractExtension { public static final String KIND = "Snapshot"; + public static final String PUBLISHED_LABEL = "content.halo.run/published"; @Schema(required = true) private SnapShotSpec spec; @@ -34,7 +37,7 @@ public class Snapshot extends AbstractExtension { public static class SnapShotSpec { @Schema(required = true) - private SubjectRef subjectRef; + private Ref subjectRef; /** * such as: markdown | html | json | asciidoc | latex. @@ -67,23 +70,6 @@ public class Snapshot extends AbstractExtension { } } - @Data - @EqualsAndHashCode - public static class SubjectRef { - @Schema(required = true) - private String kind; - - @Schema(required = true) - private String name; - - public static SubjectRef of(String kind, String name) { - SubjectRef subjectRef = new SubjectRef(); - subjectRef.setKind(kind); - subjectRef.setName(name); - return subjectRef; - } - } - public static String displayVersionFrom(Integer version) { Assert.notNull(version, "The version must not be null"); return "v" + version; @@ -91,7 +77,8 @@ public class Snapshot extends AbstractExtension { @JsonIgnore public boolean isPublished() { - return this.spec.getPublishTime() != null; + Map labels = getMetadata().getLabels(); + return labels != null && labels.getOrDefault(PUBLISHED_LABEL, "false").equals("true"); } @JsonIgnore @@ -101,29 +88,33 @@ public class Snapshot extends AbstractExtension { contributors.add(name); } - @JsonIgnore - public void setSubjectRef(String kind, String name) { - Assert.notNull(kind, "The subject kind must not be null."); - Assert.notNull(name, "The subject name must not be null."); - if (spec.subjectRef == null) { - spec.subjectRef = new SubjectRef(); - } - spec.subjectRef.setKind(kind); - spec.subjectRef.setName(name); - } - @JsonIgnore public ContentWrapper applyPatch(Snapshot baseSnapshot) { Assert.notNull(baseSnapshot, "The baseSnapshot must not be null."); if (this.spec.version == 1) { - return new ContentWrapper(this.getMetadata().getName(), this.spec.rawPatch, - this.spec.contentPatch, this.spec.rawType); + return ContentWrapper.builder() + .snapshotName(this.getMetadata().getName()) + .version(this.spec.version) + .raw(this.spec.rawPatch) + .content(this.spec.contentPatch) + .rawType(this.spec.rawType) + .build(); } String patchedContent = PatchUtils.applyPatch(baseSnapshot.getSpec().getContentPatch(), this.spec.contentPatch); String patchedRaw = PatchUtils.applyPatch(baseSnapshot.getSpec().getRawPatch(), this.spec.rawPatch); - return new ContentWrapper(this.getMetadata().getName(), patchedRaw, - patchedContent, this.spec.rawType); + return ContentWrapper.builder() + .snapshotName(this.getMetadata().getName()) + .version(this.spec.version) + .raw(patchedRaw) + .content(patchedContent) + .rawType(this.spec.rawType) + .build(); + } + + public static void putPublishedLabel(Map labels) { + Assert.notNull(labels, "The labels must not be null."); + labels.put(PUBLISHED_LABEL, "true"); } } diff --git a/src/main/java/run/halo/app/core/extension/endpoint/ContentEndpoint.java b/src/main/java/run/halo/app/core/extension/endpoint/ContentEndpoint.java index 484ac22f4..b18827cec 100644 --- a/src/main/java/run/halo/app/core/extension/endpoint/ContentEndpoint.java +++ b/src/main/java/run/halo/app/core/extension/endpoint/ContentEndpoint.java @@ -6,6 +6,8 @@ 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 io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; import org.springdoc.core.fn.builders.schema.Builder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springframework.http.MediaType; @@ -17,7 +19,6 @@ import reactor.core.publisher.Mono; import run.halo.app.content.ContentRequest; import run.halo.app.content.ContentService; import run.halo.app.content.ContentWrapper; -import run.halo.app.core.extension.Snapshot; /** * Endpoint for managing content. @@ -47,7 +48,7 @@ public class ContentEndpoint implements CustomEndpoint { .name("snapshotName") .in(ParameterIn.PATH)) .response(responseBuilder() - .implementation(ContentWrapper.class)) + .implementation(ContentResponse.class)) ) .POST("contents", this::draftSnapshotContent, builder -> builder.operationId("DraftSnapshotContent") @@ -61,7 +62,7 @@ public class ContentEndpoint implements CustomEndpoint { .implementation(ContentRequest.class)) )) .response(responseBuilder() - .implementation(ContentWrapper.class)) + .implementation(ContentResponse.class)) ) .PUT("contents/{snapshotName}", this::updateSnapshotContent, builder -> builder.operationId("UpdateSnapshotContent") @@ -79,25 +80,7 @@ public class ContentEndpoint implements CustomEndpoint { .implementation(ContentRequest.class)) )) .response(responseBuilder() - .implementation(ContentWrapper.class)) - ) - .PUT("contents/{snapshotName}/publish", this::publishSnapshotContent, - builder -> builder.operationId("PublishSnapshotContent") - .description("Publish a snapshot content.") - .tag(tag) - .parameter(parameterBuilder() - .required(true) - .name("snapshotName") - .in(ParameterIn.PATH)) - .requestBody(requestBodyBuilder() - .required(true) - .content(contentBuilder() - .mediaType(MediaType.APPLICATION_JSON_VALUE) - .schema(Builder.schemaBuilder() - .implementation(Snapshot.SubjectRef.class)) - )) - .response(responseBuilder() - .implementation(ContentWrapper.class)) + .implementation(ContentResponse.class)) ) .build(); } @@ -105,6 +88,7 @@ public class ContentEndpoint implements CustomEndpoint { private Mono obtainContent(ServerRequest request) { String snapshotName = request.pathVariable("snapshotName"); return contentService.getContent(snapshotName) + .map(ContentResponse::from) .flatMap(content -> ServerResponse.ok().bodyValue(content)); } @@ -117,19 +101,41 @@ public class ContentEndpoint implements CustomEndpoint { content.raw(), content.content(), content.rawType()); return contentService.updateContent(contentRequest); }) - .flatMap(content -> ServerResponse.ok().bodyValue(content)); - } - - private Mono publishSnapshotContent(ServerRequest request) { - String snapshotName = request.pathVariable("snapshotName"); - return request.bodyToMono(Snapshot.SubjectRef.class) - .flatMap(subjectRef -> contentService.publish(snapshotName, subjectRef)) + .map(ContentResponse::from) .flatMap(content -> ServerResponse.ok().bodyValue(content)); } private Mono draftSnapshotContent(ServerRequest request) { return request.bodyToMono(ContentRequest.class) .flatMap(contentService::draftContent) + .map(ContentResponse::from) .flatMap(content -> ServerResponse.ok().bodyValue(content)); } + + @Data + public static class ContentResponse { + @Schema(required = true, description = "The headSnapshotName if updated or new name if " + + "created.") + private String snapshotName; + @Schema(required = true) + private String raw; + + @Schema(required = true) + private String content; + + @Schema(required = true) + private String rawType; + + /** + * Converts content response from {@link ContentWrapper}. + */ + public static ContentResponse from(ContentWrapper wrapper) { + ContentResponse response = new ContentResponse(); + response.raw = wrapper.getRaw(); + response.setSnapshotName(wrapper.getSnapshotName()); + response.content = wrapper.getContent(); + response.rawType = wrapper.getRawType(); + return response; + } + } } diff --git a/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java b/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java index 965dd6085..e21316d58 100644 --- a/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java +++ b/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java @@ -6,6 +6,7 @@ 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 lombok.AllArgsConstructor; import org.springdoc.core.fn.builders.schema.Builder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springframework.http.MediaType; @@ -20,6 +21,7 @@ import run.halo.app.content.PostRequest; import run.halo.app.content.PostService; import run.halo.app.core.extension.Post; import run.halo.app.extension.ListResult; +import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.router.QueryParamBuildUtil; /** @@ -29,13 +31,11 @@ import run.halo.app.extension.router.QueryParamBuildUtil; * @since 2.0.0 */ @Component +@AllArgsConstructor public class PostEndpoint implements CustomEndpoint { private final PostService postService; - - public PostEndpoint(PostService postService) { - this.postService = postService; - } + private final ReactiveExtensionClient client; @Override public RouterFunction endpoint() { @@ -111,7 +111,14 @@ public class PostEndpoint implements CustomEndpoint { Mono publishPost(ServerRequest request) { String name = request.pathVariable("name"); - return postService.publishPost(name) + return client.fetch(Post.class, name) + .flatMap(post -> { + Post.PostSpec spec = post.getSpec(); + spec.setPublish(true); + spec.setReleaseSnapshot(spec.getHeadSnapshot()); + return client.update(post); + }) + .flatMap(post -> postService.publishPost(post.getMetadata().getName())) .flatMap(post -> ServerResponse.ok().bodyValue(post)); } diff --git a/src/main/java/run/halo/app/core/extension/endpoint/SinglePageEndpoint.java b/src/main/java/run/halo/app/core/extension/endpoint/SinglePageEndpoint.java index b4dccb94b..fa1e4c844 100644 --- a/src/main/java/run/halo/app/core/extension/endpoint/SinglePageEndpoint.java +++ b/src/main/java/run/halo/app/core/extension/endpoint/SinglePageEndpoint.java @@ -6,6 +6,7 @@ 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 lombok.AllArgsConstructor; import org.springdoc.core.fn.builders.schema.Builder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springframework.http.MediaType; @@ -20,6 +21,7 @@ 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.ReactiveExtensionClient; import run.halo.app.extension.router.QueryParamBuildUtil; /** @@ -29,13 +31,11 @@ import run.halo.app.extension.router.QueryParamBuildUtil; * @since 2.0.0 */ @Component +@AllArgsConstructor public class SinglePageEndpoint implements CustomEndpoint { private final SinglePageService singlePageService; - - public SinglePageEndpoint(SinglePageService singlePageService) { - this.singlePageService = singlePageService; - } + private final ReactiveExtensionClient client; @Override public RouterFunction endpoint() { @@ -111,8 +111,15 @@ public class SinglePageEndpoint implements CustomEndpoint { Mono publishSinglePage(ServerRequest request) { String name = request.pathVariable("name"); - return singlePageService.publish(name) - .flatMap(singlePage -> ServerResponse.ok().bodyValue(singlePage)); + return client.fetch(SinglePage.class, name) + .flatMap(singlePage -> { + SinglePage.SinglePageSpec spec = singlePage.getSpec(); + spec.setPublish(true); + spec.setReleaseSnapshot(spec.getHeadSnapshot()); + return client.update(singlePage); + }) + .flatMap(singlePage -> singlePageService.publish(singlePage.getMetadata().getName())) + .flatMap(page -> ServerResponse.ok().bodyValue(page)); } Mono listSinglePage(ServerRequest request) { diff --git a/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java index 69b6f7af6..ad11e9f20 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java @@ -1,8 +1,8 @@ package run.halo.app.core.extension.reconciler; import java.time.Instant; +import java.util.Comparator; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -11,12 +11,14 @@ import org.apache.commons.lang3.StringUtils; import org.jsoup.Jsoup; import org.springframework.util.Assert; import run.halo.app.content.ContentService; +import run.halo.app.content.PostService; import run.halo.app.content.permalinks.PostPermalinkPolicy; import run.halo.app.core.extension.Comment; import run.halo.app.core.extension.Post; import run.halo.app.core.extension.Snapshot; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionOperator; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.Ref; import run.halo.app.extension.controller.Reconciler; import run.halo.app.infra.Condition; @@ -42,13 +44,16 @@ public class PostReconciler implements Reconciler { private static final String FINALIZER_NAME = "post-protection"; private final ExtensionClient client; private final ContentService contentService; + private final PostService postService; private final PostPermalinkPolicy postPermalinkPolicy; private final CounterService counterService; public PostReconciler(ExtensionClient client, ContentService contentService, - PostPermalinkPolicy postPermalinkPolicy, CounterService counterService) { + PostService postService, PostPermalinkPolicy postPermalinkPolicy, + CounterService counterService) { this.client = client; this.contentService = contentService; + this.postService = postService; this.postPermalinkPolicy = postPermalinkPolicy; this.counterService = counterService; } @@ -63,26 +68,90 @@ public class PostReconciler implements Reconciler { } addFinalizerIfNecessary(post); + // reconcile spec first + reconcileSpec(request.name()); reconcileMetadata(request.name()); reconcileStatus(request.name()); }); return new Result(false, null); } + private void reconcileSpec(String name) { + // publish post if necessary + try { + postService.publishPost(name).block(); + } catch (Throwable e) { + publishFailed(name, e); + throw e; + } + + client.fetch(Post.class, name).ifPresent(post -> { + Post oldPost = JsonUtils.deepCopy(post); + if (post.isPublished() && Objects.equals(false, post.getSpec().getPublish())) { + Post.changePublishedState(post, false); + final Post.PostStatus status = post.getStatusOrDefault(); + Condition condition = new Condition(); + condition.setType("CancelledPublish"); + condition.setStatus(ConditionStatus.TRUE); + condition.setReason(condition.getType()); + condition.setMessage("CancelledPublish"); + condition.setLastTransitionTime(Instant.now()); + status.getConditionsOrDefault().add(condition); + status.setPhase(Post.PostPhase.DRAFT.name()); + } + if (!oldPost.equals(post)) { + client.update(post); + } + }); + } + + private void publishFailed(String name, Throwable error) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(error, "Error must not be null"); + client.fetch(Post.class, name).ifPresent(post -> { + final Post oldPost = JsonUtils.deepCopy(post); + + Post.PostStatus status = post.getStatusOrDefault(); + Post.PostPhase phase = Post.PostPhase.FAILED; + status.setPhase(phase.name()); + + final List conditions = status.getConditionsOrDefault(); + Condition condition = new Condition(); + condition.setType(phase.name()); + condition.setReason(phase.name()); + condition.setMessage(""); + condition.setStatus(ConditionStatus.TRUE); + condition.setLastTransitionTime(Instant.now()); + condition.setMessage(error.getMessage()); + condition.setStatus(ConditionStatus.FALSE); + + if (conditions.size() > 0) { + Condition lastCondition = conditions.get(conditions.size() - 1); + if (!StringUtils.equals(lastCondition.getType(), condition.getType()) + && !StringUtils.equals(lastCondition.getMessage(), condition.getMessage())) { + conditions.add(condition); + } + } + post.setStatus(status); + + if (!oldPost.equals(post)) { + client.update(post); + } + }); + } + private void reconcileMetadata(String name) { client.fetch(Post.class, name).ifPresent(post -> { final Post oldPost = JsonUtils.deepCopy(post); Post.PostSpec spec = post.getSpec(); // handle logic delete - Map labels = getLabelsOrDefault(post); + Map labels = ExtensionUtil.nullSafeLabels(post); if (Objects.equals(spec.getDeleted(), true)) { labels.put(Post.DELETED_LABEL, Boolean.TRUE.toString()); } else { labels.put(Post.DELETED_LABEL, Boolean.FALSE.toString()); } - // synchronize some fields to labels to query - labels.put(Post.PHASE_LABEL, post.getStatusOrDefault().getPhase()); labels.put(Post.VISIBLE_LABEL, Objects.requireNonNullElse(spec.getVisible(), Post.VisibleEnum.PUBLIC).name()); labels.put(Post.OWNER_LABEL, spec.getOwner()); @@ -91,7 +160,9 @@ public class PostReconciler implements Reconciler { labels.put(Post.ARCHIVE_YEAR_LABEL, HaloUtils.getYearText(publishTime)); labels.put(Post.ARCHIVE_MONTH_LABEL, HaloUtils.getMonthText(publishTime)); } - + if (!labels.containsKey(Post.PUBLISHED_LABEL)) { + labels.put(Post.PUBLISHED_LABEL, Boolean.FALSE.toString()); + } if (!oldPost.equals(post)) { client.update(post); } @@ -123,16 +194,17 @@ public class PostReconciler implements Reconciler { contentService.getContent(spec.getReleaseSnapshot()) .blockOptional() .ifPresent(content -> { - String contentRevised = content.content(); + String contentRevised = content.getContent(); status.setExcerpt(getExcerpt(contentRevised)); }); } else { status.setExcerpt(excerpt.getRaw()); } + Ref ref = Ref.of(post); // handle contributors String headSnapshot = post.getSpec().getHeadSnapshot(); - contentService.listSnapshots(Snapshot.SubjectRef.of(Post.KIND, name)) + contentService.listSnapshots(ref) .collectList() .blockOptional() .ifPresent(snapshots -> { @@ -154,24 +226,18 @@ public class PostReconciler implements Reconciler { snapshot -> snapshot.getMetadata().getName().equals(headSnapshot)) .findAny() .ifPresent(snapshot -> { - status.setInProgress(!isPublished(snapshot)); + status.setInProgress(!snapshot.isPublished()); }); + List releasedSnapshots = snapshots.stream() + .filter(Snapshot::isPublished) + .sorted(Comparator.comparing(snapshot -> snapshot.getSpec().getVersion())) + .map(snapshot -> snapshot.getMetadata().getName()) + .toList(); + status.setReleasedSnapshots(releasedSnapshots); }); - // 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()); - } + status.setConditions(limitConditionSize(status.getConditions())); if (!oldPost.equals(post)) { client.update(post); @@ -211,14 +277,12 @@ public class PostReconciler implements Reconciler { postPermalinkPolicy.onPermalinkDelete(post); // clean up snapshots - Snapshot.SubjectRef subjectRef = - Snapshot.SubjectRef.of(Post.KIND, post.getMetadata().getName()); + final Ref ref = Ref.of(post); client.list(Snapshot.class, - snapshot -> subjectRef.equals(snapshot.getSpec().getSubjectRef()), null) + snapshot -> ref.equals(snapshot.getSpec().getSubjectRef()), null) .forEach(client::delete); // clean up comments - Ref ref = Ref.of(post); client.list(Comment.class, comment -> comment.getSpec().getSubjectRef().equals(ref), null) .forEach(client::delete); @@ -228,16 +292,6 @@ public class PostReconciler implements Reconciler { .block(); } - private Map getLabelsOrDefault(Post post) { - Assert.notNull(post, "The post must not be null."); - Map labels = post.getMetadata().getLabels(); - if (labels == null) { - labels = new LinkedHashMap<>(); - post.getMetadata().setLabels(labels); - } - return labels; - } - private String getExcerpt(String htmlContent) { String shortHtmlContent = StringUtils.substring(htmlContent, 0, 500); String text = Jsoup.parse(shortHtmlContent).text(); @@ -245,11 +299,11 @@ public class PostReconciler implements Reconciler { return StringUtils.substring(text, 0, 150); } - private boolean isPublished(Snapshot snapshot) { - return snapshot.getSpec().getPublishTime() != null; - } - - private boolean isPublished(Post post) { - return Objects.equals(true, post.getSpec().getPublished()); + static List limitConditionSize(List conditions) { + if (conditions == null || conditions.size() <= 10) { + return conditions; + } + // Retain the last ten conditions + return conditions.subList(conditions.size() - 10, conditions.size()); } } diff --git a/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java index 981c01145..ba7d06b44 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java @@ -4,17 +4,19 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.springframework.web.util.UriUtils.encodePath; import java.time.Instant; +import java.util.Comparator; 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 lombok.extern.slf4j.Slf4j; 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.SinglePageService; import run.halo.app.content.permalinks.ExtensionLocator; import run.halo.app.core.extension.Comment; import run.halo.app.core.extension.Post; @@ -22,6 +24,7 @@ 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.ExtensionOperator; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.GroupVersionKind; import run.halo.app.extension.Ref; import run.halo.app.extension.controller.Reconciler; @@ -46,22 +49,26 @@ import run.halo.app.theme.router.PermalinkIndexDeleteCommand; * @author guqing * @since 2.0.0 */ +@Slf4j public class SinglePageReconciler implements Reconciler { 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 SinglePageService singlePageService; private final CounterService counterService; private final ExternalUrlSupplier externalUrlSupplier; public SinglePageReconciler(ExtensionClient client, ContentService contentService, - ApplicationContext applicationContext, CounterService counterService, + ApplicationContext applicationContext, SinglePageService singlePageService, + CounterService counterService, ExternalUrlSupplier externalUrlSupplier) { this.client = client; this.contentService = contentService; this.applicationContext = applicationContext; + this.singlePageService = singlePageService; this.counterService = counterService; this.externalUrlSupplier = externalUrlSupplier; } @@ -77,12 +84,79 @@ public class SinglePageReconciler implements Reconciler { } addFinalizerIfNecessary(oldPage); - reconcileStatus(request.name()); + // reconcile spec first + reconcileSpec(request.name()); + // then reconcileMetadata(request.name()); + reconcileStatus(request.name()); }); return new Result(false, null); } + private void reconcileSpec(String name) { + // publish single page if necessary + try { + singlePageService.publish(name).block(); + } catch (Throwable e) { + publishFailed(name, e); + throw e; + } + + client.fetch(SinglePage.class, name).ifPresent(page -> { + SinglePage oldPage = JsonUtils.deepCopy(page); + if (page.isPublished() && Objects.equals(false, page.getSpec().getPublish())) { + SinglePage.changePublishedState(page, false); + final SinglePage.SinglePageStatus status = page.getStatusOrDefault(); + Condition condition = new Condition(); + condition.setType("CancelledPublish"); + condition.setStatus(ConditionStatus.TRUE); + condition.setReason(condition.getType()); + condition.setMessage("CancelledPublish"); + condition.setLastTransitionTime(Instant.now()); + status.getConditionsOrDefault().add(condition); + status.setPhase(Post.PostPhase.DRAFT.name()); + } + if (!oldPage.equals(page)) { + client.update(page); + } + }); + } + + private void publishFailed(String name, Throwable error) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(error, "Error must not be null"); + client.fetch(SinglePage.class, name).ifPresent(page -> { + final SinglePage oldPage = JsonUtils.deepCopy(page); + + SinglePage.SinglePageStatus status = page.getStatusOrDefault(); + Post.PostPhase phase = Post.PostPhase.FAILED; + status.setPhase(phase.name()); + + final List conditions = status.getConditionsOrDefault(); + Condition condition = new Condition(); + condition.setType(phase.name()); + condition.setReason(phase.name()); + condition.setMessage(""); + condition.setStatus(ConditionStatus.TRUE); + condition.setLastTransitionTime(Instant.now()); + condition.setMessage(error.getMessage()); + condition.setStatus(ConditionStatus.FALSE); + + if (conditions.size() > 0) { + Condition lastCondition = conditions.get(conditions.size() - 1); + if (!StringUtils.equals(lastCondition.getType(), condition.getType()) + && !StringUtils.equals(lastCondition.getMessage(), condition.getMessage())) { + conditions.add(condition); + } + } + page.setStatus(status); + + if (!oldPage.equals(page)) { + client.update(page); + } + }); + } + private void addFinalizerIfNecessary(SinglePage oldSinglePage) { Set finalizers = oldSinglePage.getMetadata().getFinalizers(); if (finalizers != null && finalizers.contains(FINALIZER_NAME)) { @@ -105,14 +179,12 @@ public class SinglePageReconciler implements Reconciler { permalinkOnDelete(singlePage); // clean up snapshot - Snapshot.SubjectRef subjectRef = - Snapshot.SubjectRef.of(SinglePage.KIND, singlePage.getMetadata().getName()); + Ref ref = Ref.of(singlePage); client.list(Snapshot.class, - snapshot -> subjectRef.equals(snapshot.getSpec().getSubjectRef()), null) + snapshot -> ref.equals(snapshot.getSpec().getSubjectRef()), null) .forEach(client::delete); // clean up comments - Ref ref = Ref.of(singlePage); client.list(Comment.class, comment -> comment.getSpec().getSubjectRef().equals(ref), null) .forEach(client::delete); @@ -139,17 +211,18 @@ public class SinglePageReconciler implements Reconciler { SinglePage.SinglePageSpec spec = singlePage.getSpec(); // handle logic delete - Map labels = getLabelsOrDefault(singlePage); + Map labels = ExtensionUtil.nullSafeLabels(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 (!labels.containsKey(SinglePage.PUBLISHED_LABEL)) { + labels.put(Post.PUBLISHED_LABEL, Boolean.FALSE.toString()); + } if (!oldPage.equals(singlePage)) { client.update(singlePage); } @@ -205,7 +278,7 @@ public class SinglePageReconciler implements Reconciler { contentService.getContent(spec.getHeadSnapshot()) .blockOptional() .ifPresent(content -> { - String contentRevised = content.content(); + String contentRevised = content.getContent(); status.setExcerpt(getExcerpt(contentRevised)); }); } else { @@ -214,7 +287,7 @@ public class SinglePageReconciler implements Reconciler { // handle contributors String headSnapshot = singlePage.getSpec().getHeadSnapshot(); - contentService.listSnapshots(Snapshot.SubjectRef.of(SinglePage.KIND, name)) + contentService.listSnapshots(Ref.of(singlePage)) .collectList() .blockOptional().ifPresent(snapshots -> { List contributors = snapshots.stream() @@ -234,23 +307,18 @@ public class SinglePageReconciler implements Reconciler { .filter(snapshot -> snapshot.getMetadata().getName().equals(headSnapshot)) .findAny() .ifPresent(snapshot -> { - status.setInProgress(!isPublished(snapshot)); + status.setInProgress(!snapshot.isPublished()); }); + + List releasedSnapshots = snapshots.stream() + .filter(Snapshot::isPublished) + .sorted(Comparator.comparing(snapshot -> snapshot.getSpec().getVersion())) + .map(snapshot -> snapshot.getMetadata().getName()) + .toList(); + status.setReleasedSnapshots(releasedSnapshots); }); - // 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()); - } + status.setConditions(limitConditionSize(status.getConditions())); if (!oldPage.equals(singlePage)) { client.update(singlePage); @@ -258,16 +326,6 @@ public class SinglePageReconciler implements Reconciler { }); } - private Map getLabelsOrDefault(SinglePage singlePage) { - Assert.notNull(singlePage, "The singlePage must not be null."); - Map 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(); @@ -275,16 +333,16 @@ public class SinglePageReconciler implements Reconciler { 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; } + + static List limitConditionSize(List conditions) { + if (conditions == null || conditions.size() <= 10) { + return conditions; + } + // Retain the last ten conditions + return conditions.subList(conditions.size() - 10, conditions.size()); + } } diff --git a/src/main/java/run/halo/app/extension/ExtensionUtil.java b/src/main/java/run/halo/app/extension/ExtensionUtil.java index 650fe0e2a..751b59d26 100644 --- a/src/main/java/run/halo/app/extension/ExtensionUtil.java +++ b/src/main/java/run/halo/app/extension/ExtensionUtil.java @@ -1,5 +1,8 @@ package run.halo.app.extension; +import java.util.HashMap; +import java.util.Map; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -38,4 +41,21 @@ public final class ExtensionUtil { public static String buildStoreName(Scheme scheme, String name) { return buildStoreNamePrefix(scheme) + "/" + name; } + + /** + * Gets extension metadata labels null safe. + * + * @param extension extension must not be null + * @return extension metadata labels + */ + public static Map nullSafeLabels(AbstractExtension extension) { + Assert.notNull(extension, "The extension must not be null."); + Assert.notNull(extension.getMetadata(), "The extension metadata must not be null."); + Map labels = extension.getMetadata().getLabels(); + if (labels == null) { + labels = new HashMap<>(); + extension.getMetadata().setLabels(labels); + } + return labels; + } } diff --git a/src/main/java/run/halo/app/infra/utils/PathUtils.java b/src/main/java/run/halo/app/infra/utils/PathUtils.java index d2d22a79f..5ccc728ae 100644 --- a/src/main/java/run/halo/app/infra/utils/PathUtils.java +++ b/src/main/java/run/halo/app/infra/utils/PathUtils.java @@ -3,6 +3,7 @@ package run.halo.app.infra.utils; import java.net.URI; import java.net.URISyntaxException; import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; /** @@ -11,6 +12,7 @@ import org.apache.commons.lang3.StringUtils; * @author guqing * @since 2.0.0 */ +@Slf4j @UtilityClass public class PathUtils { @@ -42,7 +44,9 @@ public class PathUtils { URI uri = new URI(uriString); return uri.isAbsolute(); } catch (URISyntaxException e) { - throw new RuntimeException(e); + log.debug("Failed to parse uri: " + uriString, e); + // ignore this exception + return false; } } diff --git a/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java b/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java index 334835d5c..b6a90c093 100644 --- a/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java +++ b/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java @@ -49,9 +49,8 @@ import run.halo.app.theme.finders.vo.TagVo; @Finder("postFinder") public class PostFinderImpl implements PostFinder { - public static final Predicate FIXED_PREDICATE = post -> - Objects.equals(false, post.getSpec().getDeleted()) - && Objects.equals(true, post.getSpec().getPublished()) + public static final Predicate FIXED_PREDICATE = post -> post.isPublished() + && Objects.equals(false, post.getSpec().getDeleted()) && Post.VisibleEnum.PUBLIC.equals(post.getSpec().getVisible()); private final ReactiveExtensionClient client; @@ -95,8 +94,8 @@ public class PostFinderImpl implements PostFinder { return client.fetch(Post.class, postName) .map(post -> post.getSpec().getReleaseSnapshot()) .flatMap(contentService::getContent) - .map(wrapper -> ContentVo.builder().content(wrapper.content()) - .raw(wrapper.raw()).build()) + .map(wrapper -> ContentVo.builder().content(wrapper.getContent()) + .raw(wrapper.getRaw()).build()) .block(); } @@ -316,6 +315,7 @@ public class PostFinderImpl implements PostFinder { postVo.setCategories(categoryVos); postVo.setTags(tags); postVo.setContributors(contributors); + postVo.setOwner(contributorFinder.getContributor(post.getSpec().getOwner())); populateStats(postVo); return postVo; } diff --git a/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java b/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java index c7fcbb37e..fa86048be 100644 --- a/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java +++ b/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java @@ -32,10 +32,9 @@ import run.halo.app.theme.finders.vo.StatsVo; @Finder("singlePageFinder") public class SinglePageFinderImpl implements SinglePageFinder { - public static final Predicate FIXED_PREDICATE = page -> - Objects.equals(false, page.getSpec().getDeleted()) - && Objects.equals(true, page.getSpec().getPublished()) - && Post.VisibleEnum.PUBLIC.equals(page.getSpec().getVisible()); + public static final Predicate FIXED_PREDICATE = page -> page.isPublished() + && Objects.equals(false, page.getSpec().getDeleted()) + && Post.VisibleEnum.PUBLIC.equals(page.getSpec().getVisible()); private final ReactiveExtensionClient client; @@ -65,6 +64,7 @@ public class SinglePageFinderImpl implements SinglePageFinder { SinglePageVo pageVo = SinglePageVo.from(page); pageVo.setContributors(contributors); pageVo.setContent(content(pageName)); + pageVo.setOwner(contributorFinder.getContributor(page.getSpec().getOwner())); populateStats(pageVo); return pageVo; } @@ -74,8 +74,8 @@ public class SinglePageFinderImpl implements SinglePageFinder { 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()) + .map(wrapper -> ContentVo.builder().content(wrapper.getContent()) + .raw(wrapper.getRaw()).build()) .block(); } diff --git a/src/main/java/run/halo/app/theme/finders/vo/PostVo.java b/src/main/java/run/halo/app/theme/finders/vo/PostVo.java index 2394a5fd0..d37b32dcf 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/PostVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/PostVo.java @@ -35,6 +35,8 @@ public class PostVo { private List contributors; + private Contributor owner; + private StatsVo stats; /** diff --git a/src/main/java/run/halo/app/theme/finders/vo/SinglePageVo.java b/src/main/java/run/halo/app/theme/finders/vo/SinglePageVo.java index 658fc8463..cca5426f5 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/SinglePageVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/SinglePageVo.java @@ -33,6 +33,8 @@ public class SinglePageVo { private List contributors; + private Contributor owner; + /** * Convert {@link SinglePage} to {@link SinglePageVo}. * diff --git a/src/main/resources/extensions/system-configurable-configmap.yaml b/src/main/resources/extensions/system-configurable-configmap.yaml index 671a6df9d..bb2436f31 100644 --- a/src/main/resources/extensions/system-configurable-configmap.yaml +++ b/src/main/resources/extensions/system-configurable-configmap.yaml @@ -19,6 +19,10 @@ data: "globalHead": "", "footer": "" } + post: | + { + "review": false + } menu: | { "primary": "primary" diff --git a/src/main/resources/extensions/system-setting.yaml b/src/main/resources/extensions/system-setting.yaml index 6154be74b..2cf9640a6 100644 --- a/src/main/resources/extensions/system-setting.yaml +++ b/src/main/resources/extensions/system-setting.yaml @@ -50,12 +50,7 @@ spec: value: 10 min: 1 max: 100 - validation: required | max:100 - - $formkit: checkbox - label: "新文章审核" - value: false - name: review - help: "用户发布文章是否需要管理员审核" + validation: required - group: seo label: SEO 设置 formSchema: diff --git a/src/test/java/run/halo/app/content/ContentRequestTest.java b/src/test/java/run/halo/app/content/ContentRequestTest.java index b75b5601a..3f04f1fcc 100644 --- a/src/test/java/run/halo/app/content/ContentRequestTest.java +++ b/src/test/java/run/halo/app/content/ContentRequestTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; import run.halo.app.core.extension.Post; import run.halo.app.core.extension.Snapshot; +import run.halo.app.extension.Ref; import run.halo.app.infra.utils.JsonUtils; /** @@ -19,8 +20,11 @@ class ContentRequestTest { @BeforeEach void setUp() { - Snapshot.SubjectRef subjectRef = Snapshot.SubjectRef.of(Post.KIND, "test-post"); - contentRequest = new ContentRequest(subjectRef, "snapshot-1", """ + Ref ref = new Ref(); + ref.setKind(Post.KIND); + ref.setGroup("content.halo.run"); + ref.setName("test-post"); + contentRequest = new ContentRequest(ref, "snapshot-1", """ Four score and seven years ago our fathers @@ -50,6 +54,7 @@ class ContentRequestTest { "spec": { "subjectRef": { "kind": "Post", + "group": "content.halo.run", "name": "test-post" }, "rawType": "MARKDOWN", diff --git a/src/test/java/run/halo/app/content/ContentServiceTest.java b/src/test/java/run/halo/app/content/ContentServiceTest.java index a77c55638..e35ada724 100644 --- a/src/test/java/run/halo/app/content/ContentServiceTest.java +++ b/src/test/java/run/halo/app/content/ContentServiceTest.java @@ -11,6 +11,7 @@ import static run.halo.app.content.TestPost.snapshotV2; import static run.halo.app.content.TestPost.snapshotV3; import java.time.Instant; +import java.util.HashMap; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -25,6 +26,7 @@ import run.halo.app.content.impl.ContentServiceImpl; import run.halo.app.core.extension.Post; import run.halo.app.core.extension.Snapshot; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.extension.Ref; import run.halo.app.infra.utils.JsonUtils; /** @@ -50,20 +52,23 @@ class ContentServiceTest { @Test void draftContent() { Snapshot snapshotV1 = snapshotV1(); - Snapshot.SubjectRef subjectRef = Snapshot.SubjectRef.of(Post.KIND, "test-post"); - snapshotV1.getSpec().setSubjectRef(subjectRef); + Ref ref = postRef("test-post"); + snapshotV1.getSpec().setSubjectRef(ref); ContentRequest contentRequest = - new ContentRequest(subjectRef, null, + new ContentRequest(ref, null, snapshotV1.getSpec().getRawPatch(), snapshotV1.getSpec().getContentPatch(), snapshotV1.getSpec().getRawType()); pilingBaseSnapshot(snapshotV1); - - ContentWrapper contentWrapper = - new ContentWrapper("snapshot-A", contentRequest.raw(), - contentRequest.content(), snapshotV1.getSpec().getRawType()); + ContentWrapper contentWrapper = ContentWrapper.builder() + .snapshotName("snapshot-A") + .version(1) + .raw(contentRequest.raw()) + .content(contentRequest.content()) + .rawType(snapshotV1.getSpec().getRawType()) + .build(); ArgumentCaptor captor = ArgumentCaptor.forClass(Snapshot.class); when(client.create(any())).thenReturn(Mono.just(snapshotV1)); @@ -77,7 +82,7 @@ class ContentServiceTest { Snapshot snapshot = captor.getValue(); snapshotV1.getMetadata().setName(snapshot.getMetadata().getName()); - snapshotV1.getSpec().setSubjectRef(subjectRef); + snapshotV1.getSpec().setSubjectRef(ref); assertThat(snapshot).isEqualTo(snapshotV1); } @@ -85,14 +90,14 @@ class ContentServiceTest { void updateContent() { String headSnapshot = "snapshot-A"; Snapshot snapshotV1 = snapshotV1(); - Snapshot.SubjectRef subjectRef = Snapshot.SubjectRef.of(Post.KIND, "test-post"); + Ref ref = postRef("test-post"); Snapshot updated = snapshotV1(); updated.getSpec().setRawPatch("hello"); updated.getSpec().setContentPatch("

hello

"); - updated.getSpec().setSubjectRef(subjectRef); + updated.getSpec().setSubjectRef(ref); ContentRequest contentRequest = - new ContentRequest(subjectRef, headSnapshot, + new ContentRequest(ref, headSnapshot, snapshotV1.getSpec().getRawPatch(), snapshotV1.getSpec().getContentPatch(), snapshotV1.getSpec().getRawType()); @@ -102,9 +107,13 @@ class ContentServiceTest { when(client.fetch(eq(Snapshot.class), eq(contentRequest.headSnapshotName()))) .thenReturn(Mono.just(updated)); - ContentWrapper contentWrapper = - new ContentWrapper(headSnapshot, contentRequest.raw(), - contentRequest.content(), snapshotV1.getSpec().getRawType()); + ContentWrapper contentWrapper = ContentWrapper.builder() + .snapshotName(headSnapshot) + .version(1) + .raw(contentRequest.raw()) + .content(contentRequest.content()) + .rawType(snapshotV1.getSpec().getRawType()) + .build(); ArgumentCaptor captor = ArgumentCaptor.forClass(Snapshot.class); when(client.update(any())).thenReturn(Mono.just(updated)); @@ -124,12 +133,15 @@ class ContentServiceTest { void updateContentWhenHasDraftVersionButHeadPoints2Published() { final String headSnapshot = "snapshot-A"; Snapshot snapshotV1 = snapshotV1(); + final Ref ref = postRef("test-post"); Snapshot snapshotV2 = snapshotV2(); snapshotV2.getSpec().setPublishTime(null); // v1(released),v2 + snapshotV1.getMetadata().setLabels(new HashMap<>()); + Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels()); snapshotV1.getSpec().setPublishTime(Instant.now()); pilingBaseSnapshot(snapshotV2, snapshotV1); @@ -138,23 +150,25 @@ class ContentServiceTest { when(client.fetch(eq(Snapshot.class), eq(snapshotV1.getMetadata().getName()))) .thenReturn(Mono.just(snapshotV1)); - Snapshot.SubjectRef subjectRef = Snapshot.SubjectRef.of(Post.KIND, "test-post"); - - ContentRequest contentRequest = - new ContentRequest(subjectRef, headSnapshot, "C", + final ContentRequest contentRequest = + new ContentRequest(ref, headSnapshot, "C", "

C

", snapshotV1.getSpec().getRawType()); when(client.create(any())).thenReturn(Mono.just(snapshotV3())); - StepVerifier.create(contentService.latestSnapshotVersion(subjectRef)) + StepVerifier.create(contentService.latestSnapshotVersion(ref)) .expectNext(snapshotV2) .expectComplete() .verify(); + Snapshot publishedV2 = snapshotV2(); + publishedV2.getMetadata().setLabels(new HashMap<>()); + Snapshot.putPublishedLabel(publishedV2.getMetadata().getLabels()); + when(client.update(any())).thenReturn(Mono.just(publishedV2)); StepVerifier.create(contentService.updateContent(contentRequest)) .consumeNextWith(created -> { - assertThat(created.raw()).isEqualTo("C"); - assertThat(created.content()).isEqualTo("

C

"); + assertThat(created.getRaw()).isEqualTo("C"); + assertThat(created.getContent()).isEqualTo("

C

"); }) .expectComplete() .verify(); @@ -162,15 +176,16 @@ class ContentServiceTest { @Test void updateContentWhenHeadPoints2Published() { - Snapshot.SubjectRef subjectRef = Snapshot.SubjectRef.of(Post.KIND, "test-post"); - + final Ref ref = postRef("test-post"); // v1(released),v2 Snapshot snapshotV1 = snapshotV1(); + snapshotV1.getMetadata().setLabels(new HashMap<>()); + Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels()); snapshotV1.getSpec().setPublishTime(Instant.now()); - snapshotV1.getSpec().setSubjectRef(subjectRef); + snapshotV1.getSpec().setSubjectRef(ref); Snapshot snapshotV2 = snapshotV2(); - snapshotV2.getSpec().setSubjectRef(subjectRef); + snapshotV2.getSpec().setSubjectRef(ref); snapshotV2.getSpec().setPublishTime(null); final String headSnapshot = snapshotV2.getMetadata().getName(); @@ -184,20 +199,20 @@ class ContentServiceTest { .thenReturn(Mono.just(snapshotV1)); ContentRequest contentRequest = - new ContentRequest(subjectRef, headSnapshot, "C", + new ContentRequest(ref, headSnapshot, "C", "

C

", snapshotV1.getSpec().getRawType()); when(client.update(any())).thenReturn(Mono.just(snapshotV2())); - StepVerifier.create(contentService.latestSnapshotVersion(subjectRef)) + StepVerifier.create(contentService.latestSnapshotVersion(ref)) .expectNext(snapshotV2) .expectComplete() .verify(); StepVerifier.create(contentService.updateContent(contentRequest)) .consumeNextWith(updated -> { - assertThat(updated.raw()).isEqualTo("C"); - assertThat(updated.content()).isEqualTo("

C

"); + assertThat(updated.getRaw()).isEqualTo("C"); + assertThat(updated.getContent()).isEqualTo("

C

"); }) .expectComplete() .verify(); @@ -207,12 +222,11 @@ class ContentServiceTest { @Test void publishContent() { - Snapshot.SubjectRef subjectRef = Snapshot.SubjectRef.of(Post.KIND, "test-post"); - + Ref ref = postRef("test-post"); // v1(released),v2 Snapshot snapshotV1 = snapshotV1(); snapshotV1.getSpec().setPublishTime(null); - snapshotV1.getSpec().setSubjectRef(subjectRef); + snapshotV1.getSpec().setSubjectRef(ref); final String headSnapshot = snapshotV1.getMetadata().getName(); @@ -223,7 +237,7 @@ class ContentServiceTest { when(client.update(any())).thenReturn(Mono.just(snapshotV2())); - StepVerifier.create(contentService.publish(headSnapshot, subjectRef)) + StepVerifier.create(contentService.publish(headSnapshot, ref)) .expectNext() .consumeNextWith(p -> { System.out.println(JsonUtils.objectToJson(p)); @@ -234,14 +248,25 @@ class ContentServiceTest { verify(client, times(1)).update(any()); } + private static Ref postRef(String name) { + Ref ref = new Ref(); + ref.setGroup("content.halo.run"); + ref.setVersion("v1alpha1"); + ref.setKind(Post.KIND); + ref.setName(name); + return ref; + } + @Test void publishContentWhenHasPublishedThenDoNothing() { - Snapshot.SubjectRef subjectRef = Snapshot.SubjectRef.of(Post.KIND, "test-post"); + final Ref ref = postRef("test-post"); // v1(released),v2 Snapshot snapshotV1 = snapshotV1(); + snapshotV1.getMetadata().setLabels(new HashMap<>()); + Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels()); snapshotV1.getSpec().setPublishTime(Instant.now()); - snapshotV1.getSpec().setSubjectRef(subjectRef); + snapshotV1.getSpec().setSubjectRef(ref); final String headSnapshot = snapshotV1.getMetadata().getName(); @@ -252,7 +277,7 @@ class ContentServiceTest { when(client.update(any())).thenReturn(Mono.just(snapshotV2())); - StepVerifier.create(contentService.publish(headSnapshot, subjectRef)) + StepVerifier.create(contentService.publish(headSnapshot, ref)) .expectNext() .consumeNextWith(p -> { System.out.println(JsonUtils.objectToJson(p)); @@ -271,21 +296,23 @@ class ContentServiceTest { @Test void baseSnapshotVersion() { String postName = "post-1"; + final Ref ref = postRef(postName); Snapshot snapshotV1 = snapshotV1(); + snapshotV1.getMetadata().setLabels(new HashMap<>()); + Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels()); snapshotV1.getSpec().setPublishTime(Instant.now()); - snapshotV1.setSubjectRef(Post.KIND, postName); + snapshotV1.getSpec().setSubjectRef(ref); Snapshot snapshotV2 = TestPost.snapshotV2(); - snapshotV2.setSubjectRef(Post.KIND, postName); + snapshotV2.getSpec().setSubjectRef(ref); Snapshot snapshotV3 = TestPost.snapshotV3(); - snapshotV3.setSubjectRef(Post.KIND, postName); + snapshotV3.getSpec().setSubjectRef(ref); when(client.list(eq(Snapshot.class), any(), any())) .thenReturn(Flux.just(snapshotV2, snapshotV1, snapshotV3)); - Snapshot.SubjectRef subjectRef = Snapshot.SubjectRef.of(Post.KIND, postName); - StepVerifier.create(contentService.getBaseSnapshot(subjectRef)) + StepVerifier.create(contentService.getBaseSnapshot(ref)) .expectNext(snapshotV1) .expectComplete() .verify(); @@ -294,24 +321,26 @@ class ContentServiceTest { @Test void latestSnapshotVersion() { String postName = "post-1"; + final Ref ref = postRef(postName); Snapshot snapshotV1 = snapshotV1(); + snapshotV1.getMetadata().setLabels(new HashMap<>()); + Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels()); snapshotV1.getSpec().setPublishTime(Instant.now()); - snapshotV1.setSubjectRef(Post.KIND, postName); + snapshotV1.getSpec().setSubjectRef(ref); Snapshot snapshotV2 = TestPost.snapshotV2(); - snapshotV2.setSubjectRef(Post.KIND, postName); + snapshotV2.getSpec().setSubjectRef(ref); when(client.list(eq(Snapshot.class), any(), any())) .thenReturn(Flux.just(snapshotV1, snapshotV2)); - Snapshot.SubjectRef subjectRef = Snapshot.SubjectRef.of(Post.KIND, postName); - StepVerifier.create(contentService.latestSnapshotVersion(subjectRef)) + StepVerifier.create(contentService.latestSnapshotVersion(ref)) .expectNext(snapshotV2) .expectComplete() .verify(); when(client.list(eq(Snapshot.class), any(), any())) .thenReturn(Flux.just(snapshotV1, snapshotV2, snapshotV3())); - StepVerifier.create(contentService.latestSnapshotVersion(subjectRef)) + StepVerifier.create(contentService.latestSnapshotVersion(ref)) .expectNext(snapshotV3()) .expectComplete() .verify(); @@ -320,19 +349,21 @@ class ContentServiceTest { @Test void latestPublishedSnapshotThenV1() { String postName = "post-1"; + Ref ref = postRef(postName); Snapshot snapshotV1 = snapshotV1(); - snapshotV1.setSubjectRef(Post.KIND, postName); + snapshotV1.getSpec().setSubjectRef(ref); + snapshotV1.getMetadata().setLabels(new HashMap<>()); + Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels()); snapshotV1.getSpec().setPublishTime(Instant.now()); Snapshot snapshotV2 = TestPost.snapshotV2(); - snapshotV2.setSubjectRef(Post.KIND, postName); + snapshotV2.getSpec().setSubjectRef(ref); snapshotV2.getSpec().setPublishTime(null); when(client.list(eq(Snapshot.class), any(), any())) .thenReturn(Flux.just(snapshotV1, snapshotV2)); - Snapshot.SubjectRef subjectRef = Snapshot.SubjectRef.of(Post.KIND, postName); - StepVerifier.create(contentService.latestPublishedSnapshot(subjectRef)) + StepVerifier.create(contentService.latestPublishedSnapshot(ref)) .expectNext(snapshotV1) .expectComplete() .verify(); @@ -341,20 +372,23 @@ class ContentServiceTest { @Test void latestPublishedSnapshotThenV2() { String postName = "post-1"; + Ref ref = postRef(postName); Snapshot snapshotV1 = snapshotV1(); - snapshotV1.setSubjectRef(Post.KIND, postName); + snapshotV1.getSpec().setSubjectRef(ref); + snapshotV1.getMetadata().setLabels(new HashMap<>()); + Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels()); snapshotV1.getSpec().setPublishTime(Instant.now()); Snapshot snapshotV2 = TestPost.snapshotV2(); - snapshotV2.setSubjectRef(Post.KIND, postName); + snapshotV2.getSpec().setSubjectRef(ref); + snapshotV2.getMetadata().setLabels(new HashMap<>()); + Snapshot.putPublishedLabel(snapshotV2.getMetadata().getLabels()); snapshotV2.getSpec().setPublishTime(Instant.now()); when(client.list(eq(Snapshot.class), any(), any())) .thenReturn(Flux.just(snapshotV2, snapshotV1)); - Snapshot.SubjectRef subjectRef = Snapshot.SubjectRef.of(Post.KIND, postName); - - StepVerifier.create(contentService.latestPublishedSnapshot(subjectRef)) + StepVerifier.create(contentService.latestPublishedSnapshot(ref)) .expectNext(snapshotV2) .expectComplete() .verify(); diff --git a/src/test/java/run/halo/app/content/impl/PostServiceImplTest.java b/src/test/java/run/halo/app/content/impl/PostServiceImplTest.java index 32d2e8ac6..f9aa7624b 100644 --- a/src/test/java/run/halo/app/content/impl/PostServiceImplTest.java +++ b/src/test/java/run/halo/app/content/impl/PostServiceImplTest.java @@ -2,21 +2,37 @@ package run.halo.app.content.impl; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.time.Instant; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import run.halo.app.content.ContentService; import run.halo.app.content.PostQuery; import run.halo.app.content.TestPost; import run.halo.app.core.extension.Post; +import run.halo.app.core.extension.Snapshot; +import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.extension.Ref; +import run.halo.app.infra.Condition; +import run.halo.app.infra.ConditionStatus; /** * Tests for {@link PostServiceImpl}. @@ -74,4 +90,124 @@ class PostServiceImplTest { test = postService.postListPredicate(postQuery).test(post); assertThat(test).isTrue(); } + + @Test + void publishWhenPostIsNonePublishedState() { + String postName = "fake-post"; + String snapV1name = "fake-post-snapshot-v1"; + Post post = TestPost.postV1(); + post.getMetadata().setName(postName); + + // v1 not published + Snapshot snapshotV1 = TestPost.snapshotV1(); + snapshotV1.getMetadata().setName(snapV1name); + snapshotV1.getSpec().setPublishTime(null); + post.getSpec().setBaseSnapshot(snapshotV1.getMetadata().getName()); + + post.getSpec().setHeadSnapshot(null); + post.getSpec().setReleaseSnapshot(null); + when(client.fetch(eq(Post.class), eq(postName))).thenReturn(Mono.just(post)); + verify(client, times(0)).fetch(eq(Snapshot.class), eq(snapV1name)); + + postService.publishPost(postName) + .as(StepVerifier::create) + .verifyComplete(); + } + + @Test + void publishWhenPostIsPublishedStateAndNotPublishedBefore() { + String postName = "fake-post"; + String snapV1name = "fake-post-snapshot-v1"; + Post post = TestPost.postV1(); + post.getSpec().setPublish(true); + post.getSpec().setPublishTime(null); + post.getMetadata().setName(postName); + post.getSpec().setBaseSnapshot(snapV1name); + post.getSpec().setHeadSnapshot(null); + post.getSpec().setReleaseSnapshot(null); + when(client.fetch(eq(Post.class), eq(postName))).thenReturn(Mono.just(post)); + + // v1 not published + Snapshot snapshotV1 = TestPost.snapshotV1(); + snapshotV1.getMetadata().setName(snapV1name); + snapshotV1.getSpec().setPublishTime(null); + when(client.fetch(eq(Snapshot.class), eq(snapV1name))).thenReturn(Mono.just(snapshotV1)); + + when(contentService.publish(eq(snapV1name), eq(Ref.of(post)))) + .thenReturn(Mono.just(snapshotV1.applyPatch(snapshotV1))); + + when(client.update(any(Post.class))).thenAnswer((Answer>) invocation -> { + Post updated = invocation.getArgument(0); + return Mono.just(updated); + }); + + postService.publishPost(postName) + .as(StepVerifier::create) + .consumeNextWith(expected -> { + ArgumentCaptor captor = ArgumentCaptor.forClass(Post.class); + verify(client, times(1)).update(captor.capture()); + Post updated = captor.getValue(); + assertThat(updated.getSpec().getReleaseSnapshot()).isEqualTo(snapV1name); + assertThat(updated.getSpec().getHeadSnapshot()).isEqualTo(snapV1name); + assertThat(updated.getSpec().getPublishTime()).isNotNull(); + assertThat(updated.getSpec().getVersion()).isEqualTo(1); + List conditions = updated.getStatus().getConditions(); + assertThat(conditions).hasSize(1); + assertThat(conditions.get(0).getType()).isEqualTo("PUBLISHED"); + assertThat(conditions.get(0).getStatus()).isEqualTo(ConditionStatus.TRUE); + assertThat(expected).isNotNull(); + }) + .verifyComplete(); + } + + @Test + void publishWhenPostIsPublishedStateAndPublishedBefore() { + String postName = "fake-post"; + String snapV1name = "fake-post-snapshot-v1"; + String snapV2name = "fake-post-snapshot-v2"; + Post post = TestPost.postV1(); + post.getSpec().setPublish(true); + post.getSpec().setPublishTime(null); + post.getMetadata().setName(postName); + post.getSpec().setBaseSnapshot(snapV1name); + post.getSpec().setHeadSnapshot(snapV2name); + post.getSpec().setReleaseSnapshot(snapV2name); + ExtensionUtil.nullSafeLabels(post).put(Post.PUBLISHED_LABEL, "true"); + when(client.fetch(eq(Post.class), eq(postName))).thenReturn(Mono.just(post)); + + // v1 has been published + Snapshot snapshotV1 = TestPost.snapshotV1(); + snapshotV1.getMetadata().setName(snapV1name); + snapshotV1.getMetadata().setLabels(new HashMap<>()); + Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels()); + snapshotV1.getSpec().setPublishTime(Instant.now()); + + // v1 not published + Snapshot snapshotV2 = TestPost.snapshotV2(); + snapshotV2.getMetadata().setName(snapV2name); + snapshotV2.getSpec().setPublishTime(null); + when(client.fetch(eq(Snapshot.class), eq(snapV2name))).thenReturn(Mono.just(snapshotV2)); + + when(contentService.publish(eq(snapV2name), eq(Ref.of(post)))) + .thenReturn(Mono.just(snapshotV2.applyPatch(snapshotV1))); + + when(client.update(any(Post.class))).thenAnswer((Answer>) invocation -> { + Post updated = invocation.getArgument(0); + return Mono.just(updated); + }); + + postService.publishPost(postName) + .as(StepVerifier::create) + .consumeNextWith(expected -> { + assertThat(expected).isNotNull(); + ArgumentCaptor captor = ArgumentCaptor.forClass(Post.class); + verify(client, times(1)).update(captor.capture()); + Post updated = captor.getValue(); + assertThat(updated.getSpec().getReleaseSnapshot()).isEqualTo(snapV2name); + assertThat(updated.getSpec().getHeadSnapshot()).isEqualTo(snapV2name); + assertThat(updated.getSpec().getPublishTime()).isNotNull(); + assertThat(updated.getSpec().getVersion()).isEqualTo(2); + }) + .verifyComplete(); + } } \ No newline at end of file diff --git a/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java b/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java index e99e162b6..8ccc9bc92 100644 --- a/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java +++ b/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java @@ -2,11 +2,13 @@ package run.halo.app.core.extension.endpoint; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.web.reactive.server.WebTestClient; @@ -15,6 +17,7 @@ import run.halo.app.content.PostRequest; import run.halo.app.content.PostService; import run.halo.app.content.TestPost; import run.halo.app.core.extension.Post; +import run.halo.app.extension.ReactiveExtensionClient; /** * Tests for @{@link PostEndpoint}. @@ -26,13 +29,16 @@ import run.halo.app.core.extension.Post; class PostEndpointTest { @Mock private PostService postService; + @Mock + private ReactiveExtensionClient client; + + @InjectMocks + private PostEndpoint postEndpoint; private WebTestClient webTestClient; @BeforeEach void setUp() { - PostEndpoint postEndpoint = new PostEndpoint(postService); - webTestClient = WebTestClient .bindToRouterFunction(postEndpoint.endpoint()) .build(); @@ -41,7 +47,6 @@ class PostEndpointTest { @Test void draftPost() { when(postService.draftPost(any())).thenReturn(Mono.just(TestPost.postV1())); - webTestClient.post() .uri("/posts") .bodyValue(postRequest(TestPost.postV1())) @@ -68,7 +73,11 @@ class PostEndpointTest { @Test void publishPost() { - when(postService.publishPost(any())).thenReturn(Mono.just(TestPost.postV1())); + Post post = TestPost.postV1(); + when(postService.publishPost(any())).thenReturn(Mono.just(post)); + when(client.fetch(eq(Post.class), eq(post.getMetadata().getName()))) + .thenReturn(Mono.just(post)); + when(client.update(any())).thenReturn(Mono.just(post)); webTestClient.put() .uri("/posts/post-A/publish") @@ -77,7 +86,7 @@ class PostEndpointTest { .expectStatus() .isOk() .expectBody(Post.class) - .value(post -> assertThat(post).isEqualTo(TestPost.postV1())); + .value(p -> assertThat(p).isEqualTo(post)); } PostRequest postRequest(Post post) { diff --git a/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java index 25852c049..b52fc9977 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java @@ -8,6 +8,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Optional; import java.util.Set; @@ -21,12 +23,14 @@ 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.PostService; import run.halo.app.content.TestPost; import run.halo.app.content.permalinks.PostPermalinkPolicy; import run.halo.app.core.extension.Post; import run.halo.app.core.extension.Snapshot; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.controller.Reconciler; +import run.halo.app.infra.Condition; /** * Tests for {@link PostReconciler}. @@ -44,6 +48,9 @@ class PostReconcilerTest { @Mock private PostPermalinkPolicy postPermalinkPolicy; + @Mock + private PostService postService; + @InjectMocks private PostReconciler postReconciler; @@ -51,12 +58,13 @@ class PostReconcilerTest { void reconcile() { String name = "post-A"; Post post = TestPost.postV1(); - post.getSpec().setPublished(false); + post.getSpec().setPublish(false); post.getSpec().setHeadSnapshot("post-A-head-snapshot"); when(client.fetch(eq(Post.class), eq(name))) .thenReturn(Optional.of(post)); when(contentService.getContent(eq(post.getSpec().getReleaseSnapshot()))) .thenReturn(Mono.empty()); + when(postService.publishPost(eq(name))).thenReturn(Mono.empty()); Snapshot snapshotV1 = TestPost.snapshotV1(); Snapshot snapshotV2 = TestPost.snapshotV2(); @@ -85,23 +93,31 @@ class PostReconcilerTest { // https://github.com/halo-dev/halo/issues/2452 String name = "post-A"; Post post = TestPost.postV1(); - post.getSpec().setPublished(true); + post.getSpec().setPublish(true); post.getSpec().setHeadSnapshot("post-A-head-snapshot"); post.getSpec().setReleaseSnapshot("post-fake-released-snapshot"); when(client.fetch(eq(Post.class), eq(name))) .thenReturn(Optional.of(post)); when(contentService.getContent(eq(post.getSpec().getReleaseSnapshot()))) - .thenReturn(Mono.just( - new ContentWrapper(post.getSpec().getHeadSnapshot(), "hello world", - "

hello world

", "markdown"))); + .thenReturn(Mono.just(ContentWrapper.builder() + .snapshotName(post.getSpec().getHeadSnapshot()) + .raw("hello world") + .content("

hello world

") + .rawType("markdown") + .build())); + + Snapshot snapshotV2 = TestPost.snapshotV2(); + snapshotV2.getMetadata().setLabels(new HashMap<>()); + Snapshot.putPublishedLabel(snapshotV2.getMetadata().getLabels()); + snapshotV2.getSpec().setPublishTime(Instant.now()); + snapshotV2.getSpec().setContributors(Set.of("guqing", "zhangsan")); Snapshot snapshotV1 = TestPost.snapshotV1(); - Snapshot snapshotV2 = TestPost.snapshotV2(); - snapshotV2.getSpec().setPublishTime(Instant.now()); snapshotV1.getSpec().setContributors(Set.of("guqing")); - snapshotV2.getSpec().setContributors(Set.of("guqing", "zhangsan")); + when(contentService.listSnapshots(any())) .thenReturn(Flux.just(snapshotV1, snapshotV2)); + when(postService.publishPost(eq(name))).thenReturn(Mono.empty()); ArgumentCaptor captor = ArgumentCaptor.forClass(Post.class); postReconciler.reconcile(new Reconciler.Request(name)); @@ -111,4 +127,26 @@ class PostReconcilerTest { assertThat(value.getStatus().getExcerpt()).isEqualTo("hello world"); } + @Test + void limitConditionSize() { + List conditions = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Condition condition = new Condition(); + condition.setType("test-" + i); + conditions.add(condition); + } + List subConditions = PostReconciler.limitConditionSize(conditions); + assertThat(subConditions.get(0).getType()).isEqualTo("test-0"); + assertThat(subConditions.get(9).getType()).isEqualTo("test-9"); + + for (int i = 10; i < 15; i++) { + Condition condition = new Condition(); + condition.setType("test-" + i); + conditions.add(condition); + } + subConditions = PostReconciler.limitConditionSize(conditions); + assertThat(subConditions.size()).isEqualTo(10); + assertThat(subConditions.get(0).getType()).isEqualTo("test-5"); + assertThat(subConditions.get(9).getType()).isEqualTo("test-14"); + } } \ No newline at end of file diff --git a/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java index 90154f3c8..6aa1047c3 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java @@ -13,10 +13,10 @@ import java.net.URI; 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.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationContext; @@ -24,6 +24,7 @@ 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.SinglePageService; import run.halo.app.content.TestPost; import run.halo.app.core.extension.Post; import run.halo.app.core.extension.SinglePage; @@ -56,17 +57,15 @@ class SinglePageReconcilerTest { @Mock private CounterService counterService; + @Mock + private SinglePageService singlePageService; + @Mock private ExternalUrlSupplier externalUrlSupplier; + @InjectMocks private SinglePageReconciler singlePageReconciler; - @BeforeEach - void setUp() { - singlePageReconciler = new SinglePageReconciler(client, contentService, applicationContext, - counterService, externalUrlSupplier); - } - @Test void reconcile() { String name = "page-A"; @@ -75,9 +74,13 @@ class SinglePageReconcilerTest { 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", - "

hello world

", "markdown"))); + .thenReturn(Mono.just(ContentWrapper.builder() + .snapshotName(page.getSpec().getHeadSnapshot()) + .raw("hello world") + .content("

hello world

") + .rawType("markdown") + .build()) + ); Snapshot snapshotV1 = snapshotV1(); Snapshot snapshotV2 = TestPost.snapshotV2(); @@ -86,6 +89,7 @@ class SinglePageReconcilerTest { when(contentService.listSnapshots(any())) .thenReturn(Flux.just(snapshotV1, snapshotV2)); when(externalUrlSupplier.get()).thenReturn(URI.create("")); + when(singlePageService.publish(eq(name))).thenReturn(Mono.empty()); ArgumentCaptor captor = ArgumentCaptor.forClass(SinglePage.class); singlePageReconciler.reconcile(new Reconciler.Request(name)); diff --git a/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java b/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java index 108a83ae5..f432c8aff 100644 --- a/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java +++ b/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.when; import java.time.Instant; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang3.tuple.Pair; @@ -67,14 +68,19 @@ class PostFinderImplTest { void content() { Post post = post(1); post.getSpec().setReleaseSnapshot("release-snapshot"); - ContentWrapper contentWrapper = new ContentWrapper("snapshot", "raw", "content", "rawType"); + ContentWrapper contentWrapper = ContentWrapper.builder() + .snapshotName("snapshot") + .raw("raw") + .content("content") + .rawType("rawType") + .build(); when(client.fetch(eq(Post.class), eq("post-1"))) .thenReturn(Mono.just(post)); when(contentService.getContent(post.getSpec().getReleaseSnapshot())) .thenReturn(Mono.just(contentWrapper)); ContentVo content = postFinder.content("post-1"); - assertThat(content.getContent()).isEqualTo(contentWrapper.content()); - assertThat(content.getRaw()).isEqualTo(contentWrapper.raw()); + assertThat(content.getContent()).isEqualTo(contentWrapper.getContent()); + assertThat(content.getRaw()).isEqualTo(contentWrapper.getRaw()); } @Test @@ -166,17 +172,17 @@ class PostFinderImplTest { List postsForArchives() { Post post1 = post(1); - post1.getSpec().setPublished(true); + post1.getSpec().setPublish(true); post1.getSpec().setPublishTime(Instant.parse("2021-01-01T00:00:00Z")); post1.getMetadata().setCreationTimestamp(Instant.now()); Post post2 = post(2); - post2.getSpec().setPublished(true); + post2.getSpec().setPublish(true); post2.getSpec().setPublishTime(Instant.parse("2022-12-01T00:00:00Z")); post2.getMetadata().setCreationTimestamp(Instant.now()); Post post3 = post(3); - post3.getSpec().setPublished(true); + post3.getSpec().setPublish(true); post3.getSpec().setPublishTime(Instant.parse("2022-12-03T00:00:00Z")); post3.getMetadata().setCreationTimestamp(Instant.now()); return List.of(post1, post2, post3); @@ -205,7 +211,8 @@ class PostFinderImplTest { post4.getMetadata().setCreationTimestamp(Instant.now()); Post post5 = post(5); - post5.getSpec().setPublished(false); + post5.getSpec().setPublish(false); + post5.getMetadata().getLabels().clear(); post5.getMetadata().setCreationTimestamp(Instant.now()); Post post6 = post(6); @@ -222,6 +229,8 @@ class PostFinderImplTest { metadata.setName("post-" + i); metadata.setCreationTimestamp(Instant.now()); metadata.setAnnotations(Map.of("K1", "V1")); + metadata.setLabels(new HashMap<>()); + metadata.getLabels().put(Post.PUBLISHED_LABEL, "true"); post.setMetadata(metadata); Post.PostSpec postSpec = new Post.PostSpec(); @@ -230,7 +239,7 @@ class PostFinderImplTest { postSpec.setPublishTime(Instant.now()); postSpec.setPinned(false); postSpec.setPriority(0); - postSpec.setPublished(true); + postSpec.setPublish(true); postSpec.setVisible(Post.VisibleEnum.PUBLIC); postSpec.setTitle("title-" + i); postSpec.setSlug("slug-" + i);