Migrate Git-based repository management to Maven-based artifact handling and refactor services accordingly
Build and Push Docker Image / build-and-push (push) Successful in 1m21s

This commit is contained in:
2026-07-06 19:40:42 +02:00
parent 6b5f0cf58a
commit 8608ee3ce1
12 changed files with 313 additions and 204 deletions
+3 -1
View File
@@ -13,7 +13,9 @@
"Bash(sdk use:*)", "Bash(sdk use:*)",
"Bash(mvn clean:*)", "Bash(mvn clean:*)",
"Bash(source \"$HOME/.sdkman/bin/sdkman-init.sh\")", "Bash(source \"$HOME/.sdkman/bin/sdkman-init.sh\")",
"Bash(sed -i '' 's/<java.version>21</<java.version>17</' pom.xml)" "Bash(sed -i '' 's/<java.version>21</<java.version>17</' pom.xml)",
"Bash(mvn compile:*)",
"Bash(mvn javadoc:javadoc -Dmaven.javadoc.failOnError=false --no-transfer-progress -Dlombok.delombok.skip=true -Dcheckstyle.skip=true -Djacoco.skip=true -DskipTests)"
] ]
} }
} }
+2
View File
@@ -19,6 +19,8 @@
<properties> <properties>
<java.version>21</java.version> <java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
@@ -1,7 +1,9 @@
package codes.thischwa.jdvs.config; package codes.thischwa.jdvs.config;
import java.util.List; import java.util.List;
import java.util.Objects;
import lombok.Data; import lombok.Data;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -12,11 +14,14 @@ public class JdvsConfig {
private String baseDir; private String baseDir;
private boolean cleanOnStart; private boolean cleanOnStart;
private boolean runOnStart; private boolean runOnStart;
private String mavenCentralUrl;
private List<RepoConfig> repositories; private List<RepoConfig> repositories;
@Data @Data
public static class RepoConfig { public static class RepoConfig {
private String name; private String name;
private String url; private String groupId;
private String artifactId;
private String mavenRepoUrl;
} }
} }
@@ -18,9 +18,6 @@ public class GitRepository {
@Column(nullable = false, unique = true) @Column(nullable = false, unique = true)
private String name; private String name;
@Column(nullable = false)
private String url;
private String lastTag; private String lastTag;
private LocalDateTime updated; private LocalDateTime updated;
@@ -1,63 +0,0 @@
package codes.thischwa.jdvs.service;
import codes.thischwa.jdvs.config.JdvsConfig;
import codes.thischwa.jdvs.model.GitRepository;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Ref;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class GitService {
private final JdvsConfig jdvsConfig;
public Optional<String> updateAndCheckoutLatestTag(GitRepository repoEntity) {
Path repoPath = Path.of(jdvsConfig.getBaseDir(), "repos", repoEntity.getName());
try {
Git git;
if (Files.exists(repoPath)) {
git = Git.open(repoPath.toFile());
git.fetch().setCheckFetchedObjects(true).call();
} else {
git = Git.cloneRepository()
.setURI(repoEntity.getUrl())
.setDirectory(repoPath.toFile())
.setCloneAllBranches(true)
.setNoCheckout(false)
.call();
}
List<Ref> tags = git.tagList().call();
Optional<Ref> latestVTag = tags.stream()
.filter(ref -> ref.getName().startsWith("refs/tags/v"))
.max(Comparator.comparing(Ref::getName));
if (latestVTag.isPresent()) {
String tagName = latestVTag.get().getName().substring("refs/tags/".length());
git.checkout().setName(tagName).call();
log.info("Repository {} checked out at tag {}.", repoEntity.getName(), tagName);
return Optional.of(tagName);
} else {
log.warn("No v* tag found for repository {}.", repoEntity.getName());
}
} catch (IOException | GitAPIException e) {
log.error("Error processing repository " + repoEntity.getName(), e);
}
return Optional.empty();
}
public File getRepoDirectory(String name) {
return Path.of(jdvsConfig.getBaseDir(), "repos", name).toFile();
}
}
@@ -1,115 +0,0 @@
package codes.thischwa.jdvs.service;
import codes.thischwa.jdvs.config.JdvsConfig;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class JavadocService {
private final JdvsConfig jdvsConfig;
// Possible standard paths for Javadoc
private final String[] possiblePaths = {
"target/reports/apidocs",
"target/site/apidocs",
"target/apidocs"
};
public boolean generateJavadoc(String projectName, File repoDir) {
File outputDir = Path.of(jdvsConfig.getBaseDir(), "javadoc", projectName).toFile();
if (!outputDir.exists()) {
outputDir.mkdirs();
}
// Try Maven first if a pom.xml is present
if (new File(repoDir, "pom.xml").exists()) {
boolean success = runMavenJavadoc(repoDir, outputDir);
if (success) {
copyGeneratedJavadoc(repoDir, outputDir);
}
return success;
} else {
log.warn("No pom.xml found in {}. Manual javadoc generation is not implemented.", projectName);
return false;
}
}
private void copyGeneratedJavadoc(File repoDir, File targetDir) {
for (String relPath : possiblePaths) {
File sourceDir = new File(repoDir, relPath);
if (sourceDir.exists() && sourceDir.isDirectory()) {
log.info("Found Javadoc in {}, copying to {}", sourceDir.getAbsolutePath(), targetDir.getAbsolutePath());
try {
copyDirectory(sourceDir.toPath(), targetDir.toPath());
return;
} catch (IOException e) {
log.error("Error copying Javadoc from {} to {}", sourceDir.getAbsolutePath(), targetDir.getAbsolutePath(), e);
}
}
}
log.warn("No generated Javadoc files found in the standard directories of {}.", repoDir.getName());
}
private void copyDirectory(Path source, Path target) throws IOException {
try (Stream<Path> stream = Files.walk(source)) {
stream.forEach(path -> {
try {
Path dest = target.resolve(source.relativize(path));
if (Files.isDirectory(path)) {
if (!Files.exists(dest)) {
Files.createDirectories(dest);
}
} else {
Files.copy(path, dest, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
private boolean runMavenJavadoc(File repoDir, File outputDir) {
ProcessBuilder pb = new ProcessBuilder(
"mvn", "javadoc:javadoc",
"-Dmaven.javadoc.failOnError=false",
"--no-transfer-progress",
"-Dlombok.delombok.skip=true",
"-Dcheckstyle.skip=true",
"-Djacoco.skip=true",
"-DskipTests"
);
pb.directory(repoDir);
pb.redirectErrorStream(true);
try {
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
log.info("[MAVEN] {}", line);
}
}
int exitCode = process.waitFor();
if (exitCode == 0) {
log.info("Javadoc successfully generated in {}", outputDir.getAbsolutePath());
return true;
} else {
log.error("Maven javadoc failed with exit code {}", exitCode);
}
} catch (IOException | InterruptedException e) {
log.error("Error executing Maven javadoc", e);
}
return false;
}
}
@@ -0,0 +1,290 @@
package codes.thischwa.jdvs.service;
import codes.thischwa.jdvs.config.JdvsConfig;
import codes.thischwa.jdvs.config.JdvsConfig.RepoConfig;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.xml.parsers.DocumentBuilderFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
@Service
@RequiredArgsConstructor
@Slf4j
public class MavenSourceService {
private final JdvsConfig jdvsConfig;
private final RestClient restClient = RestClient.create();
public Optional<String> fetchLatestVersion(String repoName) {
RepoConfig cfg = findConfig(repoName);
String metadataUrl = baseUrl(cfg) + "/maven-metadata.xml";
log.info("Fetching Maven metadata from {}", metadataUrl);
try {
String xml = restClient.get().uri(metadataUrl).retrieve().body(String.class);
Document doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(new InputSource(new StringReader(xml)));
String version = firstText(doc, "release");
if (version == null) {
version = firstText(doc, "latest");
}
if (version != null) {
log.info("Latest version of {} is {}", repoName, version);
return Optional.of(version);
}
log.warn("Could not determine latest version for {}", repoName);
} catch (Exception e) {
log.error("Failed to fetch Maven metadata for {}", repoName, e);
}
return Optional.empty();
}
public boolean generateJavadoc(String repoName, String version) {
RepoConfig cfg = findConfig(repoName);
String resolvedVersion = version.endsWith("-SNAPSHOT")
? resolveSnapshotVersion(cfg, version)
: version;
String jarUrl = baseUrl(cfg) + "/" + version + "/"
+ cfg.getArtifactId() + "-" + resolvedVersion + "-sources.jar";
log.info("Downloading sources JAR from {}", jarUrl);
Path workDir = null;
try {
workDir = Files.createTempDirectory("jdvs-maven-" + repoName + "-");
Path srcMainJava = workDir.resolve("src/main/java");
Files.createDirectories(srcMainJava);
downloadAndExtract(jarUrl, workDir, srcMainJava);
copyPomFromMetaInf(workDir, cfg);
File outputDir = Path.of(jdvsConfig.getBaseDir(), "javadoc", repoName).toFile();
if (!outputDir.exists()) {
outputDir.mkdirs();
}
return runMavenJavadoc(workDir, cfg, repoName, outputDir.toPath());
} catch (Exception e) {
log.error("Failed to generate Javadoc for {} version {}", repoName, version, e);
return false;
} finally {
if (workDir != null) {
deleteDirectory(workDir);
}
}
}
private void downloadAndExtract(String jarUrl, Path metaDir, Path srcDir) throws IOException {
byte[] jarBytes = restClient.get().uri(jarUrl).retrieve().body(byte[].class);
if (jarBytes == null) {
throw new IOException("Empty response for " + jarUrl);
}
try (ZipInputStream zip = new ZipInputStream(new java.io.ByteArrayInputStream(jarBytes))) {
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
if (!entry.isDirectory()) {
String name = entry.getName();
// META-INF goes to metaDir, Java sources go to srcDir
Path base = name.startsWith("META-INF/") ? metaDir : srcDir;
Path dest = base.resolve(name).normalize();
if (!dest.startsWith(metaDir) && !dest.startsWith(srcDir)) {
throw new IOException("Zip slip detected: " + name);
}
Files.createDirectories(dest.getParent());
Files.copy(zip, dest, StandardCopyOption.REPLACE_EXISTING);
}
zip.closeEntry();
}
}
}
private void copyPomFromMetaInf(Path workDir, RepoConfig cfg) throws IOException {
Path pomSource = workDir.resolve("META-INF/maven")
.resolve(cfg.getGroupId())
.resolve(cfg.getArtifactId())
.resolve("pom.xml");
if (!Files.exists(pomSource)) {
throw new IOException("pom.xml not found in META-INF at " + pomSource);
}
Files.copy(pomSource, workDir.resolve("pom.xml"), StandardCopyOption.REPLACE_EXISTING);
log.info("Copied POM from META-INF to working directory");
}
private boolean runMavenJavadoc(Path workDir, RepoConfig cfg, String repoName, Path outputDir)
throws IOException, InterruptedException {
Path mavenLocalRepo = Path.of(jdvsConfig.getBaseDir(), "maven-repo").toAbsolutePath();
Files.createDirectories(mavenLocalRepo);
List<String> cmd = new ArrayList<>(List.of(
"mvn", "-U", "--no-transfer-progress",
"clean", "javadoc:javadoc",
"-Djacoco.skip=true",
"-DskipTests",
"-Dcheckstyle.skip=true",
"-Dlombok.delombok.skip=true",
"-Dmaven.repo.local=" + mavenLocalRepo
));
if (cfg.getMavenRepoUrl() != null) {
cmd.add("-s");
cmd.add(writeSettingsXml(workDir, cfg.getMavenRepoUrl()).toAbsolutePath().toString());
}
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.directory(workDir.toFile());
pb.redirectErrorStream(true);
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
log.debug("[MVN] {}", line);
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
log.error("mvn javadoc:javadoc failed with exit code {} for {}", exitCode, repoName);
return false;
}
Path apidocsDir = workDir.resolve("target/reports/apidocs");
if (!Files.exists(apidocsDir)) {
// Log actual target content to find where Maven put the output
Path targetDir = workDir.resolve("target");
if (Files.exists(targetDir)) {
try (var s = Files.walk(targetDir, 4)) {
s.filter(p -> p.toString().endsWith(".html"))
.findFirst()
.ifPresentOrElse(
p -> log.error("Javadoc not at expected path. Found HTML at: {}", p),
() -> log.error("No HTML files found under {}", targetDir)
);
}
} else {
log.error("target/ directory does not exist in {}", workDir);
}
return false;
}
copyDirectory(apidocsDir, outputDir);
log.info("Javadoc successfully generated for {} in {}", repoName, outputDir);
return true;
}
private Path writeSettingsXml(Path workDir, String repoUrl) throws IOException {
Path settingsFile = workDir.resolve("settings.xml");
String xml = """
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<profiles>
<profile>
<id>jdvs-repos</id>
<repositories>
<repository>
<id>custom</id>
<url>""" + repoUrl + """
</url>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>jdvs-repos</activeProfile>
</activeProfiles>
</settings>
""";
Files.writeString(settingsFile, xml);
return settingsFile;
}
private void copyDirectory(Path source, Path target) throws IOException {
try (var stream = Files.walk(source)) {
stream.forEach(path -> {
try {
Path dest = target.resolve(source.relativize(path));
if (Files.isDirectory(path)) {
Files.createDirectories(dest);
} else {
Files.createDirectories(dest.getParent());
Files.copy(path, dest, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
private String resolveSnapshotVersion(RepoConfig cfg, String version) {
String metadataUrl = baseUrl(cfg) + "/" + version + "/maven-metadata.xml";
log.info("Resolving SNAPSHOT version from {}", metadataUrl);
try {
String xml = restClient.get().uri(metadataUrl).retrieve().body(String.class);
Document doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(new InputSource(new StringReader(xml)));
NodeList nodes = doc.getElementsByTagName("snapshotVersion");
for (int i = 0; i < nodes.getLength(); i++) {
org.w3c.dom.Element el = (org.w3c.dom.Element) nodes.item(i);
String classifier = firstText(el, "classifier");
String ext = firstText(el, "extension");
String value = firstText(el, "value");
if ("sources".equals(classifier) && "jar".equals(ext) && value != null) {
log.info("Resolved SNAPSHOT version to {}", value);
return value;
}
}
} catch (Exception e) {
log.warn("Could not resolve SNAPSHOT version for {}, falling back to {}",
cfg.getArtifactId(), version, e);
}
return version;
}
private String baseUrl(RepoConfig cfg) {
String repoUrl = cfg.getMavenRepoUrl() != null
? cfg.getMavenRepoUrl()
: jdvsConfig.getMavenCentralUrl();
String groupPath = cfg.getGroupId().replace('.', '/');
return repoUrl + "/" + groupPath + "/" + cfg.getArtifactId();
}
private RepoConfig findConfig(String repoName) {
return jdvsConfig.getRepositories().stream()
.filter(r -> repoName.equals(r.getName()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No config found for: " + repoName));
}
private String firstText(Document doc, String tagName) {
NodeList nodes = doc.getElementsByTagName(tagName);
return nodes.getLength() > 0 ? nodes.item(0).getTextContent() : null;
}
private String firstText(org.w3c.dom.Element parent, String tagName) {
NodeList nodes = parent.getElementsByTagName(tagName);
return nodes.getLength() > 0 ? nodes.item(0).getTextContent() : null;
}
private void deleteDirectory(Path dir) {
try (var stream = Files.walk(dir)) {
stream.sorted(java.util.Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
} catch (IOException e) {
log.warn("Failed to clean temp directory {}", dir, e);
}
}
}
@@ -20,7 +20,6 @@ public class RepoConfigLoader {
if (repository.findByName(repoCfg.getName()).isEmpty()) { if (repository.findByName(repoCfg.getName()).isEmpty()) {
GitRepository repo = new GitRepository(); GitRepository repo = new GitRepository();
repo.setName(repoCfg.getName()); repo.setName(repoCfg.getName());
repo.setUrl(repoCfg.getUrl());
repository.save(repo); repository.save(repo);
log.info("Repository {} added from configuration.", repoCfg.getName()); log.info("Repository {} added from configuration.", repoCfg.getName());
} }
@@ -23,8 +23,7 @@ import org.springframework.stereotype.Service;
@Slf4j @Slf4j
public class UpdateScheduler { public class UpdateScheduler {
private final GitRepositoryRepository repository; private final GitRepositoryRepository repository;
private final GitService gitService; private final MavenSourceService mavenSourceService;
private final JavadocService javadocService;
private final JdvsConfig jdvsConfig; private final JdvsConfig jdvsConfig;
private final RepoConfigLoader repoConfigLoader; private final RepoConfigLoader repoConfigLoader;
@@ -66,19 +65,18 @@ public class UpdateScheduler {
private void updateRepo(GitRepository repo) { private void updateRepo(GitRepository repo) {
log.info("Checking repository: {}", repo.getName()); log.info("Checking repository: {}", repo.getName());
Optional<String> latestTag = gitService.updateAndCheckoutLatestTag(repo); Optional<String> latestVersion = mavenSourceService.fetchLatestVersion(repo.getName());
if (latestTag.isPresent()) { if (latestVersion.isPresent()) {
String tag = latestTag.get(); String version = latestVersion.get();
if (!tag.equals(repo.getLastTag())) { if (!version.equals(repo.getLastTag())) {
log.info("New tag {} found for {}. Generating Javadoc...", tag, repo.getName()); log.info("New version {} found for {}. Generating Javadoc...", version, repo.getName());
File repoDir = gitService.getRepoDirectory(repo.getName()); if (mavenSourceService.generateJavadoc(repo.getName(), version)) {
if (javadocService.generateJavadoc(repo.getName(), repoDir)) { repo.setLastTag(version);
repo.setLastTag(tag);
repo.setUpdated(LocalDateTime.now()); repo.setUpdated(LocalDateTime.now());
repository.save(repo); repository.save(repo);
} }
} else { } else {
log.info("Repository {} is already up to date ({}).", repo.getName(), tag); log.info("Repository {} is already up to date ({}).", repo.getName(), version);
} }
} }
} }
+1
View File
@@ -3,6 +3,7 @@ jdvs:
cron: 0 0/15 * * * ? cron: 0 0/15 * * * ?
clean-on-start: false clean-on-start: false
run-on-start: false run-on-start: false
maven-central-url: https://repo1.maven.org/maven2
## import of the individual configuration settings ## import of the individual configuration settings
spring: spring:
@@ -19,11 +19,6 @@ databaseChangeLog:
constraints: constraints:
nullable: false nullable: false
unique: true unique: true
- column:
name: url
type: VARCHAR(512)
constraints:
nullable: false
- column: - column:
name: last_tag name: last_tag
type: VARCHAR(255) type: VARCHAR(255)
+2 -4
View File
@@ -11,8 +11,7 @@
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Git URL</th> <th>Latest Version</th>
<th>Latest Tag</th>
<th>Last Update</th> <th>Last Update</th>
<th>Action</th> <th>Action</th>
</tr> </tr>
@@ -20,8 +19,7 @@
<tbody> <tbody>
<tr th:each="repo : ${repos}"> <tr th:each="repo : ${repos}">
<td th:text="${repo.name}">Project</td> <td th:text="${repo.name}">Project</td>
<td th:text="${repo.url}">URL</td> <td th:text="${repo.lastTag}">-</td>
<td th:text="${repo.lastTag}">v1.0</td>
<td th:text="${#temporals.format(repo.updated, 'dd.MM.yyyy HH:mm')}">-</td> <td th:text="${#temporals.format(repo.updated, 'dd.MM.yyyy HH:mm')}">-</td>
<td> <td>
<a th:if="${repo.lastTag != null}" th:href="@{/{name}/index.html(name=${repo.name})}" class="btn btn-primary btn-sm">Open Javadoc</a> <a th:if="${repo.lastTag != null}" th:href="@{/{name}/index.html(name=${repo.name})}" class="btn btn-primary btn-sm">Open Javadoc</a>