Refactoring, more abstraction
Signed-off-by: Minecon724 <git@m724.eu>
This commit is contained in:
parent
d6c33b4992
commit
62eb9903c0
15 changed files with 306 additions and 119 deletions
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
|
@ -2,6 +2,7 @@
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/example_workdir" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/m724" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$/m724" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -6,17 +6,19 @@
|
||||||
|
|
||||||
package eu.m724.blog;
|
package eu.m724.blog;
|
||||||
|
|
||||||
|
import eu.m724.blog.compress.CommonsCompressor;
|
||||||
|
import eu.m724.blog.compress.CompressException;
|
||||||
import eu.m724.blog.compress.FileCompressor;
|
import eu.m724.blog.compress.FileCompressor;
|
||||||
import eu.m724.blog.compress.NoSuchAlgorithmException;
|
import eu.m724.blog.compress.NoSuchAlgorithmException;
|
||||||
import eu.m724.blog.object.Article;
|
import eu.m724.blog.object.Article;
|
||||||
import eu.m724.blog.object.Feed;
|
import eu.m724.blog.object.Feed;
|
||||||
import eu.m724.blog.object.RenderOptions;
|
import eu.m724.blog.object.RenderOptions;
|
||||||
import eu.m724.blog.object.Site;
|
import eu.m724.blog.object.Site;
|
||||||
|
import eu.m724.blog.server.Server;
|
||||||
import eu.m724.blog.template.TemplateRenderer;
|
import eu.m724.blog.template.TemplateRenderer;
|
||||||
import org.apache.commons.compress.compressors.CompressorException;
|
import eu.m724.blog.vc.GitVersionControl;
|
||||||
|
import eu.m724.blog.vc.VersionControl;
|
||||||
import org.apache.commons.io.file.PathUtils;
|
import org.apache.commons.io.file.PathUtils;
|
||||||
import org.eclipse.jgit.api.Git;
|
|
||||||
import org.eclipse.jgit.lib.RepositoryBuilder;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -29,18 +31,18 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@code BlogBuilder} class facilitates building a static blog by managing templates,
|
* The {@code BlogBuilder} class facilitates building a static blog by managing templates,
|
||||||
* assets, articles, and rendering output files. It uses a Git repository as the
|
* assets, articles, and rendering output files. It uses a version control (Git etc.) repository as the
|
||||||
* source for the blog's content and configuration.
|
* source for the blog's content and configuration.
|
||||||
*/
|
*/
|
||||||
public class BlogBuilder {
|
public class BlogBuilder {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(BlogBuilder.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(BlogBuilder.class);
|
||||||
|
|
||||||
private final Git git;
|
private final Site site;
|
||||||
|
private final RenderOptions renderOptions;
|
||||||
|
private final VersionControl versionControl;
|
||||||
private final Path workingDirectory;
|
private final Path workingDirectory;
|
||||||
|
|
||||||
private Site site;
|
|
||||||
private TemplateRenderer template;
|
private TemplateRenderer template;
|
||||||
private RenderOptions renderOptions;
|
|
||||||
private Minifier minifier;
|
private Minifier minifier;
|
||||||
|
|
||||||
private Path templateDirectory;
|
private Path templateDirectory;
|
||||||
|
@ -50,12 +52,14 @@ public class BlogBuilder {
|
||||||
/**
|
/**
|
||||||
* Constructs a {@link BlogBuilder} instance using the provided Git repository.
|
* Constructs a {@link BlogBuilder} instance using the provided Git repository.
|
||||||
*
|
*
|
||||||
* @param git the Git repository to be used for the blog.
|
* @param versionControl the version control repository to be used for the blog.
|
||||||
*/
|
*/
|
||||||
public BlogBuilder(Git git) {
|
private BlogBuilder(Site site, RenderOptions renderOptions, Path workingDirectory, VersionControl versionControl) {
|
||||||
this.git = git;
|
this.site = site;
|
||||||
|
this.renderOptions = renderOptions;
|
||||||
|
this.versionControl = versionControl;
|
||||||
|
|
||||||
this.workingDirectory = git.getRepository().getDirectory().toPath().getParent();
|
this.workingDirectory = workingDirectory;
|
||||||
this.templateDirectory = workingDirectory.resolve("template");
|
this.templateDirectory = workingDirectory.resolve("template");
|
||||||
this.outputDirectory = workingDirectory.resolve("generated_out");
|
this.outputDirectory = workingDirectory.resolve("generated_out");
|
||||||
}
|
}
|
||||||
|
@ -64,19 +68,19 @@ public class BlogBuilder {
|
||||||
* Creates a new {@link BlogBuilder} instance for the specified working directory.
|
* Creates a new {@link BlogBuilder} instance for the specified working directory.
|
||||||
* The directory is expected to be a Git repository.
|
* The directory is expected to be a Git repository.
|
||||||
*
|
*
|
||||||
* @param workingDirectory the root path of the blog, which must contain a Git repository.
|
* @param directory the root path of the blog, which must be a Git repository.
|
||||||
* @return a {@link BlogBuilder} instance
|
* @return a {@link BlogBuilder} instance
|
||||||
* @throws IOException if there is an error accessing the Git repository
|
* @throws IOException if there is an error accessing the Git repository
|
||||||
*/
|
*/
|
||||||
public static BlogBuilder fromPath(Path workingDirectory) throws IOException {
|
public static BlogBuilder fromGitRepository(Path directory) throws IOException {
|
||||||
var repository = new RepositoryBuilder()
|
return BlogBuilder.fromDirectory(directory, new GitVersionControl(directory));
|
||||||
.setGitDir(workingDirectory.resolve(".git").toFile())
|
}
|
||||||
.build();
|
|
||||||
var git = new Git(repository);
|
|
||||||
|
|
||||||
//
|
public static BlogBuilder fromDirectory(Path directory, VersionControl versionControl) throws IOException {
|
||||||
|
var site = Site.fromConfig(directory.resolve("site.yml"));
|
||||||
|
var renderOptions = RenderOptions.fromConfig(directory.resolve("render.yml"));
|
||||||
|
|
||||||
return new BlogBuilder(git);
|
return new BlogBuilder(site, renderOptions, directory, versionControl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,14 +134,7 @@ public class BlogBuilder {
|
||||||
* @throws IOException if an I/O error occurs
|
* @throws IOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
public void build() throws IOException {
|
public void build() throws IOException {
|
||||||
LOGGER.debug("Loading site...");
|
if (renderOptions.minify()) {
|
||||||
if (site == null)
|
|
||||||
this.site = Site.fromConfig(workingDirectory.resolve("site.yml"));
|
|
||||||
|
|
||||||
if (renderOptions == null)
|
|
||||||
this.renderOptions = RenderOptions.fromConfig(workingDirectory.resolve("render.yml"));
|
|
||||||
|
|
||||||
if (this.site.minify()) {
|
|
||||||
try {
|
try {
|
||||||
this.minifier = new Minifier();
|
this.minifier = new Minifier();
|
||||||
} catch (NoClassDefFoundError e) {
|
} catch (NoClassDefFoundError e) {
|
||||||
|
@ -193,8 +190,7 @@ public class BlogBuilder {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
path = articleDirectory.relativize(path);
|
var article = Article.fromFile(versionControl, path);
|
||||||
var article = Article.fromFile(git, path);
|
|
||||||
|
|
||||||
if (article.draft() && !renderDrafts) {
|
if (article.draft() && !renderDrafts) {
|
||||||
LOGGER.info("[Article {}] Draft. Ignoring, because you didn't specify to render drafts.", path.getFileName());
|
LOGGER.info("[Article {}] Draft. Ignoring, because you didn't specify to render drafts.", path.getFileName());
|
||||||
|
@ -202,7 +198,7 @@ public class BlogBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
var render = template.renderArticle(article);
|
var render = template.renderArticle(article);
|
||||||
var outFile = outputDirectory.resolve("article").resolve(path);
|
var outFile = outputDirectory.resolve("article").resolve(path.getFileName());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.createDirectory(outFile.getParent());
|
Files.createDirectory(outFile.getParent());
|
||||||
|
@ -229,8 +225,9 @@ public class BlogBuilder {
|
||||||
if (renderOptions.remapAssets()) {
|
if (renderOptions.remapAssets()) {
|
||||||
var assetHashes = CacheBuster.copyTree(userAssetsDir, outputUserAssetsDir);
|
var assetHashes = CacheBuster.copyTree(userAssetsDir, outputUserAssetsDir);
|
||||||
|
|
||||||
|
// This looks weird, but it's necessary if we want to use a single map
|
||||||
assetHashes.forEach((k, v) -> {
|
assetHashes.forEach((k, v) -> {
|
||||||
fileHashes.put("assets/" + k, v); // TODO this feels like a hack
|
fileHashes.put("assets/" + k, v);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
FileUtils.copyTree(userAssetsDir, outputUserAssetsDir);
|
FileUtils.copyTree(userAssetsDir, outputUserAssetsDir);
|
||||||
|
@ -239,8 +236,9 @@ public class BlogBuilder {
|
||||||
if (renderOptions.remapTemplateStatic()) {
|
if (renderOptions.remapTemplateStatic()) {
|
||||||
var templateStaticHashes = CacheBuster.copyTree(templateStaticDir, outputTemplateStaticDir);
|
var templateStaticHashes = CacheBuster.copyTree(templateStaticDir, outputTemplateStaticDir);
|
||||||
|
|
||||||
|
// This looks weird, but it's necessary if we want to use a single map
|
||||||
templateStaticHashes.forEach((k, v) -> {
|
templateStaticHashes.forEach((k, v) -> {
|
||||||
fileHashes.put("static/" + k, v); // TODO this feels like a hack
|
fileHashes.put("static/" + k, v);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
FileUtils.copyTree(templateStaticDir, outputTemplateStaticDir);
|
FileUtils.copyTree(templateStaticDir, outputTemplateStaticDir);
|
||||||
|
@ -259,7 +257,7 @@ public class BlogBuilder {
|
||||||
|
|
||||||
for (var algorithm : renderOptions.compress()) {
|
for (var algorithm : renderOptions.compress()) {
|
||||||
try {
|
try {
|
||||||
var compressor = new FileCompressor(algorithm);
|
var compressor = new CommonsCompressor(algorithm);
|
||||||
compressors.add(compressor);
|
compressors.add(compressor);
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
LOGGER.error("No such compression algorithm, ignoring: {}", e.getAlgorithm());
|
LOGGER.error("No such compression algorithm, ignoring: {}", e.getAlgorithm());
|
||||||
|
@ -276,8 +274,8 @@ public class BlogBuilder {
|
||||||
for (var path : tree) {
|
for (var path : tree) {
|
||||||
try {
|
try {
|
||||||
compressor.compress(path);
|
compressor.compress(path);
|
||||||
} catch (CompressorException e) {
|
} catch (CompressException e) {
|
||||||
LOGGER.error("Error compressing \"{}\" to \"{}\": {}", path, compressor.getAlgorithm(), e.getMessage());
|
LOGGER.error("Error compressing \"{}\" to \"{}\": {}", path, compressor.getAlgorithmName(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,10 @@ public class Main {
|
||||||
|
|
||||||
var start = System.nanoTime();
|
var start = System.nanoTime();
|
||||||
|
|
||||||
var builder = BlogBuilder.fromPath(workingDirectory)
|
|
||||||
|
LOGGER.debug("Loading site...");
|
||||||
|
|
||||||
|
var builder = BlogBuilder.fromGitRepository(workingDirectory)
|
||||||
.templateDirectory(templateDirectory)
|
.templateDirectory(templateDirectory)
|
||||||
.outputDirectory(outputDirectory)
|
.outputDirectory(outputDirectory)
|
||||||
.renderDrafts(renderDrafts);
|
.renderDrafts(renderDrafts);
|
||||||
|
|
25
src/main/java/eu/m724/blog/YamlLoader.java
Normal file
25
src/main/java/eu/m724/blog/YamlLoader.java
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog;
|
||||||
|
|
||||||
|
import org.snakeyaml.engine.v2.api.Load;
|
||||||
|
import org.snakeyaml.engine.v2.api.LoadSettings;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class YamlLoader {
|
||||||
|
private static final Load LOAD = new Load(LoadSettings.builder().build());
|
||||||
|
|
||||||
|
public static Map<String, Object> loadMap(Path file) throws IOException {
|
||||||
|
try (var inputStream = Files.newInputStream(file)) {
|
||||||
|
return (Map<String, Object>) LOAD.loadFromInputStream(inputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
src/main/java/eu/m724/blog/compress/CommonsCompressor.java
Normal file
58
src/main/java/eu/m724/blog/compress/CommonsCompressor.java
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.compress;
|
||||||
|
|
||||||
|
import org.apache.commons.compress.compressors.CompressorException;
|
||||||
|
import org.apache.commons.compress.compressors.CompressorStreamFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.FileAlreadyExistsException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class CommonsCompressor extends FileCompressor {
|
||||||
|
/**
|
||||||
|
* Constructs a {@link FileCompressor} instance using the provided algorithm.
|
||||||
|
*
|
||||||
|
* @param algorithm The algorithm, for valid names see {@link CompressorStreamFactory}
|
||||||
|
* @throws NoSuchAlgorithmException If that algorithm is unavailable or the name is invalid.
|
||||||
|
*/
|
||||||
|
public CommonsCompressor(String algorithm) throws NoSuchAlgorithmException {
|
||||||
|
super(algorithm);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var os = new CompressorStreamFactory().createCompressorOutputStream(algorithm, OutputStream.nullOutputStream());
|
||||||
|
os.close();
|
||||||
|
} catch (NoClassDefFoundError | CompressorException e) {
|
||||||
|
throw new NoSuchAlgorithmException(algorithm, e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Unexpected IOException closing test output stream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void compress(Path file) throws IOException, CompressException {
|
||||||
|
var destination = file.resolveSibling(file.getFileName() + "." + getAlgorithmName());
|
||||||
|
compress(file, destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void compress(Path source, Path destination) throws IOException, CompressException {
|
||||||
|
if (Files.exists(destination))
|
||||||
|
throw new FileAlreadyExistsException(destination.toString());
|
||||||
|
|
||||||
|
try (
|
||||||
|
var outputStream = new CompressorStreamFactory()
|
||||||
|
.createCompressorOutputStream(getAlgorithmName(), Files.newOutputStream(destination))
|
||||||
|
) {
|
||||||
|
Files.copy(source, outputStream);
|
||||||
|
} catch (CompressorException e) {
|
||||||
|
throw new CompressException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/main/java/eu/m724/blog/compress/CompressException.java
Normal file
14
src/main/java/eu/m724/blog/compress/CompressException.java
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.compress;
|
||||||
|
|
||||||
|
// TODO really runtime exception?
|
||||||
|
public class CompressException extends RuntimeException {
|
||||||
|
public CompressException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,53 +6,20 @@
|
||||||
|
|
||||||
package eu.m724.blog.compress;
|
package eu.m724.blog.compress;
|
||||||
|
|
||||||
import org.apache.commons.compress.compressors.CompressorException;
|
|
||||||
import org.apache.commons.compress.compressors.CompressorStreamFactory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
|
|
||||||
public class FileCompressor {
|
public abstract class FileCompressor {
|
||||||
private final String algorithm;
|
private final String algorithmName;
|
||||||
|
|
||||||
/**
|
protected FileCompressor(String algorithmName) {
|
||||||
* Constructs a {@link FileCompressor} instance using the provided algorithm.
|
this.algorithmName = algorithmName;
|
||||||
*
|
|
||||||
* @param algorithm The algorithm, for valid names see {@link CompressorStreamFactory}
|
|
||||||
* @throws NoSuchAlgorithmException If that algorithm is unavailable or the name is invalid.
|
|
||||||
*/
|
|
||||||
public FileCompressor(String algorithm) throws NoSuchAlgorithmException {
|
|
||||||
this.algorithm = algorithm;
|
|
||||||
|
|
||||||
try {
|
|
||||||
var os = new CompressorStreamFactory().createCompressorOutputStream(algorithm, OutputStream.nullOutputStream());
|
|
||||||
os.close();
|
|
||||||
} catch (NoClassDefFoundError | CompressorException e) {
|
|
||||||
throw new NoSuchAlgorithmException(algorithm, e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException("Unexpected IOException closing test output stream", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void compress(Path source) throws IOException, CompressorException {
|
public String getAlgorithmName() {
|
||||||
var destination = source.resolveSibling(source.getFileName() + "." + algorithm);
|
return algorithmName;
|
||||||
compress(source, destination);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void compress(Path source, Path destination) throws IOException, CompressorException {
|
abstract public void compress(Path file) throws IOException, CompressException;
|
||||||
if (Files.exists(destination))
|
abstract public void compress(Path source, Path destination) throws IOException, CompressException;
|
||||||
throw new FileAlreadyExistsException(destination.toString());
|
|
||||||
|
|
||||||
try (
|
|
||||||
var outputStream = new CompressorStreamFactory()
|
|
||||||
.createCompressorOutputStream(algorithm, Files.newOutputStream(destination))
|
|
||||||
) {
|
|
||||||
Files.copy(source, outputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAlgorithm() {
|
|
||||||
return algorithm;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
|
|
||||||
package eu.m724.blog.object;
|
package eu.m724.blog.object;
|
||||||
|
|
||||||
import org.eclipse.jgit.api.Git;
|
import eu.m724.blog.vc.VersionControl;
|
||||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
import eu.m724.blog.vc.VersionControlException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -56,17 +56,16 @@ public record Article(
|
||||||
* The method extracts metadata properties, content, and versioning information
|
* The method extracts metadata properties, content, and versioning information
|
||||||
* based on the Git history of the file.
|
* based on the Git history of the file.
|
||||||
*
|
*
|
||||||
* @param git the Git repository used to retrieve versioning and commit information
|
* @param versionControl the version control repository used to retrieve versioning and commit information
|
||||||
* @param path the relative path to the file within the "articles" directory
|
* @param path the relative path to the file within the "articles" directory
|
||||||
* @return a {@link Article} object populated with data extracted from the specified file
|
* @return a {@link Article} object populated with data extracted from the specified file
|
||||||
* @throws IOException if an error occurs during file reading
|
* @throws IOException if an error occurs during file reading
|
||||||
*/
|
*/
|
||||||
public static Article fromFile(Git git, Path path) throws IOException {
|
public static Article fromFile(VersionControl versionControl, Path path) throws IOException {
|
||||||
/* read properties before filtering */
|
/* read properties before filtering */
|
||||||
|
|
||||||
var slug = path.getFileName().toString().split("\\.")[0];
|
var slug = path.getFileName().toString().split("\\.")[0];
|
||||||
path = Path.of("articles").resolve(path);
|
var lines = Files.readAllLines(path);
|
||||||
var lines = Files.readAllLines(git.getRepository().getDirectory().toPath().getParent().resolve(path));
|
|
||||||
|
|
||||||
var properties = new HashMap<String, String>();
|
var properties = new HashMap<String, String>();
|
||||||
|
|
||||||
|
@ -121,18 +120,18 @@ public record Article(
|
||||||
ZonedDateTime modifiedAt = Instant.ofEpochMilli(0).atZone(ZoneOffset.UTC);
|
ZonedDateTime modifiedAt = Instant.ofEpochMilli(0).atZone(ZoneOffset.UTC);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (var commit : git.log().addPath(path.toString()).call()) {
|
for (var change : versionControl.getChanges(path)) {
|
||||||
createdBy = commit.getAuthorIdent().getName();
|
createdBy = change.author();
|
||||||
createdAt = Instant.ofEpochSecond(commit.getCommitTime()).atZone(ZoneOffset.UTC);
|
createdAt = change.time();
|
||||||
|
|
||||||
if (revisions++ == 0) {
|
if (revisions++ == 0) {
|
||||||
modifiedBy = createdBy;
|
modifiedBy = createdBy;
|
||||||
modifiedAt = createdAt;
|
modifiedAt = createdAt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (GitAPIException e) {
|
} catch (VersionControlException e) {
|
||||||
draft = true;
|
draft = true;
|
||||||
LOGGER.warn("[Article {}] Draft because of a Git exception: {}", slug, e.getMessage());
|
LOGGER.warn("[Article {}] Draft because of a VC exception: {}", slug, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Article(slug, title, summary, draft, revisions, createdBy, createdAt, modifiedBy, modifiedAt, custom, content);
|
return new Article(slug, title, summary, draft, revisions, createdBy, createdAt, modifiedBy, modifiedAt, custom, content);
|
||||||
|
|
|
@ -6,41 +6,61 @@
|
||||||
|
|
||||||
package eu.m724.blog.object;
|
package eu.m724.blog.object;
|
||||||
|
|
||||||
import org.snakeyaml.engine.v2.api.Load;
|
import eu.m724.blog.YamlLoader;
|
||||||
import org.snakeyaml.engine.v2.api.LoadSettings;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options that are relevant to rendering, not about the {@link Site}.
|
||||||
|
*
|
||||||
|
* @param compress list of compression algorithms to compress output
|
||||||
|
* @param remapAssets whether to remap user assets, add a unique hash to the file name to bypass cache
|
||||||
|
* @param remapTemplateStatic whether to remap template static assets, add a unique hash to the file name to bypass cache
|
||||||
|
* @param minify whether to minify output
|
||||||
|
*/
|
||||||
public record RenderOptions(
|
public record RenderOptions(
|
||||||
List<String> compress, // TODO rename?
|
List<String> compress, // TODO rename?
|
||||||
|
|
||||||
|
boolean remapAssets,
|
||||||
boolean remapTemplateStatic,
|
boolean remapTemplateStatic,
|
||||||
boolean remapAssets
|
|
||||||
|
boolean minify
|
||||||
) {
|
) {
|
||||||
|
private static final RenderOptions DEFAULT = new RenderOptions(
|
||||||
|
List.of("gz", "zstd"),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link Site} object by reading and parsing the configuration file at the specified path.<br>
|
* Creates a {@link Site} object by reading and parsing the configuration file at the specified file.<br>
|
||||||
* The configuration file must be a JSON file.
|
* The configuration file must be a JSON file.
|
||||||
*
|
*
|
||||||
* @param path the path to the configuration file
|
* @param file the file to the configuration file
|
||||||
* @return a {@link Site} object initialized with the data from the configuration file
|
* @return a {@link Site} object initialized with the data from the configuration file
|
||||||
* @throws IOException if an error occurs during file reading
|
* @throws IOException if an error occurs during file reading
|
||||||
*/
|
*/
|
||||||
public static RenderOptions fromConfig(Path path) throws IOException {
|
public static RenderOptions fromConfig(Path file) throws IOException {
|
||||||
var load = new Load(LoadSettings.builder().build());
|
var yaml = YamlLoader.loadMap(file);
|
||||||
var yaml = (Map<String, Object>) load.loadFromInputStream(Files.newInputStream(path));
|
|
||||||
|
/* ---- */
|
||||||
|
|
||||||
|
List<String> compress = (List<String>) yaml.getOrDefault("compress", DEFAULT.compress());
|
||||||
|
|
||||||
List<String> compress = (List<String>) yaml.getOrDefault("compress", new ArrayList<>());
|
|
||||||
boolean remapTemplateStatic = (boolean) yaml.getOrDefault("remapTemplateStatic", true);
|
|
||||||
// assets are not remapped by default, because they might be hotlinked
|
// assets are not remapped by default, because they might be hotlinked
|
||||||
boolean remapAssets = (boolean) yaml.getOrDefault("remapAssets", false);
|
boolean remapAssets = (boolean) yaml.getOrDefault("remapAssets", DEFAULT.remapAssets());
|
||||||
|
|
||||||
|
boolean remapTemplateStatic = (boolean) yaml.getOrDefault("remapTemplateStatic", DEFAULT.remapTemplateStatic());
|
||||||
|
|
||||||
|
var minify = (boolean) yaml.getOrDefault("minify", DEFAULT.minify());
|
||||||
|
|
||||||
|
/* ---- */
|
||||||
|
|
||||||
return new RenderOptions(
|
return new RenderOptions(
|
||||||
compress, remapTemplateStatic, remapAssets
|
compress, remapTemplateStatic, remapAssets, minify
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,9 @@
|
||||||
|
|
||||||
package eu.m724.blog.object;
|
package eu.m724.blog.object;
|
||||||
|
|
||||||
import org.snakeyaml.engine.v2.api.Load;
|
import eu.m724.blog.YamlLoader;
|
||||||
import org.snakeyaml.engine.v2.api.LoadSettings;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -21,7 +19,6 @@ import java.util.Map;
|
||||||
* @param baseUrl the base URL of the site
|
* @param baseUrl the base URL of the site
|
||||||
* @param directory The directory that the site is installed into. Like "https://example.com/blog" turns to "/blog"
|
* @param directory The directory that the site is installed into. Like "https://example.com/blog" turns to "/blog"
|
||||||
* @param templateArticles whether to parse articles with Pebble templating
|
* @param templateArticles whether to parse articles with Pebble templating
|
||||||
* @param minify whether to minify HTML, CSS, JS, etc.
|
|
||||||
* @param custom a map of additional custom properties
|
* @param custom a map of additional custom properties
|
||||||
*/
|
*/
|
||||||
public record Site(
|
public record Site(
|
||||||
|
@ -31,28 +28,33 @@ public record Site(
|
||||||
String directory,
|
String directory,
|
||||||
|
|
||||||
boolean templateArticles,
|
boolean templateArticles,
|
||||||
boolean minify,
|
|
||||||
|
|
||||||
Map<String, Object> custom
|
Map<String, Object> custom
|
||||||
) {
|
) {
|
||||||
|
private static final Site DEFAULT = new Site(
|
||||||
|
"Misconfigured blog",
|
||||||
|
"/",
|
||||||
|
"/",
|
||||||
|
false,
|
||||||
|
Map.of()
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link Site} object by reading and parsing the configuration file at the specified path.<br>
|
* Creates a {@link Site} object by reading and parsing the configuration file at the specified path.<br>
|
||||||
* The configuration file must be a JSON file.
|
* The configuration file must be a JSON file.
|
||||||
*
|
*
|
||||||
* @param path the path to the configuration file
|
* @param file the path to the configuration file
|
||||||
* @return a {@link Site} object initialized with the data from the configuration file
|
* @return a {@link Site} object initialized with the data from the configuration file
|
||||||
* @throws IOException if an error occurs during file reading
|
* @throws IOException if an error occurs during file reading
|
||||||
*/
|
*/
|
||||||
public static Site fromConfig(Path path) throws IOException {
|
public static Site fromConfig(Path file) throws IOException {
|
||||||
var load = new Load(LoadSettings.builder().build());
|
var yaml = YamlLoader.loadMap(file);
|
||||||
var yaml = (Map<String, Object>) load.loadFromInputStream(Files.newInputStream(path));
|
|
||||||
|
|
||||||
String name = (String) yaml.get("name");
|
String name = (String) yaml.getOrDefault("name", DEFAULT.name());
|
||||||
String baseUrl = (String) yaml.getOrDefault("baseUrl", "/");
|
String baseUrl = (String) yaml.getOrDefault("baseUrl", DEFAULT.baseUrl());
|
||||||
var templateArticles = (boolean) yaml.getOrDefault("templateArticles", false);
|
var templateArticles = (boolean) yaml.getOrDefault("templateArticles", DEFAULT.templateArticles());
|
||||||
var minify = (boolean) yaml.getOrDefault("minify", true);
|
|
||||||
|
|
||||||
String directory = "/";
|
String directory = DEFAULT.directory();
|
||||||
if (baseUrl != null) {
|
if (baseUrl != null) {
|
||||||
var temp = baseUrl.substring(baseUrl.indexOf(':') + 3);
|
var temp = baseUrl.substring(baseUrl.indexOf(':') + 3);
|
||||||
var slashIndex = temp.indexOf('/');
|
var slashIndex = temp.indexOf('/');
|
||||||
|
@ -62,7 +64,7 @@ public record Site(
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Site(
|
return new Site(
|
||||||
name, baseUrl, directory, templateArticles, minify, yaml
|
name, baseUrl, directory, templateArticles, yaml
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* in the project root for the full license text.
|
* in the project root for the full license text.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package eu.m724.blog;
|
package eu.m724.blog.server;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import com.sun.net.httpserver.SimpleFileServer;
|
import com.sun.net.httpserver.SimpleFileServer;
|
14
src/main/java/eu/m724/blog/vc/Change.java
Normal file
14
src/main/java/eu/m724/blog/vc/Change.java
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.vc;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
public record Change(
|
||||||
|
String author,
|
||||||
|
ZonedDateTime time
|
||||||
|
) { }
|
56
src/main/java/eu/m724/blog/vc/GitVersionControl.java
Normal file
56
src/main/java/eu/m724/blog/vc/GitVersionControl.java
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.vc;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.lib.RepositoryBuilder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class GitVersionControl implements VersionControl {
|
||||||
|
private final Git git;
|
||||||
|
private final Path directory;
|
||||||
|
|
||||||
|
public GitVersionControl(Path directory) throws IOException {
|
||||||
|
this.directory = directory;
|
||||||
|
|
||||||
|
var repository = new RepositoryBuilder()
|
||||||
|
.setGitDir(directory.resolve(".git").toFile())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.git = new Git(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Change> getChanges(Path path) throws VersionControlException {
|
||||||
|
path = path.normalize();
|
||||||
|
if (path.startsWith(directory)) {
|
||||||
|
path = directory.relativize(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var changes = new ArrayList<Change>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (var commit : git.log().addPath(path.toString()).call()) {
|
||||||
|
var author = commit.getAuthorIdent().getName();
|
||||||
|
var time = Instant.ofEpochSecond(commit.getCommitTime()).atZone(ZoneOffset.UTC);
|
||||||
|
|
||||||
|
changes.add(new Change(author, time));
|
||||||
|
}
|
||||||
|
} catch (GitAPIException e) {
|
||||||
|
throw new VersionControlException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
}
|
15
src/main/java/eu/m724/blog/vc/VersionControl.java
Normal file
15
src/main/java/eu/m724/blog/vc/VersionControl.java
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.vc;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface VersionControl {
|
||||||
|
List<Change> getChanges(Path path) throws VersionControlException;
|
||||||
|
}
|
||||||
|
|
15
src/main/java/eu/m724/blog/vc/VersionControlException.java
Normal file
15
src/main/java/eu/m724/blog/vc/VersionControlException.java
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 blog-software-java developers
|
||||||
|
* blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file
|
||||||
|
* in the project root for the full license text.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package eu.m724.blog.vc;
|
||||||
|
|
||||||
|
// TODO runtime exception really?
|
||||||
|
public class VersionControlException extends RuntimeException {
|
||||||
|
public VersionControlException(Throwable cause) {
|
||||||
|
// TODO maybe I need to raise message here too
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue