initial commit

This commit is contained in:
2026-06-29 17:12:24 +02:00
parent 5550b06527
commit dea9d0b9a0
17 changed files with 638 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(java -version)"
]
}
}
+41
View File
@@ -0,0 +1,41 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
## local ##
/database/
/javadoc-storage/
/jvs.yml
+113
View File
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>codes.thischwa</groupId>
<artifactId>jvc</artifactId>
<version>0.1.0-SNAPSHOT</version>
<name>JavadocViewerService</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.4</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<scm>
<developerConnection>scm:git:https://git.mein-gateway.de/thischwa/JavadocViewerService.git</developerConnection>
<connection>scm:git:https://git.mein-gateway.de/thischwa/JavadocViewerService.git</connection>
<url>https://git.mein-gateway.de/thischwa/JavadocViewerService</url>
<tag>HEAD</tag>
</scm>
<distributionManagement>
<repository>
<id>mygitea</id>
<url>https://git.mein-gateway.de/api/packages/thischwa/maven</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
<snapshotRepository>
<id>mygitea</id>
<url>https://git.mein-gateway.de/api/packages/thischwa/maven</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</snapshotRepository>
</distributionManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.6.1.202309021850-r</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package codes.thischwa.jvs;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class JavadocViewerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(JavadocViewerServiceApplication.class, args);
}
}
@@ -0,0 +1,21 @@
package codes.thischwa.jvs.config;
import java.util.List;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "jvs")
@Data
public class JvsConfig {
private String configPath;
private String baseDir;
private List<RepoConfig> repositories;
@Data
public static class RepoConfig {
private String name;
private String url;
}
}
@@ -0,0 +1,27 @@
package codes.thischwa.jvs.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.time.LocalDateTime;
import lombok.Data;
@Entity
@Data
public class GitRepository {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
@Column(nullable = false)
private String url;
private String lastTag;
private LocalDateTime updated;
}
@@ -0,0 +1,9 @@
package codes.thischwa.jvs.repository;
import codes.thischwa.jvs.model.GitRepository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface GitRepositoryRepository extends JpaRepository<GitRepository, Long> {
Optional<GitRepository> findByName(String name);
}
@@ -0,0 +1,63 @@
package codes.thischwa.jvs.service;
import codes.thischwa.jvs.config.JvsConfig;
import codes.thischwa.jvs.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 JvsConfig jvsConfig;
public Optional<String> updateAndCheckoutLatestTag(GitRepository repoEntity) {
Path repoPath = Path.of(jvsConfig.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(jvsConfig.getBaseDir(), "repos", name).toFile();
}
}
@@ -0,0 +1,115 @@
package codes.thischwa.jvs.service;
import codes.thischwa.jvs.config.JvsConfig;
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 JvsConfig jvsConfig;
public boolean generateJavadoc(String projectName, File repoDir) {
File outputDir = Path.of(jvsConfig.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) {
// Possible standard paths for Javadoc
String[] possiblePaths = {
"target/reports/apidocs",
"target/site/apidocs",
"target/apidocs"
};
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"
);
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,48 @@
package codes.thischwa.jvs.service;
import codes.thischwa.jvs.config.JvsConfig;
import codes.thischwa.jvs.model.GitRepository;
import codes.thischwa.jvs.repository.GitRepositoryRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import jakarta.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class RepoConfigLoader {
private final JvsConfig jvsConfig;
private final GitRepositoryRepository repository;
@PostConstruct
public void loadConfig() {
File configFile = new File(jvsConfig.getConfigPath());
if (!configFile.exists()) {
log.info("Configuration file {} not found.", jvsConfig.getConfigPath());
return;
}
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
try {
JvsConfig yamlConfig = mapper.readValue(configFile, JvsConfig.class);
if (yamlConfig.getRepositories() != null) {
for (JvsConfig.RepoConfig repoCfg : yamlConfig.getRepositories()) {
if (repository.findByName(repoCfg.getName()).isEmpty()) {
GitRepository repo = new GitRepository();
repo.setName(repoCfg.getName());
repo.setUrl(repoCfg.getUrl());
repository.save(repo);
log.info("Repository {} added from configuration.", repoCfg.getName());
}
}
}
} catch (IOException e) {
log.error("Error reading configuration file", e);
}
}
}
@@ -0,0 +1,49 @@
package codes.thischwa.jvs.service;
import codes.thischwa.jvs.model.GitRepository;
import codes.thischwa.jvs.repository.GitRepositoryRepository;
import java.io.File;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class UpdateScheduler {
private final GitRepositoryRepository repository;
private final GitService gitService;
private final JavadocService javadocService;
@Scheduled(fixedRateString = "${jvs.update-interval:PT1H}")
public void updateAll() {
log.info("Starting scheduled update of repositories...");
List<GitRepository> repos = repository.findAll();
for (GitRepository repo : repos) {
updateRepo(repo);
}
}
private void updateRepo(GitRepository repo) {
log.info("Checking repository: {}", repo.getName());
Optional<String> latestTag = gitService.updateAndCheckoutLatestTag(repo);
if (latestTag.isPresent()) {
String tag = latestTag.get();
if (!tag.equals(repo.getLastTag())) {
log.info("New tag {} found for {}. Generating Javadoc...", tag, repo.getName());
File repoDir = gitService.getRepoDirectory(repo.getName());
if (javadocService.generateJavadoc(repo.getName(), repoDir)) {
repo.setLastTag(tag);
repo.setUpdated(LocalDateTime.now());
repository.save(repo);
}
} else {
log.info("Repository {} is already up to date ({}).", repo.getName(), tag);
}
}
}
}
@@ -0,0 +1,19 @@
package codes.thischwa.jvs.web;
import codes.thischwa.jvs.repository.GitRepositoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
@RequiredArgsConstructor
public class HomeController {
private final GitRepositoryRepository repository;
@GetMapping("/")
public String index(Model model) {
model.addAttribute("repos", repository.findAll());
return "index";
}
}
@@ -0,0 +1,26 @@
package codes.thischwa.jvs.web;
import codes.thischwa.jvs.config.JvsConfig;
import java.nio.file.Path;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final JvsConfig jvsConfig;
public WebConfig(JvsConfig jvsConfig) {
this.jvsConfig = jvsConfig;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String javadocPath = Path.of(jvsConfig.getBaseDir(), "javadoc").toAbsolutePath().toUri().toString();
if (!javadocPath.endsWith("/")) {
javadocPath += "/";
}
registry.addResourceHandler("/**")
.addResourceLocations(javadocPath);
}
}
+17
View File
@@ -0,0 +1,17 @@
spring:
datasource:
url: jdbc:h2:file:./database/jvsdb
driverClassName: org.h2.Driver
username: sa
password: ""
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: none
liquibase:
change-log: classpath:db/changelog/db.changelog-master.yaml
jvs:
config-path: jvs.yml
base-dir: ./javadoc-storage
update-interval: PT1H
@@ -0,0 +1,32 @@
databaseChangeLog:
- changeSet:
id: 1
author: junie
changes:
- createTable:
tableName: git_repository
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: name
type: VARCHAR(255)
constraints:
nullable: false
unique: true
- column:
name: url
type: VARCHAR(512)
constraints:
nullable: false
- column:
name: last_tag
type: VARCHAR(255)
- column:
name: updated
type: TIMESTAMP
@@ -0,0 +1,3 @@
databaseChangeLog:
- include:
file: db/changelog/001-init-schema.yaml
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Javadoc Viewer Service</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">
<h1>Projekt Javadoc Übersicht</h1>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Git URL</th>
<th>Letzter Tag</th>
<th>Letztes Update</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<tr th:each="repo : ${repos}">
<td th:text="${repo.name}">Projekt</td>
<td th:text="${repo.url}">URL</td>
<td th:text="${repo.lastTag}">v1.0</td>
<td th:text="${#temporals.format(repo.updated, 'dd.MM.yyyy HH:mm')}">-</td>
<td>
<a th:if="${repo.lastTag != null}" th:href="@{/{name}/index.html(name=${repo.name})}" class="btn btn-primary btn-sm">Javadoc öffnen</a>
<span th:if="${repo.lastTag == null}" class="badge bg-secondary">Noch nicht generiert</span>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>