mirror of https://github.com/halo-dev/halo
Support post content version control (#1617)
* feat: split post content to new table and support content version control * feat: Improve post version management * feat: Add post content and version record deletion * feat: Add isInProcess attribute for post list and detail api * feat: Add migrate sql script * fix: Add a sql of allow origin_content to null in posts table * feat: Assign a value to the source of the post content version recordpull/1669/head
parent
1ee7b58ef1
commit
923eb17577
|
@ -73,6 +73,7 @@ ext {
|
|||
huaweiObsVersion = '3.21.8.1'
|
||||
templateInheritanceVersion = "0.4.RELEASE"
|
||||
jsoupVersion = '1.14.3'
|
||||
diffUtilsVersion = '4.11'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -121,6 +122,7 @@ dependencies {
|
|||
implementation "net.sf.image4j:image4j:$image4jVersion"
|
||||
implementation "org.flywaydb:flyway-core:$flywayVersion"
|
||||
implementation "com.google.zxing:core:$zxingVersion"
|
||||
implementation "io.github.java-diff-utils:java-diff-utils:$diffUtilsVersion"
|
||||
|
||||
implementation "org.iq80.leveldb:leveldb:$levelDbVersion"
|
||||
runtimeOnly "com.h2database:h2:$h2Version"
|
||||
|
|
|
@ -28,7 +28,6 @@ import run.halo.app.model.dto.post.BasePostDetailDTO;
|
|||
import run.halo.app.model.dto.post.BasePostMinimalDTO;
|
||||
import run.halo.app.model.dto.post.BasePostSimpleDTO;
|
||||
import run.halo.app.model.entity.Post;
|
||||
import run.halo.app.model.enums.PostPermalinkType;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
import run.halo.app.model.params.PostContentParam;
|
||||
import run.halo.app.model.params.PostParam;
|
||||
|
@ -103,7 +102,7 @@ public class PostController {
|
|||
@GetMapping("{postId:\\d+}")
|
||||
@ApiOperation("Gets a post")
|
||||
public PostDetailVO getBy(@PathVariable("postId") Integer postId) {
|
||||
Post post = postService.getById(postId);
|
||||
Post post = postService.getWithLatestContentById(postId);
|
||||
return postService.convertToDetailVo(post, true);
|
||||
}
|
||||
|
||||
|
@ -131,7 +130,7 @@ public class PostController {
|
|||
@RequestParam(value = "autoSave", required = false, defaultValue = "false") Boolean autoSave
|
||||
) {
|
||||
// Get the post info
|
||||
Post postToUpdate = postService.getById(postId);
|
||||
Post postToUpdate = postService.getWithLatestContentById(postId);
|
||||
|
||||
postParam.update(postToUpdate);
|
||||
return postService.updateBy(postToUpdate, postParam.getTagIds(), postParam.getCategoryIds(),
|
||||
|
@ -161,9 +160,9 @@ public class PostController {
|
|||
@PathVariable("postId") Integer postId,
|
||||
@RequestBody PostContentParam contentParam) {
|
||||
// Update draft content
|
||||
Post post = postService.updateDraftContent(contentParam.getContent(), postId);
|
||||
|
||||
return new BasePostDetailDTO().convertFrom(post);
|
||||
Post post = postService.updateDraftContent(contentParam.getContent(),
|
||||
contentParam.getContent(), postId);
|
||||
return postService.convertToDetail(post);
|
||||
}
|
||||
|
||||
@DeleteMapping("{postId:\\d+}")
|
||||
|
|
|
@ -63,7 +63,7 @@ public class SheetController {
|
|||
@GetMapping("{sheetId:\\d+}")
|
||||
@ApiOperation("Gets a sheet")
|
||||
public SheetDetailVO getBy(@PathVariable("sheetId") Integer sheetId) {
|
||||
Sheet sheet = sheetService.getById(sheetId);
|
||||
Sheet sheet = sheetService.getWithLatestContentById(sheetId);
|
||||
return sheetService.convertToDetailVo(sheet);
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,7 @@ public class SheetController {
|
|||
@RequestBody @Valid SheetParam sheetParam,
|
||||
@RequestParam(value = "autoSave", required = false, defaultValue = "false")
|
||||
Boolean autoSave) {
|
||||
Sheet sheetToUpdate = sheetService.getById(sheetId);
|
||||
Sheet sheetToUpdate = sheetService.getWithLatestContentById(sheetId);
|
||||
|
||||
sheetParam.update(sheetToUpdate);
|
||||
|
||||
|
@ -127,9 +127,9 @@ public class SheetController {
|
|||
@PathVariable("sheetId") Integer sheetId,
|
||||
@RequestBody PostContentParam contentParam) {
|
||||
// Update draft content
|
||||
Sheet sheet = sheetService.updateDraftContent(contentParam.getContent(), sheetId);
|
||||
|
||||
return new BasePostDetailDTO().convertFrom(sheet);
|
||||
Sheet sheet = sheetService.updateDraftContent(contentParam.getContent(),
|
||||
contentParam.getContent(), sheetId);
|
||||
return sheetService.convertToDetail(sheet);
|
||||
}
|
||||
|
||||
@DeleteMapping("{sheetId:\\d+}")
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
|||
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
|
||||
import run.halo.app.model.dto.CategoryDTO;
|
||||
import run.halo.app.model.entity.Category;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Post;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
import run.halo.app.model.vo.PostDetailVO;
|
||||
|
@ -40,6 +41,7 @@ import run.halo.app.service.PostService;
|
|||
|
||||
/**
|
||||
* @author ryanwang
|
||||
* @author guqing
|
||||
* @date 2019-03-21
|
||||
*/
|
||||
@Slf4j
|
||||
|
@ -242,14 +244,35 @@ public class ContentFeedController {
|
|||
Assert.notNull(pageable, "Pageable must not be null");
|
||||
|
||||
Page<Post> postPage = postService.pageBy(PostStatus.PUBLISHED, pageable);
|
||||
Page<PostDetailVO> posts = convertToDetailPageVo(postPage);
|
||||
return posts.getContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to a page of detail vo.
|
||||
* Notes: this method will escape the XML tag characters in the post content and summary.
|
||||
*
|
||||
* @param postPage post page must not be null
|
||||
* @return a page of post detail vo that content and summary escaped.
|
||||
*/
|
||||
@NonNull
|
||||
private Page<PostDetailVO> convertToDetailPageVo(Page<Post> postPage) {
|
||||
Assert.notNull(postPage, "The postPage must not be null.");
|
||||
|
||||
// Populate post content
|
||||
postPage.getContent().forEach(post -> {
|
||||
Content postContent = postService.getContentById(post.getId());
|
||||
post.setContent(Content.PatchedContent.of(postContent));
|
||||
});
|
||||
|
||||
Page<PostDetailVO> posts = postService.convertToDetailVo(postPage);
|
||||
posts.getContent().forEach(postDetailVO -> {
|
||||
postDetailVO.setFormatContent(
|
||||
RegExUtils.replaceAll(postDetailVO.getFormatContent(), XML_INVALID_CHAR, ""));
|
||||
postDetailVO.setContent(
|
||||
RegExUtils.replaceAll(postDetailVO.getContent(), XML_INVALID_CHAR, ""));
|
||||
postDetailVO
|
||||
.setSummary(RegExUtils.replaceAll(postDetailVO.getSummary(), XML_INVALID_CHAR, ""));
|
||||
});
|
||||
return posts.getContent();
|
||||
return posts;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -266,13 +289,7 @@ public class ContentFeedController {
|
|||
|
||||
Page<Post> postPage =
|
||||
postCategoryService.pagePostBy(category.getId(), PostStatus.PUBLISHED, pageable);
|
||||
Page<PostDetailVO> posts = postService.convertToDetailVo(postPage);
|
||||
posts.getContent().forEach(postDetailVO -> {
|
||||
postDetailVO.setFormatContent(
|
||||
RegExUtils.replaceAll(postDetailVO.getFormatContent(), XML_INVALID_CHAR, ""));
|
||||
postDetailVO
|
||||
.setSummary(RegExUtils.replaceAll(postDetailVO.getSummary(), XML_INVALID_CHAR, ""));
|
||||
});
|
||||
Page<PostDetailVO> posts = convertToDetailPageVo(postPage);
|
||||
return posts.getContent();
|
||||
}
|
||||
|
||||
|
|
|
@ -109,7 +109,7 @@ public class PostController {
|
|||
|
||||
if (formatDisabled) {
|
||||
// Clear the format content
|
||||
postDetailVO.setFormatContent(null);
|
||||
postDetailVO.setContent(null);
|
||||
}
|
||||
|
||||
if (sourceDisabled) {
|
||||
|
@ -133,7 +133,7 @@ public class PostController {
|
|||
|
||||
if (formatDisabled) {
|
||||
// Clear the format content
|
||||
postDetailVO.setFormatContent(null);
|
||||
postDetailVO.setContent(null);
|
||||
}
|
||||
|
||||
if (sourceDisabled) {
|
||||
|
|
|
@ -78,7 +78,7 @@ public class SheetController {
|
|||
|
||||
if (formatDisabled) {
|
||||
// Clear the format content
|
||||
sheetDetailVO.setFormatContent(null);
|
||||
sheetDetailVO.setContent(null);
|
||||
}
|
||||
|
||||
if (sourceDisabled) {
|
||||
|
@ -102,7 +102,7 @@ public class SheetController {
|
|||
|
||||
if (formatDisabled) {
|
||||
// Clear the format content
|
||||
sheetDetailVO.setFormatContent(null);
|
||||
sheetDetailVO.setContent(null);
|
||||
}
|
||||
|
||||
if (sourceDisabled) {
|
||||
|
|
|
@ -16,11 +16,12 @@ import run.halo.app.cache.AbstractStringCacheStore;
|
|||
import run.halo.app.exception.ForbiddenException;
|
||||
import run.halo.app.exception.NotFoundException;
|
||||
import run.halo.app.model.entity.Category;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.entity.Post;
|
||||
import run.halo.app.model.entity.PostMeta;
|
||||
import run.halo.app.model.entity.Tag;
|
||||
import run.halo.app.model.enums.EncryptTypeEnum;
|
||||
import run.halo.app.model.enums.PostEditorType;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
import run.halo.app.model.vo.ArchiveYearVO;
|
||||
import run.halo.app.model.vo.PostListVO;
|
||||
|
@ -33,12 +34,12 @@ import run.halo.app.service.PostService;
|
|||
import run.halo.app.service.PostTagService;
|
||||
import run.halo.app.service.TagService;
|
||||
import run.halo.app.service.ThemeService;
|
||||
import run.halo.app.utils.MarkdownUtils;
|
||||
|
||||
/**
|
||||
* Post Model
|
||||
*
|
||||
* @author ryanwang
|
||||
* @author guqing
|
||||
* @date 2020-01-07
|
||||
*/
|
||||
@Component
|
||||
|
@ -116,12 +117,13 @@ public class PostModel {
|
|||
return "common/template/" + POST_PASSWORD_TEMPLATE;
|
||||
}
|
||||
|
||||
post = postService.getById(post.getId());
|
||||
|
||||
if (post.getEditorType().equals(PostEditorType.MARKDOWN)) {
|
||||
post.setFormatContent(MarkdownUtils.renderHtml(post.getOriginalContent()));
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
post = postService.getWithLatestContentById(post.getId());
|
||||
} else {
|
||||
post.setFormatContent(post.getOriginalContent());
|
||||
post = postService.getById(post.getId());
|
||||
// Set post content
|
||||
Content postContent = postService.getContentById(post.getId());
|
||||
post.setContent(PatchedContent.of(postContent));
|
||||
}
|
||||
|
||||
postService.publishVisitEvent(post.getId());
|
||||
|
@ -148,7 +150,7 @@ public class PostModel {
|
|||
model.addAttribute("meta_description", post.getMetaDescription());
|
||||
} else {
|
||||
model.addAttribute("meta_description",
|
||||
postService.generateDescription(post.getFormatContent()));
|
||||
postService.generateDescription(post.getContent().getContent()));
|
||||
}
|
||||
|
||||
model.addAttribute("is_post", true);
|
||||
|
|
|
@ -6,6 +6,8 @@ import org.springframework.stereotype.Component;
|
|||
import org.springframework.ui.Model;
|
||||
import run.halo.app.cache.AbstractStringCacheStore;
|
||||
import run.halo.app.exception.ForbiddenException;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.entity.Sheet;
|
||||
import run.halo.app.model.entity.SheetMeta;
|
||||
import run.halo.app.model.enums.PostEditorType;
|
||||
|
@ -61,6 +63,9 @@ public class SheetModel {
|
|||
|
||||
if (StringUtils.isEmpty(token)) {
|
||||
sheet = sheetService.getBy(PostStatus.PUBLISHED, sheet.getSlug());
|
||||
//Set sheet content
|
||||
Content content = sheetService.getContentById(sheet.getId());
|
||||
sheet.setContent(PatchedContent.of(content));
|
||||
} else {
|
||||
// verify token
|
||||
String cachedToken = cacheStore.getAny(token, String.class)
|
||||
|
@ -69,11 +74,14 @@ public class SheetModel {
|
|||
throw new ForbiddenException("您没有该页面的访问权限");
|
||||
}
|
||||
// render markdown to html when preview sheet
|
||||
PatchedContent sheetContent = sheetService.getLatestContentById(sheet.getId());
|
||||
if (sheet.getEditorType().equals(PostEditorType.MARKDOWN)) {
|
||||
sheet.setFormatContent(MarkdownUtils.renderHtml(sheet.getOriginalContent()));
|
||||
sheetContent.setContent(
|
||||
MarkdownUtils.renderHtml(sheetContent.getOriginalContent()));
|
||||
} else {
|
||||
sheet.setFormatContent(sheet.getOriginalContent());
|
||||
sheetContent.setContent(sheetContent.getOriginalContent());
|
||||
}
|
||||
sheet.setContent(sheetContent);
|
||||
}
|
||||
|
||||
sheetService.publishVisitEvent(sheet.getId());
|
||||
|
@ -94,7 +102,7 @@ public class SheetModel {
|
|||
model.addAttribute("meta_description", sheet.getMetaDescription());
|
||||
} else {
|
||||
model.addAttribute("meta_description",
|
||||
sheetService.generateDescription(sheet.getFormatContent()));
|
||||
sheetService.generateDescription(sheet.getContent().getContent()));
|
||||
}
|
||||
|
||||
// sheet and post all can use
|
||||
|
|
|
@ -3,11 +3,15 @@ package run.halo.app.model.dto.post;
|
|||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.lang.NonNull;
|
||||
import run.halo.app.model.entity.BasePost;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
|
||||
/**
|
||||
* Base post detail output dto.
|
||||
*
|
||||
* @author johnniang
|
||||
* @author guqing
|
||||
*/
|
||||
@Data
|
||||
@ToString
|
||||
|
@ -16,7 +20,29 @@ public class BasePostDetailDTO extends BasePostSimpleDTO {
|
|||
|
||||
private String originalContent;
|
||||
|
||||
private String formatContent;
|
||||
private String content;
|
||||
|
||||
private Long commentCount;
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends BasePostMinimalDTO> T convertFrom(@NonNull BasePost domain) {
|
||||
BasePostDetailDTO postDetailDTO = super.convertFrom(domain);
|
||||
PatchedContent content = domain.getContent();
|
||||
postDetailDTO.setContent(content.getContent());
|
||||
postDetailDTO.setOriginalContent(content.getOriginalContent());
|
||||
return (T) postDetailDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatible with the formatContent attribute existing in the old version
|
||||
* it will be removed in v2.0
|
||||
*
|
||||
* @return formatted post content
|
||||
*/
|
||||
@Deprecated(since = "1.5.0", forRemoval = true)
|
||||
public String getFormatContent() {
|
||||
return this.content;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@ public class BasePostSimpleDTO extends BasePostMinimalDTO {
|
|||
|
||||
private Long wordCount;
|
||||
|
||||
private Boolean inProgress;
|
||||
|
||||
public boolean isTopped() {
|
||||
return this.topPriority != null && this.topPriority > 0;
|
||||
}
|
||||
|
|
|
@ -13,11 +13,15 @@ import javax.persistence.Lob;
|
|||
import javax.persistence.Table;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
import javax.persistence.Transient;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.enums.PostEditorType;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
|
||||
|
@ -81,7 +85,7 @@ public class BasePost extends BaseEntity {
|
|||
/**
|
||||
* Original content,not format.
|
||||
*/
|
||||
@Column(name = "original_content", nullable = false)
|
||||
@Column(name = "original_content")
|
||||
@Lob
|
||||
private String originalContent;
|
||||
|
||||
|
@ -171,6 +175,19 @@ public class BasePost extends BaseEntity {
|
|||
@ColumnDefault("0")
|
||||
private Long wordCount;
|
||||
|
||||
/**
|
||||
* Post content version.
|
||||
*/
|
||||
@ColumnDefault("1")
|
||||
private Integer version;
|
||||
|
||||
/**
|
||||
* This extra field don't correspond to any columns in the <code>Post</code> table because we
|
||||
* don't want to save this value.
|
||||
*/
|
||||
@Transient
|
||||
private PatchedContent content;
|
||||
|
||||
@Override
|
||||
public void prePersist() {
|
||||
super.prePersist();
|
||||
|
@ -215,14 +232,6 @@ public class BasePost extends BaseEntity {
|
|||
likes = 0L;
|
||||
}
|
||||
|
||||
if (originalContent == null) {
|
||||
originalContent = "";
|
||||
}
|
||||
|
||||
if (formatContent == null) {
|
||||
formatContent = "";
|
||||
}
|
||||
|
||||
if (editorType == null) {
|
||||
editorType = PostEditorType.MARKDOWN;
|
||||
}
|
||||
|
@ -230,6 +239,30 @@ public class BasePost extends BaseEntity {
|
|||
if (wordCount == null || wordCount < 0) {
|
||||
wordCount = 0L;
|
||||
}
|
||||
|
||||
if (version == null || version < 0) {
|
||||
version = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets post content.
|
||||
*
|
||||
* @return a {@link PatchedContent} if present,otherwise an empty object
|
||||
*/
|
||||
@NonNull
|
||||
public PatchedContent getContent() {
|
||||
if (this.content == null) {
|
||||
PatchedContent patchedContent = new PatchedContent();
|
||||
patchedContent.setOriginalContent("");
|
||||
patchedContent.setContent("");
|
||||
return patchedContent;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PatchedContent getContentOfNullable() {
|
||||
return this.content;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
package run.halo.app.model.entity;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Lob;
|
||||
import javax.persistence.Table;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
|
||||
/**
|
||||
* Post content.
|
||||
*
|
||||
* @author guqing
|
||||
* @date 2021-12-18
|
||||
*/
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "contents")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Content extends BaseEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "post_id")
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "status")
|
||||
@ColumnDefault("1")
|
||||
private PostStatus status;
|
||||
|
||||
/**
|
||||
* PatchLog from which the current content comes.
|
||||
*/
|
||||
private Integer patchLogId;
|
||||
|
||||
/**
|
||||
* <p>The patch log head that the current content points to.</p>
|
||||
* <pre>
|
||||
* \-v1-v2-v3(HEAD)-v5(draft)
|
||||
* \-v4
|
||||
* </pre>
|
||||
* e.g. The latest version is v4.
|
||||
* <li>At this time, I switch to V3, and the head points to v3.
|
||||
* <li>When creating a draft (V5) in V3, the head points to V5,
|
||||
* but the <code>patchLogId</code> of the current content still points to the record
|
||||
* where V3 is located
|
||||
*/
|
||||
private Integer headPatchLogId;
|
||||
|
||||
@Lob
|
||||
private String content;
|
||||
|
||||
@Lob
|
||||
private String originalContent;
|
||||
|
||||
@Override
|
||||
protected void prePersist() {
|
||||
super.prePersist();
|
||||
|
||||
if (originalContent == null) {
|
||||
originalContent = "";
|
||||
}
|
||||
|
||||
if (content == null) {
|
||||
content = "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 based content differentiation.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2021-12-20
|
||||
*/
|
||||
@Data
|
||||
public static class ContentDiff {
|
||||
|
||||
private String diff;
|
||||
|
||||
private String originalDiff;
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual content of the post obtained by applying patch to V1 version.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2021-12-20
|
||||
*/
|
||||
@Data
|
||||
public static class PatchedContent {
|
||||
|
||||
private String content;
|
||||
|
||||
private String originalContent;
|
||||
|
||||
public PatchedContent() {
|
||||
}
|
||||
|
||||
public PatchedContent(String content, String originalContent) {
|
||||
this.content = content;
|
||||
this.originalContent = originalContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link PatchedContent} from {@link Content}.
|
||||
*
|
||||
* @param content a {@link Content} must not be null
|
||||
*/
|
||||
public PatchedContent(Content content) {
|
||||
Assert.notNull(content, "The content must not be null.");
|
||||
this.content = content.getContent();
|
||||
this.originalContent = content.getOriginalContent();
|
||||
}
|
||||
|
||||
public static PatchedContent of(Content postContent) {
|
||||
return new PatchedContent(postContent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package run.halo.app.model.entity;
|
||||
|
||||
import java.util.Date;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Index;
|
||||
import javax.persistence.Lob;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
|
||||
/**
|
||||
* Content patch log entity.
|
||||
*
|
||||
* @author guqing
|
||||
* @date 2021-12-18
|
||||
*/
|
||||
@Data
|
||||
@Entity
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Table(name = "content_patch_logs", indexes = {
|
||||
@Index(name = "idx_post_id", columnList = "post_id"),
|
||||
@Index(name = "idx_status", columnList = "status"),
|
||||
@Index(name = "idx_version", columnList = "version")})
|
||||
public class ContentPatchLog extends BaseEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY, generator = "custom-id")
|
||||
@GenericGenerator(name = "custom-id", strategy = "run.halo.app.model.entity.support"
|
||||
+ ".CustomIdGenerator")
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "post_id")
|
||||
private Integer postId;
|
||||
|
||||
@Lob
|
||||
@Column(name = "content_diff")
|
||||
private String contentDiff;
|
||||
|
||||
@Lob
|
||||
@Column(name = "original_content_diff")
|
||||
private String originalContentDiff;
|
||||
|
||||
@Column(name = "version", nullable = false)
|
||||
private Integer version;
|
||||
|
||||
@ColumnDefault("1")
|
||||
@Column(name = "status")
|
||||
private PostStatus status;
|
||||
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
@Column(name = "publish_time")
|
||||
private Date publishTime;
|
||||
|
||||
/**
|
||||
* Current version of the source patch log id, default value is 0.
|
||||
*/
|
||||
@Column(name = "source_id", nullable = false)
|
||||
private Integer sourceId;
|
||||
|
||||
@Override
|
||||
protected void prePersist() {
|
||||
super.prePersist();
|
||||
if (version == null) {
|
||||
version = 1;
|
||||
}
|
||||
|
||||
if (sourceId == null) {
|
||||
sourceId = 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,10 +10,13 @@ import lombok.Data;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import run.halo.app.model.dto.base.InputConverter;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.entity.Post;
|
||||
import run.halo.app.model.entity.PostMeta;
|
||||
import run.halo.app.model.enums.PostEditorType;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
import run.halo.app.utils.MarkdownUtils;
|
||||
import run.halo.app.utils.SlugUtils;
|
||||
|
||||
/**
|
||||
|
@ -80,7 +83,9 @@ public class PostParam implements InputConverter<Post> {
|
|||
editorType = PostEditorType.MARKDOWN;
|
||||
}
|
||||
|
||||
return InputConverter.super.convertTo();
|
||||
Post post = InputConverter.super.convertTo();
|
||||
populateContent(post);
|
||||
return post;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -94,7 +99,7 @@ public class PostParam implements InputConverter<Post> {
|
|||
if (null == editorType) {
|
||||
editorType = PostEditorType.MARKDOWN;
|
||||
}
|
||||
|
||||
populateContent(post);
|
||||
InputConverter.super.update(post);
|
||||
}
|
||||
|
||||
|
@ -110,4 +115,15 @@ public class PostParam implements InputConverter<Post> {
|
|||
}
|
||||
return postMetaSet;
|
||||
}
|
||||
|
||||
private void populateContent(Post post) {
|
||||
Content postContent = new Content();
|
||||
if (PostEditorType.MARKDOWN.equals(editorType)) {
|
||||
postContent.setContent(MarkdownUtils.renderHtml(originalContent));
|
||||
} else {
|
||||
postContent.setContent(postContent.getOriginalContent());
|
||||
}
|
||||
postContent.setOriginalContent(originalContent);
|
||||
post.setContent(PatchedContent.of(postContent));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,13 @@ import lombok.Data;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import run.halo.app.model.dto.base.InputConverter;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.entity.Sheet;
|
||||
import run.halo.app.model.entity.SheetMeta;
|
||||
import run.halo.app.model.enums.PostEditorType;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
import run.halo.app.utils.MarkdownUtils;
|
||||
import run.halo.app.utils.SlugUtils;
|
||||
|
||||
/**
|
||||
|
@ -21,6 +24,7 @@ import run.halo.app.utils.SlugUtils;
|
|||
*
|
||||
* @author johnniang
|
||||
* @author ryanwang
|
||||
* @author guqing
|
||||
* @date 2019-4-24
|
||||
*/
|
||||
@Data
|
||||
|
@ -75,7 +79,9 @@ public class SheetParam implements InputConverter<Sheet> {
|
|||
editorType = PostEditorType.MARKDOWN;
|
||||
}
|
||||
|
||||
return InputConverter.super.convertTo();
|
||||
Sheet sheet = InputConverter.super.convertTo();
|
||||
populateContent(sheet);
|
||||
return sheet;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -89,7 +95,7 @@ public class SheetParam implements InputConverter<Sheet> {
|
|||
if (null == editorType) {
|
||||
editorType = PostEditorType.MARKDOWN;
|
||||
}
|
||||
|
||||
populateContent(sheet);
|
||||
InputConverter.super.update(sheet);
|
||||
}
|
||||
|
||||
|
@ -105,4 +111,15 @@ public class SheetParam implements InputConverter<Sheet> {
|
|||
}
|
||||
return sheetMetasSet;
|
||||
}
|
||||
|
||||
private void populateContent(Sheet sheet) {
|
||||
Content sheetContent = new Content();
|
||||
if (PostEditorType.MARKDOWN.equals(editorType)) {
|
||||
sheetContent.setContent(MarkdownUtils.renderHtml(originalContent));
|
||||
} else {
|
||||
sheetContent.setContent(sheetContent.getOriginalContent());
|
||||
}
|
||||
sheetContent.setOriginalContent(originalContent);
|
||||
sheet.setContent(PatchedContent.of(sheetContent));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
package run.halo.app.repository;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import run.halo.app.model.entity.ContentPatchLog;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
import run.halo.app.repository.base.BaseRepository;
|
||||
|
||||
/**
|
||||
* Content patch log repository.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2022-01-04
|
||||
*/
|
||||
public interface ContentPatchLogRepository extends BaseRepository<ContentPatchLog, Integer> {
|
||||
|
||||
/**
|
||||
* Finds the latest version by post id and post status.
|
||||
*
|
||||
* @param postId post id
|
||||
* @param status record status to query
|
||||
* @return a {@link ContentPatchLog} record
|
||||
*/
|
||||
ContentPatchLog findFirstByPostIdAndStatusOrderByVersionDesc(Integer postId, PostStatus status);
|
||||
|
||||
/**
|
||||
* Finds the latest version by post id.
|
||||
*
|
||||
* @param postId post id to query
|
||||
* @return a {@link ContentPatchLog} record of the latest version queried bby post id
|
||||
*/
|
||||
ContentPatchLog findFirstByPostIdOrderByVersionDesc(Integer postId);
|
||||
|
||||
/**
|
||||
* Finds all records below the specified version number by post id and status.
|
||||
*
|
||||
* @param postId post id
|
||||
* @param version version number
|
||||
* @param status record status
|
||||
* @return records below the specified version
|
||||
*/
|
||||
@Query("from ContentPatchLog c where c.postId = :postId and c.version <= :version and c"
|
||||
+ ".status=:status order by c.version desc")
|
||||
List<ContentPatchLog> findByPostIdAndStatusAndVersionLessThan(Integer postId, Integer version,
|
||||
PostStatus status);
|
||||
|
||||
/**
|
||||
* Finds by post id and version
|
||||
*
|
||||
* @param postId post id
|
||||
* @param version version number
|
||||
* @return a {@link ContentPatchLog} record queried by post id and version
|
||||
*/
|
||||
ContentPatchLog findByPostIdAndVersion(Integer postId, Integer version);
|
||||
|
||||
/**
|
||||
* Finds all records by post id and status and based on version number descending order
|
||||
*
|
||||
* @param postId post id
|
||||
* @param status status
|
||||
* @return a list of {@link ContentPatchLog} queried by post id and status
|
||||
*/
|
||||
List<ContentPatchLog> findAllByPostIdAndStatusOrderByVersionDesc(Integer postId,
|
||||
PostStatus status);
|
||||
|
||||
/**
|
||||
* Finds all records by post id.
|
||||
*
|
||||
* @param postId post id to query
|
||||
* @return a list of {@link ContentPatchLog} queried by post id
|
||||
*/
|
||||
List<ContentPatchLog> findAllByPostId(Integer postId);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package run.halo.app.repository;
|
||||
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.repository.base.BaseRepository;
|
||||
|
||||
/**
|
||||
* Base content repository.
|
||||
*
|
||||
* @author guqing
|
||||
* @date 2021-12-18
|
||||
*/
|
||||
public interface ContentRepository extends BaseRepository<Content, Integer> {
|
||||
|
||||
}
|
|
@ -215,18 +215,6 @@ public interface BasePostRepository<POST extends BasePost> extends BaseRepositor
|
|||
@Query("update BasePost p set p.likes = p.likes + :likes where p.id = :postId")
|
||||
int updateLikes(@Param("likes") long likes, @Param("postId") @NonNull Integer postId);
|
||||
|
||||
/**
|
||||
* Updates post original content.
|
||||
*
|
||||
* @param content content could be blank but disallow to be null
|
||||
* @param postId post id must not be null
|
||||
* @return updated rows
|
||||
*/
|
||||
@Modifying
|
||||
@Query("update BasePost p set p.originalContent = :content where p.id = :postId")
|
||||
int updateOriginalContent(@Param("content") @NonNull String content,
|
||||
@Param("postId") @NonNull Integer postId);
|
||||
|
||||
/**
|
||||
* Updates post status by post id.
|
||||
*
|
||||
|
@ -238,16 +226,4 @@ public interface BasePostRepository<POST extends BasePost> extends BaseRepositor
|
|||
@Query("update BasePost p set p.status = :status where p.id = :postId")
|
||||
int updateStatus(@Param("status") @NonNull PostStatus status,
|
||||
@Param("postId") @NonNull Integer postId);
|
||||
|
||||
/**
|
||||
* Updates post format content by post id.
|
||||
*
|
||||
* @param formatContent format content must not be null.
|
||||
* @param postId post id must not be null.
|
||||
* @return updated rows.
|
||||
*/
|
||||
@Modifying
|
||||
@Query("update BasePost p set p.formatContent = :formatContent where p.id = :postId")
|
||||
int updateFormatContent(@Param("formatContent") @NonNull String formatContent,
|
||||
@Param("postId") @NonNull Integer postId);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
package run.halo.app.service;
|
||||
|
||||
import java.util.List;
|
||||
import run.halo.app.exception.NotFoundException;
|
||||
import run.halo.app.model.entity.Content.ContentDiff;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.entity.ContentPatchLog;
|
||||
|
||||
/**
|
||||
* Content patch log service.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2022-01-04
|
||||
*/
|
||||
public interface ContentPatchLogService {
|
||||
|
||||
/**
|
||||
* Create or update content patch log by post content.
|
||||
*
|
||||
* @param postId post id must not be null.
|
||||
* @param content post formatted content must not be null.
|
||||
* @param originalContent post original content must not be null.
|
||||
* @return created or updated content patch log record.
|
||||
*/
|
||||
ContentPatchLog createOrUpdate(Integer postId, String content, String originalContent);
|
||||
|
||||
/**
|
||||
* Apply content patch to v1.
|
||||
*
|
||||
* @param patchLog content patch log
|
||||
* @return real content of the post.
|
||||
*/
|
||||
PatchedContent applyPatch(ContentPatchLog patchLog);
|
||||
|
||||
/**
|
||||
* generate content diff based v1.
|
||||
*
|
||||
* @param postId post id must not be null.
|
||||
* @param content post formatted content must not be null.
|
||||
* @param originalContent post original content must not be null.
|
||||
* @return a content diff object.
|
||||
*/
|
||||
ContentDiff generateDiff(Integer postId, String content, String originalContent);
|
||||
|
||||
/**
|
||||
* Creates or updates the {@link ContentPatchLog}.
|
||||
*
|
||||
* @param contentPatchLog param to create or update
|
||||
*/
|
||||
void save(ContentPatchLog contentPatchLog);
|
||||
|
||||
/**
|
||||
* Gets the patch log record of the draft status of the content by post id.
|
||||
*
|
||||
* @param postId post id.
|
||||
* @return content patch log record.
|
||||
*/
|
||||
ContentPatchLog getDraftByPostId(Integer postId);
|
||||
|
||||
/**
|
||||
* Gets content patch log by id.
|
||||
*
|
||||
* @param id id
|
||||
* @return a content patch log
|
||||
* @throws NotFoundException if record not found.
|
||||
*/
|
||||
ContentPatchLog getById(Integer id);
|
||||
|
||||
/**
|
||||
* Gets content patch log by post id.
|
||||
*
|
||||
* @param postId a post id
|
||||
* @return a real content of post.
|
||||
*/
|
||||
PatchedContent getByPostId(Integer postId);
|
||||
|
||||
/**
|
||||
* Gets real post content by id.
|
||||
*
|
||||
* @param id id
|
||||
* @return Actual content of patches applied based on V1 version.
|
||||
*/
|
||||
PatchedContent getPatchedContentById(Integer id);
|
||||
|
||||
/**
|
||||
* Permanently delete post contentPatchLog by post id.
|
||||
*
|
||||
* @param postId post id
|
||||
* @return deleted post content patch logs.
|
||||
*/
|
||||
List<ContentPatchLog> removeByPostId(Integer postId);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package run.halo.app.service;
|
||||
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.ContentPatchLog;
|
||||
import run.halo.app.service.base.CrudService;
|
||||
|
||||
/**
|
||||
* Base content service interface.
|
||||
*
|
||||
* @author guqing
|
||||
* @date 2022-01-07
|
||||
*/
|
||||
public interface ContentService extends CrudService<Content, Integer> {
|
||||
|
||||
/**
|
||||
* <p>Publish post content.</p>
|
||||
* <ul>
|
||||
* <li>Copy the latest record in {@link ContentPatchLog} to the {@link Content}.
|
||||
* <li>Set status to PUBLISHED.
|
||||
* <li>Set patchLogId to the latest.
|
||||
* </ul>
|
||||
*
|
||||
* @param postId post id
|
||||
* @return published content record.
|
||||
*/
|
||||
Content publishContent(Integer postId);
|
||||
|
||||
/**
|
||||
* If the content record does not exist, it will be created; otherwise, it will be updated.
|
||||
*
|
||||
* @param postId post id
|
||||
* @param content post format content
|
||||
* @param originalContent post original content
|
||||
*/
|
||||
void createOrUpdateDraftBy(Integer postId, String content, String originalContent);
|
||||
|
||||
/**
|
||||
* There is a draft being drafted.
|
||||
*
|
||||
* @param postId post id
|
||||
* @return {@code true} if find a draft record from {@link ContentPatchLog},
|
||||
* otherwise {@code false}
|
||||
*/
|
||||
Boolean draftingInProgress(Integer postId);
|
||||
}
|
|
@ -10,6 +10,8 @@ import run.halo.app.model.dto.post.BasePostDetailDTO;
|
|||
import run.halo.app.model.dto.post.BasePostMinimalDTO;
|
||||
import run.halo.app.model.dto.post.BasePostSimpleDTO;
|
||||
import run.halo.app.model.entity.BasePost;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
|
||||
/**
|
||||
|
@ -17,6 +19,7 @@ import run.halo.app.model.enums.PostStatus;
|
|||
*
|
||||
* @author johnniang
|
||||
* @author ryanwang
|
||||
* @author guqing
|
||||
* @date 2019-04-24
|
||||
*/
|
||||
public interface BasePostService<POST extends BasePost> extends CrudService<POST, Integer> {
|
||||
|
@ -52,6 +55,15 @@ public interface BasePostService<POST extends BasePost> extends CrudService<POST
|
|||
@NonNull
|
||||
POST getBySlug(@NonNull String slug);
|
||||
|
||||
/**
|
||||
* Get post with the latest content by id.
|
||||
* content from patch log.
|
||||
*
|
||||
* @param postId post id.
|
||||
* @return post with the latest content.
|
||||
*/
|
||||
POST getWithLatestContentById(Integer postId);
|
||||
|
||||
/**
|
||||
* Gets post by post status and slug.
|
||||
*
|
||||
|
@ -72,6 +84,23 @@ public interface BasePostService<POST extends BasePost> extends CrudService<POST
|
|||
@NonNull
|
||||
POST getBy(@NonNull PostStatus status, @NonNull Integer id);
|
||||
|
||||
/**
|
||||
* Gets content by post id.
|
||||
*
|
||||
* @param id post id.
|
||||
* @return a content of post.
|
||||
*/
|
||||
Content getContentById(Integer id);
|
||||
|
||||
/**
|
||||
* Gets the latest content by id.
|
||||
* content from patch log.
|
||||
*
|
||||
* @param id post id.
|
||||
* @return a latest content from patchLog of post.
|
||||
*/
|
||||
PatchedContent getLatestContentById(Integer id);
|
||||
|
||||
/**
|
||||
* Lists all posts by post status.
|
||||
*
|
||||
|
@ -275,7 +304,8 @@ public interface BasePostService<POST extends BasePost> extends CrudService<POST
|
|||
* @return updated post
|
||||
*/
|
||||
@NonNull
|
||||
POST updateDraftContent(@Nullable String content, @NonNull Integer postId);
|
||||
POST updateDraftContent(@Nullable String content, String originalContent,
|
||||
@NonNull Integer postId);
|
||||
|
||||
/**
|
||||
* Updates post status.
|
||||
|
|
|
@ -27,10 +27,14 @@ import run.halo.app.model.dto.post.BasePostDetailDTO;
|
|||
import run.halo.app.model.dto.post.BasePostMinimalDTO;
|
||||
import run.halo.app.model.dto.post.BasePostSimpleDTO;
|
||||
import run.halo.app.model.entity.BasePost;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.enums.PostEditorType;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
import run.halo.app.model.properties.PostProperties;
|
||||
import run.halo.app.repository.base.BasePostRepository;
|
||||
import run.halo.app.service.ContentPatchLogService;
|
||||
import run.halo.app.service.ContentService;
|
||||
import run.halo.app.service.OptionService;
|
||||
import run.halo.app.service.base.AbstractCrudService;
|
||||
import run.halo.app.service.base.BasePostService;
|
||||
|
@ -44,6 +48,7 @@ import run.halo.app.utils.ServiceUtils;
|
|||
*
|
||||
* @author johnniang
|
||||
* @author ryanwang
|
||||
* @author guqing
|
||||
* @date 2019-04-24
|
||||
*/
|
||||
@Slf4j
|
||||
|
@ -54,15 +59,23 @@ public abstract class BasePostServiceImpl<POST extends BasePost>
|
|||
|
||||
private final OptionService optionService;
|
||||
|
||||
private final ContentService contentService;
|
||||
|
||||
private final ContentPatchLogService contentPatchLogService;
|
||||
|
||||
private static final Pattern summaryPattern = Pattern.compile("\t|\r|\n");
|
||||
|
||||
private static final Pattern BLANK_PATTERN = Pattern.compile("\\s");
|
||||
|
||||
public BasePostServiceImpl(BasePostRepository<POST> basePostRepository,
|
||||
OptionService optionService) {
|
||||
OptionService optionService,
|
||||
ContentService contentService,
|
||||
ContentPatchLogService contentPatchLogService) {
|
||||
super(basePostRepository);
|
||||
this.basePostRepository = basePostRepository;
|
||||
this.optionService = optionService;
|
||||
this.contentService = contentService;
|
||||
this.contentPatchLogService = contentPatchLogService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -111,6 +124,11 @@ public abstract class BasePostServiceImpl<POST extends BasePost>
|
|||
return postOptional.orElseThrow(() -> new NotFoundException("查询不到该文章的信息").setErrorData(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PatchedContent getLatestContentById(Integer id) {
|
||||
return contentPatchLogService.getByPostId(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<POST> listAllBy(PostStatus status) {
|
||||
Assert.notNull(status, "Post status must not be null");
|
||||
|
@ -283,33 +301,46 @@ public abstract class BasePostServiceImpl<POST extends BasePost>
|
|||
@Transactional
|
||||
public POST createOrUpdateBy(POST post) {
|
||||
Assert.notNull(post, "Post must not be null");
|
||||
PostStatus postStatus = post.getStatus();
|
||||
PatchedContent postContent = post.getContent();
|
||||
String originalContent = postContent.getOriginalContent();
|
||||
if (originalContent != null) {
|
||||
// CS304 issue link : https://github.com/halo-dev/halo/issues/1224
|
||||
// Render content and set word count
|
||||
if (post.getEditorType().equals(PostEditorType.MARKDOWN)) {
|
||||
postContent.setContent(MarkdownUtils.renderHtml(originalContent));
|
||||
|
||||
String originalContent = post.getOriginalContent();
|
||||
post.setWordCount(htmlFormatWordCount(postContent.getContent()));
|
||||
} else {
|
||||
postContent.setContent(originalContent);
|
||||
|
||||
// CS304 issue link : https://github.com/halo-dev/halo/issues/1224
|
||||
// Render content and set word count
|
||||
if (post.getEditorType().equals(PostEditorType.MARKDOWN)) {
|
||||
post.setFormatContent(MarkdownUtils.renderHtml(post.getOriginalContent()));
|
||||
|
||||
post.setWordCount(htmlFormatWordCount(post.getFormatContent()));
|
||||
} else {
|
||||
post.setFormatContent(originalContent);
|
||||
|
||||
post.setWordCount(htmlFormatWordCount(originalContent));
|
||||
post.setWordCount(htmlFormatWordCount(originalContent));
|
||||
}
|
||||
post.setContent(postContent);
|
||||
}
|
||||
|
||||
POST savedPost;
|
||||
// Create or update post
|
||||
if (ServiceUtils.isEmptyId(post.getId())) {
|
||||
// The sheet will be created
|
||||
return create(post);
|
||||
savedPost = create(post);
|
||||
contentService.createOrUpdateDraftBy(post.getId(),
|
||||
postContent.getContent(), postContent.getOriginalContent());
|
||||
} else {
|
||||
// The sheet will be updated
|
||||
// Set edit time
|
||||
post.setEditTime(DateUtils.now());
|
||||
contentService.createOrUpdateDraftBy(post.getId(),
|
||||
postContent.getContent(), postContent.getOriginalContent());
|
||||
// Update it
|
||||
savedPost = update(post);
|
||||
}
|
||||
|
||||
// The sheet will be updated
|
||||
// Set edit time
|
||||
post.setEditTime(DateUtils.now());
|
||||
|
||||
// Update it
|
||||
return update(post);
|
||||
if (PostStatus.PUBLISHED.equals(post.getStatus())
|
||||
|| PostStatus.INTIMATE.equals(post.getStatus())) {
|
||||
contentService.publishContent(post.getId());
|
||||
}
|
||||
return savedPost;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -319,8 +350,11 @@ public abstract class BasePostServiceImpl<POST extends BasePost>
|
|||
if (StringUtils.isNotBlank(post.getPassword())) {
|
||||
String tip = "The post is encrypted by author";
|
||||
post.setSummary(tip);
|
||||
post.setOriginalContent(tip);
|
||||
post.setFormatContent(tip);
|
||||
|
||||
Content postContent = new Content();
|
||||
postContent.setContent(tip);
|
||||
postContent.setOriginalContent(tip);
|
||||
post.setContent(PatchedContent.of(postContent));
|
||||
}
|
||||
|
||||
return post;
|
||||
|
@ -358,9 +392,11 @@ public abstract class BasePostServiceImpl<POST extends BasePost>
|
|||
BasePostSimpleDTO basePostSimpleDTO = new BasePostSimpleDTO().convertFrom(post);
|
||||
|
||||
// Set summary
|
||||
if (StringUtils.isBlank(basePostSimpleDTO.getSummary())) {
|
||||
basePostSimpleDTO.setSummary(generateSummary(post.getFormatContent()));
|
||||
}
|
||||
generateAndSetSummaryIfAbsent(post, basePostSimpleDTO);
|
||||
|
||||
// Post currently drafting in process
|
||||
Boolean isInProcess = contentService.draftingInProgress(post.getId());
|
||||
basePostSimpleDTO.setInProgress(isInProcess);
|
||||
|
||||
return basePostSimpleDTO;
|
||||
}
|
||||
|
@ -387,30 +423,33 @@ public abstract class BasePostServiceImpl<POST extends BasePost>
|
|||
public BasePostDetailDTO convertToDetail(POST post) {
|
||||
Assert.notNull(post, "Post must not be null");
|
||||
|
||||
return new BasePostDetailDTO().convertFrom(post);
|
||||
BasePostDetailDTO postDetail = new BasePostDetailDTO().convertFrom(post);
|
||||
|
||||
// Post currently drafting in process
|
||||
Boolean isInProcess = contentService.draftingInProgress(post.getId());
|
||||
postDetail.setInProgress(isInProcess);
|
||||
|
||||
return postDetail;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public POST updateDraftContent(String content, Integer postId) {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public POST updateDraftContent(String content, String originalContent, Integer postId) {
|
||||
Assert.isTrue(!ServiceUtils.isEmptyId(postId), "Post id must not be empty");
|
||||
|
||||
if (content == null) {
|
||||
content = "";
|
||||
if (originalContent == null) {
|
||||
originalContent = "";
|
||||
}
|
||||
|
||||
POST post = getById(postId);
|
||||
|
||||
if (!StringUtils.equals(content, post.getOriginalContent())) {
|
||||
// If content is different with database, then update database
|
||||
int updatedRows = basePostRepository.updateOriginalContent(content, postId);
|
||||
if (updatedRows != 1) {
|
||||
throw new ServiceException(
|
||||
"Failed to update original content of post with id " + postId);
|
||||
}
|
||||
// Set the content
|
||||
post.setOriginalContent(content);
|
||||
if (PostEditorType.MARKDOWN.equals(post.getEditorType())) {
|
||||
content = MarkdownUtils.renderHtml(originalContent);
|
||||
} else {
|
||||
content = originalContent;
|
||||
}
|
||||
contentService.createOrUpdateDraftBy(postId, content, originalContent);
|
||||
|
||||
post.setContent(getLatestContentById(postId));
|
||||
|
||||
return post;
|
||||
}
|
||||
|
@ -438,15 +477,8 @@ public abstract class BasePostServiceImpl<POST extends BasePost>
|
|||
// Sync content
|
||||
if (PostStatus.PUBLISHED.equals(status)) {
|
||||
// If publish this post, then convert the formatted content
|
||||
String formatContent = MarkdownUtils.renderHtml(post.getOriginalContent());
|
||||
int updatedRows = basePostRepository.updateFormatContent(formatContent, postId);
|
||||
|
||||
if (updatedRows != 1) {
|
||||
throw new ServiceException(
|
||||
"Failed to update post format content of post with id " + postId);
|
||||
}
|
||||
|
||||
post.setFormatContent(formatContent);
|
||||
Content postContent = contentService.publishContent(postId);
|
||||
post.setContent(PatchedContent.of(postContent));
|
||||
}
|
||||
|
||||
return post;
|
||||
|
@ -495,6 +527,12 @@ public abstract class BasePostServiceImpl<POST extends BasePost>
|
|||
return super.update(post);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Content getContentById(Integer postId) {
|
||||
Assert.notNull(postId, "The postId must not be null.");
|
||||
return contentService.getById(postId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the slug is exist.
|
||||
*
|
||||
|
@ -535,6 +573,22 @@ public abstract class BasePostServiceImpl<POST extends BasePost>
|
|||
return StringUtils.substring(text, 0, summaryLength);
|
||||
}
|
||||
|
||||
protected <T extends BasePostSimpleDTO> void generateAndSetSummaryIfAbsent(POST post,
|
||||
T postVo) {
|
||||
Assert.notNull(post, "The post must not be null.");
|
||||
if (StringUtils.isNotBlank(postVo.getSummary())) {
|
||||
return;
|
||||
}
|
||||
|
||||
PatchedContent patchedContent = post.getContentOfNullable();
|
||||
if (patchedContent == null) {
|
||||
Content postContent = getContentById(post.getId());
|
||||
postVo.setSummary(generateSummary(postContent.getContent()));
|
||||
} else {
|
||||
postVo.setSummary(generateSummary(patchedContent.getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
// CS304 issue link : https://github.com/halo-dev/halo/issues/1224
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,238 @@
|
|||
package run.halo.app.service.impl;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.domain.Example;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.exception.NotFoundException;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Content.ContentDiff;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.entity.ContentPatchLog;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
import run.halo.app.repository.ContentPatchLogRepository;
|
||||
import run.halo.app.repository.ContentRepository;
|
||||
import run.halo.app.service.ContentPatchLogService;
|
||||
import run.halo.app.utils.PatchUtils;
|
||||
|
||||
/**
|
||||
* Content patch log service.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2022-01-04
|
||||
*/
|
||||
@Service
|
||||
public class ContentPatchLogServiceImpl implements ContentPatchLogService {
|
||||
|
||||
/**
|
||||
* base version of content patch log.
|
||||
*/
|
||||
public static final int BASE_VERSION = 1;
|
||||
|
||||
private final ContentPatchLogRepository contentPatchLogRepository;
|
||||
|
||||
private final ContentRepository contentRepository;
|
||||
|
||||
public ContentPatchLogServiceImpl(ContentPatchLogRepository contentPatchLogRepository,
|
||||
ContentRepository contentRepository) {
|
||||
this.contentPatchLogRepository = contentPatchLogRepository;
|
||||
this.contentRepository = contentRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets post content by post id.
|
||||
*
|
||||
* @param postId post id
|
||||
* @return a post content of postId
|
||||
*/
|
||||
protected Optional<Content> getContentByPostId(Integer postId) {
|
||||
return contentRepository.findById(postId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ContentPatchLog createOrUpdate(Integer postId, String content, String originalContent) {
|
||||
Integer version = getVersionByPostId(postId);
|
||||
if (existDraftBy(postId)) {
|
||||
return updateDraftBy(postId, content, originalContent);
|
||||
}
|
||||
return createDraftContent(postId, version, content, originalContent);
|
||||
}
|
||||
|
||||
private Integer getVersionByPostId(Integer postId) {
|
||||
Integer version;
|
||||
ContentPatchLog latestPatchLog =
|
||||
contentPatchLogRepository.findFirstByPostIdOrderByVersionDesc(postId);
|
||||
|
||||
if (latestPatchLog == null) {
|
||||
// There is no patchLog record
|
||||
version = 1;
|
||||
} else if (PostStatus.PUBLISHED.equals(latestPatchLog.getStatus())) {
|
||||
// There is no draft, a draft record needs to be created
|
||||
// so the version number needs to be incremented
|
||||
version = latestPatchLog.getVersion() + 1;
|
||||
} else {
|
||||
// There is a draft record,Only the content needs to be updated
|
||||
// so the version number remains unchanged
|
||||
version = latestPatchLog.getVersion();
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void save(ContentPatchLog contentPatchLog) {
|
||||
contentPatchLogRepository.save(contentPatchLog);
|
||||
}
|
||||
|
||||
private ContentPatchLog createDraftContent(Integer postId, Integer version,
|
||||
String formatContent, String originalContent) {
|
||||
ContentPatchLog contentPatchLog =
|
||||
buildPatchLog(postId, version, formatContent, originalContent);
|
||||
|
||||
// Sets the upstream version of the current version.
|
||||
Integer sourceId = getContentByPostId(postId)
|
||||
.map(Content::getPatchLogId)
|
||||
.orElse(0);
|
||||
contentPatchLog.setSourceId(sourceId);
|
||||
|
||||
contentPatchLogRepository.save(contentPatchLog);
|
||||
return contentPatchLog;
|
||||
}
|
||||
|
||||
private ContentPatchLog buildPatchLog(Integer postId, Integer version, String formatContent,
|
||||
String originalContent) {
|
||||
ContentPatchLog contentPatchLog = new ContentPatchLog();
|
||||
if (Objects.equals(version, BASE_VERSION)) {
|
||||
contentPatchLog.setContentDiff(formatContent);
|
||||
contentPatchLog.setOriginalContentDiff(originalContent);
|
||||
} else {
|
||||
ContentDiff contentDiff = generateDiff(postId, formatContent, originalContent);
|
||||
contentPatchLog.setContentDiff(contentDiff.getDiff());
|
||||
contentPatchLog.setOriginalContentDiff(contentDiff.getOriginalDiff());
|
||||
}
|
||||
contentPatchLog.setPostId(postId);
|
||||
contentPatchLog.setStatus(PostStatus.DRAFT);
|
||||
ContentPatchLog latestPatchLog =
|
||||
contentPatchLogRepository.findFirstByPostIdOrderByVersionDesc(postId);
|
||||
if (latestPatchLog != null) {
|
||||
contentPatchLog.setVersion(latestPatchLog.getVersion() + 1);
|
||||
} else {
|
||||
contentPatchLog.setVersion(BASE_VERSION);
|
||||
}
|
||||
|
||||
return contentPatchLog;
|
||||
}
|
||||
|
||||
private boolean existDraftBy(Integer postId) {
|
||||
ContentPatchLog contentPatchLog = new ContentPatchLog();
|
||||
contentPatchLog.setPostId(postId);
|
||||
contentPatchLog.setStatus(PostStatus.DRAFT);
|
||||
Example<ContentPatchLog> example = Example.of(contentPatchLog);
|
||||
return contentPatchLogRepository.exists(example);
|
||||
}
|
||||
|
||||
private ContentPatchLog updateDraftBy(Integer postId, String formatContent,
|
||||
String originalContent) {
|
||||
ContentPatchLog draftPatchLog =
|
||||
contentPatchLogRepository.findFirstByPostIdAndStatusOrderByVersionDesc(postId,
|
||||
PostStatus.DRAFT);
|
||||
// Is the draft version 1
|
||||
if (Objects.equals(draftPatchLog.getVersion(), BASE_VERSION)) {
|
||||
// If it is V1, modify the content directly.
|
||||
draftPatchLog.setContentDiff(formatContent);
|
||||
draftPatchLog.setOriginalContentDiff(originalContent);
|
||||
contentPatchLogRepository.save(draftPatchLog);
|
||||
return draftPatchLog;
|
||||
}
|
||||
// Generate content diff.
|
||||
ContentDiff contentDiff = generateDiff(postId, formatContent, originalContent);
|
||||
draftPatchLog.setContentDiff(contentDiff.getDiff());
|
||||
draftPatchLog.setOriginalContentDiff(contentDiff.getOriginalDiff());
|
||||
contentPatchLogRepository.save(draftPatchLog);
|
||||
return draftPatchLog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PatchedContent applyPatch(ContentPatchLog patchLog) {
|
||||
Assert.notNull(patchLog, "The contentRecord must not be null.");
|
||||
Assert.notNull(patchLog.getVersion(), "The contentRecord.version must not be null.");
|
||||
Assert.notNull(patchLog.getPostId(), "The contentRecord.postId must not be null.");
|
||||
|
||||
PatchedContent patchedContent = new PatchedContent();
|
||||
if (patchLog.getVersion() == BASE_VERSION) {
|
||||
patchedContent.setContent(patchLog.getContentDiff());
|
||||
patchedContent.setOriginalContent(patchLog.getOriginalContentDiff());
|
||||
return patchedContent;
|
||||
}
|
||||
|
||||
ContentPatchLog baseContentRecord =
|
||||
contentPatchLogRepository.findByPostIdAndVersion(patchLog.getPostId(), BASE_VERSION);
|
||||
|
||||
String content = PatchUtils.restoreContent(patchLog.getContentDiff(),
|
||||
baseContentRecord.getContentDiff());
|
||||
patchedContent.setContent(content);
|
||||
|
||||
String originalContent = PatchUtils.restoreContent(patchLog.getOriginalContentDiff(),
|
||||
baseContentRecord.getOriginalContentDiff());
|
||||
patchedContent.setOriginalContent(originalContent);
|
||||
return patchedContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentDiff generateDiff(Integer postId, String formatContent, String originalContent) {
|
||||
ContentPatchLog basePatchLog =
|
||||
contentPatchLogRepository.findByPostIdAndVersion(postId, BASE_VERSION);
|
||||
|
||||
ContentDiff contentDiff = new ContentDiff();
|
||||
String contentChanges =
|
||||
PatchUtils.diffToJsonPatch(basePatchLog.getContentDiff(), formatContent);
|
||||
contentDiff.setDiff(contentChanges);
|
||||
|
||||
String originalContentChanges =
|
||||
PatchUtils.diffToJsonPatch(basePatchLog.getOriginalContentDiff(), originalContent);
|
||||
contentDiff.setOriginalDiff(originalContentChanges);
|
||||
return contentDiff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentPatchLog getDraftByPostId(Integer postId) {
|
||||
return contentPatchLogRepository.findFirstByPostIdAndStatusOrderByVersionDesc(postId,
|
||||
PostStatus.DRAFT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PatchedContent getByPostId(Integer postId) {
|
||||
ContentPatchLog contentPatchLog =
|
||||
contentPatchLogRepository.findFirstByPostIdOrderByVersionDesc(postId);
|
||||
if (contentPatchLog == null) {
|
||||
throw new NotFoundException(
|
||||
"Post content patch log was not found or has been deleted.");
|
||||
}
|
||||
return applyPatch(contentPatchLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PatchedContent getPatchedContentById(Integer id) {
|
||||
ContentPatchLog contentPatchLog = getById(id);
|
||||
return applyPatch(contentPatchLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentPatchLog getById(Integer id) {
|
||||
return contentPatchLogRepository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException(
|
||||
"Post content patch log was not found or has been deleted."));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<ContentPatchLog> removeByPostId(Integer postId) {
|
||||
List<ContentPatchLog> patchLogsToDelete = contentPatchLogRepository.findAllByPostId(postId);
|
||||
contentPatchLogRepository.deleteAllInBatch(patchLogsToDelete);
|
||||
return patchLogsToDelete;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package run.halo.app.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.exception.NotFoundException;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.entity.ContentPatchLog;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
import run.halo.app.repository.ContentRepository;
|
||||
import run.halo.app.service.ContentPatchLogService;
|
||||
import run.halo.app.service.ContentService;
|
||||
import run.halo.app.service.base.AbstractCrudService;
|
||||
|
||||
/**
|
||||
* Base content service implementation.
|
||||
*
|
||||
* @author guqing
|
||||
* @date 2022-01-07
|
||||
*/
|
||||
@Service
|
||||
public class ContentServiceImpl extends AbstractCrudService<Content, Integer>
|
||||
implements ContentService {
|
||||
|
||||
private final ContentRepository contentRepository;
|
||||
|
||||
private final ContentPatchLogService contentPatchLogService;
|
||||
|
||||
protected ContentServiceImpl(ContentRepository contentRepository,
|
||||
ContentPatchLogService contentPatchLogService) {
|
||||
super(contentRepository);
|
||||
this.contentRepository = contentRepository;
|
||||
this.contentPatchLogService = contentPatchLogService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void createOrUpdateDraftBy(Integer postId, String content,
|
||||
String originalContent) {
|
||||
Assert.notNull(postId, "The postId must not be null.");
|
||||
// First, we need to save the contentPatchLog
|
||||
ContentPatchLog contentPatchLog =
|
||||
contentPatchLogService.createOrUpdate(postId, content, originalContent);
|
||||
|
||||
// then update the value of headPatchLogId field.
|
||||
Optional<Content> savedContentOptional = contentRepository.findById(postId);
|
||||
if (savedContentOptional.isPresent()) {
|
||||
Content savedContent = savedContentOptional.get();
|
||||
savedContent.setHeadPatchLogId(contentPatchLog.getId());
|
||||
contentRepository.save(savedContent);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the content record does not exist, it needs to be created
|
||||
Content postContent = new Content();
|
||||
postContent.setPatchLogId(contentPatchLog.getId());
|
||||
postContent.setContent(content);
|
||||
postContent.setOriginalContent(originalContent);
|
||||
postContent.setId(postId);
|
||||
postContent.setStatus(PostStatus.DRAFT);
|
||||
postContent.setHeadPatchLogId(contentPatchLog.getId());
|
||||
contentRepository.save(postContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Content publishContent(Integer postId) {
|
||||
ContentPatchLog contentPatchLog = contentPatchLogService.getDraftByPostId(postId);
|
||||
if (contentPatchLog == null) {
|
||||
return contentRepository.getById(postId);
|
||||
}
|
||||
contentPatchLog.setStatus(PostStatus.PUBLISHED);
|
||||
contentPatchLog.setPublishTime(new Date());
|
||||
contentPatchLogService.save(contentPatchLog);
|
||||
|
||||
Content postContent = getById(postId);
|
||||
postContent.setPatchLogId(contentPatchLog.getId());
|
||||
postContent.setStatus(PostStatus.PUBLISHED);
|
||||
|
||||
PatchedContent patchedContent = contentPatchLogService.applyPatch(contentPatchLog);
|
||||
postContent.setContent(patchedContent.getContent());
|
||||
postContent.setOriginalContent(patchedContent.getOriginalContent());
|
||||
|
||||
contentRepository.save(postContent);
|
||||
|
||||
return postContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Content getById(@NonNull Integer postId) {
|
||||
Assert.notNull(postId, "The postId must not be null.");
|
||||
return contentRepository.findById(postId)
|
||||
.orElseThrow(() -> new NotFoundException("content was not found or has been deleted"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean draftingInProgress(Integer postId) {
|
||||
ContentPatchLog draft = contentPatchLogService.getDraftByPostId(postId);
|
||||
return Objects.nonNull(draft);
|
||||
}
|
||||
}
|
|
@ -16,13 +16,11 @@ import java.util.Objects;
|
|||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.persistence.criteria.CriteriaBuilder.In;
|
||||
import javax.persistence.criteria.Predicate;
|
||||
import javax.persistence.criteria.Root;
|
||||
import javax.persistence.criteria.Subquery;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
@ -41,6 +39,8 @@ import run.halo.app.exception.NotFoundException;
|
|||
import run.halo.app.model.dto.post.BasePostMinimalDTO;
|
||||
import run.halo.app.model.dto.post.BasePostSimpleDTO;
|
||||
import run.halo.app.model.entity.Category;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.entity.Post;
|
||||
import run.halo.app.model.entity.PostCategory;
|
||||
import run.halo.app.model.entity.PostComment;
|
||||
|
@ -63,6 +63,8 @@ import run.halo.app.repository.PostRepository;
|
|||
import run.halo.app.repository.base.BasePostRepository;
|
||||
import run.halo.app.service.AuthorizationService;
|
||||
import run.halo.app.service.CategoryService;
|
||||
import run.halo.app.service.ContentPatchLogService;
|
||||
import run.halo.app.service.ContentService;
|
||||
import run.halo.app.service.OptionService;
|
||||
import run.halo.app.service.PostCategoryService;
|
||||
import run.halo.app.service.PostCommentService;
|
||||
|
@ -99,6 +101,8 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
|
||||
private final PostTagService postTagService;
|
||||
|
||||
private final ContentService postContentService;
|
||||
|
||||
private final PostCategoryService postCategoryService;
|
||||
|
||||
private final PostCommentService postCommentService;
|
||||
|
@ -111,6 +115,8 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
|
||||
private final AuthorizationService authorizationService;
|
||||
|
||||
private final ContentPatchLogService postContentPatchLogService;
|
||||
|
||||
public PostServiceImpl(BasePostRepository<Post> basePostRepository,
|
||||
OptionService optionService,
|
||||
PostRepository postRepository,
|
||||
|
@ -121,8 +127,10 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
PostCommentService postCommentService,
|
||||
ApplicationEventPublisher eventPublisher,
|
||||
PostMetaService postMetaService,
|
||||
AuthorizationService authorizationService) {
|
||||
super(basePostRepository, optionService);
|
||||
AuthorizationService authorizationService,
|
||||
ContentService contentService,
|
||||
ContentPatchLogService contentPatchLogService) {
|
||||
super(basePostRepository, optionService, contentService, contentPatchLogService);
|
||||
this.postRepository = postRepository;
|
||||
this.tagService = tagService;
|
||||
this.categoryService = categoryService;
|
||||
|
@ -133,6 +141,8 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
this.postMetaService = postMetaService;
|
||||
this.optionService = optionService;
|
||||
this.authorizationService = authorizationService;
|
||||
this.postContentService = contentService;
|
||||
this.postContentPatchLogService = contentPatchLogService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -269,6 +279,11 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
.orElseThrow(() -> new NotFoundException("查询不到该文章的信息").setErrorData(slug));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PatchedContent getLatestContentById(Integer id) {
|
||||
return postContentPatchLogService.getByPostId(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Post> removeByIds(Collection<Integer> ids) {
|
||||
if (CollectionUtils.isEmpty(ids)) {
|
||||
|
@ -282,6 +297,17 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
return super.getBySlug(slug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Post getWithLatestContentById(Integer postId) {
|
||||
Post post = getById(postId);
|
||||
Content postContent = getContentById(postId);
|
||||
// Use the head pointer stored in the post content.
|
||||
PatchedContent patchedContent =
|
||||
postContentPatchLogService.getPatchedContentById(postContent.getHeadPatchLogId());
|
||||
post.setContent(patchedContent);
|
||||
return post;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArchiveYearVO> listYearArchives() {
|
||||
// Get all posts
|
||||
|
@ -523,7 +549,8 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
}
|
||||
|
||||
content.append("---\n\n");
|
||||
content.append(post.getOriginalContent());
|
||||
PatchedContent postContent = post.getContent();
|
||||
content.append(postContent.getOriginalContent());
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
|
@ -575,6 +602,10 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
List<PostComment> postComments = postCommentService.removeByPostId(postId);
|
||||
log.debug("Removed post comments: [{}]", postComments);
|
||||
|
||||
// Remove post content
|
||||
Content postContent = postContentService.removeById(postId);
|
||||
log.debug("Removed post content: [{}]", postContent);
|
||||
|
||||
Post deletedPost = super.removeById(postId);
|
||||
|
||||
// Log it
|
||||
|
@ -614,9 +645,7 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
return postPage.map(post -> {
|
||||
PostListVO postListVO = new PostListVO().convertFrom(post);
|
||||
|
||||
if (StringUtils.isBlank(postListVO.getSummary())) {
|
||||
postListVO.setSummary(generateSummary(post.getFormatContent()));
|
||||
}
|
||||
generateAndSetSummaryIfAbsent(post, postListVO);
|
||||
|
||||
Optional.ofNullable(tagListMap.get(post.getId())).orElseGet(LinkedList::new);
|
||||
|
||||
|
@ -646,6 +675,10 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
|
||||
postListVO.setFullPath(buildFullPath(post));
|
||||
|
||||
// Post currently drafting in process
|
||||
Boolean isInProcess = postContentService.draftingInProgress(post.getId());
|
||||
postListVO.setInProgress(isInProcess);
|
||||
|
||||
return postListVO;
|
||||
});
|
||||
}
|
||||
|
@ -678,9 +711,7 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
return posts.stream().map(post -> {
|
||||
PostListVO postListVO = new PostListVO().convertFrom(post);
|
||||
|
||||
if (StringUtils.isBlank(postListVO.getSummary())) {
|
||||
postListVO.setSummary(generateSummary(post.getFormatContent()));
|
||||
}
|
||||
generateAndSetSummaryIfAbsent(post, postListVO);
|
||||
|
||||
Optional.ofNullable(tagListMap.get(post.getId())).orElseGet(LinkedList::new);
|
||||
|
||||
|
@ -742,9 +773,7 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
BasePostSimpleDTO basePostSimpleDTO = new BasePostSimpleDTO().convertFrom(post);
|
||||
|
||||
// Set summary
|
||||
if (StringUtils.isBlank(basePostSimpleDTO.getSummary())) {
|
||||
basePostSimpleDTO.setSummary(generateSummary(post.getFormatContent()));
|
||||
}
|
||||
generateAndSetSummaryIfAbsent(post, basePostSimpleDTO);
|
||||
|
||||
basePostSimpleDTO.setFullPath(buildFullPath(post));
|
||||
|
||||
|
@ -767,10 +796,7 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
|
||||
// Convert to base detail vo
|
||||
PostDetailVO postDetailVO = new PostDetailVO().convertFrom(post);
|
||||
|
||||
if (StringUtils.isBlank(postDetailVO.getSummary())) {
|
||||
postDetailVO.setSummary(generateSummary(post.getFormatContent()));
|
||||
}
|
||||
generateAndSetSummaryIfAbsent(post, postDetailVO);
|
||||
|
||||
// Extract ids
|
||||
Set<Integer> tagIds = ServiceUtils.fetchProperty(tags, Tag::getId);
|
||||
|
@ -794,6 +820,14 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
|
||||
postDetailVO.setFullPath(buildFullPath(post));
|
||||
|
||||
PatchedContent postContent = post.getContent();
|
||||
postDetailVO.setContent(postContent.getContent());
|
||||
postDetailVO.setOriginalContent(postContent.getOriginalContent());
|
||||
|
||||
// Post currently drafting in process
|
||||
Boolean inProgress = postContentService.draftingInProgress(post.getId());
|
||||
postDetailVO.setInProgress(inProgress);
|
||||
|
||||
return postDetailVO;
|
||||
}
|
||||
|
||||
|
@ -903,6 +937,10 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
// Remove authorization every time an post is created or updated.
|
||||
authorizationService.deletePostAuthorization(post.getId());
|
||||
|
||||
// get draft content by head patch log id
|
||||
Content postContent = postContentService.getById(post.getId());
|
||||
post.setContent(
|
||||
postContentPatchLogService.getPatchedContentById(postContent.getHeadPatchLogId()));
|
||||
// Convert to post detail vo
|
||||
return convertTo(post, tags, categories, postMetaList);
|
||||
}
|
||||
|
@ -943,9 +981,8 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
@Override
|
||||
public List<PostMarkdownVO> listPostMarkdowns() {
|
||||
List<Post> allPostList = listAll();
|
||||
List<PostMarkdownVO> result = new ArrayList(allPostList.size());
|
||||
for (int i = 0; i < allPostList.size(); i++) {
|
||||
Post post = allPostList.get(i);
|
||||
List<PostMarkdownVO> result = new ArrayList<>(allPostList.size());
|
||||
for (Post post : allPostList) {
|
||||
result.add(convertToPostMarkdownVo(post));
|
||||
}
|
||||
return result;
|
||||
|
@ -988,11 +1025,12 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
|
|||
tagContent.append(" | ").append(tagName);
|
||||
}
|
||||
}
|
||||
frontMatter.append("tags: ").append(tagContent.toString()).append("\n");
|
||||
frontMatter.append("tags: ").append(tagContent).append("\n");
|
||||
|
||||
frontMatter.append("---\n");
|
||||
postMarkdownVO.setFrontMatter(frontMatter.toString());
|
||||
postMarkdownVO.setOriginalContent(post.getOriginalContent());
|
||||
PatchedContent postContent = post.getContent();
|
||||
postMarkdownVO.setOriginalContent(postContent.getOriginalContent());
|
||||
postMarkdownVO.setTitle(post.getTitle());
|
||||
postMarkdownVO.setSlug(post.getSlug());
|
||||
return postMarkdownVO;
|
||||
|
|
|
@ -10,7 +10,6 @@ import java.util.Optional;
|
|||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
@ -24,6 +23,8 @@ import run.halo.app.exception.AlreadyExistsException;
|
|||
import run.halo.app.exception.NotFoundException;
|
||||
import run.halo.app.model.dto.IndependentSheetDTO;
|
||||
import run.halo.app.model.dto.post.BasePostMinimalDTO;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.entity.Sheet;
|
||||
import run.halo.app.model.entity.SheetComment;
|
||||
import run.halo.app.model.entity.SheetMeta;
|
||||
|
@ -34,6 +35,8 @@ import run.halo.app.model.enums.SheetPermalinkType;
|
|||
import run.halo.app.model.vo.SheetDetailVO;
|
||||
import run.halo.app.model.vo.SheetListVO;
|
||||
import run.halo.app.repository.SheetRepository;
|
||||
import run.halo.app.service.ContentPatchLogService;
|
||||
import run.halo.app.service.ContentService;
|
||||
import run.halo.app.service.OptionService;
|
||||
import run.halo.app.service.SheetCommentService;
|
||||
import run.halo.app.service.SheetMetaService;
|
||||
|
@ -52,7 +55,8 @@ import run.halo.app.utils.ServiceUtils;
|
|||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SheetServiceImpl extends BasePostServiceImpl<Sheet> implements SheetService {
|
||||
public class SheetServiceImpl extends BasePostServiceImpl<Sheet>
|
||||
implements SheetService {
|
||||
|
||||
private final SheetRepository sheetRepository;
|
||||
|
||||
|
@ -66,19 +70,27 @@ public class SheetServiceImpl extends BasePostServiceImpl<Sheet> implements Shee
|
|||
|
||||
private final OptionService optionService;
|
||||
|
||||
private final ContentService sheetContentService;
|
||||
|
||||
private final ContentPatchLogService sheetContentPatchLogService;
|
||||
|
||||
public SheetServiceImpl(SheetRepository sheetRepository,
|
||||
ApplicationEventPublisher eventPublisher,
|
||||
SheetCommentService sheetCommentService,
|
||||
ContentService sheetContentService,
|
||||
SheetMetaService sheetMetaService,
|
||||
ThemeService themeService,
|
||||
OptionService optionService) {
|
||||
super(sheetRepository, optionService);
|
||||
OptionService optionService,
|
||||
ContentPatchLogService sheetContentPatchLogService) {
|
||||
super(sheetRepository, optionService, sheetContentService, sheetContentPatchLogService);
|
||||
this.sheetRepository = sheetRepository;
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.sheetCommentService = sheetCommentService;
|
||||
this.sheetMetaService = sheetMetaService;
|
||||
this.themeService = themeService;
|
||||
this.optionService = optionService;
|
||||
this.sheetContentService = sheetContentService;
|
||||
this.sheetContentPatchLogService = sheetContentPatchLogService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -160,6 +172,17 @@ public class SheetServiceImpl extends BasePostServiceImpl<Sheet> implements Shee
|
|||
.orElseThrow(() -> new NotFoundException("查询不到该页面的信息").setErrorData(slug));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sheet getWithLatestContentById(Integer postId) {
|
||||
Sheet sheet = getById(postId);
|
||||
Content sheetContent = getContentById(postId);
|
||||
// Use the head pointer stored in the post content.
|
||||
PatchedContent patchedContent =
|
||||
sheetContentPatchLogService.getPatchedContentById(sheetContent.getHeadPatchLogId());
|
||||
sheet.setContent(patchedContent);
|
||||
return sheet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sheet getBy(PostStatus status, String slug) {
|
||||
Assert.notNull(status, "Sheet status must not be null");
|
||||
|
@ -208,7 +231,7 @@ public class SheetServiceImpl extends BasePostServiceImpl<Sheet> implements Shee
|
|||
content.append("comments: ").append(!sheet.getDisallowComment()).append("\n");
|
||||
|
||||
content.append("---\n\n");
|
||||
content.append(sheet.getOriginalContent());
|
||||
content.append(sheet.getContent().getOriginalContent());
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
|
@ -258,6 +281,10 @@ public class SheetServiceImpl extends BasePostServiceImpl<Sheet> implements Shee
|
|||
List<SheetComment> sheetComments = sheetCommentService.removeByPostId(id);
|
||||
log.debug("Removed sheet comments: [{}]", sheetComments);
|
||||
|
||||
// Remove sheet content
|
||||
Content sheetContent = sheetContentService.removeById(id);
|
||||
log.debug("Removed sheet content: [{}]", sheetContent);
|
||||
|
||||
Sheet sheet = super.removeById(id);
|
||||
|
||||
// Log it
|
||||
|
@ -286,6 +313,10 @@ public class SheetServiceImpl extends BasePostServiceImpl<Sheet> implements Shee
|
|||
|
||||
sheetListVO.setFullPath(buildFullPath(sheet));
|
||||
|
||||
// Post currently drafting in process
|
||||
Boolean isInProcess = sheetContentService.draftingInProgress(sheet.getId());
|
||||
sheetListVO.setInProgress(isInProcess);
|
||||
|
||||
return sheetListVO;
|
||||
});
|
||||
}
|
||||
|
@ -337,15 +368,21 @@ public class SheetServiceImpl extends BasePostServiceImpl<Sheet> implements Shee
|
|||
sheetDetailVO.setMetaIds(metaIds);
|
||||
sheetDetailVO.setMetas(sheetMetaService.convertTo(metas));
|
||||
|
||||
if (StringUtils.isBlank(sheetDetailVO.getSummary())) {
|
||||
sheetDetailVO.setSummary(generateSummary(sheet.getFormatContent()));
|
||||
}
|
||||
generateAndSetSummaryIfAbsent(sheet, sheetDetailVO);
|
||||
|
||||
sheetDetailVO.setCommentCount(sheetCommentService.countByStatusAndPostId(
|
||||
CommentStatus.PUBLISHED, sheet.getId()));
|
||||
|
||||
sheetDetailVO.setFullPath(buildFullPath(sheet));
|
||||
|
||||
PatchedContent sheetContent = sheet.getContent();
|
||||
sheetDetailVO.setContent(sheetContent.getContent());
|
||||
sheetDetailVO.setOriginalContent(sheetContent.getOriginalContent());
|
||||
|
||||
// Sheet currently drafting in process
|
||||
Boolean inProgress = sheetContentService.draftingInProgress(sheet.getId());
|
||||
sheetDetailVO.setInProgress(inProgress);
|
||||
|
||||
return sheetDetailVO;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package run.halo.app.utils;
|
|||
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
|
@ -71,6 +72,23 @@ public class JsonUtils {
|
|||
return jsonToObject(json, type, DEFAULT_JSON_MAPPER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts json to the object specified type.
|
||||
*
|
||||
* @param json json content must not be blank
|
||||
* @param typeReference object type reference must not be null
|
||||
* @param <T> target object type
|
||||
* @return object specified type
|
||||
* @throws IllegalArgumentException throws when fail to convert
|
||||
*/
|
||||
public static <T> T jsonToObject(String json, TypeReference<T> typeReference) {
|
||||
try {
|
||||
return DEFAULT_JSON_MAPPER.readValue(json, typeReference);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts json to the object specified type.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package run.halo.app.utils;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.github.difflib.DiffUtils;
|
||||
import com.github.difflib.patch.AbstractDelta;
|
||||
import com.github.difflib.patch.ChangeDelta;
|
||||
import com.github.difflib.patch.Chunk;
|
||||
import com.github.difflib.patch.DeleteDelta;
|
||||
import com.github.difflib.patch.DeltaType;
|
||||
import com.github.difflib.patch.InsertDelta;
|
||||
import com.github.difflib.patch.Patch;
|
||||
import com.github.difflib.patch.PatchFailedException;
|
||||
import com.google.common.base.Splitter;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Content patch utilities.
|
||||
*
|
||||
* @author guqing
|
||||
* @date 2021-12-19
|
||||
*/
|
||||
public class PatchUtils {
|
||||
|
||||
private static final Splitter lineSplitter = Splitter.on('\n');
|
||||
|
||||
public static Patch<String> create(String deltasJson) {
|
||||
List<Delta> deltas = JsonUtils.jsonToObject(deltasJson, new TypeReference<>() {});
|
||||
Patch<String> patch = new Patch<>();
|
||||
for (Delta delta : deltas) {
|
||||
StringChunk sourceChunk = delta.getSource();
|
||||
StringChunk targetChunk = delta.getTarget();
|
||||
Chunk<String> orgChunk = new Chunk<>(sourceChunk.getPosition(), sourceChunk.getLines(),
|
||||
sourceChunk.getChangePosition());
|
||||
Chunk<String> revChunk = new Chunk<>(targetChunk.getPosition(), targetChunk.getLines(),
|
||||
targetChunk.getChangePosition());
|
||||
switch (delta.getType()) {
|
||||
case DELETE:
|
||||
patch.addDelta(new DeleteDelta<>(orgChunk, revChunk));
|
||||
break;
|
||||
case INSERT:
|
||||
patch.addDelta(new InsertDelta<>(orgChunk, revChunk));
|
||||
break;
|
||||
case CHANGE:
|
||||
patch.addDelta(new ChangeDelta<>(orgChunk, revChunk));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported delta type.");
|
||||
}
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
|
||||
public static String patchToJson(Patch<String> patch) {
|
||||
List<AbstractDelta<String>> deltas = patch.getDeltas();
|
||||
try {
|
||||
return JsonUtils.objectToJson(deltas);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String restoreContent(String json, String original) {
|
||||
Patch<String> patch = PatchUtils.create(json);
|
||||
try {
|
||||
return String.join("\n", patch.applyTo(breakLine(original)));
|
||||
} catch (PatchFailedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String diffToJsonPatch(String original, String revised) {
|
||||
Patch<String> patch = DiffUtils.diff(breakLine(original), breakLine(revised));
|
||||
return PatchUtils.patchToJson(patch);
|
||||
}
|
||||
|
||||
public static List<String> breakLine(String content) {
|
||||
return lineSplitter.splitToList(content);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Delta {
|
||||
private StringChunk source;
|
||||
private StringChunk target;
|
||||
private DeltaType type;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class StringChunk {
|
||||
private int position;
|
||||
private List<String> lines;
|
||||
private List<Integer> changePosition;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
-- Migrate post content to contents table
|
||||
INSERT INTO contents(post_id, status, patch_log_id, head_patch_log_id, content, original_content, create_time,
|
||||
update_time)
|
||||
SELECT id,
|
||||
status,
|
||||
id,
|
||||
id,
|
||||
format_content,
|
||||
original_content,
|
||||
create_time,
|
||||
update_time
|
||||
FROM posts;
|
||||
|
||||
-- Create content_patch_logs record by posts and contents table record
|
||||
INSERT INTO content_patch_logs(id, post_id, content_diff, original_content_diff, version, status, publish_time,
|
||||
source_id, create_time, update_time)
|
||||
SELECT p.id,
|
||||
p.id,
|
||||
c.content,
|
||||
c.original_content,
|
||||
1,
|
||||
p.status,
|
||||
p.create_time,
|
||||
0,
|
||||
p.create_time,
|
||||
p.update_time
|
||||
FROM contents c
|
||||
INNER JOIN posts p ON p.id = c.post_id;
|
||||
|
||||
-- Allow the original_content to be null
|
||||
alter table posts
|
||||
modify format_content longtext null;
|
||||
alter table posts
|
||||
modify original_content longtext null;
|
|
@ -0,0 +1,177 @@
|
|||
package run.halo.app.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
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.orm.jpa.AutoConfigureDataJpa;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import run.halo.app.model.entity.ContentPatchLog;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
|
||||
/**
|
||||
* Content patch log repository test.
|
||||
*
|
||||
* @author guqing
|
||||
* @date 2022-02-19
|
||||
*/
|
||||
@DataJpaTest
|
||||
@AutoConfigureDataJpa
|
||||
public class ContentPatchLogRepositoryTest {
|
||||
@Autowired
|
||||
private ContentPatchLogRepository contentPatchLogRepository;
|
||||
|
||||
/**
|
||||
* Creates some test data for {@link ContentPatchLog}.
|
||||
*/
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
List<ContentPatchLog> list = new ArrayList<>();
|
||||
|
||||
ContentPatchLog record1 = create(2, "<h2 id=\"关于页面\">关于页面</h2>\n"
|
||||
+ "<p>这是一个自定义页面,你可以在后台的 <code>页面</code> -> <code>所有页面</code> -> "
|
||||
+ "<code>自定义页面</code> 找到它,你可以用于新建关于页面、留言板页面等等。发挥你自己的想象力!</p>\n"
|
||||
+ "<blockquote>\n"
|
||||
+ "<p>这是一篇自动生成的页面,你可以在后台删除它。</p>\n"
|
||||
+ "</blockquote>\n",
|
||||
"## 关于页面\n"
|
||||
+ "\n这是一个自定义页面,你可以在后台的 `页面` -> `所有页面` -> `自定义页面` 找到它,你可以用于新"
|
||||
+ "建关于页面、留言板页面等等。发挥你自己的想象力!\n"
|
||||
+ "\n> 这是一篇自动生成的页面,你可以在后台删除它。",
|
||||
2, new Date(), 0, 1);
|
||||
list.add(record1);
|
||||
|
||||
ContentPatchLog record2 = create(3, "<p><strong>登高</strong></p>\n"
|
||||
+ "<p>风急天高猿啸哀,渚清沙白鸟飞回。</p>\n"
|
||||
+ "<p>无边落木萧萧下,不尽长江滚滚来。</p>\n"
|
||||
+ "<p>万里悲秋常作客,百年多病独登台。</p>\n"
|
||||
+ "<p>艰难苦恨繁霜鬓,潦倒新停浊酒杯。</p>\n",
|
||||
"**登高**\n"
|
||||
+ "\n"
|
||||
+ "风急天高猿啸哀,渚清沙白鸟飞回。\n"
|
||||
+ "\n"
|
||||
+ "无边落木萧萧下,不尽长江滚滚来。\n"
|
||||
+ "\n"
|
||||
+ "万里悲秋常作客,百年多病独登台。\n"
|
||||
+ "\n"
|
||||
+ "艰难苦恨繁霜鬓,潦倒新停浊酒杯。",
|
||||
3, new Date(), 0, 1);
|
||||
list.add(record2);
|
||||
|
||||
ContentPatchLog record3 = create(4, "<p>望岳</p>\n"
|
||||
+ "<p>岱宗夫如何,齐鲁青未了。</p>\n",
|
||||
"望岳\n"
|
||||
+ "\n"
|
||||
+ "岱宗夫如何,齐鲁青未了。\n",
|
||||
4, new Date(), 0, 1);
|
||||
list.add(record3);
|
||||
|
||||
ContentPatchLog record4 = create(5, "[{\"source\":{\"position\":2,\"lines\":[],"
|
||||
+ "\"changePosition\":null},\"target\":{\"position\":2,"
|
||||
+ "\"lines\":[\"<p>造化钟神秀,阴阳割昏晓。</p>\"],\"changePosition\":null},"
|
||||
+ "\"type\":\"INSERT\"}]",
|
||||
"[{\"source\":{\"position\":4,\"lines\":[],\"changePosition\":null},"
|
||||
+ "\"target\":{\"position\":4,\"lines\":[\"造化钟神秀,阴阳割昏晓。\",\"\"],"
|
||||
+ "\"changePosition\":null},\"type\":\"INSERT\"}]",
|
||||
4, new Date(), 4, 2);
|
||||
list.add(record4);
|
||||
|
||||
ContentPatchLog record5 = create(6, "[{\"source\":{\"position\":2,\"lines\":[],"
|
||||
+ "\"changePosition\":null},\"target\":{\"position\":2,"
|
||||
+ "\"lines\":[\"<p>造化钟神秀,阴阳割昏晓。</p>\",\"<p>荡胸生曾云,决眦入归鸟。</p>\"],"
|
||||
+ "\"changePosition\":null},\"type\":\"INSERT\"}]",
|
||||
"[{\"source\":{\"position\":4,\"lines\":[],\"changePosition\":null},"
|
||||
+ "\"target\":{\"position\":4,\"lines\":[\"造化钟神秀,阴阳割昏晓。\",\"\",\"荡胸生曾云,决眦入归鸟。\","
|
||||
+ "\"\"],\"changePosition\":null},\"type\":\"INSERT\"}]",
|
||||
4, new Date(), 5, 3);
|
||||
list.add(record5);
|
||||
|
||||
ContentPatchLog record6 = create(7, "[{\"source\":{\"position\":2,\"lines\":[],"
|
||||
+ "\"changePosition\":null},\"target\":{\"position\":2,"
|
||||
+ "\"lines\":[\"<p>造化钟神秀,阴阳割昏晓。</p>\",\"<p>荡胸生曾云,决眦入归鸟。</p>\","
|
||||
+ "\"<p>会当凌绝顶,一览众山小。</p>\"],\"changePosition\":null},\"type\":\"INSERT\"}]",
|
||||
"[{\"source\":{\"position\":4,\"lines\":[],\"changePosition\":null},"
|
||||
+ "\"target\":{\"position\":4,\"lines\":[\"造化钟神秀,阴阳割昏晓。\",\"\",\"荡胸生曾云,决眦入归鸟。\","
|
||||
+ "\"\",\"会当凌绝顶,一览众山小。\"],\"changePosition\":null},\"type\":\"INSERT\"}]",
|
||||
4, new Date(), 6, 4);
|
||||
list.add(record6);
|
||||
|
||||
ContentPatchLog record7 = create(8, "[{\"source\":{\"position\":2,\"lines\":[],"
|
||||
+ "\"changePosition\":null},\"target\":{\"position\":2,"
|
||||
+ "\"lines\":[\"<p>造化钟神秀,阴阳割昏晓。</p>\",\"<p>荡胸生曾云,决眦入归鸟。</p>\","
|
||||
+ "\"<p>会当凌绝顶,一览众山小。</p>\"],\"changePosition\":null},\"type\":\"INSERT\"}]",
|
||||
"[{\"source\":{\"position\":4,\"lines\":[],\"changePosition\":null},"
|
||||
+ "\"target\":{\"position\":4,\"lines\":[\"造化钟神秀,阴阳割昏晓。\",\"\",\"荡胸生曾云,决眦入归鸟。\","
|
||||
+ "\"\",\"会当凌绝顶,一览众山小。\"],\"changePosition\":null},\"type\":\"INSERT\"}]",
|
||||
4, new Date(), 7, 5);
|
||||
list.add(record7);
|
||||
|
||||
// Save records
|
||||
contentPatchLogRepository.saveAll(list);
|
||||
}
|
||||
|
||||
private ContentPatchLog create(Integer id, String contentDiff, String originalContentDiff,
|
||||
Integer postId, Date publishTime, Integer sourceId, Integer version) {
|
||||
ContentPatchLog record = new ContentPatchLog();
|
||||
record.setId(id);
|
||||
record.setCreateTime(new Date());
|
||||
record.setUpdateTime(new Date());
|
||||
record.setContentDiff(contentDiff);
|
||||
record.setOriginalContentDiff(originalContentDiff);
|
||||
record.setPostId(postId);
|
||||
record.setPublishTime(publishTime);
|
||||
record.setSourceId(sourceId);
|
||||
record.setStatus(PostStatus.PUBLISHED);
|
||||
record.setVersion(version);
|
||||
return record;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findAllByPostId() {
|
||||
List<ContentPatchLog> patchLogs = contentPatchLogRepository.findAllByPostId(4);
|
||||
assertThat(patchLogs).isNotEmpty();
|
||||
assertThat(patchLogs).hasSize(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByPostIdAndVersion() {
|
||||
ContentPatchLog v1 = contentPatchLogRepository.findByPostIdAndVersion(4, 1);
|
||||
|
||||
assertThat(v1).isNotNull();
|
||||
assertThat(v1.getOriginalContentDiff()).isEqualTo("望岳\n\n岱宗夫如何,齐鲁青未了。\n");
|
||||
assertThat(v1.getContentDiff()).isEqualTo("<p>望岳</p>\n<p>岱宗夫如何,齐鲁青未了。</p>\n");
|
||||
assertThat(v1.getSourceId()).isEqualTo(0);
|
||||
assertThat(v1.getStatus()).isEqualTo(PostStatus.PUBLISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findFirstByPostIdOrderByVersionDesc() {
|
||||
ContentPatchLog latest =
|
||||
contentPatchLogRepository.findFirstByPostIdOrderByVersionDesc(4);
|
||||
|
||||
assertThat(latest).isNotNull();
|
||||
assertThat(latest.getId()).isEqualTo(8);
|
||||
assertThat(latest.getVersion()).isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findFirstByPostIdAndStatusOrderByVersionDesc() {
|
||||
// finds the latest draft record
|
||||
ContentPatchLog latestDraft =
|
||||
contentPatchLogRepository.findFirstByPostIdAndStatusOrderByVersionDesc(4,
|
||||
PostStatus.DRAFT);
|
||||
assertThat(latestDraft).isNull();
|
||||
|
||||
// finds the latest published record
|
||||
ContentPatchLog latestPublished =
|
||||
contentPatchLogRepository.findFirstByPostIdAndStatusOrderByVersionDesc(4,
|
||||
PostStatus.PUBLISHED);
|
||||
assertThat(latestPublished).isNotNull();
|
||||
assertThat(latestPublished.getId()).isEqualTo(8);
|
||||
assertThat(latestPublished.getVersion()).isEqualTo(5);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
package run.halo.app.service.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.data.domain.Example;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import run.halo.app.model.entity.Content;
|
||||
import run.halo.app.model.entity.Content.ContentDiff;
|
||||
import run.halo.app.model.entity.Content.PatchedContent;
|
||||
import run.halo.app.model.entity.ContentPatchLog;
|
||||
import run.halo.app.model.enums.PostStatus;
|
||||
import run.halo.app.repository.ContentPatchLogRepository;
|
||||
import run.halo.app.repository.ContentRepository;
|
||||
import run.halo.app.service.ContentPatchLogService;
|
||||
|
||||
/**
|
||||
* Test for content path log service implementation.
|
||||
*
|
||||
* @author guqing
|
||||
* @date 2022-02-19
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
public class ContentPatchLogServiceImplTest {
|
||||
@MockBean
|
||||
private ContentPatchLogRepository contentPatchLogRepository;
|
||||
|
||||
@MockBean
|
||||
private ContentRepository contentRepository;
|
||||
|
||||
private ContentPatchLogService contentPatchLogService;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
contentPatchLogService =
|
||||
new ContentPatchLogServiceImpl(contentPatchLogRepository, contentRepository);
|
||||
|
||||
Content content = new Content();
|
||||
content.setId(2);
|
||||
content.setContent("好雨知时节,当春乃发生。");
|
||||
content.setOriginalContent("<p>好雨知时节,当春乃发生。</p>\n");
|
||||
content.setPatchLogId(1);
|
||||
content.setHeadPatchLogId(2);
|
||||
content.setStatus(PostStatus.PUBLISHED);
|
||||
content.setCreateTime(new Date(1645281361));
|
||||
content.setUpdateTime(new Date(1645281361));
|
||||
|
||||
ContentPatchLog contentPatchLogV1 = new ContentPatchLog();
|
||||
contentPatchLogV1.setId(1);
|
||||
contentPatchLogV1.setSourceId(0);
|
||||
contentPatchLogV1.setPostId(2);
|
||||
contentPatchLogV1.setVersion(1);
|
||||
contentPatchLogV1.setStatus(PostStatus.PUBLISHED);
|
||||
contentPatchLogV1.setCreateTime(new Date());
|
||||
contentPatchLogV1.setUpdateTime(new Date());
|
||||
contentPatchLogV1.setContentDiff("<p>望岳</p>\n<p>岱宗夫如何,齐鲁青未了。</p>\n");
|
||||
contentPatchLogV1.setOriginalContentDiff("望岳\n\n岱宗夫如何,齐鲁青未了。\n");
|
||||
|
||||
ContentPatchLog contentPatchLogV2 = new ContentPatchLog();
|
||||
contentPatchLogV2.setId(2);
|
||||
contentPatchLogV2.setSourceId(1);
|
||||
contentPatchLogV2.setPostId(2);
|
||||
contentPatchLogV2.setVersion(2);
|
||||
contentPatchLogV2.setStatus(PostStatus.DRAFT);
|
||||
contentPatchLogV2.setCreateTime(new Date());
|
||||
contentPatchLogV2.setUpdateTime(new Date());
|
||||
contentPatchLogV2.setContentDiff("[{\"source\":{\"position\":2,\"lines\":[],"
|
||||
+ "\"changePosition\":null},\"target\":{\"position\":2,"
|
||||
+ "\"lines\":[\"<p>造化钟神秀,阴阳割昏晓。</p>\"],\"changePosition\":null},\"type\":\"INSERT\"}]");
|
||||
contentPatchLogV2.setOriginalContentDiff("[{\"source\":{\"position\":4,\"lines\":[],"
|
||||
+ "\"changePosition\":null},\"target\":{\"position\":4,\"lines\":[\"造化钟神秀,阴阳割昏晓。\","
|
||||
+ "\"\"],\"changePosition\":null},\"type\":\"INSERT\"}]");
|
||||
|
||||
when(contentRepository.findById(1)).thenReturn(Optional.empty());
|
||||
when(contentRepository.findById(2)).thenReturn(Optional.of(content));
|
||||
|
||||
when(contentPatchLogRepository.findById(1)).thenReturn(Optional.of(contentPatchLogV1));
|
||||
when(contentPatchLogRepository.getById(1)).thenReturn(contentPatchLogV1);
|
||||
|
||||
when(contentPatchLogRepository.findById(2)).thenReturn(Optional.of(contentPatchLogV2));
|
||||
when(contentPatchLogRepository.getById(2)).thenReturn(contentPatchLogV2);
|
||||
|
||||
ContentPatchLog contentPatchLogExample = new ContentPatchLog();
|
||||
contentPatchLogExample.setPostId(2);
|
||||
contentPatchLogExample.setStatus(PostStatus.DRAFT);
|
||||
Example<ContentPatchLog> example = Example.of(contentPatchLogExample);
|
||||
when(contentPatchLogRepository.exists(example)).thenReturn(true);
|
||||
|
||||
when(contentPatchLogRepository.findFirstByPostIdAndStatusOrderByVersionDesc(2,
|
||||
PostStatus.DRAFT)).thenReturn(contentPatchLogV2);
|
||||
|
||||
when(contentPatchLogRepository.findByPostIdAndVersion(2, 1))
|
||||
.thenReturn(contentPatchLogV1);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createOrUpdate() {
|
||||
// record will be created
|
||||
ContentPatchLog created =
|
||||
contentPatchLogService.createOrUpdate(1, "国破山河在,城春草木深\n", "<p>国破山河在,城春草木深</p>\n");
|
||||
assertThat(created).isNotNull();
|
||||
assertThat(created.getVersion()).isEqualTo(1);
|
||||
assertThat(created.getStatus()).isEqualTo(PostStatus.DRAFT);
|
||||
assertThat(created.getSourceId()).isEqualTo(0);
|
||||
assertThat(created.getContentDiff()).isEqualTo(created.getContentDiff());
|
||||
assertThat(created.getOriginalContentDiff()).isEqualTo(created.getOriginalContentDiff());
|
||||
|
||||
// record will be updated
|
||||
ContentPatchLog updated =
|
||||
contentPatchLogService.createOrUpdate(2, "<p>好雨知时节,当春乃发生。</p>\n", "好雨知时节,当春乃发生。\n");
|
||||
assertThat(updated).isNotNull();
|
||||
assertThat(updated.getId()).isEqualTo(2);
|
||||
assertThat(updated.getContentDiff()).isEqualTo("[{\"source\":{\"position\":0,"
|
||||
+ "\"lines\":[\"<p>望岳</p>\",\"<p>岱宗夫如何,齐鲁青未了。</p>\"],\"changePosition\":null},"
|
||||
+ "\"target\":{\"position\":0,\"lines\":[\"<p>好雨知时节,当春乃发生。</p>\"],"
|
||||
+ "\"changePosition\":null},\"type\":\"CHANGE\"}]");
|
||||
assertThat(updated.getOriginalContentDiff()).isEqualTo("[{\"source"
|
||||
+ "\":{\"position\":0,\"lines\":[\"望岳\"],\"changePosition\":null},"
|
||||
+ "\"target\":{\"position\":0,\"lines\":[\"好雨知时节,当春乃发生。\"],\"changePosition\":null},"
|
||||
+ "\"type\":\"CHANGE\"},{\"source\":{\"position\":2,\"lines\":[\"岱宗夫如何,齐鲁青未了。\",\"\"],"
|
||||
+ "\"changePosition\":null},\"target\":{\"position\":2,\"lines\":[],"
|
||||
+ "\"changePosition\":null},\"type\":\"DELETE\"}]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyPatch() {
|
||||
ContentPatchLog contentPatchLogV2 = new ContentPatchLog();
|
||||
contentPatchLogV2.setId(2);
|
||||
contentPatchLogV2.setSourceId(1);
|
||||
contentPatchLogV2.setPostId(2);
|
||||
contentPatchLogV2.setVersion(2);
|
||||
contentPatchLogV2.setStatus(PostStatus.DRAFT);
|
||||
contentPatchLogV2.setCreateTime(new Date());
|
||||
contentPatchLogV2.setUpdateTime(new Date());
|
||||
contentPatchLogV2.setContentDiff("[{\"source\":{\"position\":2,\"lines\":[],"
|
||||
+ "\"changePosition\":null},\"target\":{\"position\":2,"
|
||||
+ "\"lines\":[\"<p>造化钟神秀,阴阳割昏晓。</p>\"],\"changePosition\":null},\"type\":\"INSERT\"}]");
|
||||
contentPatchLogV2.setOriginalContentDiff("[{\"source\":{\"position\":4,\"lines\":[],"
|
||||
+ "\"changePosition\":null},\"target\":{\"position\":4,\"lines\":[\"造化钟神秀,阴阳割昏晓。\","
|
||||
+ "\"\"],\"changePosition\":null},\"type\":\"INSERT\"}]");
|
||||
|
||||
PatchedContent patchedContent =
|
||||
contentPatchLogService.applyPatch(contentPatchLogV2);
|
||||
|
||||
assertThat(patchedContent).isNotNull();
|
||||
assertThat(patchedContent.getContent()).isEqualTo("<p>望岳</p>\n"
|
||||
+ "<p>岱宗夫如何,齐鲁青未了。</p>\n"
|
||||
+ "<p>造化钟神秀,阴阳割昏晓。</p>\n");
|
||||
assertThat(patchedContent.getOriginalContent()).isEqualTo("望岳\n\n岱宗夫如何,齐鲁青未了。\n"
|
||||
+ "\n造化钟神秀,阴阳割昏晓。\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateDiff() {
|
||||
ContentDiff contentDiff =
|
||||
contentPatchLogService.generateDiff(2, "<p>随风潜入夜,润物细无声。</p>", "随风潜入夜,润物细无声。");
|
||||
|
||||
assertThat(contentDiff).isNotNull();
|
||||
assertThat(contentDiff.getDiff()).isEqualTo("[{\"source\":{\"position\":0,"
|
||||
+ "\"lines\":[\"<p>望岳</p>\",\"<p>岱宗夫如何,齐鲁青未了。</p>\",\"\"],\"changePosition\":null},"
|
||||
+ "\"target\":{\"position\":0,\"lines\":[\"<p>随风潜入夜,润物细无声。</p>\"],"
|
||||
+ "\"changePosition\":null},\"type\":\"CHANGE\"}]");
|
||||
assertThat(contentDiff.getOriginalDiff()).isEqualTo("[{\"source\":{\"position\":0,"
|
||||
+ "\"lines\":[\"望岳\",\"\",\"岱宗夫如何,齐鲁青未了。\",\"\"],\"changePosition\":null},"
|
||||
+ "\"target\":{\"position\":0,\"lines\":[\"随风潜入夜,润物细无声。\"],\"changePosition\":null},"
|
||||
+ "\"type\":\"CHANGE\"}]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getPatchedContentById() {
|
||||
PatchedContent patchedContent = contentPatchLogService.getPatchedContentById(2);
|
||||
|
||||
assertThat(patchedContent).isNotNull();
|
||||
assertThat(patchedContent.getContent()).isEqualTo("<p>望岳</p>\n"
|
||||
+ "<p>岱宗夫如何,齐鲁青未了。</p>\n"
|
||||
+ "<p>造化钟神秀,阴阳割昏晓。</p>\n");
|
||||
assertThat(patchedContent.getOriginalContent()).isEqualTo("望岳\n\n"
|
||||
+ "岱宗夫如何,齐鲁青未了。\n\n"
|
||||
+ "造化钟神秀,阴阳割昏晓。\n");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue