From 44ccd727be9ac92de19cd75f1dc5d77eea2dfd8b Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Sun, 14 Sep 2025 04:14:19 +0000 Subject: [PATCH 01/18] Update shogo82148/actions-create-release digest to 7b89596 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0639fc65..61015663 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,7 +91,7 @@ jobs: - name: Create Release id: create-release - uses: shogo82148/actions-create-release@4661dc54f7b4b564074e9fbf73884d960de569a3 # v1 + uses: shogo82148/actions-create-release@7b89596097b26731bda0852f1504f813499079ee # v1 with: tag_name: v${{ steps.version.outputs.release }} release_name: v${{ steps.version.outputs.release }} From 088bcf340437d337ffc169567f6bdfaad3a6d2d6 Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 15 Sep 2025 14:54:51 +0200 Subject: [PATCH 02/18] Add PMD 7.16 Rules --- .config/pmd/java/ruleset.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.config/pmd/java/ruleset.xml b/.config/pmd/java/ruleset.xml index 267fa5e9..5d76b2b6 100644 --- a/.config/pmd/java/ruleset.xml +++ b/.config/pmd/java/ruleset.xml @@ -153,6 +153,8 @@ + + From dd4c7faeec7695b3b9d02cff6dbc674f9cdd582b Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 16 Sep 2025 10:26:07 +0200 Subject: [PATCH 03/18] Update mvnw --- .mvn/wrapper/maven-wrapper.properties | 18 +------ mvnw | 67 ++++++++++++++++++++++----- mvnw.cmd | 57 ++++++++++++++++++++--- 3 files changed, 108 insertions(+), 34 deletions(-) diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 6a6b8b2c..c0bcafe9 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,17 +1,3 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. +wrapperVersion=3.3.4 +distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/mvnw b/mvnw index 08303327..bd8896bf 100755 --- a/mvnw +++ b/mvnw @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.0 +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- @@ -97,14 +97,25 @@ die() { exit 1 } +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in - distributionUrl) distributionUrl="${value-}" ;; - distributionSha256Sum) distributionSha256Sum="${value-}" ;; + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac -done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) @@ -122,7 +133,7 @@ maven-mvnd-*bin.*) distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME @@ -131,7 +142,8 @@ esac distributionUrlName="${distributionUrl##*/}" distributionUrlNameMain="${distributionUrlName%.*}" distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" exec_maven() { unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : @@ -199,7 +211,7 @@ elif set_java_home; then public static void main( String[] args ) throws Exception { setDefault( new Downloader() ); - java.nio.file.Files.copy( new java.net.URL( args[0] ).openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); } } END @@ -218,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then @@ -243,8 +255,41 @@ if command -v unzip >/dev/null; then else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 136e686a..92450f93 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -19,7 +19,7 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.0 +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @@ -40,7 +40,7 @@ @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> @@ -73,13 +73,30 @@ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' -$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { @@ -131,7 +148,33 @@ if ($distributionSha256Sum) { # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { From 108486865e6442363bbcaff9b394ba5b36a2d63f Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 15 Sep 2025 15:03:03 +0200 Subject: [PATCH 04/18] PMD: Import and modify rules from `jPinpoint` See https://github.com/jborgers/PMD-jPinpoint-rules --- .config/pmd/java/ruleset.xml | 824 ++++++++++++++++++++++++++++++++++- 1 file changed, 818 insertions(+), 6 deletions(-) diff --git a/.config/pmd/java/ruleset.xml b/.config/pmd/java/ruleset.xml index 5d76b2b6..28bc272e 100644 --- a/.config/pmd/java/ruleset.xml +++ b/.config/pmd/java/ruleset.xml @@ -11,7 +11,6 @@ - @@ -199,6 +198,33 @@ + + + + Usually all cases where `StringBuilder` (or the outdated `StringBuffer`) is used are either due to confusing (legacy) logic or may be replaced by a simpler string concatenation. + + Solution: + * Do not use `StringBuffer` because it's thread-safe and usually this is not needed + * If `StringBuilder` is only used in a simple method (like `toString`) and is effectively inlined: Use a simpler string concatenation (`"a" + x + "b"`). This will be optimized by the Java compiler internally. + * In all other cases: + * Check what is happening and if it makes ANY sense! If for example a CSV file is built here consider using a proper library instead! + * Abstract the Strings into a DTO, join them together using a collection (or `StringJoiner`) or use Java's Streaming API instead + + 3 + + + + + + + + + - @@ -236,7 +262,7 @@ - @@ -257,7 +283,7 @@ - @@ -279,7 +305,7 @@ - @@ -303,7 +329,7 @@ - @@ -311,4 +337,790 @@ + + + + + + Do not use native HTML! Use Vaadin layouts and components to create required structure. + If you are 100% sure that you escaped the value properly and you have no better options you can suppress this. + + 2 + + + + + + + + + + + + + + + + java.text.NumberFormat: DecimalFormat and ChoiceFormat are thread-unsafe. + + Solution: Create a new local one when needed in a method. + + 1 + + + + + + + + + + + + + + + A regular expression is compiled implicitly on every invocation. + Problem: This can be (CPU) expensive, depending on the length of the regular expression. + + Solution: Compile the regex pattern only once and assign it to a private static final Pattern field. + java.util.Pattern objects are thread-safe, so they can be shared among threads. + + 2 + + + + 5 and +(matches(@Image, '[\.\$\|\(\)\[\]\{\}\^\?\*\+\\]+'))) +or +self::VariableAccess and @Name=ancestor::ClassBody[1]/FieldDeclaration/VariableDeclarator[StringLiteral[string-length(@Image) > 5 and +(matches(@Image, '[\.\$\|\(\)\[\]\{\}\^\?\*\+\\]+'))] or not(StringLiteral)]/VariableId/@Name] +]]> + + + + + + + + + + + + The default constructor of ByteArrayOutputStream creates a 32 bytes initial capacity and for StringWriter 16 chars. + Such a small buffer as capacity usually needs several expensive expansions. + + Solution: Explicitly declared the buffer size so that an expansion is not needed in most cases. + Typically much larger than 32, e.g. 4096. + + 2 + + + + + + + + + + + + + + + The time to find element is O(n); n = the number of enum values. + This identical processing is executed for every call. + Considered problematic when `n > 3`. + + Solution: Use a static field-to-enum-value Map. Access time is O(1), provided the hashCode is well-defined. + Implement a fromString method to provide the reverse conversion by using the map. + + 3 + + + + 3]//MethodDeclaration/Block + //MethodCall[pmd-java:matchesSig('java.util.stream.Stream#findFirst()') or pmd-java:matchesSig('java.util.stream.Stream#findAny()')] + [//MethodCall[pmd-java:matchesSig('java.util.stream.Stream#of(_)') or pmd-java:matchesSig('java.util.Arrays#stream(_)')] + [ArgumentList/MethodCall[pmd-java:matchesSig('_#values()')]]] +]]> + + + + + fromString(String name) { + return Stream.of(values()).filter(v -> v.toString().equals(name)).findAny(); // bad: iterates for every call, O(n) access time + } +} + +Usage: `Fruit f = Fruit.fromString("banana");` + +// GOOD +public enum Fruit { + APPLE("apple"), + ORANGE("orange"), + BANANA("banana"), + KIWI("kiwi"); + + private static final Map nameToValue = + Stream.of(values()).collect(toMap(Object::toString, v -> v)); + private final String name; + + Fruit(String name) { this.name = name; } + @Override public String toString() { return name; } + public static Optional fromString(String name) { + return Optional.ofNullable(nameToValue.get(name)); // good, get from Map, O(1) access time + } +} +]]> + + + + + + A regular expression is compiled on every invocation. + Problem: this can be expensive, depending on the length of the regular expression. + + Solution: Usually a pattern is a literal, not dynamic and can be compiled only once. Assign it to a private static field. + java.util.Pattern objects are thread-safe so they can be shared among threads. + + 2 + + + + + + + + + + + + + + + + Recreating a DateTimeFormatter is relatively expensive. + + Solution: Java 8+ java.time.DateTimeFormatter is thread-safe and can be shared among threads. + Create the formatter from a pattern only once, to initialize a static final field. + + 2 + + + + + + + + + + + + Creating a security provider is expensive because of loading of algorithms and other classes. + Additionally, it uses synchronized which leads to lock contention when used with multiple threads. + + Solution: This only needs to happen once in the JVM lifetime, because once loaded the provider is typically available from the Security class. + Create the security provider only once: Only in case when it's not yet available from the Security class. + + 2 + + + + + + + + + + + + + + + Reflection is relatively expensive. + + Solution: Avoid reflection. Use the non-reflective, explicit way like generation by IDE. + + 2 + + + + + + + + + + + + + + + java.util.SimpleDateFormat is thread-unsafe. + The usual solution is to create a new one when needed in a method. + Creating SimpleDateFormat is relatively expensive. + + Solution: Use java.time.DateTimeFormatter. These classes are immutable, thus thread-safe and can be made static. + + 2 + + + + + + + + + + + + + + + Creating Comparator instances repeatedly in methods like compareTo or sort calls is inefficient. + + Solution: Initialize the Comparator once as a static final field and reuse. + + 2 + + + + + + + + + { + @Override + public int compareTo(@NotNull Person o) { + return Comparator.comparing(Person::getFirstName) // Bad: Creates new Comparator instance on each invocation + .thenComparing(Person::getLastName) + .thenComparingInt(Person::getAge) + .compare(this, o); + } +} + +public class GoodPerson implements Comparable { + private static final Comparator COMPARE_FIRST_LAST_NAME_AGE = + Comparator.comparing(Person::getFirstName) + .thenComparing(Person::getLastName) + .thenComparingInt(Person::getAge); + + @Override + public int compareTo(@NotNull Person o) { + return COMPARE_FIRST_LAST_NAME_AGE.compare(this, o); // Good: Comparator initialized once as static final field + } +} +]]> + + + + + + Blocking calls, for instance remote calls, may exhaust the common pool for some time thereby blocking all other use of the common pool. + In addition, nested use of the common pool can lead to deadlock. Do not use the common pool for blocking calls. + The parallelStream() call uses the common pool. + + Solution: Use a dedicated thread pool with enough threads to get proper parallelism. + The number of threads in the common pool is equal to the number of CPUs and meant to utilize all of them. + It assumes CPU-intensive non-blocking processing of in-memory data. + + 2 + + + + + + + + + list = new ArrayList(); + final ForkJoinPool myFjPool = new ForkJoinPool(10); + final ExecutorService myExePool = Executors.newFixedThreadPool(10); + + void bad1() { + list.parallelStream().forEach(elem -> storeDataRemoteCall(elem)); // bad + } + + void good1() { + CompletableFuture[] futures = list.stream().map(elem -> CompletableFuture.supplyAsync(() -> storeDataRemoteCall(elem), myExePool)) + .toArray(CompletableFuture[]::new); + CompletableFuture.allOf(futures).get(10, TimeUnit.MILLISECONDS)); + } + + void good2() throws ExecutionException, InterruptedException { + myFjPool.submit(() -> + list.parallelStream().forEach(elem -> storeDataRemoteCall(elem)) + ).get(); + } + + String storeDataRemoteCall(String elem) { + // do remote call, blocking. We don't use the returned value. + RestTemplate tmpl; + return ""; + } +} +]]> + + + + + + Future.supplyAsync is typically used for remote calls. By default, it uses the common pool. + The number of threads in the common pool is equal to the number of CPU's, which is suitable for in-memory processing. + For I/O, however, this number is typically not suitable because most time is spent waiting for the response and not in CPU. + The common pool must not be used for blocking calls. + + Solution: A separate, properly sized, pool of threads (an Executor) should be used for the async calls. + + 2 + + + + + + + + +>[] futures = accounts.stream() + .map(account -> CompletableFuture.supplyAsync(() -> isAccountBlocked(account))) // bad + .toArray(CompletableFuture[]::new); + } + + void good() { + CompletableFuture>[] futures = accounts.stream() + .map(account -> CompletableFuture.supplyAsync(() -> isAccountBlocked(account), asyncPool)) // good + .toArray(CompletableFuture[]::new); + } +} +]]> + + + + + + `take()` stalls indefinitely in case of hanging threads and consumes a thread. + + Solution: use `poll()` with a timeout value and handle the timeout. + + 2 + + + + + + + + + void collectAllCollectionReplyFromThreads(CompletionService> completionService) { + try { + Future> futureLocal = completionService.take(); // bad + Future> futuresGood = completionService.poll(3, TimeUnit.SECONDS); // good + responseCollector.addAll(futuresGood.get(10, TimeUnit.SECONDS)); // good + } catch (InterruptedException | ExecutionException e) { + LOGGER.error("Error in Thread : {}", e); + } catch (TimeoutException e) { + LOGGER.error("Timeout in Thread : {}", e); + } +} +]]> + + + + + + Stalls indefinitely in case of stalled Callable(s) and consumes threads. + + Solution: Provide a timeout to the invokeAll/invokeAny method and handle the timeout. + + 2 + + + + + + + + +> executeTasksBad(Collection> tasks, ExecutorService executor) throws Exception { + return executor.invokeAll(tasks); // bad, no timeout + } + private List> executeTasksGood(Collection> tasks, ExecutorService executor) throws Exception { + return executor.invokeAll(tasks, OUR_TIMEOUT_IN_MILLIS, TimeUnit.MILLISECONDS); // good + } +} +]]> + + + + + + Stalls indefinitely in case of hanging threads and consumes a thread. + + Solution: Provide a timeout value and handle the timeout. + + 2 + + + + + + + + + complFuture) throws Exception { + return complFuture.get(); // bad +} + +public static String good(CompletableFuture complFuture) throws Exception { + return complFuture.get(10, TimeUnit.SECONDS); // good +} +]]> + + + + + + + Apache HttpClient with its connection pool and timeouts should be setup once and then used for many requests. + It is quite expensive to create and can only provide the benefits of pooling when reused in all requests for that connection. + + Solution: Create/build HttpClient with proper connection pooling and timeouts once, and then use it for requests. + + 3 + + + + + + + + + connectBad(Object req) { + HttpEntity requestEntity = new HttpEntity<>(req); + + HttpClient httpClient = HttpClientBuilder.create().setMaxConnPerRoute(10).build(); // bad + return remoteCall(httpClient, requestEntity); + } +} +]]> + + + + + + Problem: Gson creation is relatively expensive. A JMH benchmark shows a 24x improvement reusing one instance. + + Solution: Since Gson objects are thread-safe after creation, they can be shared between threads. + So reuse created instances from a static field. + Pay attention to use thread-safe (custom) adapters and serializers. + + 3 + + + + + + + + + + + From deddd914be12567ecf33146f9a2f7acb0c10fa0a Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 16 Sep 2025 14:36:18 +0200 Subject: [PATCH 05/18] PMD: Exclude unused rule --- .config/pmd/java/ruleset.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.config/pmd/java/ruleset.xml b/.config/pmd/java/ruleset.xml index 28bc272e..517b9243 100644 --- a/.config/pmd/java/ruleset.xml +++ b/.config/pmd/java/ruleset.xml @@ -185,6 +185,9 @@ + + + From 53b1e5dfd3fc70d91302cfb4fef2dd48b1b65ccb Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 16 Sep 2025 14:36:48 +0200 Subject: [PATCH 06/18] PMD: Reword and also apply to runAsync --- .config/pmd/java/ruleset.xml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.config/pmd/java/ruleset.xml b/.config/pmd/java/ruleset.xml index 517b9243..cda412b8 100644 --- a/.config/pmd/java/ruleset.xml +++ b/.config/pmd/java/ruleset.xml @@ -841,6 +841,8 @@ public class GoodPerson implements Comparable { Solution: Use a dedicated thread pool with enough threads to get proper parallelism. The number of threads in the common pool is equal to the number of CPUs and meant to utilize all of them. It assumes CPU-intensive non-blocking processing of in-memory data. + + See also: [_Be Aware of ForkJoinPool#commonPool()_](https://dzone.com/articles/be-aware-of-forkjoinpoolcommonpool) 2 @@ -897,22 +899,25 @@ public class Foo { - Future.supplyAsync is typically used for remote calls. By default, it uses the common pool. + CompletableFuture.supplyAsync/runAsync is typically used for remote calls. + By default it uses the common pool. The number of threads in the common pool is equal to the number of CPU's, which is suitable for in-memory processing. For I/O, however, this number is typically not suitable because most time is spent waiting for the response and not in CPU. The common pool must not be used for blocking calls. - Solution: A separate, properly sized, pool of threads (an Executor) should be used for the async calls. + Solution: A separate, properly sized pool of threads (an Executor) should be used for the async calls. + + See also: [_Be Aware of ForkJoinPool#commonPool()_](https://dzone.com/articles/be-aware-of-forkjoinpoolcommonpool) 2 From e6358ed1fa1e6c743e9a7b055a55862beacb8677 Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 16 Sep 2025 14:36:56 +0200 Subject: [PATCH 07/18] PMD: Fix error --- .config/pmd/java/ruleset.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/pmd/java/ruleset.xml b/.config/pmd/java/ruleset.xml index cda412b8..bb8177f4 100644 --- a/.config/pmd/java/ruleset.xml +++ b/.config/pmd/java/ruleset.xml @@ -1066,7 +1066,7 @@ public static String good(CompletableFuture complFuture) throws Exceptio //MethodDeclaration//MethodCall[ pmd-java:matchesSig('org.apache.hc.client5.http.impl.classic.HttpClientBuilder#create()') or pmd-java:matchesSig('org.apache.hc.client5.http.impl.classic.HttpClients#custom()') - or pmd-java:matchesSig('org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder#build()' + or pmd-java:matchesSig('org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder#build()') ] [ancestor::MethodDeclaration//ClassType[pmd-java:typeIs('org.springframework.http.HttpEntity') or pmd-java:typeIs('org.springframework.http.ResponseEntity')] From 042b7a495804644325788e8d174aa2d02622da85 Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 16 Sep 2025 14:37:10 +0200 Subject: [PATCH 08/18] PMD: Remove rule as it yields too many FP --- .config/pmd/java/ruleset.xml | 63 ------------------------------------ 1 file changed, 63 deletions(-) diff --git a/.config/pmd/java/ruleset.xml b/.config/pmd/java/ruleset.xml index bb8177f4..a5b76341 100644 --- a/.config/pmd/java/ruleset.xml +++ b/.config/pmd/java/ruleset.xml @@ -766,69 +766,6 @@ public class Foo { - - - Creating Comparator instances repeatedly in methods like compareTo or sort calls is inefficient. - - Solution: Initialize the Comparator once as a static final field and reuse. - - 2 - - - - - - - - - { - @Override - public int compareTo(@NotNull Person o) { - return Comparator.comparing(Person::getFirstName) // Bad: Creates new Comparator instance on each invocation - .thenComparing(Person::getLastName) - .thenComparingInt(Person::getAge) - .compare(this, o); - } -} - -public class GoodPerson implements Comparable { - private static final Comparator COMPARE_FIRST_LAST_NAME_AGE = - Comparator.comparing(Person::getFirstName) - .thenComparing(Person::getLastName) - .thenComparingInt(Person::getAge); - - @Override - public int compareTo(@NotNull Person o) { - return COMPARE_FIRST_LAST_NAME_AGE.compare(this, o); // Good: Comparator initialized once as static final field - } -} -]]> - - - Date: Wed, 17 Sep 2025 10:32:54 +0200 Subject: [PATCH 09/18] PMF: Cleanup and format --- .config/pmd/java/ruleset.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.config/pmd/java/ruleset.xml b/.config/pmd/java/ruleset.xml index a5b76341..748c826e 100644 --- a/.config/pmd/java/ruleset.xml +++ b/.config/pmd/java/ruleset.xml @@ -448,7 +448,7 @@ String good_replaceInnerLineBreakBySpace() { - + + 3 @@ -565,7 +565,7 @@ public enum Fruit { Solution: Usually a pattern is a literal, not dynamic and can be compiled only once. Assign it to a private static field. java.util.Pattern objects are thread-safe so they can be shared among threads. - + 2 From 7bcc1bd25b867131f2190e8e86d41f90aa8ada2a Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Sun, 21 Sep 2025 04:13:51 +0000 Subject: [PATCH 10/18] Update dependency org.apache.maven.plugins:maven-javadoc-plugin to v3.12.0 --- template-placeholder/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template-placeholder/pom.xml b/template-placeholder/pom.xml index 7380883f..89d4aa76 100644 --- a/template-placeholder/pom.xml +++ b/template-placeholder/pom.xml @@ -108,7 +108,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.3 + 3.12.0 attach-javadocs From e0867d36ce22ad168d47ea3c6dd59dfae4db74b4 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Mon, 22 Sep 2025 04:19:37 +0000 Subject: [PATCH 11/18] Update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.14.1 --- template-placeholder-demo/pom.xml | 2 +- template-placeholder/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/template-placeholder-demo/pom.xml b/template-placeholder-demo/pom.xml index 37022223..b6ff1fd9 100644 --- a/template-placeholder-demo/pom.xml +++ b/template-placeholder-demo/pom.xml @@ -44,7 +44,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + 3.14.1 ${maven.compiler.release} diff --git a/template-placeholder/pom.xml b/template-placeholder/pom.xml index 7380883f..f67b5057 100644 --- a/template-placeholder/pom.xml +++ b/template-placeholder/pom.xml @@ -97,7 +97,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + 3.14.1 ${maven.compiler.release} From 184b908f235cf93fade385838d806417c1ee6564 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Mon, 22 Sep 2025 04:19:40 +0000 Subject: [PATCH 12/18] Update dependency org.codehaus.mojo:flatten-maven-plugin to v1.7.3 --- template-placeholder/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template-placeholder/pom.xml b/template-placeholder/pom.xml index 7380883f..e356a34d 100644 --- a/template-placeholder/pom.xml +++ b/template-placeholder/pom.xml @@ -147,7 +147,7 @@ org.codehaus.mojo flatten-maven-plugin - 1.7.2 + 1.7.3 ossrh From 0e8aefd14b6e396990ff21fcdd9d0bc0fe7e25b3 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Wed, 24 Sep 2025 04:13:23 +0000 Subject: [PATCH 13/18] Update dependency org.sonatype.central:central-publishing-maven-plugin to v0.9.0 --- template-placeholder/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template-placeholder/pom.xml b/template-placeholder/pom.xml index 27a24c8d..4c4214ca 100644 --- a/template-placeholder/pom.xml +++ b/template-placeholder/pom.xml @@ -193,7 +193,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.8.0 + 0.9.0 true sonatype-central-portal From f08c6622dbeb98cc1db48a845335020e361b2814 Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Mon, 29 Sep 2025 04:14:48 +0000 Subject: [PATCH 14/18] Update dependency com.puppycrawl.tools:checkstyle to v11.1.0 --- pom.xml | 2 +- template-placeholder/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f824869d..61578c0b 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,7 @@ com.puppycrawl.tools checkstyle - 11.0.1 + 11.1.0 diff --git a/template-placeholder/pom.xml b/template-placeholder/pom.xml index 4c4214ca..3d8cc2cf 100644 --- a/template-placeholder/pom.xml +++ b/template-placeholder/pom.xml @@ -215,7 +215,7 @@ com.puppycrawl.tools checkstyle - 11.0.1 + 11.1.0 From 6278740ec8418dbe1933b70fbab9c3cf7c3f59e7 Mon Sep 17 00:00:00 2001 From: AB Date: Mon, 29 Sep 2025 13:42:00 +0200 Subject: [PATCH 15/18] Fix broken-links not finding issues on GH CLI v2.79+ Fix https://github.com/xdev-software/base-template/issues/8 --- .github/workflows/broken-links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/broken-links.yml b/.github/workflows/broken-links.yml index 9493fb8a..436ce503 100644 --- a/.github/workflows/broken-links.yml +++ b/.github/workflows/broken-links.yml @@ -25,7 +25,7 @@ jobs: - name: Find already existing issue id: find-issue run: | - echo "number=$(gh issue list -l 'bug' -l 'automated' -L 1 -S 'in:title \"Link Checker Report\"' -s 'open' --json 'number' --jq '.[].number')" >> $GITHUB_OUTPUT + echo "number=$(gh issue list -l 'bug' -l 'automated' -L 1 -S 'in:title "Link Checker Report"' -s 'open' --json 'number' --jq '.[].number')" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ github.token }} From 39c4703001cf7da323fb6b809f1ab733284a536d Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 30 Sep 2025 09:52:34 +0200 Subject: [PATCH 16/18] Test compile with Java 25 & Update docs --- .github/workflows/check-build.yml | 2 +- CONTRIBUTING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 01f08402..e9757649 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -28,7 +28,7 @@ jobs: timeout-minutes: 30 strategy: matrix: - java: [17, 21] + java: [17, 21, 25] distribution: [temurin] steps: - uses: actions/checkout@v5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be2a1863..2c7b0250 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ We also encourage you to read the [contribution instructions by GitHub](https:// ### Software Requirements You should have the following things installed: * Git -* Java 21 - should be as unmodified as possible (Recommended: [Eclipse Adoptium](https://adoptium.net/temurin/releases/)) +* Java 25 - should be as unmodified as possible (Recommended: [Eclipse Adoptium](https://adoptium.net/temurin/releases/)) * Maven (Note that the [Maven Wrapper](https://maven.apache.org/wrapper/) is shipped with the repo) ### Recommended setup From f71bf683b682ca9d985ee02a13b157676e21cd9f Mon Sep 17 00:00:00 2001 From: XDEV Renovate Bot Date: Thu, 2 Oct 2025 04:10:53 +0000 Subject: [PATCH 17/18] Update peter-evans/create-issue-from-file action to v6 --- .github/workflows/broken-links.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/broken-links.yml b/.github/workflows/broken-links.yml index 436ce503..a99ae52c 100644 --- a/.github/workflows/broken-links.yml +++ b/.github/workflows/broken-links.yml @@ -37,7 +37,7 @@ jobs: - name: Create Issue From File if: steps.lychee.outputs.exit_code != 0 - uses: peter-evans/create-issue-from-file@e8ef132d6df98ed982188e460ebb3b5d4ef3a9cd # v5 + uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6 with: issue-number: ${{ steps.find-issue.outputs.number }} title: Link Checker Report From 046de3916a42b79a547d1a8281de2bc875e9091c Mon Sep 17 00:00:00 2001 From: AB Date: Thu, 2 Oct 2025 10:47:35 +0200 Subject: [PATCH 18/18] Update PMD scheme location --- .config/pmd/java/ruleset.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/pmd/java/ruleset.xml b/.config/pmd/java/ruleset.xml index 748c826e..6bf58b33 100644 --- a/.config/pmd/java/ruleset.xml +++ b/.config/pmd/java/ruleset.xml @@ -2,7 +2,7 @@ + xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.github.io/ruleset_2_0_0.xsd"> This ruleset checks the code for discouraged programming constructs.