personalweb

Archived
git clone git://git.wimdupont.com/personalweb.git
Log | Files | Refs | README | LICENSE

commit fde2c53908e6d18bc738371de5f8f695ad0f86cc
parent a7b0710f946d466b8b119bf60cf67e26d655b55c
Author: Wim Dupont <wim@wimdupont.com>
Date:   Sat, 16 Dec 2023 20:22:15 +0100

blog moved to git


Former-commit-id: 857e141d6deb728e4a81435be1f1cdd0480eff4d
Diffstat:
Msrc/main/java/com/wimdupont/personalweb/api/AffirmationApi.java | 7++++++-
Msrc/main/java/com/wimdupont/personalweb/api/GitlabApi.java | 40++++++++++++++++++++++++++++++----------
Msrc/main/java/com/wimdupont/personalweb/api/dto/Affirmation.java | 5+++--
Asrc/main/java/com/wimdupont/personalweb/api/dto/Commit.java | 11+++++++++++
Msrc/main/java/com/wimdupont/personalweb/api/dto/RepositoryFile.java | 9++++++---
Msrc/main/java/com/wimdupont/personalweb/api/dto/RepositoryTreeItem.java | 5++++-
Msrc/main/java/com/wimdupont/personalweb/controller/web/BlogController.java | 34++++++++++++++++++++--------------
Msrc/main/java/com/wimdupont/personalweb/controller/web/GuideController.java | 35+++++++++++++++++++----------------
Asrc/main/java/com/wimdupont/personalweb/converter/AdocContentMetaToDtoConverter.java | 17+++++++++++++++++
Dsrc/main/java/com/wimdupont/personalweb/converter/GuideMetaToDtoConverter.java | 16----------------
Asrc/main/java/com/wimdupont/personalweb/model/AdocContentType.java | 7+++++++
Dsrc/main/java/com/wimdupont/personalweb/model/GuideMeta.java | 7-------
Asrc/main/java/com/wimdupont/personalweb/model/dao/AdocContent.java | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/com/wimdupont/personalweb/model/dao/Auditable.java | 38++++++++++++++++++++++++++++++++++++++
Dsrc/main/java/com/wimdupont/personalweb/model/dao/Guide.java | 140-------------------------------------------------------------------------------
Asrc/main/java/com/wimdupont/personalweb/model/dao/projection/AdocContentMeta.java | 10++++++++++
Asrc/main/java/com/wimdupont/personalweb/model/dto/AdocContentMetaDto.java | 40++++++++++++++++++++++++++++++++++++++++
Dsrc/main/java/com/wimdupont/personalweb/model/dto/AdocFileDto.java | 41-----------------------------------------
Dsrc/main/java/com/wimdupont/personalweb/model/dto/GuideMetaDto.java | 30------------------------------
Asrc/main/java/com/wimdupont/personalweb/repository/AdocContentRepository.java | 24++++++++++++++++++++++++
Dsrc/main/java/com/wimdupont/personalweb/repository/GuideRepository.java | 20--------------------
Asrc/main/java/com/wimdupont/personalweb/service/AdocContentService.java | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main/java/com/wimdupont/personalweb/service/AdocConverter.java | 10----------
Dsrc/main/java/com/wimdupont/personalweb/service/AdocFileService.java | 59-----------------------------------------------------------
Dsrc/main/java/com/wimdupont/personalweb/service/ArticleService.java | 18------------------
Dsrc/main/java/com/wimdupont/personalweb/service/GuideService.java | 82-------------------------------------------------------------------------------
Asrc/main/java/com/wimdupont/personalweb/service/RepositoryFileService.java | 34++++++++++++++++++++++++++++++++++
Msrc/main/java/com/wimdupont/personalweb/service/RssFeedGenerator.java | 42+++++++++---------------------------------
Msrc/main/java/com/wimdupont/personalweb/service/ScheduledFileGenerator.java | 98++++++++++++++++++++++++++++++++++---------------------------------------------
Msrc/main/java/com/wimdupont/personalweb/util/Constants.java | 12++----------
Asrc/main/resources/db/migration/V1_2__guide-to-adoccontent.sql | 8++++++++
Msrc/main/resources/templates/blog.html | 2+-
Asrc/test/java/com/wimdupont/personalweb/api/dto/CommitMother.java | 16++++++++++++++++
Asrc/test/java/com/wimdupont/personalweb/api/dto/RepositoryFileMother.java | 19+++++++++++++++++++
Asrc/test/java/com/wimdupont/personalweb/converter/AdocContentMetaToDtoConverterTest.java | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/test/java/com/wimdupont/personalweb/converter/GuideMetaToDtoConverterTest.java | 35-----------------------------------
Asrc/test/java/com/wimdupont/personalweb/model/dao/AdocContentMother.java | 27+++++++++++++++++++++++++++
Dsrc/test/java/com/wimdupont/personalweb/model/dao/GuideMother.java | 24------------------------
Asrc/test/java/com/wimdupont/personalweb/service/AdocContentServiceTest.java | 46++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/test/java/com/wimdupont/personalweb/service/GuideServiceTest.java | 99-------------------------------------------------------------------------------
Asrc/test/java/com/wimdupont/personalweb/service/RepositoryFileServiceTest.java | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/test/java/com/wimdupont/personalweb/service/RssFeedGeneratorTest.java | 37++++++++++++++-----------------------
Asrc/test/java/com/wimdupont/personalweb/service/ScheduledFileGeneratorTest.java | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
43 files changed, 852 insertions(+), 751 deletions(-)

diff --git a/src/main/java/com/wimdupont/personalweb/api/AffirmationApi.java b/src/main/java/com/wimdupont/personalweb/api/AffirmationApi.java @@ -19,7 +19,12 @@ public class AffirmationApi { private static final String AFFIRMATION_URL = "https://www.affirmations.dev/"; public Mono<Affirmation> getAffirmation() { - return webClient.get().uri(AFFIRMATION_URL).retrieve().bodyToMono(Affirmation.class).timeout(Duration.ofSeconds(3)).onErrorReturn(new Affirmation("You're the best!")); + return webClient.get() + .uri(AFFIRMATION_URL) + .retrieve() + .bodyToMono(Affirmation.class) + .timeout(Duration.ofSeconds(3)) + .onErrorReturn(new Affirmation("You're the best!")); } } diff --git a/src/main/java/com/wimdupont/personalweb/api/GitlabApi.java b/src/main/java/com/wimdupont/personalweb/api/GitlabApi.java @@ -1,7 +1,9 @@ package com.wimdupont.personalweb.api; +import com.wimdupont.personalweb.api.dto.Commit; import com.wimdupont.personalweb.api.dto.RepositoryFile; import com.wimdupont.personalweb.api.dto.RepositoryTreeItem; +import com.wimdupont.personalweb.model.AdocContentType; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; @@ -13,26 +15,44 @@ import java.util.Optional; @Component public class GitlabApi { - private final WebClient webClient; - private final String repositoryUrl; + private final String guideRepositoryUrl; + private final String blogRepositoryUrl; - public GitlabApi(@Value("${gitlab.repository.url}") String repositoryUrl, + public GitlabApi(@Value("${gitlab.guide.repository.url}") String guideRepositoryUrl, + @Value("${gitlab.blog.repository.url}") String blogRepositoryUrl, WebClient webClient) { - this.repositoryUrl = repositoryUrl; + this.guideRepositoryUrl = guideRepositoryUrl; + this.blogRepositoryUrl = blogRepositoryUrl; this.webClient = webClient; } - public Optional<List<RepositoryTreeItem>> getRepositoryTree() { - return webClient.get().uri(repositoryUrl + "/tree").retrieve() - .bodyToFlux(RepositoryTreeItem.class).timeout(Duration.ofSeconds(5)) + public Optional<List<RepositoryTreeItem>> getRepositoryTree(AdocContentType adocContentType) { + return webClient.get().uri(getRepositoryUrl(adocContentType) + "/tree").retrieve() + .bodyToFlux(RepositoryTreeItem.class) + .timeout(Duration.ofSeconds(5)) .collectList().blockOptional(); } - public Optional<RepositoryFile> getRepositoryFile(String filePath) { - return webClient.get().uri(String.format("%s/files/%s?ref=main", repositoryUrl, filePath)) - .retrieve().bodyToMono(RepositoryFile.class).timeout(Duration.ofSeconds(5)) + public Optional<RepositoryFile> getRepositoryFile(AdocContentType adocContentType, String filePath) { + return webClient.get().uri(String.format("%s/files/%s?ref=main", getRepositoryUrl(adocContentType), filePath)) + .retrieve().bodyToMono(RepositoryFile.class) + .timeout(Duration.ofSeconds(5)) + .blockOptional(); + } + + public Optional<Commit> getCommit(AdocContentType adocContentType, String commitId){ + return webClient.get().uri(String.format("%s/commits/%s", getRepositoryUrl(adocContentType), commitId)) + .retrieve().bodyToMono(Commit.class) + .timeout(Duration.ofSeconds(5)) .blockOptional(); } + private String getRepositoryUrl(AdocContentType adocContentType) { + return switch (adocContentType){ + case GUIDE -> guideRepositoryUrl; + case BLOG_ARTICLE -> blogRepositoryUrl; + }; + } + } diff --git a/src/main/java/com/wimdupont/personalweb/api/dto/Affirmation.java b/src/main/java/com/wimdupont/personalweb/api/dto/Affirmation.java @@ -1,5 +1,6 @@ package com.wimdupont.personalweb.api.dto; -public record Affirmation(String affirmation) { - +public record Affirmation( + String affirmation +) { } diff --git a/src/main/java/com/wimdupont/personalweb/api/dto/Commit.java b/src/main/java/com/wimdupont/personalweb/api/dto/Commit.java @@ -0,0 +1,11 @@ +package com.wimdupont.personalweb.api.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.Instant; + +public record Commit( + @JsonProperty(value = "committed_date") + Instant committedDate +) { +} diff --git a/src/main/java/com/wimdupont/personalweb/api/dto/RepositoryFile.java b/src/main/java/com/wimdupont/personalweb/api/dto/RepositoryFile.java @@ -2,13 +2,16 @@ package com.wimdupont.personalweb.api.dto; import com.fasterxml.jackson.annotation.JsonProperty; -public record RepositoryFile ( +public record RepositoryFile( @JsonProperty(value = "file_path") String path, @JsonProperty(value = "content_sha256") String contentSha256, @JsonProperty(value = "content") - String contentBase64 -){ + String contentBase64, + @JsonProperty(value = "last_commit_id") + String lastCommitId + +) { } diff --git a/src/main/java/com/wimdupont/personalweb/api/dto/RepositoryTreeItem.java b/src/main/java/com/wimdupont/personalweb/api/dto/RepositoryTreeItem.java @@ -1,4 +1,7 @@ package com.wimdupont.personalweb.api.dto; -public record RepositoryTreeItem(String path, String name) { +public record RepositoryTreeItem( + String path, + String name +) { } diff --git a/src/main/java/com/wimdupont/personalweb/controller/web/BlogController.java b/src/main/java/com/wimdupont/personalweb/controller/web/BlogController.java @@ -1,39 +1,45 @@ package com.wimdupont.personalweb.controller.web; -import com.wimdupont.personalweb.service.ArticleService; +import com.wimdupont.personalweb.converter.AdocContentMetaToDtoConverter; +import com.wimdupont.personalweb.model.AdocContentType; +import com.wimdupont.personalweb.service.AdocContentService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import java.io.IOException; - @Controller @RequestMapping("/blog") public class BlogController { - private final ArticleService articleService; + private final AdocContentService adocContentService; + private final AdocContentMetaToDtoConverter adocContentMetaToDtoConverter; - public BlogController(ArticleService articleService) { - this.articleService = articleService; + public BlogController(AdocContentService adocContentService, + AdocContentMetaToDtoConverter adocContentMetaToDtoConverter) { + this.adocContentService = adocContentService; + this.adocContentMetaToDtoConverter = adocContentMetaToDtoConverter; } @GetMapping public String getBlog(Model model) { - model.addAttribute("articles", articleService.getFileDtoList()); + model.addAttribute("articles", adocContentService + .findAllMetaData(AdocContentType.BLOG_ARTICLE).stream() + .map(adocContentMetaToDtoConverter::convertForMetaData) + .toList()); return "blog"; } @GetMapping(value = "/article/{name}") public String getBlogArticle(@PathVariable(value = "name") String name, Model model) { - String article; - try { - article = articleService.getHtmlFileString(name, true); - } catch (IOException e) { - article = String.format("Article with name \"%s\" not found.", name); - } - model.addAttribute("article", article); + var text = adocContentService + .findByByTitle(name, AdocContentType.BLOG_ARTICLE) + .orElseThrow() + .getHtmlText(); + model.addAttribute("article", text != null + ? text + : "Oops, nothing here."); model.addAttribute("title", name); return "article"; } diff --git a/src/main/java/com/wimdupont/personalweb/controller/web/GuideController.java b/src/main/java/com/wimdupont/personalweb/controller/web/GuideController.java @@ -1,42 +1,45 @@ package com.wimdupont.personalweb.controller.web; -import com.wimdupont.personalweb.converter.GuideMetaToDtoConverter; -import com.wimdupont.personalweb.service.GuideService; +import com.wimdupont.personalweb.converter.AdocContentMetaToDtoConverter; +import com.wimdupont.personalweb.model.AdocContentType; +import com.wimdupont.personalweb.service.AdocContentService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import java.util.Optional; - @Controller @RequestMapping("/guides") public class GuideController { - private final GuideService guideService; - private final GuideMetaToDtoConverter guideMetaToDtoConverter; + private final AdocContentService adocContentService; + private final AdocContentMetaToDtoConverter adocContentMetaToDtoConverter; - public GuideController(GuideService guideService, - GuideMetaToDtoConverter guideMetaToDtoConverter) { - this.guideService = guideService; - this.guideMetaToDtoConverter = guideMetaToDtoConverter; + public GuideController(AdocContentService adocContentService, + AdocContentMetaToDtoConverter adocContentMetaToDtoConverter) { + this.adocContentService = adocContentService; + this.adocContentMetaToDtoConverter = adocContentMetaToDtoConverter; } @GetMapping public String getGuides(Model model) { - model.addAttribute("guides", guideService.findAllMetaData().stream() - .map(guideMetaToDtoConverter::convertForMetaData) + model.addAttribute("guides", adocContentService + .findAllMetaData(AdocContentType.GUIDE).stream() + .map(adocContentMetaToDtoConverter::convertForMetaData) .toList()); return "guides"; } @GetMapping(value = "/{name}") public String getGuide(@PathVariable(value = "name") String name, Model model) { - var text = Optional.ofNullable(guideService.findGuideByTitle(name) - .orElseThrow().getHtmlText()) - .orElse("Oops, nothing here."); - model.addAttribute("guide", text); + var text = adocContentService + .findByByTitle(name, AdocContentType.GUIDE) + .orElseThrow() + .getHtmlText(); + model.addAttribute("guide", text != null + ? text + : "Oops, nothing here."); model.addAttribute("title", name); return "guide"; } diff --git a/src/main/java/com/wimdupont/personalweb/converter/AdocContentMetaToDtoConverter.java b/src/main/java/com/wimdupont/personalweb/converter/AdocContentMetaToDtoConverter.java @@ -0,0 +1,17 @@ +package com.wimdupont.personalweb.converter; + +import com.wimdupont.personalweb.model.dao.projection.AdocContentMeta; +import com.wimdupont.personalweb.model.dto.AdocContentMetaDto; +import com.wimdupont.personalweb.util.Constants; +import org.springframework.stereotype.Component; + +@Component +public class AdocContentMetaToDtoConverter { + + public AdocContentMetaDto convertForMetaData(AdocContentMeta adocContentMeta) { + return AdocContentMetaDto.Builder.newBuilder() + .title(adocContentMeta.getPath().replace(Constants.ADOC_SUFFIX, "")) + .dateTime(adocContentMeta.getCommittedDate().toLocalDate()) + .build(); + } +} diff --git a/src/main/java/com/wimdupont/personalweb/converter/GuideMetaToDtoConverter.java b/src/main/java/com/wimdupont/personalweb/converter/GuideMetaToDtoConverter.java @@ -1,16 +0,0 @@ -package com.wimdupont.personalweb.converter; - -import com.wimdupont.personalweb.model.GuideMeta; -import com.wimdupont.personalweb.model.dto.GuideMetaDto; -import com.wimdupont.personalweb.util.Constants; -import org.springframework.stereotype.Component; - -@Component -public class GuideMetaToDtoConverter { - - public GuideMetaDto convertForMetaData(GuideMeta guideMeta) { - return GuideMetaDto.Builder.newBuilder() - .title(guideMeta.getPath().replace(Constants.ADOC_SUFFIX, "")) - .build(); - } -} diff --git a/src/main/java/com/wimdupont/personalweb/model/AdocContentType.java b/src/main/java/com/wimdupont/personalweb/model/AdocContentType.java @@ -0,0 +1,7 @@ +package com.wimdupont.personalweb.model; + +public enum AdocContentType { + + GUIDE, + BLOG_ARTICLE +} diff --git a/src/main/java/com/wimdupont/personalweb/model/GuideMeta.java b/src/main/java/com/wimdupont/personalweb/model/GuideMeta.java @@ -1,7 +0,0 @@ -package com.wimdupont.personalweb.model; - -public interface GuideMeta { - - String getPath(); - -} diff --git a/src/main/java/com/wimdupont/personalweb/model/dao/AdocContent.java b/src/main/java/com/wimdupont/personalweb/model/dao/AdocContent.java @@ -0,0 +1,116 @@ +package com.wimdupont.personalweb.model.dao; + +import com.wimdupont.personalweb.model.AdocContentType; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; + +import java.time.LocalDateTime; + +@Entity +public class AdocContent extends Auditable { + + @Id + private String path; + private String contentSha256; + private String htmlText; + @Enumerated(EnumType.STRING) + private AdocContentType contentType; + private LocalDateTime committedDate; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getContentSha256() { + return contentSha256; + } + + public void setContentSha256(String contentSha256) { + this.contentSha256 = contentSha256; + } + + public String getHtmlText() { + return htmlText; + } + + public void setHtmlText(String htmlText) { + this.htmlText = htmlText; + } + + public AdocContentType getContentType() { + return contentType; + } + + public void setContentType(AdocContentType contentType) { + this.contentType = contentType; + } + + public LocalDateTime getCommittedDate() { + return committedDate; + } + + public void setCommittedDate(LocalDateTime committedDate) { + this.committedDate = committedDate; + } + + protected AdocContent() { + } + + private AdocContent(Builder builder) { + path = builder.path; + contentSha256 = builder.contentSha256; + htmlText = builder.htmlText; + contentType = builder.contentType; + committedDate = builder.committedDate; + } + + public static final class Builder { + private String path; + private String contentSha256; + private String htmlText; + private AdocContentType contentType; + private LocalDateTime committedDate; + + private Builder() { + } + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder path(String val) { + path = val; + return this; + } + + public Builder contentSha256(String val) { + contentSha256 = val; + return this; + } + + public Builder htmlText(String val) { + htmlText = val; + return this; + } + + public Builder contentType(AdocContentType val) { + contentType = val; + return this; + } + + public Builder committedDate(LocalDateTime val) { + committedDate = val; + return this; + } + + public AdocContent build() { + return new AdocContent(this); + } + } +} diff --git a/src/main/java/com/wimdupont/personalweb/model/dao/Auditable.java b/src/main/java/com/wimdupont/personalweb/model/dao/Auditable.java @@ -0,0 +1,38 @@ +package com.wimdupont.personalweb.model.dao; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class Auditable { + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime modifiedDate; + + public LocalDateTime getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(LocalDateTime createdDate) { + this.createdDate = createdDate; + } + + public LocalDateTime getModifiedDate() { + return modifiedDate; + } + + public void setModifiedDate(LocalDateTime modifiedDate) { + this.modifiedDate = modifiedDate; + } +} diff --git a/src/main/java/com/wimdupont/personalweb/model/dao/Guide.java b/src/main/java/com/wimdupont/personalweb/model/dao/Guide.java @@ -1,140 +0,0 @@ -package com.wimdupont.personalweb.model.dao; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.Id; -import jakarta.persistence.Transient; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - -@Entity -@EntityListeners(AuditingEntityListener.class) -public class Guide { - - @Id - private String path; - private String contentSha256; - @Transient - private String contentBase64; - private String htmlText; - @CreatedDate - @Column(updatable = false) - private LocalDateTime createdDate; - - @LastModifiedDate - private LocalDateTime modifiedDate; - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public String getContentSha256() { - return contentSha256; - } - - public void setContentSha256(String contentSha256) { - this.contentSha256 = contentSha256; - } - - public String getContentBase64() { - return contentBase64; - } - - public void setContentBase64(String contentBase64) { - this.contentBase64 = contentBase64; - } - - public String getHtmlText() { - return htmlText; - } - - public void setHtmlText(String htmlText) { - this.htmlText = htmlText; - } - - public LocalDateTime getCreatedDate() { - return createdDate; - } - - public void setCreatedDate(LocalDateTime createdDate) { - this.createdDate = createdDate; - } - - public LocalDateTime getModifiedDate() { - return modifiedDate; - } - - public void setModifiedDate(LocalDateTime modifiedDate) { - this.modifiedDate = modifiedDate; - } - - protected Guide() { - } - - private Guide(Builder builder) { - path = builder.path; - contentSha256 = builder.contentSha256; - contentBase64 = builder.contentBase64; - htmlText = builder.htmlText; - createdDate = builder.createdDate; - modifiedDate = builder.modifiedDate; - } - - public static final class Builder { - private String path; - private String contentSha256; - private String contentBase64; - private String htmlText; - private LocalDateTime createdDate; - private LocalDateTime modifiedDate; - - private Builder() { - } - - public static Builder newBuilder() { - return new Builder(); - } - - public Builder path(String val) { - path = val; - return this; - } - - public Builder contentSha256(String val) { - contentSha256 = val; - return this; - } - - public Builder contentBase64(String val) { - contentBase64 = val; - return this; - } - - public Builder htmlText(String val) { - htmlText = val; - return this; - } - - public Builder createdDate(LocalDateTime val) { - createdDate = val; - return this; - } - - public Builder modifiedDate(LocalDateTime val) { - modifiedDate = val; - return this; - } - - public Guide build() { - return new Guide(this); - } - } -} diff --git a/src/main/java/com/wimdupont/personalweb/model/dao/projection/AdocContentMeta.java b/src/main/java/com/wimdupont/personalweb/model/dao/projection/AdocContentMeta.java @@ -0,0 +1,10 @@ +package com.wimdupont.personalweb.model.dao.projection; + +import java.time.LocalDateTime; + +public interface AdocContentMeta { + + String getPath(); + LocalDateTime getCommittedDate(); + +} diff --git a/src/main/java/com/wimdupont/personalweb/model/dto/AdocContentMetaDto.java b/src/main/java/com/wimdupont/personalweb/model/dto/AdocContentMetaDto.java @@ -0,0 +1,40 @@ +package com.wimdupont.personalweb.model.dto; + +import java.time.LocalDate; + +public record AdocContentMetaDto( + String title, + LocalDate date +) { + + private AdocContentMetaDto(Builder builder) { + this(builder.title, + builder.date); + } + + public static final class Builder { + private String title; + private LocalDate date; + + private Builder() { + } + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder title(String val) { + title = val; + return this; + } + + public Builder dateTime(LocalDate val) { + date = val; + return this; + } + + public AdocContentMetaDto build() { + return new AdocContentMetaDto(this); + } + } +} diff --git a/src/main/java/com/wimdupont/personalweb/model/dto/AdocFileDto.java b/src/main/java/com/wimdupont/personalweb/model/dto/AdocFileDto.java @@ -1,41 +0,0 @@ -package com.wimdupont.personalweb.model.dto; - - -import java.time.LocalDate; - -public record AdocFileDto( - String name, - LocalDate date -) { - - - private AdocFileDto(Builder builder) { - this(builder.name, builder.date); - } - - public static final class Builder { - private String name; - private LocalDate date; - - private Builder() { - } - - public static Builder newBuilder() { - return new Builder(); - } - - public Builder name(String val) { - name = val; - return this; - } - - public Builder date(LocalDate val) { - date = val; - return this; - } - - public AdocFileDto build() { - return new AdocFileDto(this); - } - } -} diff --git a/src/main/java/com/wimdupont/personalweb/model/dto/GuideMetaDto.java b/src/main/java/com/wimdupont/personalweb/model/dto/GuideMetaDto.java @@ -1,30 +0,0 @@ -package com.wimdupont.personalweb.model.dto; - -public record GuideMetaDto( - String title -) { - - private GuideMetaDto(Builder builder) { - this(builder.title); - } - - public static final class Builder { - private String title; - - private Builder() { - } - - public static Builder newBuilder() { - return new Builder(); - } - - public Builder title(String val) { - title = val; - return this; - } - - public GuideMetaDto build() { - return new GuideMetaDto(this); - } - } -} diff --git a/src/main/java/com/wimdupont/personalweb/repository/AdocContentRepository.java b/src/main/java/com/wimdupont/personalweb/repository/AdocContentRepository.java @@ -0,0 +1,24 @@ +package com.wimdupont.personalweb.repository; + +import com.wimdupont.personalweb.model.AdocContentType; +import com.wimdupont.personalweb.model.dao.AdocContent; +import com.wimdupont.personalweb.model.dao.projection.AdocContentMeta; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface AdocContentRepository extends JpaRepository<AdocContent, String> { + + Optional<AdocContent> findByPathAndContentType(String path, AdocContentType contentType); + + Optional<AdocContent> findByContentSha256(String contentSha256); + + List<AdocContent> findAllByContentType(AdocContentType contentType); + + List<AdocContent> findAllByContentTypeOrderByCommittedDateDesc(AdocContentType contentType); + + List<AdocContentMeta> findAllMetaDataByContentTypeOrderByCommittedDateDesc(AdocContentType contentType); +} diff --git a/src/main/java/com/wimdupont/personalweb/repository/GuideRepository.java b/src/main/java/com/wimdupont/personalweb/repository/GuideRepository.java @@ -1,20 +0,0 @@ -package com.wimdupont.personalweb.repository; - -import com.wimdupont.personalweb.model.GuideMeta; -import com.wimdupont.personalweb.model.dao.Guide; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface GuideRepository extends JpaRepository<Guide, String> { - - Optional<Guide> findByPath(String path); - Optional<Guide> findByContentSha256(String contentSha256); - //Native to exclude columns - @Query(value = "select path from guide order by created_date desc", nativeQuery = true) - List<GuideMeta> findAllMetaData(); -} diff --git a/src/main/java/com/wimdupont/personalweb/service/AdocContentService.java b/src/main/java/com/wimdupont/personalweb/service/AdocContentService.java @@ -0,0 +1,58 @@ +package com.wimdupont.personalweb.service; + +import com.wimdupont.personalweb.api.dto.RepositoryFile; +import com.wimdupont.personalweb.model.AdocContentType; +import com.wimdupont.personalweb.model.dao.AdocContent; +import com.wimdupont.personalweb.model.dao.projection.AdocContentMeta; +import com.wimdupont.personalweb.repository.AdocContentRepository; +import com.wimdupont.personalweb.util.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional +public class AdocContentService { + + private static final Logger LOGGER = LoggerFactory.getLogger(AdocContentService.class); + private final AdocContentRepository adocContentRepository; + + public AdocContentService(AdocContentRepository adocContentRepository) { + this.adocContentRepository = adocContentRepository; + } + + public List<AdocContent> findAllRSSData() { + return adocContentRepository.findAllByContentTypeOrderByCommittedDateDesc(AdocContentType.BLOG_ARTICLE); + } + + public List<AdocContentMeta> findAllMetaData(AdocContentType adocContentType) { + return adocContentRepository.findAllMetaDataByContentTypeOrderByCommittedDateDesc(adocContentType); + } + + public Optional<AdocContent> findByByTitle(String title, AdocContentType adocContentType) { + return adocContentRepository.findByPathAndContentType(title + Constants.ADOC_SUFFIX, adocContentType); + } + + public void removeWhenRemoteNotFound(AdocContentType adocContentType, List<RepositoryFile> repositoryFiles) { + adocContentRepository.findAllByContentType(adocContentType).stream() + .filter(adocContent -> !repositoryFiles.stream() + .map(RepositoryFile::path) + .toList().contains(adocContent.getPath())) + .peek(content -> LOGGER.info("Removing adoc content: {}", content.getPath())) + .forEach(adocContentRepository::delete); + } + + public void save(AdocContent adocContent) { + var saved = adocContentRepository.save(adocContent); + LOGGER.info("{} saved: {}", saved.getContentType().name(), saved); + } + + public Optional<AdocContent> findByContentSha256(String contentSha256){ + return adocContentRepository.findByContentSha256(contentSha256); + } + +} diff --git a/src/main/java/com/wimdupont/personalweb/service/AdocConverter.java b/src/main/java/com/wimdupont/personalweb/service/AdocConverter.java @@ -6,11 +6,6 @@ import org.asciidoctor.Options; import org.asciidoctor.SafeMode; import org.springframework.stereotype.Service; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - @Service @Transactional public class AdocConverter { @@ -20,11 +15,6 @@ public class AdocConverter { .safe(SafeMode.UNSAFE) .build(); - public String convert(File file) throws IOException { - String article = Files.readString(Path.of(file.getAbsolutePath())); - return ASCIIDOCTOR.convert(article, OPTIONS); - } - public String convert(String adocString){ return ASCIIDOCTOR.convert(adocString, OPTIONS); } diff --git a/src/main/java/com/wimdupont/personalweb/service/AdocFileService.java b/src/main/java/com/wimdupont/personalweb/service/AdocFileService.java @@ -1,59 +0,0 @@ -package com.wimdupont.personalweb.service; - -import com.wimdupont.personalweb.model.dto.AdocFileDto; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.time.Instant; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.TimeZone; - -import static com.wimdupont.personalweb.util.Constants.ADOC_SUFFIX; -import static com.wimdupont.personalweb.util.Constants.HTML_SUFFIX; - -public abstract class AdocFileService { - - abstract String getAdocDirectory(); - - abstract String getHtmlDirectory(); - - public File[] getAdocFiles() { - return new File(getAdocDirectory()).listFiles(); - } - - public File[] getHtmlFiles() { - return new File(getHtmlDirectory()).listFiles(); - } - - public String getHtmlFileString(String name, boolean addSuffix) throws IOException { - return Files.readString(Paths.get(String.format("%s/%s%s", getHtmlDirectory(), name, - addSuffix ? HTML_SUFFIX : ""))); - } - - public String toHtmlPathName(File adocArticle) { - return String.format("%s/%s", getHtmlDirectory(), adocArticle.getName().replaceFirst(ADOC_SUFFIX, HTML_SUFFIX)); - } - - public List<AdocFileDto> getFileDtoList() { - List<AdocFileDto> articles = new ArrayList<>(); - File[] files = getHtmlFiles(); - if (files != null) { - Arrays.sort(files, Comparator.comparingLong(File::lastModified).reversed()); - for (File htmlArticle : files) { - articles.add(AdocFileDto.Builder.newBuilder() - .name(htmlArticle.getName().replaceFirst(HTML_SUFFIX, "")) - .date(LocalDate.ofInstant(Instant.ofEpochMilli(htmlArticle.lastModified()), TimeZone.getDefault().toZoneId())) - .build() - ); - } - } - return articles; - } - -} diff --git a/src/main/java/com/wimdupont/personalweb/service/ArticleService.java b/src/main/java/com/wimdupont/personalweb/service/ArticleService.java @@ -1,18 +0,0 @@ -package com.wimdupont.personalweb.service; - -import com.wimdupont.personalweb.util.Constants.Article; -import org.springframework.stereotype.Service; - -@Service -public class ArticleService extends AdocFileService { - - @Override - String getAdocDirectory() { - return Article.ARTICLES_DIRECTORY; - } - - @Override - String getHtmlDirectory() { - return Article.HTML_ARTICLES_DIRECTORY; - } -} diff --git a/src/main/java/com/wimdupont/personalweb/service/GuideService.java b/src/main/java/com/wimdupont/personalweb/service/GuideService.java @@ -1,82 +0,0 @@ -package com.wimdupont.personalweb.service; - -import com.wimdupont.personalweb.api.GitlabApi; -import com.wimdupont.personalweb.api.dto.RepositoryFile; -import com.wimdupont.personalweb.model.GuideMeta; -import com.wimdupont.personalweb.model.dao.Guide; -import com.wimdupont.personalweb.repository.GuideRepository; -import com.wimdupont.personalweb.util.Constants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -@Service -@Transactional -public class GuideService { - - private static final Logger LOGGER = LoggerFactory.getLogger(GuideService.class); - private final GuideRepository guideRepository; - private final GitlabApi gitlabApi; - - public GuideService(GuideRepository guideRepository, GitlabApi gitlabApi) { - this.guideRepository = guideRepository; - this.gitlabApi = gitlabApi; - } - - public Guide save(Guide guide) { - return guideRepository.save(guide); - } - - public List<GuideMeta> findAllMetaData() { - return guideRepository.findAllMetaData(); - } - - public Optional<Guide> findByPath(String path) { - return guideRepository.findByPath(path); - } - - public Optional<Guide> findGuideByTitle(String title) { - return findByPath(title + Constants.ADOC_SUFFIX); - } - - public List<RepositoryFile> findRepositoryFiles() { - var repositoryFiles = new ArrayList<RepositoryFile>(); - gitlabApi.getRepositoryTree() - .orElseThrow().stream() - .map(repositoryTreeItem -> gitlabApi.getRepositoryFile(repositoryTreeItem.path())) - .forEach(repositoryFile -> repositoryFile.ifPresent(repositoryFiles::add)); - return repositoryFiles; - } - - public void removeNotFound(List<RepositoryFile> repositoryFiles) { - guideRepository.findAll().stream() - .filter(guide -> !repositoryFiles.stream() - .map(RepositoryFile::path) - .toList().contains(guide.getPath())) - .peek(guide -> LOGGER.info("Removing guide: {}", guide.getPath())) - .forEach(guideRepository::delete); - } - - public List<Guide> findAllToUpsert(List<RepositoryFile> repositoryFiles) { - var guidesToGenerate = new ArrayList<Guide>(); - repositoryFiles.stream() - .filter(repositoryFile -> guideRepository.findByContentSha256(repositoryFile.contentSha256()).isEmpty()) - .map(repositoryFile -> Guide.Builder.newBuilder() - .contentSha256(repositoryFile.contentSha256()) - .contentBase64(repositoryFile.contentBase64()) - .path(repositoryFile.path()) - .build()) - .forEach(guidesToGenerate::add); - - if (guidesToGenerate.isEmpty()) { - LOGGER.info("No new guides to generate."); - } - return guidesToGenerate; - } - -} diff --git a/src/main/java/com/wimdupont/personalweb/service/RepositoryFileService.java b/src/main/java/com/wimdupont/personalweb/service/RepositoryFileService.java @@ -0,0 +1,34 @@ +package com.wimdupont.personalweb.service; + +import com.wimdupont.personalweb.api.GitlabApi; +import com.wimdupont.personalweb.api.dto.Commit; +import com.wimdupont.personalweb.api.dto.RepositoryFile; +import com.wimdupont.personalweb.model.AdocContentType; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class RepositoryFileService { + + private final GitlabApi gitlabApi; + + public RepositoryFileService(GitlabApi gitlabApi) { + this.gitlabApi = gitlabApi; + } + + public List<RepositoryFile> findRepositoryFiles(AdocContentType adocContentType) { + var repositoryFiles = new ArrayList<RepositoryFile>(); + gitlabApi.getRepositoryTree(adocContentType) + .orElseThrow().stream() + .map(repositoryTreeItem -> gitlabApi.getRepositoryFile(adocContentType, repositoryTreeItem.path())) + .forEach(repositoryFile -> repositoryFile.ifPresent(repositoryFiles::add)); + return repositoryFiles; + } + + public Optional<Commit> getCommit(AdocContentType adocContentType, String commitId){ + return gitlabApi.getCommit(adocContentType, commitId); + } +} diff --git a/src/main/java/com/wimdupont/personalweb/service/RssFeedGenerator.java b/src/main/java/com/wimdupont/personalweb/service/RssFeedGenerator.java @@ -4,29 +4,19 @@ import com.wimdupont.personalweb.model.rss.Channel; import com.wimdupont.personalweb.model.rss.Item; import com.wimdupont.personalweb.model.rss.RssFeed; import com.wimdupont.personalweb.util.Constants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; -import java.io.File; -import java.io.IOException; -import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; import java.util.List; @Service public class RssFeedGenerator { - private static final Logger LOG = LoggerFactory.getLogger(RssFeedGenerator.class); - private final ArticleService articleService; + private final AdocContentService adocContentService; private RssFeed rssFeed; - public RssFeedGenerator(ArticleService articleService) { - this.articleService = articleService; + public RssFeedGenerator(AdocContentService adocContentService) { + this.adocContentService = adocContentService; } public RssFeed getRssFeed() { @@ -50,26 +40,12 @@ public class RssFeedGenerator { } private List<Item> getItems() { - List<Item> items = new ArrayList<>(); - - File[] files = articleService.getHtmlFiles(); - if (files != null) { - Arrays.sort(files, Comparator.comparingLong(File::lastModified).reversed()); - for (File article : files) { - String articleString = null; - try { - articleString = articleService.getHtmlFileString(article.getName(), false); - } catch (IOException e) { - LOG.error(e.getMessage(), e); - } - - String title = article.getName().replace(Constants.HTML_SUFFIX, ""); - LocalDateTime pubDate = Instant.ofEpochMilli(article.lastModified()) - .atZone(ZoneId.systemDefault()).toLocalDateTime(); - items.add(buildItem(pubDate, title, articleString)); - } - } - return items; + return adocContentService.findAllRSSData().stream() + .map(adocContentRss -> buildItem( + adocContentRss.getCommittedDate(), + adocContentRss.getPath().replace(Constants.ADOC_SUFFIX, ""), + adocContentRss.getHtmlText())) + .toList(); } private Item buildItem(LocalDateTime postDate, String title, String content) { diff --git a/src/main/java/com/wimdupont/personalweb/service/ScheduledFileGenerator.java b/src/main/java/com/wimdupont/personalweb/service/ScheduledFileGenerator.java @@ -1,88 +1,74 @@ package com.wimdupont.personalweb.service; -import com.wimdupont.personalweb.model.dao.Guide; +import com.wimdupont.personalweb.model.AdocContentType; +import com.wimdupont.personalweb.model.dao.AdocContent; import jakarta.transaction.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import java.io.File; -import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Base64; -import static com.wimdupont.personalweb.util.Constants.ADOC_SUFFIX; - @Service @Transactional public class ScheduledFileGenerator { - private static final Logger LOG = LoggerFactory.getLogger(ScheduledFileGenerator.class); - private final AdocConverter adocConverter; + private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledFileGenerator.class); private final RssFeedGenerator rssFeedGenerator; - private final ArticleService articleService; - private final GuideService guideService; + private final AdocContentService adocContentService; + private final RepositoryFileService repositoryFileService; + private final AdocConverter adocConverter; - public ScheduledFileGenerator(AdocConverter adocConverter, - RssFeedGenerator rssFeedGenerator, - ArticleService articleService, - GuideService guideService) { - this.adocConverter = adocConverter; + public ScheduledFileGenerator(RssFeedGenerator rssFeedGenerator, + AdocContentService adocContentService, + RepositoryFileService repositoryFileService, + AdocConverter adocConverter) { this.rssFeedGenerator = rssFeedGenerator; - this.articleService = articleService; - this.guideService = guideService; + this.adocContentService = adocContentService; + this.repositoryFileService = repositoryFileService; + this.adocConverter = adocConverter; } @Scheduled(cron = "0 0 0 * * *") public void generate() { - LOG.info("File generator started."); - generateHtmlFiles(); + LOGGER.info("Content generator started."); + updateAdocContent(); rssFeedGenerator.generate(); - updateGuides(); - LOG.info("File generator completed."); + LOGGER.info("Content generator completed."); } - private void updateGuides() { - var repositoryFiles = guideService.findRepositoryFiles(); - guideService.removeNotFound(repositoryFiles); - for (Guide guide : guideService.findAllToUpsert(repositoryFiles)) { - guide.setHtmlText(adocConverter.convert(new String(Base64.getDecoder() - .decode(guide.getContentBase64().getBytes(StandardCharsets.UTF_8))))); - LOG.info("Guide saved: {}", guideService.save(guide).getPath()); + private void updateAdocContent() { + for (AdocContentType contentType : AdocContentType.values()) { + var remoteRepositoryFiles = repositoryFileService.findRepositoryFiles(contentType); + adocContentService.removeWhenRemoteNotFound(contentType, remoteRepositoryFiles); + remoteRepositoryFiles.stream() + .filter(repositoryFile -> adocContentService + .findByContentSha256(repositoryFile.contentSha256()) + .isEmpty()) + .map(repositoryFile -> AdocContent.Builder.newBuilder() + .path(repositoryFile.path()) + .contentSha256(repositoryFile.contentSha256()) + .htmlText(toHtml(repositoryFile.contentBase64())) + .contentType(contentType) + .committedDate(fetchCommittedDate(contentType, repositoryFile.lastCommitId())) + .build()) + .forEach(adocContentService::save); } } - private void generateHtmlFiles() { - File[] files = articleService.getAdocFiles(); - if (files != null) { - createHtmlDir(); - for (File adocFile : files) { - String articlePathName = articleService.toHtmlPathName(adocFile); - if (adocFile.isFile() && adocFile.getName().endsWith(ADOC_SUFFIX) && !new File(articlePathName).exists()) { - try { - String htmlArticle = adocConverter.convert(adocFile); - if (new File(articlePathName).createNewFile()) { - LOG.debug("Html file is created: {}", articlePathName); - } - Files.writeString(Paths.get(articlePathName), htmlArticle); - LOG.info("Html file [{}] has been generated.", adocFile.getName()); - } catch (IOException e) { - LOG.error(e.getMessage(), e); - } - } - } - } + private String toHtml(String contentBase64) { + return adocConverter.convert(new String(Base64.getDecoder() + .decode(contentBase64.getBytes(StandardCharsets.UTF_8)))); } - private void createHtmlDir() { - File htmlArticlesDir = new File(articleService.getHtmlDirectory()); - if (!htmlArticlesDir.exists()) { - if (htmlArticlesDir.mkdir()) { - LOG.debug("Directory is created: {}", articleService.getHtmlDirectory()); - } - } + private LocalDateTime fetchCommittedDate(AdocContentType contentType, String commitId) { + return LocalDateTime.ofInstant(repositoryFileService + .getCommit(contentType, commitId) + .orElseThrow() + .committedDate(), ZoneId.systemDefault()); } } diff --git a/src/main/java/com/wimdupont/personalweb/util/Constants.java b/src/main/java/com/wimdupont/personalweb/util/Constants.java @@ -2,19 +2,11 @@ package com.wimdupont.personalweb.util; public class Constants { - public static final String GPG_PUBLIC_KEY = System.getProperty("user.home") + "/personalweb/gpg/pub.asc"; - public static final String HTML_SUFFIX = ".html"; - public static final String ADOC_SUFFIX = ".adoc"; - private Constants() { } - public static class Article { - private Article() { - } + public static final String GPG_PUBLIC_KEY = System.getProperty("user.home") + "/personalweb/gpg/pub.asc"; + public static final String ADOC_SUFFIX = ".adoc"; - public static final String ARTICLES_DIRECTORY = System.getProperty("user.home") + "/personalweb/articles"; - public static final String HTML_ARTICLES_DIRECTORY = ARTICLES_DIRECTORY + "/html"; - } } diff --git a/src/main/resources/db/migration/V1_2__guide-to-adoccontent.sql b/src/main/resources/db/migration/V1_2__guide-to-adoccontent.sql @@ -0,0 +1,8 @@ +DELETE FROM guide; + +RENAME TABLE guide TO adoc_content; + +ALTER TABLE adoc_content ADD COLUMN committed_date DATETIME; +ALTER TABLE adoc_content ADD COLUMN content_type VARCHAR(20) NOT NULL; + +ALTER TABLE adoc_content ADD INDEX content_type_idx (content_type); diff --git a/src/main/resources/templates/blog.html b/src/main/resources/templates/blog.html @@ -11,7 +11,7 @@ <div th:insert="~{navigation}"/> </header> <tr th:each="article : ${articles}"> - <p><a th:href="@{/blog/article/{article}(article=${article.name})}">[[${article.name}]]</a> - [[${article.date}]]</p> + <p><a th:href="@{/blog/article/{article}(article=${article.title})}">[[${article.title}]]</a> - [[${article.date}]]</p> </tr> </div> </body> diff --git a/src/test/java/com/wimdupont/personalweb/api/dto/CommitMother.java b/src/test/java/com/wimdupont/personalweb/api/dto/CommitMother.java @@ -0,0 +1,16 @@ +package com.wimdupont.personalweb.api.dto; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +public class CommitMother { + + public static class Defaults { + public static final Instant COMMITTED_DATE = LocalDateTime.now().toInstant(ZoneOffset.UTC); + } + + public static Commit withDefaults() { + return new Commit(Defaults.COMMITTED_DATE); + } +} diff --git a/src/test/java/com/wimdupont/personalweb/api/dto/RepositoryFileMother.java b/src/test/java/com/wimdupont/personalweb/api/dto/RepositoryFileMother.java @@ -0,0 +1,19 @@ +package com.wimdupont.personalweb.api.dto; + +public class RepositoryFileMother { + + public static class Defaults { + public static final String PATH = "path"; + public static final String CONTENT_SHA256 = "contentSha256"; + public static final String CONTENT_BASE64 = "Y29udGVudA=="; + public static final String LAST_COMMIT_ID = "lastCommitId"; + } + + public static RepositoryFile withDefaults() { + return new RepositoryFile( + Defaults.PATH, + Defaults.CONTENT_SHA256, + Defaults.CONTENT_BASE64, + Defaults.LAST_COMMIT_ID); + } +} diff --git a/src/test/java/com/wimdupont/personalweb/converter/AdocContentMetaToDtoConverterTest.java b/src/test/java/com/wimdupont/personalweb/converter/AdocContentMetaToDtoConverterTest.java @@ -0,0 +1,74 @@ +package com.wimdupont.personalweb.converter; + +import com.wimdupont.personalweb.model.dao.projection.AdocContentMeta; +import com.wimdupont.personalweb.model.dto.AdocContentMetaDto; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +class AdocContentMetaToDtoConverterTest { + + private AdocContentMetaToDtoConverter converter; + + @BeforeEach + void setup() { + converter = new AdocContentMetaToDtoConverter(); + } + + @Test + void convertWithAdocExtensionShouldRemoveExtension() { + var dateTime = LocalDateTime.now(); + AdocContentMetaDto result = converter.convertForMetaData(new AdocContentMeta() { + @Override + public String getPath() { + return "guide.adoc"; + } + + @Override + public LocalDateTime getCommittedDate() { + return dateTime; + } + }); + Assertions.assertEquals("guide", result.title()); + Assertions.assertEquals(dateTime.toLocalDate(), result.date()); + } + + @Test + void convertWithOtherExtensionShouldNotRemoveExtension() { + var dateTime = LocalDateTime.now(); + AdocContentMetaDto result = converter.convertForMetaData(new AdocContentMeta() { + @Override + public String getPath() { + return "guide.md"; + } + + @Override + public LocalDateTime getCommittedDate() { + return dateTime; + } + }); + Assertions.assertEquals("guide.md", result.title()); + Assertions.assertEquals(dateTime.toLocalDate(), result.date()); + } + + @Test + void convertWithoutExtensionRemainTheSame() { + var dateTime = LocalDateTime.now(); + AdocContentMetaDto result = converter.convertForMetaData(new AdocContentMeta() { + @Override + public String getPath() { + return "guide"; + } + + @Override + public LocalDateTime getCommittedDate() { + return dateTime; + } + }); + Assertions.assertEquals("guide", result.title()); + Assertions.assertEquals(dateTime.toLocalDate(), result.date()); + } + +} diff --git a/src/test/java/com/wimdupont/personalweb/converter/GuideMetaToDtoConverterTest.java b/src/test/java/com/wimdupont/personalweb/converter/GuideMetaToDtoConverterTest.java @@ -1,35 +0,0 @@ -package com.wimdupont.personalweb.converter; - -import com.wimdupont.personalweb.model.dto.GuideMetaDto; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class GuideMetaToDtoConverterTest { - - private GuideMetaToDtoConverter converter; - - @BeforeEach - void setup() { - converter = new GuideMetaToDtoConverter(); - } - - @Test - void convertWithAdocExtension() { - GuideMetaDto result = converter.convertForMetaData(() -> "guide.adoc"); - Assertions.assertEquals("guide", result.title()); - } - - @Test - void convertWithOtherExtension() { - GuideMetaDto result = converter.convertForMetaData(() -> "guide.md"); - Assertions.assertEquals("guide.md", result.title()); - } - - @Test - void convertWithoutExtension() { - GuideMetaDto result = converter.convertForMetaData(() -> "guide"); - Assertions.assertEquals("guide", result.title()); - } - -} diff --git a/src/test/java/com/wimdupont/personalweb/model/dao/AdocContentMother.java b/src/test/java/com/wimdupont/personalweb/model/dao/AdocContentMother.java @@ -0,0 +1,27 @@ +package com.wimdupont.personalweb.model.dao; + +import com.wimdupont.personalweb.model.AdocContentType; + +import java.time.LocalDateTime; + +public class AdocContentMother { + + public static class Defaults { + + public static final String PATH = "path"; + public static final String HTML_TEXT = "htmlText"; + public static final String CONTENT_SHA_256 = "contentSha256"; + public static final LocalDateTime COMMITTED_DATE = LocalDateTime.of(2000, 1, 1, 0, 0); + public static final AdocContentType ADOC_CONTENT_TYPE = AdocContentType.GUIDE; + } + + public static AdocContent.Builder withDefaults() { + return AdocContent.Builder.newBuilder() + .path(Defaults.PATH) + .htmlText(Defaults.HTML_TEXT) + .contentSha256(Defaults.CONTENT_SHA_256) + .committedDate(Defaults.COMMITTED_DATE) + .contentType(Defaults.ADOC_CONTENT_TYPE); + } + +} diff --git a/src/test/java/com/wimdupont/personalweb/model/dao/GuideMother.java b/src/test/java/com/wimdupont/personalweb/model/dao/GuideMother.java @@ -1,24 +0,0 @@ -package com.wimdupont.personalweb.model.dao; - -import java.time.LocalDateTime; - -public class GuideMother { - - private static final String PATH = "path"; - private static final String CONTENT_BASE_64 = "contentBase64"; - private static final String HTML_TEXT = "htmlText"; - private static final String CONTENT_SHA_256 = "contentSha256"; - private static final LocalDateTime CREATED_DATE = LocalDateTime.of(2000, 1, 1, 0, 0); - private static final LocalDateTime MODIFIED_DATE = LocalDateTime.of(2002, 1, 1, 0, 0); - - public static Guide.Builder withDefaults() { - return Guide.Builder.newBuilder() - .path(PATH) - .contentBase64(CONTENT_BASE_64) - .htmlText(HTML_TEXT) - .contentSha256(CONTENT_SHA_256) - .createdDate(CREATED_DATE) - .modifiedDate(MODIFIED_DATE); - } - -} diff --git a/src/test/java/com/wimdupont/personalweb/service/AdocContentServiceTest.java b/src/test/java/com/wimdupont/personalweb/service/AdocContentServiceTest.java @@ -0,0 +1,46 @@ +package com.wimdupont.personalweb.service; + +import com.wimdupont.personalweb.api.dto.RepositoryFile; +import com.wimdupont.personalweb.api.dto.RepositoryFileMother; +import com.wimdupont.personalweb.model.dao.AdocContent; +import com.wimdupont.personalweb.model.dao.AdocContentMother; +import com.wimdupont.personalweb.model.dao.AdocContentMother.Defaults; +import com.wimdupont.personalweb.repository.AdocContentRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +@ExtendWith(MockitoExtension.class) +class AdocContentServiceTest { + + private RepositoryFile repositoryFile; + @Mock + private AdocContentRepository adocContentRepository; + @InjectMocks + private AdocContentService adocContentService; + + @BeforeEach + public void setup() { + repositoryFile = RepositoryFileMother.withDefaults(); + } + + @Test + public void removeNotFound() { + Mockito.when(adocContentRepository.findAllByContentType(Defaults.ADOC_CONTENT_TYPE)) + .thenReturn(List.of( + AdocContentMother.withDefaults().path("path").build(), + AdocContentMother.withDefaults().path("someOtherPath").build())); + + adocContentService.removeWhenRemoteNotFound(Defaults.ADOC_CONTENT_TYPE, List.of(repositoryFile)); + + Mockito.verify(adocContentRepository, Mockito.times(1)) + .delete(Mockito.any(AdocContent.class)); + } + +} diff --git a/src/test/java/com/wimdupont/personalweb/service/GuideServiceTest.java b/src/test/java/com/wimdupont/personalweb/service/GuideServiceTest.java @@ -1,99 +0,0 @@ -package com.wimdupont.personalweb.service; - -import com.wimdupont.personalweb.api.GitlabApi; -import com.wimdupont.personalweb.api.dto.RepositoryFile; -import com.wimdupont.personalweb.api.dto.RepositoryTreeItem; -import com.wimdupont.personalweb.model.dao.Guide; -import com.wimdupont.personalweb.model.dao.GuideMother; -import com.wimdupont.personalweb.repository.GuideRepository; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.List; -import java.util.Optional; - -@ExtendWith(MockitoExtension.class) -class GuideServiceTest { - - private List<RepositoryTreeItem> repositoryTreeItems; - private RepositoryFile repositoryFile; - @Mock - private GuideRepository guideRepository; - @Mock - private GitlabApi gitlabApi; - @InjectMocks - private GuideService guideService; - @Captor - private ArgumentCaptor<String> sha256Captor; - - @BeforeEach - public void setup() { - repositoryTreeItems = List.of(new RepositoryTreeItem("path", "name")); - repositoryFile = new RepositoryFile("path", "contentSha256", "contentBase64"); - } - - @Test - public void findRepositoryFiles() { - Mockito.when(gitlabApi.getRepositoryTree()) - .thenReturn(Optional.of(repositoryTreeItems)); - Mockito.when(gitlabApi.getRepositoryFile("path")) - .thenReturn(Optional.of(repositoryFile)); - - var result = guideService.findRepositoryFiles(); - - Assertions.assertEquals(1, result.size()); - Assertions.assertEquals(repositoryFile, result.get(0)); - } - - @Test - public void removeNotFound() { - Mockito.when(guideRepository.findAll()) - .thenReturn(List.of( - GuideMother.withDefaults().path("path").build(), - GuideMother.withDefaults().path("someOtherPath").build())); - - guideService.removeNotFound(List.of(repositoryFile)); - - Mockito.verify(guideRepository, Mockito.times(1)) - .delete(Mockito.any(Guide.class)); - } - - @Test - public void findAllToUpsertWhenHashFound() { - var guideToUpsert = GuideMother.withDefaults().contentSha256("contentSha256").build(); - Mockito.when(guideRepository.findByContentSha256(sha256Captor.capture())) - .thenReturn(Optional.of(guideToUpsert)); - - var result = guideService.findAllToUpsert(List.of(repositoryFile)); - - Mockito.verify(guideRepository, Mockito.times(1)) - .findByContentSha256(Mockito.any(String.class)); - Assertions.assertEquals(repositoryFile.contentSha256(), sha256Captor.getValue()); - Assertions.assertEquals(0, result.size()); - } - - @Test - public void findAllToUpsertWhenHashNotFound() { - Mockito.when(guideRepository.findByContentSha256(sha256Captor.capture())) - .thenReturn(Optional.empty()); - - var result = guideService.findAllToUpsert(List.of(repositoryFile)); - - Mockito.verify(guideRepository, Mockito.times(1)) - .findByContentSha256(Mockito.any(String.class)); - Assertions.assertEquals(repositoryFile.contentSha256(), sha256Captor.getValue()); - Assertions.assertEquals(1, result.size()); - Assertions.assertEquals(repositoryFile.path(), result.get(0).getPath()); - Assertions.assertEquals(repositoryFile.contentSha256(), result.get(0).getContentSha256()); - Assertions.assertEquals(repositoryFile.contentBase64(), result.get(0).getContentBase64()); - } - -} diff --git a/src/test/java/com/wimdupont/personalweb/service/RepositoryFileServiceTest.java b/src/test/java/com/wimdupont/personalweb/service/RepositoryFileServiceTest.java @@ -0,0 +1,49 @@ +package com.wimdupont.personalweb.service; + +import com.wimdupont.personalweb.api.GitlabApi; +import com.wimdupont.personalweb.api.dto.RepositoryFile; +import com.wimdupont.personalweb.api.dto.RepositoryFileMother; +import com.wimdupont.personalweb.api.dto.RepositoryTreeItem; +import com.wimdupont.personalweb.model.AdocContentType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +class RepositoryFileServiceTest { + + private RepositoryFile repositoryFile; + private List<RepositoryTreeItem> repositoryTreeItems; + @Mock + private GitlabApi gitlabApi; + @InjectMocks + private RepositoryFileService repositoryFileService; + + @BeforeEach + public void setup() { + repositoryTreeItems = List.of(new RepositoryTreeItem("path", "name")); + repositoryFile = RepositoryFileMother.withDefaults(); + } + + @Test + public void findRepositoryFiles() { + Mockito.when(gitlabApi.getRepositoryTree(AdocContentType.GUIDE)) + .thenReturn(Optional.of(repositoryTreeItems)); + Mockito.when(gitlabApi.getRepositoryFile(AdocContentType.GUIDE, "path")) + .thenReturn(Optional.of(repositoryFile)); + + var result = repositoryFileService.findRepositoryFiles(AdocContentType.GUIDE); + + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals(repositoryFile, result.getFirst()); + } + +} diff --git a/src/test/java/com/wimdupont/personalweb/service/RssFeedGeneratorTest.java b/src/test/java/com/wimdupont/personalweb/service/RssFeedGeneratorTest.java @@ -1,5 +1,7 @@ package com.wimdupont.personalweb.service; +import com.wimdupont.personalweb.model.dao.AdocContentMother; +import com.wimdupont.personalweb.model.dao.AdocContentMother.Defaults; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -8,32 +10,21 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import java.io.File; -import java.io.IOException; -import java.time.LocalDateTime; -import java.time.ZoneId; +import java.util.List; @ExtendWith(MockitoExtension.class) class RssFeedGeneratorTest { - private static final LocalDateTime LAST_MODIFIED_DATE = LocalDateTime.of(2000, 1, 1, 0, 0); @Mock - private ArticleService articleService; - @Mock - private File file; + private AdocContentService adocContentService; @InjectMocks private RssFeedGenerator rssFeedGenerator; @Test - public void generate() throws IOException { - Mockito.when(articleService.getHtmlFiles()) - .thenReturn(new File[]{file}); - Mockito.when(articleService.getHtmlFileString("file", false)) - .thenReturn("content"); - Mockito.when(file.getName()) - .thenReturn("file"); - Mockito.when(file.lastModified()) - .thenReturn(LAST_MODIFIED_DATE.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + public void generate() { + + Mockito.when(adocContentService.findAllRSSData()) + .thenReturn(List.of(AdocContentMother.withDefaults().build())); rssFeedGenerator.generate(); @@ -45,12 +36,12 @@ class RssFeedGeneratorTest { Assertions.assertEquals("en-US", channel.language()); Assertions.assertNotNull(channel.pubDate()); Assertions.assertEquals(1, channel.item().size()); - Assertions.assertEquals("file", channel.item().get(0).title()); - Assertions.assertEquals("Wim Dupont", channel.item().get(0).author()); - Assertions.assertEquals("https://wimdupont.com/blog/article/file", channel.item().get(0).link()); - Assertions.assertEquals("content", channel.item().get(0).description()); - Assertions.assertEquals(LAST_MODIFIED_DATE.toString(), channel.item().get(0).pubDate()); - Assertions.assertEquals("file-" + LAST_MODIFIED_DATE, channel.item().get(0).guid()); + Assertions.assertEquals(Defaults.PATH, channel.item().getFirst().title()); + Assertions.assertEquals("Wim Dupont", channel.item().getFirst().author()); + Assertions.assertEquals(String.format("https://wimdupont.com/blog/article/%s", Defaults.PATH), channel.item().getFirst().link()); + Assertions.assertEquals(Defaults.HTML_TEXT, channel.item().getFirst().description()); + Assertions.assertEquals(Defaults.COMMITTED_DATE.toString(), channel.item().getFirst().pubDate()); + Assertions.assertEquals(String.format("%s-%s", Defaults.PATH, Defaults.COMMITTED_DATE), channel.item().getFirst().guid()); } } diff --git a/src/test/java/com/wimdupont/personalweb/service/ScheduledFileGeneratorTest.java b/src/test/java/com/wimdupont/personalweb/service/ScheduledFileGeneratorTest.java @@ -0,0 +1,102 @@ +package com.wimdupont.personalweb.service; + +import com.wimdupont.personalweb.api.dto.Commit; +import com.wimdupont.personalweb.api.dto.CommitMother; +import com.wimdupont.personalweb.api.dto.RepositoryFile; +import com.wimdupont.personalweb.api.dto.RepositoryFileMother; +import com.wimdupont.personalweb.model.AdocContentType; +import com.wimdupont.personalweb.model.dao.AdocContent; +import com.wimdupont.personalweb.model.dao.AdocContentMother; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +class ScheduledFileGeneratorTest { + + private RepositoryFile repositoryFile; + private Commit commit; + @Mock + private RssFeedGenerator rssFeedGenerator; + @Mock + private AdocContentService adocContentService; + @Mock + private RepositoryFileService repositoryFileService; + @Mock + private AdocConverter adocConverter; + @InjectMocks + private ScheduledFileGenerator scheduledFileGenerator; + @Captor + private ArgumentCaptor<AdocContent> adocContentArgumentCaptor; + + @BeforeEach + public void setup() { + repositoryFile = RepositoryFileMother.withDefaults(); + commit = CommitMother.withDefaults(); + } + + @Test + public void generateWhenHashFoundShouldNotSaveEntity() { + var guideToUpsert = AdocContentMother.withDefaults().contentSha256("contentSha256").build(); + var foundFiles = List.of(repositoryFile); + Mockito.when(repositoryFileService.findRepositoryFiles(AdocContentType.GUIDE)) + .thenReturn(new ArrayList<>()); + Mockito.when(repositoryFileService.findRepositoryFiles(AdocContentType.BLOG_ARTICLE)) + .thenReturn(List.of(repositoryFile)); + Mockito.when(adocContentService.findByContentSha256(repositoryFile.contentSha256())) + .thenReturn(Optional.of(guideToUpsert)); + + scheduledFileGenerator.generate(); + + Mockito.verify(adocContentService, Mockito.times(1)) + .removeWhenRemoteNotFound(AdocContentType.BLOG_ARTICLE, foundFiles); + Mockito.verify(rssFeedGenerator, Mockito.times(1)) + .generate(); + Mockito.verify(adocContentService, Mockito.times(0)) + .save(adocContentArgumentCaptor.capture()); + } + + @Test + public void generateWhenHashNotFoundShouldSaveEntity() { + var foundFiles = List.of(repositoryFile); + Mockito.when(repositoryFileService.findRepositoryFiles(AdocContentType.GUIDE)) + .thenReturn(new ArrayList<>()); + Mockito.when(repositoryFileService.findRepositoryFiles(AdocContentType.BLOG_ARTICLE)) + .thenReturn(List.of(repositoryFile)); + Mockito.when(adocContentService.findByContentSha256(repositoryFile.contentSha256())) + .thenReturn(Optional.empty()); + Mockito.when(adocConverter.convert(Mockito.anyString())) + .thenReturn("content"); + Mockito.when(repositoryFileService.getCommit(AdocContentType.BLOG_ARTICLE, repositoryFile.lastCommitId())) + .thenReturn(Optional.of(commit)); + + scheduledFileGenerator.generate(); + + Mockito.verify(adocContentService, Mockito.times(1)) + .removeWhenRemoteNotFound(AdocContentType.BLOG_ARTICLE, foundFiles); + Mockito.verify(rssFeedGenerator, Mockito.times(1)) + .generate(); + Mockito.verify(adocContentService, Mockito.times(1)) + .save(adocContentArgumentCaptor.capture()); + + Assertions.assertEquals(repositoryFile.path(), adocContentArgumentCaptor.getValue().getPath()); + Assertions.assertEquals(repositoryFile.contentSha256(), adocContentArgumentCaptor.getValue().getContentSha256()); + Assertions.assertEquals("content", adocContentArgumentCaptor.getValue().getHtmlText()); + Assertions.assertEquals(AdocContentType.BLOG_ARTICLE, adocContentArgumentCaptor.getValue().getContentType()); + Assertions.assertEquals(LocalDateTime.ofInstant(commit.committedDate(), ZoneId.systemDefault()), + adocContentArgumentCaptor.getValue().getCommittedDate()); + } +}