Initial commit
Build and Push Docker Image / build-and-push (push) Successful in 13s

This commit is contained in:
2026-07-12 13:43:37 +02:00
commit 0c9ad846f4
28 changed files with 1518 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
name: Build and Push Docker Image
on:
push:
tags:
- 'v*'
branches:
- develop
- 'feature/*'
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Compute image name and tag
id: compute-tag
run: |
REGISTRY=$(echo "${{ github.server_url }}" | sed 's|https\?://||')
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
ACTOR=$(echo "${{ github.actor }}" | tr '[:upper:]' '[:lower:]')
IMAGE_NAME="javadocviewerservice"
FULL_IMAGE="${REGISTRY}/${OWNER}/${IMAGE_NAME}"
REF="${{ github.ref_name }}"
if [[ "${REF}" == v* ]]; then
TAG="${REF#v}"
TAGS="${FULL_IMAGE}:${TAG},${FULL_IMAGE}:latest"
else
TAG=$(echo "${REF}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/-/g')
TAGS="${FULL_IMAGE}:${TAG}"
fi
echo "Registry: ${REGISTRY}"
echo "Actor: ${ACTOR}"
echo "Tags: ${TAGS}"
echo "registry=${REGISTRY}" >> "$GITHUB_OUTPUT"
echo "actor=${ACTOR}" >> "$GITHUB_OUTPUT"
echo "image=${FULL_IMAGE}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"
- name: Network Debug
run: |
curl -v https://git.mein-gateway.de/v2/
- name: Debug Secret
run: |
echo "Secret length: ${#TOKEN}"
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: git.mein-gateway.de
username: thischwa
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.compute-tag.outputs.tags }}
+43
View File
@@ -0,0 +1,43 @@
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/
/jdvs.yml
/.claude/
/info.txt
+33
View File
@@ -0,0 +1,33 @@
# JavadocViewerService
## Tech Stack
- **Java 21**
- **Spring Boot 4.x** (spring-boot-starter-parent)
- Spring MVC (spring-boot-starter-web)
- Spring Data JPA (spring-boot-starter-data-jpa)
- Thymeleaf (spring-boot-starter-thymeleaf)
- Bean Validation (spring-boot-starter-validation)
- Liquibase (spring-boot-starter-liquibase)
- **H2** — file-based embedded database (`./database/jdvsdb`)
- **Lombok** — boilerplate reduction (`@Data`, `@RequiredArgsConstructor`, `@Slf4j`)
- **JGit 7.x** — Git operations (clone, fetch, tag resolution)
- **Jackson YAML** — parsing of `jdvs.yml` config file
- **Maven** — build tool
## Configuration
- `application.yml` — base configuration (datasource, JPA, default jdvs properties)
- `jdvs.yml` — external runtime config (repositories, property overrides); imported via `spring.config.import`
## Package Structure
- `codes.thischwa.jdvs.config``@ConfigurationProperties` beans
- `codes.thischwa.jdvs.service` — business logic (Git, Javadoc generation, scheduling)
- `codes.thischwa.jdvs.jpa` — Spring Data repositories
- `codes.thischwa.jdvs.model` — JPA entities
- `codes.thischwa.jdvs.web` — MVC controllers and `WebMvcConfigurer`
## Coding
The generated code follows the principles of clean architecture and follows the SOLID principles. It is designed to be modular, testable, and maintainable. The code is written in a way that promotes readability and reduces complexity. It uses modern Java features and best practices to ensure efficient and reliable operation. To formate the code use che checkstyle definition in [google_custom_checks.xml](src/checkstyle/google_custom_checks.xml). Each changing of code must be verified with compiling.
+32
View File
@@ -0,0 +1,32 @@
# Stage 1: Build the JAR using Maven
FROM maven:3.9-eclipse-temurin-21 AS builder
LABEL org.opencontainers.image.description="JavadocViewerService serves and manages Javadoc documentation from Git repositories."
WORKDIR /build
# Copy pom.xml and download dependencies first (for Docker cache)
COPY pom.xml .
RUN mvn dependency:go-offline
# Copy the full source tree and build the application
COPY src ./src
RUN mvn clean package -DskipTests
# Stage 2: Minimal runtime image
FROM eclipse-temurin:21-jdk-jammy
WORKDIR /app
# Install tini and Maven
RUN apt-get update && apt-get install -y tini maven && rm -rf /var/lib/apt/lists/*
# Copy the built jar from the builder stage
COPY --from=builder /build/target/*.jar /app/jvs.jar
VOLUME ["/app/javadoc-storage", "/app/database"]
EXPOSE 8080
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["java", "-jar", "jvs.jar"]
+16
View File
@@ -0,0 +1,16 @@
MIT No Attribution
Copyright <YEAR> <COPYRIGHT HOLDER>
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+77
View File
@@ -0,0 +1,77 @@
# JavadocViewerService
## Preface
This project Spring Boot web service that automatically fetches, manages, and serves Javadoc documentation for Maven artifacts.
If you encounter any bugs or find missing features, feel free to report them on
the [GitHub Issues page](https://github.com/th-schwarz/JavadocViewerService/issues).
## Features
- Retrieves Javadoc JARs from Maven Central or custom Maven repositories
- Tracks multiple Maven artefacts with version detection (only the latest version is taken into account).
- Delivers generated Javadoc content via a simple bootstrap web interface
- Scheduled updates via a configurable cron expression
## Requirements
At least JRE-21 or docker
## Start
### ... with Java
```bash
java -jar jdvc-<version>.jar --spring.config.import=file:./jdvs.yml
```
### .. with Docker
```yaml
services:
jvs-dockerized:
image: git.mein-gateway.de/thischwa/javadocviewerservice:develop
volumes:
- /opt/javadocviewerservice-dockerized/jdvs.yml:/app/jdvs.yml:ro
- /opt/javadocviewerservice-dockerized/database:/app/database
- /opt/javadocviewerservice-dockerized/javadoc-storage:/app/javadoc-storage
restart: unless-stopped
ports:
- "127.0.0.1:8086:8080"
```
Volume mapping:
| Mount | Purpose |
|---|---|
| `jdvs.yml:/app/jdvs.yml:ro` | Injects the repository configuration (which artifacts to track) as read-only — the only file you need to edit |
| `database:/app/database` | Persists the H2 database so tracked version metadata survives restarts and image updates |
| `javadoc-storage:/app/javadoc-storage` | Persists generated Javadoc HTML so it is not lost on container recreation |
## Configuration
### `application.yml`
Key defaults:
| Property | Default | Description |
|---|----------------------------------|------------------------------------|
| `jdvs.base-dir` | `./javadoc-storage` | Storage root for generated Javadoc |
| `jdvs.cron` | `0 0/30 * * * ?` | Update schedule (every 30 min) |
| `jdvs.clean-on-start` | `false` | Wipe storage and DB on startup |
| `jdvs.run-on-start` | `false` | Run update immediately on startup |
| `jdvs.maven-central-url` | `https://repo1.maven.org/maven2` | Default Maven repository |
### `jdvs.yml`
Defines the repositories to track and individual settings (all key defaults can be overridden):
```yaml
jdvs:
repositories:
- name: my-lib
group-id: com.example
artifact-id: my-lib
# maven-repo-url: https://custom.repo/maven # optional, defaults to Maven Central
```
+144
View File
@@ -0,0 +1,144 @@
<?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>jdvc</artifactId>
<version>0.2.0-SNAPSHOT</version>
<name>JavadocViewerService</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.5</version>
<relativePath />
</parent>
<properties>
<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>
</properties>
<issueManagement>
<url>https://git.mein-gateway.de/thischwa/JavadocViewerService/issues</url>
<system>Gitea Issues</system>
</issueManagement>
<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.springframework.boot</groupId>
<artifactId>spring-boot-starter-liquibase</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>7.2.1.202505142326-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.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<!-- build the fat jar -->
<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>
+484
View File
@@ -0,0 +1,484 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!--
Checkstyle configuration that checks the Google coding conventions from Google Java Style
that can be found at https://google.github.io/styleguide/javaguide.html
Checkstyle is very configurable. Be sure to read the documentation at
http://checkstyle.org (or in your downloaded distribution).
To completely disable a check, just comment it out or delete it from the file.
To suppress certain violations please review suppression filters.
Authors: Max Vetrenko, Mauryan Kansara, Ruslan Diachenko, Roman Ivanov.
-->
<!-- version: 13.1.0 -->
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="${org.checkstyle.google.severity}" default="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/filefilters/index.html -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<module name="SuppressWarningsFilter"/>
<!-- https://checkstyle.org/filters/suppressionfilter.html -->
<module name="SuppressionFilter">
<property name="file" value="${org.checkstyle.google.suppressionfilter.config}"
default="checkstyle-suppressions.xml" />
<property name="optional" value="true"/>
</module>
<!-- https://checkstyle.org/filters/suppresswithnearbytextfilter.html -->
<module name="SuppressWithNearbyTextFilter">
<property name="nearbyTextPattern"
value="CHECKSTYLE.SUPPRESS\: (\w+) for ([+-]\d+) lines"/>
<property name="checkPattern" value="$1"/>
<property name="lineRange" value="$2"/>
</module>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.org/checks/whitespace/index.html -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<module name="LineLength">
<property name="fileExtensions" value="java"/>
<property name="max" value="140"/>
<property name="ignorePattern"
value="^package.*|^import.*|href\s*=\s*&quot;[^&quot;]*&quot;|http://|https://|ftp://"/>
</module>
<!-- Suppression to prevent LineLength Check from flagging lines in Text-blocks -->
<module name="SuppressWithPlainTextCommentFilter">
<property name="checkFormat" value="LineLength"/>
<property name="offCommentFormat" value='^.*"""\s*$'/>
<property name="onCommentFormat" value='^\s*"""\s*(?:[,;]|.+)$'/>
</module>
<module name="TreeWalker">
<module name="OuterTypeFilename"/>
<module name="MatchXpath">
<property name="id" value="singleLineCommentStartWithSpace"/>
<property name="query"
value="//SINGLE_LINE_COMMENT[./COMMENT_CONTENT[not(starts-with(@text, ' '))
and not(starts-with(@text, '/'))
and not(@text = '\n') and not(ends-with(@text, '//\n'))]]"/>
<message key="matchxpath.match" value="''//'' must be followed by a whitespace."/>
</module>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL, TEXT_BLOCK_CONTENT"/>
<property name="format"
value="\\u00(08|09|0(a|A)|0(c|C)|0(d|D)|20|22|27|5(C|c))|\\(0(10|11|12|14|15|40|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap">
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>
</module>
<module name="NeedBraces">
<property name="tokens"
value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/>
</module>
<module name="LeftCurly">
<property name="id" value="LeftCurlyEol"/>
<property name="tokens"
value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
INTERFACE_DEF, LAMBDA, LITERAL_CATCH,
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,
OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="LeftCurly">
<property name="id" value="LeftCurlyNl"/>
<property name="option" value="nl"/>
<property name="tokens"
value="LITERAL_CASE, LITERAL_DEFAULT"/>
</module>
<module name="SuppressionXpathSingleFilter">
<!-- LITERAL_CASE, LITERAL_DEFAULT are reused in SWITCH_RULE -->
<property name="id" value="LeftCurlyNl"/>
<property name="query" value="//SWITCH_RULE/SLIST"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_IF, LITERAL_ELSE,
LITERAL_DO"/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="id" value="RightCurlySame"/>
<property name="query" value="//RCURLY[parent::SLIST[parent::LITERAL_CATCH
and not(parent::LITERAL_CATCH/following-sibling::*)]]"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF, LITERAL_SWITCH, LITERAL_CASE, LITERAL_FINALLY,
LITERAL_CATCH"/>
</module>
<module name="SuppressionXpathSingleFilter">
<!-- suppression is required till https://github.com/checkstyle/checkstyle/issues/7541 -->
<property name="id" value="RightCurlyAlone"/>
<property name="query" value="//RCURLY[parent::SLIST[count(./*)=1
and not(parent::LITERAL_CATCH)]
or (preceding-sibling::*[last()][self::LCURLY]
and not(parent::SLIST/parent::LITERAL_CATCH))
or (parent::SLIST/parent::LITERAL_CATCH
and parent::SLIST/parent::LITERAL_CATCH/following-sibling::*)]"/>
</module>
<module name="WhitespaceAfter">
<property name="tokens"
value="COMMA, SEMI, TYPECAST, ELLIPSIS, LITERAL_YIELD, LITERAL_CASE, ANNOTATIONS"/>
</module>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyLambdas" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<property name="allowEmptySwitchBlockStatements" value="true"/>
<property name="ignoreEnhancedForColon" value="false"/>
<property name="tokens"
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,
LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,
LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,
LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT,
TYPE_EXTENSION_AND, LITERAL_WHEN"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks
may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="WhitespaceAround"/>
<property name="query" value="//*[self::LITERAL_IF or self::LITERAL_ELSE or
self::STATIC_INIT]/SLIST[count(./*)=1]
| //*[self::STATIC_INIT or self::LITERAL_TRY or self::LITERAL_IF]
//*[self::RCURLY][parent::SLIST[count(./*)=1]]
| //SLIST[count(./*)=1][parent::LITERAL_TRY and
not(following-sibling::*)]
| //SLIST[count(./*)=1][parent::LITERAL_CATCH and
not(parent::LITERAL_CATCH/following-sibling::*)]"/>
</module>
<module name="RegexpSinglelineJava">
<property name="format" value="\{[ ]+\}"/>
<property name="message" value="Empty blocks should have no spaces. Empty blocks
may only be represented as '{}' when not part of a
multi-block statement (4.1.3)"/>
</module>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="JavadocLeadingAsteriskAlign"/>
<module name="JavadocMissingLeadingAsterisk"/>
<module name="JavadocContentLocation"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="TextBlockGoogleStyleFormatting"/>
<module name="EmptyLineSeparator">
<property name="tokens"
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,
COMPACT_CTOR_DEF"/>
<property name="allowNoEmptyLineBetweenFields" value="true"/>
<property name="allowMultipleEmptyLines" value="false"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<property name="format" value="^[A-Z][a-zA-Z0-9]*(?:[0-9](?:_[0-9]+)*)?$"/>
<property name="tokens" value="CLASS_DEF"/>
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<property name="tokens" value="INTERFACE_DEF, ENUM_DEF,
ANNOTATION_DEF, RECORD_DEF"/>
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format"
value="^(?![a-z]$)(?![a-z][A-Z])[a-z][a-zA-Z0-9]*(?:_[0-9]+)*$"/>
<message key="name.invalidPattern"
value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LambdaParameterName">
<property name="format" value="^(_|[a-z]([a-z0-9][a-zA-Z0-9]*)?)$"/>
<message key="name.invalidPattern"
value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^(_|[a-z]([a-z0-9][a-zA-Z0-9]*)?)$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="format" value="^(_|[a-z]([a-z0-9][a-zA-Z0-9]*)?)$"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="PatternVariableName">
<property name="format" value="^(_|[a-z]([a-z0-9][a-zA-Z0-9]*)?)$"/>
<message key="name.invalidPattern"
value="Pattern variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="RecordComponentName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Record component name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="RecordTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Record type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="2"/>
<property name="braceAdjustment" value="2"/>
<property name="caseIndent" value="2"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="arrayInitIndent" value="2"/>
</module>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="0"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,
RECORD_COMPONENT_DEF"/>
</module>
<module name="NoWhitespaceBeforeCaseDefaultColon"/>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="ConstructorsDeclarationGrouping"/>
<module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
<property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF"/>
</module>
<module name="MethodParamPad">
<property name="tokens"
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF, CTOR_CALL,
SUPER_CTOR_CALL, ENUM_CONSTANT_DEF, RECORD_DEF, RECORD_PATTERN_DEF"/>
</module>
<module name="NoWhitespaceBefore">
<property name="tokens"
value="COMMA, SEMI, POST_INC, POST_DEC, DOT,
LABELED_STAT, METHOD_REF, ELLIPSIS"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="NoWhitespaceBefore"/>
<property name="query"
value="//ELLIPSIS[preceding-sibling::TYPE/ANNOTATIONS[ANNOTATION[LPAREN]
or not(following-sibling::*)]]"/>
</module>
<module name="ParenPad">
<property name="tokens"
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,
METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA,
RECORD_DEF, RECORD_PATTERN_DEF"/>
</module>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF,
TYPE_EXTENSION_AND "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,
RECORD_DEF, COMPACT_CTOR_DEF, PACKAGE_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="InvalidJavadocPosition"/>
<module name="JavadocTagContinuationIndentation"/>
<module name="SummaryJavadoc">
<property name="forbiddenSummaryFragments"
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )|^[a-z]"/>
</module>
<module name="JavadocParagraph">
<property name="allowNewlineParagraph" value="false"/>
</module>
<module name="RequireEmptyLineBeforeBlockTagGroup"/>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="accessModifiers" value="public"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF"/>
</module>
<module name="MissingJavadocMethod">
<property name="scope" value="protected"/>
<property name="allowMissingPropertyJavadoc" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,
COMPACT_CTOR_DEF"/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="MissingJavadocMethod"/>
<property name="query" value="//*[self::METHOD_DEF or self::CTOR_DEF
or self::ANNOTATION_FIELD_DEF or self::COMPACT_CTOR_DEF]
[ancestor::*[self::INTERFACE_DEF or self::CLASS_DEF
or self::RECORD_DEF or self::ENUM_DEF]
[not(./MODIFIERS/LITERAL_PUBLIC)]]"/>
</module>
<module name="MissingJavadocType">
<property name="scope" value="protected"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
RECORD_DEF, ANNOTATION_DEF"/>
<property name="excludeScope" value="nothing"/>
</module>
<module name="MethodName">
<property name="format"
value="^(?![a-z]$)(?![a-z][A-Z])[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*(?:_[0-9]+)*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SuppressionXpathSingleFilter">
<property name="checks" value="MethodName"/>
<property name="query" value="//METHOD_DEF[
./MODIFIERS/ANNOTATION//IDENT[contains(@text, 'Test')]
]/IDENT"/>
<property name="message" value="'[a-z][a-z0-9][a-zA-Z0-9]*(?:_[a-z][a-z0-9][a-zA-Z0-9]*)*'"/>
</module>
<module name="SingleLineJavadoc"/>
<module name="TodoComment">
<property name="format" value="^[ \t]*(?!TODO:)(?i:TODO)\b:?"/>
<message key="todo.match"
value="''TODO:'' must be written in all caps and followed by a colon."/>
</module>
<module name="EmptyCatchBlock">
<property name="commentFormat" value="\w+"/>
</module>
<module name="CommentsIndentation">
<property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/>
</module>
<!-- https://checkstyle.org/filters/suppressionxpathfilter.html -->
<module name="SuppressionXpathFilter">
<property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}"
default="checkstyle-xpath-suppressions.xml" />
<property name="optional" value="true"/>
</module>
<module name="SuppressWarningsHolder" />
<module name="SuppressionCommentFilter">
<property name="offCommentFormat" value="CHECKSTYLE.OFF\: ([\w\|]+)" />
<property name="onCommentFormat" value="CHECKSTYLE.ON\: ([\w\|]+)" />
<property name="checkFormat" value="$1" />
</module>
<module name="SuppressWithNearbyCommentFilter">
<property name="commentFormat" value="CHECKSTYLE.SUPPRESS\: ([\w\|]+)"/>
<!-- $1 refers to the first match group in the regex defined in commentFormat -->
<property name="checkFormat" value="$1"/>
<!-- The check is suppressed in the next line of code after the comment -->
<property name="influenceFormat" value="1"/>
</module>
</module>
</module>
@@ -0,0 +1,21 @@
package codes.thischwa.jdvs;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@Slf4j
public class JdvsApplication {
public static void main(String[] args) {
try {
SpringApplication.run(JdvsApplication.class, args);
} catch (Exception e) {
log.error("Unexpected exception, Spring Boot stops! Message: {}", e.getMessage());
System.exit(10);
}
}
}
@@ -0,0 +1,29 @@
package codes.thischwa.jdvs.config;
import codes.thischwa.jdvs.model.config.JdvsConfig;
import codes.thischwa.jdvs.jpa.GitRepositoryRepository;
import codes.thischwa.jdvs.model.GitRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class RepoConfigLoader {
private final JdvsConfig jdvsConfig;
private final GitRepositoryRepository repository;
public void loadConfig() {
if (jdvsConfig.getRepositories() != null) {
for (JdvsConfig.RepoConfig repoCfg : jdvsConfig.getRepositories()) {
if (repository.findByName(repoCfg.getName()).isEmpty()) {
GitRepository repo = new GitRepository();
repo.setName(repoCfg.getName());
repository.save(repo);
log.info("Repository {} added from configuration.", repoCfg.getName());
}
}
}
}
}
@@ -0,0 +1,9 @@
package codes.thischwa.jdvs.jpa;
import codes.thischwa.jdvs.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,24 @@
package codes.thischwa.jdvs.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;
private String lastTag;
private LocalDateTime updated;
}
@@ -0,0 +1,14 @@
package codes.thischwa.jdvs.model.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
@Configuration
public class AppConfig {
@Bean
public RestClient restClient() {
return RestClient.create();
}
}
@@ -0,0 +1,13 @@
package codes.thischwa.jdvs.model.config;
import java.util.List;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "git")
@Data
public class GitConfig {
private List<String> javadocPaths;
}
@@ -0,0 +1,31 @@
package codes.thischwa.jdvs.model.config;
import java.util.List;
import java.util.Objects;
import lombok.Data;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "jdvs")
@Data
public class JdvsConfig {
private String baseDir;
private boolean cleanOnStart;
private boolean runOnStart;
private String mavenCentralUrl;
private List<RepoConfig> repositories;
public String getEffectiveMavenRepoUrl(RepoConfig cfg) {
return Objects.requireNonNullElse(cfg.getMavenRepoUrl(), mavenCentralUrl);
}
@Data
public static class RepoConfig {
private String name;
private String groupId;
private String artifactId;
private String mavenRepoUrl;
}
}
@@ -0,0 +1,58 @@
package codes.thischwa.jdvs.service;
import codes.thischwa.jdvs.config.RepoConfigLoader;
import codes.thischwa.jdvs.jpa.GitRepositoryRepository;
import codes.thischwa.jdvs.model.config.JdvsConfig;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class ApplicationStartupListener {
private final GitRepositoryRepository repository;
private final JdvsConfig jdvsConfig;
private final RepoConfigLoader repoConfigLoader;
private final UpdateScheduler updateScheduler;
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
if (jdvsConfig.isCleanOnStart()) {
log.info("'jdvs.clean-on-start' is enabled cleaning base directory and database.");
cleanBaseDir();
repository.deleteAll();
}
repoConfigLoader.loadConfig();
if (jdvsConfig.isRunOnStart()) {
log.info("'jdvs.run-on-start' is enabled running initial update.");
updateScheduler.updateAll();
}
}
private void cleanBaseDir() {
Path baseDir = Path.of(jdvsConfig.getBaseDir());
if (!Files.exists(baseDir)) {
return;
}
try (var stream = Files.walk(baseDir)) {
stream.sorted(Comparator.reverseOrder()).forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
log.warn("Failed to delete path: {}", path, e);
}
});
log.info("Deleted base directory: {}", baseDir);
} catch (IOException e) {
log.error("Failed to walk base directory: {}", baseDir, e);
}
}
}
@@ -0,0 +1,153 @@
package codes.thischwa.jdvs.service;
import codes.thischwa.jdvs.model.config.JdvsConfig;
import codes.thischwa.jdvs.model.config.JdvsConfig.RepoConfig;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
@Service
@RequiredArgsConstructor
@Slf4j
public class MavenJavadocService {
private final JdvsConfig jdvsConfig;
private final RestClient restClient;
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 = parseXml(xml);
Optional<String> version = resolveVersionTag(doc);
if (version.isPresent()) {
log.info("Latest version of {} is {}", repoName, version.get());
return version;
}
log.warn("Could not determine latest version for {}", repoName);
} catch (ParserConfigurationException | SAXException | IOException e) {
log.error("Failed to parse Maven metadata for {}", repoName, e);
} catch (RestClientException 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 + "-javadoc.jar";
log.info("Downloading Javadoc JAR from {}", jarUrl);
Path outputDir = Path.of(jdvsConfig.getBaseDir(), "javadoc", repoName);
try {
Files.createDirectories(outputDir);
extractJavadocJar(jarUrl, outputDir);
log.info("Javadoc successfully extracted for {} to {}", repoName, outputDir);
return true;
} catch (IOException | RestClientException e) {
log.error("Failed to generate Javadoc for {} version {}", repoName, version, e);
return false;
}
}
private void extractJavadocJar(String jarUrl, Path outputDir) throws IOException {
byte[] jarBytes = restClient.get().uri(jarUrl).retrieve().body(byte[].class);
if (jarBytes == null) {
throw new IOException("Empty response for " + jarUrl);
}
Path safeOutputDir = outputDir.toAbsolutePath().normalize();
try (ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(jarBytes))) {
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
String name = entry.getName();
if (!entry.isDirectory() && !name.startsWith("META-INF/")) {
Path dest = safeOutputDir.resolve(name).normalize();
if (!dest.startsWith(safeOutputDir)) {
throw new IOException("Zip slip detected: " + name);
}
Files.createDirectories(dest.getParent());
Files.copy(zip, dest, StandardCopyOption.REPLACE_EXISTING);
}
zip.closeEntry();
}
}
}
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 = parseXml(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.getElementsByTagName("classifier"));
String ext = firstText(el.getElementsByTagName("extension"));
String value = firstText(el.getElementsByTagName("value"));
if ("javadoc".equals(classifier) && "jar".equals(ext) && value != null) {
log.info("Resolved SNAPSHOT version to {}", value);
return value;
}
}
} catch (ParserConfigurationException | SAXException | IOException e) {
log.warn("Could not parse SNAPSHOT metadata for {}, falling back to {}",
cfg.getArtifactId(), version, e);
} catch (RestClientException e) {
log.warn("Could not fetch SNAPSHOT metadata for {}, falling back to {}",
cfg.getArtifactId(), version, e);
}
return version;
}
private String baseUrl(RepoConfig cfg) {
String groupPath = cfg.getGroupId().replace('.', '/');
return jdvsConfig.getEffectiveMavenRepoUrl(cfg) + "/" + 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 static Optional<String> resolveVersionTag(Document doc) {
return Optional.ofNullable(firstText(doc.getElementsByTagName("release")))
.or(() -> Optional.ofNullable(firstText(doc.getElementsByTagName("latest"))));
}
private static String firstText(NodeList nodes) {
return nodes.getLength() > 0 ? nodes.item(0).getTextContent() : null;
}
private static Document parseXml(String xml)
throws ParserConfigurationException, SAXException, IOException {
return DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(new InputSource(new StringReader(xml)));
}
}
@@ -0,0 +1,47 @@
package codes.thischwa.jdvs.service;
import codes.thischwa.jdvs.jpa.GitRepositoryRepository;
import codes.thischwa.jdvs.model.GitRepository;
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 MavenJavadocService mavenJavadocService;
@Scheduled(cron = "${jdvs.cron}")
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> latestVersion = mavenJavadocService.fetchLatestVersion(repo.getName());
if (latestVersion.isPresent()) {
String version = latestVersion.get();
if (!version.equals(repo.getLastTag())) {
log.info("New version {} found for {}. Generating Javadoc...", version, repo.getName());
if (mavenJavadocService.generateJavadoc(repo.getName(), version)) {
repo.setLastTag(version);
repo.setUpdated(LocalDateTime.now());
repository.save(repo);
}
} else {
log.info("Repository {} is already up to date ({}).", repo.getName(), version);
}
}
}
}
@@ -0,0 +1,34 @@
package codes.thischwa.jdvs.web;
import codes.thischwa.jdvs.jpa.GitRepositoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.server.ResponseStatusException;
@Controller
@RequiredArgsConstructor
public class BadgeController {
private static final String BADGE_URL_PREFIX =
"https://img.shields.io/badge/javadoc%40JdVS-";
private static final String BADGE_URL_SUFFIX = "-green";
private final GitRepositoryRepository repository;
@GetMapping("/badge/{repositoryName}")
public String badge(@PathVariable String repositoryName) {
String tag =
repository
.findByName(repositoryName)
.map(r -> r.getLastTag() != null ? r.getLastTag() : "unknown")
.orElseThrow(
() ->
new ResponseStatusException(
HttpStatus.NOT_FOUND, "Repository not found: " + repositoryName));
return "redirect:" + BADGE_URL_PREFIX + tag + BADGE_URL_SUFFIX;
}
}
@@ -0,0 +1,16 @@
package codes.thischwa.jdvs.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Controller for favicon.ico, just for reducing 404 errors in the logs.
*/
@RestController
public class FaviconController {
@GetMapping("favicon.ico")
void returnNoFavicon() {
// see class comment
}
}
@@ -0,0 +1,19 @@
package codes.thischwa.jdvs.web;
import codes.thischwa.jdvs.jpa.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 UiController {
private final GitRepositoryRepository repository;
@GetMapping("/")
public String index(Model model) {
model.addAttribute("repos", repository.findAll());
return "index";
}
}
@@ -0,0 +1,24 @@
package codes.thischwa.jdvs.web;
import codes.thischwa.jdvs.model.config.JdvsConfig;
import java.nio.file.Path;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final JdvsConfig jdvsConfig;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String javadocPath = Path.of(jdvsConfig.getBaseDir(), "javadoc").toAbsolutePath().toUri().toString();
if (!javadocPath.endsWith("/")) {
javadocPath += "/";
}
registry.addResourceHandler("/**")
.addResourceLocations(javadocPath);
}
}
+27
View File
@@ -0,0 +1,27 @@
jdvs:
base-dir: ./javadoc-storage
cron: 0 0/30 * * * ?
clean-on-start: false
run-on-start: false
maven-central-url: https://repo1.maven.org/maven2
git:
javadoc-paths:
- target/reports/apidocs
- target/apidocs
- target/site/apidocs
## import of the individual configuration settings
spring:
config:
import: optional:file:./jdvs.yml
datasource:
url: jdbc:h2:file:./database/jdvsdb
driverClassName: org.h2.Driver
username: sa
password: ""
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: none
+11
View File
@@ -0,0 +1,11 @@
___ _ _ _ _____
|_ | | | | | / ___|
| | __| | | | \ `--.
| |/ _` | | | |`--. \
/\__/ / (_| \ \_/ /\__/ /
\____/ \__,_|\___/\____/
Version: ${application.version}
:: Spring Boot${spring-boot.formatted-version} ::
running on java ${java.version}
@@ -0,0 +1,27 @@
databaseChangeLog:
- changeSet:
id: 1
author: th-schwarz
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: last_tag
type: VARCHAR(255)
- column:
name: updated
type: TIMESTAMP
@@ -0,0 +1,3 @@
databaseChangeLog:
- include:
file: db/changelog/001-init-schema.yaml
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<appender name="current"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.apache.http" level="info"/>
<logger name="org.springframework.boot.autoconfigure.logging" level="info"/>
<logger name="org.springframework.context" level="info"/>
<logger name="org.eclipse.jgit" level="info"/>
<logger name="com.zaxxer.hikari" level="info"/>
<root level="debug">
<appender-ref ref="current"/>
</root>
</configuration>
+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>Project Javadoc Overview</h1>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Latest Version</th>
<th>Last Update</th>
<th>Javadoc link</th>
</tr>
</thead>
<tbody>
<tr th:each="repo : ${repos}">
<td th:text="${repo.name}">Project</td>
<td th:text="${repo.lastTag}">-</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})}">
<img th:src="@{/badge/{name}(name=${repo.name})}" alt="javadoc badge">
</a>
<span th:if="${repo.lastTag == null}" class="badge bg-secondary">Not yet generated</span>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>