Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: snapshot attributes and post publish #2709

Merged
merged 10 commits into from
Nov 18, 2022
9 changes: 4 additions & 5 deletions src/main/java/run/halo/app/config/ExtensionConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
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;
Expand Down Expand Up @@ -151,11 +150,11 @@ Controller themeController(ExtensionClient client, HaloProperties haloProperties
@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)
Expand Down Expand Up @@ -204,10 +203,10 @@ Controller attachmentController(ExtensionClient client,
@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();
Expand Down
15 changes: 4 additions & 11 deletions src/main/java/run/halo/app/content/ContentRequest.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand Down
6 changes: 2 additions & 4 deletions src/main/java/run/halo/app/content/ContentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
1 change: 0 additions & 1 deletion src/main/java/run/halo/app/content/ContentWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
@Builder
public class ContentWrapper {
private String snapshotName;
private Integer version;
private String raw;
private String content;
private String rawType;
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/run/halo/app/content/PostService.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,4 @@ public interface PostService {
Mono<Post> draftPost(PostRequest postRequest);

Mono<Post> updatePost(PostRequest postRequest);

Mono<Post> publishPost(String postName);
}
2 changes: 0 additions & 2 deletions src/main/java/run/halo/app/content/SinglePageService.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,4 @@ public interface SinglePageService {
Mono<SinglePage> draft(SinglePageRequest pageRequest);

Mono<SinglePage> update(SinglePageRequest pageRequest);

Mono<SinglePage> publish(String name);
}
211 changes: 66 additions & 145 deletions src/main/java/run/halo/app/content/impl/ContentServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
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;
Expand All @@ -28,10 +28,6 @@
*/
@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) {
Expand All @@ -46,61 +42,56 @@ public Mono<ContentWrapper> getContent(String name) {
}

@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);
Expand All @@ -109,78 +100,17 @@ private Mono<ContentWrapper> restoredContent(Snapshot headSnapshot) {
@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();
}

Expand All @@ -197,47 +127,38 @@ private Mono<String> getContextUsername() {
.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);
String revisedContent = contentRequest.contentPatchFrom(originalContent);
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();
}
}