diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c1b1c9d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(java -version)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f244710 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1d38f09 --- /dev/null +++ b/pom.xml @@ -0,0 +1,113 @@ + + + 4.0.0 + + codes.thischwa + jvc + 0.1.0-SNAPSHOT + + JavadocViewerService + + + org.springframework.boot + spring-boot-starter-parent + 4.0.4 + + + + + 17 + UTF-8 + + + + scm:git:https://git.mein-gateway.de/thischwa/JavadocViewerService.git + scm:git:https://git.mein-gateway.de/thischwa/JavadocViewerService.git + https://git.mein-gateway.de/thischwa/JavadocViewerService + HEAD + + + + + mygitea + https://git.mein-gateway.de/api/packages/thischwa/maven + + true + + + + mygitea + https://git.mein-gateway.de/api/packages/thischwa/maven + + true + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + org.liquibase + liquibase-core + + + com.h2database + h2 + runtime + + + org.projectlombok + lombok + true + + + org.eclipse.jgit + org.eclipse.jgit + 6.6.1.202309021850-r + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + \ No newline at end of file diff --git a/src/main/java/codes/thischwa/jvs/JavadocViewerServiceApplication.java b/src/main/java/codes/thischwa/jvs/JavadocViewerServiceApplication.java new file mode 100644 index 0000000..030c97c --- /dev/null +++ b/src/main/java/codes/thischwa/jvs/JavadocViewerServiceApplication.java @@ -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); + } +} diff --git a/src/main/java/codes/thischwa/jvs/config/JvsConfig.java b/src/main/java/codes/thischwa/jvs/config/JvsConfig.java new file mode 100644 index 0000000..2e2b661 --- /dev/null +++ b/src/main/java/codes/thischwa/jvs/config/JvsConfig.java @@ -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 repositories; + + @Data + public static class RepoConfig { + private String name; + private String url; + } +} diff --git a/src/main/java/codes/thischwa/jvs/model/GitRepository.java b/src/main/java/codes/thischwa/jvs/model/GitRepository.java new file mode 100644 index 0000000..dc3a82a --- /dev/null +++ b/src/main/java/codes/thischwa/jvs/model/GitRepository.java @@ -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; +} diff --git a/src/main/java/codes/thischwa/jvs/repository/GitRepositoryRepository.java b/src/main/java/codes/thischwa/jvs/repository/GitRepositoryRepository.java new file mode 100644 index 0000000..9cc81f9 --- /dev/null +++ b/src/main/java/codes/thischwa/jvs/repository/GitRepositoryRepository.java @@ -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 { + Optional findByName(String name); +} diff --git a/src/main/java/codes/thischwa/jvs/service/GitService.java b/src/main/java/codes/thischwa/jvs/service/GitService.java new file mode 100644 index 0000000..f8782fe --- /dev/null +++ b/src/main/java/codes/thischwa/jvs/service/GitService.java @@ -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 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 tags = git.tagList().call(); + Optional 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(); + } +} diff --git a/src/main/java/codes/thischwa/jvs/service/JavadocService.java b/src/main/java/codes/thischwa/jvs/service/JavadocService.java new file mode 100644 index 0000000..b4b4172 --- /dev/null +++ b/src/main/java/codes/thischwa/jvs/service/JavadocService.java @@ -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 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; + } +} diff --git a/src/main/java/codes/thischwa/jvs/service/RepoConfigLoader.java b/src/main/java/codes/thischwa/jvs/service/RepoConfigLoader.java new file mode 100644 index 0000000..128b88d --- /dev/null +++ b/src/main/java/codes/thischwa/jvs/service/RepoConfigLoader.java @@ -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); + } + } +} diff --git a/src/main/java/codes/thischwa/jvs/service/UpdateScheduler.java b/src/main/java/codes/thischwa/jvs/service/UpdateScheduler.java new file mode 100644 index 0000000..842d650 --- /dev/null +++ b/src/main/java/codes/thischwa/jvs/service/UpdateScheduler.java @@ -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 repos = repository.findAll(); + for (GitRepository repo : repos) { + updateRepo(repo); + } + } + + private void updateRepo(GitRepository repo) { + log.info("Checking repository: {}", repo.getName()); + Optional 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); + } + } + } +} diff --git a/src/main/java/codes/thischwa/jvs/web/HomeController.java b/src/main/java/codes/thischwa/jvs/web/HomeController.java new file mode 100644 index 0000000..fb8f334 --- /dev/null +++ b/src/main/java/codes/thischwa/jvs/web/HomeController.java @@ -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"; + } +} diff --git a/src/main/java/codes/thischwa/jvs/web/WebConfig.java b/src/main/java/codes/thischwa/jvs/web/WebConfig.java new file mode 100644 index 0000000..ae2c061 --- /dev/null +++ b/src/main/java/codes/thischwa/jvs/web/WebConfig.java @@ -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); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..5b27097 --- /dev/null +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/main/resources/db/changelog/001-init-schema.yaml b/src/main/resources/db/changelog/001-init-schema.yaml new file mode 100644 index 0000000..e4734bb --- /dev/null +++ b/src/main/resources/db/changelog/001-init-schema.yaml @@ -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 diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 0000000..4acda8f --- /dev/null +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,3 @@ +databaseChangeLog: + - include: + file: db/changelog/001-init-schema.yaml diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..a986264 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,35 @@ + + + + Javadoc Viewer Service + + + + + Projekt Javadoc Übersicht + + + + Name + Git URL + Letzter Tag + Letztes Update + Aktion + + + + + Projekt + URL + v1.0 + - + + Javadoc öffnen + Noch nicht generiert + + + + + + +