├── .java-version
├── .mvn
├── maven.config
├── wrapper
│ ├── maven-wrapper.jar
│ ├── maven-wrapper.properties
│ └── MavenWrapperDownloader.java
├── jvm.config
└── extensions.xml
├── config
├── logging.properties
├── owasp-suppressions.xml
├── pmd
│ └── custom-rules.xml
├── checkstyle
│ ├── sun_checks.xml
│ ├── checkstyle.xml
│ └── google_checks.xml
└── ide
│ └── intellij-java-google-style.xml
├── .gitattributes
├── images
├── try.png
├── changes.png
├── table-of-contents.png
├── modern-agile-wheel-english.png
├── public-domain.svg
├── jacoco.svg
├── cc0.svg
└── Wikibooks-contribute-icon.svg
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── src
├── test
│ └── java
│ │ └── demo
│ │ ├── package-info.java
│ │ ├── ApplicationIT.java
│ │ ├── TheFooTest.java
│ │ └── TerminalContext.java
├── lombok.config
├── main
│ └── java
│ │ └── demo
│ │ ├── package-info.java
│ │ ├── Application.java
│ │ └── TheFoo.java
└── integrationTest
│ └── java
│ └── demo
│ ├── package-info.java
│ └── ApplicationTest.java
├── dot-envrc-template
├── .editorconfig
├── .gitignore
├── gradle.properties
├── settings.gradle
├── .github
├── dependabot.yml
└── workflows
│ ├── ci-earthly-gradle.yml
│ └── ci-earthly-maven.yml
├── Earthfile
├── CONTRIBUTING.md
├── gradlew.bat
├── run-with-maven.sh
├── run-with-gradle.sh
├── coverage.sh
├── mvnw.cmd
├── LICENSE.md
├── gradlew
├── mvnw
├── README.md
└── pom.xml
/.java-version:
--------------------------------------------------------------------------------
1 | 21
2 |
--------------------------------------------------------------------------------
/.mvn/maven.config:
--------------------------------------------------------------------------------
1 | --strict-checksums
2 |
--------------------------------------------------------------------------------
/config/logging.properties:
--------------------------------------------------------------------------------
1 | .level = WARNING
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.bat text eol=crlf
3 | *.cmd text eol=crlf
4 |
--------------------------------------------------------------------------------
/images/try.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binkley/modern-java-practices/HEAD/images/try.png
--------------------------------------------------------------------------------
/images/changes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binkley/modern-java-practices/HEAD/images/changes.png
--------------------------------------------------------------------------------
/images/table-of-contents.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binkley/modern-java-practices/HEAD/images/table-of-contents.png
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binkley/modern-java-practices/HEAD/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binkley/modern-java-practices/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/images/modern-agile-wheel-english.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/binkley/modern-java-practices/HEAD/images/modern-agile-wheel-english.png
--------------------------------------------------------------------------------
/.mvn/jvm.config:
--------------------------------------------------------------------------------
1 | --add-opens java.base/java.lang=ALL-UNNAMED
2 | -Djava.util.logging.config.file=config/logging.properties
3 | -XX:+UseCodeCacheFlushing
4 | -XX:TieredStopAtLevel=1
5 |
--------------------------------------------------------------------------------
/src/test/java/demo/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Tests for
3 | * Modern
4 | * Java Practices.
5 | */
6 | package demo;
7 |
--------------------------------------------------------------------------------
/src/lombok.config:
--------------------------------------------------------------------------------
1 | config.stopBubbling = true
2 | lombok.addLombokGeneratedAnnotation = true
3 | lombok.anyConstructor.addConstructorProperties = true
4 | lombok.extern.findbugs.addSuppressFBWarnings = true
5 |
--------------------------------------------------------------------------------
/src/main/java/demo/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Demonstrations for
3 | * Modern
4 | * Java Practices.
5 | */
6 | package demo;
7 |
--------------------------------------------------------------------------------
/dot-envrc-template:
--------------------------------------------------------------------------------
1 | # For Earthly workaround
2 | export EARTHLY_DISABLE_REMOTE_REGISTRY_PROXY=true
3 | # For OWASP
4 | export OWASP_NVD_API_KEY='XXX'
5 | # To make Maven happier
6 | export JAVA_HOME="$(jenv javahome)"
7 |
--------------------------------------------------------------------------------
/src/integrationTest/java/demo/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Integration tests for
3 | * Modern
4 | * Java Practices.
5 | */
6 | package demo;
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [**]
4 | # Unless you are a Windows-only source code base, best to use linefeeds (`\n`)
5 | end_of_line = lf
6 | # Most teams will prefer 100 or 120 line limits, or no limits at all
7 | max_line_length = 80
8 |
--------------------------------------------------------------------------------
/.mvn/extensions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | fr.jcgay.maven
5 | maven-profiler
6 | 3.2
7 |
8 |
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Assumes use of IntelliJ -- adjust to the editors you use
2 | *.iml
3 | /.idea/
4 |
5 | # Gradle -- pick this or Maven
6 | /.gradle/
7 | /build/
8 |
9 | # Maven -- pick this or Gradle
10 | /target/
11 |
12 | # When running GitHub actions locally
13 | /workflow-artifacts/
14 |
15 | # DO NOT commit, push, or share. The env vars may hold secrets.
16 | /.envrc
17 |
--------------------------------------------------------------------------------
/images/public-domain.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/main/java/demo/Application.java:
--------------------------------------------------------------------------------
1 | package demo;
2 |
3 | import lombok.Generated;
4 |
5 | import static java.lang.System.out;
6 |
7 | /** Production class for integration test demonstration. */
8 | @Generated // Lie to JaCoCo and PIT
9 | public final class Application {
10 | /**
11 | * Main entry point.
12 | *
13 | * @param args the command-line arguments
14 | */
15 | public static void main(final String... args) {
16 | out.println(new TheFoo("I AM FOOCUTUS OF BORG"));
17 | }
18 |
19 | // This is a "utility" class, ie, no instances, only static methods
20 | private Application() { }
21 | }
22 |
--------------------------------------------------------------------------------
/src/test/java/demo/ApplicationIT.java:
--------------------------------------------------------------------------------
1 | package demo;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static demo.TerminalContext.captureTerminal;
6 | import static org.assertj.core.api.Assertions.assertThat;
7 |
8 | /**
9 | * Integration test for the application.
10 | */
11 | @SuppressWarnings({
12 | // PMD does not understand FooIT.java as a test class
13 | "PMD.AtLeastOneConstructor",
14 | })
15 | class ApplicationIT {
16 | /**
17 | * Use case: the application runs normally.
18 | */
19 | @Test
20 | void shouldRun() {
21 | final var out = captureTerminal(Application::main);
22 |
23 | assertThat(out).isEqualTo("TheFoo(label=I AM FOOCUTUS OF BORG)\n");
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/integrationTest/java/demo/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package demo;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static demo.TerminalContext.captureTerminal;
6 | import static org.assertj.core.api.Assertions.assertThat;
7 |
8 | /**
9 | * Just an example of Unit test.
10 | */
11 | @SuppressWarnings({
12 | "PMD.AtLeastOneConstructor",
13 | "PMD.SignatureDeclareThrowsException"
14 | })
15 | class ApplicationTest {
16 |
17 | /**
18 | * Use case: the application runs normally.
19 | **/
20 | @Test
21 | void shouldRun() throws Exception {
22 | final var out = captureTerminal(() -> {
23 | Application.main("Hello", "world!");
24 | });
25 |
26 | assertThat(out).isEqualTo("TheFoo(label=I AM FOOCUTUS OF BORG)\n");
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Gradle
2 | org.gradle.jvmargs=--add-opens java.base/java.lang=ALL-UNNAMED
3 | # Versions -- see gradle/libs.versions.toml
4 | checkstyleVersion=10.17.0
5 | coverageBranches=1.00
6 | coverageInstructions=1.00
7 | coverageLines=1.00
8 | # Use Enforcer 0.14.0 or higher which fixes a clash with DependencyCheck
9 | enforcerPluginVersion=0.14.0
10 | findsecbugsPluginVersion=1.13.0
11 | # Hey! This is important. This property sets your Gradle version.
12 | gradleWrapperVersion=8.9
13 | jacocoVersion=0.8.12
14 | jdkVersion=21
15 | mainClass=demo.Application
16 | pitestJUnit5PluginVersion=1.2.1
17 | pitestPluginVersion=1.15.0
18 | # Cannot name this "pitestVersion" -- the plugin has a property of the same
19 | # name, so this property needs to have a distinct name to satisfy Gradle
20 | pitestToolVersion=1.17.0
21 | pmdVersion=7.13.0
22 | spotbugsPluginVersion=6.0.15
23 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | // Note:
2 | // When you see "$fooVersion", those are versions pulled from
3 | // "gradle.properties".
4 |
5 | // TODO: Migrate these to use the version catalog (like other plugins in
6 | // "build.gradle").
7 | pluginManagement {
8 | plugins {
9 | id "com.github.spotbugs" version "$spotbugsPluginVersion"
10 | id "info.solidsoft.pitest" version "$pitestPluginVersion"
11 | }
12 | }
13 |
14 | buildscript {
15 | repositories {
16 | gradlePluginPortal()
17 | mavenCentral()
18 | }
19 |
20 | dependencies {
21 | classpath "org.kordamp.gradle:enforcer-gradle-plugin:$enforcerPluginVersion"
22 | }
23 | }
24 |
25 | apply plugin: "org.kordamp.gradle.enforcer"
26 |
27 | // See https://kordamp.org/enforcer-gradle-plugin/#_rules for the kinds of
28 | // rules you can set up.
29 | enforce {
30 | rule(enforcer.rules.RequireGradleVersion) { r ->
31 | r.version = "$gradleWrapperVersion"
32 | }
33 | }
34 |
35 | rootProject.name = "modern-java-practices"
36 |
--------------------------------------------------------------------------------
/images/jacoco.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gradle"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | groups:
8 | gradle-minors:
9 | applies-to: version-updates
10 | update-types:
11 | - "minor"
12 | - "patch"
13 |
14 | - package-ecosystem: "maven"
15 | directory: "/"
16 | schedule:
17 | interval: "daily"
18 | groups:
19 | maven-minors:
20 | applies-to: version-updates
21 | update-types:
22 | - "minor"
23 | - "patch"
24 |
25 | - package-ecosystem: "github-actions"
26 | directory: "/"
27 | schedule:
28 | interval: "daily"
29 | groups:
30 | gha-core:
31 | applies-to: version-updates
32 | update-types:
33 | - "minor"
34 | - "patch"
35 | patterns:
36 | - "actions/*"
37 | gha-third-party:
38 | applies-to: version-updates
39 | update-types:
40 | - "minor"
41 | - "patch"
42 | patterns:
43 | - "*"
44 | exclude-patterns:
45 | - "actions/*"
46 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | # Licensed to the Apache Software Foundation (ASF) under one
2 | # or more contributor license agreements. See the NOTICE file
3 | # distributed with this work for additional information
4 | # regarding copyright ownership. The ASF licenses this file
5 | # to you under the Apache License, Version 2.0 (the
6 | # "License"); you may not use this file except in compliance
7 | # with the License. You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing,
12 | # software distributed under the License is distributed on an
13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 | # KIND, either express or implied. See the License for the
15 | # specific language governing permissions and limitations
16 | # under the License.
17 | wrapperVersion=3.3.2
18 | distributionType=source
19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
20 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
21 |
--------------------------------------------------------------------------------
/src/main/java/demo/TheFoo.java:
--------------------------------------------------------------------------------
1 | package demo;
2 |
3 | import lombok.EqualsAndHashCode;
4 | import lombok.Getter;
5 | import lombok.RequiredArgsConstructor;
6 | import lombok.ToString;
7 |
8 | import javax.annotation.Nonnull;
9 | import java.util.regex.Pattern;
10 |
11 | import static java.util.regex.Pattern.CASE_INSENSITIVE;
12 | import static java.util.regex.Pattern.compile;
13 |
14 | /** Demonstration class. */
15 | @Getter
16 | @EqualsAndHashCode
17 | @RequiredArgsConstructor
18 | @ToString
19 | public final class TheFoo {
20 | /** Check for the Borg. */
21 | private static final Pattern BORG = compile("BORG", CASE_INSENSITIVE);
22 |
23 | /** The foo. */
24 | @Nonnull
25 | private final String label;
26 |
27 | /**
28 | * Is there danger?
29 | *
30 | * @return if the ship should go to red alert
31 | */
32 | public boolean isRedAlert() {
33 | return checkRedAlert(label);
34 | }
35 |
36 | /**
37 | * Checks text for the presence of the Borg.
38 | *
39 | * @param text the text to check
40 | *
41 | * @return {@code true} if Borg is found
42 | */
43 | public static boolean checkRedAlert(final String text) {
44 | return BORG.matcher(text).find();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Earthfile:
--------------------------------------------------------------------------------
1 | VERSION 0.8
2 | FROM eclipse-temurin:21-jdk-jammy
3 | WORKDIR /code
4 |
5 | # build-with-gradle builds and tests with Gradle, and saves the build/ directory
6 | build-with-gradle:
7 | COPY gradlew .
8 | COPY gradle gradle
9 | COPY gradle.properties .
10 | COPY settings.gradle .
11 | COPY build.gradle .
12 | COPY config config
13 | COPY src src
14 | RUN --secret OWASP_NVD_API_KEY ./gradlew clean build
15 |
16 | # For CI so that GitHub can copy artifacts:
17 | # Just copy everything rather than maintain a whitelist of files/dirs.
18 | SAVE ARTIFACT --keep-ts build AS LOCAL build
19 |
20 | # run-with-gradle runs the demo program with Gradle, building if needed
21 | run-with-gradle:
22 | FROM +build-with-gradle
23 | COPY run-with-gradle.sh .
24 | RUN ./run-with-gradle.sh
25 |
26 | # build-with-maven builds and tests with Maven, and saves the target/ directory
27 | build-with-maven:
28 | COPY mvnw .
29 | COPY .mvn .mvn
30 | COPY pom.xml .
31 | COPY config config
32 | COPY src src
33 | RUN --secret OWASP_NVD_API_KEY ./mvnw --no-transfer-progress clean verify
34 |
35 | # For CI so that GitHub can copy artifacts:
36 | # Just copy everything rather than maintain a whitelist of files/dirs.
37 | SAVE ARTIFACT --keep-ts target AS LOCAL target
38 |
39 | # run-with-maven runs the demo program with Maven, building if needed
40 | run-with-maven:
41 | FROM +build-with-maven
42 | COPY run-with-maven.sh .
43 | RUN ./run-with-maven.sh
44 |
45 | # build executes both +build-with-gradle and +build-with-maven targets
46 | build:
47 | BUILD +build-with-gradle
48 | BUILD +build-with-maven
49 |
50 | # run executes both +run-with-gradle and +run-with-maven targets
51 | run:
52 | BUILD +run-with-gradle
53 | BUILD +run-with-maven
54 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Maintainance
2 |
3 | Notes for maintaining this repository:
4 |
5 | Use the helper scripts, and see that they:
6 | 1. **Work.**
7 | Working scripts is the #1 thing to help others.
8 | 2. **Work in CI and make sense there.**
9 | A top goal for you: your local build should mirror the build in CI.
10 | Check the GitHub actions.
11 | 3. **Make sense for the project.**
12 | An example is moving from Batect to Earthly.
13 | Scripts should migrate from (the archived) Batect.
14 |
15 | # Contributing
16 |
17 | Several scripts in the project root are to aid in contributing.
18 |
19 | - [`build-as-ci-does.sh`](./build-as-ci-does.sh) —
20 | Recreate locally the build used by CI in GitHub actions using Batect and
21 | Earthly (both use Docker):
22 | * Run a clean full build for Gradle
23 | * Run the Gradle-built demo program
24 | * Run a clean full build for Maven
25 | * Run the Maven-built demo program
26 | Helpful when CI has steps that local developers do not, and you want to
27 | reproduce or explore locally a CI problem. The script should match the
28 | actions your CI takes on pushes (this project uses GitHub actions).
29 | - [`coverage`](./coverage)
30 | Checks if the local code passes at given levels of code coverage.
31 | The script is focused on Maven, but with edits would do the same for Gradle.
32 | This supports the ["ratchet"
33 | pattern](https://robertgreiner.com/continuous-code-improvement-using-ratcheting/).
34 | - [`run-with-gradle.sh`](./run-with-gradle.sh)
35 | If you are a Gradle project, you will likely rename this to just `run` or
36 | similar.
37 | - [`run-with-maven.sh`](./run-with-maven.sh)
38 | If you are a Maven project, you will likely rename this to just `run` or
39 | similar.
40 | - Use the
41 | [`wiki-outline.py`](https://raw.githubusercontent.com/wiki/binkley/modern-java-practices/etc/wiki-outline.py)
42 | script in the wiki repo to get a Table-of-Contents view from the command
43 | line.
44 |
--------------------------------------------------------------------------------
/config/owasp-suppressions.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 | CVE-2023-2976
11 |
12 |
13 |
19 | CVE-2022-4244
20 |
21 |
22 |
28 | CVE-2022-4245
29 |
30 |
31 |
37 | CVE-2023-45960
38 |
39 |
40 |
45 | CVE-2015-0897
46 |
47 |
48 |
--------------------------------------------------------------------------------
/images/cc0.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
25 |
--------------------------------------------------------------------------------
/config/pmd/custom-rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | Custom rules to use on the project
8 | The source of rule sets
9 | https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/category/java
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/test/java/demo/TheFooTest.java:
--------------------------------------------------------------------------------
1 | package demo;
2 |
3 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4 | import org.junit.jupiter.api.Test;
5 | import org.mockito.MockedStatic;
6 |
7 | import static demo.TheFoo.checkRedAlert;
8 | import static org.assertj.core.api.Assertions.assertThat;
9 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
10 | import static org.mockito.ArgumentMatchers.eq;
11 | import static org.mockito.Mockito.mockStatic;
12 |
13 | /**
14 | * An example of test class.
15 | */
16 | @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION")
17 | @SuppressWarnings({
18 | "PMD.TooManyStaticImports",
19 | "PMD.AtLeastOneConstructor"
20 | })
21 | class TheFooTest {
22 | @Test
23 | void shouldKnowTheFoo() {
24 | final var theFoo = "I am the Foo!";
25 | final var didTheFoo = new TheFoo(theFoo);
26 |
27 | assertThat(didTheFoo.getLabel()).isEqualTo(theFoo);
28 | }
29 |
30 | @Test
31 | void shouldBeSameFooAnObject() {
32 | final var theFoo = "I am the Foo!";
33 | final var didTheFoo = new TheFoo(theFoo);
34 | final var anotherFoo = new TheFoo(theFoo);
35 |
36 | assertThat(didTheFoo).isEqualTo(anotherFoo);
37 | }
38 |
39 | @Test
40 | void shouldBeSameFooAsAKey() {
41 | final var theFoo = "I am the Foo!";
42 | final var didTheFoo = new TheFoo(theFoo);
43 | final var anotherFoo = new TheFoo(theFoo);
44 |
45 | assertThat(didTheFoo.hashCode()).isEqualTo(anotherFoo.hashCode());
46 | }
47 |
48 | @SuppressWarnings("ConstantConditions")
49 | @Test
50 | void shouldComplainWhenInvalid() {
51 | assertThatThrownBy(() -> new TheFoo(null))
52 | .isInstanceOf(NullPointerException.class);
53 | }
54 |
55 | @Test
56 | void shouldRedAlert() {
57 | assertThat(new TheFoo("We are the Borg.").isRedAlert())
58 | .isTrue();
59 | }
60 |
61 | @Test
62 | void shouldNotRedAlert() {
63 | assertThat(new TheFoo("My debt is repaid.").isRedAlert())
64 | .isFalse();
65 | }
66 |
67 | @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE")
68 | @Test
69 | void shouldRedAlertAsStaticMock() {
70 | final var label = "Some label, *mumble, mumble*. ";
71 |
72 | try (MockedStatic mockFoo = mockStatic(TheFoo.class)) {
73 | mockFoo.when(() -> checkRedAlert(eq(label))).thenReturn(true);
74 |
75 | assertThat(new TheFoo(label).isRedAlert())
76 | .isTrue();
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | com-diffplug-spotless-plugin = "7.0.0.BETA4"
3 | com-dorongold-task-tree-plugin = "4.0.0"
4 | com-github-andygoossens-gradle-modernizer-plugin = "1.11.0"
5 | com-github-ben-manes-versions-plugin = "0.51.0"
6 | com-github-spotbugs = "4.8.6"
7 | com-google-code-findbugs = "3.0.1"
8 | com-h3xstream-findsecbugs-findsecbugs-plugin = "1.13.0"
9 | com-puppycrawl-tools-checkstyle = "10.20.0"
10 | nl-littlerobots-version-catalog-update-plugin = "0.8.5"
11 | org-assertj = "3.26.3"
12 | org-gaul-modernizer = "2.9.0"
13 | org-jacoco = "0.8.12"
14 | org-junit-jupiter = "5.11.1"
15 | org-kordamp-gradle-jdeps-plugin = "0.20.0"
16 | org-mockito = "5.13.0"
17 | org-owasp-dependencycheck-plugin = "11.1.0"
18 | org-pitest-pitest-command-line = "1.17.3"
19 | org-pitest-pitest-junit5-plugin = "1.2.1"
20 | org-projectlombok = "1.18.36"
21 |
22 | [libraries]
23 | com-github-spotbugs-spotbugs-annotations = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "com-github-spotbugs" }
24 | com-google-code-findbugs-findbugs-annotations = { module = "com.google.code.findbugs:findbugs-annotations", version.ref = "com-google-code-findbugs" }
25 | com-h3xstream-findsecbugs-findsecbugs-plugin = { module = "com.h3xstream.findsecbugs:findsecbugs-plugin", version.ref = "com-h3xstream-findsecbugs-findsecbugs-plugin" }
26 | com-puppycrawl-tools-checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "com-puppycrawl-tools-checkstyle" }
27 | org-apache-bcel = "org.apache.bcel:bcel:6.10.0"
28 | org-assertj-assertj-core = { module = "org.assertj:assertj-core", version.ref = "org-assertj" }
29 | org-gaul-modernizer-maven-annotations = { module = "org.gaul:modernizer-maven-annotations", version.ref = "org-gaul-modernizer" }
30 | org-jacoco-org-jacoco-agent = { module = "org.jacoco:org.jacoco.agent", version.ref = "org-jacoco" }
31 | org-jacoco-org-jacoco-ant = { module = "org.jacoco:org.jacoco.ant", version.ref = "org-jacoco" }
32 | org-junit-jupiter-junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "org-junit-jupiter" }
33 | org-junit-jupiter-junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "org-junit-jupiter" }
34 | org-mockito-mockito-core = { module = "org.mockito:mockito-core", version.ref = "org-mockito" }
35 | org-pitest-pitest-command-line = { module = "org.pitest:pitest-command-line", version.ref = "org-pitest-pitest-command-line" }
36 | org-pitest-pitest-junit5-plugin = { module = "org.pitest:pitest-junit5-plugin", version.ref = "org-pitest-pitest-junit5-plugin" }
37 | org-projectlombok-lombok = { module = "org.projectlombok:lombok", version.ref = "org-projectlombok" }
38 |
39 | [plugins]
40 | com-diffplug-spotless-plugin = { id = "com.diffplug.spotless", version.ref = "com-diffplug-spotless-plugin" }
41 | com-dorongold-task-tree-plugin = { id = "com.dorongold.task-tree", version.ref = "com-dorongold-task-tree-plugin" }
42 | com-github-andygoossens-gradle-modernizer-plugin = { id = "com.github.andygoossens.gradle-modernizer-plugin", version.ref = "com-github-andygoossens-gradle-modernizer-plugin" }
43 | com-github-ben-manes-versions-plugin = { id = "com.github.ben-manes.versions", version.ref = "com-github-ben-manes-versions-plugin" }
44 | nl-littlerobots-version-catalog-update-plugin = { id = "nl.littlerobots.version-catalog-update", version.ref = "nl-littlerobots-version-catalog-update-plugin" }
45 | org-kordamp-gradle-jdeps-plugin = { id = "org.kordamp.gradle.jdeps", version.ref = "org-kordamp-gradle-jdeps-plugin" }
46 | org-owasp-dependencycheck-plugin = { id = "org.owasp.dependencycheck", version.ref = "org-owasp-dependencycheck-plugin" }
47 |
--------------------------------------------------------------------------------
/run-with-maven.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # shellcheck disable=SC2214,SC2215
3 |
4 | if ((BASH_VERSINFO[0] < 4))
5 | then
6 | cat <&2
7 | $0: This script requires Bash 4.0 or higher.
8 | If you are on Mac, the native /bin/bash is from 2006 or older; consider using a
9 | more recent Bash from Homebrew.
10 | EOM
11 | exit 3
12 | fi
13 |
14 | export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:+${FUNCNAME[0]}():} '
15 |
16 | # Edit these to suit
17 | readonly package=demo
18 | readonly artifactId=modern-java-practices
19 | readonly version=0-SNAPSHOT
20 | jvm_flags=()
21 |
22 | # No editable parts below here
23 |
24 | set -e
25 | set -u
26 | set -o pipefail
27 |
28 | readonly progname="${0##*/}"
29 |
30 | printf -v pbold "\e[1m"
31 | printf -v pred "\e[31m"
32 | printf -v preset "\e[0m"
33 |
34 | function print-help() {
35 | cat <&2
111 | exit 2
112 | fi
113 | readonly jar=target/$artifactId-$version-jar-with-dependencies.jar
114 |
115 | case "$alt_class" in
116 | '') jvm_flags=("${jvm_flags[@]}" -jar "$jar") ;;
117 | *)
118 | readonly runtime_classname="$(runtime-classname "$package.$alt_class")"
119 | jvm_flags=("${jvm_flags[@]}" -cp "$jar" "$runtime_classname")
120 | ;;
121 | esac
122 |
123 | rebuild-if-needed
124 |
125 | expected='TheFoo(label=I AM FOOCUTUS OF BORG)'
126 | captured_output=$(exec java "${jvm_flags[@]}" "$@")
127 | case $captured_output in
128 | $expected ) echo "$captured_output" ;;
129 | * )
130 | echo "$pbold$pred$0: ERROR: expected: $expected, actual: $captured_output$preset"
131 | false
132 | ;;
133 | esac
134 |
--------------------------------------------------------------------------------
/run-with-gradle.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # shellcheck disable=SC2214,SC2215
3 |
4 | if ((BASH_VERSINFO[0] < 4))
5 | then
6 | cat <&2
7 | $0: This script requires Bash 4.0 or higher.
8 | If you are on Mac, the native /bin/bash is from 2006 or older; consider using a
9 | more recent Bash from Homebrew.
10 | EOM
11 | exit 3
12 | fi
13 |
14 | export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:+${FUNCNAME[0]}():} '
15 |
16 | # Edit these to suit
17 | readonly package=hm.binkley.md
18 | readonly artifactId=modern-java-practices
19 | readonly version=0-SNAPSHOT
20 | jvm_flags=()
21 |
22 | # No editable parts below here
23 |
24 | set -e
25 | set -u
26 | set -o pipefail
27 |
28 | readonly progname="${0##*/}"
29 |
30 | printf -v pbold "\e[1m"
31 | printf -v pred "\e[31m"
32 | printf -v preset "\e[0m"
33 |
34 | function print-help() {
35 | cat <&2
118 | exit 2
119 | fi
120 | readonly jar=build/libs/$artifactId-$version.jar
121 |
122 | case "$alt_class" in
123 | '') jvm_flags=("${jvm_flags[@]}" -jar "$jar") ;;
124 | *)
125 | readonly runtime_classname="$(runtime-classname "$package.$alt_class")"
126 | jvm_flags=("${jvm_flags[@]}" -cp "$jar" "$runtime_classname")
127 | ;;
128 | esac
129 |
130 | rebuild-if-needed
131 |
132 | expected='TheFoo(label=I AM FOOCUTUS OF BORG)'
133 | captured_output=$(exec java "${jvm_flags[@]}" "$@")
134 | case $captured_output in
135 | $expected ) echo "$captured_output" ;;
136 | * )
137 | echo "$pbold$pred$0: ERROR: expected: $expected, actual: $captured_output$preset"
138 | false
139 | ;;
140 | esac
141 |
--------------------------------------------------------------------------------
/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed to the Apache Software Foundation (ASF) under one
3 | * or more contributor license agreements. See the NOTICE file
4 | * distributed with this work for additional information
5 | * regarding copyright ownership. The ASF licenses this file
6 | * to you under the Apache License, Version 2.0 (the
7 | * "License"); you may not use this file except in compliance
8 | * with the License. You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing,
13 | * software distributed under the License is distributed on an
14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | * KIND, either express or implied. See the License for the
16 | * specific language governing permissions and limitations
17 | * under the License.
18 | */
19 |
20 | import java.io.IOException;
21 | import java.io.InputStream;
22 | import java.net.Authenticator;
23 | import java.net.PasswordAuthentication;
24 | import java.net.URI;
25 | import java.net.URL;
26 | import java.nio.file.Files;
27 | import java.nio.file.Path;
28 | import java.nio.file.Paths;
29 | import java.nio.file.StandardCopyOption;
30 | import java.util.concurrent.ThreadLocalRandom;
31 |
32 | public final class MavenWrapperDownloader {
33 | private static final String WRAPPER_VERSION = "3.3.2";
34 |
35 | private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv("MVNW_VERBOSE"));
36 |
37 | public static void main(String[] args) {
38 | log("Apache Maven Wrapper Downloader " + WRAPPER_VERSION);
39 |
40 | if (args.length != 2) {
41 | System.err.println(" - ERROR wrapperUrl or wrapperJarPath parameter missing");
42 | System.exit(1);
43 | }
44 |
45 | try {
46 | log(" - Downloader started");
47 | final URL wrapperUrl = URI.create(args[0]).toURL();
48 | final String jarPath = args[1].replace("..", ""); // Sanitize path
49 | final Path wrapperJarPath = Paths.get(jarPath).toAbsolutePath().normalize();
50 | downloadFileFromURL(wrapperUrl, wrapperJarPath);
51 | log("Done");
52 | } catch (IOException e) {
53 | System.err.println("- Error downloading: " + e.getMessage());
54 | if (VERBOSE) {
55 | e.printStackTrace();
56 | }
57 | System.exit(1);
58 | }
59 | }
60 |
61 | private static void downloadFileFromURL(URL wrapperUrl, Path wrapperJarPath)
62 | throws IOException {
63 | log(" - Downloading to: " + wrapperJarPath);
64 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
65 | final String username = System.getenv("MVNW_USERNAME");
66 | final char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
67 | Authenticator.setDefault(new Authenticator() {
68 | @Override
69 | protected PasswordAuthentication getPasswordAuthentication() {
70 | return new PasswordAuthentication(username, password);
71 | }
72 | });
73 | }
74 | Path temp = wrapperJarPath
75 | .getParent()
76 | .resolve(wrapperJarPath.getFileName() + "."
77 | + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
78 | try (InputStream inStream = wrapperUrl.openStream()) {
79 | Files.copy(inStream, temp, StandardCopyOption.REPLACE_EXISTING);
80 | Files.move(temp, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING);
81 | } finally {
82 | Files.deleteIfExists(temp);
83 | }
84 | log(" - Downloader complete");
85 | }
86 |
87 | private static void log(String msg) {
88 | if (VERBOSE) {
89 | System.out.println(msg);
90 | }
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/coverage.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:+${FUNCNAME[0]}():} '
4 |
5 | set -e
6 | set -u
7 | set -o pipefail
8 |
9 | readonly progname="${0##*/}"
10 |
11 | function print-help() {
12 | cat <&2
58 | return 1
59 | fi
60 | }
61 |
62 | full=none
63 | mutation=false
64 | open=false
65 | run=true
66 | # shellcheck disable=SC2214
67 | while getopts :f:hmox-: opt; do
68 | [[ $opt == - ]] && opt=${OPTARG%%=*} OPTARG=${OPTARG#*=}
69 | # shellcheck disable=SC2213
70 | case $opt in
71 | f | full-coverage)
72 | case "$OPTARG" in
73 | unit) full="$OPTARG" ;;
74 | all | mutation) mutation=true full="$OPTARG" ;;
75 | *)
76 | bad-option-argument "-f" "$OPTARG"
77 | exit 2
78 | ;;
79 | esac
80 | ;;
81 | h | help)
82 | print-help
83 | exit 0
84 | ;;
85 | m | with-mutation) mutation=true ;;
86 | o | open-report) open=true ;;
87 | x | no-rerun) open=true run=false ;;
88 | \?)
89 | bad-option "$OPTARG"
90 | exit 2
91 | ;;
92 | :)
93 | bad-option-argument "$OPTARG"
94 | exit 2
95 | ;;
96 | esac
97 | done
98 | shift $((OPTIND - 1))
99 |
100 | case "$full" in
101 | none) flags='' ;;
102 | all)
103 | flags='-Dcoverage.lines=100 -Dcoverage.branches=100 -Dcoverage.instructions=100 -Dcoverage.mutation=100'
104 | ;;
105 | mutation)
106 | flags='-Dcoverage.mutation=100'
107 | ;;
108 | unit)
109 | flags='-Dcoverage.lines=100 -Dcoverage.branches=100 -Dcoverage.instructions=100'
110 | ;;
111 | esac
112 |
113 | ((rc = 0)) || true
114 | if $run; then
115 | if $mutation; then
116 | targets='-DwithHistory clean verify'
117 | else
118 | targets='clean test jacoco:report jacoco:check'
119 | fi
120 |
121 | # shellcheck disable=SC2086
122 | ./mvnw $flags "$@" $targets || ((rc += $?)) || true
123 | else
124 | # Refresh coverage report when editing limits in pom.xml without rebuild
125 | ./mvnw $flags "$@" jacoco:report jacoco:check || ((rc += $?)) || true
126 | fi
127 |
128 | if $open; then
129 | if require-file target/site/jacoco/index.html; then
130 | (cd target/site/jacoco && open index.html)
131 | else
132 | ((++rc))
133 | fi
134 | if $mutation && require-file target/pit-reports/index.html; then
135 | (cd target/pit-reports && open index.html)
136 | else
137 | ((++rc))
138 | fi
139 | fi
140 |
141 | # shellcheck disable=SC2086
142 | exit $rc
143 |
--------------------------------------------------------------------------------
/src/test/java/demo/TerminalContext.java:
--------------------------------------------------------------------------------
1 | package demo;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.PrintStream;
5 |
6 | import static java.nio.charset.StandardCharsets.UTF_8;
7 |
8 | /**
9 | * Replaces writing to STDOUT/STDERR with capture to an array of bytes all for
10 | * testing.
11 | *
12 | * One does not usually test STDOUT/STDERR output unless working on a terminal
13 | * program.
14 | * This example project does so to demonstrate integration testing.
15 | * We do not want to focus on complex alternatives (databases, dependency
16 | * injection, remote REST calls, et al) but it is important to demonstrate
17 | * contrast between unit tests and integration tests in the
18 | * build.
19 | *
20 | * Note: Mucking with UTF-8/character encodings is required in this
21 | * code even though our tests are all ASCII, and users wanting a richer API
22 | * can turn to {@code system-lambda} or similar libraries. Anytime you work
23 | * with text for humans, keep UTF and encodings in mind.
24 | *
25 | * Note: Use of this test helper means: do not run integration
26 | * tests in parallel: their output to the terminal will become interleaved.
27 | * This is a concern always and anytime you access STDOUT/STDERR.
28 | * Note: A recommended pattern is that a class initializes as much as
29 | * possible in the fields so that the constructor has minimal work.
30 | *
31 | * @see TerminalContext#captureTerminal(Runnable) the main entry point for this
32 | * class
33 | * @see system-lambda
35 | * for more sophisticated handling of STDOUT/STDERR
36 | */
37 | @SuppressWarnings({
38 | // We follow JVM statics for out and err which confuses PMD
39 | "PMD.FinalFieldCouldBeStatic",
40 | // The code is too complex for PMD to follow because of JVM statics
41 | "PMD.CloseResource",
42 | // File issue with PMD, and suppress in config.
43 | // Why is long Javadoc triggering this?
44 | "PMD.CommentSize"})
45 | public final class TerminalContext implements AutoCloseable {
46 | /**
47 | * Combine STDOUT/STDERR into a single buffer usable in tests.
48 | *