diff --git a/pom.xml b/pom.xml index e6d15581..56db5e04 100644 --- a/pom.xml +++ b/pom.xml @@ -333,7 +333,12 @@ s3 2.25.35 - + + + io.github.java-diff-utils + java-diff-utils + 4.12 + diff --git a/src/main/java/de/holarse/backend/db/repositories/ArticleRevisionRepository.java b/src/main/java/de/holarse/backend/db/repositories/ArticleRevisionRepository.java index b5acd778..d22fdb90 100644 --- a/src/main/java/de/holarse/backend/db/repositories/ArticleRevisionRepository.java +++ b/src/main/java/de/holarse/backend/db/repositories/ArticleRevisionRepository.java @@ -8,6 +8,8 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.Optional; + /** * * @author comrad @@ -18,4 +20,7 @@ public interface ArticleRevisionRepository extends JpaRepository findHistory(@Param("nodeId") final Integer nodeId, final Pageable pageable); + @Query("from ArticleRevision ar where ar.nodeId = :nodeId and ar.revision = :revisionId") + Optional findByRevisionId(@Param("nodeId") final Integer nodeId, @Param("revisionId") final Integer revisionId); + } diff --git a/src/main/java/de/holarse/backend/view/ArticleView.java b/src/main/java/de/holarse/backend/view/ArticleView.java index 871e5f53..a31a5770 100644 --- a/src/main/java/de/holarse/backend/view/ArticleView.java +++ b/src/main/java/de/holarse/backend/view/ArticleView.java @@ -39,6 +39,8 @@ public class ArticleView { private List websiteLinks = new ArrayList<>(); private List youtubeVideos = new ArrayList<>(); private List screenshotViews = new ArrayList<>(); + + private Integer revision; public static ArticleView of(final ArticleRevision ar, final NodeSlug slug) { final ArticleView av = new ArticleView(); @@ -53,6 +55,7 @@ public static ArticleView of(final ArticleRevision ar, final NodeSlug slug) { av.setContent(ar.getContent()); av.setCreated(ar.getCreated().toLocalDateTime()); av.setUpdated(ar.getUpdated().toLocalDateTime()); + av.setRevision(ar.getRevision()); if (StringUtils.isNotBlank(ar.getTitle2())) { av.getAlternativeTitles().add(ar.getTitle2()); } if (StringUtils.isNotBlank(ar.getTitle3())) { av.getAlternativeTitles().add(ar.getTitle3()); } @@ -217,4 +220,12 @@ public List getScreenshots() { public void setScreenshots(List screenshotViews) { this.screenshotViews = screenshotViews; } + + public Integer getRevision() { + return revision; + } + + public void setRevision(Integer revision) { + this.revision = revision; + } } diff --git a/src/main/java/de/holarse/web/controller/RevisionController.java b/src/main/java/de/holarse/web/controller/RevisionController.java index 087f5429..592abb65 100644 --- a/src/main/java/de/holarse/web/controller/RevisionController.java +++ b/src/main/java/de/holarse/web/controller/RevisionController.java @@ -1,5 +1,8 @@ package de.holarse.web.controller; +import com.github.difflib.DiffUtils; +import com.github.difflib.text.DiffRow; +import com.github.difflib.text.DiffRowGenerator; import de.holarse.backend.db.Article; import de.holarse.backend.db.ArticleRevision; import de.holarse.backend.db.NodeSlug; @@ -9,6 +12,7 @@ import de.holarse.backend.types.NodeType; import de.holarse.backend.view.ArticleView; import de.holarse.web.defines.WebDefines; +import de.holarse.web.services.ArticleService; import jakarta.persistence.EntityNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,8 +25,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.servlet.ModelAndView; - import static de.holarse.web.defines.WebDefines.REVISION_DEFAULT_PAGE_SIZE; +import java.util.Arrays; +import java.util.List; @Controller public class RevisionController { @@ -35,6 +40,9 @@ public class RevisionController { @Autowired private ArticleRevisionRepository articleRevisionRepository; + @Autowired + private ArticleService articleService; + @Autowired private NodeSlugRepository nodeSlugRepository; @@ -51,6 +59,8 @@ public ModelAndView revisions(@PathVariable final Integer nodeId, @PageableDefau mv.addObject("view", ArticleView.of(currentArticle.getNodeRevision(), mainSlug)); mv.addObject("revisions", articleRevisionRepository.findHistory(nodeId, pageable)); + + return mv; } @@ -60,9 +70,45 @@ public ModelAndView show(@PathVariable final Integer nodeId, @PathVariable final mv.addObject("title", "Die Linuxspiele-Seite für Linuxspieler"); mv.addObject(WebDefines.DEFAULT_VIEW_ATTRIBUTE_NAME, "sites/revisions/show"); + final ArticleRevision articleRevision = articleRevisionRepository.findByRevisionId(nodeId, revision).orElseThrow(EntityNotFoundException::new); + + final Article article = articleRepository.findByNodeId(nodeId).orElseThrow(EntityNotFoundException::new); + final ArticleView view = articleService.buildArticleView(article, articleRevision); + mv.addObject("nodeid", articleRevision.getNodeId()); + mv.addObject("view", view); return mv; } + @GetMapping("/wiki/{nodeId}/revisions/view/{revision1}/{revision2}") + public ModelAndView diff(@PathVariable final Integer nodeId, @PathVariable final Integer revision1, @PathVariable final Integer revision2, final ModelAndView mv) { + mv.setViewName("layouts/bare"); + mv.addObject("title", "Die Linuxspiele-Seite für Linuxspieler"); + mv.addObject(WebDefines.DEFAULT_VIEW_ATTRIBUTE_NAME, "sites/revisions/diff"); + + final ArticleRevision articleRevision1 = articleRevisionRepository.findByRevisionId(nodeId, revision1).orElseThrow(EntityNotFoundException::new); + final ArticleRevision articleRevision2 = articleRevisionRepository.findByRevisionId(nodeId, revision2).orElseThrow(EntityNotFoundException::new); + + final Article article = articleRepository.findByNodeId(nodeId).orElseThrow(EntityNotFoundException::new); + + final ArticleView view1 = articleService.buildArticleView(article, articleRevision1); + final ArticleView view2 = articleService.buildArticleView(article, articleRevision2); + + final DiffRowGenerator diffGenerator = DiffRowGenerator.create().showInlineDiffs(true).mergeOriginalRevised(true).inlineDiffByWord(true).oldTag(f -> "~").newTag(f -> "**").build(); + final List diffRows = diffGenerator.generateDiffRows(Arrays.asList(view1.getContent().split("\n")), Arrays.asList(view2.getContent().split("\n"))); + + final StringBuilder buffer = new StringBuilder(4096); + for (final DiffRow diffRow : diffRows) { + buffer.append(diffRow.getOldLine()).append("\n"); + buffer.append(diffRow.getNewLine()).append("\n"); + } + + mv.addObject("nodeid", article.getNodeId()); + mv.addObject("view1", view1); + mv.addObject("view2", view2); + mv.addObject("diff", buffer.toString()); + + return mv; + } } diff --git a/src/main/java/de/holarse/web/controller/WikiController.java b/src/main/java/de/holarse/web/controller/WikiController.java index 8fb5d4b2..5e4ee718 100644 --- a/src/main/java/de/holarse/web/controller/WikiController.java +++ b/src/main/java/de/holarse/web/controller/WikiController.java @@ -90,6 +90,9 @@ public class WikiController { @Autowired private EntityLockService entityLockService; + @Autowired + private ArticleService articleService; + @Autowired private Renderer renderer; @@ -151,41 +154,10 @@ public ModelAndView show(@PathVariable("slug") final String slug, final ModelAnd final Article article = articleRepository.findBySlug(slug).orElseThrow(EntityNotFoundException::new); final ArticleRevision articleRevision = article.getNodeRevision(); - final Set tags = article.getTags(); - final List relevantTagGroups = tags.stream().map(t -> t.getTagGroup()).toList(); - final NodeSlug mainSlug = nodeSlugRepository.findMainSlug(articleRevision.getNodeId(), NodeType.article).orElseThrow(EntityNotFoundException::new); - - mv.addObject("nodeid", article.getNodeId()); - - // TODO -// if (article.getNodeStatus().isDeleted() || !article.getNodeStatus().isPublished()) { -// logger.debug("Principal: {}", principal); -// if (principal instanceof HolarsePrincipal holarsePrincipal) { -// if (holarsePrincipal.getAuthorities().stream().filter(a -> a.getAuthority().equalsIgnoreCase(RoleUserTypes.ROLE_USER_ADMIN)).count() > 0) { -// adminOverride = true; -// } -// } -// -// if (!adminOverride) { -// mv.addObject(WebDefines.DEFAULT_VIEW_ATTRIBUTE_NAME, "sites/wiki/blocked"); -// return mv; -// } -// } - - final List websiteLinks = attachmentService.getAttachments(article, attachmentGroupRepository.findByCode(AttachmentGroupType.website.name())); - final List videos = attachmentService.getAttachments(article, attachmentGroupRepository.findByCode(AttachmentGroupType.video.name())); - final List screenshots = attachmentService.getAttachments(article, attachmentGroupRepository.findByCode(AttachmentGroupType.image.name())); - // View zusammenstellen - final ArticleView view = ArticleView.of(articleRevision, mainSlug); - view.setNodeId(article.getNodeId()); - view.setTagList(tags.stream().map(TagView::of).sorted(Comparator.comparingInt(TagView::getWeight).reversed().thenComparing(TagView::getUseCount).reversed().thenComparing(TagView::getName)).toList()); - view.setContent(renderer.render(view.getContent(), null)); - //view.setSlug(mainSlug.getName()); - view.setWebsiteLinks(websiteLinks.stream().map(AttachmentView::of).toList()); - view.setYoutubeVideos(videos.stream().map(YoutubeView::of).toList()); - view.setScreenshots(screenshots.stream().map(ScreenshotView::of).map(ssv -> objectStorageService.patchUrl(ssv)).toList()); + final ArticleView view = articleService.buildArticleView(article, articleRevision); + mv.addObject("nodeid", articleRevision.getNodeId()); mv.addObject("view", view); return mv; @@ -244,7 +216,7 @@ public ModelAndView edit(@PathVariable final Integer nodeId, final ModelAndView return mv.addObject("form", form); } - + @Transactional @PostMapping("{nodeId}") public ModelAndView update(@PathVariable final int nodeId, @ModelAttribute("form") final ArticleForm form, final ModelAndView mv, final Authentication authentication) throws JsonProcessingException { diff --git a/src/main/java/de/holarse/web/services/ArticleService.java b/src/main/java/de/holarse/web/services/ArticleService.java new file mode 100644 index 00000000..8ffcf370 --- /dev/null +++ b/src/main/java/de/holarse/web/services/ArticleService.java @@ -0,0 +1,73 @@ +package de.holarse.web.services; + +import de.holarse.backend.db.*; +import de.holarse.backend.db.repositories.AttachmentGroupRepository; +import de.holarse.backend.db.repositories.NodeSlugRepository; +import de.holarse.backend.types.AttachmentGroupType; +import de.holarse.backend.types.NodeType; +import de.holarse.backend.view.*; +import de.holarse.web.renderer.Renderer; +import jakarta.persistence.EntityNotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +@Service +public class ArticleService { + + @Autowired + private AttachmentService attachmentService; + + @Autowired + private ObjectStorageService objectStorageService; + + @Autowired + private AttachmentGroupRepository attachmentGroupRepository; + + @Autowired + private NodeSlugRepository nodeSlugRepository; + + @Autowired + private Renderer renderer; + + public ArticleView buildArticleView(final Article article, final ArticleRevision articleRevision) { + final Set tags = article.getTags(); + final List relevantTagGroups = tags.stream().map(Tag::getTagGroup).toList(); + final NodeSlug mainSlug = nodeSlugRepository.findMainSlug(articleRevision.getNodeId(), NodeType.article).orElseThrow(EntityNotFoundException::new); + + // TODO +// if (article.getNodeStatus().isDeleted() || !article.getNodeStatus().isPublished()) { +// logger.debug("Principal: {}", principal); +// if (principal instanceof HolarsePrincipal holarsePrincipal) { +// if (holarsePrincipal.getAuthorities().stream().filter(a -> a.getAuthority().equalsIgnoreCase(RoleUserTypes.ROLE_USER_ADMIN)).count() > 0) { +// adminOverride = true; +// } +// } +// +// if (!adminOverride) { +// mv.addObject(WebDefines.DEFAULT_VIEW_ATTRIBUTE_NAME, "sites/wiki/blocked"); +// return mv; +// } +// } + + final List websiteLinks = attachmentService.getAttachments(article, attachmentGroupRepository.findByCode(AttachmentGroupType.website.name())); + final List videos = attachmentService.getAttachments(article, attachmentGroupRepository.findByCode(AttachmentGroupType.video.name())); + final List screenshots = attachmentService.getAttachments(article, attachmentGroupRepository.findByCode(AttachmentGroupType.image.name())); + + // View zusammenstellen + final ArticleView view = ArticleView.of(articleRevision, mainSlug); + view.setNodeId(article.getNodeId()); + view.setTagList(tags.stream().map(TagView::of).sorted(Comparator.comparingInt(TagView::getWeight).reversed().thenComparing(TagView::getUseCount).reversed().thenComparing(TagView::getName)).toList()); + view.setContent(renderer.render(view.getContent(), null)); + //view.setSlug(mainSlug.getName()); + view.setWebsiteLinks(websiteLinks.stream().map(AttachmentView::of).toList()); + view.setYoutubeVideos(videos.stream().map(YoutubeView::of).toList()); + view.setScreenshots(screenshots.stream().map(ScreenshotView::of).map(ssv -> objectStorageService.patchUrl(ssv)).toList()); + + return view; + } + +} diff --git a/src/main/webapp/WEB-INF/templates/sites/revisions/diff.html b/src/main/webapp/WEB-INF/templates/sites/revisions/diff.html new file mode 100644 index 00000000..d6da8cfc --- /dev/null +++ b/src/main/webapp/WEB-INF/templates/sites/revisions/diff.html @@ -0,0 +1,8 @@ +
+
+ Diff zwischen Revision
und
+
+ +
+ +
diff --git a/src/main/webapp/WEB-INF/templates/sites/revisions/show.html b/src/main/webapp/WEB-INF/templates/sites/revisions/show.html index b9f1e749..56569fd6 100644 --- a/src/main/webapp/WEB-INF/templates/sites/revisions/show.html +++ b/src/main/webapp/WEB-INF/templates/sites/revisions/show.html @@ -1,5 +1,8 @@
- Revision + Revision
von
+ +
+
diff --git a/src/main/webapp/WEB-INF/templates/sites/wiki/show.html b/src/main/webapp/WEB-INF/templates/sites/wiki/show.html index fde44464..7d44ad37 100644 --- a/src/main/webapp/WEB-INF/templates/sites/wiki/show.html +++ b/src/main/webapp/WEB-INF/templates/sites/wiki/show.html @@ -25,6 +25,10 @@

Statistik +
+ Aktuelle Revision:
+
+

Tags