diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 78d62a3..a5da9dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,30 +33,28 @@ jobs: key: ${{ runner.os }}-sonar-${{ hashFiles('**/*.gradle') }} restore-keys: ${{ runner.os }}-sonar- - - uses: gradle/actions/wrapper-validation@v3 - - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: ${{ matrix.java }} - cache: 'gradle' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 - name: Enable testcontainer reuse run: echo 'testcontainers.reuse.enable=true' > "$HOME/.testcontainers.properties" - - name: Build with Java ${{ matrix.java }} - run: | - ./gradlew clean build --info \ - --exclude-task integrationTest \ - -PjavaVersion=${{matrix.java}} + - name: Build with Java ${{ matrix.java }} without integration tests + run: ./gradlew clean build --info --exclude-task integrationTest -PjavaVersion=${{matrix.java}} + + - name: Build with Java ${{ matrix.java }} with integration tests + if: ${{ env.DEFAULT_JAVA == matrix.java }} + run: ./gradlew clean build --info -PjavaVersion=${{matrix.java}} - name: Sonar analysis if: ${{ env.DEFAULT_JAVA == matrix.java && env.SONAR_TOKEN != null }} - run: | - ./gradlew sonar --info \ - --exclude-task integrationTest \ - -Dsonar.token=$SONAR_TOKEN + run: ./gradlew sonar --info --exclude-task integrationTest -Dsonar.token=$SONAR_TOKEN env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6f63d23..da43028 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,9 +30,9 @@ jobs: with: distribution: 'temurin' java-version: 17 - cache: 'gradle' - - uses: gradle/actions/wrapper-validation@v3 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/.github/workflows/github_release.sh b/.github/workflows/github_release.sh new file mode 100755 index 0000000..7b3d001 --- /dev/null +++ b/.github/workflows/github_release.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -o errexit +set -o nounset +set -o pipefail + +base_dir="$( cd "$(dirname "$0")/../.." >/dev/null 2>&1 ; pwd -P )" +readonly base_dir + +cd "$base_dir" +echo "Reading project version from Gradle project at ${base_dir}..." +project_version=$(./gradlew properties --console=plain --quiet | grep "^version:" | awk '{print $2}') +readonly project_version +echo "Read project version '$project_version' from Gradle project" + +readonly title="Release $project_version" +readonly tag="$project_version" +echo "Creating release:" +echo "Git tag : $tag" +echo "Title : $title" + +release_url=$(gh release create --latest --title "$title" --target main "$tag") +readonly release_url +echo "Release URL: $release_url" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..92d4445 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release + +on: + workflow_dispatch: + inputs: + skip-deploy-maven-central: + description: "Skip deployment to Maven Central" + required: true + type: boolean + default: false + +jobs: + release: + runs-on: ubuntu-latest + defaults: + run: + shell: "bash" + concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + permissions: + contents: write # Required for creating GitHub release + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Fail if not running on main branch + if: ${{ github.ref != 'refs/heads/main' }} + uses: actions/github-script@v7 + with: + script: | + core.setFailed('Not running on main branch, github.ref is ${{ github.ref }}. Please start this workflow only on main') + + - uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: 17 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build + run: ./gradlew build --warning-mode all + + - name: Publish to Maven Central + if: ${{ !inputs.skip-deploy-maven-central }} + run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository --warning-mode all + env: + ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.OSSRH_USERNAME }} + ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.OSSRH_PASSWORD }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSSRH_GPG_SECRET_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} + + - name: Create GitHub Release + run: ./.github/workflows/github_release.sh + env: + GH_TOKEN: ${{ github.token }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e728db..a414622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.8.0] - unreleased +## [0.7.1] - 2024-09-01 + +- [PR #26](https://github.com/itsallcode/simple-jdbc/pull/26): Update dependencies + ## [0.7.0] - 2024-05-04 - [PR #22](https://github.com/itsallcode/simple-jdbc/pull/22): Added `module-info.java` diff --git a/README.md b/README.md index e87a813..675fafa 100644 --- a/README.md +++ b/README.md @@ -23,17 +23,24 @@ Add dependency to your gradle project: ```groovy dependencies { - implementation 'org.itsallcode:simple-jdbc:0.7.0' + implementation 'org.itsallcode:simple-jdbc:0.7.1' } ``` ```java + +// Define a model record or class record Name(int id, String name) { Object[] toRow() { return new Object[] { id, name }; } } +import org.itsallcode.jdbc.ConnectionFactory; +import org.itsallcode.jdbc.SimpleConnection; +import org.itsallcode.jdbc.resultset.SimpleResultSet; + +// Execute query and fetch result ConnectionFactory connectionFactory = ConnectionFactory.create(); try (SimpleConnection connection = connectionFactory.create("jdbc:h2:mem:", "user", "password")) { connection.executeScript(readResource("/schema.sql")); @@ -46,6 +53,7 @@ try (SimpleConnection connection = connectionFactory.create("jdbc:h2:mem:", "use } } ``` + ## Development ### Check if dependencies are up-to-date @@ -73,29 +81,18 @@ open build/reports/jacoco/test/html/index.html ### Publish to Maven Central -1. Add the following to your `~/.gradle/gradle.properties`: - - ```properties - ossrhUsername= - ossrhPassword= - - signing.keyId= - signing.password= - signing.secretKeyRingFile= - ``` - -2. Increment version number in `build.gradle` and `README.md`, update [CHANGELOG.md](CHANGELOG.md), commit and push. -3. Optional: run the following command to do a dry-run: - - ```sh - ./gradlew clean check build publishToSonatype closeSonatypeStagingRepository --info - ``` +#### Preparations -4. Run the following command to publish to Maven Central: +1. Checkout the `main` branch, create a new branch. +2. Update version number in `build.gradle` and `README.md`. +3. Add changes in new version to `CHANGELOG.md`. +4. Commit and push changes. +5. Create a new pull request, have it reviewed and merged to `main`. - ```sh - ./gradlew clean check build publishToSonatype closeAndReleaseSonatypeStagingRepository --info - ``` +#### Perform the Release -5. Create a new [release](https://github.com/itsallcode/simple-jdbc/releases) on GitHub. -6. After some time the release will be available at [Maven Central](https://repo1.maven.org/maven2/org/itsallcode/simple-jdbc/). +1. Start the release workflow + * Run command `gh workflow run release.yml --repo itsallcode/simple-jdbc --ref main` + * or go to [GitHub Actions](https://github.com/itsallcode/simple-jdbc/actions/workflows/release.yml) and start the `release.yml` workflow on branch `main`. +2. Update title and description of the newly created [GitHub release](https://github.com/itsallcode/simple-jdbc/releases). +3. After some time the release will be available at [Maven Central](https://repo1.maven.org/maven2/org/itsallcode/simple-jdbc/). diff --git a/build.gradle b/build.gradle index 2ca0ac1..8677cd3 100644 --- a/build.gradle +++ b/build.gradle @@ -5,20 +5,21 @@ plugins { id 'jacoco-report-aggregation' id 'signing' id 'maven-publish' - id 'org.sonarqube' version '5.0.0.4638' + id 'org.sonarqube' version '5.1.0.4882' id "io.github.gradle-nexus.publish-plugin" version "2.0.0" id 'com.github.ben-manes.versions' version '0.51.0' } group 'org.itsallcode' -version = '0.7.0' +version = '0.7.1' dependencies { } java { toolchain { - languageVersion = JavaLanguageVersion.of(getPropertyWithDefault('javaVersion', '17')) + def javaVersion = project.hasProperty('javaVersion') ? project.getProperty('javaVersion') : 17 + languageVersion = JavaLanguageVersion.of(javaVersion) } withJavadocJar() withSourcesJar() @@ -39,7 +40,7 @@ tasks.withType(JavaCompile) { testing { suites { configureEach { - useJUnitJupiter('5.10.1') + useJUnitJupiter() dependencies { implementation project() implementation libs.assertj @@ -93,6 +94,10 @@ jacocoTestReport { } } +test { + finalizedBy jacocoTestReport +} + sonar { properties { property("sonar.organization", "itsallcode") @@ -102,24 +107,6 @@ sonar { rootProject.tasks['sonar'].dependsOn(tasks['testCodeCoverageReport'], tasks['integrationTestCodeCoverageReport']) -def getPropertyWithDefault(String name, String defaultValue) { - if(project.hasProperty(name)) { - def value = project.property(name) - logger.info("Found value '${value}' for project property '${name}'") - return value - } - logger.info("Project property '${name}' not defined, using default '${defaultValue}'") - return defaultValue -} - -def getOptionalProperty(String name) { - if(project.hasProperty(name)) { - return project.property(name) - } - logger.info("Project property '${name}' not available. Please it to ~/.gradle/gradle.properties") - return null -} - publishing { publications { mavenJava(MavenPublication) { @@ -150,22 +137,12 @@ publishing { } } } - - repositories { - maven { - def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" - url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl - allowInsecureProtocol = false - credentials(PasswordCredentials) { - username = getOptionalProperty("ossrhUsername") - password = getOptionalProperty("ossrhPassword") - } - } - } } signing { + def signingKey = findProperty("signingKey") + def signingPassword = findProperty("signingPassword") + useInMemoryPgpKeys(signingKey, signingPassword) sign publishing.publications.mavenJava } @@ -174,8 +151,6 @@ nexusPublishing { repositories { sonatype { stagingProfileId = "546ea6ce74787e" - username = getOptionalProperty("ossrhUsername") - password = getOptionalProperty("ossrhPassword") } } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e644113..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23..9355b41 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30d..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/settings.gradle b/settings.gradle index 14aea87..6d3b97d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,8 @@ -rootProject.name = 'simple-jdbc' +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} +rootProject.name = 'simple-jdbc' dependencyResolutionManagement { repositories { @@ -7,18 +10,18 @@ dependencyResolutionManagement { } versionCatalogs { libs { - library('assertj', 'org.assertj:assertj-core:3.25.3') - library('h2', 'com.h2database:h2:2.2.224') + library('assertj', 'org.assertj:assertj-core:3.26.3') + library('h2', 'com.h2database:h2:2.3.232') library('junitPioneer', 'org.junit-pioneer:junit-pioneer:2.2.0') - library('equalsverifier', 'nl.jqno.equalsverifier:equalsverifier:3.16.1') + library('equalsverifier', 'nl.jqno.equalsverifier:equalsverifier:3.16.2') library('tostringverifier', 'com.jparams:to-string-verifier:1.4.8') - library('hamcrest', 'org.hamcrest:hamcrest-all:1.3') + library('hamcrest', 'org.hamcrest:hamcrest:3.0') library('hamcrestResultSetMatcher', 'com.exasol:hamcrest-resultset-matcher:1.6.3') library('mockito', 'org.mockito:mockito-core:5.11.0') - library('mockitoJunit', 'org.mockito:mockito-junit-jupiter:5.11.0') - library('slf4jLogger', 'org.slf4j:slf4j-jdk14:2.0.13') - library('exasolJdbc', 'com.exasol:exasol-jdbc:24.1.0') - library('exasolTestcontainers', 'com.exasol:exasol-testcontainers:7.1.0') + library('mockitoJunit', 'org.mockito:mockito-junit-jupiter:5.13.0') + library('slf4jLogger', 'org.slf4j:slf4j-jdk14:2.0.16') + library('exasolJdbc', 'com.exasol:exasol-jdbc:24.1.2') + library('exasolTestcontainers', 'com.exasol:exasol-testcontainers:7.1.1') } } } diff --git a/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java b/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java index 920a666..e2f6ee1 100644 --- a/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java +++ b/src/integrationTest/java/org/itsallcode/jdbc/ExasolTypeTest.java @@ -22,7 +22,7 @@ class ExasolTypeTest { - private static final ExasolContainer container = new ExasolContainer<>("8.27.0") + private static final ExasolContainer container = new ExasolContainer<>("8.29.1") .withRequiredServices(ExasolService.JDBC).withReuse(true); @BeforeAll @@ -67,8 +67,10 @@ void genericRowNullValue(final TypeTest test) { final ColumnValue value = result.toList().get(0).get(0); assertAll( () -> assertThat(value.value()).isNull(), - () -> assertThat(value.type().jdbcType()).as("jdbc type").isEqualTo(test.expectedType()), - () -> assertThat(value.type().typeName()).as("type name").isEqualTo(test.expectedTypeName()), + () -> assertThat(value.type().jdbcType()).as("jdbc type") + .isEqualTo(test.expectedType()), + () -> assertThat(value.type().typeName()).as("type name") + .isEqualTo(test.expectedTypeName()), () -> assertThat(value.type().className()).as("type class name") .isEqualTo(test.expectedClassName())); } @@ -115,7 +117,8 @@ static Stream testTypes() { typeTest("123", "SHORTINT", 123, JDBCType.INTEGER, "INTEGER", Integer.class), typeTest("123", "SMALLINT", 123, JDBCType.INTEGER, "INTEGER", Integer.class), typeTest("123", "TINYINT", (short) 123, JDBCType.SMALLINT, "SMALLINT", Short.class), - typeTest("123", "DECIMAL(4,0)", (short) 123, JDBCType.SMALLINT, "SMALLINT", Short.class), + typeTest("123", "DECIMAL(4,0)", (short) 123, JDBCType.SMALLINT, "SMALLINT", + Short.class), typeTest("123", "DECIMAL(5,0)", 123, JDBCType.INTEGER, "INTEGER", Integer.class), typeTest("123", "DECIMAL(9,0)", 123, JDBCType.INTEGER, "INTEGER", Integer.class), typeTest("123", "DECIMAL(10,0)", 123L, JDBCType.BIGINT, "BIGINT", Long.class), @@ -127,7 +130,8 @@ static Stream testTypes() { BigDecimal.class), typeTest("123", "DEC", 123L, JDBCType.BIGINT, "BIGINT", Long.class), typeTest("123", "NUMERIC", 123L, JDBCType.BIGINT, "BIGINT", Long.class), - typeTest("123.457", "DECIMAL(6,3)", BigDecimal.valueOf(123.457d), JDBCType.DECIMAL, "DECIMAL", + typeTest("123.457", "DECIMAL(6,3)", BigDecimal.valueOf(123.457d), JDBCType.DECIMAL, + "DECIMAL", BigDecimal.class), typeTest("123.458", "DOUBLE", 123.458d, JDBCType.DOUBLE, "DOUBLE PRECISION", Double.class), diff --git a/src/main/java/org/itsallcode/jdbc/ConnectionFactory.java b/src/main/java/org/itsallcode/jdbc/ConnectionFactory.java index 9e33b3c..d656859 100644 --- a/src/main/java/org/itsallcode/jdbc/ConnectionFactory.java +++ b/src/main/java/org/itsallcode/jdbc/ConnectionFactory.java @@ -6,7 +6,7 @@ /** * This class connects to a database and returns new {@link SimpleConnection}s. */ -public class ConnectionFactory { +public final class ConnectionFactory { private final Context context; private final DbDialectFactory dialectFactory; @@ -70,7 +70,7 @@ public SimpleConnection create(final String url, final Properties info) { return new SimpleConnection(createConnection(url, info), context, dialectFactory.createDialect(url)); } - private Connection createConnection(final String url, final Properties info) { + private static Connection createConnection(final String url, final Properties info) { try { return DriverManager.getConnection(url, info); } catch (final SQLException e) { diff --git a/src/main/java/org/itsallcode/jdbc/Context.java b/src/main/java/org/itsallcode/jdbc/Context.java index 7c32505..fdca903 100644 --- a/src/main/java/org/itsallcode/jdbc/Context.java +++ b/src/main/java/org/itsallcode/jdbc/Context.java @@ -3,7 +3,7 @@ /** * This represents a context with configuration for the Simple JDBC framework. */ -public class Context { +public final class Context { private Context() { } @@ -13,6 +13,7 @@ private Context() { * * @return parameter mapper */ + @SuppressWarnings("java:S2325") // Not-static by intention public ParameterMapper getParameterMapper() { return ParameterMapper.create(); } @@ -29,7 +30,7 @@ public static ContextBuilder builder() { /** * A builder for {@link Context} objects. */ - public static class ContextBuilder { + public static final class ContextBuilder { private ContextBuilder() { } @@ -39,6 +40,7 @@ private ContextBuilder() { * * @return a new context */ + @SuppressWarnings("java:S2325") // Not-static by intention public Context build() { return new Context(); } diff --git a/src/main/java/org/itsallcode/jdbc/DbDialectFactory.java b/src/main/java/org/itsallcode/jdbc/DbDialectFactory.java index bed7678..892d0c0 100644 --- a/src/main/java/org/itsallcode/jdbc/DbDialectFactory.java +++ b/src/main/java/org/itsallcode/jdbc/DbDialectFactory.java @@ -8,7 +8,7 @@ class DbDialectFactory { public DbDialect createDialect(final String url) { final ServiceLoader serviceLoader = ServiceLoader.load(DbDialect.class, - DbDialect.class.getClassLoader()); + Thread.currentThread().getContextClassLoader()); return serviceLoader.stream() .map(Provider::get) .filter(dialect -> dialect.supportsUrl(url)) diff --git a/src/main/java/org/itsallcode/jdbc/ParamConverter.java b/src/main/java/org/itsallcode/jdbc/ParamConverter.java index 7fd9c52..468203d 100644 --- a/src/main/java/org/itsallcode/jdbc/ParamConverter.java +++ b/src/main/java/org/itsallcode/jdbc/ParamConverter.java @@ -21,7 +21,7 @@ public interface ParamConverter { * * @return a new identity parameter converter */ - public static ParamConverter identity() { + static ParamConverter identity() { return row -> row; } } diff --git a/src/main/java/org/itsallcode/jdbc/ParameterMapper.java b/src/main/java/org/itsallcode/jdbc/ParameterMapper.java index a74d410..b3db6ce 100644 --- a/src/main/java/org/itsallcode/jdbc/ParameterMapper.java +++ b/src/main/java/org/itsallcode/jdbc/ParameterMapper.java @@ -11,17 +11,17 @@ * This class converts parameters before setting them for a prepared statement, * e.g. in {@link PreparedStatementSetter}. */ -public class ParameterMapper { - private final Map, Mapper> mappers; +public final class ParameterMapper { + private final Map> mappers; - private ParameterMapper(final Map, Mapper> mappers) { - this.mappers = mappers; + private ParameterMapper(final Map> mappers) { + this.mappers = new HashMap<>(mappers); } /** * Create a new mapper with predefined converters for date time types. * - * @return a preconfigured mapper + * @return a pre-configured mapper */ public static ParameterMapper create() { final List> mappers = new ArrayList<>(); @@ -32,13 +32,17 @@ public static ParameterMapper create() { mappers.add(createMapper(Instant.class, o -> instantFormatter.format(LocalDateTime.ofInstant(o, utc)))); mappers.add(createMapper(LocalDateTime.class, instantFormatter::format)); - return new ParameterMapper(mappers.stream().collect(toMap(Mapper::getType, Function.identity()))); + return new ParameterMapper(mappers.stream().collect(toMap(Mapper::getTypeName, Function.identity()))); } private static Mapper createMapper(final Class type, final Function mapper) { return new Mapper<>(type, mapper); } + private Optional> getMapper(final Class type) { + return Optional.ofNullable(mappers.get(type.getName())); + } + /** * Converts a single value. * @@ -49,10 +53,9 @@ public Object map(final Object value) { if (value == null) { return null; } - if (mappers.containsKey(value.getClass())) { - return mappers.get(value.getClass()).map(value); - } - return value; + return getMapper(value.getClass()) + .map(m -> m.map(value)) + .orElse(value); } private static class Mapper { @@ -68,8 +71,8 @@ Object map(final Object value) { return mapperFunction.apply(type.cast(value)); } - Class getType() { - return type; + String getTypeName() { + return type.getName(); } } } diff --git a/src/main/java/org/itsallcode/jdbc/SimpleBatch.java b/src/main/java/org/itsallcode/jdbc/SimpleBatch.java index e9b00d5..2c433ce 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleBatch.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleBatch.java @@ -9,14 +9,14 @@ class SimpleBatch implements AutoCloseable { private static final Logger LOG = Logger.getLogger(SimpleBatch.class.getName()); - private static final int BATCH_SIZE = 200000; + private static final int BATCH_SIZE = 200_000; private final SimplePreparedStatement statement; private final Context context; private final List parameterMetadata; - private int rows = 0; - private int currentBatchSize = 0; + private int rows; + private int currentBatchSize; SimpleBatch(final SimplePreparedStatement statement, final Context context) { this.statement = Objects.requireNonNull(statement, "statement"); @@ -24,11 +24,13 @@ class SimpleBatch implements AutoCloseable { this.parameterMetadata = statement.getParameterMetadata().parameters(); } + @SuppressWarnings("java:S923") // Varargs required SimpleBatch add(final Object... args) { validateParameters(args); return add(new ArgumentPreparedStatementSetter(context.getParameterMapper(), args)); } + @SuppressWarnings("java:S923") // Varargs required private void validateParameters(final Object... args) { if (args.length != this.parameterMetadata.size()) { throw new IllegalStateException( @@ -41,7 +43,8 @@ private SimpleBatch add(final PreparedStatementSetter preparedStatementSetter) { statement.setValues(preparedStatementSetter); statement.addBatch(); currentBatchSize++; - if (++rows % BATCH_SIZE == 0) { + rows++; + if (rows % BATCH_SIZE == 0) { executeBatch(); } return this; diff --git a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java index c621081..85c241d 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleConnection.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleConnection.java @@ -130,7 +130,7 @@ public void insert(final Identifier table, final List columnName insert(createInsertStatement(table, columnNames), rowMapper, rows); } - private String createInsertStatement(final Identifier table, final List columnNames) { + private static String createInsertStatement(final Identifier table, final List columnNames) { final String columns = columnNames.stream().map(Identifier::quote).collect(joining(",")); final String placeholders = columnNames.stream().map(n -> "?").collect(joining(",")); return "insert into " + table.quote() + " (" + columns + ") values (" + placeholders + ")"; diff --git a/src/main/java/org/itsallcode/jdbc/SimpleParameterMetaData.java b/src/main/java/org/itsallcode/jdbc/SimpleParameterMetaData.java index b022847..5f59935 100644 --- a/src/main/java/org/itsallcode/jdbc/SimpleParameterMetaData.java +++ b/src/main/java/org/itsallcode/jdbc/SimpleParameterMetaData.java @@ -41,11 +41,11 @@ public enum ParameterMode { /** Parameter mode OUT */ OUT(ParameterMetaData.parameterModeOut), /** Parameter mode is unknown */ - UNKNWON(ParameterMetaData.parameterModeUnknown); + UNKNOWN(ParameterMetaData.parameterModeUnknown); private final int mode; - private ParameterMode(final int mode) { + ParameterMode(final int mode) { this.mode = mode; } @@ -71,7 +71,7 @@ public enum ParameterNullable { private final int mode; - private ParameterNullable(final int mode) { + ParameterNullable(final int mode) { this.mode = mode; } diff --git a/src/main/java/org/itsallcode/jdbc/dialect/ColumnValueExtractor.java b/src/main/java/org/itsallcode/jdbc/dialect/ColumnValueExtractor.java index e8233bb..6b857c2 100644 --- a/src/main/java/org/itsallcode/jdbc/dialect/ColumnValueExtractor.java +++ b/src/main/java/org/itsallcode/jdbc/dialect/ColumnValueExtractor.java @@ -17,4 +17,4 @@ public interface ColumnValueExtractor { * @throws SQLException if reading the result set fails */ Object getObject(ResultSet resultSet, int columnIndex) throws SQLException; -} \ No newline at end of file +} diff --git a/src/main/java/org/itsallcode/jdbc/dialect/Extractors.java b/src/main/java/org/itsallcode/jdbc/dialect/Extractors.java index 85948cd..c5a97c1 100644 --- a/src/main/java/org/itsallcode/jdbc/dialect/Extractors.java +++ b/src/main/java/org/itsallcode/jdbc/dialect/Extractors.java @@ -4,7 +4,7 @@ import java.util.Calendar; import java.util.TimeZone; -class Extractors { +final class Extractors { private Extractors() { } @@ -23,6 +23,7 @@ static ColumnValueExtractor dateToLocalDate() { return nonNull((resultSet, columnIndex) -> resultSet.getDate(columnIndex, utcCalendar).toLocalDate()); } + @SuppressWarnings("java:S2143") // Need to use calendar api private static Calendar createUtcCalendar() { return Calendar.getInstance(TimeZone.getTimeZone("UTC")); } diff --git a/src/main/java/org/itsallcode/jdbc/identifier/Identifier.java b/src/main/java/org/itsallcode/jdbc/identifier/Identifier.java index 9741c40..f300392 100644 --- a/src/main/java/org/itsallcode/jdbc/identifier/Identifier.java +++ b/src/main/java/org/itsallcode/jdbc/identifier/Identifier.java @@ -32,6 +32,7 @@ static Identifier simple(final String id) { * @param id parts of the ID * @return a new {@link QualifiedIdentifier} */ + @SuppressWarnings("java:S923") // Varargs required static Identifier qualified(final String... id) { return QualifiedIdentifier.of(Arrays.stream(id).map(SimpleIdentifier::of).toArray(SimpleIdentifier[]::new)); } diff --git a/src/main/java/org/itsallcode/jdbc/identifier/QualifiedIdentifier.java b/src/main/java/org/itsallcode/jdbc/identifier/QualifiedIdentifier.java index 4aa2278..753c673 100644 --- a/src/main/java/org/itsallcode/jdbc/identifier/QualifiedIdentifier.java +++ b/src/main/java/org/itsallcode/jdbc/identifier/QualifiedIdentifier.java @@ -16,6 +16,7 @@ record QualifiedIdentifier(List id) implements Identifier { * @param ids the IDs * @return a new instance */ + @SuppressWarnings("java:S923") // Varargs required public static Identifier of(final Identifier... ids) { return new QualifiedIdentifier(asList(ids)); } diff --git a/src/main/java/org/itsallcode/jdbc/resultset/ContextRowMapper.java b/src/main/java/org/itsallcode/jdbc/resultset/ContextRowMapper.java index d126a98..beec393 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/ContextRowMapper.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/ContextRowMapper.java @@ -34,7 +34,7 @@ public interface ContextRowMapper { * @param dialect DB dialect * @return a new row mapper */ - public static RowMapper generic(final DbDialect dialect) { + static RowMapper generic(final DbDialect dialect) { return generic(dialect, row -> row); } @@ -45,7 +45,7 @@ public static RowMapper generic(final DbDialect dialect) { * @param dialect DB dialect * @return a new row mapper */ - public static RowMapper> columnValueList(final DbDialect dialect) { + static RowMapper> columnValueList(final DbDialect dialect) { return generic(dialect, row -> row.columnValues().stream().map(ColumnValue::value).toList()); } @@ -62,7 +62,7 @@ private static RowMapper generic(final DbDialect dialect, final ColumnVal * @param mapper the simple row mapper * @return a new {@link ContextRowMapper} */ - public static ContextRowMapper create(final RowMapper mapper) { + static ContextRowMapper create(final RowMapper mapper) { return (context, resultSet, rowNum) -> mapper.mapRow(resultSet, rowNum); } } diff --git a/src/main/java/org/itsallcode/jdbc/resultset/ConvertingResultSet.java b/src/main/java/org/itsallcode/jdbc/resultset/ConvertingResultSet.java index 78f6627..befd7aa 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/ConvertingResultSet.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/ConvertingResultSet.java @@ -1,7 +1,5 @@ package org.itsallcode.jdbc.resultset; -import static java.util.stream.Collectors.toList; - import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; @@ -19,7 +17,7 @@ *
  • {@link ResultSet#getObject(int, Class)}
  • * */ -public class ConvertingResultSet extends DelegatingResultSet { +public final class ConvertingResultSet extends DelegatingResultSet { private final ResultSet delegate; private final ResultSetValueConverter converter; @@ -40,7 +38,7 @@ public static ConvertingResultSet create(final DbDialect dialect, final ResultSe final SimpleMetaData metaData = SimpleMetaData.create(delegate); final List converters = metaData.columns().stream() .map(col -> ColumnValueConverter.simple(dialect.createExtractor(col))) - .collect(toList()); + .toList(); return new ConvertingResultSet(delegate, ResultSetValueConverter.create(metaData, converters)); } diff --git a/src/main/java/org/itsallcode/jdbc/resultset/DelegatingResultSet.java b/src/main/java/org/itsallcode/jdbc/resultset/DelegatingResultSet.java index c303997..6d02e70 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/DelegatingResultSet.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/DelegatingResultSet.java @@ -8,6 +8,7 @@ import java.util.Calendar; import java.util.Map; +@SuppressWarnings("java:S1448") // Long file required for implementing ResultSet class DelegatingResultSet implements ResultSet { private final ResultSet delegate; @@ -88,7 +89,7 @@ public double getDouble(final int columnIndex) throws SQLException { * {@code getBigDecimal(String columnLabel)} */ @Override - @Deprecated + @Deprecated(since = "0.7.1") public BigDecimal getBigDecimal(final int columnIndex, final int scale) throws SQLException { return delegate.getBigDecimal(columnIndex, scale); } @@ -125,7 +126,7 @@ public InputStream getAsciiStream(final int columnIndex) throws SQLException { * {@code getUnicodeStream} */ @Override - @Deprecated + @Deprecated(since = "0.7.1") public InputStream getUnicodeStream(final int columnIndex) throws SQLException { return delegate.getUnicodeStream(columnIndex); } @@ -182,7 +183,7 @@ public double getDouble(final String columnLabel) throws SQLException { * {@code getBigDecimal(String columnLabel)} */ @Override - @Deprecated + @Deprecated(since = "0.7.1") public BigDecimal getBigDecimal(final String columnLabel, final int scale) throws SQLException { return delegate.getBigDecimal(columnLabel, scale); } @@ -218,7 +219,7 @@ public InputStream getAsciiStream(final String columnLabel) throws SQLException * @deprecated use {@code getCharacterStream} instead */ @Override - @Deprecated + @Deprecated(since = "0.7.1") public InputStream getUnicodeStream(final String columnLabel) throws SQLException { return delegate.getUnicodeStream(columnLabel); } diff --git a/src/main/java/org/itsallcode/jdbc/resultset/ResultSetValueConverter.java b/src/main/java/org/itsallcode/jdbc/resultset/ResultSetValueConverter.java index d619a44..81c6ef3 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/ResultSetValueConverter.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/ResultSetValueConverter.java @@ -10,14 +10,14 @@ /** * Helper class used by {@link ConvertingResultSet}. */ -class ResultSetValueConverter { +final class ResultSetValueConverter { private final Map convertersByIndex; private final Map columnIndexByLabel; private ResultSetValueConverter(final Map convertersByIndex, final Map columnIndexByLabel) { - this.convertersByIndex = convertersByIndex; - this.columnIndexByLabel = columnIndexByLabel; + this.convertersByIndex = new HashMap<>(convertersByIndex); + this.columnIndexByLabel = new HashMap<>(columnIndexByLabel); } static ResultSetValueConverter create(final SimpleMetaData resultSetMetadata, diff --git a/src/main/java/org/itsallcode/jdbc/resultset/SimpleResultSet.java b/src/main/java/org/itsallcode/jdbc/resultset/SimpleResultSet.java index 94aff09..b11b671 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/SimpleResultSet.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/SimpleResultSet.java @@ -3,7 +3,8 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; -import java.util.stream.*; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.itsallcode.jdbc.Context; import org.itsallcode.jdbc.UncheckedSQLException; @@ -36,7 +37,7 @@ public SimpleResultSet(final Context context, final ResultSet resultSet, final C /** * Get in {@link Iterator} of all rows. * - * @return an interator with all rows. + * @return an iterator with all rows. */ @Override public Iterator iterator() { @@ -53,7 +54,7 @@ public Iterator iterator() { * @return a list with all rows. */ public List toList() { - return stream().collect(Collectors.toList()); + return stream().toList(); } /** @@ -89,12 +90,12 @@ private boolean next() { } } - private static class ResultSetIterator implements Iterator { + private static final class ResultSetIterator implements Iterator { private final Context context; private final SimpleResultSet resultSet; private final ContextRowMapper rowMapper; private boolean hasNext; - private int currentRowIndex = 0; + private int currentRowIndex; private ResultSetIterator(final Context context, final SimpleResultSet simpleResultSet, final ContextRowMapper rowMapper, diff --git a/src/main/java/org/itsallcode/jdbc/resultset/generic/ColumnMetaData.java b/src/main/java/org/itsallcode/jdbc/resultset/generic/ColumnMetaData.java index 220f176..f290633 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/generic/ColumnMetaData.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/generic/ColumnMetaData.java @@ -41,4 +41,4 @@ private static ColumnMetaData create(final ResultSetMetaData metaData, final int final ColumnType columnType = ColumnType.create(metaData, columnIndex); return new ColumnMetaData(columnIndex, name, label, columnType); } -} \ No newline at end of file +} diff --git a/src/main/java/org/itsallcode/jdbc/resultset/generic/GenericRowMapper.java b/src/main/java/org/itsallcode/jdbc/resultset/generic/GenericRowMapper.java index 78924a9..1008f10 100644 --- a/src/main/java/org/itsallcode/jdbc/resultset/generic/GenericRowMapper.java +++ b/src/main/java/org/itsallcode/jdbc/resultset/generic/GenericRowMapper.java @@ -39,7 +39,7 @@ public T mapRow(final ResultSet resultSet, final int rowNum) throws SQLException return converter.mapRow(row); } - private class ResultSetRowBuilder { + private static final class ResultSetRowBuilder { private final SimpleMetaData metadata; private ResultSetRowBuilder(final SimpleMetaData metaData) { @@ -56,12 +56,13 @@ private Row buildRow(final ResultSet resultSet, final int rowIndex) { return new Row(rowIndex, columns, fields); } - private ColumnValue getField(final ResultSet resultSet, final ColumnMetaData column, final int rowIndex) { + private static ColumnValue getField(final ResultSet resultSet, final ColumnMetaData column, + final int rowIndex) { final Object value = getValue(resultSet, column, rowIndex); return new ColumnValue(column.type(), value); } - private Object getValue(final ResultSet resultSet, final ColumnMetaData column, final int rowIndex) { + private static Object getValue(final ResultSet resultSet, final ColumnMetaData column, final int rowIndex) { try { return resultSet.getObject(column.columnIndex()); } catch (final SQLException e) { diff --git a/src/test/java/org/itsallcode/jdbc/ParameterMapperTest.java b/src/test/java/org/itsallcode/jdbc/ParameterMapperTest.java new file mode 100644 index 0000000..3a23850 --- /dev/null +++ b/src/test/java/org/itsallcode/jdbc/ParameterMapperTest.java @@ -0,0 +1,32 @@ +package org.itsallcode.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.*; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ParameterMapperTest { + + static Stream mappedTypes() { + return Stream.of(mapType(null, null), mapType("test", "test"), mapType(1, 1), mapType(1L, 1L), + mapType(1.0, 1.0), mapType(1.0f, 1.0f), mapType(true, true), mapType(false, false), + mapType(LocalDate.of(2024, 9, 1), "2024-09-01"), + mapType(Instant.parse("2007-12-03T10:15:30.00Z"), "2007-12-03 10:15:30.000"), + mapType(LocalDateTime.parse("2007-12-03T10:15:30"), "2007-12-03 10:15:30.000")); + } + + static Arguments mapType(final Object input, final Object expected) { + return Arguments.of(input, expected); + } + + @ParameterizedTest + @MethodSource("mappedTypes") + void map(final Object input, final Object expected) { + final Object actual = ParameterMapper.create().map(input); + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java index 549f492..689e6e9 100644 --- a/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java +++ b/src/test/java/org/itsallcode/jdbc/SimpleConnectionITest.java @@ -1,6 +1,5 @@ package org.itsallcode.jdbc; -import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; @@ -35,7 +34,7 @@ void executeStatementFails() { .isInstanceOf(UncheckedSQLException.class) .hasMessage( "Error executing 'select count(*) from missingtable': Table \"MISSINGTABLE\" not found (this database is empty); SQL statement:\n" - + "select count(*) from missingtable [42104-224]") + + "select count(*) from missingtable [42104-232]") .hasCauseInstanceOf(SQLException.class); } } @@ -56,7 +55,7 @@ void executeQueryFails() { () -> connection.query("select count(*) from missingtable")) .isInstanceOf(UncheckedSQLException.class).hasMessage( "Error preparing statement 'select count(*) from missingtable': Table \"MISSINGTABLE\" not found (this database is empty); SQL statement:\n" - + "select count(*) from missingtable [42104-224]"); + + "select count(*) from missingtable [42104-232]"); } } @@ -66,7 +65,7 @@ void executeQueryWithGenericRowMapper() { connection.executeScript("CREATE TABLE TEST(ID INT, NAME VARCHAR(255));" + "insert into test (id, name) values (1, 'test');"); try (SimpleResultSet resultSet = connection.query("select count(*) as result from test")) { - final List rows = resultSet.stream().collect(toList()); + final List rows = resultSet.stream().toList(); assertAll( () -> assertThat(rows).hasSize(1), () -> assertThat(rows.get(0).rowIndex()).isZero(), diff --git a/src/test/java/org/itsallcode/jdbc/resultset/DelegatingResultSetTest.java b/src/test/java/org/itsallcode/jdbc/resultset/DelegatingResultSetTest.java index 810df2e..74bdb90 100644 --- a/src/test/java/org/itsallcode/jdbc/resultset/DelegatingResultSetTest.java +++ b/src/test/java/org/itsallcode/jdbc/resultset/DelegatingResultSetTest.java @@ -1,7 +1,6 @@ package org.itsallcode.jdbc.resultset; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.*; @@ -36,13 +35,13 @@ void unwrap() throws SQLException { @Test void isWrapperFor() throws SQLException { when(resultSetMock.isWrapperFor(String.class)).thenReturn(true); - assertEquals(true, testee().isWrapperFor(String.class)); + assertTrue(testee().isWrapperFor(String.class)); } @Test void next() throws SQLException { when(resultSetMock.next()).thenReturn(true); - assertEquals(true, testee().next()); + assertTrue(testee().next()); } @Test @@ -54,7 +53,7 @@ void close() throws SQLException { @Test void wasNull() throws SQLException { when(resultSetMock.wasNull()).thenReturn(true); - assertEquals(true, testee().wasNull()); + assertTrue(testee().wasNull()); } @Test @@ -66,7 +65,7 @@ void getStringIndex() throws SQLException { @Test void getBooleanIndex() throws SQLException { when(resultSetMock.getBoolean(1)).thenReturn(true); - assertEquals(true, testee().getBoolean(1)); + assertTrue(testee().getBoolean(1)); } @Test @@ -169,7 +168,7 @@ void getStringLabel() throws SQLException { @Test void getBoolean() throws SQLException { when(resultSetMock.getBoolean("a")).thenReturn(true); - assertEquals(true, testee().getBoolean("a")); + assertTrue(testee().getBoolean("a")); } @Test @@ -338,25 +337,25 @@ void getBigDecimalLabel() throws SQLException { @Test void isBeforeFirst() throws SQLException { when(resultSetMock.isBeforeFirst()).thenReturn(true); - assertEquals(true, testee().isBeforeFirst()); + assertTrue(testee().isBeforeFirst()); } @Test void isAfterLast() throws SQLException { when(resultSetMock.isAfterLast()).thenReturn(true); - assertEquals(true, testee().isAfterLast()); + assertTrue(testee().isAfterLast()); } @Test void isFirst() throws SQLException { when(resultSetMock.isFirst()).thenReturn(true); - assertEquals(true, testee().isFirst()); + assertTrue(testee().isFirst()); } @Test void isLast() throws SQLException { when(resultSetMock.isLast()).thenReturn(true); - assertEquals(true, testee().isLast()); + assertTrue(testee().isLast()); } @Test @@ -374,13 +373,13 @@ void afterLast() throws SQLException { @Test void first() throws SQLException { when(resultSetMock.first()).thenReturn(true); - assertEquals(true, testee().first()); + assertTrue(testee().first()); } @Test void last() throws SQLException { when(resultSetMock.last()).thenReturn(true); - assertEquals(true, testee().last()); + assertTrue(testee().last()); } @Test @@ -392,19 +391,19 @@ void getRow() throws SQLException { @Test void absolute() throws SQLException { when(resultSetMock.absolute(1)).thenReturn(true); - assertEquals(true, testee().absolute(1)); + assertTrue(testee().absolute(1)); } @Test void relative() throws SQLException { when(resultSetMock.relative(1)).thenReturn(true); - assertEquals(true, testee().relative(1)); + assertTrue(testee().relative(1)); } @Test void previous() throws SQLException { when(resultSetMock.previous()).thenReturn(true); - assertEquals(true, testee().previous()); + assertTrue(testee().previous()); } @Test @@ -446,19 +445,19 @@ void getConcurrency() throws SQLException { @Test void rowUpdated() throws SQLException { when(resultSetMock.rowUpdated()).thenReturn(true); - assertEquals(true, testee().rowUpdated()); + assertTrue(testee().rowUpdated()); } @Test void rowInserted() throws SQLException { when(resultSetMock.rowInserted()).thenReturn(true); - assertEquals(true, testee().rowInserted()); + assertTrue(testee().rowInserted()); } @Test void rowDeleted() throws SQLException { when(resultSetMock.rowDeleted()).thenReturn(true); - assertEquals(true, testee().rowDeleted()); + assertTrue(testee().rowDeleted()); } @Test @@ -988,7 +987,7 @@ void getHoldability() throws SQLException { @Test void isClosed() throws SQLException { when(resultSetMock.isClosed()).thenReturn(true); - assertEquals(true, testee().isClosed()); + assertTrue(testee().isClosed()); } @Test