diff --git a/.config/checkstyle/checkstyle.xml b/.config/checkstyle/checkstyle.xml
index 43b5290..cd40139 100644
--- a/.config/checkstyle/checkstyle.xml
+++ b/.config/checkstyle/checkstyle.xml
@@ -65,6 +65,7 @@
+
@@ -74,11 +75,20 @@
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
@@ -91,7 +101,7 @@
-
+
@@ -123,7 +133,8 @@
-
+
+
@@ -134,6 +145,9 @@
+
+
+
diff --git a/.config/pmd/java/ruleset.xml b/.config/pmd/java/ruleset.xml
index 267fa5e..4c72bf2 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.
@@ -10,11 +10,15 @@
+
+
+
+
-
+
@@ -26,6 +30,7 @@
+
@@ -41,8 +46,10 @@
+
+
@@ -134,6 +141,7 @@
+
@@ -142,22 +150,26 @@
-
+
+
+
+
-
+
+
@@ -184,6 +196,9 @@
+
+
+
@@ -197,22 +212,79 @@
+
+
+
+`Optional#get` can be interpreted as a getter by developers, however this is not the case as it throws an exception when empty.
+
+It should be replaced by
+* doing a mapping directly using `.map` or `.ifPresent`
+* using the preferred `.orElseThrow`, `.orElse` or `.or` methods
+
+Java Developer Brian Goetz also writes regarding this topic:
+
+> Java 8 was a huge improvement to the platform, but one of the few mistakes we made was the naming of `Optional.get()`, because the name just invites people to call it without calling `isPresent()`, undermining the whole point of using `Optional` in the first place.
+>
+> During the Java 9 time frame, we proposed to deprecate `Optional.get()`, but the public response to that was ... let's say cold. As a smaller step, we introduced `orElseThrow()` in 10 (see [JDK-8140281](https://bugs.openjdk.java.net/browse/JDK-8140281)) as a more transparently named synonym for the current pernicious behavior of `get()`. IDEs warn on unconditional use of `get()`, but not on `orElseThrow()`, which is a step forward in teaching people to code better. The question is, in a sense, a "glass half empty" view of the current situation; `get()` is still problematic.
+
+ 3
+
+
+
+
+
+
+
+
+
+
+
+Usually all cases where `StringBuilder` (or the outdated `StringBuffer`) is used are either due to confusing (legacy) logic or in situations where it may be easily 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](https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.18.1).
+* 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
+
+
+
+
+
+
+
+
+
- Calling setters of java.lang.System usually indicates bad design and likely causes unexpected behavior.
- For example, it may break when multiple Threads are setting the value.
- It may also overwrite user defined options or properties.
+Calling setters of `java.lang.System` usually indicates bad design and likely causes unexpected behavior.
+For example, it may break when multiple Threads are working with the same value.
+It may also overwrite user defined options or properties.
- Try to pass the value only to the place where it's really needed and use it there accordingly.
+Try to pass the value only to the place where it's really needed and use it there accordingly.
3
-
@@ -225,16 +297,16 @@
message="Avoid @PostConstruct"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
- Using a `@PostConstruct` method is usually only done when field injection is used and initialization needs to be performed after that.
+Using a `@PostConstruct` method is usually only done when field injection is used and initialization needs to be performed after that.
- It's better to do this directly in the constructor with constructor injection, so that all logic will be encapsulated there.
- This also makes using the bean in environments where JavaEE is not present - for example in tests - a lot easier, as forgetting to call the `@PostConstruct` method is no longer possible.
+It's better to do this directly in the constructor with constructor injection, so that all logic will be encapsulated there.
+This also makes using the bean in environments where JavaEE is not present - for example in tests - a lot easier, as forgetting to call the `@PostConstruct` method is no longer possible.
3
-
@@ -247,15 +319,15 @@
message="Avoid @PreDestroy"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
- `@PreDestroy` should be replaced by implementing `AutoCloseable` and overwriting the `close` method instead.
+`@PreDestroy` should be replaced by implementing `AutoCloseable` and overwriting the `close` method instead.
- This also makes using the bean in environments where JavaEE is not present - for example in tests - a lot easier, as forgetting to call the `@PreDestroy` method is no much more difficult.
+This also makes using the bean in environments where JavaEE is not present - for example in tests - a lot easier, as forgetting to call the `@PreDestroy` method is no much more difficult.
3
-
@@ -268,16 +340,16 @@
message="Avoid unmanaged threads"
class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
- Trying to manually manage threads usually gets quickly out of control and may result in various problems like uncontrollable spawning of threads.
- Threads can also not be cancelled properly.
+Trying to manually manage threads usually gets quickly out of control and may result in various problems like uncontrollable spawning of threads.
+Threads can also not be cancelled properly.
- Use managed Thread services like `ExecutorService` and `CompletableFuture` instead.
+Use managed Thread services like `ExecutorService` and `CompletableFuture` instead.
3
-
@@ -285,23 +357,46 @@
+
+
+ZipEntry name should be sanitized.
+Unsanitized names may contain '..' which can result in path traversal ("ZipSlip").
+
+You can suppress this warning when you properly sanitized the name.
+
+ 4
+
+
+
+
+
+
+
+
+
- Nearly every known usage of (Java) Object Deserialization has resulted in [a security vulnerability](https://cloud.google.com/blog/topics/threat-intelligence/hunting-deserialization-exploits?hl=en).
- Vulnerabilities are so common that there are [dedicated projects for exploit payload generation](https://github.com/frohoff/ysoserial).
+Nearly every known usage of (Java) Object Deserialization has resulted in [a security vulnerability](https://cloud.google.com/blog/topics/threat-intelligence/hunting-deserialization-exploits?hl=en).
+Vulnerabilities are so common that there are [dedicated projects for exploit payload generation](https://github.com/frohoff/ysoserial).
- Java Object Serialization may also fail to deserialize when the underlying classes are changed.
+Java Object Serialization may also fail to deserialize properly when the underlying classes are changed.
+This can result in unexpected crashes when outdated data is deserialized.
- Use proven data interchange formats like JSON instead.
+Use proven data interchange formats like JSON instead.
2
-
@@ -309,4 +404,758 @@
+
+
+
+
+
+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
+
+
+
+
+
+
+
+
+
+
+
+
+`List` allows duplicates while a `Set` does not.
+A `Set` also prevents duplicates when the ORM reads multiple identical rows from the database (e.g. when using JOIN).
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+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.
+
+See also: [_Be Aware of ForkJoinPool#commonPool()_](https://dzone.com/articles/be-aware-of-forkjoinpoolcommonpool)
+
+ 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 "";
+ }
+}
+]]>
+
+
+
+
+
+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.
+
+See also: [_Be Aware of ForkJoinPool#commonPool()_](https://dzone.com/articles/be-aware-of-forkjoinpoolcommonpool)
+
+ 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
+
+
+
+
+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
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.config/topo/upstream.yml b/.config/topo/upstream.yml
new file mode 100644
index 0000000..0cc67cd
--- /dev/null
+++ b/.config/topo/upstream.yml
@@ -0,0 +1,2 @@
+- url: https://github.com/xdev-software/standard-maven-template.git
+ branch: master
diff --git a/.github/.lycheeignore b/.github/.lycheeignore
index dc88a07..217b0ae 100644
--- a/.github/.lycheeignore
+++ b/.github/.lycheeignore
@@ -1,3 +1,4 @@
# Ignorefile for broken link check
localhost
mvnrepository.com
+stackoverflow.com
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index a167664..b3c5aa6 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -61,4 +61,4 @@ body:
attributes:
label: Additional information
description: |
- Any other relevant information you'd like to include
+ Any other relevant information you'd like to include, e.g. screenshots or video recordings of the bug
diff --git a/.github/workflows/broken-links.yml b/.github/workflows/broken-links.yml
index 5921f76..8aeed09 100644
--- a/.github/workflows/broken-links.yml
+++ b/.github/workflows/broken-links.yml
@@ -3,7 +3,7 @@ name: Broken links
on:
workflow_dispatch:
schedule:
- - cron: "23 23 * * 0"
+ - cron: "23 5 * * 0"
permissions:
issues: write
@@ -13,20 +13,21 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- run: mv .github/.lycheeignore .lycheeignore
- name: Link Checker
id: lychee
- uses: lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2 # v2
+ uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2
with:
+ args: "--verbose --no-progress './**/*.md'"
fail: false # Don't fail on broken links, create an issue instead
- 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 }}
@@ -38,7 +39,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
diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml
index 61ca4db..7e97850 100644
--- a/.github/workflows/check-build.yml
+++ b/.github/workflows/check-build.yml
@@ -28,24 +28,25 @@ jobs:
timeout-minutes: 30
strategy:
matrix:
- java: [21]
+ java: [21, 25]
distribution: [temurin]
steps:
- - uses: actions/checkout@v5
-
- - name: Set up JDK
- uses: actions/setup-java@v5
- with:
- distribution: ${{ matrix.distribution }}
- java-version: ${{ matrix.java }}
-
- - name: Cache Maven
- uses: actions/cache@v4
- with:
- path: ~/.m2/repository
- key: ${{ runner.os }}-mvn-build-${{ hashFiles('**/pom.xml') }}
- restore-keys: |
- ${{ runner.os }}-mvn-build-
+ - uses: actions/checkout@v7
+
+ - parallel:
+ - name: Set up JDK
+ uses: actions/setup-java@v6
+ with:
+ distribution: ${{ matrix.distribution }}
+ java-version: ${{ matrix.java }}
+
+ - name: Cache Maven
+ uses: actions/cache@v6
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-mvn-build-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-mvn-build-
- name: Build with Maven
run: ./mvnw -B clean package
@@ -69,7 +70,7 @@ jobs:
fi
- name: Upload demo files
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: demo-files-java-${{ matrix.java }}
path: ${{ env.DEMO_MAVEN_MODULE }}/target/${{ env.DEMO_MAVEN_MODULE }}.jar
@@ -84,29 +85,30 @@ jobs:
java: [21]
distribution: [temurin]
steps:
- - uses: actions/checkout@v5
-
- - name: Set up JDK
- uses: actions/setup-java@v5
- with:
- distribution: ${{ matrix.distribution }}
- java-version: ${{ matrix.java }}
-
- - name: Cache Maven
- uses: actions/cache@v4
- with:
- path: ~/.m2/repository
- key: ${{ runner.os }}-mvn-checkstyle-${{ hashFiles('**/pom.xml') }}
- restore-keys: |
- ${{ runner.os }}-mvn-checkstyle-
-
- - name: CheckStyle Cache
- uses: actions/cache@v4
- with:
- path: '**/target/checkstyle-cachefile'
- key: ${{ runner.os }}-checkstyle-${{ hashFiles('**/pom.xml') }}
- restore-keys: |
- ${{ runner.os }}-checkstyle-
+ - uses: actions/checkout@v7
+
+ - parallel:
+ - name: Set up JDK
+ uses: actions/setup-java@v6
+ with:
+ distribution: ${{ matrix.distribution }}
+ java-version: ${{ matrix.java }}
+
+ - name: Cache Maven
+ uses: actions/cache@v6
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-mvn-checkstyle-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-mvn-checkstyle-
+
+ - name: CheckStyle Cache
+ uses: actions/cache@v6
+ with:
+ path: '**/target/checkstyle-cachefile'
+ key: ${{ runner.os }}-checkstyle-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-checkstyle-
- name: Run Checkstyle
run: ./mvnw -B checkstyle:check -P checkstyle -T2C
@@ -120,29 +122,30 @@ jobs:
java: [21]
distribution: [temurin]
steps:
- - uses: actions/checkout@v5
-
- - name: Set up JDK
- uses: actions/setup-java@v5
- with:
- distribution: ${{ matrix.distribution }}
- java-version: ${{ matrix.java }}
-
- - name: Cache Maven
- uses: actions/cache@v4
- with:
- path: ~/.m2/repository
- key: ${{ runner.os }}-mvn-pmd-${{ hashFiles('**/pom.xml') }}
- restore-keys: |
- ${{ runner.os }}-mvn-pmd-
-
- - name: PMD Cache
- uses: actions/cache@v4
- with:
- path: '**/target/pmd/pmd.cache'
- key: ${{ runner.os }}-pmd-${{ hashFiles('**/pom.xml') }}
- restore-keys: |
- ${{ runner.os }}-pmd-
+ - uses: actions/checkout@v7
+
+ - parallel:
+ - name: Set up JDK
+ uses: actions/setup-java@v6
+ with:
+ distribution: ${{ matrix.distribution }}
+ java-version: ${{ matrix.java }}
+
+ - name: Cache Maven
+ uses: actions/cache@v6
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-mvn-pmd-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-mvn-pmd-
+
+ - name: PMD Cache
+ uses: actions/cache@v6
+ with:
+ path: '**/target/pmd/pmd.cache'
+ key: ${{ runner.os }}-pmd-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-pmd-
- name: Run PMD
run: ./mvnw -B test pmd:aggregate-pmd-no-fork pmd:check -P pmd -DskipTests -T2C
@@ -151,8 +154,8 @@ jobs:
run: ./mvnw -B pmd:aggregate-cpd pmd:cpd-check -P pmd -DskipTests -T2C
- name: Upload report
- if: always()
- uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@v7
with:
name: pmd-report
if-no-files-found: ignore
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 652ee3d..dc504ce 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -18,22 +18,23 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- - uses: actions/checkout@v5
-
- - name: Set up JDK
- uses: actions/setup-java@v5
- with:
- java-version: '21'
- distribution: 'temurin'
-
- # Try to reuse existing cache from check-build
- - name: Try restore Maven Cache
- uses: actions/cache/restore@v4
- with:
- path: ~/.m2/repository
- key: ${{ runner.os }}-mvn-build-${{ hashFiles('**/pom.xml') }}
- restore-keys: |
- ${{ runner.os }}-mvn-build-
+ - uses: actions/checkout@v7
+
+ - parallel:
+ - name: Set up JDK
+ uses: actions/setup-java@v6
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+
+ # Try to reuse existing cache from check-build
+ - name: Try restore Maven Cache
+ uses: actions/cache/restore@v6
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-mvn-build-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-mvn-build-
- name: Build with Maven
run: ./mvnw -B clean package -T2C
@@ -63,7 +64,7 @@ jobs:
outputs:
upload_url: ${{ steps.create-release.outputs.upload_url }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- name: Configure Git
run: |
@@ -91,7 +92,7 @@ jobs:
- name: Create Release
id: create-release
- uses: shogo82148/actions-create-release@7b89596097b26731bda0852f1504f813499079ee # v1
+ uses: shogo82148/actions-create-release@8674c0ecee617c8dfb006526bcc22a362c70b88a # v1
with:
tag_name: v${{ steps.version.outputs.release }}
release_name: v${{ steps.version.outputs.release }}
@@ -115,7 +116,7 @@ jobs:
needs: [prepare-release]
timeout-minutes: 60
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- name: Init Git and pull
run: |
@@ -124,7 +125,7 @@ jobs:
git pull
- name: Set up JDK
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with: # running setup-java overwrites the settings.xml
distribution: 'temurin'
java-version: '21'
@@ -141,7 +142,7 @@ jobs:
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
- name: Set up JDK
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with: # running setup-java again overwrites the settings.xml
distribution: 'temurin'
java-version: '21'
@@ -163,7 +164,7 @@ jobs:
needs: [prepare-release]
timeout-minutes: 15
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- name: Init Git and pull
run: |
@@ -172,14 +173,14 @@ jobs:
git pull
- name: Setup - Java
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with:
java-version: '21'
distribution: 'temurin'
# Try to reuse existing cache from check-build
- name: Try restore Maven Cache
- uses: actions/cache/restore@v4
+ uses: actions/cache/restore@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-mvn-build-${{ hashFiles('**/pom.xml') }}
@@ -191,7 +192,7 @@ jobs:
working-directory: ${{ env.PRIMARY_MAVEN_MODULE }}
- name: Deploy to Github pages
- uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4
+ uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./${{ env.PRIMARY_MAVEN_MODULE }}/target/site
@@ -202,7 +203,7 @@ jobs:
needs: [publish-maven]
timeout-minutes: 10
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- name: Init Git and pull
run: |
diff --git a/.github/workflows/report-gha-workflow-security-problems.yml b/.github/workflows/report-gha-workflow-security-problems.yml
new file mode 100644
index 0000000..7847028
--- /dev/null
+++ b/.github/workflows/report-gha-workflow-security-problems.yml
@@ -0,0 +1,61 @@
+name: Report workflow security problems
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [ develop ]
+ paths:
+ - '.github/workflows/**'
+
+permissions:
+ issues: write
+
+jobs:
+ prt:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ # Only run this in our repos (Prevent notification spam by forks)
+ if: ${{ github.repository_owner == 'xdev-software' }}
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Check
+ id: check
+ run: |
+ grep -l 'pull_request_target:' --exclude report-gha-workflow-security-problems.yml *.yml > reported.txt && exit 1 || exit 0
+ working-directory: .github/workflows
+
+ - name: Find already existing issue
+ id: find-issue
+ if: ${{ !cancelled() }}
+ run: |
+ echo "number=$(gh issue list -l 'bug' -l 'automated' -L 1 -S 'in:title "Incorrectly configure GHA workflow (prt)"' -s 'open' --json 'number' --jq '.[].number')" >> $GITHUB_OUTPUT
+ env:
+ GH_TOKEN: ${{ github.token }}
+
+ - name: Close issue if everything is fine
+ if: ${{ success() && steps.find-issue.outputs.number != '' }}
+ run: gh issue close -r 'not planned' ${{ steps.find-issue.outputs.number }}
+ env:
+ GH_TOKEN: ${{ github.token }}
+
+ - name: Create report
+ if: ${{ failure() && steps.check.conclusion == 'failure' }}
+ run: |
+ echo 'Detected usage of `pull_request_target`. This event is dangerous and MUST NOT BE USED AT ALL COST!' > reported.md
+ echo '' >> reported.md
+ echo '/cc @xdev-software/gha-workflow-security' >> reported.md
+ echo '' >> reported.md
+ echo '```' >> reported.md
+ cat .github/workflows/reported.txt >> reported.md
+ echo '```' >> reported.md
+ cat reported.md
+
+ - name: Create Issue From File
+ if: ${{ failure() && steps.check.conclusion == 'failure' }}
+ uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6
+ with:
+ issue-number: ${{ steps.find-issue.outputs.number }}
+ title: 'Incorrectly configure GHA workflow (prt)'
+ content-filepath: ./reported.md
+ labels: bug, automated
diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml
index f6c50a1..dc68d05 100644
--- a/.github/workflows/sync-labels.yml
+++ b/.github/workflows/sync-labels.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
with:
sparse-checkout: .github/labels.yml
diff --git a/.github/workflows/test-deploy.yml b/.github/workflows/test-deploy.yml
index eed206f..3f23c1e 100644
--- a/.github/workflows/test-deploy.yml
+++ b/.github/workflows/test-deploy.yml
@@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- name: Set up JDK
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with: # running setup-java overwrites the settings.xml
distribution: 'temurin'
java-version: '21'
@@ -31,7 +31,7 @@ jobs:
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
- name: Set up JDK
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v6
with: # running setup-java again overwrites the settings.xml
distribution: 'temurin'
java-version: '21'
diff --git a/.github/workflows/update-from-template.yml b/.github/workflows/update-from-template.yml
deleted file mode 100644
index 1088171..0000000
--- a/.github/workflows/update-from-template.yml
+++ /dev/null
@@ -1,320 +0,0 @@
-name: Update from Template
-
-# This workflow keeps the repo up to date with changes from the template repo (REMOTE_URL)
-# It duplicates the REMOTE_BRANCH (into UPDATE_BRANCH) and tries to merge it into
-# this repos default branch (which is checked out here)
-# Note that this requires a PAT (Personal Access Token) - at best from a servicing account
-# PAT permissions: read:discussion, read:org, repo, workflow
-# Also note that you should have at least once merged the template repo into the current repo manually
-# otherwise a "refusing to merge unrelated histories" error might occur.
-
-on:
- schedule:
- - cron: '55 2 * * 1'
- workflow_dispatch:
- inputs:
- no_automatic_merge:
- type: boolean
- description: 'No automatic merge'
- default: false
-
-env:
- UPDATE_BRANCH: update-from-template
- UPDATE_BRANCH_MERGED: update-from-template-merged
- REMOTE_URL: https://github.com/xdev-software/standard-maven-template.git
- REMOTE_BRANCH: master
-
-permissions:
- contents: write
- pull-requests: write
-
-jobs:
- update:
- runs-on: ubuntu-latest
- timeout-minutes: 60
- outputs:
- update_branch_merged_commit: ${{ steps.manage-branches.outputs.update_branch_merged_commit }}
- create_update_branch_merged_pr: ${{ steps.manage-branches.outputs.create_update_branch_merged_pr }}
- steps:
- - uses: actions/checkout@v5
- with:
- # Required because otherwise there are always changes detected when executing diff/rev-list
- fetch-depth: 0
- # If no PAT is used the following error occurs on a push:
- # refusing to allow a GitHub App to create or update workflow `.github/workflows/xxx.yml` without `workflows` permission
- token: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }}
-
- - name: Init Git
- run: |
- git config --global user.email "111048771+xdev-gh-bot@users.noreply.github.com"
- git config --global user.name "XDEV Bot"
-
- - name: Manage branches
- id: manage-branches
- run: |
- echo "Adding remote template-repo"
- git remote add template ${{ env.REMOTE_URL }}
-
- echo "Fetching remote template repo"
- git fetch template
-
- echo "Deleting local branches that will contain the updates - if present"
- git branch -D ${{ env.UPDATE_BRANCH }} || true
- git branch -D ${{ env.UPDATE_BRANCH_MERGED }} || true
-
- echo "Checking if the remote template repo has new commits"
- git rev-list ..template/${{ env.REMOTE_BRANCH }}
-
- if [ $(git rev-list --count ..template/${{ env.REMOTE_BRANCH }}) -eq 0 ]; then
- echo "There are no commits new commits on the template repo"
-
- echo "Deleting origin branch(es) that contain the updates - if present"
- git push -f origin --delete ${{ env.UPDATE_BRANCH }} || true
- git push -f origin --delete ${{ env.UPDATE_BRANCH_MERGED }} || true
-
- echo "create_update_branch_pr=0" >> $GITHUB_OUTPUT
- echo "create_update_branch_merged_pr=0" >> $GITHUB_OUTPUT
- exit 0
- fi
-
- echo "Found new commits on the template repo"
-
- echo "Creating update branch"
- git branch ${{ env.UPDATE_BRANCH }} template/${{ env.REMOTE_BRANCH }}
- git branch --unset-upstream ${{ env.UPDATE_BRANCH }}
-
- echo "Pushing update branch"
- git push -f -u origin ${{ env.UPDATE_BRANCH }}
-
- echo "Getting base branch"
- base_branch=$(git branch --show-current)
- echo "Base branch is $base_branch"
- echo "base_branch=$base_branch" >> $GITHUB_OUTPUT
-
- echo "Trying to create auto-merged branch ${{ env.UPDATE_BRANCH_MERGED }}"
- git branch ${{ env.UPDATE_BRANCH_MERGED }} ${{ env.UPDATE_BRANCH }}
- git checkout ${{ env.UPDATE_BRANCH_MERGED }}
-
- echo "Merging branch $base_branch into ${{ env.UPDATE_BRANCH_MERGED }}"
- git merge $base_branch && merge_exit_code=$? || merge_exit_code=$?
- if [ $merge_exit_code -ne 0 ]; then
- echo "Auto merge failed! Manual merge required"
- echo "::notice ::Auto merge failed - Manual merge required"
-
- echo "Cleaning up failed merge"
- git merge --abort
- git checkout $base_branch
- git branch -D ${{ env.UPDATE_BRANCH_MERGED }} || true
-
- echo "Deleting auto-merge branch - if present"
- git push -f origin --delete ${{ env.UPDATE_BRANCH_MERGED }} || true
-
- echo "create_update_branch_pr=1" >> $GITHUB_OUTPUT
- echo "create_update_branch_merged_pr=0" >> $GITHUB_OUTPUT
- exit 0
- fi
-
- echo "Post processing: Trying to automatically fill in template variables"
- find . -type f \
- -not -path "./.git/**" \
- -not -path "./.github/workflows/update-from-template.yml" -print0 \
- | xargs -0 sed -i "s/template-placeholder/${GITHUB_REPOSITORY#*/}/g"
-
- git status
- git add --all
-
- if [[ "$(git status --porcelain)" != "" ]]; then
- echo "Filled in template; Committing"
-
- git commit -m "Fill in template"
- fi
-
- echo "Pushing auto-merged branch"
- git push -f -u origin ${{ env.UPDATE_BRANCH_MERGED }}
-
- echo "update_branch_merged_commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-
- echo "Restoring base branch $base_branch"
- git checkout $base_branch
-
- echo "create_update_branch_pr=0" >> $GITHUB_OUTPUT
- echo "create_update_branch_merged_pr=1" >> $GITHUB_OUTPUT
- echo "try_close_update_branch_pr=1" >> $GITHUB_OUTPUT
-
- - name: Create/Update PR update_branch
- if: steps.manage-branches.outputs.create_update_branch_pr == 1
- env:
- GH_TOKEN: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }}
- run: |
- gh_pr_up() {
- gh pr create -H "${{ env.UPDATE_BRANCH }}" "$@" || (git checkout "${{ env.UPDATE_BRANCH }}" && gh pr edit "$@")
- }
- gh_pr_up -B "${{ steps.manage-branches.outputs.base_branch }}" \
- --title "Update from template" \
- --body "An automated PR to sync changes from the template into this repo"
-
- # Ensure that only a single PR is open (otherwise confusion and spam)
- - name: Close PR update_branch
- if: steps.manage-branches.outputs.try_close_update_branch_pr == 1
- env:
- GH_TOKEN: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }}
- run: |
- gh pr close "${{ env.UPDATE_BRANCH }}" || true
-
- - name: Create/Update PR update_branch_merged
- if: steps.manage-branches.outputs.create_update_branch_merged_pr == 1
- env:
- GH_TOKEN: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }}
- run: |
- gh_pr_up() {
- gh pr create -H "${{ env.UPDATE_BRANCH_MERGED }}" "$@" || (git checkout "${{ env.UPDATE_BRANCH_MERGED }}" && gh pr edit "$@")
- }
- gh_pr_up -B "${{ steps.manage-branches.outputs.base_branch }}" \
- --title "Update from template (auto-merged)" \
- --body "An automated PR to sync changes from the template into this repo"
-
- # Wait a moment so that checks of PR have higher prio than following job
- sleep 3
-
- # Split into two jobs to help with executor starvation
- auto-merge:
- needs: [update]
- if: needs.update.outputs.create_update_branch_merged_pr == 1
- runs-on: ubuntu-latest
- timeout-minutes: 60
- steps:
- - uses: actions/checkout@v5
- with:
- # Required because otherwise there are always changes detected when executing diff/rev-list
- fetch-depth: 0
- # If no PAT is used the following error occurs on a push:
- # refusing to allow a GitHub App to create or update workflow `.github/workflows/xxx.yml` without `workflows` permission
- token: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }}
-
- - name: Init Git
- run: |
- git config --global user.email "111048771+xdev-gh-bot@users.noreply.github.com"
- git config --global user.name "XDEV Bot"
-
- - name: Checking if auto-merge for PR update_branch_merged can be done
- id: auto-merge-check
- env:
- GH_TOKEN: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }}
- run: |
- not_failed_conclusion="skipped|neutral|success"
- not_relevant_app_slug="dependabot|github-pages|sonarqubecloud"
-
- echo "Waiting for checks to start..."
- sleep 40s
-
- for i in {1..20}; do
- echo "Checking if PR can be auto-merged. Try: $i"
-
- echo "Checking if update-branch-merged exists"
- git fetch
- if [[ $(git ls-remote --heads origin refs/heads/${{ env.UPDATE_BRANCH_MERGED }}) ]]; then
- echo "Branch still exists; Continuing..."
- else
- echo "Branch origin/${{ env.UPDATE_BRANCH_MERGED }} is missing"
- exit 0
- fi
-
- echo "Fetching checks"
- cs_response=$(curl -sL \
- --fail-with-body \
- --connect-timeout 60 \
- --max-time 120 \
- -H "Accept: application/vnd.github+json" \
- -H "Authorization: Bearer $GH_TOKEN" \
- -H "X-GitHub-Api-Version: 2022-11-28" \
- https://api.github.com/repos/${{ github.repository }}/commits/${{ needs.update.outputs.update_branch_merged_commit }}/check-suites)
-
- cs_data=$(echo $cs_response | jq '.check_suites[] | { conclusion: .conclusion, slug: .app.slug, check_runs_url: .check_runs_url }')
- echo $cs_data
-
- if [[ -z "$cs_data" ]]; then
- echo "No check suite data - Assuming that there are no checks to run"
-
- echo "perform=1" >> $GITHUB_OUTPUT
- exit 0
- fi
-
- cs_failed=$(echo $cs_data | jq --arg x "$not_failed_conclusion" 'select ((.conclusion == null or (.conclusion | test($x))) | not)')
- if [[ -z "$cs_failed" ]]; then
- echo "No check failed so far; Checking if relevant checks are still running"
-
- cs_relevant_still_running=$(echo $cs_data | jq --arg x "$not_relevant_app_slug" 'select (.conclusion == null and (.slug | test($x) | not))')
- if [[ -z $cs_relevant_still_running ]]; then
- echo "All relevant checks finished - PR can be merged"
-
- echo "perform=1" >> $GITHUB_OUTPUT
- exit 0
- else
- echo "Relevant checks are still running"
- echo $cs_relevant_still_running
- fi
- else
- echo "Detected failed check"
- echo $cs_failed
-
- echo "perform=0" >> $GITHUB_OUTPUT
- exit 0
- fi
-
- echo "Waiting before next run..."
- sleep 30s
- done
-
- echo "Timed out - Assuming executor starvation - Forcing merge"
- echo "perform=1" >> $GITHUB_OUTPUT
-
- - name: Auto-merge update_branch_merged
- if: steps.auto-merge-check.outputs.perform == 1
- run: |
- echo "Getting base branch"
- base_branch=$(git branch --show-current)
- echo "Base branch is $base_branch"
-
- echo "Fetching..."
- git fetch
- if [[ $(git rev-parse origin/${{ env.UPDATE_BRANCH_MERGED }}) ]]; then
- echo "Branch still exists; Continuing..."
- else
- echo "Branch origin/${{ env.UPDATE_BRANCH_MERGED }} is missing"
- exit 0
- fi
-
- expected_commit="${{ needs.update.outputs.update_branch_merged_commit }}"
- actual_commit=$(git rev-parse origin/${{ env.UPDATE_BRANCH_MERGED }})
- if [[ "$expected_commit" != "$actual_commit" ]]; then
- echo "Branch ${{ env.UPDATE_BRANCH_MERGED }} contains unexpected commit $actual_commit"
- echo "Expected: $expected_commit"
-
- exit 0
- fi
-
- echo "Ensuring that current branch $base_branch is up-to-date"
- git pull
-
- echo "Merging origin/${{ env.UPDATE_BRANCH_MERGED }} into $base_branch"
- git merge origin/${{ env.UPDATE_BRANCH_MERGED }} && merge_exit_code=$? || merge_exit_code=$?
- if [ $merge_exit_code -ne 0 ]; then
- echo "Unexpected merge failure $merge_exit_code - Requires manual resolution"
-
- exit 0
- fi
-
- if [[ "${{ inputs.no_automatic_merge }}" == "true" ]]; then
- echo "Exiting due no_automatic_merge"
-
- exit 0
- fi
-
- echo "Pushing"
- git push
-
- echo "Cleaning up"
- git branch -D ${{ env.UPDATE_BRANCH }} || true
- git branch -D ${{ env.UPDATE_BRANCH_MERGED }} || true
- git push -f origin --delete ${{ env.UPDATE_BRANCH }} || true
- git push -f origin --delete ${{ env.UPDATE_BRANCH_MERGED }} || true
diff --git a/.gitignore b/.gitignore
index 464aa81..eb4294a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,6 +44,7 @@ hs_err_pid*
!.idea/saveactions_settings.xml
!.idea/checkstyle-idea.xml
!.idea/externalDependencies.xml
+!.idea/pmd-x.xml
!.idea/PMDPlugin.xml
!.idea/inspectionProfiles/
diff --git a/.idea/PMDPlugin.xml b/.idea/PMDPlugin.xml
deleted file mode 100644
index 0936e51..0000000
--- a/.idea/PMDPlugin.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml
index ec555b5..b8b753e 100644
--- a/.idea/checkstyle-idea.xml
+++ b/.idea/checkstyle-idea.xml
@@ -1,7 +1,7 @@
- 11.0.0
+ latestJavaOnlyWithTeststruetrue
diff --git a/.idea/externalDependencies.xml b/.idea/externalDependencies.xml
index 78be5b8..0b477b8 100644
--- a/.idea/externalDependencies.xml
+++ b/.idea/externalDependencies.xml
@@ -3,5 +3,6 @@
+
\ No newline at end of file
diff --git a/.idea/pmd-x.xml b/.idea/pmd-x.xml
new file mode 100644
index 0000000..7b3b48f
--- /dev/null
+++ b/.idea/pmd-x.xml
@@ -0,0 +1,27 @@
+
+
+
+ false
+ true
+ true
+ SUPPORTED_ONLY_WITH_TESTS
+