mirror of https://github.com/halo-dev/halo
refactor: snapshot attributes and post publish (#2709)
#### What type of PR is this?
/kind improvement
/area core
/milestone 2.0
#### What this PR does / why we need it:
1. 简化 Snapshot 的指责,去除 Snapshot 中关于 version,publishTime 等与发布相关的东西,Snapshot本身没有发布概念
2. 对应修改文章和自定义页面,将版本的概念转移到拥有 Snapshot 的模型上,如 Post 和 SinglePage
⚠️此 PR 为破坏性更新,对旧数据不兼容,可能导致旧数据文章内容无法显示问题
Console 端需要做出一些修改:
- 将文章发布前调用保存内容的 API 修改为 `/posts/{name}/content`
- 将自定义页面发布前调用保存内容的 API 修改为 `/singlepages/{name}/content`
发布接口提供了一个 `async` 参数,默认值为 `false`此时发布API需要等待发布成功后才返回结果,如果等待超时则提示`Publishing wait timeout.`, 如果传递 `async=true` 表示异步发布,更新完数据就成功,reconciler 在后台慢慢执行。
#### Special notes for your reviewer:
how to test it?
1. 新创建一篇文章,测试点击保存是否成功
2. 新创建一篇文章,测试编辑内容后直接发布是否成功
3. 测试发布过的文章的取消发布状态是否显示未发布状态
4. 对于 Snapshot,新建文章点保存会创建一个snapshot,点击发布则是更新之前点保存创建的一条,所以记录数不会多,发布之后进入编辑随便写点内容直接点发布后查询snapshot则会多一条记录。
5. 文章的 status 里的 conditions 最新添加的都在第一个即头插入,如果连续两次发布则不会在增加 condition,长度超过20条会把旧的 condition 删除掉
6. 自定义页面上同
/cc @halo-dev/sig-halo
#### Does this PR introduce a user-facing change?
```release-note
Action Required: 简化 Snapshot 模型的职责,去除版本相关属性
```
pull/2726/head
v2.0.0-beta.2
parent
e48c228fc3
commit
cca95cbfab
|
@ -9,7 +9,6 @@ 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;
|
||||
|
@ -151,11 +150,11 @@ public class ExtensionConfiguration {
|
|||
@Bean
|
||||
Controller postController(ExtensionClient client, ContentService contentService,
|
||||
PostPermalinkPolicy postPermalinkPolicy, CounterService counterService,
|
||||
PostService postService) {
|
||||
PostService postService, ApplicationContext applicationContext) {
|
||||
return new ControllerBuilder("post", client)
|
||||
.reconciler(new PostReconciler(client, contentService, postService,
|
||||
postPermalinkPolicy,
|
||||
counterService))
|
||||
counterService, applicationContext))
|
||||
.extension(new Post())
|
||||
// TODO Make it configurable
|
||||
.workerCount(10)
|
||||
|
@ -204,10 +203,10 @@ public class ExtensionConfiguration {
|
|||
@Bean
|
||||
Controller singlePageController(ExtensionClient client, ContentService contentService,
|
||||
ApplicationContext applicationContext, CounterService counterService,
|
||||
SinglePageService singlePageService, ExternalUrlSupplier externalUrlSupplier) {
|
||||
ExternalUrlSupplier externalUrlSupplier) {
|
||||
return new ControllerBuilder("single-page", client)
|
||||
.reconciler(new SinglePageReconciler(client, contentService,
|
||||
applicationContext, singlePageService, counterService, externalUrlSupplier)
|
||||
applicationContext, counterService, externalUrlSupplier)
|
||||
)
|
||||
.extension(new SinglePage())
|
||||
.build();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.HashMap;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
import run.halo.app.extension.Metadata;
|
||||
|
@ -17,31 +18,23 @@ public record ContentRequest(@Schema(required = true) Ref subjectRef,
|
|||
@Schema(required = true) String rawType) {
|
||||
|
||||
public Snapshot toSnapshot() {
|
||||
Snapshot snapshot = new Snapshot();
|
||||
final Snapshot snapshot = new Snapshot();
|
||||
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName(defaultName(subjectRef));
|
||||
metadata.setAnnotations(new HashMap<>());
|
||||
snapshot.setMetadata(metadata);
|
||||
|
||||
Snapshot.SnapShotSpec snapShotSpec = new Snapshot.SnapShotSpec();
|
||||
snapShotSpec.setSubjectRef(subjectRef);
|
||||
snapShotSpec.setVersion(1);
|
||||
|
||||
snapShotSpec.setRawType(rawType);
|
||||
snapShotSpec.setRawPatch(StringUtils.defaultString(raw()));
|
||||
snapShotSpec.setContentPatch(StringUtils.defaultString(content()));
|
||||
String displayVersion = Snapshot.displayVersionFrom(snapShotSpec.getVersion());
|
||||
snapShotSpec.setDisplayVersion(displayVersion);
|
||||
|
||||
snapshot.setSpec(snapShotSpec);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private String defaultName(Ref subjectRef) {
|
||||
// example: Post-apost-v1-snapshot
|
||||
return String.join("-", subjectRef.getKind(),
|
||||
subjectRef.getName(), "v1", "snapshot");
|
||||
}
|
||||
|
||||
public String rawPatchFrom(String originalRaw) {
|
||||
// originalRaw content from v1
|
||||
return PatchUtils.diffToJsonPatch(originalRaw, this.raw);
|
||||
|
|
|
@ -17,15 +17,13 @@ public interface ContentService {
|
|||
|
||||
Mono<ContentWrapper> draftContent(ContentRequest content);
|
||||
|
||||
Mono<ContentWrapper> updateContent(ContentRequest content);
|
||||
Mono<ContentWrapper> draftContent(ContentRequest content, String parentName);
|
||||
|
||||
Mono<ContentWrapper> publish(String headSnapshotName, Ref subjectRef);
|
||||
Mono<ContentWrapper> updateContent(ContentRequest content);
|
||||
|
||||
Mono<Snapshot> getBaseSnapshot(Ref subjectRef);
|
||||
|
||||
Mono<Snapshot> latestSnapshotVersion(Ref subjectRef);
|
||||
|
||||
Mono<Snapshot> latestPublishedSnapshot(Ref subjectRef);
|
||||
|
||||
Flux<Snapshot> listSnapshots(Ref subjectRef);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import lombok.Data;
|
|||
@Builder
|
||||
public class ContentWrapper {
|
||||
private String snapshotName;
|
||||
private Integer version;
|
||||
private String raw;
|
||||
private String content;
|
||||
private String rawType;
|
||||
|
|
|
@ -17,6 +17,4 @@ public interface PostService {
|
|||
Mono<Post> draftPost(PostRequest postRequest);
|
||||
|
||||
Mono<Post> updatePost(PostRequest postRequest);
|
||||
|
||||
Mono<Post> publishPost(String postName);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,4 @@ public interface SinglePageService {
|
|||
Mono<SinglePage> draft(SinglePageRequest pageRequest);
|
||||
|
||||
Mono<SinglePage> update(SinglePageRequest pageRequest);
|
||||
|
||||
Mono<SinglePage> publish(String name);
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ 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 java.util.function.Function;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.thymeleaf.util.StringUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.ContentRequest;
|
||||
|
@ -28,10 +28,6 @@ import run.halo.app.extension.Ref;
|
|||
*/
|
||||
@Component
|
||||
public class ContentServiceImpl implements ContentService {
|
||||
private static final Comparator<Snapshot> SNAPSHOT_COMPARATOR =
|
||||
Comparator.comparing(snapshot -> snapshot.getSpec().getVersion());
|
||||
public static Comparator<Snapshot> LATEST_SNAPSHOT_COMPARATOR = SNAPSHOT_COMPARATOR.reversed();
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public ContentServiceImpl(ReactiveExtensionClient client) {
|
||||
|
@ -46,61 +42,56 @@ public class ContentServiceImpl implements ContentService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentWrapper> draftContent(ContentRequest contentRequest) {
|
||||
return getContextUsername()
|
||||
.flatMap(username -> {
|
||||
// create snapshot
|
||||
Snapshot snapshot = contentRequest.toSnapshot();
|
||||
snapshot.addContributor(username);
|
||||
return client.create(snapshot)
|
||||
.flatMap(this::restoredContent);
|
||||
});
|
||||
public Mono<ContentWrapper> draftContent(ContentRequest content) {
|
||||
return this.draftContent(content, content.headSnapshotName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentWrapper> draftContent(ContentRequest contentRequest, String parentName) {
|
||||
return Mono.defer(
|
||||
() -> {
|
||||
Snapshot snapshot = contentRequest.toSnapshot();
|
||||
snapshot.getMetadata().setName(UUID.randomUUID().toString());
|
||||
snapshot.getSpec().setParentSnapshotName(parentName);
|
||||
return getBaseSnapshot(contentRequest.subjectRef())
|
||||
.defaultIfEmpty(snapshot)
|
||||
.map(baseSnapshot -> determineRawAndContentPatch(snapshot, baseSnapshot,
|
||||
contentRequest))
|
||||
.flatMap(source -> getContextUsername()
|
||||
.map(username -> {
|
||||
Snapshot.addContributor(source, username);
|
||||
source.getSpec().setOwner(username);
|
||||
return source;
|
||||
})
|
||||
.defaultIfEmpty(source)
|
||||
);
|
||||
})
|
||||
.flatMap(snapshot -> client.create(snapshot)
|
||||
.flatMap(this::restoredContent));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentWrapper> updateContent(ContentRequest contentRequest) {
|
||||
Assert.notNull(contentRequest, "The contentRequest must not be null");
|
||||
Assert.notNull(contentRequest.headSnapshotName(), "The headSnapshotName must not be null");
|
||||
return Mono.zip(getContextUsername(),
|
||||
client.fetch(Snapshot.class, contentRequest.headSnapshotName()))
|
||||
.flatMap(tuple -> {
|
||||
String username = tuple.getT1();
|
||||
Snapshot headSnapShot = tuple.getT2();
|
||||
return handleSnapshot(headSnapShot, contentRequest, username);
|
||||
})
|
||||
Ref subjectRef = contentRequest.subjectRef();
|
||||
return client.fetch(Snapshot.class, contentRequest.headSnapshotName())
|
||||
.flatMap(headSnapshot -> getBaseSnapshot(subjectRef)
|
||||
.map(baseSnapshot -> determineRawAndContentPatch(headSnapshot, baseSnapshot,
|
||||
contentRequest)
|
||||
)
|
||||
)
|
||||
.flatMap(headSnapshot -> getContextUsername()
|
||||
.map(username -> {
|
||||
Snapshot.addContributor(headSnapshot, username);
|
||||
return headSnapshot;
|
||||
})
|
||||
.defaultIfEmpty(headSnapshot)
|
||||
)
|
||||
.flatMap(client::update)
|
||||
.flatMap(this::restoredContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentWrapper> publish(String headSnapshotName, Ref subjectRef) {
|
||||
Assert.notNull(headSnapshotName, "The headSnapshotName must not be null");
|
||||
return client.fetch(Snapshot.class, headSnapshotName)
|
||||
.flatMap(snapshot -> {
|
||||
if (snapshot.isPublished()) {
|
||||
// there is nothing to publish
|
||||
return restoredContent(snapshot.getMetadata().getName(),
|
||||
subjectRef);
|
||||
}
|
||||
Map<String, String> labels = ExtensionUtil.nullSafeLabels(snapshot);
|
||||
Snapshot.putPublishedLabel(labels);
|
||||
Snapshot.SnapShotSpec snapshotSpec = snapshot.getSpec();
|
||||
snapshotSpec.setPublishTime(Instant.now());
|
||||
snapshotSpec.setDisplayVersion(
|
||||
Snapshot.displayVersionFrom(snapshotSpec.getVersion()));
|
||||
return client.update(snapshot)
|
||||
.then(Mono.defer(
|
||||
() -> restoredContent(snapshot.getMetadata().getName(), subjectRef))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<ContentWrapper> restoredContent(String snapshotName,
|
||||
Ref subjectRef) {
|
||||
return getBaseSnapshot(subjectRef)
|
||||
.flatMap(baseSnapshot -> client.fetch(Snapshot.class, snapshotName)
|
||||
.map(snapshot -> snapshot.applyPatch(baseSnapshot)));
|
||||
}
|
||||
|
||||
private Mono<ContentWrapper> restoredContent(Snapshot headSnapshot) {
|
||||
return getBaseSnapshot(headSnapshot.getSpec().getSubjectRef())
|
||||
.map(headSnapshot::applyPatch);
|
||||
|
@ -109,78 +100,17 @@ public class ContentServiceImpl implements ContentService {
|
|||
@Override
|
||||
public Mono<Snapshot> getBaseSnapshot(Ref subjectRef) {
|
||||
return listSnapshots(subjectRef)
|
||||
.filter(snapshot -> snapshot.getSpec().getVersion() == 1)
|
||||
.sort(createTimeReversedComparator().reversed())
|
||||
.filter(p -> StringUtils.equals(Boolean.TRUE.toString(),
|
||||
ExtensionUtil.nullSafeAnnotations(p).get(Snapshot.KEEP_RAW_ANNO)))
|
||||
.next();
|
||||
}
|
||||
|
||||
private Mono<Snapshot> handleSnapshot(Snapshot headSnapshot, ContentRequest contentRequest,
|
||||
String username) {
|
||||
Ref subjectRef = contentRequest.subjectRef();
|
||||
return getBaseSnapshot(subjectRef).flatMap(baseSnapshot -> {
|
||||
String baseSnapshotName = baseSnapshot.getMetadata().getName();
|
||||
return latestPublishedSnapshot(subjectRef)
|
||||
.flatMap(latestReleasedSnapshot -> {
|
||||
Snapshot newSnapshot = contentRequest.toSnapshot();
|
||||
newSnapshot.getSpec().setSubjectRef(subjectRef);
|
||||
newSnapshot.addContributor(username);
|
||||
// has released snapshot, there are 3 assumptions:
|
||||
// if headPtr != releasePtr && head is not published, then update its content
|
||||
// directly
|
||||
// if headPtr != releasePtr && head is published, then create a new snapshot
|
||||
// if headPtr == releasePtr, then create a new snapshot too
|
||||
return latestSnapshotVersion(subjectRef)
|
||||
.flatMap(latestSnapshot -> {
|
||||
String headSnapshotName = contentRequest.headSnapshotName();
|
||||
newSnapshot.getSpec()
|
||||
.setVersion(latestSnapshot.getSpec().getVersion() + 1);
|
||||
newSnapshot.getSpec().setDisplayVersion(
|
||||
Snapshot.displayVersionFrom(newSnapshot.getSpec().getVersion()));
|
||||
newSnapshot.getSpec()
|
||||
.setParentSnapshotName(headSnapshotName);
|
||||
// head is published or headPtr == releasePtr
|
||||
String releasedSnapshotName =
|
||||
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);
|
||||
}
|
||||
|
||||
// otherwise update its content directly
|
||||
return updateRawAndContentToHeadSnapshot(headSnapshot, baseSnapshotName,
|
||||
contentRequest);
|
||||
});
|
||||
})
|
||||
// no released snapshot, indicating v1 now, just update the content directly
|
||||
.switchIfEmpty(Mono.defer(
|
||||
() -> updateRawAndContentToHeadSnapshot(headSnapshot, baseSnapshotName,
|
||||
contentRequest)));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Snapshot> latestSnapshotVersion(Ref subjectRef) {
|
||||
Assert.notNull(subjectRef, "The subjectRef must not be null.");
|
||||
return listSnapshots(subjectRef)
|
||||
.sort(LATEST_SNAPSHOT_COMPARATOR)
|
||||
.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Snapshot> latestPublishedSnapshot(Ref subjectRef) {
|
||||
Assert.notNull(subjectRef, "The subjectRef must not be null.");
|
||||
return listSnapshots(subjectRef)
|
||||
.filter(Snapshot::isPublished)
|
||||
.sort(LATEST_SNAPSHOT_COMPARATOR)
|
||||
.sort(createTimeReversedComparator())
|
||||
.next();
|
||||
}
|
||||
|
||||
|
@ -197,41 +127,22 @@ public class ContentServiceImpl implements ContentService {
|
|||
.map(Principal::getName);
|
||||
}
|
||||
|
||||
private Mono<Snapshot> updateRawAndContentToHeadSnapshot(Snapshot snapshotToUpdate,
|
||||
String baseSnapshotName,
|
||||
ContentRequest contentRequest) {
|
||||
return client.fetch(Snapshot.class, baseSnapshotName)
|
||||
.flatMap(baseSnapshot -> {
|
||||
determineRawAndContentPatch(snapshotToUpdate,
|
||||
baseSnapshot, contentRequest);
|
||||
return client.update(snapshotToUpdate)
|
||||
.thenReturn(snapshotToUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Snapshot> createNewSnapshot(Snapshot snapshotToCreate,
|
||||
String baseSnapshotName,
|
||||
ContentRequest contentRequest) {
|
||||
return client.fetch(Snapshot.class, baseSnapshotName)
|
||||
.flatMap(baseSnapshot -> {
|
||||
determineRawAndContentPatch(snapshotToCreate,
|
||||
baseSnapshot, contentRequest);
|
||||
snapshotToCreate.getMetadata().setName(UUID.randomUUID().toString());
|
||||
snapshotToCreate.getSpec().setSubjectRef(contentRequest.subjectRef());
|
||||
return client.create(snapshotToCreate)
|
||||
.thenReturn(snapshotToCreate);
|
||||
});
|
||||
}
|
||||
|
||||
private void determineRawAndContentPatch(Snapshot snapshotToUse, Snapshot baseSnapshot,
|
||||
private Snapshot determineRawAndContentPatch(Snapshot snapshotToUse, Snapshot baseSnapshot,
|
||||
ContentRequest contentRequest) {
|
||||
Assert.notNull(baseSnapshot, "The baseSnapshot must not be null.");
|
||||
Assert.notNull(contentRequest, "The contentRequest must not be null.");
|
||||
Assert.notNull(snapshotToUse, "The snapshotToUse not be null.");
|
||||
String originalRaw = baseSnapshot.getSpec().getRawPatch();
|
||||
String originalContent = baseSnapshot.getSpec().getContentPatch();
|
||||
String baseSnapshotName = baseSnapshot.getMetadata().getName();
|
||||
|
||||
snapshotToUse.getSpec().setLastModifyTime(Instant.now());
|
||||
// it is the v1 snapshot, set the content directly
|
||||
if (snapshotToUse.getSpec().getVersion() == 1) {
|
||||
if (StringUtils.equals(baseSnapshotName, snapshotToUse.getMetadata().getName())) {
|
||||
snapshotToUse.getSpec().setRawPatch(contentRequest.raw());
|
||||
snapshotToUse.getSpec().setContentPatch(contentRequest.content());
|
||||
ExtensionUtil.nullSafeAnnotations(snapshotToUse)
|
||||
.put(Snapshot.KEEP_RAW_ANNO, Boolean.TRUE.toString());
|
||||
} else {
|
||||
// otherwise diff a patch based on the v1 snapshot
|
||||
String revisedRaw = contentRequest.rawPatchFrom(originalRaw);
|
||||
|
@ -239,5 +150,15 @@ public class ContentServiceImpl implements ContentService {
|
|||
snapshotToUse.getSpec().setRawPatch(revisedRaw);
|
||||
snapshotToUse.getSpec().setContentPatch(revisedContent);
|
||||
}
|
||||
return snapshotToUse;
|
||||
}
|
||||
|
||||
Comparator<Snapshot> createTimeReversedComparator() {
|
||||
Function<Snapshot, String> name = snapshot -> snapshot.getMetadata().getName();
|
||||
Function<Snapshot, Instant> createTime = snapshot -> snapshot.getMetadata()
|
||||
.getCreationTimestamp();
|
||||
return Comparator.comparing(createTime)
|
||||
.thenComparing(name)
|
||||
.reversed();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
|
@ -21,7 +22,9 @@ import org.springframework.util.Assert;
|
|||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.content.ContentRequest;
|
||||
import run.halo.app.content.ContentService;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.Contributor;
|
||||
import run.halo.app.content.ListedPost;
|
||||
import run.halo.app.content.PostQuery;
|
||||
|
@ -32,7 +35,6 @@ import run.halo.app.content.Stats;
|
|||
import run.halo.app.core.extension.Category;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
import run.halo.app.core.extension.Tag;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.extension.ListResult;
|
||||
|
@ -40,8 +42,6 @@ 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;
|
||||
|
||||
|
@ -53,18 +53,12 @@ import run.halo.app.metrics.MeterUtils;
|
|||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class PostServiceImpl implements PostService {
|
||||
private final ContentService contentService;
|
||||
private final ReactiveExtensionClient client;
|
||||
private final CounterService counterService;
|
||||
|
||||
public PostServiceImpl(ContentService contentService, ReactiveExtensionClient client,
|
||||
CounterService counterService) {
|
||||
this.contentService = contentService;
|
||||
this.client = client;
|
||||
this.counterService = counterService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<ListedPost>> listPost(PostQuery query) {
|
||||
Comparator<Post> comparator =
|
||||
|
@ -239,31 +233,79 @@ public class PostServiceImpl implements PostService {
|
|||
|
||||
@Override
|
||||
public Mono<Post> draftPost(PostRequest postRequest) {
|
||||
return contentService.draftContent(postRequest.contentRequest())
|
||||
.flatMap(contentWrapper -> getContextUsername()
|
||||
.flatMap(username -> {
|
||||
return Mono.defer(
|
||||
() -> {
|
||||
Post post = postRequest.post();
|
||||
post.getSpec().setBaseSnapshot(contentWrapper.getSnapshotName());
|
||||
post.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());
|
||||
post.getSpec().setOwner(username);
|
||||
appendPublishedCondition(post, Post.PostPhase.DRAFT);
|
||||
return client.create(post)
|
||||
.then(Mono.defer(() ->
|
||||
client.fetch(Post.class, postRequest.post().getMetadata().getName())));
|
||||
}));
|
||||
return getContextUsername()
|
||||
.map(username -> {
|
||||
post.getSpec().setOwner(username);
|
||||
return post;
|
||||
})
|
||||
.defaultIfEmpty(post);
|
||||
}
|
||||
)
|
||||
.flatMap(client::create)
|
||||
.flatMap(post -> {
|
||||
var contentRequest =
|
||||
new ContentRequest(Ref.of(post), post.getSpec().getHeadSnapshot(),
|
||||
postRequest.content().raw(), postRequest.content().content(),
|
||||
postRequest.content().rawType());
|
||||
return contentService.draftContent(contentRequest)
|
||||
.flatMap(contentWrapper -> waitForPostToDraftConcludingWork(
|
||||
post.getMetadata().getName(),
|
||||
contentWrapper)
|
||||
);
|
||||
})
|
||||
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance));
|
||||
}
|
||||
|
||||
private Mono<Post> waitForPostToDraftConcludingWork(String postName,
|
||||
ContentWrapper contentWrapper) {
|
||||
return client.fetch(Post.class, postName)
|
||||
.flatMap(post -> {
|
||||
post.getSpec().setBaseSnapshot(contentWrapper.getSnapshotName());
|
||||
post.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());
|
||||
if (Objects.equals(true, post.getSpec().getPublish())) {
|
||||
post.getSpec().setReleaseSnapshot(post.getSpec().getHeadSnapshot());
|
||||
}
|
||||
Condition condition = Condition.builder()
|
||||
.type(Post.PostPhase.DRAFT.name())
|
||||
.reason("DraftedSuccessfully")
|
||||
.message("Drafted post successfully.")
|
||||
.status(ConditionStatus.TRUE)
|
||||
.lastTransitionTime(Instant.now())
|
||||
.build();
|
||||
Post.PostStatus status = post.getStatusOrDefault();
|
||||
status.setPhase(Post.PostPhase.DRAFT.name());
|
||||
status.getConditionsOrDefault().addAndEvictFIFO(condition);
|
||||
return client.update(post);
|
||||
})
|
||||
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Post> updatePost(PostRequest postRequest) {
|
||||
Post post = postRequest.post();
|
||||
String headSnapshot = post.getSpec().getHeadSnapshot();
|
||||
String releaseSnapshot = post.getSpec().getReleaseSnapshot();
|
||||
|
||||
if (StringUtils.equals(releaseSnapshot, headSnapshot)) {
|
||||
// create new snapshot to update first
|
||||
return contentService.draftContent(postRequest.contentRequest(), headSnapshot)
|
||||
.flatMap(contentWrapper -> {
|
||||
post.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());
|
||||
return client.update(post);
|
||||
});
|
||||
}
|
||||
return contentService.updateContent(postRequest.contentRequest())
|
||||
.flatMap(contentWrapper -> {
|
||||
post.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());
|
||||
return client.update(post);
|
||||
})
|
||||
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
||||
.filter(throwable -> throwable instanceof OptimisticLockingFailureException))
|
||||
.then(Mono.defer(() -> client.fetch(Post.class, post.getMetadata().getName())));
|
||||
.filter(throwable -> throwable instanceof OptimisticLockingFailureException));
|
||||
}
|
||||
|
||||
private Mono<String> getContextUsername() {
|
||||
|
@ -271,66 +313,4 @@ public class PostServiceImpl implements PostService {
|
|||
.map(SecurityContext::getAuthentication)
|
||||
.map(Principal::getName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Post> publishPost(String postName) {
|
||||
return client.fetch(Post.class, postName)
|
||||
.filter(post -> Objects.equals(true, post.getSpec().getPublish()))
|
||||
.flatMap(post -> {
|
||||
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 (StringUtils.isBlank(postSpec.getReleaseSnapshot())) {
|
||||
postSpec.setReleaseSnapshot(postSpec.getHeadSnapshot());
|
||||
postSpec.setVersion(0);
|
||||
}
|
||||
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);
|
||||
});
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> Mono.error(new NotFoundException(
|
||||
String.format("Snapshot [%s] not found", postSpec.getReleaseSnapshot()))))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void appendPublishedCondition(Post post, Post.PostPhase phase) {
|
||||
Assert.notNull(post, "The post must not be null.");
|
||||
Post.PostStatus status = post.getStatusOrDefault();
|
||||
status.setPhase(phase.name());
|
||||
List<Condition> conditions = status.getConditionsOrDefault();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,10 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
|
@ -21,7 +23,9 @@ import org.springframework.util.Assert;
|
|||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.content.ContentRequest;
|
||||
import run.halo.app.content.ContentService;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.Contributor;
|
||||
import run.halo.app.content.ListedSinglePage;
|
||||
import run.halo.app.content.SinglePageQuery;
|
||||
|
@ -32,15 +36,12 @@ import run.halo.app.content.Stats;
|
|||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.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;
|
||||
|
||||
|
@ -52,6 +53,7 @@ import run.halo.app.metrics.MeterUtils;
|
|||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class SinglePageServiceImpl implements SinglePageService {
|
||||
private final ContentService contentService;
|
||||
|
||||
|
@ -59,12 +61,7 @@ public class SinglePageServiceImpl implements SinglePageService {
|
|||
|
||||
private final CounterService counterService;
|
||||
|
||||
public SinglePageServiceImpl(ContentService contentService, ReactiveExtensionClient client,
|
||||
CounterService counterService) {
|
||||
this.contentService = contentService;
|
||||
this.client = client;
|
||||
this.counterService = counterService;
|
||||
}
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<ListedSinglePage>> list(SinglePageQuery query) {
|
||||
|
@ -86,77 +83,80 @@ public class SinglePageServiceImpl implements SinglePageService {
|
|||
|
||||
@Override
|
||||
public Mono<SinglePage> draft(SinglePageRequest pageRequest) {
|
||||
return contentService.draftContent(pageRequest.contentRequest())
|
||||
.flatMap(contentWrapper -> getContextUsername()
|
||||
.flatMap(username -> {
|
||||
return Mono.defer(
|
||||
() -> {
|
||||
SinglePage page = pageRequest.page();
|
||||
page.getSpec().setBaseSnapshot(contentWrapper.getSnapshotName());
|
||||
page.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());
|
||||
page.getSpec().setOwner(username);
|
||||
appendPublishedCondition(page, Post.PostPhase.DRAFT);
|
||||
return client.create(page)
|
||||
.then(Mono.defer(() ->
|
||||
client.fetch(SinglePage.class,
|
||||
pageRequest.page().getMetadata().getName())));
|
||||
}));
|
||||
return getContextUsername()
|
||||
.map(username -> {
|
||||
page.getSpec().setOwner(username);
|
||||
return page;
|
||||
})
|
||||
.defaultIfEmpty(page);
|
||||
}
|
||||
)
|
||||
.flatMap(client::create)
|
||||
.flatMap(page -> {
|
||||
var contentRequest =
|
||||
new ContentRequest(Ref.of(page), page.getSpec().getHeadSnapshot(),
|
||||
pageRequest.content().raw(), pageRequest.content().content(),
|
||||
pageRequest.content().rawType());
|
||||
return contentService.draftContent(contentRequest)
|
||||
.flatMap(
|
||||
contentWrapper -> waitForPageToDraftConcludingWork(
|
||||
page.getMetadata().getName(),
|
||||
contentWrapper
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<SinglePage> waitForPageToDraftConcludingWork(String pageName,
|
||||
ContentWrapper contentWrapper) {
|
||||
return client.fetch(SinglePage.class, pageName)
|
||||
.flatMap(page -> {
|
||||
page.getSpec().setBaseSnapshot(contentWrapper.getSnapshotName());
|
||||
page.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());
|
||||
if (Objects.equals(true, page.getSpec().getPublish())) {
|
||||
page.getSpec().setReleaseSnapshot(page.getSpec().getHeadSnapshot());
|
||||
}
|
||||
Condition condition = Condition.builder()
|
||||
.type(Post.PostPhase.DRAFT.name())
|
||||
.reason("DraftedSuccessfully")
|
||||
.message("Drafted page successfully")
|
||||
.status(ConditionStatus.TRUE)
|
||||
.lastTransitionTime(Instant.now())
|
||||
.build();
|
||||
SinglePage.SinglePageStatus status = page.getStatusOrDefault();
|
||||
status.getConditionsOrDefault().addAndEvictFIFO(condition);
|
||||
status.setPhase(Post.PostPhase.DRAFT.name());
|
||||
return client.update(page);
|
||||
})
|
||||
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SinglePage> update(SinglePageRequest pageRequest) {
|
||||
SinglePage page = pageRequest.page();
|
||||
String headSnapshot = page.getSpec().getHeadSnapshot();
|
||||
String releaseSnapshot = page.getSpec().getReleaseSnapshot();
|
||||
|
||||
// create new snapshot to update first
|
||||
if (StringUtils.equals(headSnapshot, releaseSnapshot)) {
|
||||
return contentService.draftContent(pageRequest.contentRequest(), headSnapshot)
|
||||
.flatMap(contentWrapper -> {
|
||||
page.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());
|
||||
return client.update(page);
|
||||
});
|
||||
}
|
||||
return contentService.updateContent(pageRequest.contentRequest())
|
||||
.flatMap(contentWrapper -> {
|
||||
page.getSpec().setHeadSnapshot(contentWrapper.getSnapshotName());
|
||||
return client.update(page);
|
||||
})
|
||||
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
||||
.filter(throwable -> throwable instanceof OptimisticLockingFailureException))
|
||||
.then(Mono.defer(() -> client.fetch(SinglePage.class, page.getMetadata().getName())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SinglePage> publish(String name) {
|
||||
return client.fetch(SinglePage.class, name)
|
||||
.filter(page -> Objects.equals(true, page.getSpec().getPublish()))
|
||||
.flatMap(page -> {
|
||||
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 (StringUtils.isBlank(spec.getReleaseSnapshot())) {
|
||||
spec.setReleaseSnapshot(spec.getHeadSnapshot());
|
||||
// first-time to publish reset version to 0
|
||||
spec.setVersion(0);
|
||||
}
|
||||
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);
|
||||
});
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> Mono.error(new NotFoundException(
|
||||
String.format("Snapshot [%s] not found", spec.getReleaseSnapshot()))))
|
||||
);
|
||||
});
|
||||
.filter(throwable -> throwable instanceof OptimisticLockingFailureException));
|
||||
}
|
||||
|
||||
private Mono<String> getContextUsername() {
|
||||
|
@ -285,23 +285,4 @@ public class SinglePageServiceImpl implements SinglePageService {
|
|||
}
|
||||
return right.stream().anyMatch(left::contains);
|
||||
}
|
||||
|
||||
void appendPublishedCondition(SinglePage page, Post.PostPhase phase) {
|
||||
Assert.notNull(page, "The singlePage must not be null.");
|
||||
SinglePage.SinglePageStatus status = page.getStatusOrDefault();
|
||||
status.setPhase(phase.name());
|
||||
List<Condition> conditions = status.getConditionsOrDefault();
|
||||
Condition condition = new Condition();
|
||||
conditions.add(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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import static java.lang.Boolean.parseBoolean;
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
@ -16,7 +15,7 @@ import run.halo.app.extension.AbstractExtension;
|
|||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
import run.halo.app.infra.Condition;
|
||||
import run.halo.app.infra.ConditionList;
|
||||
|
||||
/**
|
||||
* <p>Post extension.</p>
|
||||
|
@ -33,6 +32,8 @@ import run.halo.app.infra.Condition;
|
|||
public class Post extends AbstractExtension {
|
||||
public static final String KIND = "Post";
|
||||
public static final String CATEGORIES_ANNO = "content.halo.run/categories";
|
||||
public static final String LAST_RELEASED_SNAPSHOT_ANNO =
|
||||
"content.halo.run/last-released-snapshot";
|
||||
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";
|
||||
|
@ -113,9 +114,6 @@ public class Post extends AbstractExtension {
|
|||
@Schema(required = true, defaultValue = "PUBLIC")
|
||||
private VisibleEnum visible;
|
||||
|
||||
@Schema(required = true, defaultValue = "1")
|
||||
private Integer version;
|
||||
|
||||
@Schema(required = true, defaultValue = "0")
|
||||
private Integer priority;
|
||||
|
||||
|
@ -135,7 +133,7 @@ public class Post extends AbstractExtension {
|
|||
private String phase;
|
||||
|
||||
@Schema
|
||||
private List<Condition> conditions;
|
||||
private ConditionList conditions;
|
||||
|
||||
private String permalink;
|
||||
|
||||
|
@ -147,12 +145,10 @@ public class Post extends AbstractExtension {
|
|||
|
||||
private List<String> contributors;
|
||||
|
||||
private List<String> releasedSnapshots;
|
||||
|
||||
@JsonIgnore
|
||||
public List<Condition> getConditionsOrDefault() {
|
||||
public ConditionList getConditionsOrDefault() {
|
||||
if (this.conditions == null) {
|
||||
this.conditions = new ArrayList<>();
|
||||
this.conditions = new ConditionList();
|
||||
}
|
||||
return conditions;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ 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 LAST_RELEASED_SNAPSHOT_ANNO =
|
||||
"content.halo.run/last-released-snapshot";
|
||||
public static final String OWNER_LABEL = "content.halo.run/owner";
|
||||
public static final String VISIBLE_LABEL = "content.halo.run/visible";
|
||||
|
||||
|
@ -90,9 +92,6 @@ public class SinglePage extends AbstractExtension {
|
|||
@Schema(required = true, defaultValue = "PUBLIC")
|
||||
private Post.VisibleEnum visible;
|
||||
|
||||
@Schema(required = true, defaultValue = "1")
|
||||
private Integer version;
|
||||
|
||||
@Schema(required = true, defaultValue = "0")
|
||||
private Integer priority;
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@ 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;
|
||||
import lombok.ToString;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.PatchUtils;
|
||||
|
@ -28,7 +28,7 @@ import run.halo.app.extension.Ref;
|
|||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Snapshot extends AbstractExtension {
|
||||
public static final String KIND = "Snapshot";
|
||||
public static final String PUBLISHED_LABEL = "content.halo.run/published";
|
||||
public static final String KEEP_RAW_ANNO = "content.halo.run/keep-raw";
|
||||
|
||||
@Schema(required = true)
|
||||
private SnapShotSpec spec;
|
||||
|
@ -51,50 +51,31 @@ public class Snapshot extends AbstractExtension {
|
|||
|
||||
private String parentSnapshotName;
|
||||
|
||||
@Schema(required = true)
|
||||
private String displayVersion;
|
||||
private Instant lastModifyTime;
|
||||
|
||||
@Schema(required = true, defaultValue = "1")
|
||||
private Integer version;
|
||||
|
||||
private Instant publishTime;
|
||||
@Schema(required = true, minLength = 1)
|
||||
private String owner;
|
||||
|
||||
private Set<String> contributors;
|
||||
|
||||
@JsonIgnore
|
||||
public Set<String> getContributorsOrDefault() {
|
||||
if (this.contributors == null) {
|
||||
this.contributors = new LinkedHashSet<>();
|
||||
}
|
||||
return this.contributors;
|
||||
}
|
||||
}
|
||||
|
||||
public static String displayVersionFrom(Integer version) {
|
||||
Assert.notNull(version, "The version must not be null");
|
||||
return "v" + version;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isPublished() {
|
||||
Map<String, String> labels = getMetadata().getLabels();
|
||||
return labels != null && labels.getOrDefault(PUBLISHED_LABEL, "false").equals("true");
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public void addContributor(String name) {
|
||||
public static void addContributor(Snapshot snapshot, String name) {
|
||||
Assert.notNull(name, "The username must not be null.");
|
||||
Set<String> contributors = spec.getContributorsOrDefault();
|
||||
Set<String> contributors = snapshot.getSpec().getContributors();
|
||||
if (contributors == null) {
|
||||
contributors = new LinkedHashSet<>();
|
||||
snapshot.getSpec().setContributors(contributors);
|
||||
}
|
||||
contributors.add(name);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public ContentWrapper applyPatch(Snapshot baseSnapshot) {
|
||||
Assert.notNull(baseSnapshot, "The baseSnapshot must not be null.");
|
||||
if (this.spec.version == 1) {
|
||||
String baseSnapshotName = baseSnapshot.getMetadata().getName();
|
||||
if (StringUtils.equals(getMetadata().getName(), baseSnapshotName)) {
|
||||
return ContentWrapper.builder()
|
||||
.snapshotName(this.getMetadata().getName())
|
||||
.version(this.spec.version)
|
||||
.raw(this.spec.rawPatch)
|
||||
.content(this.spec.contentPatch)
|
||||
.rawType(this.spec.rawType)
|
||||
|
@ -106,15 +87,9 @@ public class Snapshot extends AbstractExtension {
|
|||
PatchUtils.applyPatch(baseSnapshot.getSpec().getRawPatch(), this.spec.rawPatch);
|
||||
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<String, String> labels) {
|
||||
Assert.notNull(labels, "The labels must not be null.");
|
||||
labels.put(PUBLISHED_LABEL, "true");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,15 +8,18 @@ import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuil
|
|||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import java.time.Duration;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.core.fn.builders.schema.Builder;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.retry.RetryException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.thymeleaf.util.StringUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.content.ListedPost;
|
||||
|
@ -24,9 +27,9 @@ import run.halo.app.content.PostQuery;
|
|||
import run.halo.app.content.PostRequest;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.event.post.PostPublishedEvent;
|
||||
import run.halo.app.event.post.PostRecycledEvent;
|
||||
import run.halo.app.event.post.PostUnpublishedEvent;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.QueryParamBuildUtil;
|
||||
|
@ -37,6 +40,7 @@ import run.halo.app.extension.router.QueryParamBuildUtil;
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class PostEndpoint implements CustomEndpoint {
|
||||
|
@ -92,6 +96,24 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
.response(responseBuilder()
|
||||
.implementation(Post.class))
|
||||
)
|
||||
.PUT("posts/{name}/content", this::updateContent,
|
||||
builder -> builder.operationId("UpdatePostContent")
|
||||
.description("Update a post's content.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(PostRequest.Content.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Post.class))
|
||||
)
|
||||
.PUT("posts/{name}/publish", this::publishPost,
|
||||
builder -> builder.operationId("PublishPost")
|
||||
.description("Publish a post.")
|
||||
|
@ -132,6 +154,18 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
.flatMap(post -> ServerResponse.ok().bodyValue(post));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> updateContent(ServerRequest request) {
|
||||
String postName = request.pathVariable("name");
|
||||
return request.bodyToMono(PostRequest.Content.class)
|
||||
.flatMap(content -> client.fetch(Post.class, postName)
|
||||
.flatMap(post -> {
|
||||
PostRequest postRequest = new PostRequest(post, content);
|
||||
return postService.updatePost(postRequest);
|
||||
})
|
||||
)
|
||||
.flatMap(post -> ServerResponse.ok().bodyValue(post));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> updatePost(ServerRequest request) {
|
||||
return request.bodyToMono(PostRequest.class)
|
||||
.flatMap(postService::updatePost)
|
||||
|
@ -140,22 +174,46 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
|
||||
Mono<ServerResponse> publishPost(ServerRequest request) {
|
||||
var name = request.pathVariable("name");
|
||||
boolean asyncPublish = request.queryParam("async")
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false);
|
||||
return client.get(Post.class, name)
|
||||
.doOnNext(post -> {
|
||||
var spec = post.getSpec();
|
||||
request.queryParam("headSnapshot").ifPresent(spec::setHeadSnapshot);
|
||||
spec.setPublish(true);
|
||||
if (spec.getHeadSnapshot() == null) {
|
||||
spec.setHeadSnapshot(spec.getBaseSnapshot());
|
||||
}
|
||||
// TODO Provide release snapshot query param to control
|
||||
spec.setReleaseSnapshot(spec.getHeadSnapshot());
|
||||
})
|
||||
.flatMap(client::update)
|
||||
.retryWhen(Retry.backoff(3, Duration.ofMillis(100))
|
||||
.filter(t -> t instanceof OptimisticLockingFailureException))
|
||||
.flatMap(post -> postService.publishPost(post.getMetadata().getName()))
|
||||
// TODO Fire published event in reconciler in the future
|
||||
.doOnNext(post -> eventPublisher.publishEvent(
|
||||
new PostPublishedEvent(this, post.getMetadata().getName())))
|
||||
.flatMap(post -> ServerResponse.ok().bodyValue(post));
|
||||
.flatMap(post -> {
|
||||
if (asyncPublish) {
|
||||
return Mono.just(post);
|
||||
}
|
||||
return client.fetch(Post.class, name)
|
||||
.map(latest -> {
|
||||
String latestReleasedSnapshotName =
|
||||
ExtensionUtil.nullSafeAnnotations(latest)
|
||||
.get(Post.LAST_RELEASED_SNAPSHOT_ANNO);
|
||||
if (StringUtils.equals(latestReleasedSnapshotName,
|
||||
latest.getSpec().getReleaseSnapshot())) {
|
||||
return latest;
|
||||
}
|
||||
throw new RetryException("Post publishing status is not as expected");
|
||||
})
|
||||
.retryWhen(Retry.fixedDelay(10, Duration.ofMillis(100))
|
||||
.filter(t -> t instanceof RetryException))
|
||||
.doOnError(IllegalStateException.class, err -> {
|
||||
log.error("Failed to publish post [{}]", name, err);
|
||||
throw new IllegalStateException("Publishing wait timeout.");
|
||||
});
|
||||
})
|
||||
.flatMap(publishResult -> ServerResponse.ok().bodyValue(publishResult));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> unpublishPost(ServerRequest request) {
|
||||
|
|
|
@ -6,20 +6,27 @@ 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 java.time.Duration;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.core.fn.builders.schema.Builder;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.retry.RetryException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.thymeleaf.util.StringUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.content.ListedSinglePage;
|
||||
import run.halo.app.content.SinglePageQuery;
|
||||
import run.halo.app.content.SinglePageRequest;
|
||||
import run.halo.app.content.SinglePageService;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.QueryParamBuildUtil;
|
||||
|
@ -30,6 +37,7 @@ import run.halo.app.extension.router.QueryParamBuildUtil;
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class SinglePageEndpoint implements CustomEndpoint {
|
||||
|
@ -83,6 +91,24 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
|||
.response(responseBuilder()
|
||||
.implementation(SinglePage.class))
|
||||
)
|
||||
.PUT("singlepages/{name}/content", this::updateContent,
|
||||
builder -> builder.operationId("UpdateSinglePageContent")
|
||||
.description("Update a single page's content.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(SinglePageRequest.Content.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Post.class))
|
||||
)
|
||||
.PUT("singlepages/{name}/publish", this::publishSinglePage,
|
||||
builder -> builder.operationId("PublishSinglePage")
|
||||
.description("Publish a single page.")
|
||||
|
@ -103,6 +129,18 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
|||
.flatMap(singlePage -> ServerResponse.ok().bodyValue(singlePage));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> updateContent(ServerRequest request) {
|
||||
String pageName = request.pathVariable("name");
|
||||
return request.bodyToMono(SinglePageRequest.Content.class)
|
||||
.flatMap(content -> client.fetch(SinglePage.class, pageName)
|
||||
.flatMap(page -> {
|
||||
SinglePageRequest pageRequest = new SinglePageRequest(page, content);
|
||||
return singlePageService.update(pageRequest);
|
||||
})
|
||||
)
|
||||
.flatMap(post -> ServerResponse.ok().bodyValue(post));
|
||||
}
|
||||
|
||||
Mono<ServerResponse> updateSinglePage(ServerRequest request) {
|
||||
return request.bodyToMono(SinglePageRequest.class)
|
||||
.flatMap(singlePageService::update)
|
||||
|
@ -111,14 +149,41 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
|||
|
||||
Mono<ServerResponse> publishSinglePage(ServerRequest request) {
|
||||
String name = request.pathVariable("name");
|
||||
boolean asyncPublish = request.queryParam("async")
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false);
|
||||
return client.fetch(SinglePage.class, name)
|
||||
.flatMap(singlePage -> {
|
||||
SinglePage.SinglePageSpec spec = singlePage.getSpec();
|
||||
spec.setPublish(true);
|
||||
if (spec.getHeadSnapshot() == null) {
|
||||
spec.setHeadSnapshot(spec.getBaseSnapshot());
|
||||
}
|
||||
spec.setReleaseSnapshot(spec.getHeadSnapshot());
|
||||
return client.update(singlePage);
|
||||
})
|
||||
.flatMap(singlePage -> singlePageService.publish(singlePage.getMetadata().getName()))
|
||||
.flatMap(post -> {
|
||||
if (asyncPublish) {
|
||||
return Mono.just(post);
|
||||
}
|
||||
return client.fetch(SinglePage.class, name)
|
||||
.map(latest -> {
|
||||
String latestReleasedSnapshotName =
|
||||
ExtensionUtil.nullSafeAnnotations(latest)
|
||||
.get(Post.LAST_RELEASED_SNAPSHOT_ANNO);
|
||||
if (StringUtils.equals(latestReleasedSnapshotName,
|
||||
latest.getSpec().getReleaseSnapshot())) {
|
||||
return latest;
|
||||
}
|
||||
throw new RetryException("SinglePage publishing status is not as expected");
|
||||
})
|
||||
.retryWhen(Retry.fixedDelay(10, Duration.ofMillis(100))
|
||||
.filter(t -> t instanceof RetryException))
|
||||
.doOnError(IllegalStateException.class, err -> {
|
||||
log.error("Failed to publish single page [{}]", name, err);
|
||||
throw new IllegalStateException("Publishing wait timeout.");
|
||||
});
|
||||
})
|
||||
.flatMap(page -> ServerResponse.ok().bodyValue(page));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import lombok.AllArgsConstructor;
|
||||
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.PostService;
|
||||
|
@ -16,12 +17,15 @@ 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.event.post.PostPublishedEvent;
|
||||
import run.halo.app.event.post.PostUnpublishedEvent;
|
||||
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;
|
||||
import run.halo.app.infra.ConditionList;
|
||||
import run.halo.app.infra.ConditionStatus;
|
||||
import run.halo.app.infra.utils.HaloUtils;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
@ -40,6 +44,7 @@ import run.halo.app.metrics.MeterUtils;
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class PostReconciler implements Reconciler<Reconciler.Request> {
|
||||
private static final String FINALIZER_NAME = "post-protection";
|
||||
private final ExtensionClient client;
|
||||
|
@ -48,15 +53,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
private final PostPermalinkPolicy postPermalinkPolicy;
|
||||
private final CounterService counterService;
|
||||
|
||||
public PostReconciler(ExtensionClient client, ContentService contentService,
|
||||
PostService postService, PostPermalinkPolicy postPermalinkPolicy,
|
||||
CounterService counterService) {
|
||||
this.client = client;
|
||||
this.contentService = contentService;
|
||||
this.postService = postService;
|
||||
this.postPermalinkPolicy = postPermalinkPolicy;
|
||||
this.counterService = counterService;
|
||||
}
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
|
@ -77,18 +74,87 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
|
||||
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())) {
|
||||
// un-publish post if necessary
|
||||
if (post.isPublished()
|
||||
&& Objects.equals(false, post.getSpec().getPublish())) {
|
||||
boolean success = unPublishReconcile(name);
|
||||
if (success) {
|
||||
applicationContext.publishEvent(new PostUnpublishedEvent(this, name));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
publishPost(name);
|
||||
} catch (Throwable e) {
|
||||
publishFailed(name, e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void publishPost(String name) {
|
||||
client.fetch(Post.class, name)
|
||||
.filter(post -> Objects.equals(true, post.getSpec().getPublish()))
|
||||
.ifPresent(post -> {
|
||||
Map<String, String> annotations = ExtensionUtil.nullSafeAnnotations(post);
|
||||
String lastReleasedSnapshot = annotations.get(Post.LAST_RELEASED_SNAPSHOT_ANNO);
|
||||
String releaseSnapshot = post.getSpec().getReleaseSnapshot();
|
||||
if (StringUtils.isBlank(releaseSnapshot)) {
|
||||
return;
|
||||
}
|
||||
// do nothing if release snapshot is not changed
|
||||
if (StringUtils.equals(lastReleasedSnapshot, releaseSnapshot)) {
|
||||
return;
|
||||
}
|
||||
Post.PostStatus status = post.getStatusOrDefault();
|
||||
|
||||
// validate release snapshot
|
||||
boolean present = client.fetch(Snapshot.class, releaseSnapshot)
|
||||
.isPresent();
|
||||
if (!present) {
|
||||
Condition condition = Condition.builder()
|
||||
.type(Post.PostPhase.FAILED.name())
|
||||
.reason("SnapshotNotFound")
|
||||
.message(
|
||||
String.format("Snapshot [%s] not found for publish", releaseSnapshot))
|
||||
.status(ConditionStatus.FALSE)
|
||||
.lastTransitionTime(Instant.now())
|
||||
.build();
|
||||
status.getConditionsOrDefault().addAndEvictFIFO(condition);
|
||||
status.setPhase(Post.PostPhase.FAILED.name());
|
||||
client.update(post);
|
||||
return;
|
||||
}
|
||||
// do publish
|
||||
annotations.put(Post.LAST_RELEASED_SNAPSHOT_ANNO, releaseSnapshot);
|
||||
status.setPhase(Post.PostPhase.PUBLISHED.name());
|
||||
Condition condition = Condition.builder()
|
||||
.type(Post.PostPhase.PUBLISHED.name())
|
||||
.reason("Published")
|
||||
.message("Post published successfully.")
|
||||
.lastTransitionTime(Instant.now())
|
||||
.status(ConditionStatus.TRUE)
|
||||
.build();
|
||||
status.getConditionsOrDefault().addAndEvictFIFO(condition);
|
||||
|
||||
Post.changePublishedState(post, true);
|
||||
if (post.getSpec().getPublishTime() == null) {
|
||||
post.getSpec().setPublishTime(Instant.now());
|
||||
}
|
||||
|
||||
client.update(post);
|
||||
applicationContext.publishEvent(new PostPublishedEvent(this, name));
|
||||
});
|
||||
}
|
||||
|
||||
private boolean unPublishReconcile(String name) {
|
||||
return client.fetch(Post.class, name)
|
||||
.map(post -> {
|
||||
final Post oldPost = JsonUtils.deepCopy(post);
|
||||
Post.changePublishedState(post, false);
|
||||
|
||||
final Post.PostStatus status = post.getStatusOrDefault();
|
||||
Condition condition = new Condition();
|
||||
condition.setType("CancelledPublish");
|
||||
|
@ -96,13 +162,15 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
condition.setReason(condition.getType());
|
||||
condition.setMessage("CancelledPublish");
|
||||
condition.setLastTransitionTime(Instant.now());
|
||||
status.getConditionsOrDefault().add(condition);
|
||||
status.getConditionsOrDefault().addAndEvictFIFO(condition);
|
||||
|
||||
status.setPhase(Post.PostPhase.DRAFT.name());
|
||||
}
|
||||
if (!oldPost.equals(post)) {
|
||||
client.update(post);
|
||||
}
|
||||
});
|
||||
if (!oldPost.equals(post)) {
|
||||
client.update(post);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private void publishFailed(String name, Throwable error) {
|
||||
|
@ -115,23 +183,16 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
Post.PostPhase phase = Post.PostPhase.FAILED;
|
||||
status.setPhase(phase.name());
|
||||
|
||||
final List<Condition> 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);
|
||||
final ConditionList conditions = status.getConditionsOrDefault();
|
||||
Condition condition = Condition.builder()
|
||||
.type(phase.name())
|
||||
.reason("PublishFailed")
|
||||
.message(error.getMessage())
|
||||
.status(ConditionStatus.FALSE)
|
||||
.lastTransitionTime(Instant.now())
|
||||
.build();
|
||||
conditions.addAndEvictFIFO(condition);
|
||||
|
||||
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)) {
|
||||
|
@ -221,24 +282,10 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
status.setContributors(contributors);
|
||||
|
||||
// update in progress status
|
||||
snapshots.stream()
|
||||
.filter(
|
||||
snapshot -> snapshot.getMetadata().getName().equals(headSnapshot))
|
||||
.findAny()
|
||||
.ifPresent(snapshot -> {
|
||||
status.setInProgress(!snapshot.isPublished());
|
||||
});
|
||||
|
||||
List<String> releasedSnapshots = snapshots.stream()
|
||||
.filter(Snapshot::isPublished)
|
||||
.sorted(Comparator.comparing(snapshot -> snapshot.getSpec().getVersion()))
|
||||
.map(snapshot -> snapshot.getMetadata().getName())
|
||||
.toList();
|
||||
status.setReleasedSnapshots(releasedSnapshots);
|
||||
status.setInProgress(
|
||||
!StringUtils.equals(headSnapshot, post.getSpec().getReleaseSnapshot()));
|
||||
});
|
||||
|
||||
status.setConditions(limitConditionSize(status.getConditions()));
|
||||
|
||||
if (!oldPost.equals(post)) {
|
||||
client.update(post);
|
||||
}
|
||||
|
@ -298,12 +345,4 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
// TODO The default capture 150 words as excerpt
|
||||
return StringUtils.substring(text, 0, 150);
|
||||
}
|
||||
|
||||
static List<Condition> limitConditionSize(List<Condition> conditions) {
|
||||
if (conditions == null || conditions.size() <= 10) {
|
||||
return conditions;
|
||||
}
|
||||
// Retain the last ten conditions
|
||||
return conditions.subList(conditions.size() - 10, conditions.size());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,19 +4,18 @@ 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.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.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;
|
||||
|
@ -29,6 +28,7 @@ import run.halo.app.extension.GroupVersionKind;
|
|||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.Condition;
|
||||
import run.halo.app.infra.ConditionList;
|
||||
import run.halo.app.infra.ConditionStatus;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
@ -50,29 +50,17 @@ import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
||||
private static final String FINALIZER_NAME = "single-page-protection";
|
||||
private static final GroupVersionKind GVK = GroupVersionKind.fromExtension(SinglePage.class);
|
||||
private final ExtensionClient client;
|
||||
private final ContentService contentService;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final SinglePageService singlePageService;
|
||||
private final CounterService counterService;
|
||||
|
||||
private final ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
public SinglePageReconciler(ExtensionClient client, ContentService contentService,
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(SinglePage.class, request.name())
|
||||
|
@ -94,28 +82,93 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
|
||||
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);
|
||||
// un-publish if necessary
|
||||
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());
|
||||
unPublish(name);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
publishPage(name);
|
||||
} catch (Throwable e) {
|
||||
publishFailed(name, e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void publishPage(String name) {
|
||||
client.fetch(SinglePage.class, name)
|
||||
.filter(page -> Objects.equals(true, page.getSpec().getPublish()))
|
||||
.ifPresent(page -> {
|
||||
Map<String, String> annotations = ExtensionUtil.nullSafeAnnotations(page);
|
||||
String lastReleasedSnapshot = annotations.get(Post.LAST_RELEASED_SNAPSHOT_ANNO);
|
||||
String releaseSnapshot = page.getSpec().getReleaseSnapshot();
|
||||
if (StringUtils.isBlank(releaseSnapshot)) {
|
||||
return;
|
||||
}
|
||||
// do nothing if release snapshot is not changed
|
||||
if (StringUtils.equals(lastReleasedSnapshot, releaseSnapshot)) {
|
||||
return;
|
||||
}
|
||||
SinglePage.SinglePageStatus status = page.getStatusOrDefault();
|
||||
|
||||
// validate release snapshot
|
||||
boolean present = client.fetch(Snapshot.class, releaseSnapshot)
|
||||
.isPresent();
|
||||
if (!present) {
|
||||
Condition condition = Condition.builder()
|
||||
.type(Post.PostPhase.FAILED.name())
|
||||
.reason("SnapshotNotFound")
|
||||
.message(
|
||||
String.format("Snapshot [%s] not found for publish", releaseSnapshot))
|
||||
.status(ConditionStatus.FALSE)
|
||||
.lastTransitionTime(Instant.now())
|
||||
.build();
|
||||
status.getConditionsOrDefault().addAndEvictFIFO(condition);
|
||||
status.setPhase(Post.PostPhase.FAILED.name());
|
||||
client.update(page);
|
||||
return;
|
||||
}
|
||||
|
||||
// do publish
|
||||
annotations.put(SinglePage.LAST_RELEASED_SNAPSHOT_ANNO, releaseSnapshot);
|
||||
status.setPhase(Post.PostPhase.PUBLISHED.name());
|
||||
Condition condition = Condition.builder()
|
||||
.type(Post.PostPhase.PUBLISHED.name())
|
||||
.reason("Published")
|
||||
.message("SinglePage published successfully.")
|
||||
.lastTransitionTime(Instant.now())
|
||||
.status(ConditionStatus.TRUE)
|
||||
.build();
|
||||
status.getConditionsOrDefault().addAndEvictFIFO(condition);
|
||||
|
||||
SinglePage.changePublishedState(page, true);
|
||||
if (page.getSpec().getPublishTime() == null) {
|
||||
page.getSpec().setPublishTime(Instant.now());
|
||||
}
|
||||
|
||||
client.update(page);
|
||||
});
|
||||
}
|
||||
|
||||
private void unPublish(String name) {
|
||||
client.fetch(SinglePage.class, name).ifPresent(page -> {
|
||||
final SinglePage oldPage = JsonUtils.deepCopy(page);
|
||||
|
||||
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().addAndEvictFIFO(condition);
|
||||
|
||||
status.setPhase(Post.PostPhase.DRAFT.name());
|
||||
if (!oldPage.equals(page)) {
|
||||
client.update(page);
|
||||
}
|
||||
|
@ -132,23 +185,16 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
Post.PostPhase phase = Post.PostPhase.FAILED;
|
||||
status.setPhase(phase.name());
|
||||
|
||||
final List<Condition> 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);
|
||||
final ConditionList conditions = status.getConditionsOrDefault();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Condition condition = Condition.builder()
|
||||
.type(phase.name())
|
||||
.reason("PublishFailed")
|
||||
.message(error.getMessage())
|
||||
.lastTransitionTime(Instant.now())
|
||||
.status(ConditionStatus.FALSE)
|
||||
.build();
|
||||
conditions.addAndEvictFIFO(condition);
|
||||
page.setStatus(status);
|
||||
|
||||
if (!oldPage.equals(page)) {
|
||||
|
@ -303,23 +349,10 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
status.setContributors(contributors);
|
||||
|
||||
// update in progress status
|
||||
snapshots.stream()
|
||||
.filter(snapshot -> snapshot.getMetadata().getName().equals(headSnapshot))
|
||||
.findAny()
|
||||
.ifPresent(snapshot -> {
|
||||
status.setInProgress(!snapshot.isPublished());
|
||||
});
|
||||
|
||||
List<String> releasedSnapshots = snapshots.stream()
|
||||
.filter(Snapshot::isPublished)
|
||||
.sorted(Comparator.comparing(snapshot -> snapshot.getSpec().getVersion()))
|
||||
.map(snapshot -> snapshot.getMetadata().getName())
|
||||
.toList();
|
||||
status.setReleasedSnapshots(releasedSnapshots);
|
||||
String releaseSnapshot = singlePage.getSpec().getReleaseSnapshot();
|
||||
status.setInProgress(!StringUtils.equals(releaseSnapshot, headSnapshot));
|
||||
});
|
||||
|
||||
status.setConditions(limitConditionSize(status.getConditions()));
|
||||
|
||||
if (!oldPage.equals(singlePage)) {
|
||||
client.update(singlePage);
|
||||
}
|
||||
|
@ -337,12 +370,4 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
return Objects.equals(true, singlePage.getSpec().getDeleted())
|
||||
|| singlePage.getMetadata().getDeletionTimestamp() != null;
|
||||
}
|
||||
|
||||
static List<Condition> limitConditionSize(List<Condition> conditions) {
|
||||
if (conditions == null || conditions.size() <= 10) {
|
||||
return conditions;
|
||||
}
|
||||
// Retain the last ten conditions
|
||||
return conditions.subList(conditions.size() - 10, conditions.size());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,4 +58,21 @@ public final class ExtensionUtil {
|
|||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets extension metadata annotations null safe.
|
||||
*
|
||||
* @param extension extension must not be null
|
||||
* @return extension metadata annotations
|
||||
*/
|
||||
public static Map<String, String> nullSafeAnnotations(AbstractExtension extension) {
|
||||
Assert.notNull(extension, "The extension must not be null.");
|
||||
Assert.notNull(extension.getMetadata(), "The extension metadata must not be null.");
|
||||
Map<String, String> annotations = extension.getMetadata().getAnnotations();
|
||||
if (annotations == null) {
|
||||
annotations = new HashMap<>();
|
||||
extension.getMetadata().setAnnotations(annotations);
|
||||
}
|
||||
return annotations;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,10 @@ package run.halo.app.infra;
|
|||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.Instant;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -11,6 +14,9 @@ import lombok.Data;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Condition {
|
||||
/**
|
||||
* type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import java.util.AbstractCollection;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
/**
|
||||
* <p>This {@link ConditionList} to stores multiple {@link Condition}.</p>
|
||||
* <p>The element added after is always the first, the first to be removed is always the first to
|
||||
* be added.</p>
|
||||
* <p>The queue head is the one whose element index is 0</p>
|
||||
* Note that: this class is not thread-safe.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ConditionList extends AbstractCollection<Condition> {
|
||||
private static final int EVICT_THRESHOLD = 20;
|
||||
private final Deque<Condition> conditions = new ArrayDeque<>();
|
||||
|
||||
@Override
|
||||
public boolean add(@NonNull Condition condition) {
|
||||
if (isSame(conditions.peekFirst(), condition)) {
|
||||
return false;
|
||||
}
|
||||
return conditions.add(condition);
|
||||
}
|
||||
|
||||
public boolean addFirst(@NonNull Condition condition) {
|
||||
conditions.addFirst(condition);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add {@param #condition} and evict the first item if the size of conditions is greater than
|
||||
* {@link #EVICT_THRESHOLD}.
|
||||
*
|
||||
* @param condition item to add
|
||||
*/
|
||||
public boolean addAndEvictFIFO(@NonNull Condition condition) {
|
||||
return addAndEvictFIFO(condition, EVICT_THRESHOLD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add {@param #condition} and evict the first item if the size of conditions is greater than
|
||||
* {@param evictThreshold}.
|
||||
*
|
||||
* @param condition item to add
|
||||
*/
|
||||
public boolean addAndEvictFIFO(@NonNull Condition condition, int evictThreshold) {
|
||||
boolean result = this.addFirst(condition);
|
||||
while (conditions.size() > evictThreshold) {
|
||||
removeLast();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void remove(Condition condition) {
|
||||
conditions.remove(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves, but does not remove, the head of the queue represented by
|
||||
* this deque (in other words, the first element of this deque), or
|
||||
* returns {@code null} if this deque is empty.
|
||||
*
|
||||
* <p>This method is equivalent to {@link #peekFirst()}.
|
||||
*
|
||||
* @return the head of the queue represented by this deque, or
|
||||
* {@code null} if this deque is empty
|
||||
*/
|
||||
public Condition peek() {
|
||||
return peekFirst();
|
||||
}
|
||||
|
||||
public Condition peekFirst() {
|
||||
return conditions.peekFirst();
|
||||
}
|
||||
|
||||
public Condition removeLast() {
|
||||
return conditions.removeLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
conditions.clear();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return conditions.size();
|
||||
}
|
||||
|
||||
private boolean isSame(Condition a, Condition b) {
|
||||
if (a == null || b == null) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(a.getType(), b.getType())
|
||||
&& Objects.equals(a.getStatus(), b.getStatus())
|
||||
&& Objects.equals(a.getReason(), b.getReason())
|
||||
&& Objects.equals(a.getMessage(), b.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Condition> iterator() {
|
||||
return conditions.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forEach(Consumer<? super Condition> action) {
|
||||
conditions.forEach(action);
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ rules:
|
|||
resources: [ "posts" ]
|
||||
verbs: [ "*" ]
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "posts", "posts/publish", "posts/unpublish", "posts/recycle", "contents", "contents/publish" ]
|
||||
resources: [ "posts", "posts/publish", "posts/unpublish", "posts/recycle", "posts/content", "contents", "contents/publish" ]
|
||||
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
|
|
|
@ -15,7 +15,7 @@ rules:
|
|||
resources: [ "singlepages" ]
|
||||
verbs: [ "*" ]
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "singlepages", "singlepages/publish", "contents", "contents/publish" ]
|
||||
resources: [ "singlepages", "singlepages/publish", "singlepages/content", "contents", "contents/publish" ]
|
||||
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
|
|
|
@ -59,14 +59,13 @@ class ContentRequestTest {
|
|||
},
|
||||
"rawType": "MARKDOWN",
|
||||
"rawPatch": "%s",
|
||||
"contentPatch": "%s",
|
||||
"displayVersion": "v1",
|
||||
"version": 1
|
||||
"contentPatch": "%s"
|
||||
},
|
||||
"apiVersion": "content.halo.run/v1alpha1",
|
||||
"kind": "Snapshot",
|
||||
"metadata": {
|
||||
"name": "7b149646-ac60-4a5c-98ee-78b2dd0631b2"
|
||||
"name": "7b149646-ac60-4a5c-98ee-78b2dd0631b2",
|
||||
"annotations": {}
|
||||
}
|
||||
}
|
||||
""".formatted(expectedRawPatch, expectedContentPath),
|
||||
|
|
|
@ -10,8 +10,8 @@ import static run.halo.app.content.TestPost.snapshotV1;
|
|||
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 java.util.Set;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
@ -25,9 +25,9 @@ import reactor.test.StepVerifier;
|
|||
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.ExtensionUtil;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link ContentService}.
|
||||
|
@ -64,7 +64,6 @@ class ContentServiceTest {
|
|||
pilingBaseSnapshot(snapshotV1);
|
||||
ContentWrapper contentWrapper = ContentWrapper.builder()
|
||||
.snapshotName("snapshot-A")
|
||||
.version(1)
|
||||
.raw(contentRequest.raw())
|
||||
.content(contentRequest.content())
|
||||
.rawType(snapshotV1.getSpec().getRawType())
|
||||
|
@ -81,9 +80,12 @@ class ContentServiceTest {
|
|||
verify(client, times(1)).create(captor.capture());
|
||||
Snapshot snapshot = captor.getValue();
|
||||
|
||||
snapshotV1.getMetadata().setName(snapshot.getMetadata().getName());
|
||||
snapshotV1.getSpec().setSubjectRef(ref);
|
||||
assertThat(snapshot).isEqualTo(snapshotV1);
|
||||
assertThat(snapshot.getMetadata().getName())
|
||||
.isNotEqualTo(snapshotV1.getMetadata().getName());
|
||||
assertThat(snapshot.getSpec().getLastModifyTime()).isNotNull();
|
||||
assertThat(snapshot.getSpec().getOwner()).isEqualTo("guqing");
|
||||
assertThat(snapshot.getSpec().getContributors()).isEqualTo(Set.of("guqing"));
|
||||
assertThat(snapshot.getSpec().getSubjectRef()).isEqualTo(ref);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -109,7 +111,6 @@ class ContentServiceTest {
|
|||
|
||||
ContentWrapper contentWrapper = ContentWrapper.builder()
|
||||
.snapshotName(headSnapshot)
|
||||
.version(1)
|
||||
.raw(contentRequest.raw())
|
||||
.content(contentRequest.content())
|
||||
.rawType(snapshotV1.getSpec().getRawType())
|
||||
|
@ -129,68 +130,21 @@ class ContentServiceTest {
|
|||
assertThat(snapshot).isEqualTo(updated);
|
||||
}
|
||||
|
||||
@Test
|
||||
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);
|
||||
|
||||
when(client.fetch(eq(Snapshot.class), eq(snapshotV2.getMetadata().getName())))
|
||||
.thenReturn(Mono.just(snapshotV2));
|
||||
when(client.fetch(eq(Snapshot.class), eq(snapshotV1.getMetadata().getName())))
|
||||
.thenReturn(Mono.just(snapshotV1));
|
||||
|
||||
final ContentRequest contentRequest =
|
||||
new ContentRequest(ref, headSnapshot, "C",
|
||||
"<p>C</p>", snapshotV1.getSpec().getRawType());
|
||||
|
||||
when(client.create(any())).thenReturn(Mono.just(snapshotV3()));
|
||||
|
||||
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.getRaw()).isEqualTo("C");
|
||||
assertThat(created.getContent()).isEqualTo("<p>C</p>");
|
||||
})
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateContentWhenHeadPoints2Published() {
|
||||
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());
|
||||
ExtensionUtil.nullSafeAnnotations(snapshotV1)
|
||||
.put(Snapshot.KEEP_RAW_ANNO, "true");
|
||||
snapshotV1.getSpec().setSubjectRef(ref);
|
||||
|
||||
Snapshot snapshotV2 = snapshotV2();
|
||||
snapshotV2.getSpec().setSubjectRef(ref);
|
||||
snapshotV2.getSpec().setPublishTime(null);
|
||||
|
||||
final String headSnapshot = snapshotV2.getMetadata().getName();
|
||||
|
||||
|
||||
pilingBaseSnapshot(snapshotV2, snapshotV1);
|
||||
|
||||
when(client.fetch(eq(Snapshot.class), eq(snapshotV2.getMetadata().getName())))
|
||||
|
@ -202,7 +156,7 @@ class ContentServiceTest {
|
|||
new ContentRequest(ref, headSnapshot, "C",
|
||||
"<p>C</p>", snapshotV1.getSpec().getRawType());
|
||||
|
||||
when(client.update(any())).thenReturn(Mono.just(snapshotV2()));
|
||||
when(client.update(any())).thenReturn(Mono.just(snapshotV2));
|
||||
|
||||
StepVerifier.create(contentService.latestSnapshotVersion(ref))
|
||||
.expectNext(snapshotV2)
|
||||
|
@ -220,34 +174,6 @@ class ContentServiceTest {
|
|||
verify(client, times(1)).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishContent() {
|
||||
Ref ref = postRef("test-post");
|
||||
// v1(released),v2
|
||||
Snapshot snapshotV1 = snapshotV1();
|
||||
snapshotV1.getSpec().setPublishTime(null);
|
||||
snapshotV1.getSpec().setSubjectRef(ref);
|
||||
|
||||
final String headSnapshot = snapshotV1.getMetadata().getName();
|
||||
|
||||
pilingBaseSnapshot(snapshotV1);
|
||||
|
||||
when(client.fetch(eq(Snapshot.class), eq(snapshotV1.getMetadata().getName())))
|
||||
.thenReturn(Mono.just(snapshotV1));
|
||||
|
||||
when(client.update(any())).thenReturn(Mono.just(snapshotV2()));
|
||||
|
||||
StepVerifier.create(contentService.publish(headSnapshot, ref))
|
||||
.expectNext()
|
||||
.consumeNextWith(p -> {
|
||||
System.out.println(JsonUtils.objectToJson(p));
|
||||
})
|
||||
.expectComplete()
|
||||
.verify();
|
||||
// has benn published,do nothing
|
||||
verify(client, times(1)).update(any());
|
||||
}
|
||||
|
||||
private static Ref postRef(String name) {
|
||||
Ref ref = new Ref();
|
||||
ref.setGroup("content.halo.run");
|
||||
|
@ -257,37 +183,6 @@ class ContentServiceTest {
|
|||
return ref;
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishContentWhenHasPublishedThenDoNothing() {
|
||||
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(ref);
|
||||
|
||||
final String headSnapshot = snapshotV1.getMetadata().getName();
|
||||
|
||||
pilingBaseSnapshot(snapshotV1);
|
||||
|
||||
when(client.fetch(eq(Snapshot.class), eq(snapshotV1.getMetadata().getName())))
|
||||
.thenReturn(Mono.just(snapshotV1));
|
||||
|
||||
when(client.update(any())).thenReturn(Mono.just(snapshotV2()));
|
||||
|
||||
StepVerifier.create(contentService.publish(headSnapshot, ref))
|
||||
.expectNext()
|
||||
.consumeNextWith(p -> {
|
||||
System.out.println(JsonUtils.objectToJson(p));
|
||||
})
|
||||
.expectComplete()
|
||||
.verify();
|
||||
// has benn published,do nothing
|
||||
verify(client, times(0)).update(any());
|
||||
}
|
||||
|
||||
private void pilingBaseSnapshot(Snapshot... expected) {
|
||||
when(client.list(eq(Snapshot.class), any(), any()))
|
||||
.thenReturn(Flux.just(expected));
|
||||
|
@ -299,8 +194,6 @@ class ContentServiceTest {
|
|||
final Ref ref = postRef(postName);
|
||||
Snapshot snapshotV1 = snapshotV1();
|
||||
snapshotV1.getMetadata().setLabels(new HashMap<>());
|
||||
Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels());
|
||||
snapshotV1.getSpec().setPublishTime(Instant.now());
|
||||
snapshotV1.getSpec().setSubjectRef(ref);
|
||||
|
||||
Snapshot snapshotV2 = TestPost.snapshotV2();
|
||||
|
@ -324,8 +217,6 @@ class ContentServiceTest {
|
|||
final Ref ref = postRef(postName);
|
||||
Snapshot snapshotV1 = snapshotV1();
|
||||
snapshotV1.getMetadata().setLabels(new HashMap<>());
|
||||
Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels());
|
||||
snapshotV1.getSpec().setPublishTime(Instant.now());
|
||||
snapshotV1.getSpec().setSubjectRef(ref);
|
||||
Snapshot snapshotV2 = TestPost.snapshotV2();
|
||||
snapshotV2.getSpec().setSubjectRef(ref);
|
||||
|
@ -338,60 +229,12 @@ class ContentServiceTest {
|
|||
.expectComplete()
|
||||
.verify();
|
||||
|
||||
Snapshot snapshotV3 = snapshotV3();
|
||||
when(client.list(eq(Snapshot.class), any(), any()))
|
||||
.thenReturn(Flux.just(snapshotV1, snapshotV2, snapshotV3()));
|
||||
.thenReturn(Flux.just(snapshotV1, snapshotV2, snapshotV3));
|
||||
StepVerifier.create(contentService.latestSnapshotVersion(ref))
|
||||
.expectNext(snapshotV3())
|
||||
.expectNext(snapshotV3)
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void latestPublishedSnapshotThenV1() {
|
||||
String postName = "post-1";
|
||||
Ref ref = postRef(postName);
|
||||
Snapshot snapshotV1 = snapshotV1();
|
||||
snapshotV1.getSpec().setSubjectRef(ref);
|
||||
snapshotV1.getMetadata().setLabels(new HashMap<>());
|
||||
Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels());
|
||||
snapshotV1.getSpec().setPublishTime(Instant.now());
|
||||
|
||||
Snapshot snapshotV2 = TestPost.snapshotV2();
|
||||
snapshotV2.getSpec().setSubjectRef(ref);
|
||||
snapshotV2.getSpec().setPublishTime(null);
|
||||
|
||||
when(client.list(eq(Snapshot.class), any(), any()))
|
||||
.thenReturn(Flux.just(snapshotV1, snapshotV2));
|
||||
|
||||
StepVerifier.create(contentService.latestPublishedSnapshot(ref))
|
||||
.expectNext(snapshotV1)
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void latestPublishedSnapshotThenV2() {
|
||||
String postName = "post-1";
|
||||
Ref ref = postRef(postName);
|
||||
Snapshot snapshotV1 = snapshotV1();
|
||||
snapshotV1.getSpec().setSubjectRef(ref);
|
||||
snapshotV1.getMetadata().setLabels(new HashMap<>());
|
||||
Snapshot.putPublishedLabel(snapshotV1.getMetadata().getLabels());
|
||||
snapshotV1.getSpec().setPublishTime(Instant.now());
|
||||
|
||||
Snapshot snapshotV2 = TestPost.snapshotV2();
|
||||
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));
|
||||
|
||||
StepVerifier.create(contentService.latestPublishedSnapshot(ref))
|
||||
.expectNext(snapshotV2)
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.service.RoleService;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link PostService}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureWebTestClient
|
||||
@AutoConfigureTestDatabase
|
||||
@WithMockUser(username = "fake-user", password = "fake-password", roles = "fake-super-role")
|
||||
public class PostIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
private WebTestClient webTestClient;
|
||||
|
||||
@MockBean
|
||||
RoleService roleService;
|
||||
|
||||
@Autowired
|
||||
ReactiveExtensionClient client;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
var rule = new Role.PolicyRule.Builder()
|
||||
.apiGroups("*")
|
||||
.resources("*")
|
||||
.verbs("*")
|
||||
.build();
|
||||
var role = new Role();
|
||||
role.setRules(List.of(rule));
|
||||
when(roleService.getMonoRole("authenticated")).thenReturn(Mono.just(role));
|
||||
webTestClient = webTestClient.mutateWith(csrf());
|
||||
}
|
||||
|
||||
@Test
|
||||
void draftPost() {
|
||||
webTestClient.post()
|
||||
.uri("/apis/api.console.halo.run/v1alpha1/posts")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(postDraftRequest())
|
||||
.exchange()
|
||||
.expectBody(Post.class)
|
||||
.value(post -> {
|
||||
MetadataOperator metadata = post.getMetadata();
|
||||
Post.PostSpec spec = post.getSpec();
|
||||
assertThat(spec.getTitle()).isEqualTo("无标题文章");
|
||||
assertThat(metadata.getCreationTimestamp()).isNotNull();
|
||||
assertThat(metadata.getName()).startsWith("post-");
|
||||
assertThat(spec.getHeadSnapshot()).isNotNull();
|
||||
assertThat(spec.getHeadSnapshot()).isEqualTo(spec.getBaseSnapshot());
|
||||
assertThat(spec.getOwner()).isEqualTo("fake-user");
|
||||
|
||||
assertThat(post.getStatus()).isNotNull();
|
||||
assertThat(post.getStatus().getPhase()).isEqualTo("DRAFT");
|
||||
assertThat(post.getStatus().getConditions().peek().getType()).isEqualTo("DRAFT");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void draftPostAsPublish() {
|
||||
PostRequest postRequest = postDraftRequest();
|
||||
postRequest.post().getSpec().setPublish(true);
|
||||
webTestClient.post()
|
||||
.uri("/apis/api.console.halo.run/v1alpha1/posts")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(postRequest)
|
||||
.exchange()
|
||||
.expectBody(Post.class)
|
||||
.value(post -> {
|
||||
assertThat(post.getSpec().getReleaseSnapshot()).isNotNull();
|
||||
assertThat(post.getSpec().getReleaseSnapshot())
|
||||
.isEqualTo(post.getSpec().getHeadSnapshot());
|
||||
assertThat(post.getSpec().getHeadSnapshot())
|
||||
.isEqualTo(post.getSpec().getBaseSnapshot());
|
||||
});
|
||||
}
|
||||
|
||||
PostRequest postDraftRequest() {
|
||||
String s = """
|
||||
{
|
||||
"post": {
|
||||
"spec": {
|
||||
"title": "无标题文章",
|
||||
"slug": "41c2ad39-21b4-45e4-a36b-5768245a0555",
|
||||
"template": "",
|
||||
"cover": "",
|
||||
"deleted": false,
|
||||
"publish": true,
|
||||
"publishTime": "",
|
||||
"pinned": false,
|
||||
"allowComment": true,
|
||||
"visible": "PUBLIC",
|
||||
"version": 1,
|
||||
"priority": 0,
|
||||
"excerpt": {
|
||||
"autoGenerate": true,
|
||||
"raw": ""
|
||||
},
|
||||
"categories": [],
|
||||
"tags": [],
|
||||
"htmlMetas": []
|
||||
},
|
||||
"apiVersion": "content.halo.run/v1alpha1",
|
||||
"kind": "Post",
|
||||
"metadata": {
|
||||
"name": "",
|
||||
"generateName": "post-"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"raw": "<p>hello world</p>",
|
||||
"content": "<p>hello world</p>",
|
||||
"rawType": "HTML"
|
||||
}
|
||||
}
|
||||
""";
|
||||
return JsonUtils.jsonToObject(s, PostRequest.class);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import java.time.Instant;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.extension.Metadata;
|
||||
|
||||
|
@ -23,7 +25,6 @@ public class TestPost {
|
|||
post.setSpec(postSpec);
|
||||
|
||||
postSpec.setTitle("post-A");
|
||||
postSpec.setVersion(1);
|
||||
postSpec.setBaseSnapshot(snapshotV1().getMetadata().getName());
|
||||
postSpec.setHeadSnapshot("base-snapshot");
|
||||
postSpec.setReleaseSnapshot(null);
|
||||
|
@ -37,13 +38,13 @@ public class TestPost {
|
|||
snapshot.setApiVersion(getApiVersion(Snapshot.class));
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("snapshot-A");
|
||||
metadata.setCreationTimestamp(Instant.now());
|
||||
snapshot.setMetadata(metadata);
|
||||
ExtensionUtil.nullSafeAnnotations(snapshot).put(Snapshot.KEEP_RAW_ANNO, "true");
|
||||
Snapshot.SnapShotSpec spec = new Snapshot.SnapShotSpec();
|
||||
snapshot.setSpec(spec);
|
||||
|
||||
spec.setDisplayVersion("v1");
|
||||
spec.setVersion(1);
|
||||
snapshot.addContributor("guqing");
|
||||
Snapshot.addContributor(snapshot, "guqing");
|
||||
spec.setRawType("MARKDOWN");
|
||||
spec.setRawPatch("A");
|
||||
spec.setContentPatch("<p>A</p>");
|
||||
|
@ -56,14 +57,12 @@ public class TestPost {
|
|||
snapshot.setKind(Snapshot.KIND);
|
||||
snapshot.setApiVersion(getApiVersion(Snapshot.class));
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setCreationTimestamp(Instant.now().plusSeconds(10));
|
||||
metadata.setName("snapshot-B");
|
||||
snapshot.setMetadata(metadata);
|
||||
Snapshot.SnapShotSpec spec = new Snapshot.SnapShotSpec();
|
||||
snapshot.setSpec(spec);
|
||||
|
||||
spec.setDisplayVersion("v2");
|
||||
spec.setVersion(2);
|
||||
snapshot.addContributor("guqing");
|
||||
Snapshot.addContributor(snapshot, "guqing");
|
||||
spec.setRawType("MARKDOWN");
|
||||
spec.setRawPatch(PatchUtils.diffToJsonPatch("A", "B"));
|
||||
spec.setContentPatch(PatchUtils.diffToJsonPatch("<p>A</p>", "<p>B</p>"));
|
||||
|
@ -74,10 +73,9 @@ public class TestPost {
|
|||
public static Snapshot snapshotV3() {
|
||||
Snapshot snapshotV3 = snapshotV2();
|
||||
snapshotV3.getMetadata().setName("snapshot-C");
|
||||
snapshotV3.getMetadata().setCreationTimestamp(Instant.now().plusSeconds(20));
|
||||
Snapshot.SnapShotSpec spec = snapshotV3.getSpec();
|
||||
spec.setDisplayVersion("v3");
|
||||
spec.setVersion(3);
|
||||
snapshotV3.addContributor("guqing");
|
||||
Snapshot.addContributor(snapshotV3, "guqing");
|
||||
spec.setRawType("MARKDOWN");
|
||||
spec.setRawPatch(PatchUtils.diffToJsonPatch("B", "C"));
|
||||
spec.setContentPatch(PatchUtils.diffToJsonPatch("<p>B</p>", "<p>C</p>"));
|
||||
|
|
|
@ -285,8 +285,7 @@ class CommentServiceImplTest {
|
|||
"spec": {
|
||||
"title": "post-A",
|
||||
"headSnapshot": "base-snapshot",
|
||||
"baseSnapshot": "snapshot-A",
|
||||
"version": 1
|
||||
"baseSnapshot": "snapshot-A"
|
||||
},
|
||||
"apiVersion": "content.halo.run/v1alpha1",
|
||||
"kind": "Post",
|
||||
|
@ -330,8 +329,7 @@ class CommentServiceImplTest {
|
|||
"spec": {
|
||||
"title": "post-A",
|
||||
"headSnapshot": "base-snapshot",
|
||||
"baseSnapshot": "snapshot-A",
|
||||
"version": 1
|
||||
"baseSnapshot": "snapshot-A"
|
||||
},
|
||||
"apiVersion": "content.halo.run/v1alpha1",
|
||||
"kind": "Post",
|
||||
|
@ -374,8 +372,7 @@ class CommentServiceImplTest {
|
|||
"spec": {
|
||||
"title": "post-A",
|
||||
"headSnapshot": "base-snapshot",
|
||||
"baseSnapshot": "snapshot-A",
|
||||
"version": 1
|
||||
"baseSnapshot": "snapshot-A"
|
||||
},
|
||||
"apiVersion": "content.halo.run/v1alpha1",
|
||||
"kind": "Post",
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
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.when;
|
||||
|
||||
import java.time.Instant;
|
||||
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 reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.content.TestPost;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Tests for {@link ContentServiceImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ContentServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
@InjectMocks
|
||||
private ContentServiceImpl contentService;
|
||||
|
||||
@Test
|
||||
void getBaseSnapshot() {
|
||||
Snapshot snapshotV1 = TestPost.snapshotV1();
|
||||
ExtensionUtil.nullSafeAnnotations(snapshotV1)
|
||||
.put(Snapshot.KEEP_RAW_ANNO, "true");
|
||||
when(client.list(eq(Snapshot.class), any(), any()))
|
||||
.thenReturn(Flux.just(TestPost.snapshotV2(), snapshotV1, TestPost.snapshotV3()));
|
||||
contentService.getBaseSnapshot(Ref.of("fake-post"))
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(
|
||||
baseSnapshot -> assertThat(baseSnapshot.getMetadata().getName())
|
||||
.isEqualTo(snapshotV1.getMetadata().getName()))
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void latestSnapshotVersion() {
|
||||
Snapshot snapshotV1 = TestPost.snapshotV1();
|
||||
snapshotV1.getMetadata().setCreationTimestamp(Instant.now());
|
||||
|
||||
Snapshot snapshotV2 = TestPost.snapshotV2();
|
||||
snapshotV2.getMetadata().setCreationTimestamp(Instant.now().plusSeconds(2));
|
||||
|
||||
Snapshot snapshotV3 = TestPost.snapshotV3();
|
||||
snapshotV3.getMetadata().setCreationTimestamp(Instant.now().plusSeconds(3));
|
||||
|
||||
when(client.list(eq(Snapshot.class), any(), any()))
|
||||
.thenReturn(Flux.just(snapshotV2, snapshotV1, snapshotV3));
|
||||
|
||||
contentService.latestSnapshotVersion(Ref.of("fake-post"))
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(s -> {
|
||||
assertThat(s.getMetadata().getName()).isEqualTo(snapshotV3.getMetadata().getName());
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
|
@ -2,37 +2,21 @@ 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}.
|
||||
|
@ -92,122 +76,7 @@ class PostServiceImplTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void publishWhenPostIsNonePublishedState() {
|
||||
String postName = "fake-post";
|
||||
String snapV1name = "fake-post-snapshot-v1";
|
||||
Post post = TestPost.postV1();
|
||||
post.getMetadata().setName(postName);
|
||||
void draftPost() {
|
||||
|
||||
// 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<Mono<Post>>) invocation -> {
|
||||
Post updated = invocation.getArgument(0);
|
||||
return Mono.just(updated);
|
||||
});
|
||||
|
||||
postService.publishPost(postName)
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(expected -> {
|
||||
ArgumentCaptor<Post> 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<Condition> 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<Mono<Post>>) invocation -> {
|
||||
Post updated = invocation.getArgument(0);
|
||||
return Mono.just(updated);
|
||||
});
|
||||
|
||||
postService.publishPost(postName)
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(expected -> {
|
||||
assertThat(expected).isNotNull();
|
||||
ArgumentCaptor<Post> 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();
|
||||
}
|
||||
}
|
|
@ -2,9 +2,6 @@ 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.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
@ -20,7 +17,6 @@ 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.event.post.PostPublishedEvent;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
||||
/**
|
||||
|
@ -79,25 +75,6 @@ class PostEndpointTest {
|
|||
.value(post -> assertThat(post).isEqualTo(TestPost.postV1()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishPost() {
|
||||
Post post = TestPost.postV1();
|
||||
when(postService.publishPost(any())).thenReturn(Mono.just(post));
|
||||
when(client.get(eq(Post.class), eq(post.getMetadata().getName())))
|
||||
.thenReturn(Mono.just(post));
|
||||
when(client.update(any())).thenReturn(Mono.just(post));
|
||||
doNothing().when(eventPublisher).publishEvent(isA(PostPublishedEvent.class));
|
||||
|
||||
webTestClient.put()
|
||||
.uri("/posts/post-A/publish")
|
||||
.bodyValue(postRequest(TestPost.postV1()))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody(Post.class)
|
||||
.value(p -> assertThat(p).isEqualTo(post));
|
||||
}
|
||||
|
||||
PostRequest postRequest(Post post) {
|
||||
return new PostRequest(post, new PostRequest.Content("B", "<p>B</p>", "MARKDOWN"));
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ 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.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
@ -30,7 +28,6 @@ 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}.
|
||||
|
@ -64,7 +61,6 @@ class PostReconcilerTest {
|
|||
.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();
|
||||
|
@ -108,8 +104,6 @@ class PostReconcilerTest {
|
|||
|
||||
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();
|
||||
|
@ -117,36 +111,12 @@ class PostReconcilerTest {
|
|||
|
||||
when(contentService.listSnapshots(any()))
|
||||
.thenReturn(Flux.just(snapshotV1, snapshotV2));
|
||||
when(postService.publishPost(eq(name))).thenReturn(Mono.empty());
|
||||
|
||||
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
|
||||
postReconciler.reconcile(new Reconciler.Request(name));
|
||||
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
verify(client, times(4)).update(captor.capture());
|
||||
Post value = captor.getValue();
|
||||
assertThat(value.getStatus().getExcerpt()).isEqualTo("hello world");
|
||||
}
|
||||
|
||||
@Test
|
||||
void limitConditionSize() {
|
||||
List<Condition> conditions = new ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Condition condition = new Condition();
|
||||
condition.setType("test-" + i);
|
||||
conditions.add(condition);
|
||||
}
|
||||
List<Condition> 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");
|
||||
}
|
||||
}
|
|
@ -89,7 +89,6 @@ 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<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class);
|
||||
singlePageReconciler.reconcile(new Reconciler.Request(name));
|
||||
|
@ -138,7 +137,6 @@ class SinglePageReconcilerTest {
|
|||
|
||||
spec.setTitle("page-A");
|
||||
spec.setSlug("page-slug");
|
||||
spec.setVersion(1);
|
||||
spec.setBaseSnapshot(snapshotV1().getMetadata().getName());
|
||||
spec.setHeadSnapshot("base-snapshot");
|
||||
spec.setReleaseSnapshot(null);
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.Iterator;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link ConditionList}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class ConditionListTest {
|
||||
|
||||
@Test
|
||||
void add() {
|
||||
ConditionList conditionList = new ConditionList();
|
||||
conditionList.add(condition("type", "message", "reason", ConditionStatus.FALSE));
|
||||
conditionList.add(condition("type", "message", "reason", ConditionStatus.FALSE));
|
||||
|
||||
assertThat(conditionList.size()).isEqualTo(1);
|
||||
conditionList.add(condition("type", "message", "reason", ConditionStatus.TRUE));
|
||||
assertThat(conditionList.size()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addAndEvictFIFO() throws JSONException {
|
||||
ConditionList conditionList = new ConditionList();
|
||||
conditionList.addFirst(condition("type", "message", "reason", ConditionStatus.FALSE));
|
||||
conditionList.addFirst(condition("type2", "message2", "reason2", ConditionStatus.FALSE));
|
||||
conditionList.addFirst(condition("type3", "message3", "reason3", ConditionStatus.FALSE));
|
||||
|
||||
JSONAssert.assertEquals("""
|
||||
[
|
||||
{
|
||||
"type": "type3",
|
||||
"status": "FALSE",
|
||||
"message": "message3",
|
||||
"reason": "reason3"
|
||||
},
|
||||
{
|
||||
"type": "type2",
|
||||
"status": "FALSE",
|
||||
"message": "message2",
|
||||
"reason": "reason2"
|
||||
},
|
||||
{
|
||||
"type": "type",
|
||||
"status": "FALSE",
|
||||
"message": "message",
|
||||
"reason": "reason"
|
||||
}
|
||||
]
|
||||
""",
|
||||
JsonUtils.objectToJson(conditionList),
|
||||
true);
|
||||
assertThat(conditionList.size()).isEqualTo(3);
|
||||
|
||||
conditionList.addAndEvictFIFO(
|
||||
condition("type4", "message4", "reason4", ConditionStatus.FALSE), 1);
|
||||
|
||||
assertThat(conditionList.size()).isEqualTo(1);
|
||||
|
||||
// json serialize test.
|
||||
JSONAssert.assertEquals("""
|
||||
[
|
||||
{
|
||||
"type": "type4",
|
||||
"status": "FALSE",
|
||||
"message": "message4",
|
||||
"reason": "reason4"
|
||||
}
|
||||
]
|
||||
""",
|
||||
JsonUtils.objectToJson(conditionList), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void peek() {
|
||||
ConditionList conditionList = new ConditionList();
|
||||
conditionList.addFirst(condition("type", "message", "reason", ConditionStatus.FALSE));
|
||||
Condition condition = condition("type2", "message2", "reason2", ConditionStatus.FALSE);
|
||||
conditionList.addFirst(condition);
|
||||
|
||||
Condition peek = conditionList.peek();
|
||||
assertThat(peek).isEqualTo(condition);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeLast() {
|
||||
ConditionList conditionList = new ConditionList();
|
||||
Condition condition = condition("type", "message", "reason", ConditionStatus.FALSE);
|
||||
conditionList.addFirst(condition);
|
||||
|
||||
conditionList.addFirst(condition("type2", "message2", "reason2", ConditionStatus.FALSE));
|
||||
|
||||
assertThat(conditionList.size()).isEqualTo(2);
|
||||
assertThat(conditionList.removeLast()).isEqualTo(condition);
|
||||
assertThat(conditionList.size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
ConditionList conditionList = new ConditionList();
|
||||
conditionList.addAndEvictFIFO(
|
||||
condition("type", "message", "reason", ConditionStatus.FALSE));
|
||||
conditionList.addAndEvictFIFO(
|
||||
condition("type2", "message2", "reason2", ConditionStatus.FALSE));
|
||||
|
||||
Iterator<Condition> iterator = conditionList.iterator();
|
||||
assertThat(iterator.next().getType()).isEqualTo("type2");
|
||||
assertThat(iterator.next().getType()).isEqualTo("type");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deserialization() {
|
||||
String s = """
|
||||
[{
|
||||
"type": "type3",
|
||||
"status": "FALSE",
|
||||
"message": "message3",
|
||||
"reason": "reason3"
|
||||
},
|
||||
{
|
||||
"type": "type2",
|
||||
"status": "FALSE",
|
||||
"message": "message2",
|
||||
"reason": "reason2"
|
||||
},
|
||||
{
|
||||
"type": "type",
|
||||
"status": "FALSE",
|
||||
"message": "message",
|
||||
"reason": "reason"
|
||||
}]
|
||||
""";
|
||||
ConditionList conditions = JsonUtils.jsonToObject(s, ConditionList.class);
|
||||
assertThat(conditions.peek().getType()).isEqualTo("type3");
|
||||
}
|
||||
|
||||
private Condition condition(String type, String message, String reason,
|
||||
ConditionStatus status) {
|
||||
Condition condition = new Condition();
|
||||
condition.setType(type);
|
||||
condition.setMessage(message);
|
||||
condition.setReason(reason);
|
||||
condition.setStatus(status);
|
||||
return condition;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue