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
+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>