diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 00000000000..0c4b142e9a7 --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false diff --git a/.github/workflows/auto-cherry-pick.yml b/.github/workflows/auto-cherry-pick.yml index 4a9c4479efd..6ba14dde29d 100644 --- a/.github/workflows/auto-cherry-pick.yml +++ b/.github/workflows/auto-cherry-pick.yml @@ -8,6 +8,6 @@ on: jobs: cherry-pick-commit: - uses: spring-io/spring-github-workflows/.github/workflows/spring-cherry-pick.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-cherry-pick.yml@v5 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/backport-issue.yml b/.github/workflows/backport-issue.yml index ae3ea05130e..71e42771d5c 100644 --- a/.github/workflows/backport-issue.yml +++ b/.github/workflows/backport-issue.yml @@ -7,6 +7,6 @@ on: jobs: backport-issue: - uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@v5 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index a94396f7729..a21192cd2cb 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -21,6 +21,6 @@ jobs: with: gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} secrets: - GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 1771c582651..2065ee71873 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -16,4 +16,4 @@ permissions: jobs: dispatch-docs-build: if: github.repository_owner == 'spring-projects' - uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@v5 diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml index 743781a0dc3..f513c72567a 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -12,6 +12,7 @@ jobs: merge-dependabot-pr: permissions: write-all - uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v5 with: - mergeArguments: --auto --squash \ No newline at end of file + mergeArguments: --auto --squash + autoMergeSnapshots: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7e832bdef2..90f17f1609d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,14 +15,13 @@ jobs: uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - OSSRH_URL: ${{ secrets.OSSRH_URL }} OSSRH_S01_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} OSSRH_S01_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} OSSRH_STAGING_PROFILE_NAME: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - SPRING_RELEASE_SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }} \ No newline at end of file + SPRING_RELEASE_CHAT_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index baee455d611..eceb0143c7e 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -9,7 +9,7 @@ on: type: string env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index 1d9a4e2713a..526641b6007 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -24,12 +24,10 @@ Is there already an issue that addresses your concern? Search the https://github.com/spring-projects/spring-integration/issues[GitHub issue tracker] to see if you can find something similar. If not, please create a new issue in GitHub before submitting a pull request unless the change is truly trivial, e.g. typo fixes, removing compiler warnings, etc. -== Sign the contributor license agreement +== Developer Certificate of Origin -If you have not previously done so, please fill out and submit the https://cla.pivotal.io/sign/spring[Contributor License Agreement (CLA)]. - -Very important, before we can accept any *Spring Integration contributions*, we will need you to sign the CLA. -Signing the CLA does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. +All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin. +For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring]. == Fork the Repository diff --git a/build.gradle b/build.gradle index 09f0a2e2ebc..ef3f4f6e9be 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.9.24' + ext.kotlinVersion = '1.9.25' ext.isCI = System.getenv('GITHUB_ACTION') repositories { gradlePluginPortal() @@ -18,12 +18,13 @@ plugins { id 'base' id 'io.spring.nohttp' version '0.0.11' apply false id 'org.ajoberstar.grgit' version '5.2.2' - id 'io.spring.dependency-management' version '1.1.5' + id 'io.spring.dependency-management' version '1.1.7' id 'org.jetbrains.dokka' version '1.9.20' id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.14' - id 'com.google.protobuf' version '0.9.4' apply false + id 'com.github.spotbugs' version '6.0.28' + id 'com.google.protobuf' version '0.9.5' apply false + id 'io.freefair.aggregate-javadoc' version '8.6' } if (isCI) { @@ -55,71 +56,71 @@ ext { apacheSshdVersion = '2.12.1' artemisVersion = '2.33.0' - aspectjVersion = '1.9.22.1' + aspectjVersion = '1.9.24' assertjVersion = '3.25.3' assertkVersion = '0.28.1' - avroVersion = '1.11.3' - awaitilityVersion = '4.2.1' + avroVersion = '1.11.4' + awaitilityVersion = '4.2.2' camelVersion = '4.5.0' commonsDbcp2Version = '2.12.0' commonsIoVersion = '2.16.1' commonsNetVersion = '3.10.0' curatorVersion = '5.6.0' - debeziumVersion = '2.6.1.Final' + debeziumVersion = '2.6.2.Final' derbyVersion = '10.16.1.1' - ftpServerVersion = '1.2.0' - graalvmVersion = '23.1.3' - greenmailVersion = '2.1.0-alpha-4' - groovyVersion = '4.0.21' + ftpServerVersion = '1.2.1' + graalvmVersion = '23.1.7' + greenmailVersion = '2.1.3' + groovyVersion = '4.0.27' hamcrestVersion = '2.2' hazelcastVersion = '5.4.0' - hibernateVersion = '6.4.8.Final' - hsqldbVersion = '2.7.2' + hibernateVersion = '6.4.10.Final' + hsqldbVersion = '2.7.4' h2Version = '2.2.224' - jacksonVersion = '2.17.1' + jacksonVersion = '2.17.3' jaxbVersion = '4.0.5' - jcifsVersion = '2.1.37' + jcifsVersion = '2.1.39' jeroMqVersion = '0.6.0' jmsApiVersion = '3.1.0' jpaApiVersion = '3.1.0' - jrubyVersion = '9.4.7.0' + jrubyVersion = '9.4.13.0' jsonpathVersion = '2.9.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.10.2' - jythonVersion = '2.7.3' + junitJupiterVersion = '5.10.5' + jythonVersion = '2.7.4' kotlinCoroutinesVersion = '1.8.1' - kryoVersion = '5.6.0' + kryoVersion = '5.6.2' lettuceVersion = '6.3.2.RELEASE' log4jVersion = '2.23.1' mailVersion = '2.0.3' - micrometerTracingVersion = '1.3.0' - micrometerVersion = '1.13.0' + micrometerTracingVersion = '1.3.13' + micrometerVersion = '1.13.15' mockitoVersion = '5.11.0' mongoDriverVersion = '5.0.1' mysqlVersion = '8.3.0' oracleVersion = '23.3.0.23.09' pahoMqttClientVersion = '1.2.5' - postgresVersion = '42.7.3' + postgresVersion = '42.7.7' protobufVersion = '4.26.1' r2dbch2Version = '1.0.0.RELEASE' - reactorVersion = '2023.0.6' + reactorVersion = '2023.0.19' resilience4jVersion = '2.2.0' romeToolsVersion = '2.1.0' - rsocketVersion = '1.1.4' + rsocketVersion = '1.1.5' servletApiVersion = '6.0.0' smackVersion = '4.4.8' - springAmqpVersion = '3.1.5' - springDataVersion = '2024.0.0' - springGraphqlVersion = '1.3.0' - springKafkaVersion = '3.2.0' - springRetryVersion = '2.0.6' - springSecurityVersion = '6.3.0' - springVersion = '6.1.7' - springWsVersion = '4.0.11' + springAmqpVersion = '3.1.12' + springDataVersion = '2024.0.13' + springGraphqlVersion = '1.3.5' + springKafkaVersion = '3.2.10' + springRetryVersion = '2.0.12' + springSecurityVersion = '6.3.10' + springVersion = '6.1.21' + springWsVersion = '4.0.15' testcontainersVersion = '1.19.8' - tomcatVersion = '10.1.24' + tomcatVersion = '10.1.42' xmlUnitVersion = '2.9.1' - xstreamVersion = '1.4.20' + xstreamVersion = '1.4.21' ztZipVersion = '1.17' javaProjects = subprojects - project(':spring-integration-bom') @@ -204,6 +205,26 @@ configure(javaProjects) { subproject -> apply from: "${rootDir}/gradle/publish-maven.gradle" + def scopeAttribute = Attribute.of('dependency.scope', String) + + configurations { + provided { + attributes { + attribute(scopeAttribute, 'provided') + } + } + + [compileClasspath, testCompileClasspath, testRuntimeClasspath].each { + it.extendsFrom(provided) + } + } + + components.java.with { + it.addVariantsFromConfiguration(configurations.provided) { + mapToMavenScope('compile') // This is temporary. Gradle doesn't natively support the provided scope + } + } + sourceSets { test { resources { @@ -213,14 +234,14 @@ configure(javaProjects) { subproject -> } java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } withJavadocJar() withSourcesJar() registerFeature('optional') { usingSourceSet(sourceSets.main) } - registerFeature('provided') { - usingSourceSet(sourceSets.main) - } } compileJava { @@ -256,6 +277,10 @@ configure(javaProjects) { subproject -> // dependencies that are common across all java projects dependencies { + attributesSchema { + attribute(scopeAttribute) + } + if (!(subproject.name ==~ /.*-test.*/)) { testImplementation(project(':spring-integration-test-support')) { exclude group: 'org.hamcrest' @@ -426,14 +451,8 @@ configure(javaProjects) { subproject -> from components.java pom.withXml { def pomDeps = asNode().dependencies.first() - subproject.configurations.providedImplementation.allDependencies.each { dep -> - pomDeps.remove(pomDeps.'*'.find { it.artifactId.text() == dep.name }) - pomDeps.appendNode('dependency').with { - it.appendNode('groupId', dep.group) - it.appendNode('artifactId', dep.name) - it.appendNode('version', dep.version) - it.appendNode('scope', 'provided') - } + subproject.configurations.provided.allDependencies.each { dep -> + pomDeps.'*'.find { it.artifactId.text() == dep.name }.scope.first().value = 'provided' } } } @@ -543,7 +562,7 @@ project('spring-integration-core') { } dokkaHtmlPartial { - outputDirectory.set(new File(buildDir, 'kdoc')) + outputDirectory.set(new File('build', 'kdoc')) dokkaSourceSets { main { sourceRoots.setFrom(file('src/main/kotlin')) @@ -677,7 +696,7 @@ project('spring-integration-http') { description = 'Spring Integration HTTP Support' dependencies { api 'org.springframework:spring-webmvc' - providedImplementation "jakarta.servlet:jakarta.servlet-api:$servletApiVersion" + provided "jakarta.servlet:jakarta.servlet-api:$servletApiVersion" optionalApi "com.rometools:rome:$romeToolsVersion" optionalApi 'org.springframework:spring-webflux' @@ -736,7 +755,7 @@ project('spring-integration-jms') { description = 'Spring Integration JMS Support' dependencies { api 'org.springframework:spring-jms' - providedImplementation "jakarta.jms:jakarta.jms-api:$jmsApiVersion" + provided "jakarta.jms:jakarta.jms-api:$jmsApiVersion" testImplementation("org.apache.activemq:artemis-server:$artemisVersion") { exclude group: 'org.jboss.logmanager' @@ -782,7 +801,7 @@ project('spring-integration-mail') { dependencies { api 'org.springframework:spring-context-support' - providedImplementation "org.eclipse.angus:jakarta.mail:$mailVersion" + provided "org.eclipse.angus:jakarta.mail:$mailVersion" testImplementation "com.icegreen:greenmail:$greenmailVersion" @@ -848,8 +867,8 @@ project('spring-integration-scripting') { description = 'Spring Integration Scripting Support' dependencies { optionalApi 'org.jetbrains.kotlin:kotlin-scripting-jsr223' - providedImplementation "org.graalvm.sdk:graal-sdk:$graalvmVersion" - providedImplementation "org.graalvm.polyglot:js:$graalvmVersion" + provided "org.graalvm.sdk:graal-sdk:$graalvmVersion" + provided "org.graalvm.polyglot:js:$graalvmVersion" testImplementation "org.jruby:jruby-complete:$jrubyVersion" testImplementation 'org.apache.groovy:groovy-jsr223' @@ -969,7 +988,7 @@ project('spring-integration-websocket') { dependencies { api 'org.springframework:spring-websocket' optionalApi 'org.springframework:spring-webmvc' - providedImplementation "jakarta.servlet:jakarta.servlet-api:$servletApiVersion" + provided "jakarta.servlet:jakarta.servlet-api:$servletApiVersion" testImplementation project(':spring-integration-event') testImplementation "org.apache.tomcat.embed:tomcat-embed-websocket:$tomcatVersion" @@ -994,7 +1013,7 @@ project('spring-integration-ws') { exclude group: 'org.glassfish.jaxb' } - providedImplementation "com.sun.xml.bind:jaxb-impl:$jaxbVersion" + provided "com.sun.xml.bind:jaxb-impl:$jaxbVersion" testImplementation "com.thoughtworks.xstream:xstream:$xstreamVersion" testImplementation('org.springframework.ws:spring-ws-support') { @@ -1090,15 +1109,19 @@ project('spring-integration-bom') { } } -tasks.register('api', Javadoc) { - group = 'Documentation' - description = 'Generates aggregated Javadoc API documentation.' +dependencies { + javaProjects.each { + javadoc it + } +} + +javadoc { title = "${rootProject.description} ${version} API" options { encoding = 'UTF-8' memberLevel = JavadocMemberLevel.PROTECTED author = true - header = rootProject.description + header = project.description use = true overview = 'src/api/overview.html' splitIndex = true @@ -1106,13 +1129,14 @@ tasks.register('api', Javadoc) { addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint } - source javaProjects.collect { project -> - project.sourceSets.main.allJava - } - destinationDir = new File(buildDir, 'api') - classpath = files(javaProjects.collect { project -> - project.sourceSets.main.compileClasspath - }) + destinationDir = file('build/api') + classpath = files().from { files(javaProjects.collect { it.sourceSets.main.compileClasspath }) } +} + +tasks.register('api') { + group = 'Documentation' + description = 'Generates aggregated Javadoc API documentation.' + dependsOn javadoc } dokkaHtmlMultiModule { @@ -1169,7 +1193,7 @@ tasks.register('docsZip', Zip) { include 'changelog.txt' } - from(api) { + from(javadoc) { into 'api' } diff --git a/gradle.properties b/gradle.properties index fe335413987..518acfb0e72 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.3.1-SNAPSHOT +version=6.3.12-SNAPSHOT org.gradle.jvmargs=-Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 kotlin.jvm.target.validation.mode=IGNORE org.gradle.caching=true diff --git a/gradle/publish-maven.gradle b/gradle/publish-maven.gradle index 649f5f2b4e7..1388fce23ce 100644 --- a/gradle/publish-maven.gradle +++ b/gradle/publish-maven.gradle @@ -41,7 +41,7 @@ publishing { developer { id = 'markfisher' name = 'Mark Fisher' - email = 'mark.fisher@broadcom.com' + email = 'mark.ryan.fisher@gmail.com' roles = ['project founder and lead emeritus'] } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd491770..a4b76b9530d 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 4baf5a11d45..d71047787f8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42690..f3b75f3b0d4 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,7 @@ 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\n' "$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 6689b85beec..9b42019c791 100755 --- 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 ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/settings.gradle b/settings.gradle index dac569f1480..69be99a2f47 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + plugins { - id 'com.gradle.develocity' version '3.17.4' - id 'io.spring.ge.conventions' version '0.0.17' + id 'io.spring.develocity.conventions' version '0.0.23' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0' } rootProject.name = 'spring-integration' diff --git a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AbstractAmqpInboundAdapterParser.java b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AbstractAmqpInboundAdapterParser.java index 32833e80122..44a615b499c 100644 --- a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AbstractAmqpInboundAdapterParser.java +++ b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AbstractAmqpInboundAdapterParser.java @@ -59,6 +59,7 @@ abstract class AbstractAmqpInboundAdapterParser extends AbstractSingleBeanDefini "receive-timeout", "shutdown-timeout", "tx-size", + "batch-size", "missing-queues-fatal" }; @@ -154,7 +155,13 @@ private BeanDefinition buildListenerContainer(Element element, ParserContext par } builder.addConstructorArgReference(connectionFactoryRef); for (String attributeName : CONTAINER_VALUE_ATTRIBUTES) { - IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, attributeName); + // TODO remove 'tx-size' in 6.5 + if ("tx-size".equals(attributeName)) { + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, attributeName, "batchSize"); + } + else { + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, attributeName); + } } for (String attributeName : CONTAINER_REFERENCE_ATTRIBUTES) { IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, attributeName); diff --git a/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp.xsd b/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp.xsd index bf514627342..e6172835ef1 100644 --- a/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp.xsd +++ b/spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp.xsd @@ -969,7 +969,7 @@ standard headers to also be mapped. To map all non-standard headers the 'NON_STA Acknowledge Mode for the MessageListenerContainer; default 'AUTO' meaning the adapter automatically acknowledges the message(s) - according to the tx-size. + according to the batch-size. @@ -1039,7 +1039,7 @@ standard headers to also be mapped. To map all non-standard headers the 'NON_STA Specifies how many messages to send to each consumer in a single request. Often this can be set quite high - to improve throughput. It should be greater than or equal to the tx-size value. + to improve throughput. It should be greater than or equal to the batch-size value. @@ -1117,10 +1117,23 @@ standard headers to also be mapped. To map all non-standard headers the 'NON_STA + [DEPRECATED] How many messages to process in a single transaction (if the channel is transactional). For best results it should be less than or equal to the prefetch count. Not allowed when 'consumers-per-queue' is set. + Deprecated in favor of 'batch-size'. + + + + + + + + + How many messages to process in a single request. + For best results it should be less than or equal to the prefetch count. + Not allowed when 'consumers-per-queue' is set. diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/AmqpInboundChannelAdapterParserTests-context.xml b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/AmqpInboundChannelAdapterParserTests-context.xml index 8b105f34e6c..85e4ee30bb8 100644 --- a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/AmqpInboundChannelAdapterParserTests-context.xml +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/AmqpInboundChannelAdapterParserTests-context.xml @@ -1,14 +1,12 @@ @@ -20,32 +18,34 @@ + batch-mode="EXTRACT_PAYLOADS" tx-size="2"/> + auto-startup="false" phase="123" acknowledge-mode="${ackMode}" + missing-queues-fatal="false" + batch-size="3"/> + channel="requestChannel" queue-names="inboundchanneladapter.test.2" + auto-startup="false" phase="123" + mapped-request-headers="foo*, STANDARD_REQUEST_HEADERS"/> + channel="requestChannel" queue-names="inboundchanneladapter.test.2" + auto-startup="false" phase="123" + mapped-request-headers="foo*"/> + channel="requestChannel" queue-names="inboundchanneladapter.test.2" + auto-startup="false" phase="123" + mapped-request-headers=""/> + channel="requestChannel" queue-names="inboundchanneladapter.test.2" + auto-startup="false" phase="123"/> + auto-startup="false"/> diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/AmqpInboundChannelAdapterParserTests.java b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/AmqpInboundChannelAdapterParserTests.java index 1bc7a1703b3..04eb584d5f3 100644 --- a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/AmqpInboundChannelAdapterParserTests.java +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/config/AmqpInboundChannelAdapterParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,7 @@ package org.springframework.integration.amqp.config; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Message; @@ -37,10 +36,10 @@ import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.test.util.TestUtils; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Mark Fisher @@ -49,8 +48,7 @@ * * @since 2.1 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig @DirtiesContext public class AmqpInboundChannelAdapterParserTests { @@ -71,6 +69,8 @@ public void verifyIdAsChannel() { .isInstanceOf(SimpleMessageListenerContainer.class); assertThat(TestUtils.getPropertyValue(adapter, "batchMode", BatchMode.class)) .isEqualTo(BatchMode.EXTRACT_PAYLOADS); + assertThat(TestUtils.getPropertyValue(adapter, "messageListenerContainer.batchSize", Integer.class)) + .isEqualTo(2); } @Test @@ -95,6 +95,8 @@ public void verifyLifeCycle() { .isEqualTo(AcknowledgeMode.NONE); assertThat(TestUtils.getPropertyValue(adapter, "messageListenerContainer.missingQueuesFatal", Boolean.class)) .isFalse(); + assertThat(TestUtils.getPropertyValue(adapter, "messageListenerContainer.batchSize", Integer.class)) + .isEqualTo(3); } @Test @@ -216,14 +218,13 @@ public void withHeaderMapperDefaultMapping() throws Exception { @Test public void testInt2971HeaderMapperAndMappedHeadersExclusivity() { - try { - new ClassPathXmlApplicationContext("AmqpInboundChannelAdapterParserTests-headerMapper-fail-context.xml", - this.getClass()).close(); - } - catch (BeanDefinitionParsingException e) { - assertThat(e.getMessage().startsWith("Configuration problem: The 'header-mapper' attribute " + - "is mutually exclusive with 'mapped-request-headers' or 'mapped-reply-headers'")).isTrue(); - } + assertThatExceptionOfType(BeanDefinitionParsingException.class) + .isThrownBy(() -> + new ClassPathXmlApplicationContext( + "AmqpInboundChannelAdapterParserTests-headerMapper-fail-context.xml", + getClass())) + .withMessageStartingWith("Configuration problem: The 'header-mapper' attribute " + + "is mutually exclusive with 'mapped-request-headers' or 'mapped-reply-headers'"); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandler.java index 911101139b4..59629ab4f1d 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/AbstractCorrelatingMessageHandler.java @@ -282,7 +282,7 @@ public void setMinimumTimeoutForEmptyGroups(long minimumTimeoutForEmptyGroups) { */ public void setReleasePartialSequences(boolean releasePartialSequences) { if (!this.releaseStrategySet && releasePartialSequences) { - setReleaseStrategy(new SequenceSizeReleaseStrategy()); + setReleaseStrategy(new SequenceSizeReleaseStrategy(releasePartialSequences)); } this.releasePartialSequences = releasePartialSequences; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/BarrierMessageHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/BarrierMessageHandler.java index fd0ed2475ee..deb126142f1 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/aggregator/BarrierMessageHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/aggregator/BarrierMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.integration.handler.DiscardingMessageHandler; import org.springframework.integration.handler.MessageTriggerAction; import org.springframework.integration.store.SimpleMessageGroup; +import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandlingException; @@ -170,7 +171,8 @@ public BarrierMessageHandler(long requestTimeout, long triggerTimeout, MessageGr } /** - * Set the name of the channel to which late arriving trigger messages are sent. + * Set the name of the channel to which late arriving trigger messages are sent, + * or request message does not arrive in time. * @param discardChannelName the discard channel. * @since 5.0 */ @@ -179,7 +181,8 @@ public void setDiscardChannelName(String discardChannelName) { } /** - * Set the channel to which late arriving trigger messages are sent. + * Set the channel to which late arriving trigger messages are sent, + * or request message does not arrive in time. * @param discardChannel the discard channel. * @since 5.0 */ @@ -188,8 +191,11 @@ public void setDiscardChannel(MessageChannel discardChannel) { } /** + * Return the discard message channel for trigger action message. + * @return a discard message channel. * @since 5.0 */ + @Nullable @Override public MessageChannel getDiscardChannel() { String channelName = this.discardChannelName; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/channel/AbstractMessageChannel.java b/spring-integration-core/src/main/java/org/springframework/integration/channel/AbstractMessageChannel.java index 2ddc0c0d241..a46824b2def 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/channel/AbstractMessageChannel.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/channel/AbstractMessageChannel.java @@ -30,6 +30,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import org.springframework.beans.factory.BeanFactory; @@ -59,6 +60,7 @@ import org.springframework.messaging.MessageDeliveryException; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.ErrorMessage; import org.springframework.messaging.support.InterceptableChannel; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -335,12 +337,22 @@ else if (this.metricsCaptor != null) { private boolean sendWithObservation(Message message, long timeout) { MutableMessage messageToSend = MutableMessage.of(message); - return IntegrationObservation.PRODUCER.observation( - this.observationConvention, - DefaultMessageSenderObservationConvention.INSTANCE, - () -> new MessageSenderContext(messageToSend, getComponentName()), - this.observationRegistry) - .observe(() -> sendInternal(messageToSend, timeout)); // NOSONAR - never null + Observation observation = IntegrationObservation.PRODUCER.observation( + this.observationConvention, + DefaultMessageSenderObservationConvention.INSTANCE, + () -> new MessageSenderContext(messageToSend, getComponentName()), + this.observationRegistry); + Boolean observe = observation.observe(() -> { + Message messageToSendInternal = messageToSend; + if (message instanceof ErrorMessage errorMessage) { + messageToSendInternal = + new ErrorMessage(errorMessage.getPayload(), + messageToSend.getHeaders(), + errorMessage.getOriginalMessage()); + } + return sendInternal(messageToSendInternal, timeout); + }); + return Boolean.TRUE.equals(observe); } private boolean sendWithMetrics(Message message, long timeout) { diff --git a/spring-integration-core/src/main/java/org/springframework/integration/channel/FluxMessageChannel.java b/spring-integration-core/src/main/java/org/springframework/integration/channel/FluxMessageChannel.java index 01de10f5b23..13c1bbca54a 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/channel/FluxMessageChannel.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/channel/FluxMessageChannel.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ package org.springframework.integration.channel; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; @@ -28,10 +30,9 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; -import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; import reactor.util.context.ContextView; +import org.springframework.context.Lifecycle; import org.springframework.core.log.LogMessage; import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.integration.StaticMessageHeaderAccessor; @@ -44,6 +45,9 @@ /** * The {@link AbstractMessageChannel} implementation for the * Reactive Streams {@link Publisher} based on the Project Reactor {@link Flux}. + *

+ * This class implements {@link Lifecycle} to control subscriptions to publishers + * attached via {@link #subscribeTo(Publisher)}, when this channel is restarted. * * @author Artem Bilan * @author Gary Russell @@ -52,15 +56,13 @@ * @since 5.0 */ public class FluxMessageChannel extends AbstractMessageChannel - implements Publisher>, ReactiveStreamsSubscribableChannel { - - private final Scheduler scheduler = Schedulers.boundedElastic(); + implements Publisher>, ReactiveStreamsSubscribableChannel, Lifecycle { private final Sinks.Many> sink = Sinks.many().multicast().onBackpressureBuffer(1, false); - private final Sinks.Many subscribedSignal = Sinks.many().replay().limit(1); + private final List>> sourcePublishers = new ArrayList<>(); - private final Disposable.Composite upstreamSubscriptions = Disposables.composite(); + private volatile Disposable.Composite upstreamSubscriptions = Disposables.composite(); private volatile boolean active = true; @@ -92,47 +94,47 @@ private boolean tryEmitMessage(Message message) { .setHeader(IntegrationMessageHeaderAccessor.REACTOR_CONTEXT, contextView) .build(); } - return switch (this.sink.tryEmitNext(messageToEmit)) { - case OK -> true; - case FAIL_NON_SERIALIZED, FAIL_OVERFLOW -> false; - case FAIL_ZERO_SUBSCRIBER -> - throw new IllegalStateException("The [" + this + "] doesn't have subscribers to accept messages"); - case FAIL_TERMINATED, FAIL_CANCELLED -> - throw new IllegalStateException("Cannot emit messages into the cancelled or terminated sink: " - + this.sink); - }; + + if (this.active) { + return switch (this.sink.tryEmitNext(messageToEmit)) { + case OK -> true; + case FAIL_NON_SERIALIZED, FAIL_OVERFLOW -> false; + case FAIL_ZERO_SUBSCRIBER -> + throw new IllegalStateException("The [" + this + "] doesn't have subscribers to accept messages"); + case FAIL_TERMINATED, FAIL_CANCELLED -> + throw new IllegalStateException("Cannot emit messages into the cancelled or terminated sink: " + + this.sink); + }; + } + else { + return false; + } } @Override public void subscribe(Subscriber> subscriber) { this.sink.asFlux() - .doFinally((s) -> this.subscribedSignal.tryEmitNext(this.sink.currentSubscriberCount() > 0)) - .share() + .publish(1) + .refCount() .subscribe(subscriber); - - Mono subscribersBarrier = - Mono.fromCallable(() -> this.sink.currentSubscriberCount() > 0) - .filter(Boolean::booleanValue) - .doOnNext(this.subscribedSignal::tryEmitNext) - .repeatWhenEmpty((repeat) -> - this.active ? repeat.delayElements(Duration.ofMillis(100)) : repeat); // NOSONAR - - addPublisherToSubscribe(Flux.from(subscribersBarrier)); } - private void addPublisherToSubscribe(Flux publisher) { - AtomicReference disposableReference = new AtomicReference<>(); + @Override + public void start() { + this.active = true; + this.upstreamSubscriptions = Disposables.composite(); + this.sourcePublishers.forEach(this::doSubscribeTo); + } - Disposable disposable = - publisher - .doOnTerminate(() -> disposeUpstreamSubscription(disposableReference)) - .subscribe(); + @Override + public void stop() { + this.active = false; + this.upstreamSubscriptions.dispose(); + } - if (!disposable.isDisposed()) { - if (this.upstreamSubscriptions.add(disposable)) { - disposableReference.set(disposable); - } - } + @Override + public boolean isRunning() { + return this.active; } private void disposeUpstreamSubscription(AtomicReference disposableReference) { @@ -145,10 +147,19 @@ private void disposeUpstreamSubscription(AtomicReference disposableR @Override public void subscribeTo(Publisher> publisher) { + this.sourcePublishers.add(publisher); + doSubscribeTo(publisher); + } + + private void doSubscribeTo(Publisher> publisher) { Flux upstreamPublisher = Flux.from(publisher) - .delaySubscription(this.subscribedSignal.asFlux().filter(Boolean::booleanValue).next()) - .publishOn(this.scheduler) + .doOnComplete(() -> this.sourcePublishers.remove(publisher)) + .delaySubscription( + Mono.fromCallable(this.sink::currentSubscriberCount) + .filter((value) -> value > 0) + .repeatWhenEmpty((repeat) -> + this.active ? repeat.delayElements(Duration.ofMillis(100)) : repeat)) .flatMap((message) -> Mono.just(message) .handle((messageToHandle, syncSink) -> sendReactiveMessage(messageToHandle)) @@ -158,6 +169,21 @@ public void subscribeTo(Publisher> publisher) { addPublisherToSubscribe(upstreamPublisher); } + private void addPublisherToSubscribe(Flux publisher) { + AtomicReference disposableReference = new AtomicReference<>(); + + Disposable disposable = + publisher + .doOnTerminate(() -> disposeUpstreamSubscription(disposableReference)) + .subscribe(); + + if (!disposable.isDisposed()) { + if (this.upstreamSubscriptions.add(disposable)) { + disposableReference.set(disposable); + } + } + } + private void sendReactiveMessage(Message message) { Message messageToSend = message; // We have just restored Reactor context, so no need in a header anymore. @@ -175,7 +201,7 @@ private void sendReactiveMessage(Message message) { } } catch (Exception ex) { - logger.warn(ex, LogMessage.format("Error during processing event: %s", messageToSend)); + logger.error(ex, LogMessage.format("Error during processing event: %s", messageToSend)); } } @@ -183,9 +209,8 @@ private void sendReactiveMessage(Message message) { public void destroy() { this.active = false; this.upstreamSubscriptions.dispose(); - this.subscribedSignal.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST); - this.sink.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST); - this.scheduler.dispose(); + this.sourcePublishers.clear(); + this.sink.emitComplete(Sinks.EmitFailureHandler.busyLooping(Duration.ofSeconds(1))); super.destroy(); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/channel/interceptor/ThreadStatePropagationChannelInterceptor.java b/spring-integration-core/src/main/java/org/springframework/integration/channel/interceptor/ThreadStatePropagationChannelInterceptor.java index 4904d4f0d53..7b1dd87ee7c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/channel/interceptor/ThreadStatePropagationChannelInterceptor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/channel/interceptor/ThreadStatePropagationChannelInterceptor.java @@ -16,8 +16,8 @@ package org.springframework.integration.channel.interceptor; -import java.util.LinkedList; import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; import io.micrometer.common.lang.Nullable; @@ -104,14 +104,14 @@ private static final class MessageWithThreadState implements Message, Me private final Queue stateQueue; MessageWithThreadState(Message message, Object state) { - this(message, new LinkedList<>()); + this(message, new LinkedBlockingQueue<>()); this.stateQueue.add(state); } @SuppressWarnings("unchecked") private MessageWithThreadState(Message message, Queue stateQueue) { this.message = (Message) message; - this.stateQueue = new LinkedList<>(stateQueue); + this.stateQueue = new LinkedBlockingQueue<>(stateQueue); } @Override diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java index 4b1816c0ecf..3e17f28c080 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationEvaluationContextFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,13 +20,16 @@ import java.util.Map.Entry; import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.MapAccessor; import org.springframework.expression.BeanResolver; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypeLocator; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.support.StandardTypeLocator; import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.lang.Nullable; /** *

@@ -65,7 +68,8 @@ public class IntegrationEvaluationContextFactoryBean extends AbstractEvaluationContextFactoryBean implements FactoryBean { - private volatile TypeLocator typeLocator; + @Nullable + private TypeLocator typeLocator; private BeanResolver beanResolver; @@ -80,8 +84,12 @@ public boolean isSingleton() { @Override public void afterPropertiesSet() { - if (getApplicationContext() != null) { - this.beanResolver = new BeanFactoryResolver(getApplicationContext()); + ApplicationContext applicationContext = getApplicationContext(); + if (applicationContext != null) { + this.beanResolver = new BeanFactoryResolver(applicationContext); + if (this.typeLocator == null) { + this.typeLocator = new StandardTypeLocator(applicationContext.getClassLoader()); + } } initialize(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationManagementConfiguration.java b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationManagementConfiguration.java index 22018753fbf..9e430dae7c0 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationManagementConfiguration.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationManagementConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 the original author or authors. + * Copyright 2015-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,10 @@ package org.springframework.integration.config; +import java.util.Collection; import java.util.HashSet; import java.util.Map; -import java.util.Set; +import java.util.stream.Collectors; import io.micrometer.observation.ObservationRegistry; @@ -88,17 +89,27 @@ public IntegrationManagementConfigurer managementConfigurer( } private String[] obtainObservationPatterns() { - Set observationPatterns = new HashSet<>(); + Collection observationPatterns = new HashSet<>(); String[] patternsProperties = (String[]) this.attributes.get("observationPatterns"); + boolean hasAsterisk = false; for (String patternProperty : patternsProperties) { String patternValue = this.environment.resolvePlaceholders(patternProperty); String[] patternsToProcess = StringUtils.commaDelimitedListToStringArray(patternValue); for (String pattern : patternsToProcess) { - if (StringUtils.hasText(pattern)) { + hasAsterisk |= "*".equals(pattern); + if (StringUtils.hasText(pattern) && (pattern.startsWith("!") || !hasAsterisk)) { observationPatterns.add(pattern); } } } + if (hasAsterisk) { + observationPatterns = + observationPatterns.stream() + .filter((pattern) -> pattern.startsWith("!")) + .collect(Collectors.toList()); + + observationPatterns.add("*"); + } return observationPatterns.toArray(new String[0]); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationSimpleEvaluationContextFactoryBean.java b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationSimpleEvaluationContextFactoryBean.java index 36fb99a83e8..24f848d51de 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationSimpleEvaluationContextFactoryBean.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/IntegrationSimpleEvaluationContextFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2018-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,6 +82,7 @@ public SimpleEvaluationContext getObject() { SimpleEvaluationContext.forPropertyAccessors(accessorArray) .withTypeConverter(getTypeConverter()) .withInstanceMethods() + .withAssignmentDisabled() .build(); for (Entry functionEntry : getFunctions().entrySet()) { evaluationContext.setVariable(functionEntry.getKey(), functionEntry.getValue()); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/BarrierSpec.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/BarrierSpec.java index 3003841eae4..39be5b0a586 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/BarrierSpec.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/BarrierSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ import org.springframework.integration.aggregator.HeaderAttributeCorrelationStrategy; import org.springframework.integration.aggregator.MessageGroupProcessor; import org.springframework.integration.config.ConsumerEndpointFactoryBean; +import org.springframework.lang.Nullable; +import org.springframework.messaging.MessageChannel; import org.springframework.util.Assert; /** @@ -43,6 +45,15 @@ public class BarrierSpec extends ConsumerEndpointSpec doGet() { - this.handler = new BarrierMessageHandler(this.timeout, this.outputProcessor, this.correlationStrategy); + if (this.triggerTimeout == null) { + this.handler = new BarrierMessageHandler(this.timeout, this.outputProcessor, this.correlationStrategy); + } + else { + this.handler = + new BarrierMessageHandler(this.timeout, this.triggerTimeout, this.outputProcessor, + this.correlationStrategy); + } + if (this.discardChannel != null) { + this.handler.setDiscardChannel(this.discardChannel); + } + else if (this.discardChannelName != null) { + this.handler.setDiscardChannelName(this.discardChannelName); + } return super.doGet(); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/DelayerEndpointSpec.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/DelayerEndpointSpec.java index 8f89a508c36..f3c3dfa6140 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/DelayerEndpointSpec.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/DelayerEndpointSpec.java @@ -30,6 +30,7 @@ import org.springframework.integration.transaction.TransactionInterceptorBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; +import org.springframework.scheduling.TaskScheduler; import org.springframework.transaction.TransactionManager; import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.util.Assert; @@ -243,4 +244,16 @@ public DelayerEndpointSpec messageGroupId(String messageGroupId) { return this; } + /** + * Set a provided {@link TaskScheduler} into the {@link DelayHandler}, + * as well as call {@code super} to set it into an endpoint for this handler (if necessary). + * @param taskScheduler the {@link TaskScheduler} to use. + * @return the spec + */ + @Override + public DelayerEndpointSpec taskScheduler(TaskScheduler taskScheduler) { + this.handler.setTaskScheduler(taskScheduler); + return super.taskScheduler(taskScheduler); + } + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/PollerSpec.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/PollerSpec.java index efbcf020ddd..7d3a3dee12c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/PollerSpec.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/PollerSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -189,8 +189,14 @@ public PollerSpec taskExecutor(Executor taskExecutor) { return this; } + /** + * The timeout for blocking send on channels. + * @param sendTimeout the timeout to use. + * @return the spec. + * @deprecated in favor of {@link SourcePollingChannelAdapterSpec#sendTimeout(long)} + */ + @Deprecated(forRemoval = true, since = "6.3.9") public PollerSpec sendTimeout(long sendTimeout) { - this.target.setSendTimeout(sendTimeout); return this; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/SourcePollingChannelAdapterSpec.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/SourcePollingChannelAdapterSpec.java index 594cf4641f3..6a05a800bf7 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/SourcePollingChannelAdapterSpec.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/SourcePollingChannelAdapterSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,4 +60,15 @@ public SourcePollingChannelAdapterSpec role(String role) { return this; } + /** + * The timeout for blocking send on channels. + * @param sendTimeout the timeout to use. + * @return the spec. + * @since 6.3.9 + */ + public SourcePollingChannelAdapterSpec sendTimeout(long sendTimeout) { + this.endpointFactoryBean.setSendTimeout(sendTimeout); + return this; + } + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/context/IntegrationFlowBeanPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/context/IntegrationFlowBeanPostProcessor.java index b746627c833..3386fe73926 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/context/IntegrationFlowBeanPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/context/IntegrationFlowBeanPostProcessor.java @@ -74,6 +74,7 @@ import org.springframework.integration.gateway.AnnotationGatewayProxyFactoryBean; import org.springframework.integration.support.context.NamedComponent; import org.springframework.lang.Nullable; +import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -181,11 +182,15 @@ else if (useFlowIdAsPrefix) { } else if (component instanceof MessageChannelReference messageChannelReference) { String channelBeanName = messageChannelReference.name(); + MessageChannel channelByName; if (!this.beanFactory.containsBean(channelBeanName)) { - DirectChannel directChannel = new DirectChannel(); - registerComponent(directChannel, channelBeanName, flowBeanName); - targetIntegrationComponents.put(directChannel, channelBeanName); + channelByName = new DirectChannel(); + registerComponent(channelByName, channelBeanName, flowBeanName); } + else { + channelByName = this.beanFactory.getBean(channelBeanName, MessageChannel.class); + } + targetIntegrationComponents.put(channelByName, channelBeanName); } else if (component instanceof SourcePollingChannelAdapterSpec spec) { Map componentsToRegister = spec.getComponentsToRegister(); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/endpoint/MessageProducerSupport.java b/spring-integration-core/src/main/java/org/springframework/integration/endpoint/MessageProducerSupport.java index b2022b552fb..81ff34875ec 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/endpoint/MessageProducerSupport.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/endpoint/MessageProducerSupport.java @@ -164,7 +164,7 @@ public void setShouldTrack(boolean shouldTrack) { * @param errorMessageStrategy the {@link ErrorMessageStrategy}. * @since 4.3.10 */ - public final void setErrorMessageStrategy(ErrorMessageStrategy errorMessageStrategy) { + public void setErrorMessageStrategy(ErrorMessageStrategy errorMessageStrategy) { Assert.notNull(errorMessageStrategy, "'errorMessageStrategy' cannot be null"); this.errorMessageStrategy = errorMessageStrategy; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/events/IntegrationEvent.java b/spring-integration-core/src/main/java/org/springframework/integration/events/IntegrationEvent.java index 379c8432610..1794432d123 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/events/IntegrationEvent.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/events/IntegrationEvent.java @@ -43,6 +43,7 @@ public IntegrationEvent(Object source, @Nullable Throwable cause) { this.cause = cause; } + @Nullable public Throwable getCause() { return this.cause; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.java b/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.java index 345990bbc7a..af179116c9a 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/gateway/MessagingGatewaySupport.java @@ -423,6 +423,7 @@ protected void onInit() { if (!this.replyTimeoutSet) { this.messagingTemplate.setReceiveTimeout(endpointsDefaultTimeout); } + super.onInit(); this.initialized = true; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/graph/MicrometerNodeEnhancer.java b/spring-integration-core/src/main/java/org/springframework/integration/graph/MicrometerNodeEnhancer.java index acf1c7f2a0c..eb1ba2dd7ff 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/graph/MicrometerNodeEnhancer.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/graph/MicrometerNodeEnhancer.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 the original author or authors. + * Copyright 2019-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.integration.graph; -import java.util.Map; import java.util.concurrent.TimeUnit; import io.micrometer.common.docs.KeyName; @@ -26,6 +25,7 @@ import io.micrometer.core.instrument.search.Search; import io.micrometer.observation.ObservationConvention; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; import org.springframework.integration.support.management.IntegrationManagement; import org.springframework.integration.support.management.observation.DefaultMessageReceiverObservationConvention; @@ -58,13 +58,8 @@ public class MicrometerNodeEnhancer { private final MeterRegistry registry; MicrometerNodeEnhancer(ApplicationContext applicationContext) { - Map registries = applicationContext.getBeansOfType(MeterRegistry.class, false, false); - if (registries.size() == 1) { - this.registry = registries.values().iterator().next(); - } - else { - this.registry = null; - } + ObjectProvider meterRegistryProvider = applicationContext.getBeanProvider(MeterRegistry.class); + this.registry = meterRegistryProvider.getIfUnique(); } /** diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProducingHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProducingHandler.java index 021b81e714c..5838ad7f9fa 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProducingHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractMessageProducingHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import java.util.function.BiConsumer; import org.reactivestreams.Publisher; +import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @@ -47,6 +48,7 @@ import org.springframework.integration.routingslip.RoutingSlipRouteStrategy; import org.springframework.integration.support.AbstractIntegrationMessageBuilder; import org.springframework.integration.support.utils.IntegrationUtils; +import org.springframework.integration.util.IntegrationReactiveUtils; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -366,6 +368,7 @@ private static Publisher toPublisherReply(Object reply, @Nullable ReactiveAda } } + @SuppressWarnings("try") private static CompletableFuture toFutureReply(Object reply, @Nullable ReactiveAdapter reactiveAdapter) { if (reactiveAdapter != null) { Mono reactiveReply; @@ -377,7 +380,31 @@ private static CompletableFuture toFutureReply(Object reply, @Nullable Reacti reactiveReply = Mono.from(publisher); } - return reactiveReply.publishOn(Schedulers.boundedElastic()).toFuture(); + CompletableFuture replyFuture = new CompletableFuture<>(); + + reactiveReply + .publishOn(Schedulers.boundedElastic()) + // TODO until Reactor supports context propagation from the MonoToCompletableFuture + .doOnEach((signal) -> { + try (AutoCloseable scope = IntegrationReactiveUtils + .setThreadLocalsFromReactorContext(signal.getContextView())) { + + if (signal.isOnError()) { + replyFuture.completeExceptionally(signal.getThrowable()); + } + else { + replyFuture.complete(signal.get()); + } + + } + catch (Exception ex) { + throw Exceptions.bubble(ex); + } + }) + .contextCapture() + .subscribe(); + + return replyFuture; } else { return toCompletableFuture(reply); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractReplyProducingMessageHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractReplyProducingMessageHandler.java index 0bf1c9f91c8..0af35ad0dc8 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractReplyProducingMessageHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/AbstractReplyProducingMessageHandler.java @@ -154,7 +154,7 @@ else if (this.requiresReply && !isAsync()) { throw new ReplyRequiredException(message, "No reply produced by handler '" + getComponentName() + "', and its 'requiresReply' property is set to true."); } - else if (!isAsync()) { + else if (!isAsync() && isLoggingEnabled()) { logger.debug(LogMessage.format("handler '%s' produced no reply for request Message: %s", this, message)); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/DelayHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/DelayHandler.java index 42725cf30da..59c20fbd339 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/handler/DelayHandler.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/DelayHandler.java @@ -31,6 +31,8 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import org.aopalliance.aop.Advice; import org.springframework.aop.framework.ProxyFactory; @@ -97,6 +99,7 @@ * @author Artem Bilan * @author Gary Russell * @author Christian Tzolov + * @author Youbin Wu * * @since 1.0.3 */ @@ -688,7 +691,10 @@ public static final class DelayedMessageWrapper implements Serializable { private final Message original; - DelayedMessageWrapper(Message original, long requestDate) { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + DelayedMessageWrapper(@JsonProperty("original") Message original, + @JsonProperty("requestDate") long requestDate) { + this.original = original; this.requestDate = requestDate; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/advice/RequestHandlerRetryAdvice.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/advice/RequestHandlerRetryAdvice.java index cdf50b3baa9..f75d2523674 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/handler/advice/RequestHandlerRetryAdvice.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/advice/RequestHandlerRetryAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.integration.handler.advice; +import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.integration.support.ErrorMessageUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; @@ -78,7 +79,7 @@ protected void onInit() { @Override protected Object doInvoke(ExecutionCallback callback, Object target, Message message) { - IntegrationRetryCallback retryCallback = new IntegrationRetryCallback(message, callback); + IntegrationRetryCallback retryCallback = new IntegrationRetryCallback(message, callback, target); RetryState retryState = this.retryStateGenerator.determineRetryState(message); try { return this.retryTemplate.execute(retryCallback, this.recoveryCallback, retryState); @@ -113,7 +114,7 @@ public boolean open(RetryContext context, RetryCallback } - private record IntegrationRetryCallback(Message messageToTry, ExecutionCallback callback) + private record IntegrationRetryCallback(Message messageToTry, ExecutionCallback callback, Object target) implements RetryCallback { @Override @@ -121,6 +122,13 @@ public Object doWithRetry(RetryContext context) { return this.callback.cloneAndExecute(); } + @Override + public String getLabel() { + return this.target instanceof AbstractReplyProducingMessageHandler.RequestHandler requestHandler + ? requestHandler.getAdvisedHandler().getComponentName() + : this.target.getClass().getName(); + } + } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/SimpleJsonSerializer.java b/spring-integration-core/src/main/java/org/springframework/integration/json/SimpleJsonSerializer.java index 1266de8b033..63a911915ed 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/json/SimpleJsonSerializer.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/SimpleJsonSerializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Set; -import java.util.regex.Matcher; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -100,7 +99,7 @@ private static String toElement(Object result) { return result.toString(); } else { - return "\"" + (result == null ? "null" : Matcher.quoteReplacement(result.toString())) + "\""; + return "\"" + (result == null ? "null" : result.toString().replace("\\", "\\\\")) + "\""; } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java index a737518b368..8156e6c6cdf 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; @@ -490,13 +491,13 @@ public PatternBasedHeaderMatcher(Collection patterns) { Assert.notNull(patterns, "Patterns must no be null"); Assert.notEmpty(patterns, "At least one pattern must be specified"); for (String pattern : patterns) { - this.patterns.add(pattern.toLowerCase()); + this.patterns.add(pattern.toLowerCase(Locale.ROOT)); } } @Override public boolean matchHeader(String headerName) { - String header = headerName.toLowerCase(); + String header = headerName.toLowerCase(Locale.ROOT); for (String pattern : this.patterns) { if (PatternMatchUtils.simpleMatch(pattern, header)) { if (LOGGER.isDebugEnabled()) { @@ -534,13 +535,13 @@ public SinglePatternBasedHeaderMatcher(String pattern) { public SinglePatternBasedHeaderMatcher(String pattern, boolean negate) { Assert.notNull(pattern, "Pattern must no be null"); - this.pattern = pattern.toLowerCase(); + this.pattern = pattern.toLowerCase(Locale.ROOT); this.negate = negate; } @Override public boolean matchHeader(String headerName) { - String header = headerName.toLowerCase(); + String header = headerName.toLowerCase(Locale.ROOT); if (PatternMatchUtils.simpleMatch(this.pattern, header)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(MessageFormat.format( diff --git a/spring-integration-core/src/main/java/org/springframework/integration/scheduling/PollerMetadata.java b/spring-integration-core/src/main/java/org/springframework/integration/scheduling/PollerMetadata.java index 8ef87bb7a54..84b26ad6e11 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/scheduling/PollerMetadata.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/scheduling/PollerMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -138,10 +138,12 @@ public Executor getTaskExecutor() { return this.taskExecutor; } + @Deprecated(forRemoval = true, since = "6.3.9") public long getSendTimeout() { return this.sendTimeout; } + @Deprecated(forRemoval = true, since = "6.3.9") public void setSendTimeout(long sendTimeout) { this.sendTimeout = sendTimeout; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/MutableMessageHeaders.java b/spring-integration-core/src/main/java/org/springframework/integration/support/MutableMessageHeaders.java index 45b14f05307..5b876b1580a 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/MutableMessageHeaders.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/MutableMessageHeaders.java @@ -16,6 +16,8 @@ package org.springframework.integration.support; +import java.io.ObjectStreamException; +import java.io.Serial; import java.nio.ByteBuffer; import java.util.Map; import java.util.UUID; @@ -31,6 +33,7 @@ * @author David Turanski * @author Artem Bilan * @author Nathan Kurtyka + * @author Mitchell McDonald * * @since 4.2 */ @@ -73,6 +76,11 @@ public Object remove(Object key) { return super.getRawHeaders().remove(key); } + @Serial + private Object readResolve() throws ObjectStreamException { + return new MutableMessageHeaders(this); + } + @Nullable private static UUID extractId(@Nullable Map headers) { if (headers != null && headers.containsKey(MessageHeaders.ID)) { diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonUtils.java index 8beed4b565e..f8e7076dc1a 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ * * @author Artem Bilan * @author Gary Russell + * @author Youbin Wu * * @since 3.0 * @@ -63,7 +64,8 @@ public final class JacksonJsonUtils { "org.springframework.integration.support", "org.springframework.integration.message", "org.springframework.integration.store", - "org.springframework.integration.history" + "org.springframework.integration.history", + "org.springframework.integration.handler" ); private JacksonJsonUtils() { diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/management/observation/MessageReceiverContext.java b/spring-integration-core/src/main/java/org/springframework/integration/support/management/observation/MessageReceiverContext.java index f3bb801de1a..c7b26164a4a 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/management/observation/MessageReceiverContext.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/management/observation/MessageReceiverContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.integration.support.management.observation; +import java.nio.charset.StandardCharsets; + import io.micrometer.observation.transport.ReceiverContext; import org.springframework.lang.Nullable; @@ -35,7 +37,7 @@ public class MessageReceiverContext extends ReceiverContext> { private final String handlerName; public MessageReceiverContext(Message message, @Nullable String handlerName) { - super((carrier, key) -> carrier.getHeaders().get(key, String.class)); + super(MessageReceiverContext::getHeader); this.message = message; this.handlerName = handlerName != null ? handlerName : "unknown"; } @@ -49,4 +51,12 @@ public String getHandlerName() { return this.handlerName; } + @Nullable + private static String getHeader(Message message, String key) { + Object value = message.getHeaders().get(key); + return value instanceof byte[] bytes + ? new String(bytes, StandardCharsets.UTF_8) + : (value != null ? value.toString() : null); + } + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/utils/PatternMatchUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/support/utils/PatternMatchUtils.java index 94f8d581074..ff934f7d5ff 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/utils/PatternMatchUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/utils/PatternMatchUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.integration.support.utils; import java.util.Arrays; +import java.util.Locale; /** * Utility methods for pattern matching. @@ -48,9 +49,9 @@ private PatternMatchUtils() { */ public static Boolean smartMatchIgnoreCase(String str, String... patterns) { if (patterns != null) { - return smartMatch(str.toLowerCase(), + return smartMatch(str.toLowerCase(Locale.ROOT), Arrays.stream(patterns) - .map(String::toLowerCase) + .map((pattern) -> pattern.toLowerCase(Locale.ROOT)) .toArray(String[]::new)); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/util/CheckedCallable.java b/spring-integration-core/src/main/java/org/springframework/integration/util/CheckedCallable.java index cb6200926f4..9e9231e3815 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/util/CheckedCallable.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/util/CheckedCallable.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.integration.util; +import java.util.concurrent.Callable; + /** * A Callable-like interface which allows throwing any Throwable. * Checked exceptions are wrapped in an IllegalStateException. @@ -32,10 +34,29 @@ public interface CheckedCallable { T call() throws E; + /** + * Wrap the {@link #call()} into unchecked {@link Runnable} (by mistake). + * Re-throw its exception wrapped with a {@link IllegalStateException}. + * @return the Runnable (by mistake). + * @deprecated since 6.3.7 in favor of {@link #uncheckedCallable()}. + * Will be restored back, but with a proper {@link Callable} return type. + */ + @Deprecated default Runnable unchecked() { + return this::uncheckedCallable; + } + + /** + * Wrap the {@link #call()} into unchecked {@link Callable}. + * Re-throw its exception wrapped with a {@link IllegalStateException}. + * Will be replaced with a proper {@link #unchecked()} implementation in 6.5. + * @return the unchecked {@link Callable}. + * @since 6.3.7 + */ + default Callable uncheckedCallable() { return () -> { try { - call(); + return call(); } catch (Throwable t) { // NOSONAR if (t instanceof RuntimeException runtimeException) { // NOSONAR diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AggregatorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AggregatorTests.java index 12369cd16a8..0e667a06d5f 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AggregatorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/AggregatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -133,7 +133,6 @@ public void testAggPerf() throws InterruptedException, ExecutionException, Timeo public void testAggPerfDefaultPartial() throws InterruptedException, ExecutionException, TimeoutException { AggregatingMessageHandler handler = new AggregatingMessageHandler(new DefaultAggregatingMessageGroupProcessor()); handler.setCorrelationStrategy(message -> "foo"); - handler.setReleasePartialSequences(true); DirectChannel outputChannel = new DirectChannel(); handler.setOutputChannel(outputChannel); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/FluxAggregatorMessageHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/FluxAggregatorMessageHandlerTests.java index c8ec685422e..6ddbb415807 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/FluxAggregatorMessageHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/FluxAggregatorMessageHandlerTests.java @@ -19,13 +19,13 @@ import java.time.Duration; import java.util.List; import java.util.Objects; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; @@ -156,8 +156,6 @@ void testCustomCombineFunction() { } @Test - @DisabledIfEnvironmentVariable(named = "bamboo_buildKey", matches = ".*?", - disabledReason = "Timing is too short for CI") void testWindowTimespan() { QueueChannel resultChannel = new QueueChannel(); FluxAggregatorMessageHandler fluxAggregatorMessageHandler = new FluxAggregatorMessageHandler(); @@ -165,18 +163,18 @@ void testWindowTimespan() { fluxAggregatorMessageHandler.setWindowTimespan(Duration.ofMillis(100)); fluxAggregatorMessageHandler.start(); - Executors.newSingleThreadExecutor() - .submit(() -> { - for (int i = 0; i < 10; i++) { - Message messageToAggregate = - MessageBuilder.withPayload(i) - .setCorrelationId("1") - .build(); - fluxAggregatorMessageHandler.handleMessage(messageToAggregate); - Thread.sleep(20); - } - return null; - }); + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.submit(() -> { + for (int i = 0; i < 10; i++) { + Message messageToAggregate = + MessageBuilder.withPayload(i) + .setCorrelationId("1") + .build(); + fluxAggregatorMessageHandler.handleMessage(messageToAggregate); + Thread.sleep(20); + } + return null; + }); Message result = resultChannel.receive(10_000); assertThat(result).isNotNull(); @@ -211,6 +209,8 @@ void testWindowTimespan() { .doesNotContain(0, 1); fluxAggregatorMessageHandler.stop(); + + executorService.shutdown(); } @Test diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorExpressionIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorExpressionIntegrationTests.java index cb304e7b2c8..037ca813724 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorExpressionIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AggregatorExpressionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,7 @@ import java.util.HashMap; import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -28,8 +27,8 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -39,8 +38,8 @@ * @author Oleg Zhurakousky * @author Artem Bilan */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitConfig +@DirtiesContext public class AggregatorExpressionIntegrationTests { @Autowired @@ -51,7 +50,7 @@ public class AggregatorExpressionIntegrationTests { @Qualifier("output") private PollableChannel output; - @Test//(timeout=5000) + @Test public void testVanillaAggregation() { for (int i = 0; i < 5; i++) { Map headers = stubHeaders(i, 5, 1); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AnnotationAggregatorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AnnotationAggregatorTests.java index 9010b6f1b88..425b5466842 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AnnotationAggregatorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/AnnotationAggregatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.PollableChannel; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -47,6 +48,7 @@ * @author Artem Bilan */ @SpringJUnitConfig(classes = AnnotationAggregatorTests.TestConfiguration.class) +@DirtiesContext public class AnnotationAggregatorTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/MethodInvokingAggregatorReturningMessageTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/MethodInvokingAggregatorReturningMessageTests.java index 8d23f207c3c..2b122949f06 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/MethodInvokingAggregatorReturningMessageTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/MethodInvokingAggregatorReturningMessageTests.java @@ -19,16 +19,15 @@ import java.util.Collections; import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -36,8 +35,8 @@ * @author Mark Fisher * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class MethodInvokingAggregatorReturningMessageTests { @Autowired @@ -52,7 +51,7 @@ public class MethodInvokingAggregatorReturningMessageTests { @Autowired PollableChannel defaultOutput; - @Test // INT-1107 + @Test public void messageReturningPojoAggregatorResultIsNotWrappedInAnotherMessage() { List payload = Collections.singletonList("test"); this.pojoInput.send(MessageBuilder.withPayload(payload).build()); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerIntegrationTests.java index 5a486989aea..6935ada554f 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aggregator/integration/ResequencerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,7 @@ package org.springframework.integration.aggregator.integration; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -31,8 +30,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -41,8 +39,7 @@ * @author David Liu * @author Artem Bilan */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitConfig @DirtiesContext public class ResequencerIntegrationTests { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/channel/config/ThreadLocalChannelParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/channel/config/ThreadLocalChannelParserTests.java index d392163d3f7..b1d0ef52749 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/channel/config/ThreadLocalChannelParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/channel/config/ThreadLocalChannelParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,11 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -32,8 +31,7 @@ import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -42,8 +40,7 @@ * @author Dave Syer * @author Artem Bilan */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitConfig @DirtiesContext public class ThreadLocalChannelParserTests { @@ -58,28 +55,29 @@ public class ThreadLocalChannelParserTests { @Test public void testSendInAnotherThread() throws Exception { - simpleChannel.send(new GenericMessage("test")); - Executor otherThreadExecutor = Executors.newSingleThreadExecutor(); + simpleChannel.send(new GenericMessage<>("test")); + ExecutorService otherThreadExecutor = Executors.newSingleThreadExecutor(); final CountDownLatch latch = new CountDownLatch(1); otherThreadExecutor.execute(() -> { - simpleChannel.send(new GenericMessage("crap")); + simpleChannel.send(new GenericMessage<>("crap")); latch.countDown(); }); assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(simpleChannel.receive(10).getPayload()).isEqualTo("test"); // Message sent on another thread is not collected here - assertThat(simpleChannel.receive(10)).isEqualTo(null); + assertThat(simpleChannel.receive(1)).isEqualTo(null); + otherThreadExecutor.shutdown(); } @Test public void testReceiveInAnotherThread() throws Exception { - simpleChannel.send(new GenericMessage("test-1.1")); - simpleChannel.send(new GenericMessage("test-1.2")); - simpleChannel.send(new GenericMessage("test-1.3")); - channelWithInterceptor.send(new GenericMessage("test-2.1")); - channelWithInterceptor.send(new GenericMessage("test-2.2")); - Executor otherThreadExecutor = Executors.newSingleThreadExecutor(); - final List otherThreadResults = new ArrayList(); + simpleChannel.send(new GenericMessage<>("test-1.1")); + simpleChannel.send(new GenericMessage<>("test-1.2")); + simpleChannel.send(new GenericMessage<>("test-1.3")); + channelWithInterceptor.send(new GenericMessage<>("test-2.1")); + channelWithInterceptor.send(new GenericMessage<>("test-2.2")); + ExecutorService otherThreadExecutor = Executors.newSingleThreadExecutor(); + final List otherThreadResults = new ArrayList<>(); final CountDownLatch latch = new CountDownLatch(2); otherThreadExecutor.execute(() -> { otherThreadResults.add(simpleChannel.receive(0)); @@ -100,12 +98,14 @@ public void testReceiveInAnotherThread() throws Exception { assertThat(channelWithInterceptor.receive(0).getPayload()).isEqualTo("test-2.1"); assertThat(channelWithInterceptor.receive(0).getPayload()).isEqualTo("test-2.2"); assertThat(channelWithInterceptor.receive(0)).isNull(); + + otherThreadExecutor.shutdown(); } @Test public void testInterceptor() { int before = interceptor.getSendCount(); - channelWithInterceptor.send(new GenericMessage("test")); + channelWithInterceptor.send(new GenericMessage<>("test")); assertThat(interceptor.getSendCount()).isEqualTo(before + 1); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ChannelAutoCreationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ChannelAutoCreationTests.java index 11763d3f492..569f4292ef1 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ChannelAutoCreationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ChannelAutoCreationTests.java @@ -16,38 +16,53 @@ package org.springframework.integration.config.xml; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; import org.springframework.context.support.ClassPathXmlApplicationContext; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; + /** * * @author Oleg Zhurakousky * @author Gary Russell + * @author Artem Bilan * */ public class ChannelAutoCreationTests { - @Test // no assertions since it validates that no exception is thrown + @Test public void testEnablingAutoChannelCreationBeforeWithCustom() { - new ClassPathXmlApplicationContext("TestEnableChannelAutoCreation-before-context.xml", this.getClass()).close(); + assertThatNoException() + .isThrownBy(() -> + new ClassPathXmlApplicationContext( + "TestEnableChannelAutoCreation-before-context.xml", this.getClass())); } - @Test // no assertions since it validates that no exception is thrown + @Test public void testEnablingAutoChannelCreationAfterWithCustom() { - new ClassPathXmlApplicationContext("TestEnableChannelAutoCreation-after-context.xml", this.getClass()).close(); + assertThatNoException() + .isThrownBy(() -> + new ClassPathXmlApplicationContext( + "TestEnableChannelAutoCreation-after-context.xml", this.getClass())); } - @Test(expected = BeanCreationException.class) + @Test public void testDisablingAutoChannelCreationAfter() { - new ClassPathXmlApplicationContext("TestDisableChannelAutoCreation-after-context.xml", this.getClass()).close(); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> + new ClassPathXmlApplicationContext( + "TestDisableChannelAutoCreation-after-context.xml", getClass())); } - @Test(expected = BeanCreationException.class) + @Test public void testDisablingAutoChannelCreationBefore() { - new ClassPathXmlApplicationContext("TestDisableChannelAutoCreation-before-context.xml", this.getClass()) - .close(); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> + new ClassPathXmlApplicationContext( + "TestDisableChannelAutoCreation-before-context.xml", this.getClass())); } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests2-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser2Tests-context.xml similarity index 100% rename from spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests2-context.xml rename to spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser2Tests-context.xml diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests2.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser2Tests.java similarity index 79% rename from spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests2.java rename to spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser2Tests.java index 8456e23e3e5..6d26276b8cc 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests2.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser2Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,13 @@ package org.springframework.integration.config.xml; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.integration.test.util.TestUtils; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -34,22 +33,20 @@ * * @since 2.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class EnricherParserTests2 { +@SpringJUnitConfig +@DirtiesContext +public class EnricherParser2Tests { @Autowired private ApplicationContext context; @Test public void configurationCheckRequiresReply() { - Object endpoint = context.getBean("enricher"); boolean requiresReply = TestUtils.getPropertyValue(endpoint, "handler.requiresReply", Boolean.class); assertThat(requiresReply).as("Was expecting requiresReply to be 'false'").isFalse(); - } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests3-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser3Tests-context.xml similarity index 95% rename from spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests3-context.xml rename to spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser3Tests-context.xml index 24372067f3b..ab21f11679d 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests3-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser3Tests-context.xml @@ -17,7 +17,7 @@ - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests3-fail-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser3Tests-fail-context.xml similarity index 95% rename from spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests3-fail-context.xml rename to spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser3Tests-fail-context.xml index f419e79e02e..8183a2dd160 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests3-fail-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser3Tests-fail-context.xml @@ -17,7 +17,7 @@ - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests3.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser3Tests.java similarity index 87% rename from spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests3.java rename to spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser3Tests.java index 63ed99c62bc..0e7bde2a54b 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests3.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser3Tests.java @@ -27,14 +27,14 @@ import org.springframework.messaging.support.GenericMessage; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Gary Russell * * @since 2.1.1 */ -public class EnricherParserTests3 { +public class EnricherParser3Tests { @Test public void testSourceBeanResolver() { @@ -44,7 +44,7 @@ public void testSourceBeanResolver() { PollableChannel beanResolveOut = context.getBean("beanResolveOut", PollableChannel.class); SomeBean payload = new SomeBean("foo"); assertThat(payload.getNested().getValue()).isEqualTo("foo"); - beanResolveIn.send(new GenericMessage(payload)); + beanResolveIn.send(new GenericMessage<>(payload)); @SuppressWarnings("unchecked") Message out = (Message) beanResolveOut.receive(); assertThat(out.getPayload()).isSameAs(payload); @@ -59,13 +59,9 @@ public void testTargetBeanResolver() { MessageChannel beanResolveIn = context.getBean("beanResolveIn", MessageChannel.class); SomeBean payload = new SomeBean("foo"); assertThat(payload.getNested().getValue()).isEqualTo("foo"); - try { - beanResolveIn.send(new GenericMessage(payload)); - fail("Expected SpEL Exception"); - } - catch (MessageHandlingException e) { - assertThat(e.getCause() instanceof SpelEvaluationException).isTrue(); - } + assertThatExceptionOfType(MessageHandlingException.class) + .isThrownBy(() -> beanResolveIn.send(new GenericMessage<>(payload))) + .withCauseInstanceOf(SpelEvaluationException.class); context.close(); } @@ -85,7 +81,7 @@ public String getSomeOtherProperty() { return "bar"; } - public class Nested { + public static class Nested { private String value; diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests4-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser4Tests-context.xml similarity index 95% rename from spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests4-context.xml rename to spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser4Tests-context.xml index a21f083900c..b7ebcc83f63 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests4-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser4Tests-context.xml @@ -40,10 +40,10 @@ null-result-expression="'Could not determine the notOverwrite'"/> - + - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests4.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser4Tests.java similarity index 93% rename from spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests4.java rename to spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser4Tests.java index a47ad9d9131..a8ebaaf9981 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests4.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser4Tests.java @@ -16,8 +16,7 @@ package org.springframework.integration.config.xml; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -29,19 +28,20 @@ import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.SubscribableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** * @author Liujiong + * @author Artem Bilan * * @since 4.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class EnricherParserTests4 { +@SpringJUnitConfig +@DirtiesContext +public class EnricherParser4Tests { @Autowired private ApplicationContext context; diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests5-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser5Tests-context.xml similarity index 100% rename from spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests5-context.xml rename to spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser5Tests-context.xml diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests5.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser5Tests.java similarity index 90% rename from spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests5.java rename to spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser5Tests.java index 8a5c1accac4..eead53b248f 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests5.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParser5Tests.java @@ -16,8 +16,7 @@ package org.springframework.integration.config.xml; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -26,8 +25,8 @@ import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -36,11 +35,13 @@ * a default object in case of downstream failure. * * @author Kris Jacyna + * @author Artem Bilan + * * @since 4.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration -public class EnricherParserTests5 { +@SpringJUnitConfig +@DirtiesContext +public class EnricherParser5Tests { @Autowired private ApplicationContext context; diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests-context.xml index 05269420adc..413959efff6 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests-context.xml @@ -43,10 +43,6 @@ - -
- - diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests.java index 0a4c3eb9df2..41bd673850b 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,9 @@ import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.DirectFieldAccessor; -import org.springframework.beans.TypeMismatchException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.expression.Expression; @@ -35,12 +33,10 @@ import org.springframework.integration.transformer.ContentEnricher; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.MessageHandlingException; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.SubscribableChannel; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -52,8 +48,8 @@ * * @since 2.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitConfig +@DirtiesContext public class EnricherParserTests { @Autowired @@ -101,7 +97,6 @@ else if ("married".equals(e.getKey().getExpressionString())) { @Test public void configurationCheckTimeoutParameters() { - Object endpoint = context.getBean("enricher"); Long requestTimeout = TestUtils.getPropertyValue(endpoint, "handler.requestTimeout", Long.class); @@ -109,18 +104,15 @@ public void configurationCheckTimeoutParameters() { assertThat(requestTimeout).isEqualTo(Long.valueOf(1234L)); assertThat(replyTimeout).isEqualTo(Long.valueOf(9876L)); - } @Test public void configurationCheckRequiresReply() { - Object endpoint = context.getBean("enricher"); boolean requiresReply = TestUtils.getPropertyValue(endpoint, "handler.requiresReply", Boolean.class); assertThat(requiresReply).as("Was expecting requiresReply to be 'false'").isTrue(); - } @Test @@ -167,32 +159,7 @@ protected Object handleRequestMessage(Message requestMessage) { adviceCalled--; } - @Test - public void testInt3027WrongHeaderType() { - MessageChannel input = context.getBean("input2", MessageChannel.class); - try { - input.send(new GenericMessage("test")); - } - catch (Exception e) { - assertThat(e).isInstanceOf(MessageHandlingException.class); - assertThat(e.getCause()).isInstanceOf(TypeMismatchException.class); - assertThat(e.getCause().getMessage()) - .startsWith("Failed to convert value of type 'java.util.Date' to required type 'int'"); - } - } - - private static class Source { - - private final String sourceName; - - Source(String sourceName) { - this.sourceName = sourceName; - } - - @SuppressWarnings("unused") - public String getSourceName() { - return sourceName; - } + private record Source(String sourceName) { } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel.java index 596254cd032..b528d727ed6 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserTestsWithoutRequestChannel.java @@ -18,8 +18,7 @@ import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; @@ -32,8 +31,8 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -43,8 +42,8 @@ * * @since 2.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitConfig +@DirtiesContext public class EnricherParserTestsWithoutRequestChannel { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserWithRequestPayloadExpressionTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserWithRequestPayloadExpressionTests.java index 4b2d450c729..2271f8f3ba7 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserWithRequestPayloadExpressionTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/EnricherParserWithRequestPayloadExpressionTests.java @@ -18,8 +18,7 @@ import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; @@ -34,8 +33,8 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.SubscribableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -45,8 +44,8 @@ * * @since 2.1 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitConfig +@DirtiesContext public class EnricherParserWithRequestPayloadExpressionTests { @Autowired @@ -114,18 +113,7 @@ protected Object handleRequestMessage(Message requestMessage) { assertThat(enriched).isSameAs(original); } - private static class Source { - - private final String sourceName; - - Source(String sourceName) { - this.sourceName = sourceName; - } - - @SuppressWarnings("unused") - public String getSourceName() { - return sourceName; - } + private record Source(String sourceName) { } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ErrorChannelAutoCreationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ErrorChannelAutoCreationTests.java index 7ebd0c87380..52b6fb12611 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ErrorChannelAutoCreationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ErrorChannelAutoCreationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -36,6 +37,7 @@ * @author Artem Bilan */ @SpringJUnitConfig +@DirtiesContext public class ErrorChannelAutoCreationTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ErrorMessageExceptionTypeRouterParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ErrorMessageExceptionTypeRouterParserTests.java index 313e2f144f0..fc30947b58b 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ErrorMessageExceptionTypeRouterParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/ErrorMessageExceptionTypeRouterParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,14 @@ package org.springframework.integration.config.xml; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.channel.QueueChannel; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.support.ErrorMessage; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -33,8 +32,8 @@ * @author Gunnar Hillert * @author Artem Bilan */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitConfig +@DirtiesContext public class ErrorMessageExceptionTypeRouterParserTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests.java index 98a4f21f90d..16f4d19ac20 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests.java @@ -20,6 +20,7 @@ import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -395,7 +396,8 @@ public Message preSend(Message message, MessageChannel channel) { } private void startResponder(final PollableChannel requestChannel, final MessageChannel replyChannel) { - Executors.newSingleThreadExecutor().execute(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { Message request = requestChannel.receive(60000); assertThat(request).as("Request not received").isNotNull(); Message reply = MessageBuilder.fromMessage(request) @@ -405,7 +407,7 @@ private void startResponder(final PollableChannel requestChannel, final MessageC payload = CompletableFuture.completedFuture(reply); } else if (request.getPayload().equals("flowCompletable")) { - payload = CompletableFuture.completedFuture("SYNC_COMPLETABLE"); + payload = CompletableFuture.completedFuture("SYNC_COMPLETABLE"); } else if (request.getPayload().equals("flowCustomCompletable")) { MyCompletableFuture myCompletableFuture1 = new MyCompletableFuture(); @@ -427,6 +429,7 @@ else if (request.getPayload().equals("flowCustomCompletableM")) { } replyChannel.send(reply); }); + executorService.shutdown(); } @SuppressWarnings("unused") diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherMethodInvokingTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherMethodInvokingTests.java index 0eca3794432..b5de2fbed73 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherMethodInvokingTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherMethodInvokingTests.java @@ -19,8 +19,7 @@ import java.util.HashMap; import java.util.Map; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -28,8 +27,8 @@ import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -39,8 +38,8 @@ * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class HeaderEnricherMethodInvokingTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherOverwriteTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherOverwriteTests.java index e713e94b3c4..d946044b773 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherOverwriteTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherOverwriteTests.java @@ -16,8 +16,7 @@ package org.springframework.integration.config.xml; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -29,8 +28,8 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -40,8 +39,8 @@ * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class HeaderEnricherOverwriteTests { @Autowired @@ -156,7 +155,7 @@ public void priorityExplicitOverwriteTrue() { MessageChannel channel = this.context.getBean("priorityExplicitOverwriteTrueInput", MessageChannel.class); MessagingTemplate template = new MessagingTemplate(); template.setDefaultDestination(channel); - Message result = template.sendAndReceive(new GenericMessage("test")); + Message result = template.sendAndReceive(new GenericMessage<>("test")); assertThat(result).isNotNull(); assertThat(new IntegrationMessageHeaderAccessor(result).getPriority()).isEqualTo(42); } @@ -312,17 +311,7 @@ public void beanExplicitOverwriteFalseButNoExistingHeader() { assertThat(result.getHeaders().get("foo")).isEqualTo("ABC"); } - public static class TestBean { - - private final String text; - - public TestBean(String text) { - this.text = text; - } - - public String text() { - return this.text; - } + public record TestBean(String text) { } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherParserTests.java index d142b077d6c..079ab824c14 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherParserTests.java @@ -27,6 +27,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -39,6 +40,7 @@ * @since 2.0 */ @SpringJUnitConfig +@DirtiesContext class HeaderEnricherParserTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherTests.java index ebfad485a13..275d5758c98 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/HeaderEnricherTests.java @@ -37,6 +37,7 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -49,6 +50,7 @@ * @since 2.0 */ @SpringJUnitConfig +@DirtiesContext public class HeaderEnricherTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InnerBeanConfigTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InnerBeanConfigTests.java index 9c5d9216cb0..f19314c9613 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InnerBeanConfigTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/InnerBeanConfigTests.java @@ -16,24 +16,25 @@ package org.springframework.integration.config.xml; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.test.util.TestUtils; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Mark Fisher + * @author Artem Bilan */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitConfig +@DirtiesContext public class InnerBeanConfigTests { @Autowired @@ -42,12 +43,12 @@ public class InnerBeanConfigTests { @Autowired private ApplicationContext context; - // INT-1528: the inner bean should not be registered in the context - @Test(expected = NoSuchBeanDefinitionException.class) + @Test public void checkInnerBean() { Object innerBean = TestUtils.getPropertyValue(testEndpoint, "handler.processor.delegate.targetObject"); assertThat(innerBean).isNotNull(); - context.getBean(TestBean.class); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(TestBean.class)); } public static class TestBean { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/OrderedHandlersTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/OrderedHandlersTests.java index a8e837b9f84..cdd9604f4cd 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/OrderedHandlersTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/OrderedHandlersTests.java @@ -16,24 +16,25 @@ package org.springframework.integration.config.xml; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** * @author Mark Fisher + * @author Artem Bilan + * * @since 1.0.3 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitConfig +@DirtiesContext public class OrderedHandlersTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PNamespaceTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PNamespaceTests.java index 1cbe6662e8f..3edb8d07419 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PNamespaceTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PNamespaceTests.java @@ -18,16 +18,15 @@ import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.integration.aggregator.AggregatingMessageHandler; import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.test.util.TestUtils; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -37,8 +36,8 @@ * @author Oleg Zhurakousky * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class PNamespaceTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PayloadDeserializingTransformerParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PayloadDeserializingTransformerParserTests.java index 62937b065de..8d91b7a7939 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PayloadDeserializingTransformerParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/PayloadDeserializingTransformerParserTests.java @@ -37,6 +37,7 @@ import org.springframework.messaging.MessageHandler; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.util.FileCopyUtils; @@ -48,6 +49,7 @@ * @author Artem Bilan */ @SpringJUnitConfig +@DirtiesContext public class PayloadDeserializingTransformerParserTests { @Autowired @@ -69,7 +71,7 @@ public class PayloadDeserializingTransformerParserTests { @Test public void directChannelWithSerializedStringMessage() throws Exception { byte[] bytes = serialize("foo"); - directInput.send(new GenericMessage(bytes)); + directInput.send(new GenericMessage<>(bytes)); Message result = output.receive(10000); assertThat(result).isNotNull(); assertThat(result.getPayload() instanceof String).isTrue(); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/core/AsyncMessagingTemplateTests.java b/spring-integration-core/src/test/java/org/springframework/integration/core/AsyncMessagingTemplateTests.java index f23af0aeb4b..d6ae4cd88de 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/core/AsyncMessagingTemplateTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/core/AsyncMessagingTemplateTests.java @@ -18,6 +18,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -432,8 +433,8 @@ public void cancellationException() throws Throwable { } private static void sendMessageAfterDelay(MessageChannel channel, GenericMessage message, int delay) { - Executors.newSingleThreadExecutor() - .execute(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { try { Thread.sleep(delay); } @@ -443,6 +444,7 @@ private static void sendMessageAfterDelay(MessageChannel channel, GenericMessage } channel.send(message); }); + executorService.shutdown(); } private static class EchoHandler extends AbstractReplyProducingMessageHandler { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/TransactionalPollerWithMixedAopConfig-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/TransactionalPollerWithMixedAopConfigTests-context.xml similarity index 100% rename from spring-integration-core/src/test/java/org/springframework/integration/dispatcher/TransactionalPollerWithMixedAopConfig-context.xml rename to spring-integration-core/src/test/java/org/springframework/integration/dispatcher/TransactionalPollerWithMixedAopConfigTests-context.xml diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/TransactionalPollerWithMixedAopConfigTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/TransactionalPollerWithMixedAopConfigTests.java index d0d6cffdca9..e0cffc95f00 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/TransactionalPollerWithMixedAopConfigTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/TransactionalPollerWithMixedAopConfigTests.java @@ -16,32 +16,37 @@ package org.springframework.integration.dispatcher; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.aop.framework.Advised; -import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** + * This test was influenced by INT-1483 where via registering TX Advisor + * in the BeanFactory while having resent resulted in + * TX Advisor being applied on all beans in AC + * * @author Oleg Zhurakousky * @author Gunnar Hillert * @author Gary Russell - * - * This test was influenced by INT-1483 where by registering TX Advisor - * in the BeanFactory while having resent resulted in - * TX Advisor being applied on all beans in AC + * @author Artem Bilan */ +@SpringJUnitConfig +@DirtiesContext public class TransactionalPollerWithMixedAopConfigTests { + @Autowired + ApplicationContext applicationContext; + @Test public void validateTransactionalProxyIsolationToThePollerOnly() { - ClassPathXmlApplicationContext context = - new ClassPathXmlApplicationContext("TransactionalPollerWithMixedAopConfig-context.xml", this.getClass()); - - assertThat(!(context.getBean("foo") instanceof Advised)).isTrue(); - assertThat(!(context.getBean("inputChannel") instanceof Advised)).isTrue(); - context.close(); + assertThat(this.applicationContext.getBean("foo")).isNotInstanceOf(Advised.class); + assertThat(applicationContext.getBean("inputChannel")).isNotInstanceOf(Advised.class); } public static class SampleService { @@ -58,9 +63,4 @@ public Foo(String value) { } -// public static class SampleAdvice implements MethodInterceptor{ -// public Object invoke(MethodInvocation invocation) throws Throwable { -// return invocation.proceed(); -// } -// } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/unicasting-with-async.xml b/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/UnicastingDispatcherTests-context.xml similarity index 100% rename from spring-integration-core/src/test/java/org/springframework/integration/dispatcher/unicasting-with-async.xml rename to spring-integration-core/src/test/java/org/springframework/integration/dispatcher/UnicastingDispatcherTests-context.xml diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/UnicastingDispatcherTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/UnicastingDispatcherTests.java index 11fccac7e5f..1b9a875048b 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/UnicastingDispatcherTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dispatcher/UnicastingDispatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,10 @@ package org.springframework.integration.dispatcher; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.integration.gateway.RequestReplyExchanger; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -26,6 +27,8 @@ import org.springframework.messaging.MessageHandler; import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -33,26 +36,28 @@ * @author Oleg Zhurakousky * @author Gunnar Hillert * @author Gary Russell - * + * @author Artem Bilan */ +@SpringJUnitConfig +@DirtiesContext public class UnicastingDispatcherTests { - @SuppressWarnings("unchecked") + @Autowired + ApplicationContext applicationContext; + @Test - public void withInboundGatewayAsyncRequestChannelAndExplicitErrorChannel() throws Exception { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("unicasting-with-async.xml", this.getClass()); - SubscribableChannel errorChannel = context.getBean("errorChannel", SubscribableChannel.class); + public void withInboundGatewayAsyncRequestChannelAndExplicitErrorChannel() { + SubscribableChannel errorChannel = this.applicationContext.getBean("errorChannel", SubscribableChannel.class); MessageHandler errorHandler = message -> { MessageChannel replyChannel = (MessageChannel) message.getHeaders().getReplyChannel(); assertThat(message.getPayload() instanceof MessageDeliveryException).isTrue(); - replyChannel.send(new GenericMessage("reply")); + replyChannel.send(new GenericMessage<>("reply")); }; errorChannel.subscribe(errorHandler); - RequestReplyExchanger exchanger = context.getBean(RequestReplyExchanger.class); - Message reply = (Message) exchanger.exchange(new GenericMessage("Hello")); + RequestReplyExchanger exchanger = this.applicationContext.getBean(RequestReplyExchanger.class); + Message reply = exchanger.exchange(new GenericMessage<>("Hello")); assertThat(reply.getPayload()).isEqualTo("reply"); - context.close(); } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/LambdaMessageProcessorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/LambdaMessageProcessorTests.java index df1027b2444..150982d8371 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dsl/LambdaMessageProcessorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/LambdaMessageProcessorTests.java @@ -35,6 +35,7 @@ import org.springframework.integration.handler.LambdaMessageProcessor; import org.springframework.messaging.Message; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -47,6 +48,7 @@ * @since 5.0 */ @SpringJUnitConfig +@DirtiesContext public class LambdaMessageProcessorTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/composition/IntegrationFlowCompositionTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/composition/IntegrationFlowCompositionTests.java index d3413cacf1a..ee84fef3ae9 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dsl/composition/IntegrationFlowCompositionTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/composition/IntegrationFlowCompositionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -202,11 +202,17 @@ IntegrationFlow middleFlow(IntegrationFlow firstFlow, IntegrationFlow lastFlow) .to(lastFlow); } + @Bean + DirectChannel lastFlowInput() { + return new DirectChannel(); + } + @Bean IntegrationFlow lastFlow() { - return f -> f + return IntegrationFlow.from("lastFlowInput") .transform(p -> p + ", and last flow") - .channel(c -> c.queue("lastFlowResult")); + .channel(c -> c.queue("lastFlowResult")) + .get(); } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/correlation/CorrelationHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/correlation/CorrelationHandlerTests.java index 4796f3296a9..d4b4b8caca0 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dsl/correlation/CorrelationHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/correlation/CorrelationHandlerTests.java @@ -298,6 +298,7 @@ public IntegrationFlow barrierFlow() { return f -> f .barrier(10000, b -> b .correlationStrategy(new HeaderAttributeCorrelationStrategy(BARRIER)) + .discardChannel("nullChannel") .outputProcessor(g -> g.getMessages() .stream() diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java index 0e57f95fe7f..8d34bbca46b 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/flows/IntegrationFlowTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,6 +72,7 @@ import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.gateway.GatewayProxyFactoryBean; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +import org.springframework.integration.handler.DelayHandler; import org.springframework.integration.handler.LoggingHandler; import org.springframework.integration.handler.advice.ErrorMessageSendingRecoverer; import org.springframework.integration.handler.advice.ExpressionEvaluatingRequestHandlerAdvice; @@ -81,6 +82,7 @@ import org.springframework.integration.store.SimpleMessageStore; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.support.MutableMessageBuilder; +import org.springframework.integration.test.util.TestUtils; import org.springframework.integration.transformer.PayloadSerializingTransformer; import org.springframework.integration.util.NoBeansOverrideAnnotationConfigContextLoader; import org.springframework.messaging.Message; @@ -94,6 +96,7 @@ import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.messaging.support.GenericMessage; import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.stereotype.Component; @@ -161,6 +164,10 @@ public class IntegrationFlowTests { @Qualifier("bridgeFlow2Input") private MessageChannel bridgeFlow2Input; + @Autowired + @Qualifier("delayer.handler") + DelayHandler delayHandler; + @Autowired @Qualifier("bridgeFlow2Output") private PollableChannel bridgeFlow2Output; @@ -187,6 +194,9 @@ public class IntegrationFlowTests { @Autowired AbstractEndpoint stringSupplierEndpoint; + @Autowired + TaskScheduler customScheduler; + @Test public void testWithSupplierMessageSourceImpliedPoller() { assertThat(this.stringSupplierEndpoint.isAutoStartup()).isFalse(); @@ -259,6 +269,8 @@ public void testBridge() { assertThat(reply).isNotNull(); assertThat(reply.getPayload()).isEqualTo("test"); assertThat(this.delayedAdvice.getInvoked()).isTrue(); + + assertThat(TestUtils.getPropertyValue(this.delayHandler, "taskScheduler")).isSameAs(this.customScheduler); } @Test @@ -589,6 +601,11 @@ public TaskScheduler taskScheduler() { return threadPoolTaskScheduler; } + @Bean + public TaskScheduler customScheduler() { + return new SimpleAsyncTaskScheduler(); + } + @Bean public QueueChannelSpec suppliedChannel() { return MessageChannels.queue(10); @@ -812,7 +829,7 @@ public IntegrationFlow bridgeFlow() { } @Bean - public IntegrationFlow bridgeFlow2() { + public IntegrationFlow bridgeFlow2(TaskScheduler customScheduler) { return IntegrationFlow.from("bridgeFlow2Input") .bridge(c -> c.autoStartup(false).id("bridge")) .fixedSubscriberChannel() @@ -820,7 +837,9 @@ public IntegrationFlow bridgeFlow2() { .messageGroupId("delayer") .delayExpression("200") .advice(this.delayedAdvice) - .messageStore(this.messageStore())) + .messageStore(messageStore()) + .taskScheduler(customScheduler) + .id("delayer")) .channel(MessageChannels.queue("bridgeFlow2Output")) .get(); } @@ -833,8 +852,8 @@ public SimpleMessageStore messageStore() { @Bean public IntegrationFlow claimCheckFlow() { return IntegrationFlow.from("claimCheckInput") - .claimCheckIn(this.messageStore()) - .claimCheckOut(this.messageStore()) + .claimCheckIn(messageStore()) + .claimCheckOut(messageStore()) .get(); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/lifecycle/IntegrationComponentSpecLifecycleTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/lifecycle/IntegrationComponentSpecLifecycleTests.java index bbeb9e79c6b..a83677bfeb0 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dsl/lifecycle/IntegrationComponentSpecLifecycleTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/lifecycle/IntegrationComponentSpecLifecycleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2018-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,7 @@ package org.springframework.integration.dsl.lifecycle; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; @@ -42,7 +41,7 @@ import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.dsl.IntegrationComponentSpec; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.util.StringValueResolver; import static org.assertj.core.api.Assertions.assertThat; @@ -52,7 +51,7 @@ * * @since 5.0.7 */ -@RunWith(SpringRunner.class) +@SpringJUnitConfig @DirtiesContext public class IntegrationComponentSpecLifecycleTests { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/publishsubscribe/PublishSubscribeTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/publishsubscribe/PublishSubscribeTests.java index 579a558e54a..d5b0e951b11 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dsl/publishsubscribe/PublishSubscribeTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/publishsubscribe/PublishSubscribeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,7 @@ import java.util.List; import java.util.function.Consumer; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -34,7 +33,8 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -44,7 +44,8 @@ * * @since 5.0 */ -@RunWith(SpringRunner.class) +@SpringJUnitConfig +@DirtiesContext public class PublishSubscribeTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/reactivestreams/ReactiveStreamsTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/reactivestreams/ReactiveStreamsTests.java index ee98ade390b..b1a4094b6fa 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dsl/reactivestreams/ReactiveStreamsTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/reactivestreams/ReactiveStreamsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.Lifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -52,6 +53,7 @@ import org.springframework.integration.endpoint.MessageProducerSupport; import org.springframework.integration.endpoint.ReactiveMessageSourceProducer; import org.springframework.integration.endpoint.ReactiveStreamsConsumer; +import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.support.GenericMessage; @@ -122,6 +124,7 @@ void testReactiveFlow() throws Exception { disposable.dispose(); assertThat(this.messageSource.isRunning()).isFalse(); + assertThat(TestUtils.getPropertyValue(this.messageSource, "messagingTemplate.sendTimeout")).isEqualTo(256L); } @Test @@ -264,6 +267,35 @@ void messageProducerIsNotStartedAutomatically() { .verify(Duration.ofSeconds(10)); } + @Autowired + QueueChannel fromPublisherResult; + + @Autowired + ConfigurableApplicationContext applicationContext; + + @Test + @DirtiesContext + // Use disruptive this.applicationContext.start() + void verifyFluxMessageChannelRestart() { + for (long i = 0; i < 3L; i++) { + assertThat(this.fromPublisherResult.receive(10_000)).extracting(Message::getPayload).isEqualTo(i); + } + + this.applicationContext.stop(); + + this.fromPublisherResult.purge(null); + + this.applicationContext.start(); + + // The applicationContext restart causes all the endpoint to be started, + // while we really don't have a subscription to this producer + this.testMessageProducer.stop(); + + for (long i = 0; i < 3L; i++) { + assertThat(this.fromPublisherResult.receive(10_000)).extracting(Message::getPayload).isEqualTo(i); + } + } + @Configuration @EnableIntegration public static class ContextConfiguration { @@ -275,6 +307,7 @@ public Publisher> reactiveFlow() { return IntegrationFlow .from(() -> new GenericMessage<>("a,b,c,d,e,f"), e -> e.poller(p -> p.trigger(ctx -> this.invoked.getAndSet(true) ? null : Instant.now())) + .sendTimeout(256) .id("reactiveStreamsMessageSource")) .split(String.class, p -> p.split(",")) .log() @@ -322,6 +355,13 @@ public Publisher> messageProducerFlow() { .toReactivePublisher(true); } + @Bean + IntegrationFlow fromPublisher() { + return IntegrationFlow.from(Flux.interval(Duration.ofMillis(100)).map(GenericMessage::new)) + .channel(c -> c.queue("fromPublisherResult")) + .get(); + } + } private static class TestMessageProducerSpec diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/routers/NoFallbackAllowedTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/routers/NoFallbackAllowedTests.java index 045a0db8ba4..4c30e692716 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/dsl/routers/NoFallbackAllowedTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/routers/NoFallbackAllowedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the original author or authors. + * Copyright 2019-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -39,6 +40,7 @@ * */ @SpringJUnitConfig +@DirtiesContext public class NoFallbackAllowedTests { @Test diff --git a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/MessageProducerSupportTests.java b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/MessageProducerSupportTests.java index 7670af9351d..c648f960da9 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/MessageProducerSupportTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/MessageProducerSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.After; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.PublishSubscribeChannel; @@ -33,6 +33,7 @@ import org.springframework.messaging.support.GenericMessage; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Oleg Zhurakousky @@ -45,14 +46,14 @@ */ public class MessageProducerSupportTests { - private TestApplicationContext context = TestUtils.createTestApplicationContext(); + private final TestApplicationContext context = TestUtils.createTestApplicationContext(); - @After + @AfterEach public void tearDown() { this.context.close(); } - @Test(expected = MessageDeliveryException.class) + @Test public void validateExceptionIfNoErrorChannel() { DirectChannel outChannel = new DirectChannel(); @@ -66,10 +67,11 @@ public void validateExceptionIfNoErrorChannel() { mps.setBeanFactory(this.context); mps.afterPropertiesSet(); mps.start(); - mps.sendMessage(new GenericMessage<>("hello")); + assertThatExceptionOfType(MessageDeliveryException.class) + .isThrownBy(() -> mps.sendMessage(new GenericMessage<>("hello"))); } - @Test(expected = MessageDeliveryException.class) + @Test public void validateExceptionIfSendToErrorChannelFails() { DirectChannel outChannel = new DirectChannel(); outChannel.subscribe(message -> { @@ -87,7 +89,8 @@ public void validateExceptionIfSendToErrorChannelFails() { mps.setBeanFactory(this.context); mps.afterPropertiesSet(); mps.start(); - mps.sendMessage(new GenericMessage<>("hello")); + assertThatExceptionOfType(MessageDeliveryException.class) + .isThrownBy(() -> mps.sendMessage(new GenericMessage<>("hello"))); } @Test diff --git a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ProducerAndConsumerAutoStartupTests.java b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ProducerAndConsumerAutoStartupTests.java index 054162439b6..ab0efdf7ecb 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ProducerAndConsumerAutoStartupTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ProducerAndConsumerAutoStartupTests.java @@ -23,13 +23,12 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -40,7 +39,7 @@ * * @since 2.0 */ -@RunWith(SpringRunner.class) +@SpringJUnitConfig @DirtiesContext public class ProducerAndConsumerAutoStartupTests { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/returnAddressTests.xml b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ReturnAddressTests-context.xml similarity index 100% rename from spring-integration-core/src/test/java/org/springframework/integration/endpoint/returnAddressTests.xml rename to spring-integration-core/src/test/java/org/springframework/integration/endpoint/ReturnAddressTests-context.xml diff --git a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ReturnAddressTests.java b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ReturnAddressTests.java index 0c72e718a1d..2a4d58cdf93 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ReturnAddressTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ReturnAddressTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,10 @@ package org.springframework.integration.endpoint; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -26,54 +27,52 @@ import org.springframework.messaging.PollableChannel; import org.springframework.messaging.core.DestinationResolutionException; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Mark Fisher * @author Gary Russell + * @author Artem Bilan */ +@SpringJUnitConfig +@DirtiesContext public class ReturnAddressTests { + @Autowired + ApplicationContext context; + @Test public void returnAddressFallbackWithChannelReference() { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - "returnAddressTests.xml", this.getClass()); MessageChannel channel3 = (MessageChannel) context.getBean("channel3"); PollableChannel channel5 = (PollableChannel) context.getBean("channel5"); - context.start(); Message message = MessageBuilder.withPayload("*") .setReplyChannel(channel5).build(); channel3.send(message); Message response = channel5.receive(3000); assertThat(response).isNotNull(); assertThat(response.getPayload()).isEqualTo("**"); - context.close(); } @Test public void returnAddressFallbackWithChannelName() { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - "returnAddressTests.xml", this.getClass()); MessageChannel channel3 = (MessageChannel) context.getBean("channel3"); PollableChannel channel5 = (PollableChannel) context.getBean("channel5"); - context.start(); Message message = MessageBuilder.withPayload("*") .setReplyChannelName("channel5").build(); channel3.send(message); Message response = channel5.receive(3000); assertThat(response).isNotNull(); assertThat(response.getPayload()).isEqualTo("**"); - context.close(); } @Test public void returnAddressWithChannelReferenceAfterMultipleEndpoints() { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - "returnAddressTests.xml", this.getClass()); MessageChannel channel1 = (MessageChannel) context.getBean("channel1"); PollableChannel replyChannel = (PollableChannel) context.getBean("replyChannel"); - context.start(); Message message = MessageBuilder.withPayload("*") .setReplyChannel(replyChannel).build(); channel1.send(message); @@ -82,16 +81,12 @@ public void returnAddressWithChannelReferenceAfterMultipleEndpoints() { assertThat(response.getPayload()).isEqualTo("********"); PollableChannel channel2 = (PollableChannel) context.getBean("channel2"); assertThat(channel2.receive(0)).isNull(); - context.close(); } @Test public void returnAddressWithChannelNameAfterMultipleEndpoints() { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - "returnAddressTests.xml", this.getClass()); MessageChannel channel1 = (MessageChannel) context.getBean("channel1"); PollableChannel replyChannel = (PollableChannel) context.getBean("replyChannel"); - context.start(); Message message = MessageBuilder.withPayload("*") .setReplyChannelName("replyChannel").build(); channel1.send(message); @@ -100,47 +95,32 @@ public void returnAddressWithChannelNameAfterMultipleEndpoints() { assertThat(response.getPayload()).isEqualTo("********"); PollableChannel channel2 = (PollableChannel) context.getBean("channel2"); assertThat(channel2.receive(0)).isNull(); - context.close(); } @Test public void returnAddressFallbackButNotAvailable() { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - "returnAddressTests.xml", this.getClass()); MessageChannel channel3 = (MessageChannel) context.getBean("channel3"); - context.start(); - GenericMessage message = new GenericMessage("*"); - try { - channel3.send(message); - } - catch (MessagingException e) { - assertThat(e.getCause() instanceof DestinationResolutionException).isTrue(); - } - context.close(); + GenericMessage message = new GenericMessage<>("*"); + assertThatExceptionOfType(MessagingException.class) + .isThrownBy(() -> channel3.send(message)) + .withCauseInstanceOf(DestinationResolutionException.class); } @Test public void outputChannelWithNoReturnAddress() { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - "returnAddressTests.xml", this.getClass()); MessageChannel channel4 = (MessageChannel) context.getBean("channel4"); PollableChannel replyChannel = (PollableChannel) context.getBean("replyChannel"); - context.start(); - GenericMessage message = new GenericMessage("*"); + GenericMessage message = new GenericMessage<>("*"); channel4.send(message); Message response = replyChannel.receive(3000); assertThat(response).isNotNull(); assertThat(response.getPayload()).isEqualTo("**"); - context.close(); } @Test public void outputChannelTakesPrecedence() { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - "returnAddressTests.xml", this.getClass()); MessageChannel channel4 = (MessageChannel) context.getBean("channel4"); PollableChannel replyChannel = (PollableChannel) context.getBean("replyChannel"); - context.start(); Message message = MessageBuilder.withPayload("*") .setReplyChannelName("channel5").build(); channel4.send(message); @@ -149,7 +129,6 @@ public void outputChannelTakesPrecedence() { assertThat(response.getPayload()).isEqualTo("**"); PollableChannel channel5 = (PollableChannel) context.getBean("channel5"); assertThat(channel5.receive(0)).isNull(); - context.close(); } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/expression/ForeignClassloaderTests.java b/spring-integration-core/src/test/java/org/springframework/integration/expression/ForeignClassloaderTests.java index 1c17d9e85a3..e3e1667e777 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/expression/ForeignClassloaderTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/expression/ForeignClassloaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,7 @@ package org.springframework.integration.expression; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.Message; @@ -25,8 +24,7 @@ import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -35,8 +33,7 @@ * @since 3.0.2 * */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig @DirtiesContext public class ForeignClassloaderTests { @@ -54,7 +51,7 @@ public class ForeignClassloaderTests { public void testThreadHasWrongClassLoader() { Thread t = new Thread(() -> { try { - foo.send(new GenericMessage("foo")); + foo.send(new GenericMessage<>("foo")); } catch (Exception e) { e.printStackTrace(); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/filter/DynamicExpressionFilterIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/filter/DynamicExpressionFilterIntegrationTests.java index 274e6b4b682..369795d75b6 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/filter/DynamicExpressionFilterIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/filter/DynamicExpressionFilterIntegrationTests.java @@ -22,6 +22,7 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -31,6 +32,7 @@ * @author Artem Bilan */ @SpringJUnitConfig +@DirtiesContext public class DynamicExpressionFilterIntegrationTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/filter/FilterContextTests.java b/spring-integration-core/src/test/java/org/springframework/integration/filter/FilterContextTests.java index d4f6a9ef441..4e59a431495 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/filter/FilterContextTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/filter/FilterContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,7 @@ package org.springframework.integration.filter; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.endpoint.AbstractEndpoint; @@ -25,8 +24,8 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -34,8 +33,8 @@ * @author Mark Fisher * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class FilterContextTests { @Autowired @@ -52,7 +51,7 @@ public class FilterContextTests { @Test public void methodInvokingFilterRejects() { - this.input.send(new GenericMessage("foo")); + this.input.send(new GenericMessage<>("foo")); Message reply = this.output.receive(0); assertThat(reply).isNull(); @@ -65,7 +64,7 @@ public void methodInvokingFilterRejects() { @Test public void methodInvokingFilterAccepts() { - this.input.send(new GenericMessage("foobar")); + this.input.send(new GenericMessage<>("foobar")); Message reply = this.output.receive(0); assertThat(reply.getPayload()).isEqualTo("foobar"); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/filter/SpelFilterIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/filter/SpelFilterIntegrationTests.java index 3cc0a449132..a0cc882b554 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/filter/SpelFilterIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/filter/SpelFilterIntegrationTests.java @@ -22,6 +22,7 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -31,6 +32,7 @@ * @author Artem Bilan */ @SpringJUnitConfig +@DirtiesContext public class SpelFilterIntegrationTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/AsyncGatewayTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/AsyncGatewayTests.java index 56fc565432a..ba168382492 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/AsyncGatewayTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/AsyncGatewayTests.java @@ -110,7 +110,6 @@ public void listenableFutureWithMessageReturned() throws Exception { proxyFactory.afterPropertiesSet(); TestEchoService service = proxyFactory.getObject(); CompletableFuture> f = service.returnMessageListenable("foo"); - long start = System.currentTimeMillis(); final AtomicReference> result = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); f.whenComplete((message, throwable) -> { @@ -120,8 +119,6 @@ public void listenableFutureWithMessageReturned() throws Exception { } }); assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); - long elapsed = System.currentTimeMillis() - start; - assertThat(elapsed >= 200).isTrue(); assertThat(result.get().getPayload()).isEqualTo("foobar"); Object thread = result.get().getHeaders().get("thread"); assertThat(thread).isNotEqualTo(Thread.currentThread()); @@ -363,13 +360,7 @@ private static void startResponder(final PollableChannel requestChannel) { Message reply = MessageBuilder.withPayload(payload) .copyHeaders(input.getHeaders()) .build(); - try { - Thread.sleep(200); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } + String header = (String) input.getHeaders().get("method"); if (header != null && header.startsWith("returnCustomFuture")) { reply = MessageBuilder.withPayload(new CustomFuture(payload, diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInvokingMessageHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInvokingMessageHandlerTests.java index b027786ee1a..914e814664a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInvokingMessageHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayInvokingMessageHandlerTests.java @@ -16,8 +16,7 @@ package org.springframework.integration.gateway; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -28,19 +27,19 @@ import org.springframework.messaging.support.GenericMessage; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Oleg Zhurakousky * @author Gunnar Hillert + * @author Artem Bilan + * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) public class GatewayInvokingMessageHandlerTests { @@ -95,21 +94,14 @@ public void validateGatewayWithErrorMessageReturned() { assertThat(result).isNotNull(); assertThat(result).isEqualTo("Error happened in message: echoWithRuntimeExceptionChannel"); - try { - gatewayWithError.process("echoWithRuntimeExceptionChannel"); - fail("SampleRuntimeException expected"); - } - catch (SampleRuntimeException e) { - assertThat(e.getMessage()).isEqualTo("echoWithRuntimeExceptionChannel"); - } + assertThatExceptionOfType(SampleRuntimeException.class) + .isThrownBy(() -> gatewayWithError.process("echoWithRuntimeExceptionChannel")) + .withMessage("echoWithRuntimeExceptionChannel"); - try { - gatewayWithError.process("echoWithMessagingExceptionChannel"); - fail("MessageHandlingException expected"); - } - catch (MessageHandlingException e) { - assertThat(e.getFailedMessage().getPayload()).isEqualTo("echoWithMessagingExceptionChannel"); - } + assertThatExceptionOfType(MessageHandlingException.class) + .isThrownBy(() -> gatewayWithError.process("echoWithMessagingExceptionChannel")) + .extracting("failedMessage.payload") + .isEqualTo("echoWithMessagingExceptionChannel"); result = gatewayWithErrorChannelAndTransformer.process("echoWithMessagingExceptionChannel"); assertThat(result).isNotNull(); @@ -118,13 +110,8 @@ public void validateGatewayWithErrorMessageReturned() { @Test public void validateGatewayWithErrorAsync() { - try { - gatewayWithErrorAsync.process("echoWithErrorAsyncChannel"); - fail("SampleRuntimeException expected"); - } - catch (Exception e) { - assertThat(e.getClass()).isEqualTo(SampleRuntimeException.class); - } + assertThatExceptionOfType(SampleRuntimeException.class) + .isThrownBy(() -> gatewayWithErrorAsync.process("echoWithErrorAsyncChannel")); } @Test diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayRequiresReplyTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayRequiresReplyTests.java index 55fc8020dbc..08e8dbe84c7 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayRequiresReplyTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayRequiresReplyTests.java @@ -16,16 +16,16 @@ package org.springframework.integration.gateway; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.integration.handler.ReplyRequiredException; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Mark Fisher @@ -34,8 +34,8 @@ * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class GatewayRequiresReplyTests { @Autowired @@ -48,10 +48,11 @@ public void replyReceived() { assertThat(result).isEqualTo("bar"); } - @Test(expected = ReplyRequiredException.class) + @Test public void noReplyReceived() { TestService gateway = this.applicationContext.getBean("gateway", TestService.class); - gateway.test("bad"); + assertThatExceptionOfType(ReplyRequiredException.class) + .isThrownBy(() -> gateway.test("bad")); } @Test diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotations-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotationsTests-context.xml similarity index 93% rename from spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotations-context.xml rename to spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotationsTests-context.xml index db4344cff12..b91730cb084 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotations-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotationsTests-context.xml @@ -9,7 +9,7 @@ + service-interface="org.springframework.integration.gateway.GatewayWithHeaderAnnotationsTests$TestService" /> diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotations.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotationsTests.java similarity index 83% rename from spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotations.java rename to spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotationsTests.java index 4a6b39911fa..f7399e40485 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotations.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithHeaderAnnotationsTests.java @@ -16,31 +16,32 @@ package org.springframework.integration.gateway; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.messaging.handler.annotation.Header; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** * @author Mark Fisher * @author Gary Russell + * @author Artem Bilan + * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) -public class GatewayWithHeaderAnnotations { +@SpringJUnitConfig +@DirtiesContext +public class GatewayWithHeaderAnnotationsTests { @Autowired private ApplicationContext applicationContext; - @Test // INT-1205 + @Test public void priorityAsArgument() { TestService gateway = (TestService) applicationContext.getBean("gateway"); String result = gateway.test("foo", 99, "bar", "qux"); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithPayloadExpressionTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithPayloadExpressionTests.java index 102398cea9e..974476a3c97 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithPayloadExpressionTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayWithPayloadExpressionTests.java @@ -22,6 +22,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -34,6 +35,7 @@ * @since 2.0 */ @SpringJUnitConfig +@DirtiesContext public class GatewayWithPayloadExpressionTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayXmlAndAnnotationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayXmlAndAnnotationTests.java index 527e51965d9..8ee5204c4f0 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayXmlAndAnnotationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/GatewayXmlAndAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.expression.Expression; import org.springframework.integration.annotation.Gateway; import org.springframework.integration.test.util.TestUtils; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -38,6 +39,7 @@ * */ @SpringJUnitConfig +@DirtiesContext public class GatewayXmlAndAnnotationTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests.java index fcf3cf96ec4..beeeba1b56a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/HeaderEnrichedGatewayTests.java @@ -16,15 +16,14 @@ package org.springframework.integration.gateway; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.Message; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.handler.annotation.Header; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -32,10 +31,12 @@ * @author Oleg Zhurakousky * @author Mark Fisher * @author Gunnar Hillert + * @author Artem Bilan + * * @since 2.0 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitConfig +@DirtiesContext public class HeaderEnrichedGatewayTests { @Autowired @@ -50,7 +51,7 @@ public class HeaderEnrichedGatewayTests { private Object testPayload; @Test - public void validateHeaderValueMappings() throws Exception { + public void validateHeaderValueMappings() { testPayload = "hello"; gatewayWithHeaderValues.sendString((String) testPayload); Message message1 = channel.receive(0); @@ -77,7 +78,7 @@ public void validateHeaderValueMappings() throws Exception { } @Test - public void validateHeaderExpressionMappings() throws Exception { + public void validateHeaderExpressionMappings() { testPayload = "hello"; gatewayWithHeaderExpressions.sendString((String) testPayload); Message message1 = channel.receive(0); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/InnerGatewayWithChainTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/InnerGatewayWithChainTests.java index f46d6c3cac7..1912c666ec6 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/InnerGatewayWithChainTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/InnerGatewayWithChainTests.java @@ -19,16 +19,16 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.endpoint.SourcePollingChannelAdapter; import org.springframework.messaging.SubscribableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Oleg Zhurakousky @@ -36,8 +36,8 @@ * @author Artem Bilan * */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class InnerGatewayWithChainTests { @Autowired @@ -79,10 +79,11 @@ public void testExceptionHandledByInnerGateway() { assertThat(reply).isEqualTo("ERROR from errorChannelB"); } - // if no error channels explicitly defined exception is rethrown - @Test(expected = ArithmeticException.class) + // If no error channels explicitly defined exception is rethrown + @Test public void testGatewaysNoErrorChannel() { - testGatewayWithNoErrorChannelAAA.echo(0); + assertThatExceptionOfType(ArithmeticException.class) + .isThrownBy(() -> testGatewayWithNoErrorChannelAAA.echo(0)); } @Test diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/MultiMethodGatewayConfigTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/MultiMethodGatewayConfigTests.java index 563b20f0e7b..ea775f97436 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/MultiMethodGatewayConfigTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/MultiMethodGatewayConfigTests.java @@ -16,13 +16,12 @@ package org.springframework.integration.gateway; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -33,8 +32,8 @@ * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class MultiMethodGatewayConfigTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/gateway/MultipleEndpointGatewayTests.java b/spring-integration-core/src/test/java/org/springframework/integration/gateway/MultipleEndpointGatewayTests.java index 9633778028e..ab1861b5dcd 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/gateway/MultipleEndpointGatewayTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/gateway/MultipleEndpointGatewayTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,22 @@ package org.springframework.integration.gateway; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * @author Oleg Zhurakousky * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class MultipleEndpointGatewayTests { @Autowired @@ -44,20 +44,17 @@ public class MultipleEndpointGatewayTests { @Test public void gatewayNoDefaultReplyChannel() { - gatewayA.echo("echoAsMessageChannel"); - // there is nothing to assert. Successful execution of the above is all we care in this test + Assertions.assertThatNoException().isThrownBy(() -> gatewayA.echo("echoAsMessageChannel")); } @Test public void gatewayWithDefaultReplyChannel() { - gatewayB.echo("echoAsMessageChannelIgnoreDefOutChannel"); - // there is nothing to assert. Successful execution of the above is all we care in this test + Assertions.assertThatNoException().isThrownBy(() -> gatewayB.echo("echoAsMessageChannelIgnoreDefOutChannel")); } @Test public void gatewayWithReplySentBackToDefaultReplyChannel() { - gatewayB.echo("echoAsMessageChannelDefaultOutputChannel"); - // there is nothing to assert. Successful execution of the above is all we care in this test + Assertions.assertThatNoException().isThrownBy(() -> gatewayB.echo("echoAsMessageChannelDefaultOutputChannel")); } public interface SampleGateway { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/graph/IntegrationGraphServerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/graph/IntegrationGraphServerTests.java index 6ad0fd94e49..9f5663479a3 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/graph/IntegrationGraphServerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/graph/IntegrationGraphServerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.context.annotation.Primary; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.integration.annotation.Filter; import org.springframework.integration.annotation.InboundChannelAdapter; @@ -80,6 +81,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; /** * @author Gary Russell @@ -290,16 +292,23 @@ void timersViaObservationArePopulated() { @Configuration @EnableIntegration - @EnableIntegrationManagement(observationPatterns = "myFilter") + @EnableIntegrationManagement(observationPatterns = { "myFilter", "*" }) @IntegrationComponentScan @ImportResource("org/springframework/integration/graph/integration-graph-context.xml") public static class Config { @Bean + @Primary public static MeterRegistry meterRegistry() { return new SimpleMeterRegistry(); } + // To be sure that @Primary one wins for the MicrometerNodeEnhancer + @Bean + public static MeterRegistry mockRegistry() { + return mock(); + } + @Bean public ObservationRegistry observationRegistry(MeterRegistry meterRegistry) { ObservationRegistry registry = ObservationRegistry.create(); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/CGLibProxyHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/CGLibProxyHandlerTests.java index 8685b4d7b80..79f3edd763c 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/CGLibProxyHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/CGLibProxyHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,7 @@ package org.springframework.integration.handler; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.aop.framework.ProxyFactoryBean; import org.springframework.aop.support.AopUtils; @@ -30,16 +29,20 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell + * @author Artem Bilan + * * @since 4.3.10 * */ -@RunWith(SpringRunner.class) +@SpringJUnitConfig +@DirtiesContext public class CGLibProxyHandlerTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/CustomConverterMessageProcessingTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/CustomConverterMessageProcessingTests.java index 7ab87d237e8..b91622d1541 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/CustomConverterMessageProcessingTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/CustomConverterMessageProcessingTests.java @@ -16,8 +16,7 @@ package org.springframework.integration.handler; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -27,18 +26,19 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** * @author Mark Fisher * @author Artem Bilan + * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class CustomConverterMessageProcessingTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/LoggingHandlerTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/handler/LoggingHandlerTests-context.xml index a146c413715..fa2fd86fc08 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/LoggingHandlerTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/LoggingHandlerTests-context.xml @@ -7,6 +7,6 @@ http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd"> - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/LoggingHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/LoggingHandlerTests.java index 1453a99d560..c433a8a84a7 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/LoggingHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/LoggingHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.log.LogAccessor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; @@ -35,10 +36,10 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -55,39 +56,45 @@ * @since 2.0 */ @SpringJUnitConfig +@DirtiesContext @LogLevels(categories = "test.logging.handler") public class LoggingHandlerTests { + @Autowired + @Qualifier("input.handler") + LoggingHandler loggingHandler; + @Autowired MessageChannel input; @Test public void logWithExpression() { + DirectFieldAccessor accessor = new DirectFieldAccessor(loggingHandler); + LogAccessor log = (LogAccessor) accessor.getPropertyValue("messageLogger"); + log = spy(log); + accessor.setPropertyValue("messageLogger", log); + TestBean bean = new TestBean("test", 55); input.send(MessageBuilder.withPayload(bean).setHeader("foo", "bar").build()); + + verify(log) + .info(ArgumentMatchers.>argThat(logMessage -> + logMessage.get().equals("test:55"))); } @Test public void assertMutuallyExclusive() { LoggingHandler loggingHandler = new LoggingHandler("INFO"); - loggingHandler.setLogExpressionString("'foo'"); - try { - loggingHandler.setShouldLogFullMessage(true); - fail("Expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage()).isEqualTo("Cannot set both 'expression' AND 'shouldLogFullMessage' properties"); - } - - loggingHandler = new LoggingHandler("INFO"); - loggingHandler.setShouldLogFullMessage(true); - try { - loggingHandler.setLogExpressionString("'foo'"); - fail("Expected IllegalArgumentException"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage()).isEqualTo("Cannot set both 'expression' AND 'shouldLogFullMessage' properties"); - } + loggingHandler.setLogExpressionString("'test'"); + assertThatIllegalArgumentException() + .isThrownBy(() -> loggingHandler.setShouldLogFullMessage(true)) + .withMessage("Cannot set both 'expression' AND 'shouldLogFullMessage' properties"); + + LoggingHandler loggingHandler2 = new LoggingHandler("INFO"); + loggingHandler2.setShouldLogFullMessage(true); + assertThatIllegalArgumentException() + .isThrownBy(() -> loggingHandler2.setLogExpressionString("'test'")) + .withMessage("Cannot set both 'expression' AND 'shouldLogFullMessage' properties"); } @Test @@ -149,24 +156,7 @@ public void testUsageWithoutSpringInitialization() { logMessage.get().equals(testPayload))); } - public static class TestBean { - - private final String name; - - private final int age; - - public TestBean(String name, int age) { - this.name = name; - this.age = age; - } - - public String getName() { - return this.name; - } - - public int getAge() { - return this.age; - } + public record TestBean(String name, int age) { } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/MockHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/MockHandlerTests.java index 35699c5d007..4646cebc6cb 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/MockHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/MockHandlerTests.java @@ -16,8 +16,7 @@ package org.springframework.integration.handler; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; @@ -26,8 +25,8 @@ import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -36,8 +35,8 @@ * @author Mark Fisher * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class MockHandlerTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/SendTimeoutConfigurationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/SendTimeoutConfigurationTests.java index 20636ad927f..53b48ca8ec8 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/SendTimeoutConfigurationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/SendTimeoutConfigurationTests.java @@ -16,22 +16,22 @@ package org.springframework.integration.handler; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.integration.test.util.TestUtils; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** * @author Mark Fisher + * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class SendTimeoutConfigurationTests { @Autowired @@ -39,31 +39,31 @@ public class SendTimeoutConfigurationTests { @Test public void serviceActivator() { - assertThat(this.getTimeout("serviceActivator")).isEqualTo(123); + assertThat(getTimeout("serviceActivator")).isEqualTo(123); } @Test public void filter() { - assertThat(this.getTimeout("filter")).isEqualTo(123); + assertThat(getTimeout("filter")).isEqualTo(123); } @Test public void transformer() { - assertThat(this.getTimeout("transformer")).isEqualTo(123); + assertThat(getTimeout("transformer")).isEqualTo(123); } @Test public void splitter() { - assertThat(this.getTimeout("splitter")).isEqualTo(123); + assertThat(getTimeout("splitter")).isEqualTo(123); } @Test public void router() { - assertThat(this.getTimeout("router")).isEqualTo(123); + assertThat(getTimeout("router")).isEqualTo(123); } private long getTimeout(String endpointName) { - return TestUtils.getPropertyValue(context.getBean(endpointName), + return TestUtils.getPropertyValue(this.context.getBean(endpointName), "handler.messagingTemplate.sendTimeout", Long.class); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/ServiceActivatorDefaultFrameworkMethodTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/ServiceActivatorDefaultFrameworkMethodTests.java index 0e458da846c..6946282df47 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/ServiceActivatorDefaultFrameworkMethodTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/ServiceActivatorDefaultFrameworkMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import org.springframework.messaging.MessagingException; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.ErrorMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -55,6 +56,7 @@ * @since 2.0.1 */ @SpringJUnitConfig +@DirtiesContext public class ServiceActivatorDefaultFrameworkMethodTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/ServiceActivatorOnMockitoMockTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/ServiceActivatorOnMockitoMockTests.java index e2b6e4bb54f..d6b4e3511c1 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/ServiceActivatorOnMockitoMockTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/ServiceActivatorOnMockitoMockTests.java @@ -16,8 +16,7 @@ package org.springframework.integration.handler; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -26,8 +25,8 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.handler.annotation.Header; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.mockito.Mockito.verify; @@ -36,8 +35,8 @@ * @author Gunnar Hillert * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class ServiceActivatorOnMockitoMockTests { @Autowired @@ -83,14 +82,6 @@ public void shouldInvokeMockedSingleMethodOnClass() { verify(singleMethodOnClass).move("SingleMethodOnClass"); } - public static class SingleMethodAcceptingHeaderOnClass { - - public String move(@Header("s") String s) { - return s; - } - - } - @Autowired SingleMethodAcceptingHeaderOnClass singleMethodAcceptingHeaderOnClass; @@ -102,4 +93,12 @@ public void shouldInvokeMockedSingleMethodAcceptingHeaderOnClass() { verify(singleMethodAcceptingHeaderOnClass).move("SingleMethodAcceptingHeaderOnClass"); } + public static class SingleMethodAcceptingHeaderOnClass { + + public String move(@Header("s") String s) { + return s; + } + + } + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/AdvisedMessageHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/AdvisedMessageHandlerTests.java index a585ea036da..be570a7b7f1 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/AdvisedMessageHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/AdvisedMessageHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,9 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -58,6 +61,7 @@ import org.springframework.retry.RetryContext; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.DefaultRetryState; +import org.springframework.retry.support.MetricsRetryListener; import org.springframework.retry.support.RetryTemplate; import org.springframework.scheduling.TaskScheduler; import org.springframework.test.annotation.DirtiesContext; @@ -993,6 +997,46 @@ public void enhancedRecoverer() { assertThat(((ErrorMessage) error).getOriginalMessage().getPayload()).isEqualTo("foo"); } + @Test + public void retryAdviceWithMetricsListener() { + AbstractReplyProducingMessageHandler handler = new AbstractReplyProducingMessageHandler() { + + @Override + protected Object handleRequestMessage(Message requestMessage) { + throw new RuntimeException("intentional"); + } + }; + + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + + RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice(); + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.registerListener(new MetricsRetryListener(meterRegistry)); + advice.setRetryTemplate(retryTemplate); + advice.setBeanFactory(mock(BeanFactory.class)); + advice.afterPropertiesSet(); + + List adviceChain = new ArrayList<>(); + adviceChain.add(advice); + handler.setAdviceChain(adviceChain); + handler.setBeanName("testEndpoint"); + handler.setBeanFactory(mock(BeanFactory.class)); + handler.afterPropertiesSet(); + + Message message = new GenericMessage<>("Hello, world!"); + assertThatExceptionOfType(MessagingException.class) + .isThrownBy(() -> handler.handleMessage(message)) + .withRootCauseInstanceOf(RuntimeException.class) + .withStackTraceContaining("intentional"); + + Timer retryTimer = meterRegistry.find(MetricsRetryListener.TIMER_NAME) + .tag("name", "testEndpoint") + .tag("retry.count", "3") + .timer(); + + assertThat(retryTimer.count()).isEqualTo(1); + } + private interface Bar { Object handleRequestMessage(Message message) throws Throwable; diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdviceTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdviceTests.java index babbace5a39..c6f181faff1 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdviceTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdviceTests.java @@ -33,6 +33,7 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.support.ErrorMessage; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -45,6 +46,7 @@ * */ @SpringJUnitConfig +@DirtiesContext public class ExpressionEvaluatingRequestHandlerAdviceTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/IdempotentReceiverTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/IdempotentReceiverTests.java index 7f9bf771237..50e493a3935 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/IdempotentReceiverTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/IdempotentReceiverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.aop.framework.ProxyFactory; @@ -41,18 +40,17 @@ import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; /** * @author Artem Bilan + * * @since 4.1 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig @DirtiesContext public class IdempotentReceiverTests { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/RateLimiterRequestHandlerAdviceTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/RateLimiterRequestHandlerAdviceTests.java index fad27d8c03e..ee460e161a8 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/RateLimiterRequestHandlerAdviceTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/RateLimiterRequestHandlerAdviceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the original author or authors. + * Copyright 2019-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.springframework.messaging.MessagingException; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -46,6 +47,7 @@ * @since 5.2 */ @SpringJUnitConfig +@DirtiesContext public class RateLimiterRequestHandlerAdviceTests { private static final Duration REFRESH_PERIOD = Duration.ofMillis(500); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/SpelExpressionRetryStateGeneratorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/SpelExpressionRetryStateGeneratorTests.java index ad33fda3fe1..e83c43eb707 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/SpelExpressionRetryStateGeneratorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/SpelExpressionRetryStateGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,15 @@ package org.springframework.integration.handler.advice; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.classify.ClassifierSupport; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.retry.RetryState; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -36,8 +35,8 @@ * @since 2.2 * */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class SpelExpressionRetryStateGeneratorTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/ContentTypeConversionTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/ContentTypeConversionTests.java index 9f9afd651d1..3c133ab3ccc 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/ContentTypeConversionTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/ContentTypeConversionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -49,6 +50,7 @@ * @since 5.0 */ @SpringJUnitConfig +@DirtiesContext public class ContentTypeConversionTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java index 575a8e3f06e..ca53cb5c9dc 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonPathTests.java @@ -25,9 +25,8 @@ import com.jayway.jsonpath.Filter; import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.Predicate; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -39,18 +38,18 @@ import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.transformer.MessageTransformationException; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.support.AnnotationConfigContextLoader; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Artem Bilan @@ -58,9 +57,8 @@ * * @since 3.0 */ -@ContextConfiguration(classes = JsonPathTests.JsonPathTestsContextConfiguration.class, - loader = AnnotationConfigContextLoader.class) -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig(classes = JsonPathTests.JsonPathTestsContextConfiguration.class) +@DirtiesContext public class JsonPathTests { private static File JSON_FILE; @@ -69,14 +67,14 @@ public class JsonPathTests { private static Message testMessage; - @BeforeClass + @BeforeAll public static void setUp() throws IOException { ClassPathResource jsonResource = new ClassPathResource("JsonPathTests.json", JsonPathTests.class); JSON_FILE = jsonResource.getFile(); Scanner scanner = new Scanner(JSON_FILE); JSON = scanner.useDelimiter("\\Z").next(); scanner.close(); - testMessage = new GenericMessage(JSON); + testMessage = new GenericMessage<>(JSON); } @Autowired @@ -116,7 +114,7 @@ public static void setUp() throws IOException { private PollableChannel routerOutput2; @Test - public void testInt3139JsonPathTransformer() throws IOException { + public void testInt3139JsonPathTransformer() { this.transformerInput.send(testMessage); Message receive = this.output.receive(10000); assertThat(receive).isNotNull(); @@ -127,20 +125,14 @@ public void testInt3139JsonPathTransformer() throws IOException { assertThat(receive).isNotNull(); assertThat(receive.getPayload()).isEqualTo("Nigel Rees"); - this.transformerInput.send(new GenericMessage(JSON_FILE)); + this.transformerInput.send(new GenericMessage<>(JSON_FILE)); receive = this.output.receive(1000); assertThat(receive).isNotNull(); assertThat(receive.getPayload()).isEqualTo("Nigel Rees"); - try { - this.transformerInput.send(new GenericMessage(new Object())); - fail("IllegalArgumentException expected"); - } - catch (Exception e) { - //MessageTransformationException / MessageHandlingException / InvocationTargetException / IllegalArgumentException - Throwable cause = e.getCause().getCause().getCause(); - assertThat(cause instanceof PathNotFoundException).isTrue(); - } + assertThatExceptionOfType(MessageTransformationException.class) + .isThrownBy(() -> this.transformerInput.send(new GenericMessage<>(new Object()))) + .withRootCauseInstanceOf(PathNotFoundException.class); } @Test @@ -165,13 +157,9 @@ public void testInt3139JsonPathFilter() { receive = this.output.receive(10000); assertThat(receive).isNotNull(); - try { - this.filterInput1.send(new GenericMessage("{foo:{}}")); - fail("MessageRejectedException is expected."); - } - catch (Exception e) { - assertThat(e).isInstanceOf(MessageRejectedException.class); - } + assertThatExceptionOfType(MessageRejectedException.class) + .isThrownBy(() -> this.filterInput1.send(new GenericMessage<>("{foo:{}}"))); + receive = this.output.receive(0); assertThat(receive).isNull(); @@ -186,7 +174,7 @@ public void testInt3139JsonPathSplitter() { for (int i = 0; i < 4; i++) { Message receive = this.splitterOutput.receive(10000); assertThat(receive).isNotNull(); - assertThat(receive.getPayload() instanceof Map).isTrue(); + assertThat(receive.getPayload()).isInstanceOf(Map.class); } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests.java index 93c8c218ef6..b52aca572ea 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -48,6 +49,7 @@ * @since 2.0 */ @SpringJUnitConfig +@DirtiesContext public class JsonToObjectTransformerParserTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerParserTests.java index c324ac552f6..135aa770e91 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerParserTests.java @@ -37,6 +37,7 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -50,6 +51,7 @@ * @since 2.0 */ @SpringJUnitConfig +@DirtiesContext public class ObjectToJsonTransformerParserTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/SimpleJsonSerializerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/SimpleJsonSerializerTests.java index b040775622f..6ea1f1bac81 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/SimpleJsonSerializerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/SimpleJsonSerializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.integration.json; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.integration.support.json.JsonObjectMapperProvider; @@ -29,10 +29,10 @@ * @since 5.0 * */ -public class SimpleJsonSerializerTests { +class SimpleJsonSerializerTests { @Test - public void test() throws Exception { + void verifySimpleJsonSerializerAgainstSimpleContent() throws Exception { Foo foo = new Foo("foo"); String json = SimpleJsonSerializer.toJson(foo, "fileInfo"); Foo fooOut = JsonObjectMapperProvider.newInstance().fromJson(json, Foo.class); @@ -43,7 +43,15 @@ public void test() throws Exception { assertThat(fooOut.fileInfo).isNull(); } - public static class Foo { + @Test + void verifySimpleJsonSerializerAgainstDollarContent() throws Exception { + Foo foo = new Foo("some content with $"); + String json = SimpleJsonSerializer.toJson(foo); + Foo fooOut = JsonObjectMapperProvider.newInstance().fromJson(json, Foo.class); + assertThat(fooOut.fileInfo).isEqualTo("some content with $"); + } + + static class Foo { private final String foo = "bar"; @@ -55,12 +63,11 @@ public static class Foo { private String fileInfo; - public Foo() { - super(); + Foo() { } - public Foo(String info) { - this.fileInfo = "foo"; + Foo(String info) { + this.fileInfo = info; } public String getFoo() { @@ -79,7 +86,11 @@ public boolean isBool() { return this.bool; } - public String fileInfo() { + public void setFileInfo(String fileInfo) { + this.fileInfo = fileInfo; + } + + public String getFileInfo() { return this.fileInfo; } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/message/MethodInvokingMessageHandlerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/message/MethodInvokingMessageHandlerTests.java index dd88203c9b0..c63429a3d02 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/message/MethodInvokingMessageHandlerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/message/MethodInvokingMessageHandlerTests.java @@ -93,7 +93,7 @@ public void subscription() throws Exception { endpoint.setTrigger(new PeriodicTrigger(Duration.ofMillis(10))); context.registerEndpoint("testEndpoint", endpoint); context.refresh(); - String result = queue.poll(2000, TimeUnit.MILLISECONDS); + String result = queue.poll(10000, TimeUnit.MILLISECONDS); assertThat(result).isNotNull(); assertThat(result).isEqualTo("testing"); context.close(); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/config/DynamicExpressionRouterIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/config/DynamicExpressionRouterIntegrationTests.java index d3de1a2fe37..1ed3434b3f5 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/router/config/DynamicExpressionRouterIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/router/config/DynamicExpressionRouterIntegrationTests.java @@ -16,16 +16,15 @@ package org.springframework.integration.router.config; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -33,8 +32,8 @@ * @author Mark Fisher * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class DynamicExpressionRouterIntegrationTests { @Autowired @@ -68,17 +67,7 @@ public void dynamicExpressionBasedRouter() { assertThat(even.receive(0)).isNull(); } - static class TestBean { - - private final int number; - - TestBean(int number) { - this.number = number; - } - - public int getNumber() { - return this.number; - } + record TestBean(int number) { } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/config/HeaderValueRouterConvertibleTypeTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/config/HeaderValueRouterConvertibleTypeTests.java index 24ad20315b0..99d012419ee 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/router/config/HeaderValueRouterConvertibleTypeTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/router/config/HeaderValueRouterConvertibleTypeTests.java @@ -16,16 +16,15 @@ package org.springframework.integration.router.config; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -33,8 +32,8 @@ * @author Mark Fisher * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class HeaderValueRouterConvertibleTypeTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/config/HeaderValueRouterParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/config/HeaderValueRouterParserTests.java index bbb231c5e42..16fb568e495 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/router/config/HeaderValueRouterParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/router/config/HeaderValueRouterParserTests.java @@ -16,16 +16,15 @@ package org.springframework.integration.router.config; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -33,8 +32,8 @@ * @author Oleg Zhurakousky * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class HeaderValueRouterParserTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/config/PayloadTypeRouterParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/config/PayloadTypeRouterParserTests.java index 4c2de95150b..9b1c46621c7 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/router/config/PayloadTypeRouterParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/router/config/PayloadTypeRouterParserTests.java @@ -18,8 +18,7 @@ import java.io.ByteArrayInputStream; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.annotation.Autowired; @@ -30,16 +29,18 @@ import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Oleg Zhurakousky + * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class PayloadTypeRouterParserTests { @Autowired @@ -63,47 +64,37 @@ public void testPayloadTypeRouter() { PollableChannel chanel2 = (PollableChannel) context.getBean("channel2"); PollableChannel chanel3 = (PollableChannel) context.getBean("channel3"); PollableChannel chanel4 = (PollableChannel) context.getBean("channel4"); - assertThat(chanel1.receive(100).getPayload() instanceof String).isTrue(); - assertThat(chanel2.receive(100).getPayload() instanceof Integer).isTrue(); + assertThat(chanel1.receive(100).getPayload()).isInstanceOf(String.class); + assertThat(chanel2.receive(100).getPayload()).isInstanceOf(Integer.class); assertThat(chanel3.receive(100).getPayload().getClass().isArray()).isTrue(); assertThat(chanel4.receive(100).getPayload().getClass().isArray()).isTrue(); } - @Test(expected = BeanDefinitionStoreException.class) + @Test public void testNoMappingElement() { - ByteArrayInputStream stream = new ByteArrayInputStream(routerConfigNoMaping.getBytes()); + ByteArrayInputStream stream = new ByteArrayInputStream(routerConfigNoMapping.getBytes()); GenericApplicationContext ac = new GenericApplicationContext(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(ac); reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_XSD); - reader.loadBeanDefinitions(new InputStreamResource(stream)); + assertThatExceptionOfType(BeanDefinitionStoreException.class) + .isThrownBy(() -> reader.loadBeanDefinitions(new InputStreamResource(stream))); } - @SuppressWarnings("unused") - private final String routerConfigFakeType = - "" + - "" + - " " + - " " + - " " + - " " + - ""; - - private final String routerConfigNoMaping = - "" + - "" + - " " + - " " + - ""; + private static final String routerConfigNoMapping = + """ + + + + + + + """; public interface TestService { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/config/RecipientListRouterParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/config/RecipientListRouterParserTests.java index c5c85292ed1..c7f4d9a9a81 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/router/config/RecipientListRouterParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/router/config/RecipientListRouterParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -40,6 +41,7 @@ * @since 1.0.3 */ @SpringJUnitConfig +@DirtiesContext public class RecipientListRouterParserTests { @Autowired @@ -57,7 +59,6 @@ public class RecipientListRouterParserTests { @Test public void checkMessageRouting() { - context.start(); Message message = new GenericMessage<>(1); channel.send(message); PollableChannel chanel1 = (PollableChannel) context.getBean("channel1"); @@ -93,7 +94,6 @@ public void customRouter() { @Test public void simpleDynamicRouter() { - context.start(); Message message = new GenericMessage<>(1); simpleDynamicInput.send(message); PollableChannel chanel1 = (PollableChannel) context.getBean("channel1"); @@ -104,7 +104,6 @@ public void simpleDynamicRouter() { @Test public void noSelectorMatchRouter() { - context.start(); Message message = new GenericMessage<>(1); noSelectorMatchInput.send(message); PollableChannel chanel1 = (PollableChannel) context.getBean("channel1"); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/config/RouterParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/config/RouterParserTests.java index 5187f9495ea..50b8b615ada 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/router/config/RouterParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/router/config/RouterParserTests.java @@ -40,6 +40,7 @@ import org.springframework.messaging.core.DestinationResolutionException; import org.springframework.messaging.core.DestinationResolver; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -55,6 +56,7 @@ * @author Artem Bilan */ @SpringJUnitConfig +@DirtiesContext public class RouterParserTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/config/RouterWithMappingTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/config/RouterWithMappingTests.java index 2b8b8189762..47f2a2b271a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/router/config/RouterWithMappingTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/router/config/RouterWithMappingTests.java @@ -16,8 +16,7 @@ package org.springframework.integration.router.config; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -28,17 +27,18 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** * @author Mark Fisher * @author Gary Russell + * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class RouterWithMappingTests { @Autowired @@ -125,17 +125,7 @@ public void pojoRouter() { assertThat(this.testBean.isRunning()).isTrue(); } - private static class TestBean { - - private final String name; - - TestBean(String name) { - this.name = name; - } - - public String getName() { - return this.name; - } + private record TestBean(String name) { } @@ -145,7 +135,7 @@ private static class TestRouter implements Lifecycle { @SuppressWarnings("unused") public String route(TestBean bean) { - return bean.getName(); + return bean.name(); } @Override diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/config/SpelRouterIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/config/SpelRouterIntegrationTests.java index 015ef907889..359a7e97144 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/router/config/SpelRouterIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/router/config/SpelRouterIntegrationTests.java @@ -16,8 +16,7 @@ package org.springframework.integration.router.config; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.channel.QueueChannel; @@ -25,8 +24,8 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -34,8 +33,8 @@ * @author Mark Fisher * @author Artme Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class SpelRouterIntegrationTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/scattergather/config/ScatterGatherParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/scattergather/config/ScatterGatherParserTests.java index 4232a5ce984..e8d885e34bd 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/scattergather/config/ScatterGatherParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/scattergather/config/ScatterGatherParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,7 @@ import java.util.Collection; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -31,8 +30,8 @@ import org.springframework.integration.scattergather.ScatterGatherHandler; import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.MessageHandler; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -42,8 +41,8 @@ * * @since 4.1 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class ScatterGatherParserTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/splitter/DynamicExpressionSplitterIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/splitter/DynamicExpressionSplitterIntegrationTests.java index 1456afc32cb..7e25b2f55d8 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/splitter/DynamicExpressionSplitterIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/splitter/DynamicExpressionSplitterIntegrationTests.java @@ -19,16 +19,15 @@ import java.util.ArrayList; import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -36,8 +35,8 @@ * @author Mark Fisher * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class DynamicExpressionSplitterIntegrationTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/splitter/SpelSplitterIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/splitter/SpelSplitterIntegrationTests.java index 948f7344675..c0f4d93a410 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/splitter/SpelSplitterIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/splitter/SpelSplitterIntegrationTests.java @@ -30,6 +30,7 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -40,6 +41,7 @@ * @author Gary Russell */ @SpringJUnitConfig +@DirtiesContext public class SpelSplitterIntegrationTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/support/MessageScenariosTests.java b/spring-integration-core/src/test/java/org/springframework/integration/support/MessageScenariosTests.java index 7efb158ae0a..d5120e49601 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/support/MessageScenariosTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/support/MessageScenariosTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,11 +26,17 @@ import org.springframework.integration.test.support.RequestResponseScenario; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import static org.assertj.core.api.Assertions.assertThat; +/** + * @author Gary Russell + * @author Artem Bilan + */ @ContextConfiguration +@DirtiesContext public class MessageScenariosTests extends AbstractRequestResponseScenarioTests { @Override diff --git a/spring-integration-core/src/test/java/org/springframework/integration/support/MutableMessageTests.java b/spring-integration-core/src/test/java/org/springframework/integration/support/MutableMessageTests.java index 99612a45090..9b1eb07ee4f 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/support/MutableMessageTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/support/MutableMessageTests.java @@ -16,6 +16,11 @@ package org.springframework.integration.support; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; @@ -26,10 +31,12 @@ import org.springframework.messaging.MessageHeaders; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * @author Stuart Williams * @author Nathan Kurtyka + * @author Mitchell McDonald * * @since 4.2 */ @@ -104,4 +111,34 @@ public void testMessageHeaderIsSerializable() { assertThat(mutableMessageBytes.getHeaders().getTimestamp()).isEqualTo(timestamp); } + @Test + public void testMessageHeaderIsSerializableAndDeserializableWithNonSerializableValues() + throws IOException, ClassNotFoundException { + + String payload = "payload"; + + Map headerMap = new HashMap<>(); + headerMap.put("header1", "serializableValue"); + headerMap.put("header2", new Object()); // Non-Serializable value + + MutableMessage mutableMessage = new MutableMessage<>(payload, headerMap); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream); + outputStream.writeObject(mutableMessage); + outputStream.flush(); + + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); + ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream); + Object deserializedObject = inputStream.readObject(); + + assertThat(deserializedObject).isInstanceOf(MutableMessage.class); + MutableMessage deserializedMessage = + (MutableMessage) deserializedObject; + + assertThat(deserializedMessage.getHeaders().get("header2")).isNull(); // Non-serializable value removed + assertThat(deserializedMessage.getHeaders().get("header1")).isEqualTo("serializableValue"); + assertThatNoException().isThrownBy(() -> deserializedMessage.getRawHeaders().put("header3", "newValue")); + } + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/support/SingleScenarioTests.java b/spring-integration-core/src/test/java/org/springframework/integration/support/SingleScenarioTests.java index c7478fa5275..840e66dfcde 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/support/SingleScenarioTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/support/SingleScenarioTests.java @@ -19,16 +19,23 @@ import org.springframework.integration.test.support.PayloadValidator; import org.springframework.integration.test.support.RequestResponseScenario; import org.springframework.integration.test.support.SingleRequestResponseScenarioTests; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import static org.assertj.core.api.Assertions.assertThat; +/** + * @author David Turanski + * @author Mark Fisher + * @author Artem Bilan + */ @ContextConfiguration("MessageScenariosTests-context.xml") +@DirtiesContext public class SingleScenarioTests extends SingleRequestResponseScenarioTests { @Override protected RequestResponseScenario defineRequestResponseScenario() { - RequestResponseScenario scenario = new RequestResponseScenario( + return new RequestResponseScenario( "inputChannel", "outputChannel") .setPayload("hello") .setResponseValidator(new PayloadValidator() { @@ -38,7 +45,6 @@ protected void validateResponse(String response) { assertThat(response).isEqualTo("HELLO"); } }); - return scenario; } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/support/management/micrometer/MicrometerCustomMetricsTests.java b/spring-integration-core/src/test/java/org/springframework/integration/support/management/micrometer/MicrometerCustomMetricsTests.java index 9292885e780..5f42efc7eb9 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/support/management/micrometer/MicrometerCustomMetricsTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/support/management/micrometer/MicrometerCustomMetricsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2018-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.springframework.integration.config.EnableIntegrationManagement; import org.springframework.integration.support.management.metrics.MetricsCaptor; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; @@ -46,6 +47,7 @@ */ @SpringJUnitConfig @TestExecutionListeners(DependencyInjectionTestExecutionListener.class) +@DirtiesContext public class MicrometerCustomMetricsTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/support/management/observation/IntegrationObservabilityZipkinTests.java b/spring-integration-core/src/test/java/org/springframework/integration/support/management/observation/IntegrationObservabilityZipkinTests.java index 46ede28b3df..a54473ba2f1 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/support/management/observation/IntegrationObservabilityZipkinTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/support/management/observation/IntegrationObservabilityZipkinTests.java @@ -48,7 +48,9 @@ import org.springframework.integration.handler.advice.HandleMessageAdvice; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; +import org.springframework.messaging.support.ErrorMessage; import org.springframework.messaging.support.GenericMessage; import static org.assertj.core.api.Assertions.assertThat; @@ -102,6 +104,18 @@ public SampleTestRunnerConsumer yourCode() { assertThat(configuration.observedHandlerLatch.await(10, TimeUnit.SECONDS)).isTrue(); await().untilAsserted(() -> assertThat(configuration.observationReference.get()).isNotNull()); + + MessageChannel errorChannel = applicationContext.getBean("myErrorChannel", MessageChannel.class); + ErrorMessage errorMessage = + new ErrorMessage(new RuntimeException("some error"), new GenericMessage<>("some original")); + errorChannel.send(errorMessage); + + assertThat(configuration.errorMessageReceivedLatch.await(10, TimeUnit.SECONDS)).isTrue(); + + ErrorMessage receivedErrorMessage = configuration.errorMessageReference.get(); + assertThat(receivedErrorMessage.getOriginalMessage()).isEqualTo(errorMessage.getOriginalMessage()); + assertThat(receivedErrorMessage.getPayload()).isEqualTo(errorMessage.getPayload()); + assertThat(receivedErrorMessage.getHeaders()).containsKeys("X-B3-TraceId", "X-B3-SpanId"); } SpansAssert.assertThat(bb.getFinishedSpans()) @@ -119,7 +133,7 @@ public SampleTestRunnerConsumer yourCode() { .hasTag(IntegrationObservation.ProducerTags.COMPONENT_NAME.asString(), "queueChannel") .hasTag(IntegrationObservation.ProducerTags.COMPONENT_TYPE.asString(), "producer") .hasKindEqualTo(Span.Kind.PRODUCER)) - .hasSize(4); + .hasSize(5); MeterRegistryAssert.assertThat(getMeterRegistry()) .hasTimerWithNameAndTags("spring.integration.handler", @@ -134,7 +148,7 @@ public SampleTestRunnerConsumer yourCode() { @EnableIntegration @EnableIntegrationManagement( observationPatterns = { - "${spring.integration.management.observation-patterns:testInboundGateway,skippedObservationInboundGateway,queueChannel,observedEndpoint,publishSubscribeChannel}", + "${spring.integration.management.observation-patterns:testInboundGateway,skippedObservationInboundGateway,queueChannel,myErrorChannel,observedEndpoint,publishSubscribeChannel}", "${spring.integration.management.observation-patterns:}" }) public static class ObservationIntegrationTestConfiguration { @@ -181,6 +195,16 @@ BridgeHandler bridgeHandler(PublishSubscribeChannel publishSubscribeChannel) { return bridgeHandler; } + AtomicReference errorMessageReference = new AtomicReference<>(); + + CountDownLatch errorMessageReceivedLatch = new CountDownLatch(1); + + @ServiceActivator(inputChannel = "myErrorChannel") + void handleError(ErrorMessage errorMessage) { + this.errorMessageReference.set(errorMessage); + this.errorMessageReceivedLatch.countDown(); + } + @Bean HandleMessageAdvice observedHandlerAdvice() { return invocation -> { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/support/mutable/MutableMessageBuilderFactoryTests.java b/spring-integration-core/src/test/java/org/springframework/integration/support/mutable/MutableMessageBuilderFactoryTests.java index 86ac2082913..8cc4032ed3f 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/support/mutable/MutableMessageBuilderFactoryTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/support/mutable/MutableMessageBuilderFactoryTests.java @@ -19,8 +19,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -36,17 +35,19 @@ import org.springframework.integration.support.MutableMessageBuilderFactory; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** * @author Stuart Williams + * @author Artem Bilan + * * @since 4.2 */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@SpringJUnitConfig +@DirtiesContext public class MutableMessageBuilderFactoryTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transaction/TransactionInterceptorBuilderTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transaction/TransactionInterceptorBuilderTests.java index c52ab7b7546..017c81a314a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transaction/TransactionInterceptorBuilderTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transaction/TransactionInterceptorBuilderTests.java @@ -16,13 +16,13 @@ package org.springframework.integration.transaction; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionManager; import org.springframework.transaction.annotation.Isolation; @@ -38,7 +38,8 @@ * * @since 5.0 */ -@RunWith(SpringRunner.class) +@SpringJUnitConfig +@DirtiesContext public class TransactionInterceptorBuilderTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/AvroTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/AvroTests.java index b2778d1fc69..a4b9f218462 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/AvroTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/AvroTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the original author or authors. + * Copyright 2019-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -47,6 +48,7 @@ * */ @SpringJUnitConfig +@DirtiesContext @LogLevels(categories = "foo", level = "DEBUG") public class AvroTests { diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/DynamicExpressionHeaderEnricherIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/DynamicExpressionHeaderEnricherIntegrationTests.java index fcead3b75eb..8b333ccb4d5 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/DynamicExpressionHeaderEnricherIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/DynamicExpressionHeaderEnricherIntegrationTests.java @@ -16,16 +16,15 @@ package org.springframework.integration.transformer; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -33,8 +32,8 @@ * @author Mark Fisher * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class DynamicExpressionHeaderEnricherIntegrationTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/DynamicExpressionTransformerIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/DynamicExpressionTransformerIntegrationTests.java index c352f1b8b9f..81f846e2cf1 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/DynamicExpressionTransformerIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/DynamicExpressionTransformerIntegrationTests.java @@ -16,16 +16,15 @@ package org.springframework.integration.transformer; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.support.MessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -33,8 +32,8 @@ * @author Mark Fisher * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class DynamicExpressionTransformerIntegrationTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/MessageHistoryParameterTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/MessageHistoryParameterTests.java index fc262ba2af5..37c2d747900 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/MessageHistoryParameterTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/MessageHistoryParameterTests.java @@ -16,8 +16,7 @@ package org.springframework.integration.transformer; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.annotation.Transformer; @@ -29,18 +28,20 @@ import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell + * @author Artem Bilan + * * @since 2.2 * */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class MessageHistoryParameterTests { @Autowired @@ -51,7 +52,7 @@ public class MessageHistoryParameterTests { @Test public void test() { - input.send(new GenericMessage("foo")); + input.send(new GenericMessage<>("foo")); assertThat(output.receive(10000)).isNotNull(); } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/ProtoTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/ProtoTests.java index 2ade213c76b..c5fdac00924 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/ProtoTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/ProtoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -41,10 +42,12 @@ /** * * @author Christian Tzolov + * @author Artem Bilan * * @since 6.1 */ @SpringJUnitConfig +@DirtiesContext public class ProtoTests { @Test diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelHeaderEnricherIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelHeaderEnricherIntegrationTests.java index 1689e7c2839..60c09c58791 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelHeaderEnricherIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/SpelHeaderEnricherIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,7 @@ package org.springframework.integration.transformer; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -25,8 +24,8 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -35,8 +34,8 @@ * @author David Turanski * @author Artem Bilan */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class SpelHeaderEnricherIntegrationTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/TransformerContextTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/TransformerContextTests.java index 6201bd24ffa..d64480ae940 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/TransformerContextTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/TransformerContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; @@ -39,6 +40,7 @@ * @author Artem Bilan */ @SpringJUnitConfig +@DirtiesContext public class TransformerContextTests { private static volatile int adviceCalled; diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/FileReadingMessageSource.java b/spring-integration-file/src/main/java/org/springframework/integration/file/FileReadingMessageSource.java index f26b97baf75..be8181c5833 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/FileReadingMessageSource.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/FileReadingMessageSource.java @@ -42,7 +42,6 @@ import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; -import java.util.regex.Matcher; import org.springframework.context.Lifecycle; import org.springframework.integration.endpoint.AbstractMessageSource; @@ -388,10 +387,7 @@ protected AbstractIntegrationMessageBuilder doReceive() { if (file != null) { return getMessageBuilderFactory() .withPayload(file) - .setHeader(FileHeaders.RELATIVE_PATH, - file.getAbsolutePath() - .replaceFirst(Matcher.quoteReplacement( - this.directory.getAbsolutePath() + File.separator), "")) + .setHeader(FileHeaders.RELATIVE_PATH, this.directory.toPath().relativize(file.toPath()).toString()) .setHeader(FileHeaders.FILENAME, file.getName()) .setHeader(FileHeaders.ORIGINAL_FILE, file); } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java b/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java index 6568e2656db..0a24b651eb2 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/FileWritingMessageHandler.java @@ -629,6 +629,7 @@ private File handleFileMessage(File sourceFile, File tempFile, File resultFile, if (!FileExistsMode.APPEND.equals(this.fileExistsMode) && this.deleteSourceFiles) { rename(sourceFile, resultFile); + setPermissions(resultFile); return resultFile; } else { diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java index 5b6e085d4f7..28621adc837 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java @@ -570,10 +570,17 @@ private void sendFileToRemoteDirectory(InputStream inputStream, String temporary if (this.autoCreateDirectory) { try { RemoteFileUtils.makeDirectories(remoteDirectory, session, this.remoteFileSeparator, this.logger); + if (!temporaryRemoteDirectory.equals(remoteDirectory)) { + RemoteFileUtils.makeDirectories(temporaryRemoteDirectory, session, this.remoteFileSeparator, + this.logger); + } } catch (@SuppressWarnings("unused") IllegalStateException e) { // Revert to old FTP behavior if recursive mkdir fails, for backwards compatibility session.mkdir(remoteDirectory); + if (!temporaryRemoteDirectory.equals(remoteDirectory)) { + session.mkdir(temporaryRemoteDirectory); + } } } diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/FileReadingMessageSourceTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/FileReadingMessageSourceTests.java index fbb0dcf7dd8..f01a3615fad 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/FileReadingMessageSourceTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/FileReadingMessageSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,15 +17,12 @@ package org.springframework.integration.file; import java.io.File; +import java.nio.file.Path; import java.util.Comparator; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.BeanFactory; import org.springframework.messaging.Message; import static org.assertj.core.api.Assertions.assertThat; @@ -42,43 +39,40 @@ * @author Artem Bilan * @author Gary Russell */ -@RunWith(MockitoJUnitRunner.class) -public class FileReadingMessageSourceTests { +class FileReadingMessageSourceTests { private FileReadingMessageSource source; - @Mock - private File inputDirectoryMock; + private final File inputDirectoryMock = mock(); - @Mock - private File fileMock; + private final File fileMock = mock(); - @Mock - private FileLocker locker; + private final FileLocker locker = mock(); - @Mock - private Comparator comparator; + private final Comparator comparator = mock(); public void prepResource() { - when(inputDirectoryMock.getAbsolutePath()).thenReturn("foo/bar"); - when(fileMock.getAbsolutePath()).thenReturn("foo/bar/fileMock"); + when(inputDirectoryMock.toPath()).thenReturn(Path.of("[dir]")); + when(fileMock.toPath()).thenReturn(Path.of("[dir]/fileMock")); when(locker.lock(isA(File.class))).thenReturn(true); } - @Before + @BeforeEach public void initialize() { prepResource(); this.source = new FileReadingMessageSource(comparator); this.source.setDirectory(inputDirectoryMock); this.source.setLocker(locker); - this.source.setBeanFactory(mock(BeanFactory.class)); + this.source.setBeanFactory(mock()); this.source.afterPropertiesSet(); } @Test public void straightProcess() { when(inputDirectoryMock.listFiles()).thenReturn(new File[] {fileMock}); - assertThat(source.receive().getPayload()).isEqualTo(fileMock); + Message fileMessage = source.receive(); + assertThat(fileMessage.getPayload()).isEqualTo(fileMock); + assertThat(fileMessage.getHeaders()).containsEntry(FileHeaders.RELATIVE_PATH, "fileMock"); } @Test @@ -88,13 +82,13 @@ public void requeueOnFailure() { assertThat(received).isNotNull(); source.onFailure(received); assertThat(source.receive().getPayload()).isEqualTo(received.getPayload()); - verify(inputDirectoryMock, times(1)).listFiles(); + verify(inputDirectoryMock).listFiles(); } @Test public void scanEachPoll() { - File anotherFileMock = mock(File.class); - when(anotherFileMock.getAbsolutePath()).thenReturn("foo/bar/anotherFileMock"); + File anotherFileMock = mock(); + when(anotherFileMock.toPath()).thenReturn(Path.of("[dir]/anotherFileMock")); when(inputDirectoryMock.listFiles()).thenReturn(new File[] {fileMock, anotherFileMock}); source.setScanEachPoll(true); assertThat(source.receive()).isNotNull(); @@ -139,12 +133,12 @@ public void lockedFilesAreIgnored() { @Test public void orderedReception() { - File file1 = mock(File.class); - when(file1.getAbsolutePath()).thenReturn("foo/bar/file1"); - File file2 = mock(File.class); - when(file2.getAbsolutePath()).thenReturn("foo/bar/file2"); - File file3 = mock(File.class); - when(file3.getAbsolutePath()).thenReturn("foo/bar/file3"); + File file1 = mock(); + when(file1.toPath()).thenReturn(Path.of("[dir]/file1")); + File file2 = mock(); + when(file2.toPath()).thenReturn(Path.of("[dir]/file2")); + File file3 = mock(); + when(file3.toPath()).thenReturn(Path.of("[dir]/file3")); // record the comparator to reverse order the files when(comparator.compare(file1, file2)).thenReturn(1); diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java index b006fa0336f..a6492c3569a 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/FileWritingMessageHandlerTests.java @@ -36,6 +36,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; import org.springframework.beans.DirectFieldAccessor; @@ -301,6 +303,23 @@ public void deleteFilesTrueWithFilePayload() throws Exception { assertThat(sourceFile.exists()).isFalse(); } + @Test + @DisabledOnOs(OS.WINDOWS) + public void deleteFilesWithChmod() throws Exception { + QueueChannel output = new QueueChannel(); + handler.setDeleteSourceFiles(true); + handler.setOutputChannel(output); + handler.setChmod(0400); + Message message = MessageBuilder.withPayload(sourceFile).build(); + handler.handleMessage(message); + Message result = output.receive(0); + assertFileContentIsMatching(result); + File resultFile = messageToFile(result); + Set posixFilePermissions = Files.getPosixFilePermissions(resultFile.toPath()); + assertThat(posixFilePermissions).containsOnly(PosixFilePermission.OWNER_READ); + assertThat(sourceFile.exists()).isFalse(); + } + @Test public void deleteSourceFileWithStringPayloadAndFileInstanceHeader() throws Exception { QueueChannel output = new QueueChannel(); diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/filters/PersistentAcceptOnceFileListFilterExternalStoreTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/filters/PersistentAcceptOnceFileListFilterExternalStoreTests.java index 02961189eaf..af79905ef3a 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/filters/PersistentAcceptOnceFileListFilterExternalStoreTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/filters/PersistentAcceptOnceFileListFilterExternalStoreTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -48,6 +49,7 @@ * @author Artem Bilan * @author Bojan Vukasovic * @author Artem Vozhdayenko + * * @since 4.0 */ public class PersistentAcceptOnceFileListFilterExternalStoreTests implements RedisContainerTest { @@ -139,7 +141,8 @@ private void testFileSystem(ConcurrentMetadataStore store) throws Exception { suspend.set(true); assertThat(file.setLastModified(file.lastModified() + 5000L)).isTrue(); - Future result = Executors.newSingleThreadExecutor() + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future result = executorService .submit(() -> filter.filterFiles(new File[] {file}).size()); assertThat(latch2.await(10, TimeUnit.SECONDS)).isTrue(); store.put("foo:" + file.getAbsolutePath(), "43"); @@ -149,6 +152,7 @@ private void testFileSystem(ConcurrentMetadataStore store) throws Exception { assertThat(file.delete()).isTrue(); filter.close(); + executorService.shutdown(); } } diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/filters/PersistentAcceptOnceFileListFilterTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/filters/PersistentAcceptOnceFileListFilterTests.java index b70ec87d006..5373503763b 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/filters/PersistentAcceptOnceFileListFilterTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/filters/PersistentAcceptOnceFileListFilterTests.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -82,8 +83,8 @@ public boolean replace(String key, String oldValue, String newValue) { suspend.set(true); file.setLastModified(file.lastModified() + 5000L); - Future result = Executors.newSingleThreadExecutor() - .submit(() -> filter.filterFiles(new File[] {file}).size()); + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future result = executorService.submit(() -> filter.filterFiles(new File[] {file}).size()); assertThat(latch2.await(10, TimeUnit.SECONDS)).isTrue(); store.put("foo:" + file.getAbsolutePath(), "43"); latch1.countDown(); @@ -92,6 +93,7 @@ public boolean replace(String key, String oldValue, String newValue) { file.delete(); filter.close(); + executorService.shutdown(); } @Override diff --git a/spring-integration-hazelcast/src/main/java/org/springframework/integration/hazelcast/leader/LeaderInitiator.java b/spring-integration-hazelcast/src/main/java/org/springframework/integration/hazelcast/leader/LeaderInitiator.java index 1abd8276ccc..726aee7035b 100644 --- a/spring-integration-hazelcast/src/main/java/org/springframework/integration/hazelcast/leader/LeaderInitiator.java +++ b/spring-integration-hazelcast/src/main/java/org/springframework/integration/hazelcast/leader/LeaderInitiator.java @@ -56,6 +56,7 @@ * @author Alexey Tsoy * @author Robert Höglund * @author Christian Tzolov + * @author Emil Palm */ public class LeaderInitiator implements SmartLifecycle, DisposableBean, ApplicationEventPublisherAware { @@ -317,6 +318,10 @@ public Void call() { this.leader = true; handleGranted(); } + if (!acquired && this.leader) { + //If we no longer can acquire the lock but still have the leader status + revokeLeadership(); + } } } catch (Exception ex) { diff --git a/spring-integration-hazelcast/src/test/java/org/springframework/integration/hazelcast/leader/LeaderInitiatorTests.java b/spring-integration-hazelcast/src/test/java/org/springframework/integration/hazelcast/leader/LeaderInitiatorTests.java index 114e378dbde..514a8967dce 100644 --- a/spring-integration-hazelcast/src/test/java/org/springframework/integration/hazelcast/leader/LeaderInitiatorTests.java +++ b/spring-integration-hazelcast/src/test/java/org/springframework/integration/hazelcast/leader/LeaderInitiatorTests.java @@ -24,26 +24,33 @@ import com.hazelcast.config.Config; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; -import org.junit.Test; -import org.junit.runner.RunWith; +import com.hazelcast.cp.CPGroupId; +import com.hazelcast.cp.CPSubsystem; +import com.hazelcast.cp.lock.FencedLock; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.integration.leader.Candidate; import org.springframework.integration.leader.Context; import org.springframework.integration.leader.DefaultCandidate; import org.springframework.integration.leader.event.AbstractLeaderEvent; import org.springframework.integration.leader.event.DefaultLeaderEventPublisher; import org.springframework.integration.leader.event.LeaderEventPublisher; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * Tests for hazelcast leader election. @@ -53,9 +60,9 @@ * @author Dave Syer * @author Artem Bilan * @author Mael Le Guével + * @author Emil Palm */ -@RunWith(SpringRunner.class) -@ContextConfiguration +@SpringJUnitConfig @DirtiesContext public class LeaderInitiatorTests { @@ -204,16 +211,64 @@ public void publishOnGranted(Object source, Context context, String role) { initiator.destroy(); } + @Test + public void testRevokeLeadershipCalledWhenLockNotAcquiredButStillLeader() throws Exception { + // Initialize mocks and objects needed for the revoke leadership when fenced lock is no longer acquired + HazelcastInstance hazelcastInstance = mock(); + Candidate candidate = mock(); + FencedLock fencedLock = mock(); + LeaderEventPublisher leaderEventPublisher = mock(); + + CPSubsystem cpSubsystem = mock(CPSubsystem.class); + given(candidate.getRole()).willReturn("role"); + given(hazelcastInstance.getCPSubsystem()).willReturn(cpSubsystem); + given(cpSubsystem.getLock(anyString())).willReturn(fencedLock); + given(fencedLock.getGroupId()) + .willReturn(new CPGroupId() { + + @Override + public String getName() { + return ""; + } + + @Override + public long getId() { + return 0; + } + }); + + LeaderInitiator leaderInitiator = new LeaderInitiator(hazelcastInstance, candidate); + leaderInitiator.setLeaderEventPublisher(leaderEventPublisher); + + // Simulate that the lock is currently held by this thread + given(fencedLock.isLockedByCurrentThread()).willReturn(true, false); + given(fencedLock.tryLock(anyLong(), any(TimeUnit.class))).willReturn(false); // Lock acquisition fails + + // Start the LeaderInitiator to trigger the leader election process + leaderInitiator.start(); + + // Simulate the lock acquisition check process + Thread.sleep(1000); // Give time for the async task to run + + // Verify that revokeLeadership was called due to lock not being acquired + // unlock is part of revokeLeadership + verify(fencedLock).unlock(); + // verify revoke event is published + verify(leaderEventPublisher).publishOnRevoked(any(Object.class), any(Context.class), anyString()); + + leaderInitiator.destroy(); + } + @Configuration public static class TestConfig { @Bean - public TestCandidate candidate() { + TestCandidate candidate() { return new TestCandidate(); } @Bean - public Config hazelcastConfig() { + Config hazelcastConfig() { Config config = new Config(); config.getCPSubsystemConfig() .setSessionHeartbeatIntervalSeconds(1); @@ -221,17 +276,17 @@ public Config hazelcastConfig() { } @Bean(destroyMethod = "shutdown") - public HazelcastInstance hazelcastInstance() { + HazelcastInstance hazelcastInstance() { return Hazelcast.newHazelcastInstance(hazelcastConfig()); } @Bean - public LeaderInitiator initiator() { + LeaderInitiator initiator() { return new LeaderInitiator(hazelcastInstance(), candidate()); } @Bean - public TestEventListener testEventListener() { + TestEventListener testEventListener() { return new TestEventListener(); } diff --git a/spring-integration-hazelcast/src/test/java/org/springframework/integration/hazelcast/lock/HazelcastLockRegistryTests.java b/spring-integration-hazelcast/src/test/java/org/springframework/integration/hazelcast/lock/HazelcastLockRegistryTests.java index d254d8ec75b..21346f39fd5 100644 --- a/spring-integration-hazelcast/src/test/java/org/springframework/integration/hazelcast/lock/HazelcastLockRegistryTests.java +++ b/spring-integration-hazelcast/src/test/java/org/springframework/integration/hazelcast/lock/HazelcastLockRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.integration.hazelcast.lock; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -28,8 +29,8 @@ import com.hazelcast.core.HazelcastInstance; import com.hazelcast.cp.lock.FencedLock; import com.hazelcast.instance.impl.HazelcastInstanceFactory; -import org.junit.AfterClass; -import org.junit.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -46,7 +47,7 @@ public class HazelcastLockRegistryTests { private static final HazelcastInstance instance = Hazelcast.newHazelcastInstance(CONFIG); - @AfterClass + @AfterAll public static void destroy() { HazelcastInstanceFactory.terminateAll(); } @@ -144,7 +145,8 @@ public void testTwoThreadsSecondFailsToGetLock() throws Exception { lock1.lockInterruptibly(); AtomicBoolean locked = new AtomicBoolean(); CountDownLatch latch = new CountDownLatch(1); - Future result = Executors.newSingleThreadExecutor().submit(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future result = executorService.submit(() -> { Lock lock2 = registry.obtain("foo"); locked.set(lock2.tryLock(200, TimeUnit.MILLISECONDS)); latch.countDown(); @@ -162,6 +164,7 @@ public void testTwoThreadsSecondFailsToGetLock() throws Exception { Object ise = result.get(10, TimeUnit.SECONDS); assertThat(ise).isInstanceOf(IllegalMonitorStateException.class); assertThat(((Exception) ise).getMessage()).contains("Current thread is not owner of the lock!"); + executorService.shutdown(); } @Test @@ -173,7 +176,8 @@ public void testTwoThreads() throws Exception { CountDownLatch latch2 = new CountDownLatch(1); CountDownLatch latch3 = new CountDownLatch(1); lock1.lockInterruptibly(); - Executors.newSingleThreadExecutor().execute(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { Lock lock2 = registry.obtain("foo"); try { latch1.countDown(); @@ -195,6 +199,7 @@ public void testTwoThreads() throws Exception { latch2.countDown(); assertThat(latch3.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(locked.get()).isTrue(); + executorService.shutdown(); } @Test @@ -207,7 +212,8 @@ public void testTwoThreadsDifferentRegistries() throws Exception { CountDownLatch latch2 = new CountDownLatch(1); CountDownLatch latch3 = new CountDownLatch(1); lock1.lockInterruptibly(); - Executors.newSingleThreadExecutor().execute(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { Lock lock2 = registry2.obtain("foo"); try { latch1.countDown(); @@ -229,6 +235,7 @@ public void testTwoThreadsDifferentRegistries() throws Exception { latch2.countDown(); assertThat(latch3.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(locked.get()).isTrue(); + executorService.shutdown(); } @Test @@ -238,7 +245,8 @@ public void testTwoThreadsWrongOneUnlocks() throws Exception { lock.lockInterruptibly(); final AtomicBoolean locked = new AtomicBoolean(); final CountDownLatch latch = new CountDownLatch(1); - Future result = Executors.newSingleThreadExecutor().submit(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future result = executorService.submit(() -> { try { lock.unlock(); } @@ -254,6 +262,7 @@ public void testTwoThreadsWrongOneUnlocks() throws Exception { Object imse = result.get(10, TimeUnit.SECONDS); assertThat(imse).isInstanceOf(IllegalMonitorStateException.class); assertThat(((Exception) imse).getMessage()).contains("Current thread is not owner of the lock!"); + executorService.shutdown(); } @Test diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java index e279004c21a..db98e61a377 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ import javax.xml.transform.Source; import org.springframework.beans.factory.BeanFactory; -import org.springframework.context.ApplicationContext; import org.springframework.core.ParameterizedTypeReference; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; @@ -100,6 +99,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac private boolean expectReply = true; + @Nullable private Expression expectedResponseTypeExpression; private boolean extractPayload = true; @@ -114,6 +114,7 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac private HeaderMapper headerMapper = DefaultHttpHeaderMapper.outboundMapper(); + @Nullable private Expression uriVariablesExpression; public AbstractHttpRequestExecutingMessageHandler(Expression uriExpression) { @@ -196,7 +197,7 @@ public void setExpectReply(boolean expectReply) { * Specify the expected response type for the REST request. * Otherwise, it is null and an empty {@link ResponseEntity} is returned from HTTP client. * To take advantage of the HttpMessageConverters - * registered on this adapter, provide a different type). + * registered on this adapter, provide a different type. * @param expectedResponseType The expected type. * Also see {@link #setExpectedResponseTypeExpression(Expression)} */ @@ -322,7 +323,7 @@ protected Object handleRequestMessage(Message requestMessage) { @Nullable protected abstract Object exchange(Object uri, HttpMethod httpMethod, HttpEntity httpRequest, - Object expectedResponseType, Message requestMessage, Map uriVariables); + @Nullable Object expectedResponseType, Message requestMessage, @Nullable Map uriVariables); protected Object getReply(ResponseEntity httpResponse) { HttpHeaders httpHeaders = httpResponse.getHeaders(); @@ -377,6 +378,7 @@ private HttpEntity createHttpEntityFromPayload(Message message, HttpMethod } HttpHeaders httpHeaders = mapHeaders(message); if (!shouldIncludeRequestBody(httpMethod)) { + httpHeaders.remove(HttpHeaders.CONTENT_LENGTH); return new HttpEntity<>(httpHeaders); } // otherwise, we are creating a request with a body and need to deal with the content-type header as well @@ -514,6 +516,7 @@ private HttpMethod determineHttpMethod(Message requestMessage) { } } + @Nullable private Object determineExpectedResponseType(Message requestMessage) { return evaluateTypeFromExpression(requestMessage, this.expectedResponseTypeExpression, "expectedResponseType"); } @@ -536,9 +539,7 @@ protected Object evaluateTypeFromExpression(Message requestMessage, @Nullable "evaluation resulted in a " + typeClass + "."); if (type instanceof String && StringUtils.hasText((String) type)) { try { - ApplicationContext applicationContext = getApplicationContext(); - type = ClassUtils.forName((String) type, - applicationContext == null ? null : applicationContext.getClassLoader()); + type = ClassUtils.forName((String) type, getApplicationContext().getClassLoader()); } catch (ClassNotFoundException e) { throw new IllegalStateException("Cannot load class for name: " + type, e); diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java index e1fbe5c087e..b9ad0e598e1 100755 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,7 +97,7 @@ public HttpRequestExecutingMessageHandler(Expression uriExpression) { * @param uri The URI. * @param restTemplate The rest template. */ - public HttpRequestExecutingMessageHandler(String uri, RestTemplate restTemplate) { + public HttpRequestExecutingMessageHandler(String uri, @Nullable RestTemplate restTemplate) { this(new LiteralExpression(uri), restTemplate); /* * We'd prefer to do this assertion first, but the compiler doesn't allow it. However, @@ -173,7 +173,7 @@ public void setEncodingMode(DefaultUriBuilderFactory.EncodingMode encodingMode) @Override @Nullable protected Object exchange(Object uri, HttpMethod httpMethod, HttpEntity httpRequest, - Object expectedResponseType, Message requestMessage, Map uriVariables) { + @Nullable Object expectedResponseType, Message requestMessage, @Nullable Map uriVariables) { ResponseEntity httpResponse; try { diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/package-info.java b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/package-info.java index c829cdd5c79..63a84c612ab 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/package-info.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/package-info.java @@ -1,4 +1,6 @@ /** * Provides classes supporting outbound endpoints. */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields package org.springframework.integration.http.outbound; diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/support/DefaultHttpHeaderMapper.java b/spring-integration-http/src/main/java/org/springframework/integration/http/support/DefaultHttpHeaderMapper.java index c47dbe912e5..066d0fe606c 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/support/DefaultHttpHeaderMapper.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/support/DefaultHttpHeaderMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -202,10 +202,10 @@ public class DefaultHttpHeaderMapper implements HeaderMapper, BeanF static { for (String header : HTTP_REQUEST_HEADER_NAMES) { - HTTP_REQUEST_HEADER_NAMES_LOWER.add(header.toLowerCase()); + HTTP_REQUEST_HEADER_NAMES_LOWER.add(header.toLowerCase(Locale.ROOT)); } for (String header : HTTP_RESPONSE_HEADER_NAMES) { - HTTP_RESPONSE_HEADER_NAMES_LOWER.add(header.toLowerCase()); + HTTP_RESPONSE_HEADER_NAMES_LOWER.add(header.toLowerCase(Locale.ROOT)); } } @@ -266,13 +266,13 @@ else if (Arrays.equals(HTTP_RESPONSE_HEADER_NAMES, outboundHeaderNames)) { outboundHeaderNamesLower[i] = this.outboundHeaderNames[i]; } else { - outboundHeaderNamesLower[i] = this.outboundHeaderNames[i].toLowerCase(); + outboundHeaderNamesLower[i] = this.outboundHeaderNames[i].toLowerCase(Locale.ROOT); } } this.outboundHeaderNamesLowerWithContentType = Arrays.copyOf(outboundHeaderNamesLower, this.outboundHeaderNames.length + 1); this.outboundHeaderNamesLowerWithContentType[this.outboundHeaderNamesLowerWithContentType.length - 1] - = MessageHeaders.CONTENT_TYPE.toLowerCase(); + = MessageHeaders.CONTENT_TYPE.toLowerCase(Locale.ROOT); } /** @@ -298,7 +298,7 @@ public void setInboundHeaderNames(String... inboundHeaderNamesArg) { this.inboundHeaderNamesLower[i] = this.inboundHeaderNames[i]; } else { - this.inboundHeaderNamesLower[i] = this.inboundHeaderNames[i].toLowerCase(); + this.inboundHeaderNamesLower[i] = this.inboundHeaderNames[i].toLowerCase(Locale.ROOT); } } } @@ -360,7 +360,7 @@ public void fromHeaders(MessageHeaders headers, HttpHeaders target) { for (Entry entry : headers.entrySet()) { String name = entry.getKey(); Object value = entry.getValue(); - String lowerName = name.toLowerCase(); + String lowerName = name.toLowerCase(Locale.ROOT); if (value != null && shouldMapOutboundHeader(lowerName)) { if (!HTTP_REQUEST_HEADER_NAMES_LOWER.contains(lowerName) && !HTTP_RESPONSE_HEADER_NAMES_LOWER.contains(lowerName) && @@ -380,7 +380,7 @@ public void fromHeaders(MessageHeaders headers, HttpHeaders target) { } private void setHttpHeader(HttpHeaders target, String name, Object value) { // NOSONAR - switch (name.toLowerCase()) { + switch (name.toLowerCase(Locale.ROOT)) { case ACCEPT_LOWER: setAccept(target, value); break; @@ -775,7 +775,7 @@ public Map toHeaders(HttpHeaders source) { Map target = new HashMap<>(); Set headerNames = source.keySet(); for (String name : headerNames) { - String lowerName = name.toLowerCase(); + String lowerName = name.toLowerCase(Locale.ROOT); if (shouldMapInboundHeader(lowerName)) { if (!HTTP_REQUEST_HEADER_NAMES_LOWER.contains(lowerName) && !HTTP_RESPONSE_HEADER_NAMES_LOWER.contains(lowerName)) { @@ -810,49 +810,51 @@ private void populateStandardHeader(HttpHeaders source, Map targ } protected Object getHttpHeader(HttpHeaders source, String name) { // NOSONAR - switch (name.toLowerCase()) { - case ACCEPT_LOWER: - return source.getAccept(); - case ACCEPT_CHARSET_LOWER: - return source.getAcceptCharset(); - case ALLOW_LOWER: - return source.getAllow(); - case CACHE_CONTROL_LOWER: + return switch (name.toLowerCase(Locale.ROOT)) { + case ACCEPT_LOWER -> source.getAccept(); + case ACCEPT_CHARSET_LOWER -> source.getAcceptCharset(); + case ALLOW_LOWER -> source.getAllow(); + case CACHE_CONTROL_LOWER -> { String cacheControl = source.getCacheControl(); - return (StringUtils.hasText(cacheControl)) ? cacheControl : null; - case CONTENT_LENGTH_LOWER: + yield (StringUtils.hasText(cacheControl)) ? cacheControl : null; + } + case CONTENT_LENGTH_LOWER -> { long contentLength = source.getContentLength(); - return (contentLength > -1) ? contentLength : null; - case CONTENT_TYPE_LOWER: - return source.getContentType(); - case DATE_LOWER: + yield (contentLength > -1) ? contentLength : null; + } + case CONTENT_TYPE_LOWER -> source.getContentType(); + case DATE_LOWER -> { long date = source.getDate(); - return (date > -1) ? date : null; - case ETAG_LOWER: + yield (date > -1) ? date : null; + } + case ETAG_LOWER -> { String eTag = source.getETag(); - return (StringUtils.hasText(eTag)) ? eTag : null; - case EXPIRES_LOWER: + yield (StringUtils.hasText(eTag)) ? eTag : null; + } + case EXPIRES_LOWER -> { long expires = source.getExpires(); - return (expires > -1) ? expires : null; - case IF_NONE_MATCH_LOWER: - return source.getIfNoneMatch(); - case IF_MODIFIED_SINCE_LOWER: + yield (expires > -1) ? expires : null; + } + case IF_NONE_MATCH_LOWER -> source.getIfNoneMatch(); + case IF_MODIFIED_SINCE_LOWER -> { long modifiedSince = source.getIfModifiedSince(); - return (modifiedSince > -1) ? modifiedSince : null; - case IF_UNMODIFIED_SINCE_LOWER: + yield (modifiedSince > -1) ? modifiedSince : null; + } + case IF_UNMODIFIED_SINCE_LOWER -> { long unmodifiedSince = source.getIfUnmodifiedSince(); - return (unmodifiedSince > -1) ? unmodifiedSince : null; - case LAST_MODIFIED_LOWER: + yield (unmodifiedSince > -1) ? unmodifiedSince : null; + } + case LAST_MODIFIED_LOWER -> { long lastModified = source.getLastModified(); - return (lastModified > -1) ? lastModified : null; - case LOCATION_LOWER: - return source.getLocation(); - case PRAGMA_LOWER: + yield (lastModified > -1) ? lastModified : null; + } + case LOCATION_LOWER -> source.getLocation(); + case PRAGMA_LOWER -> { String pragma = source.getPragma(); - return (StringUtils.hasText(pragma)) ? pragma : null; - default: - return source.get(name); - } + yield (StringUtils.hasText(pragma)) ? pragma : null; + } + default -> source.get(name); + }; } private void setMessageHeader(Map target, String name, Object value) { diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/HttpRequestHandlingControllerTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/HttpRequestHandlingControllerTests.java index 5611a5b5760..a938b885e2c 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/HttpRequestHandlingControllerTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/inbound/HttpRequestHandlingControllerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.integration.http.inbound; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -357,7 +358,8 @@ protected Object handleRequestMessage(Message requestMessage) { MockHttpServletResponse response = new MockHttpServletResponse(); final AtomicInteger active = new AtomicInteger(); final AtomicBoolean expected503 = new AtomicBoolean(); - Executors.newSingleThreadExecutor().execute(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { try { // wait for the active thread latch2.await(10, TimeUnit.SECONDS); @@ -387,6 +389,7 @@ protected Object handleRequestMessage(Message requestMessage) { Object reply = modelAndView.getModel().get("reply"); assertThat(reply).isNotNull(); assertThat(reply).isEqualTo("HELLO"); + executorService.shutdown(); } @Test diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/TcpSendingMessageHandler.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/TcpSendingMessageHandler.java index fe33345b7f3..ceaa8d437c4 100644 --- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/TcpSendingMessageHandler.java +++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/TcpSendingMessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -90,7 +90,6 @@ protected TcpConnection obtainConnection(Message message) { connection = this.clientConnectionFactory.getConnection(); } catch (Exception ex) { - logger.error(ex, "Error creating connection"); throw new MessageHandlingException(message, "Failed to obtain a connection in the [" + this + ']', ex); } return connection; @@ -123,7 +122,6 @@ private void handleMessageAsServer(Message message) { connection.send(message); } catch (Exception ex) { - logger.error(ex, "Error sending message"); connection.close(); throw IntegrationUtils.wrapInHandlingExceptionIfNecessary(message, () -> "Error sending message in the [" + this + ']', ex); @@ -135,7 +133,6 @@ private void handleMessageAsServer(Message message) { } } else { - logger.error(() -> "Unable to find outbound socket for " + message); MessageHandlingException messageHandlingException = new MessageHandlingException(message, "Unable to find outbound socket in the [" + this + ']'); publishNoConnectionEvent(messageHandlingException, connectionId); diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/AbstractConnectionFactory.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/AbstractConnectionFactory.java index 55c6da1f631..b8ebbb995d4 100644 --- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/AbstractConnectionFactory.java +++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/AbstractConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,9 +78,9 @@ public abstract class AbstractConnectionFactory extends IntegrationObjectSupport protected final Lock lifecycleMonitor = new ReentrantLock(); // NOSONAR final - private final Map connections = new ConcurrentHashMap<>(); + protected final Map connections = new ConcurrentHashMap<>(); // NOSONAR final - private final Lock connectionsMonitor = new ReentrantLock(); + protected final Lock connectionsMonitor = new ReentrantLock(); // NOSONAR final private final BlockingQueue delayedReads = new LinkedBlockingQueue<>(); diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/CachingClientConnectionFactory.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/CachingClientConnectionFactory.java index 50c3f600bff..1e5f8594aca 100644 --- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/CachingClientConnectionFactory.java +++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/CachingClientConnectionFactory.java @@ -33,6 +33,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.support.ErrorMessage; +import org.springframework.util.Assert; /** * Connection factory that caches connections from the underlying target factory. The underlying @@ -358,6 +359,34 @@ public void enableManualListenerRegistration() { this.targetConnectionFactory.enableManualListenerRegistration(); } + @Override + public boolean closeConnection(String connectionId) { + Assert.notNull(connectionId, "'connectionId' to close must not be null"); + String targetConnectionId = connectionId.replaceFirst("Cached:", ""); + this.connectionsMonitor.lock(); + try { + TcpConnectionSupport targetConnection = this.targetConnectionFactory.connections.get(targetConnectionId); + if (targetConnection != null) { + /* + * If the delegate is stopped, actually close the connection, but still release + * it to the pool, it will be discarded/renewed the next time it is retrieved. + */ + if (!isRunning()) { + logger.debug(() -> "Factory not running - closing " + connectionId); + super.closeConnection(targetConnectionId); + } + CachingClientConnectionFactory.this.pool.releaseItem(targetConnection); + return true; + } + else { + return false; + } + } + finally { + this.connectionsMonitor.unlock(); + } + } + @Override public void start() { setActive(true); diff --git a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioClientConnectionFactory.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioClientConnectionFactory.java index e23a4a4c455..221d789474d 100644 --- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioClientConnectionFactory.java +++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNioClientConnectionFactory.java @@ -39,6 +39,7 @@ * @author Gary Russell * @author Artem Bilan * @author Christian Tzolov + * @author Jooyoung Pyoung * * @since 2.0 * @@ -84,8 +85,9 @@ protected void checkActive() { @Override protected TcpConnectionSupport buildNewConnection() { + SocketChannel socketChannel = null; try { - SocketChannel socketChannel = SocketChannel.open(); + socketChannel = SocketChannel.open(); setSocketAttributes(socketChannel.socket()); connect(socketChannel); TcpNioConnection connection = @@ -112,6 +114,14 @@ protected TcpConnectionSupport buildNewConnection() { return wrappedConnection; } catch (IOException e) { + try { + if (socketChannel != null) { + socketChannel.close(); + } + } + catch (IOException e2) { + logger.error(e2, "Error closing socket channel"); + } throw new UncheckedIOException(e); } catch (InterruptedException e) { diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/dsl/IpIntegrationTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/dsl/IpIntegrationTests.java index b283e158f63..a243d6b4684 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/dsl/IpIntegrationTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/dsl/IpIntegrationTests.java @@ -16,11 +16,14 @@ package org.springframework.integration.ip.dsl; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; import org.aopalliance.intercept.MethodInterceptor; import org.junit.jupiter.api.Test; @@ -32,6 +35,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; import org.springframework.integration.MessageTimeoutException; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.config.EnableIntegration; @@ -47,6 +51,8 @@ import org.springframework.integration.ip.tcp.TcpSendingMessageHandler; import org.springframework.integration.ip.tcp.connection.AbstractClientConnectionFactory; import org.springframework.integration.ip.tcp.connection.AbstractServerConnectionFactory; +import org.springframework.integration.ip.tcp.connection.CachingClientConnectionFactory; +import org.springframework.integration.ip.tcp.connection.TcpConnectionOpenEvent; import org.springframework.integration.ip.tcp.connection.TcpConnectionServerListeningEvent; import org.springframework.integration.ip.tcp.connection.TcpNetClientConnectionFactory; import org.springframework.integration.ip.tcp.connection.TcpNetServerConnectionFactory; @@ -55,12 +61,12 @@ import org.springframework.integration.ip.udp.MulticastSendingMessageHandler; import org.springframework.integration.ip.udp.UdpServerListeningEvent; import org.springframework.integration.ip.udp.UnicastReceivingChannelAdapter; -import org.springframework.integration.ip.udp.UnicastSendingMessageHandler; import org.springframework.integration.ip.util.TestingUtilities; import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.PollableChannel; import org.springframework.messaging.support.GenericMessage; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -102,9 +108,6 @@ public class IpIntegrationTests { @Autowired private UnicastReceivingChannelAdapter udpInbound; - @Autowired - private UnicastSendingMessageHandler udpOutbound; - @Autowired private QueueChannel udpIn; @@ -236,6 +239,48 @@ void async() { assertThat(TestUtils.getPropertyValue(this.tcpOutAsync, "async", Boolean.class)).isTrue(); } + @Autowired + private AbstractServerConnectionFactory server2; + + @Autowired + private TcpNetClientConnectionFactory client3; + + @Autowired + @Qualifier("outboundFlow.input") + MessageChannel outboundFlowInput; + + @Autowired + PollableChannel cachingRepliesChannel; + + @Test + void allRepliesAreReceivedViaLimitedCachingConnectionFactory() { + this.client3.stop(); + TestingUtilities.waitListening(this.server2, null); + this.client3.setPort(this.server2.getPort()); + this.client3.start(); + + List expected = + IntStream.rangeClosed('a', 'z') + .mapToObj((characterCode) -> (char) characterCode) + .map((character) -> "" + character) + .parallel() + .peek((character) -> this.outboundFlowInput.send(new GenericMessage<>(character))) + .map(String::toUpperCase) + .toList(); + + List replies = new ArrayList<>(); + + for (int i = 0; i < expected.size(); i++) { + Message replyMessage = this.cachingRepliesChannel.receive(10_000); + assertThat(replyMessage).isNotNull(); + replies.add(replyMessage.getPayload().toString()); + } + + assertThat(replies).containsAll(expected); + + assertThat(config.openEvents).hasSizeLessThanOrEqualTo(5); + } + @Configuration @EnableIntegration public static class Config { @@ -318,8 +363,9 @@ public ApplicationListener events() { } @Bean - public TcpNetClientConnectionFactorySpec client1(TcpNetServerConnectionFactory server1) { - return Tcp.netClient("localhost", server1.getPort()) + public TcpNetClientConnectionFactorySpec client1() { + // The port from server is assigned + return Tcp.netClient("localhost", 0) .serializer(TcpCodecs.crlf()) .deserializer(TcpCodecs.lengthHeader1()); } @@ -337,8 +383,9 @@ public QueueChannel unsolicited() { } @Bean - public TcpNetClientConnectionFactorySpec client2(TcpNetServerConnectionFactory server1) { - return Tcp.netClient("localhost", server1.getPort()) + public TcpNetClientConnectionFactorySpec client2() { + // The port from server is assigned + return Tcp.netClient("localhost", 0) .serializer(TcpCodecs.crlf()) .deserializer(TcpCodecs.lengthHeader1()); } @@ -370,6 +417,54 @@ public IntegrationFlow clientTcpFlow(TcpOutboundGateway tcpOut) { .transform(Transformers.objectToString()); } + @Bean + public TcpNetServerConnectionFactorySpec server2() { + return Tcp.netServer(0); + } + + @Bean + public IntegrationFlow server2Flow(TcpNetServerConnectionFactory server2) { + return IntegrationFlow.from(Tcp.inboundGateway(server2)) + .transform(Transformers.objectToString()) + .transform(String::toUpperCase) + .get(); + } + + @Bean + public TcpNetClientConnectionFactorySpec client3() { + // The port from server is assigned + return Tcp.netClient("localhost", 0); + } + + final List openEvents = new ArrayList<>(); + + @EventListener + void connectionOpened(TcpConnectionOpenEvent tcpConnectionOpenEvent) { + if ("client3".equals(tcpConnectionOpenEvent.getConnectionFactoryName())) { + this.openEvents.add(tcpConnectionOpenEvent); + } + } + + @Bean + CachingClientConnectionFactory cachingClient(TcpNetClientConnectionFactory client3) { + var cachingClientConnectionFactory = new CachingClientConnectionFactory(client3, 5); + cachingClientConnectionFactory.setConnectionWaitTimeout(10_000); + return cachingClientConnectionFactory; + } + + @Bean + IntegrationFlow outboundFlow(CachingClientConnectionFactory cachingClient) { + return (flow) -> flow.handle(Tcp.outboundAdapter(cachingClient)); + } + + @Bean + IntegrationFlow inboundFlow(CachingClientConnectionFactory cachingClient) { + return IntegrationFlow.from(Tcp.inboundAdapter(cachingClient)) + .transform(Transformers.objectToString()) + .channel((channels) -> channels.queue("cachingRepliesChannel")) + .get(); + } + } } diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/CachingClientConnectionFactoryTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/CachingClientConnectionFactoryTests.java index b339edafe96..ca0e3a25508 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/CachingClientConnectionFactoryTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/CachingClientConnectionFactoryTests.java @@ -389,12 +389,7 @@ private TcpConnectionSupport mockedTcpNetConnection() throws IOException { private TcpConnectionSupport mockedTcpNioConnection() throws Exception { SocketChannel socketChannel = mock(SocketChannel.class); - if (System.getProperty("java.version").startsWith("1.8")) { - new DirectFieldAccessor(socketChannel).setPropertyValue("open", false); - } - else { - new DirectFieldAccessor(socketChannel).setPropertyValue("closed", true); - } + new DirectFieldAccessor(socketChannel).setPropertyValue("closed", true); doThrow(new IOException("Foo")).when(socketChannel).write(Mockito.any(ByteBuffer.class)); when(socketChannel.socket()).thenReturn(mock(Socket.class)); TcpNioConnection conn = new TcpNioConnection(socketChannel, false, false, event -> { diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionReadTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionReadTests.java index 68f0d564717..813e18619dd 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionReadTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpNioConnectionReadTests.java @@ -296,7 +296,7 @@ public void removeDeadConnection(TcpConnection connection) { @Test public void testReadCrLfOverflow() throws Exception { ByteArrayCrLfSerializer serializer = new ByteArrayCrLfSerializer(); - serializer.setMaxMessageSize(1024); + serializer.setMaxMessageSize(16); final Semaphore semaphore = new Semaphore(0); final List added = new ArrayList<>(); final List removed = new ArrayList<>(); @@ -332,14 +332,14 @@ public void removeDeadConnection(TcpConnection connection) { whileOpen(semaphore, added); assertThat(added.size()).isEqualTo(1); - assertThat(errorMessageLetch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(errorMessageLetch.await(20, TimeUnit.SECONDS)).isTrue(); assertThat(errorMessageRef.get().getMessage()) .satisfiesAnyOf( - s -> assertThat(s).contains("CRLF not found before max message length: 1024"), + s -> assertThat(s).contains("CRLF not found before max message length: 16"), s -> assertThat(s).contains("Connection is closed")); - assertThat(semaphore.tryAcquire(10000, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(semaphore.tryAcquire(20, TimeUnit.SECONDS)).isTrue(); assertThat(removed).hasSizeGreaterThan(0); scf.stop(); done.countDown(); @@ -531,9 +531,9 @@ public void removeDeadConnection(TcpConnection connection) { scf.stop(); } - private void whileOpen(Semaphore semaphore, final List added) + private void whileOpen(Semaphore semaphore, List added) throws InterruptedException { - assertThat(semaphore.tryAcquire(10000, TimeUnit.MILLISECONDS)).isTrue(); + assertThat(semaphore.tryAcquire(20, TimeUnit.SECONDS)).isTrue(); with().pollInterval(Duration.ofMillis(50)).await("Failed to close socket") .atMost(Duration.ofSeconds(20)) .until(() -> !added.get(0).isOpen()); diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/serializer/DeserializationTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/serializer/DeserializationTests.java index 842fc3f231b..f6ecaa33f55 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/serializer/DeserializationTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/serializer/DeserializationTests.java @@ -28,8 +28,7 @@ import javax.net.ServerSocketFactory; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.core.serializer.DefaultDeserializer; @@ -42,12 +41,12 @@ import org.springframework.integration.ip.util.SocketTestUtils; import org.springframework.integration.ip.util.TestingUtilities; import org.springframework.integration.support.MessageBuilder; -import org.springframework.integration.test.support.LongRunningIntegrationTest; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.support.GenericMessage; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; import static org.assertj.core.api.Assertions.fail; import static org.mockito.Mockito.mock; @@ -60,9 +59,6 @@ */ public class DeserializationTests { - @Rule - public LongRunningIntegrationTest longRunningIntegrationTest = new LongRunningIntegrationTest(); - @Test public void testReadLength() throws Exception { ServerSocket server = ServerSocketFactory.getDefault().createServerSocket(0); @@ -240,17 +236,11 @@ public void testReadCrLfTimeout() throws Exception { server.setSoTimeout(10000); CountDownLatch latch = SocketTestUtils.testSendCrLfOverflow(port); Socket socket = server.accept(); - socket.setSoTimeout(500); + socket.setSoTimeout(100); ByteArrayCrLfSerializer serializer = new ByteArrayCrLfSerializer(); - try { - serializer.deserialize(socket.getInputStream()); - fail("Expected timout exception"); - } - catch (IOException e) { - if (!e.getMessage().startsWith("Read timed out")) { - fail("Unexpected IO Error:" + e.getMessage()); - } - } + assertThatIOException() + .isThrownBy(() -> serializer.deserialize(socket.getInputStream())) + .withMessageStartingWith("Read timed out"); server.close(); latch.countDown(); } @@ -264,16 +254,10 @@ public void testReadCrLfOverflow() throws Exception { Socket socket = server.accept(); socket.setSoTimeout(5000); ByteArrayCrLfSerializer serializer = new ByteArrayCrLfSerializer(); - serializer.setMaxMessageSize(1024); - try { - serializer.deserialize(socket.getInputStream()); - fail("Expected message length exceeded exception"); - } - catch (IOException e) { - if (!e.getMessage().startsWith("CRLF not found")) { - fail("Unexpected IO Error:" + e.getMessage()); - } - } + serializer.setMaxMessageSize(16); + assertThatIOException() + .isThrownBy(() -> serializer.deserialize(socket.getInputStream())) + .withMessageStartingWith("CRLF not found"); server.close(); latch.countDown(); } @@ -306,7 +290,8 @@ public void deserializationEvents() { assertThat(new String(event.getBuffer()).substring(0, 1)).isEqualTo(new String(new byte[] {7})); doDeserialize(new ByteArrayLfSerializer(), "Terminator '0xa' not found before max message length: 5"); doDeserialize(new ByteArrayRawSerializer(), "Socket was not closed before max message length: 5"); - doDeserialize(new ByteArraySingleTerminatorSerializer((byte) 0xfe), "Terminator '0xfe' not found before max message length: 5"); + doDeserialize(new ByteArraySingleTerminatorSerializer((byte) 0xfe), + "Terminator '0xfe' not found before max message length: 5"); doDeserialize(new ByteArrayStxEtxSerializer(), "Expected STX to begin message"); event = doDeserialize(new ByteArrayStxEtxSerializer(), "Socket closed during message assembly", new byte[] {0x02, 0, 0}, 5); @@ -365,7 +350,7 @@ private void testTimeoutWhileDecoding(AbstractByteArraySerializer deserializer, TcpNioClientConnectionFactory clientNio = new TcpNioClientConnectionFactory("localhost", serverNio.getPort()); clientNio.setSerializer(serializer); clientNio.setDeserializer(deserializer); - clientNio.setSoTimeout(1000); + clientNio.setSoTimeout(500); clientNio.afterPropertiesSet(); final TcpOutboundGateway out = new TcpOutboundGateway(); out.setConnectionFactory(clientNio); diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/util/SocketTestUtils.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/util/SocketTestUtils.java index 8008a545b23..30c8e567c3f 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/util/SocketTestUtils.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/util/SocketTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -325,16 +325,15 @@ public static CountDownLatch testSendCrLfOverflow(final int port) { Thread thread = new Thread(() -> { try (Socket socket = new Socket(InetAddress.getLocalHost(), port)) { OutputStream outputStream = socket.getOutputStream(); - for (int i = 0; i < 1500; i++) { + for (int i = 0; i < 20; i++) { writeByte(outputStream, 'x', true); } - testCompleteLatch.await(10, TimeUnit.SECONDS); + testCompleteLatch.await(30, TimeUnit.SECONDS); } catch (Exception e) { } }); - thread.setDaemon(true); thread.start(); return testCompleteLatch; } diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/channel/PostgresChannelMessageTableSubscriber.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/channel/PostgresChannelMessageTableSubscriber.java index 7a7028f9407..eb0bc800529 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/channel/PostgresChannelMessageTableSubscriber.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/channel/PostgresChannelMessageTableSubscriber.java @@ -19,6 +19,7 @@ import java.sql.SQLException; import java.sql.Statement; import java.time.Duration; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -192,7 +193,7 @@ private void doStart(CountDownLatch startingLatch) { try { PgConnection conn = this.connectionSupplier.get(); try (Statement stmt = conn.createStatement()) { - stmt.execute("LISTEN " + this.tablePrefix.toLowerCase() + "channel_message_notify"); + stmt.execute("LISTEN " + this.tablePrefix.toLowerCase(Locale.ROOT) + "channel_message_notify"); } catch (Exception ex) { try { diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/lock/DefaultLockRepository.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/lock/DefaultLockRepository.java index e442a6d1259..162d160a306 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/lock/DefaultLockRepository.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/lock/DefaultLockRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -305,6 +305,19 @@ public String getRenewQuery() { return this.renewQuery; } + /** + * The flag to perform a database check query on start or not. + * @param checkDatabaseOnStart false to not perform the database check. + * @since 6.2 + */ + public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) { + this.checkDatabaseOnStart = checkDatabaseOnStart; + if (!checkDatabaseOnStart) { + LOGGER.info("The 'DefaultLockRepository' won't be started automatically " + + "and required table is not going be checked."); + } + } + @Override public void afterPropertiesSet() { this.deleteQuery = String.format(this.deleteQuery, this.prefix); @@ -347,19 +360,6 @@ public void afterSingletonsInstantiated() { this.readCommittedTransactionTemplate = new TransactionTemplate(this.transactionManager, transactionDefinition); } - /** - * The flag to perform a database check query on start or not. - * @param checkDatabaseOnStart false to not perform the database check. - * @since 6.2 - */ - public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) { - this.checkDatabaseOnStart = checkDatabaseOnStart; - if (!checkDatabaseOnStart) { - LOGGER.info("The 'DefaultLockRepository' won't be started automatically " + - "and required table is not going be checked."); - } - } - @Override public boolean isAutoStartup() { return this.checkDatabaseOnStart; diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/metadata/JdbcMetadataStore.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/metadata/JdbcMetadataStore.java index 2faca2a2243..478e92e35aa 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/metadata/JdbcMetadataStore.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/metadata/JdbcMetadataStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 the original author or authors. + * Copyright 2017-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,9 @@ import org.springframework.core.log.LogAccessor; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.TransientDataAccessException; import org.springframework.integration.metadata.ConcurrentMetadataStore; +import org.springframework.jdbc.core.ConnectionCallback; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.annotation.Transactional; @@ -168,12 +170,18 @@ public void setLockHint(String lockHint) { @Override public void afterPropertiesSet() { + String dataBaseVendor = + this.jdbcTemplate.execute((ConnectionCallback) connection -> + connection.getMetaData().getDatabaseProductName()); this.getValueQuery = String.format(this.getValueQuery, this.tablePrefix); this.getValueForUpdateQuery = String.format(this.getValueForUpdateQuery, this.tablePrefix, this.lockHint); this.replaceValueQuery = String.format(this.replaceValueQuery, this.tablePrefix); this.replaceValueByKeyQuery = String.format(this.replaceValueByKeyQuery, this.tablePrefix); this.removeValueQuery = String.format(this.removeValueQuery, this.tablePrefix); this.putIfAbsentValueQuery = String.format(this.putIfAbsentValueQuery, this.tablePrefix, this.tablePrefix); + if ("PostgreSQL".equals(dataBaseVendor)) { + this.putIfAbsentValueQuery += " ON CONFLICT DO NOTHING"; + } this.countQuery = String.format(this.countQuery, this.tablePrefix); } @@ -185,7 +193,7 @@ public void afterPropertiesSet() { public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) { this.checkDatabaseOnStart = checkDatabaseOnStart; if (!checkDatabaseOnStart) { - LOGGER.info("The 'DefaultLockRepository' won't be started automatically " + + LOGGER.info("The 'JdbcMetadataStore' won't be started automatically " + "and required table is not going be checked."); } } @@ -247,7 +255,7 @@ private int tryToPutIfAbsent(String key, String value) { ps.setString(5, this.region); // NOSONAR magic number }); } - catch (DataIntegrityViolationException ex) { + catch (TransientDataAccessException | DataIntegrityViolationException ex) { return 0; } } diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcChannelMessageStore.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcChannelMessageStore.java index 9261f02f148..4dbf98c02c2 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcChannelMessageStore.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcChannelMessageStore.java @@ -431,7 +431,7 @@ public void afterPropertiesSet() { public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) { this.checkDatabaseOnStart = checkDatabaseOnStart; if (!checkDatabaseOnStart) { - LOGGER.info("The 'DefaultLockRepository' won't be started automatically " + + LOGGER.info("The 'JdbcChannelMessageStore' won't be started automatically " + "and required table is not going be checked."); } } diff --git a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcMessageStore.java b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcMessageStore.java index 0bd914510d7..07555066033 100644 --- a/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcMessageStore.java +++ b/spring-integration-jdbc/src/main/java/org/springframework/integration/jdbc/store/JdbcMessageStore.java @@ -368,7 +368,7 @@ public void addAllowedPatterns(String... patterns) { public void setCheckDatabaseOnStart(boolean checkDatabaseOnStart) { this.checkDatabaseOnStart = checkDatabaseOnStart; if (!checkDatabaseOnStart) { - logger.info("The 'DefaultLockRepository' won't be started automatically " + + logger.info("The 'JdbcMessageStore' won't be started automatically " + "and required table is not going be checked."); } } diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/mysql/MySqlMetadataStoreTests.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/mysql/MySqlMetadataStoreTests.java new file mode 100644 index 00000000000..978a4dbe1c5 --- /dev/null +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/mysql/MySqlMetadataStoreTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.jdbc.mysql; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.integration.jdbc.metadata.JdbcMetadataStore; +import org.springframework.integration.metadata.ConcurrentMetadataStore; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Artem Bilan + * + * @since 6.4 + */ +@SpringJUnitConfig +@DirtiesContext +class MySqlMetadataStoreTests implements MySqlContainerTest { + + @Autowired + ConcurrentMetadataStore jdbcMetadataStore; + + @Test + void verifyJdbcMetadataStoreConcurrency() throws InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(100); + CountDownLatch successPutIfAbsents = new CountDownLatch(100); + for (int i = 0; i < 100; i++) { + executorService.execute(() -> { + this.jdbcMetadataStore.putIfAbsent("testKey", "testValue"); + successPutIfAbsents.countDown(); + }); + } + assertThat(successPutIfAbsents.await(10, TimeUnit.SECONDS)).isTrue(); + executorService.shutdown(); + } + + @Configuration(proxyBeanMethods = false) + @EnableTransactionManagement + static class TestConfiguration { + + @Value("org/springframework/integration/jdbc/schema-mysql.sql") + Resource createSchemaScript; + + @Bean + DataSource dataSource() { + return MySqlContainerTest.dataSource(); + } + + @Bean + DataSourceInitializer dataSourceInitializer(DataSource dataSource) { + DataSourceInitializer dataSourceInitializer = new DataSourceInitializer(); + dataSourceInitializer.setDataSource(dataSource); + dataSourceInitializer.setDatabasePopulator(new ResourceDatabasePopulator(this.createSchemaScript)); + return dataSourceInitializer; + } + + @Bean + PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + @Bean + JdbcMetadataStore jdbcMetadataStore(DataSource dataSource) { + return new JdbcMetadataStore(dataSource); + } + + } + +} diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/channel/PostgresChannelMessageTableSubscriberTests.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/postgres/PostgresChannelMessageTableSubscriberTests.java similarity index 97% rename from spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/channel/PostgresChannelMessageTableSubscriberTests.java rename to spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/postgres/PostgresChannelMessageTableSubscriberTests.java index e595af5ec93..9d96f975bc4 100644 --- a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/channel/PostgresChannelMessageTableSubscriberTests.java +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/postgres/PostgresChannelMessageTableSubscriberTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.integration.jdbc.channel; +package org.springframework.integration.jdbc.postgres; import java.sql.DriverManager; import java.sql.SQLException; @@ -44,6 +44,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ByteArrayResource; import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.jdbc.channel.PgConnectionSupplier; +import org.springframework.integration.jdbc.channel.PostgresChannelMessageTableSubscriber; +import org.springframework.integration.jdbc.channel.PostgresSubscribableChannel; import org.springframework.integration.jdbc.store.JdbcChannelMessageStore; import org.springframework.integration.jdbc.store.channel.PostgresChannelMessageStoreQueryProvider; import org.springframework.jdbc.datasource.DataSourceTransactionManager; diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/channel/PostgresContainerTest.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/postgres/PostgresContainerTest.java similarity index 94% rename from spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/channel/PostgresContainerTest.java rename to spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/postgres/PostgresContainerTest.java index d85167891be..71ef40d625c 100644 --- a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/channel/PostgresContainerTest.java +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/postgres/PostgresContainerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.integration.jdbc.channel; +package org.springframework.integration.jdbc.postgres; import org.junit.jupiter.api.BeforeAll; import org.testcontainers.containers.PostgreSQLContainer; diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/postgres/PostgresMetadataStoreTests.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/postgres/PostgresMetadataStoreTests.java new file mode 100644 index 00000000000..f4ddb475c6b --- /dev/null +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/postgres/PostgresMetadataStoreTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.jdbc.postgres; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import javax.sql.DataSource; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.jdbc.metadata.JdbcMetadataStore; +import org.springframework.integration.metadata.ConcurrentMetadataStore; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Artem Bilan + * + * @since 6.2.9 + */ +@SpringJUnitConfig +@DirtiesContext +class PostgresMetadataStoreTests implements PostgresContainerTest { + + @Autowired + ConcurrentMetadataStore jdbcMetadataStore; + + @Test + void verifyJdbcMetadataStoreConcurrency() throws InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(100); + CountDownLatch successPutIfAbsents = new CountDownLatch(100); + for (int i = 0; i < 100; i++) { + executorService.execute(() -> { + this.jdbcMetadataStore.putIfAbsent("testKey", "testValue"); + successPutIfAbsents.countDown(); + }); + } + assertThat(successPutIfAbsents.await(10, TimeUnit.SECONDS)).isTrue(); + executorService.shutdown(); + } + + @Configuration(proxyBeanMethods = false) + @EnableTransactionManagement + static class TestConfiguration { + + @Bean + DataSource dataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setUrl(PostgresContainerTest.getJdbcUrl()); + dataSource.setUsername(PostgresContainerTest.getUsername()); + dataSource.setPassword(PostgresContainerTest.getPassword()); + return dataSource; + } + + @Bean + PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + @Bean + JdbcMetadataStore jdbcMetadataStore(DataSource dataSource) { + return new JdbcMetadataStore(dataSource); + } + + } + +} diff --git a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/store/channel/PostgresJdbcChannelMessageStoreTests.java b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/store/channel/PostgresJdbcChannelMessageStoreTests.java index 9a7835f0b59..b0b694a91c1 100644 --- a/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/store/channel/PostgresJdbcChannelMessageStoreTests.java +++ b/spring-integration-jdbc/src/test/java/org/springframework/integration/jdbc/store/channel/PostgresJdbcChannelMessageStoreTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.integration.jdbc.channel.PostgresContainerTest; +import org.springframework.integration.jdbc.postgres.PostgresContainerTest; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.test.context.ContextConfiguration; import org.springframework.transaction.PlatformTransactionManager; diff --git a/spring-integration-jms/src/main/java/org/springframework/integration/jms/ChannelPublishingJmsMessageListener.java b/spring-integration-jms/src/main/java/org/springframework/integration/jms/ChannelPublishingJmsMessageListener.java index 3ca51ce9f87..831e3704f67 100644 --- a/spring-integration-jms/src/main/java/org/springframework/integration/jms/ChannelPublishingJmsMessageListener.java +++ b/spring-integration-jms/src/main/java/org/springframework/integration/jms/ChannelPublishingJmsMessageListener.java @@ -42,6 +42,7 @@ import org.springframework.integration.gateway.MessagingGatewaySupport; import org.springframework.integration.jms.support.JmsMessageHeaderErrorMessageStrategy; import org.springframework.integration.support.DefaultMessageBuilderFactory; +import org.springframework.integration.support.ErrorMessageStrategy; import org.springframework.integration.support.ErrorMessageUtils; import org.springframework.integration.support.MessageBuilderFactory; import org.springframework.integration.support.management.TrackableComponent; @@ -337,6 +338,10 @@ public void setExtractReplyPayload(boolean extractReplyPayload) { this.extractReplyPayload = extractReplyPayload; } + public void setErrorMessageStrategy(ErrorMessageStrategy errorMessageStrategy) { + this.gatewayDelegate.setErrorMessageStrategy(errorMessageStrategy); + } + public void setMetricsCaptor(MetricsCaptor captor) { this.gatewayDelegate.registerMetricsCaptor(captor); } diff --git a/spring-integration-jms/src/main/java/org/springframework/integration/jms/JmsMessageDrivenEndpoint.java b/spring-integration-jms/src/main/java/org/springframework/integration/jms/JmsMessageDrivenEndpoint.java index 845a5ffae36..ffd126384ce 100644 --- a/spring-integration-jms/src/main/java/org/springframework/integration/jms/JmsMessageDrivenEndpoint.java +++ b/spring-integration-jms/src/main/java/org/springframework/integration/jms/JmsMessageDrivenEndpoint.java @@ -23,6 +23,7 @@ import org.springframework.integration.context.OrderlyShutdownCapable; import org.springframework.integration.endpoint.MessageProducerSupport; import org.springframework.integration.jms.util.JmsAdapterUtils; +import org.springframework.integration.support.ErrorMessageStrategy; import org.springframework.integration.support.management.metrics.MetricsCaptor; import org.springframework.integration.support.management.observation.MessageReceiverObservationConvention; import org.springframework.jms.listener.AbstractMessageListenerContainer; @@ -137,6 +138,12 @@ public void setShouldTrack(boolean shouldTrack) { this.listener.setShouldTrack(shouldTrack); } + @Override + public void setErrorMessageStrategy(ErrorMessageStrategy errorMessageStrategy) { + super.setErrorMessageStrategy(errorMessageStrategy); + this.listener.setErrorMessageStrategy(errorMessageStrategy); + } + /** * Set to {@code false} to prevent listener container shutdown when the endpoint is stopped. * Then, if so configured, any cached consumer(s) in the container will remain. diff --git a/spring-integration-jms/src/test/java/org/springframework/integration/jms/config/JmsMessageDrivenEndpointTests.java b/spring-integration-jms/src/test/java/org/springframework/integration/jms/config/JmsMessageDrivenEndpointTests.java index ce9435eb9a4..8cba9eed39d 100644 --- a/spring-integration-jms/src/test/java/org/springframework/integration/jms/config/JmsMessageDrivenEndpointTests.java +++ b/spring-integration-jms/src/test/java/org/springframework/integration/jms/config/JmsMessageDrivenEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.integration.jms.ActiveMQMultiContextTests; import org.springframework.integration.jms.ChannelPublishingJmsMessageListener; import org.springframework.integration.jms.JmsMessageDrivenEndpoint; +import org.springframework.integration.support.ErrorMessageStrategy; import org.springframework.integration.test.util.TestUtils; import org.springframework.jms.core.JmsTemplate; import org.springframework.jms.listener.AbstractMessageListenerContainer; @@ -61,7 +62,9 @@ public void testListenerIsAlreadyProvided() { @Test public void testStopStart(@Autowired JmsTemplate template, - @Autowired JmsMessageDrivenEndpoint endpoint, @Autowired QueueChannel out) { + @Autowired JmsMessageDrivenEndpoint endpoint, @Autowired QueueChannel out, + @Autowired ErrorMessageStrategy mockErrorMessageStrategy) { + template.convertAndSend("stop.start", "foo"); assertThat(out.receive(10_000).getPayload()).isEqualTo("foo"); endpoint.stop(); @@ -69,6 +72,9 @@ public void testStopStart(@Autowired JmsTemplate template, endpoint.start(); template.convertAndSend("stop.start", "bar"); assertThat(out.receive(10_000).getPayload()).isEqualTo("bar"); + + assertThat(TestUtils.getPropertyValue(endpoint, "listener.gatewayDelegate.errorMessageStrategy")) + .isSameAs(mockErrorMessageStrategy); } @Configuration @@ -81,8 +87,15 @@ public JmsTemplate template() { } @Bean - public JmsMessageDrivenEndpoint inbound() { - return new JmsMessageDrivenEndpoint(container(), listener()); + ErrorMessageStrategy mockErrorMessageStrategy() { + return mock(); + } + + @Bean + public JmsMessageDrivenEndpoint inbound(ErrorMessageStrategy mockErrorMessageStrategy) { + JmsMessageDrivenEndpoint jmsMessageDrivenEndpoint = new JmsMessageDrivenEndpoint(container(), listener()); + jmsMessageDrivenEndpoint.setErrorMessageStrategy(mockErrorMessageStrategy); + return jmsMessageDrivenEndpoint; } @Bean diff --git a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaExecutor.java b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaExecutor.java index a42f01ad342..0b96c8189a5 100644 --- a/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaExecutor.java +++ b/spring-integration-jpa/src/main/java/org/springframework/integration/jpa/core/JpaExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -569,7 +569,7 @@ else if (this.entityClass != null) { else { throw new IllegalStateException("For the polling operation, one of " + "the following properties must be specified: " - + "query, namedQuery or entityClass."); + + "jpaQuery, nativeQuery, namedQuery or entityClass."); } return payload; } diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/JpaExecutorTests.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/JpaExecutorTests.java index e170d42d588..c310d2836cb 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/JpaExecutorTests.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/JpaExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,7 +75,7 @@ public void testExecutePollWithNoEntityClassSpecified() { .isThrownBy(jpaExecutor::poll) .withMessage("For the polling operation, one of " + "the following properties must be specified: " - + "query, namedQuery or entityClass."); + + "jpaQuery, nativeQuery, namedQuery or entityClass."); } @Test diff --git a/spring-integration-kafka/src/main/resources/org/springframework/integration/kafka/config/spring-integration-kafka.xsd b/spring-integration-kafka/src/main/resources/org/springframework/integration/kafka/config/spring-integration-kafka.xsd index 0a1548b3ca8..77b9a6bc170 100644 --- a/spring-integration-kafka/src/main/resources/org/springframework/integration/kafka/config/spring-integration-kafka.xsd +++ b/spring-integration-kafka/src/main/resources/org/springframework/integration/kafka/config/spring-integration-kafka.xsd @@ -453,6 +453,13 @@ + + + + Set the 'group.id' KafkaConsumer property. + + + @@ -764,13 +771,6 @@ - - - - Set the 'group.id' KafkaConsumer property. - - - diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundGatewayTests-context.xml b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundGatewayTests-context.xml index 91e330442f6..08a10bf84bb 100644 --- a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundGatewayTests-context.xml +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundGatewayTests-context.xml @@ -21,7 +21,8 @@ retry-template="retryTemplate" recovery-callback="recoveryCallback" bind-source-record="true" - on-partitions-assigned-seek-callback="onPartitionsAssignedSeekCallback"/> + on-partitions-assigned-seek-callback="onPartitionsAssignedSeekCallback" + role="testRole"/> diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundGatewayTests.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundGatewayTests.java index da03ee622ac..b95d99e0ec9 100644 --- a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundGatewayTests.java +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundGatewayTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 the original author or authors. + * Copyright 2019-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.integration.kafka.inbound.KafkaInboundGateway; +import org.springframework.integration.support.SmartLifecycleRoleController; import org.springframework.integration.test.util.TestUtils; import org.springframework.kafka.listener.KafkaMessageListenerContainer; import org.springframework.test.annotation.DirtiesContext; @@ -45,6 +46,9 @@ public class KafkaInboundGatewayTests { @Autowired private ApplicationContext context; + @Autowired + SmartLifecycleRoleController roleController; + @Test public void testProps() { assertThat(this.gateway1.isAutoStartup()).isFalse(); @@ -69,6 +73,8 @@ public void testProps() { assertThat(TestUtils.getPropertyValue(this.gateway1, "messagingTemplate.sendTimeout")).isEqualTo(5000L); assertThat(TestUtils.getPropertyValue(this.gateway1, "messagingTemplate.receiveTimeout")).isEqualTo(43L); assertThat(TestUtils.getPropertyValue(this.gateway1, "bindSourceRecord", Boolean.class)).isTrue(); + assertThat(this.roleController.getRoles()).contains("testRole"); + assertThat(this.roleController.getEndpointsRunningStatus("testRole")).containsEntry("gateway1", false); } } diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaOutboundAdapterParserTests.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaOutboundAdapterParserTests.java index d5ba60362a6..b60793bda86 100644 --- a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaOutboundAdapterParserTests.java +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaOutboundAdapterParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeoutException; @@ -125,8 +126,8 @@ public void close(Duration timeout) { handler.setTimeoutBuffer(200); handler.setTopicExpression(new LiteralExpression("foo")); - Executors.newSingleThreadExecutor() - .submit(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.submit(() -> { RuntimeException exception = new RuntimeException("Async Producer Mock exception"); while (!mockProducer.errorNext(exception)) { Thread.sleep(100); @@ -146,6 +147,8 @@ public void close(Duration timeout) { .isThrownBy(() -> handler.handleMessage(new GenericMessage<>("foo"))) .withCauseInstanceOf(TimeoutException.class) .withStackTraceContaining("Timeout waiting for response from KafkaProducer"); + + executorService.shutdown(); } } diff --git a/spring-integration-mail/src/main/java/org/springframework/integration/mail/AbstractMailReceiver.java b/spring-integration-mail/src/main/java/org/springframework/integration/mail/AbstractMailReceiver.java index c91fb692709..b3e118f5ace 100755 --- a/spring-integration-mail/src/main/java/org/springframework/integration/mail/AbstractMailReceiver.java +++ b/spring-integration-mail/src/main/java/org/springframework/integration/mail/AbstractMailReceiver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,8 @@ * @author Artem Bilan * @author Dominik Simmen * @author Yuxin Wang + * @author Filip Hrisafov + * @author Jiandong Ma */ public abstract class AbstractMailReceiver extends IntegrationObjectSupport implements MailReceiver, DisposableBean { @@ -356,6 +358,9 @@ protected void openFolder() throws MessagingException { } private Folder obtainFolderInstance() throws MessagingException { + if (this.url == null) { + return this.store.getDefaultFolder(); + } return this.store.getFolder(this.url); } @@ -502,18 +507,30 @@ private Object byteArrayToContent(Map headers, ByteArrayOutputSt } private void postProcessFilteredMessages(Message[] filteredMessages) throws MessagingException { - setMessageFlags(filteredMessages); - - if (shouldDeleteMessages()) { - deleteMessages(filteredMessages); - } // Copy messages to cause an eager fetch + Message[] messages = filteredMessages; if (this.headerMapper == null && (this.autoCloseFolder || this.simpleContent)) { + messages = new Message[filteredMessages.length]; for (int i = 0; i < filteredMessages.length; i++) { - MimeMessage mimeMessage = new IntegrationMimeMessage((MimeMessage) filteredMessages[i]); + Message originalMessage = filteredMessages[i]; + messages[i] = originalMessage; + MimeMessage mimeMessage = new IntegrationMimeMessage((MimeMessage) originalMessage); filteredMessages[i] = mimeMessage; } } + + setMessageFlagsAndMaybeDeleteMessages(messages); + if (filteredMessages.length > 0 && filteredMessages[0] instanceof IntegrationMimeMessage) { + setMessageFlagsAndMaybeDeleteMessages(filteredMessages); + } + } + + private void setMessageFlagsAndMaybeDeleteMessages(Message[] messages) throws MessagingException { + setMessageFlags(messages); + + if (shouldDeleteMessages()) { + deleteMessages(messages); + } } private void setMessageFlags(Message[] filteredMessages) throws MessagingException { diff --git a/spring-integration-mail/src/main/java/org/springframework/integration/mail/ImapIdleChannelAdapter.java b/spring-integration-mail/src/main/java/org/springframework/integration/mail/ImapIdleChannelAdapter.java index 8782442c06c..943a854a6d4 100755 --- a/spring-integration-mail/src/main/java/org/springframework/integration/mail/ImapIdleChannelAdapter.java +++ b/spring-integration-mail/src/main/java/org/springframework/integration/mail/ImapIdleChannelAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.integration.transaction.IntegrationResourceHolder; import org.springframework.integration.transaction.IntegrationResourceHolderSynchronization; import org.springframework.integration.transaction.TransactionSynchronizationFactory; +import org.springframework.lang.Nullable; import org.springframework.messaging.MessagingException; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -200,22 +201,24 @@ private void callIdle() { } catch (Exception ex) { publishException(ex); - if (this.shouldReconnectAutomatically - && ex.getCause() instanceof jakarta.mail.MessagingException messagingException) { - - //run again after a delay - logger.info(messagingException, - () -> "Failed to execute IDLE task. Will attempt to resubmit in " - + this.reconnectDelay + " milliseconds."); - delayNextIdleCall(); - } - else { - logger.warn(ex, - "Failed to execute IDLE task. " + - "Won't resubmit since not a 'shouldReconnectAutomatically' " + - "or not a 'jakarta.mail.MessagingException'"); - break; + if (this.shouldReconnectAutomatically) { + jakarta.mail.MessagingException messagingException = + getJakartaMailMessagingExceptionFromCause(ex.getCause()); + + if (messagingException != null) { + //run again after a delay + logger.info(messagingException, + () -> "Failed to execute IDLE task. Will attempt to resubmit in " + + this.reconnectDelay + " milliseconds."); + delayNextIdleCall(); + continue; + } } + logger.warn(ex, + "Failed to execute IDLE task. " + + "Won't resubmit since not a 'shouldReconnectAutomatically' " + + "or not a 'jakarta.mail.MessagingException'"); + break; } } } @@ -256,6 +259,21 @@ private void delayNextIdleCall() { } } + @Nullable + private static jakarta.mail.MessagingException getJakartaMailMessagingExceptionFromCause(Throwable cause) { + if (cause == null) { + return null; + } + if (cause instanceof jakarta.mail.MessagingException messagingException) { + return messagingException; + } + Throwable nextCause = cause.getCause(); + if (cause == nextCause) { + return null; + } + return getJakartaMailMessagingExceptionFromCause(nextCause); + } + private class MessageSender implements Consumer { MessageSender() { diff --git a/spring-integration-mail/src/main/java/org/springframework/integration/mail/ImapMailReceiver.java b/spring-integration-mail/src/main/java/org/springframework/integration/mail/ImapMailReceiver.java index 2f61d4fc979..8eadb19f4dd 100755 --- a/spring-integration-mail/src/main/java/org/springframework/integration/mail/ImapMailReceiver.java +++ b/spring-integration-mail/src/main/java/org/springframework/integration/mail/ImapMailReceiver.java @@ -18,6 +18,7 @@ import java.time.Instant; import java.util.Arrays; +import java.util.Locale; import java.util.Objects; import java.util.Properties; import java.util.concurrent.ScheduledFuture; @@ -79,7 +80,7 @@ public ImapMailReceiver() { public ImapMailReceiver(String url) { super(url); if (url != null) { - Assert.isTrue(url.toLowerCase().startsWith(PROTOCOL), + Assert.isTrue(url.toLowerCase(Locale.ROOT).startsWith(PROTOCOL), "URL must start with 'imap' for the IMAP Mail receiver."); } else { diff --git a/spring-integration-mail/src/main/java/org/springframework/integration/mail/config/MailReceiverFactoryBean.java b/spring-integration-mail/src/main/java/org/springframework/integration/mail/config/MailReceiverFactoryBean.java index 169085b7a8a..23eb391b179 100644 --- a/spring-integration-mail/src/main/java/org/springframework/integration/mail/config/MailReceiverFactoryBean.java +++ b/spring-integration-mail/src/main/java/org/springframework/integration/mail/config/MailReceiverFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.integration.mail.config; +import java.util.Locale; import java.util.Properties; import jakarta.mail.Authenticator; @@ -162,8 +163,8 @@ public Class getObjectType() { private MailReceiver createReceiver() { // NOSONAR verifyProtocol(); - boolean isPop3 = this.protocol.toLowerCase().startsWith("pop3"); - boolean isImap = this.protocol.toLowerCase().startsWith("imap"); + boolean isPop3 = this.protocol.toLowerCase(Locale.ROOT).startsWith("pop3"); + boolean isImap = this.protocol.toLowerCase(Locale.ROOT).startsWith("imap"); Assert.isTrue(isPop3 || isImap, "the store URI must begin with 'pop3' or 'imap'"); AbstractMailReceiver mailReceiver = isPop3 ? new Pop3MailReceiver(this.storeUri) diff --git a/spring-integration-mail/src/test/java/org/springframework/integration/mail/ImapMailReceiverTests.java b/spring-integration-mail/src/test/java/org/springframework/integration/mail/ImapMailReceiverTests.java index dbb3976bb0c..375ae722ba4 100644 --- a/spring-integration-mail/src/test/java/org/springframework/integration/mail/ImapMailReceiverTests.java +++ b/spring-integration-mail/src/test/java/org/springframework/integration/mail/ImapMailReceiverTests.java @@ -17,6 +17,7 @@ package org.springframework.integration.mail; import java.io.IOException; +import java.io.OutputStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; @@ -24,6 +25,7 @@ import java.util.Properties; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Handler; @@ -88,6 +90,7 @@ import org.springframework.util.MimeTypeUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -107,6 +110,7 @@ * @author Artem Bilan * @author Alexander Pinske * @author Dominik Simmen + * @author Filip Hrisafov */ @SpringJUnitConfig @ContextConfiguration( @@ -151,7 +155,7 @@ void startImapServer() { imapSearches.stores.clear(); ServerSetup imap = ServerSetupTest.IMAP.verbose(true).dynamicPort(); imap.setServerStartupTimeout(10000); - imap.setReadTimeout(2000); + imap.setReadTimeout(10000); imapIdleServer = new GreenMail(imap); user = imapIdleServer.setUser("user", "pw"); imapIdleServer.start(); @@ -187,7 +191,6 @@ public void testIdleWithServerDefaultSearch() throws Exception { } @Test - @Disabled("GreenMail server closes socket for some reason") public void testIdleWithMessageMapping() throws Exception { ImapMailReceiver receiver = new ImapMailReceiver("imap://user:pw@localhost:" + imapIdleServer.getImap().getPort() + "/INBOX"); @@ -196,6 +199,7 @@ public void testIdleWithMessageMapping() throws Exception { } @Test + @Disabled public void testIdleWithServerDefaultSearchSimple() throws Exception { ImapMailReceiver receiver = new ImapMailReceiver("imap://user:pw@localhost:" + imapIdleServer.getImap().getPort() + "/INBOX"); @@ -258,7 +262,7 @@ public void testIdleWithServerGuts(ImapMailReceiver receiver, boolean mapped, bo assertThat(received).isNotNull(); MessageHeaders headers = received.getHeaders(); assertThat(headers.get(MailHeaders.RAW_HEADERS)).isNotNull(); - assertThat(headers.get(MailHeaders.CONTENT_TYPE)).isEqualTo("TEXT/PLAIN; charset=us-ascii"); + assertThat(headers.get(MailHeaders.CONTENT_TYPE)).isEqualTo("text/plain; charset=us-ascii"); assertThat(headers.get(MessageHeaders.CONTENT_TYPE)).isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE); assertThat(headers.get(MailHeaders.FROM)).isEqualTo("Bar "); String[] toHeader = headers.get(MailHeaders.TO, String[].class); @@ -299,6 +303,11 @@ public void receiveAndMarkAsReadDontDelete() throws Exception { private AbstractMailReceiver receiveAndMarkAsReadDontDeleteGuts(AbstractMailReceiver receiver, Message msg1, Message msg2) throws NoSuchFieldException, IllegalAccessException, MessagingException { + return receiveAndMarkAsReadDontDeleteGuts(receiver, msg1, msg2, true); + } + + private AbstractMailReceiver receiveAndMarkAsReadDontDeleteGuts(AbstractMailReceiver receiver, Message msg1, + Message msg2, boolean receive) throws NoSuchFieldException, IllegalAccessException, MessagingException { ((ImapMailReceiver) receiver).setShouldMarkMessagesAsRead(true); receiver = spy(receiver); @@ -326,7 +335,9 @@ private AbstractMailReceiver receiveAndMarkAsReadDontDeleteGuts(AbstractMailRece willAnswer(invocation -> messages).given(folder).search(any(SearchTerm.class)); willAnswer(invocation -> null).given(receiver).fetchMessages(messages); - receiver.receive(); + if (receive) { + receiver.receive(); + } return receiver; } @@ -347,14 +358,14 @@ public void receiveAndMarkAsReadDontDeletePassingFilter() throws Exception { public void receiveAndMarkAsReadDontDeleteFiltered() throws Exception { AbstractMailReceiver receiver = new ImapMailReceiver(); Message msg1 = GreenMailUtil.newMimeMessage("test1"); - Message msg2 = GreenMailUtil.newMimeMessage("test2"); + Message msg2 = spy(GreenMailUtil.newMimeMessage("test2")); given(msg2.getSubject()).willReturn("foo"); // should not be marked seen Expression selectorExpression = new SpelExpressionParser() .parseExpression("subject == null OR !subject.equals('foo')"); receiver.setSelectorExpression(selectorExpression); receiver = receiveAndMarkAsReadDontDeleteGuts(receiver, msg1, msg2); assertThat(msg1.getFlags().contains(Flag.SEEN)).isTrue(); - assertThat(msg2.getFlags().contains(Flag.SEEN)).isTrue(); + assertThat(msg2.getFlags().contains(Flag.SEEN)).isFalse(); verify(receiver, times(0)).deleteMessages(Mockito.any()); } @@ -441,7 +452,7 @@ public void receiveMarkAsReadAndDelete() throws Exception { assertThat(msg1.getFlags().contains(Flag.SEEN)).isTrue(); assertThat(msg2.getFlags().contains(Flag.SEEN)).isTrue(); - verify(receiver, times(1)).deleteMessages(Mockito.any()); + verify(receiver, times(2)).deleteMessages(Mockito.any()); } @Test @@ -980,6 +991,31 @@ private void setUpScheduler(ImapMailReceiver mailReceiver, ThreadPoolTaskSchedul mailReceiver.setBeanFactory(bf); } + @Test + public void receiveAndMarkAsReadDontDeleteWithThrowingWhenCopying() throws Exception { + AbstractMailReceiver receiver = new ImapMailReceiver(); + MimeMessage msg1 = spy(GreenMailUtil.newMimeMessage("test1")); + MimeMessage greenMailMsg2 = GreenMailUtil.newMimeMessage("test2"); + TestThrowingMimeMessage msg2 = new TestThrowingMimeMessage(greenMailMsg2); + receiver = receiveAndMarkAsReadDontDeleteGuts(receiver, msg1, msg2, false); + assertThatThrownBy(receiver::receive) + .isInstanceOf(MessagingException.class) + .hasMessage("IOException while copying message") + .cause() + .isInstanceOf(IOException.class) + .hasMessage("Simulated exception"); + assertThat(msg1.getFlags().contains(Flag.SEEN)).isFalse(); + assertThat(msg2.getFlags().contains(Flag.SEEN)).isFalse(); + verify(msg1, times(0)).setFlags(Mockito.any(), Mockito.anyBoolean()); + + receiver.receive(); + assertThat(msg1.getFlags().contains(Flag.SEEN)).isTrue(); + assertThat(msg2.getFlags().contains(Flag.SEEN)).isTrue(); + // msg2 is marked with the user and seen flags + verify(msg1, times(2)).setFlags(Mockito.any(), Mockito.anyBoolean()); + verify(receiver, times(0)).deleteMessages(Mockito.any()); + } + private static class ImapSearchLoggingHandler extends Handler { private final List searches = new ArrayList<>(); @@ -1015,4 +1051,21 @@ public void close() throws SecurityException { } + private static class TestThrowingMimeMessage extends MimeMessage { + + protected final AtomicBoolean throwExceptionBeforeWrite = new AtomicBoolean(true); + + private TestThrowingMimeMessage(MimeMessage source) throws MessagingException { + super(source); + } + + @Override + public void writeTo(OutputStream os) throws IOException, MessagingException { + if (this.throwExceptionBeforeWrite.getAndSet(false)) { + throw new IOException("Simulated exception"); + } + super.writeTo(os); + } + } + } diff --git a/spring-integration-mail/src/test/java/org/springframework/integration/mail/MailReceiverTests.java b/spring-integration-mail/src/test/java/org/springframework/integration/mail/MailReceiverTests.java index 4de6537ee18..752a1b8319a 100644 --- a/spring-integration-mail/src/test/java/org/springframework/integration/mail/MailReceiverTests.java +++ b/spring-integration-mail/src/test/java/org/springframework/integration/mail/MailReceiverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import jakarta.mail.Message; import jakarta.mail.Session; import jakarta.mail.Store; -import jakarta.mail.URLName; import org.junit.jupiter.api.Test; import org.springframework.beans.DirectFieldAccessor; @@ -68,7 +67,7 @@ protected Message[] searchForNewMessages() { Folder folder = mock(Folder.class); when(folder.exists()).thenReturn(true); when(folder.isOpen()).thenReturn(false, true); - doReturn(folder).when(store).getFolder((URLName) null); + doReturn(folder).when(store).getDefaultFolder(); doNothing().when(store).connect(); receiver.openFolder(); receiver.openFolder(); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java index 3d015b8713f..86053614cc1 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/Mqttv5PahoMessageDrivenChannelAdapter.java @@ -19,15 +19,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.ConcurrentModificationException; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.IntStream; import org.eclipse.paho.mqttv5.client.IMqttAsyncClient; -import org.eclipse.paho.mqttv5.client.IMqttMessageListener; import org.eclipse.paho.mqttv5.client.IMqttToken; import org.eclipse.paho.mqttv5.client.MqttAsyncClient; import org.eclipse.paho.mqttv5.client.MqttCallback; @@ -109,8 +108,6 @@ public class Mqttv5PahoMessageDrivenChannelAdapter private volatile boolean readyToSubscribeOnStart; - private final AtomicInteger subscriptionIdentifierCounter = new AtomicInteger(0); - /** * Create an instance based on the MQTT url, client id and subscriptions. * @param url the MQTT url to connect. @@ -293,7 +290,7 @@ protected void doStop() { try { if (this.mqttClient != null && this.mqttClient.isConnected()) { if (this.connectionOptions.isCleanStart()) { - this.mqttClient.unsubscribe(topics).waitForCompletion(getCompletionTimeout()); + unsubscribe(topics); // Have to re-subscribe on next start if connection is not lost. this.readyToSubscribeOnStart = true; @@ -314,12 +311,22 @@ protected void doStop() { } } + private void unsubscribe(String... topics) throws MqttException { + try { + // Catch ConcurrentModificationException: https://github.com/eclipse/paho.mqtt.java/issues/986 + this.mqttClient.unsubscribe(topics).waitForCompletion(getCompletionTimeout()); + } + catch (ConcurrentModificationException ex) { + logger.error(ex, () -> "Error unsubscribing from " + Arrays.toString(topics)); + } + } + @Override public void destroy() { super.destroy(); try { if (getClientManager() == null && this.mqttClient != null) { - this.mqttClient.close(true); + this.mqttClient.close(); } } catch (MqttException ex) { @@ -344,9 +351,10 @@ public void addTopic(String topic, int qos) { } if (this.mqttClient != null && this.mqttClient.isConnected()) { MqttProperties subscriptionProperties = new MqttProperties(); - subscriptionProperties.setSubscriptionIdentifier(this.subscriptionIdentifierCounter.incrementAndGet()); + // Make use of mqttSession.getNextSubscriptionIdentifier() if available in connection + subscriptionProperties.setSubscriptionIdentifiers(List.of(0)); this.mqttClient.subscribe(new MqttSubscription[] {subscription}, - null, null, new IMqttMessageListener[] {this::messageArrived}, subscriptionProperties) + null, null, this::messageArrived, subscriptionProperties) .waitForCompletion(getCompletionTimeout()); } } @@ -363,7 +371,7 @@ public void removeTopic(String... topic) { this.topicLock.lock(); try { if (this.mqttClient != null && this.mqttClient.isConnected()) { - this.mqttClient.unsubscribe(topic).waitForCompletion(getCompletionTimeout()); + unsubscribe(topic); } super.removeTopic(topic); if (!CollectionUtils.isEmpty(this.subscriptions)) { @@ -472,15 +480,10 @@ private void subscribe() { ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); this.topicLock.lock(); try { - IMqttMessageListener listener = this::messageArrived; - IMqttMessageListener[] listeners = IntStream.range(0, mqttSubscriptions.length) - .mapToObj(t -> listener) - .toArray(IMqttMessageListener[]::new); MqttProperties subscriptionProperties = new MqttProperties(); - subscriptionProperties.setSubscriptionIdentifiers(IntStream.range(0, mqttSubscriptions.length) - .mapToObj(i -> this.subscriptionIdentifierCounter.incrementAndGet()) - .toList()); - this.mqttClient.subscribe(mqttSubscriptions, null, null, listeners, subscriptionProperties) + // Make use of mqttSession.getNextSubscriptionIdentifier() if available in connection + subscriptionProperties.setSubscriptionIdentifiers(List.of(0)); + this.mqttClient.subscribe(mqttSubscriptions, null, null, this::messageArrived, subscriptionProperties) .waitForCompletion(getCompletionTimeout()); String message = "Connected and subscribed to " + Arrays.toString(mqttSubscriptions); logger.debug(message); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java index 60ce021d45b..6189e6c644c 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/Mqttv5PahoMessageHandler.java @@ -266,7 +266,15 @@ protected void publish(String topic, Object mqttMessage, Message message) { long completionTimeout = getCompletionTimeout(); try { if (!this.mqttClient.isConnected()) { - this.mqttClient.connect(this.connectionOptions).waitForCompletion(completionTimeout); + this.lock.lock(); + try { + if (!this.mqttClient.isConnected()) { + this.mqttClient.connect(this.connectionOptions).waitForCompletion(completionTimeout); + } + } + finally { + this.lock.unlock(); + } } IMqttToken token = this.mqttClient.publish(topic, (MqttMessage) mqttMessage); ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/Mqttv5AdapterTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/Mqttv5AdapterTests.java index 61dd1dab34e..8b77caf8a8b 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/Mqttv5AdapterTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/Mqttv5AdapterTests.java @@ -57,7 +57,7 @@ public void testStop() throws Exception { adapter.stop(); verify(client).connect(any(MqttConnectionOptions.class)); - verify(client).subscribe(any(MqttSubscription[].class), any(), any(), any(IMqttMessageListener[].class), any()); + verify(client).subscribe(any(MqttSubscription[].class), any(), any(), any(IMqttMessageListener.class), any()); verify(client).unsubscribe(any(String[].class)); } @@ -71,11 +71,12 @@ public void testStopNotClean() throws Exception { adapter.stop(); verify(client).connect(any(MqttConnectionOptions.class)); - verify(client).subscribe(any(MqttSubscription[].class), any(), any(), any(IMqttMessageListener[].class), any()); + verify(client).subscribe(any(MqttSubscription[].class), any(), any(), any(IMqttMessageListener.class), any()); verify(client, never()).unsubscribe(any(String[].class)); } - private static Mqttv5PahoMessageDrivenChannelAdapter buildAdapterIn(final IMqttAsyncClient client, boolean cleanStart) throws MqttException { + private static Mqttv5PahoMessageDrivenChannelAdapter buildAdapterIn(final IMqttAsyncClient client, + boolean cleanStart) throws MqttException { MqttConnectionOptions connectionOptions = new MqttConnectionOptions(); connectionOptions.setServerURIs(new String[] {"tcp://localhost:1883"}); @@ -85,9 +86,11 @@ private static Mqttv5PahoMessageDrivenChannelAdapter buildAdapterIn(final IMqttA IMqttToken token = mock(IMqttToken.class); given(client.disconnect()).willReturn(token); given(client.connect(any(MqttConnectionOptions.class))).willReturn(token); - given(client.subscribe(any(MqttSubscription[].class), any(), any(), any(IMqttMessageListener[].class), any())).willReturn(token); + given(client.subscribe(any(MqttSubscription[].class), any(), any(), any(IMqttMessageListener.class), any())) + .willReturn(token); given(client.unsubscribe(any(String[].class))).willReturn(token); - Mqttv5PahoMessageDrivenChannelAdapter adapter = new Mqttv5PahoMessageDrivenChannelAdapter(connectionOptions, "client", "foo"); + Mqttv5PahoMessageDrivenChannelAdapter adapter = + new Mqttv5PahoMessageDrivenChannelAdapter(connectionOptions, "client", "foo"); ReflectionTestUtils.setField(adapter, "mqttClient", client); adapter.setBeanFactory(mock(BeanFactory.class)); adapter.setApplicationEventPublisher(mock(ApplicationEventPublisher.class)); diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ResubscribeAfterAutomaticReconnectTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ResubscribeAfterAutomaticReconnectTests.java index e6a21ff609d..c09afc9d45f 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ResubscribeAfterAutomaticReconnectTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/ResubscribeAfterAutomaticReconnectTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.integration.mqtt; +import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -82,7 +83,7 @@ void messageReceivedAfterResubscriptionOnLostConnection() throws InterruptedExce assertThat(this.config.subscribeSecondLatch.await(10, TimeUnit.SECONDS)).isTrue(); - await().untilAsserted(() -> this.mqttOutFlowInput.send(testMessage)); + await().atMost(Duration.ofSeconds(30)).untilAsserted(() -> this.mqttOutFlowInput.send(testMessage)); assertThat(this.fromMqttChannel.receive(10_000)).isNotNull(); // Re-subscription on channel adapter restart with cleanStart diff --git a/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java b/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java index 7f73eebbc7c..1f1d44a38e5 100644 --- a/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java +++ b/spring-integration-redis/src/main/java/org/springframework/integration/redis/util/RedisLockRegistry.java @@ -17,6 +17,7 @@ package org.springframework.integration.redis.util; import java.text.SimpleDateFormat; +import java.time.Duration; import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; @@ -86,6 +87,7 @@ * @author Eddie Cho * @author Myeonghyeon Lee * @author Roman Zabaluev + * @author Alex Peelman * * @since 4.0 * @@ -98,8 +100,12 @@ public final class RedisLockRegistry implements ExpirableLockRegistry, Disposabl private static final int DEFAULT_CAPACITY = 100_000; + private static final int DEFAULT_IDLE = 100; + private final Lock lock = new ReentrantLock(); + private Duration idleBetweenTries = Duration.ofMillis(DEFAULT_IDLE); + private final Map locks = new LinkedHashMap<>(16, 0.75F, true) { @@ -209,6 +215,17 @@ public void setCacheCapacity(int cacheCapacity) { this.cacheCapacity = cacheCapacity; } + /** + * Specify a @link Duration} to sleep between obtainLock attempts. + * Defaults to 100 milliseconds. + * @param idleBetweenTries the {@link Duration} to sleep between obtainLock attempts. + * @since 6.4.0 + */ + public void setIdleBetweenTries(Duration idleBetweenTries) { + Assert.notNull(idleBetweenTries, "'idleBetweenTries' must not be null"); + this.idleBetweenTries = idleBetweenTries; + } + /** * Set {@link RedisLockType} mode to work in. * By default, the {@link RedisLockType#SPIN_LOCK} is used - works in all the environment. @@ -279,7 +296,7 @@ public void destroy() { public enum RedisLockType { /** - * The lock is acquired by periodically(100ms) checking whether the lock can be acquired. + * The lock is acquired by periodically(idleBetweenTries property) checking whether the lock can be acquired. */ SPIN_LOCK, @@ -657,7 +674,7 @@ private boolean subscribeLock(long time) throws ExecutionException, InterruptedE } private void runRedisMessageListenerContainer() { - RedisLockRegistry.this.lock.tryLock(); + RedisLockRegistry.this.lock.lock(); try { if (!(RedisLockRegistry.this.isRunningRedisMessageListenerContainer && RedisLockRegistry.this.redisMessageListenerContainer != null @@ -741,7 +758,7 @@ protected boolean tryRedisLockInner(long time) throws InterruptedException { long now = System.currentTimeMillis(); if (time == -1L) { while (!obtainLock()) { - Thread.sleep(100); //NOSONAR + Thread.sleep(RedisLockRegistry.this.idleBetweenTries.toMillis()); //NOSONAR } return true; } @@ -749,7 +766,7 @@ protected boolean tryRedisLockInner(long time) throws InterruptedException { long expire = now + TimeUnit.MILLISECONDS.convert(time, TimeUnit.MILLISECONDS); boolean acquired; while (!(acquired = obtainLock()) && System.currentTimeMillis() < expire) { //NOSONAR - Thread.sleep(100); //NOSONAR + Thread.sleep(RedisLockRegistry.this.idleBetweenTries.toMillis()); //NOSONAR } return acquired; } diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java index 57efe56cacb..68bd2d112ce 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/store/RedisMessageGroupStoreTests.java @@ -20,7 +20,6 @@ import java.util.Date; import java.util.Iterator; import java.util.List; -import java.util.Objects; import java.util.Properties; import java.util.UUID; import java.util.concurrent.ExecutorService; @@ -35,13 +34,16 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.beans.BeanUtils; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.SerializationException; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.NullChannel; import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.handler.DelayHandler; import org.springframework.integration.history.MessageHistory; import org.springframework.integration.message.AdviceMessage; import org.springframework.integration.redis.RedisContainerTest; @@ -56,14 +58,15 @@ import org.springframework.messaging.support.GenericMessage; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.assertj.core.api.Assertions.fail; /** * @author Oleg Zhurakousky * @author Artem Bilan * @author Gary Russell * @author Artem Vozhdayenko + * @author Youbin Wu */ class RedisMessageGroupStoreTests implements RedisContainerTest { @@ -316,7 +319,7 @@ void testConcurrentModifications() throws Exception { executor.execute(() -> { store2.removeMessagesFromGroup(this.groupId, message); MessageGroup group = store2.getMessageGroup(this.groupId); - if (group.getMessages().size() != 0) { + if (!group.getMessages().isEmpty()) { failures.add("REMOVE"); throw new AssertionFailedError("Failed on Remove"); } @@ -400,11 +403,17 @@ void testJsonSerialization() { Message mutableMessage = new MutableMessage<>(UUID.randomUUID()); Message adviceMessage = new AdviceMessage<>("foo", genericMessage); ErrorMessage errorMessage = new ErrorMessage(new RuntimeException("test exception"), mutableMessage); + var delayedMessageWrapperConstructor = + BeanUtils.getResolvableConstructor(DelayHandler.DelayedMessageWrapper.class); + Message delayMessage = new GenericMessage<>( + BeanUtils.instantiateClass(delayedMessageWrapperConstructor, genericMessage, + System.currentTimeMillis())); - store.addMessagesToGroup(this.groupId, genericMessage, mutableMessage, adviceMessage, errorMessage); + store.addMessagesToGroup(this.groupId, + genericMessage, mutableMessage, adviceMessage, errorMessage, delayMessage); MessageGroup messageGroup = store.getMessageGroup(this.groupId); - assertThat(messageGroup.size()).isEqualTo(4); + assertThat(messageGroup.size()).isEqualTo(5); List> messages = new ArrayList<>(messageGroup.getMessages()); assertThat(messages.get(0)).isEqualTo(genericMessage); assertThat(messages.get(0).getHeaders()).containsKeys(MessageHistory.HEADER_NAME); @@ -417,22 +426,21 @@ void testJsonSerialization() { .isEqualTo(errorMessage.getOriginalMessage()); assertThat(((ErrorMessage) errorMessageResult).getPayload().getMessage()) .isEqualTo(errorMessage.getPayload().getMessage()); + assertThat(messages.get(4)).isEqualTo(delayMessage); Message fooMessage = new GenericMessage<>(new Foo("foo")); - try { - store.addMessageToGroup(this.groupId, fooMessage) - .getMessages() - .iterator() - .next(); - fail("SerializationException expected"); - } - catch (Exception e) { - assertThat(e.getCause().getCause()).isInstanceOf(IllegalArgumentException.class); - assertThat(e.getMessage()).contains("The class with " + - "org.springframework.integration.redis.store.RedisMessageGroupStoreTests$Foo and name of " + - "org.springframework.integration.redis.store.RedisMessageGroupStoreTests$Foo " + - "is not in the trusted packages:"); - } + + assertThatExceptionOfType(SerializationException.class) + .isThrownBy(() -> + store.addMessageToGroup(this.groupId, fooMessage) + .getMessages() + .iterator() + .next()) + .withRootCauseInstanceOf(IllegalArgumentException.class) + .withMessageContaining("The class with " + + "org.springframework.integration.redis.store.RedisMessageGroupStoreTests$Foo and name of " + + "org.springframework.integration.redis.store.RedisMessageGroupStoreTests$Foo " + + "is not in the trusted packages:"); mapper = JacksonJsonUtils.messagingAwareMapper(getClass().getPackage().getName()); @@ -485,43 +493,7 @@ public void removeMessagesFromGroupDontRemoveSameMessageInOtherGroup() { assertThat(store.messageGroupSize("2")).isEqualTo(1); } - private static class Foo { - - private String foo; - - Foo() { - } - - Foo(String foo) { - this.foo = foo; - } - - public String getFoo() { - return this.foo; - } - - public void setFoo(String foo) { - this.foo = foo; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - Foo foo1 = (Foo) o; - - return this.foo != null ? this.foo.equals(foo1.foo) : foo1.foo == null; - } - - @Override - public int hashCode() { - return Objects.hashCode(this.foo); - } + private record Foo(String foo) { } diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/AggregatorWithRedisLocksTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/AggregatorWithRedisLocksTests.java index b49922895dc..05e02dcd3ad 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/AggregatorWithRedisLocksTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/AggregatorWithRedisLocksTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -88,8 +89,9 @@ public void setup() { @Test void testLockSingleGroup() throws Exception { this.releaseStrategy.reset(1); - Executors.newSingleThreadExecutor().execute(asyncSend("foo", 1, 1)); - Executors.newSingleThreadExecutor().execute(asyncSend("bar", 2, 1)); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(asyncSend("foo", 1, 1)); + executorService.execute(asyncSend("bar", 2, 1)); assertThat(this.releaseStrategy.latch2.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.template.keys("aggregatorWithRedisLocksTests:*")).hasSize(1); this.releaseStrategy.latch1.countDown(); @@ -98,17 +100,19 @@ void testLockSingleGroup() throws Exception { this.assertNoLocksAfterTest(); assertThat(this.exception) .as("Unexpected exception:" + (this.exception != null ? this.exception.toString() : "")).isNull(); + executorService.shutdown(); } @Test void testLockThreeGroups() throws Exception { this.releaseStrategy.reset(3); - Executors.newSingleThreadExecutor().execute(asyncSend("foo", 1, 1)); - Executors.newSingleThreadExecutor().execute(asyncSend("bar", 2, 1)); - Executors.newSingleThreadExecutor().execute(asyncSend("foo", 1, 2)); - Executors.newSingleThreadExecutor().execute(asyncSend("bar", 2, 2)); - Executors.newSingleThreadExecutor().execute(asyncSend("foo", 1, 3)); - Executors.newSingleThreadExecutor().execute(asyncSend("bar", 2, 3)); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(asyncSend("foo", 1, 1)); + executorService.execute(asyncSend("bar", 2, 1)); + executorService.execute(asyncSend("foo", 1, 2)); + executorService.execute(asyncSend("bar", 2, 2)); + executorService.execute(asyncSend("foo", 1, 3)); + executorService.execute(asyncSend("bar", 2, 3)); assertThat(this.releaseStrategy.latch2.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.template.keys("aggregatorWithRedisLocksTests:*")).hasSize(3); this.releaseStrategy.latch1.countDown(); @@ -121,13 +125,15 @@ void testLockThreeGroups() throws Exception { this.assertNoLocksAfterTest(); assertThat(this.exception) .as("Unexpected exception:" + (this.exception != null ? this.exception.toString() : "")).isNull(); + executorService.shutdown(); } @RepeatedTest(10) void testDistributedAggregator() throws Exception { this.releaseStrategy.reset(1); - Executors.newSingleThreadExecutor().execute(asyncSend("foo", 1, 1)); - Executors.newSingleThreadExecutor().execute(() -> { + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(asyncSend("foo", 1, 1)); + executorService.execute(() -> { try { in2.send(new GenericMessage<>("bar", stubHeaders(2, 2, 1))); } @@ -143,6 +149,7 @@ void testDistributedAggregator() throws Exception { this.assertNoLocksAfterTest(); assertThat(this.exception) .as("Unexpected exception:" + (this.exception != null ? this.exception.toString() : "")).isNull(); + executorService.shutdown(); } private void assertNoLocksAfterTest() throws Exception { diff --git a/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java index a0874de6202..c654b6b85d3 100644 --- a/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java +++ b/spring-integration-redis/src/test/java/org/springframework/integration/redis/util/RedisLockRegistryTests.java @@ -228,7 +228,8 @@ void testTwoThreadsSecondFailsToGetLock(RedisLockType testRedisLockType) throws lock1.lockInterruptibly(); final AtomicBoolean locked = new AtomicBoolean(); final CountDownLatch latch = new CountDownLatch(1); - Future result = Executors.newSingleThreadExecutor().submit(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future result = executorService.submit(() -> { Lock lock2 = registry.obtain("foo"); locked.set(lock2.tryLock(200, TimeUnit.MILLISECONDS)); latch.countDown(); @@ -249,6 +250,7 @@ void testTwoThreadsSecondFailsToGetLock(RedisLockType testRedisLockType) throws registry.expireUnusedOlderThan(-1000); assertThat(getRedisLockRegistryLocks(registry)).isEmpty(); registry.destroy(); + executorService.shutdown(); } @ParameterizedTest @@ -263,7 +265,8 @@ void testTwoThreads(RedisLockType testRedisLockType) throws Exception { CountDownLatch latch3 = new CountDownLatch(1); lock1.lockInterruptibly(); assertThat(getRedisLockRegistryLocks(registry)).hasSize(1); - Executors.newSingleThreadExecutor().execute(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { Lock lock2 = registry.obtain("foo"); try { latch1.countDown(); @@ -289,6 +292,7 @@ void testTwoThreads(RedisLockType testRedisLockType) throws Exception { registry.expireUnusedOlderThan(-1000); assertThat(getRedisLockRegistryLocks(registry)).isEmpty(); registry.destroy(); + executorService.shutdown(); } @ParameterizedTest @@ -305,7 +309,8 @@ void testTwoThreadsDifferentRegistries(RedisLockType testRedisLockType) throws E CountDownLatch latch3 = new CountDownLatch(1); lock1.lockInterruptibly(); assertThat(getRedisLockRegistryLocks(registry1)).hasSize(1); - Executors.newSingleThreadExecutor().execute(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { Lock lock2 = registry2.obtain("foo"); try { latch1.countDown(); @@ -340,6 +345,7 @@ void testTwoThreadsDifferentRegistries(RedisLockType testRedisLockType) throws E assertThat(getRedisLockRegistryLocks(registry2)).isEmpty(); registry1.destroy(); registry2.destroy(); + executorService.shutdown(); } @ParameterizedTest @@ -351,7 +357,8 @@ void testTwoThreadsWrongOneUnlocks(RedisLockType testRedisLockType) throws Excep lock.lockInterruptibly(); AtomicBoolean locked = new AtomicBoolean(); CountDownLatch latch = new CountDownLatch(1); - Future result = Executors.newSingleThreadExecutor().submit(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future result = executorService.submit(() -> { try { lock.unlock(); } @@ -370,6 +377,7 @@ void testTwoThreadsWrongOneUnlocks(RedisLockType testRedisLockType) throws Excep registry.expireUnusedOlderThan(-1000); assertThat(getRedisLockRegistryLocks(registry)).isEmpty(); registry.destroy(); + executorService.shutdown(); } @ParameterizedTest @@ -481,7 +489,8 @@ void testExpireNotChanged(RedisLockType testRedisLockType) throws Exception { Long expire = getExpire(registry, "foo"); - Future result = Executors.newSingleThreadExecutor().submit(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future result = executorService.submit(() -> { Lock lock2 = registry.obtain("foo"); assertThat(lock2.tryLock()).isFalse(); return null; @@ -490,6 +499,7 @@ void testExpireNotChanged(RedisLockType testRedisLockType) throws Exception { assertThat(getExpire(registry, "foo")).isEqualTo(expire); lock.unlock(); registry.destroy(); + executorService.shutdown(); } @ParameterizedTest diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/DefaultSftpSessionFactory.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/DefaultSftpSessionFactory.java index da50831a722..8a0d06b7060 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/DefaultSftpSessionFactory.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/DefaultSftpSessionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.time.Duration; @@ -49,6 +50,7 @@ import org.apache.sshd.sftp.client.impl.AbstractSftpClient; import org.apache.sshd.sftp.client.impl.DefaultSftpClient; +import org.springframework.beans.factory.DisposableBean; import org.springframework.core.io.Resource; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.file.remote.session.SessionFactory; @@ -74,10 +76,11 @@ * @author Auke Zaaiman * @author Christian Tzolov * @author Adama Sorho + * @author Darryl Smith * * @since 2.0 */ -public class DefaultSftpSessionFactory implements SessionFactory, SharedSessionCapable { +public class DefaultSftpSessionFactory implements SessionFactory, SharedSessionCapable, DisposableBean { private final Lock lock = new ReentrantLock(); @@ -294,7 +297,7 @@ public SftpSession getSession() { sftpClient = createSftpClient(initClientSession(), this.sftpVersionSelector, SftpErrorDataHandler.EMPTY); freshSftpClient = true; } - sftpSession = new SftpSession(sftpClient); + sftpSession = new SftpSession(sftpClient, this.isSharedSession); sftpSession.connect(); if (this.isSharedSession && freshSftpClient) { this.sharedSftpClient = sftpClient; @@ -421,6 +424,33 @@ protected SftpClient createSftpClient( return new ConcurrentSftpClient(clientSession, initialVersionSelector, errorDataHandler); } + @Override + public void destroy() throws Exception { + if (this.isInnerClient && this.sshClient != null && this.sshClient.isStarted()) { + this.sshClient.stop(); + } + + SftpClient sharedSftpClientToClose = this.sharedSftpClient; + if (sharedSftpClientToClose != null) { + try { + sharedSftpClientToClose.close(); + } + catch (IOException ex) { + throw new UncheckedIOException("failed to close an SFTP client", ex); + } + + try { + ClientSession session = sharedSftpClientToClose.getSession(); + if (session != null && session.isOpen()) { + session.close(); + } + } + catch (IOException ex) { + throw new UncheckedIOException("failed to close an SFTP client (session)", ex); + } + } + } + /** * The {@link DefaultSftpClient} extension to lock the {@link #send(int, Buffer)} * for concurrent interaction. diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpSession.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpSession.java index 414cd2fa5de..9255fc4e25d 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpSession.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpSession.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; +import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.util.net.SshdSocketAddress; import org.apache.sshd.sftp.SftpModuleProperties; import org.apache.sshd.sftp.client.SftpClient; @@ -47,15 +48,30 @@ * @author Gary Russell * @author Artem Bilan * @author Christian Tzolov + * @author Darryl Smith * @since 2.0 */ public class SftpSession implements Session { private final SftpClient sftpClient; + private final boolean isSharedClient; + public SftpSession(SftpClient sftpClient) { + this(sftpClient, false); + } + + /** + * Construct an instance based on a {@link SftpClient} and its {@code shared} status. + * When {@code isSharedClient == true}, the {@link #close()} is void. + * @param sftpClient the {@link SftpClient} to use. + * @param isSharedClient whether the {@link SftpClient} is shared. + * @since 6.3.9 + */ + public SftpSession(SftpClient sftpClient, boolean isSharedClient) { Assert.notNull(sftpClient, "'sftpClient' must not be null"); this.sftpClient = sftpClient; + this.isSharedClient = isSharedClient; } @Override @@ -99,7 +115,7 @@ public Stream doList(String path) throws IOException { boolean isPattern = remoteFile != null && remoteFile.contains("*"); if (!isPattern && remoteFile != null) { - SftpClient.Attributes attributes = this.sftpClient.lstat(path); + SftpClient.Attributes attributes = this.sftpClient.stat(path); if (!attributes.isDirectory()) { return Stream.of(new SftpClient.DirEntry(remoteFile, path, attributes)); } @@ -150,12 +166,26 @@ public void append(InputStream inputStream, String destination) throws IOExcepti @Override public void close() { + if (this.isSharedClient) { + return; + } + try { this.sftpClient.close(); } catch (IOException ex) { throw new UncheckedIOException("failed to close an SFTP client", ex); } + + try { + ClientSession session = this.sftpClient.getSession(); + if (session != null && session.isOpen()) { + session.close(); + } + } + catch (IOException ex) { + throw new UncheckedIOException("failed to close an SFTP client (session)", ex); + } } @Override @@ -189,7 +219,7 @@ public boolean rmdir(String remoteDirectory) throws IOException { @Override public boolean exists(String path) { try { - this.sftpClient.lstat(normalizePath(path)); + this.sftpClient.stat(normalizePath(path)); return true; } catch (SftpException ex) { diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/dsl/SftpTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/dsl/SftpTests.java index 15a7a49922f..f9762a33044 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/dsl/SftpTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/dsl/SftpTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.condition.OS; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.integration.channel.QueueChannel; @@ -43,6 +44,7 @@ import org.springframework.integration.file.FileHeaders; import org.springframework.integration.file.remote.RemoteFileTemplate; import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway; +import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.file.support.FileExistsMode; import org.springframework.integration.sftp.SftpTestSupport; import org.springframework.integration.sftp.session.SftpRemoteFileTemplate; @@ -59,6 +61,7 @@ * @author Gary Russell * @author Joaquin Santana * @author Deepak Gunasekaran + * @author Darryl Smith * * @since 5.0 */ @@ -69,11 +72,14 @@ public class SftpTests extends SftpTestSupport { @Autowired private IntegrationFlowContext flowContext; + @Autowired + private SessionFactory sessionFactory; + @Test public void testSftpInboundFlow() { QueueChannel out = new QueueChannel(); IntegrationFlow flow = IntegrationFlow - .from(Sftp.inboundAdapter(sessionFactory()) + .from(Sftp.inboundAdapter(sessionFactory) .preserveTimestamp(true) .remoteDirectory("/sftpSource") .regexFilter(".*\\.txt$") @@ -106,7 +112,7 @@ public void testSftpInboundFlow() { public void testSftpInboundStreamFlow() throws Exception { QueueChannel out = new QueueChannel(); StandardIntegrationFlow flow = IntegrationFlow.from( - Sftp.inboundStreamingAdapter(new SftpRemoteFileTemplate(sessionFactory())) + Sftp.inboundStreamingAdapter(new SftpRemoteFileTemplate(sessionFactory)) .remoteDirectory("sftpSource") .regexFilter(".*\\.txt$"), e -> e.id("sftpInboundAdapter").poller(Pollers.fixedDelay(100))) @@ -133,7 +139,7 @@ public void testSftpInboundStreamFlow() throws Exception { @Test public void testSftpOutboundFlow() { - IntegrationFlow flow = f -> f.handle(Sftp.outboundAdapter(sessionFactory(), FileExistsMode.FAIL) + IntegrationFlow flow = f -> f.handle(Sftp.outboundAdapter(sessionFactory, FileExistsMode.FAIL) .useTemporaryFileName(false) .fileNameExpression("headers['" + FileHeaders.FILENAME + "']") .remoteDirectory("sftpTarget")); @@ -143,7 +149,7 @@ public void testSftpOutboundFlow() { .setHeader(FileHeaders.FILENAME, fileName) .build()); - RemoteFileTemplate template = new RemoteFileTemplate<>(sessionFactory()); + RemoteFileTemplate template = new RemoteFileTemplate<>(sessionFactory); SftpClient.DirEntry[] files = template.execute(session -> session.list(getTargetRemoteDirectory().getName() + "/" + fileName)); assertThat(files.length).isEqualTo(1); @@ -154,7 +160,7 @@ public void testSftpOutboundFlow() { @Test public void testSftpOutboundFlowSftpTemplate() { - SftpRemoteFileTemplate sftpTemplate = new SftpRemoteFileTemplate(sessionFactory()); + SftpRemoteFileTemplate sftpTemplate = new SftpRemoteFileTemplate(sessionFactory); IntegrationFlow flow = f -> f.handle(Sftp.outboundAdapter(sftpTemplate) .useTemporaryFileName(false) .fileNameExpression("headers['" + FileHeaders.FILENAME + "']") @@ -175,7 +181,7 @@ public void testSftpOutboundFlowSftpTemplate() { @Test public void testSftpOutboundFlowSftpTemplateAndMode() { - SftpRemoteFileTemplate sftpTemplate = new SftpRemoteFileTemplate(sessionFactory()); + SftpRemoteFileTemplate sftpTemplate = new SftpRemoteFileTemplate(sessionFactory); IntegrationFlow flow = f -> f.handle(Sftp.outboundAdapter(sftpTemplate, FileExistsMode.APPEND) .useTemporaryFileName(false) .fileNameExpression("headers['" + FileHeaders.FILENAME + "']") @@ -200,7 +206,7 @@ public void testSftpOutboundFlowSftpTemplateAndMode() { @Test @DisabledOnOs(OS.WINDOWS) public void testSftpOutboundFlowWithChmod() { - IntegrationFlow flow = f -> f.handle(Sftp.outboundAdapter(sessionFactory(), FileExistsMode.FAIL) + IntegrationFlow flow = f -> f.handle(Sftp.outboundAdapter(sessionFactory, FileExistsMode.FAIL) .useTemporaryFileName(false) .fileNameExpression("headers['" + FileHeaders.FILENAME + "']") .chmod(0644) @@ -211,7 +217,7 @@ public void testSftpOutboundFlowWithChmod() { .setHeader(FileHeaders.FILENAME, fileName) .build()); - RemoteFileTemplate template = new RemoteFileTemplate<>(sessionFactory()); + RemoteFileTemplate template = new RemoteFileTemplate<>(sessionFactory); SftpClient.DirEntry[] files = template.execute(session -> session.list(getTargetRemoteDirectory().getName() + "/" + fileName)); assertThat(files.length).isEqualTo(1); @@ -230,7 +236,7 @@ public void testSftpOutboundFlowWithChmod() { public void testSftpMgetFlow() { QueueChannel out = new QueueChannel(); IntegrationFlow flow = f -> f - .handle(Sftp.outboundGateway(sessionFactory(), AbstractRemoteFileOutboundGateway.Command.MGET, + .handle(Sftp.outboundGateway(sessionFactory, AbstractRemoteFileOutboundGateway.Command.MGET, "payload") .options(AbstractRemoteFileOutboundGateway.Option.RECURSIVE) .regexFileNameFilter("(subSftpSource|.*1.txt)") @@ -259,7 +265,7 @@ public void testSftpMgetFlow() { public void testSftpSessionCallback() { QueueChannel out = new QueueChannel(); IntegrationFlow flow = f -> f - .handle((p, h) -> new SftpRemoteFileTemplate(sessionFactory()).execute(s -> s.list(p))) + .handle((p, h) -> new SftpRemoteFileTemplate(sessionFactory).execute(s -> s.list(p))) .channel(out); IntegrationFlowRegistration registration = this.flowContext.registration(flow).register(); registration.getInputChannel().send(new GenericMessage<>("sftpSource")); @@ -277,7 +283,7 @@ public void testSftpSessionCallback() { public void testSftpMv() { QueueChannel out = new QueueChannel(); IntegrationFlow flow = f -> f - .handle(Sftp.outboundGateway(sessionFactory(), AbstractRemoteFileOutboundGateway.Command.MV, "payload") + .handle(Sftp.outboundGateway(sessionFactory, AbstractRemoteFileOutboundGateway.Command.MV, "payload") .renameExpression("payload.concat('.done')") .remoteDirectoryExpression("'sftpSource'")) .channel(out); @@ -301,6 +307,11 @@ public void testSftpMv() { @EnableIntegration public static class ContextConfiguration { + @Bean + public SessionFactory ftpsessionFactory() { + return SftpTests.sessionFactory(); + } + } } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpInboundRemoteFileSystemSynchronizerTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpInboundRemoteFileSystemSynchronizerTests.java index acb1383ea7e..1b6cda0141a 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpInboundRemoteFileSystemSynchronizerTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpInboundRemoteFileSystemSynchronizerTests.java @@ -61,6 +61,7 @@ * @author Gary Russell * @author Artem Bilan * @author Joaquin Santana + * @author Darryl Smith * * @since 2.0 */ @@ -160,6 +161,8 @@ public void testCopyFileToLocalDir() throws Exception { ms.stop(); verify(synchronizer).close(); verify(store).close(); + + ftpSessionFactory.destroy(); } public static class TestSftpSessionFactory extends DefaultSftpSessionFactory { diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpMessageSourceTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpMessageSourceTests.java index 399738e9cd3..72303bd1df6 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpMessageSourceTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpMessageSourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2018-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package org.springframework.integration.sftp.inbound; +import org.apache.sshd.sftp.client.SftpClient; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.integration.file.FileHeaders; +import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.sftp.SftpTestSupport; import org.springframework.messaging.Message; import org.springframework.test.annotation.DirtiesContext; @@ -32,6 +35,7 @@ /** * @author Gary Russell * @author Artem bilan + * @author Darryl Smith * * @since 5.0.7 * @@ -43,6 +47,9 @@ public class SftpMessageSourceTests extends SftpTestSupport { @Autowired private ApplicationContext context; + @Autowired + private SessionFactory sessionFactory; + @Test public void testMaxFetch() { SftpInboundFileSynchronizingMessageSource messageSource = buildSource(); @@ -53,7 +60,7 @@ public void testMaxFetch() { } private SftpInboundFileSynchronizingMessageSource buildSource() { - SftpInboundFileSynchronizer sync = new SftpInboundFileSynchronizer(sessionFactory()); + SftpInboundFileSynchronizer sync = new SftpInboundFileSynchronizer(sessionFactory); sync.setRemoteDirectory("/sftpSource/"); sync.setBeanFactory(this.context); SftpInboundFileSynchronizingMessageSource messageSource = new SftpInboundFileSynchronizingMessageSource(sync); @@ -68,6 +75,11 @@ private SftpInboundFileSynchronizingMessageSource buildSource() { @Configuration public static class Config { + @Bean + public SessionFactory ftpSessionFactory() { + return SftpMessageSourceTests.sessionFactory(); + } + } } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpOutboundTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpOutboundTests.java index 695b90400a2..d4813d1791b 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpOutboundTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpOutboundTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,6 +76,7 @@ * @author Gary Russell * @author Gunnar Hillert * @author Artem Bilan + * @author Darryl Smith */ public class SftpOutboundTests { @@ -84,7 +85,7 @@ public void testHandleFileMessage() throws Exception { File targetDir = new File("remote-target-dir"); assertThat(targetDir.exists()).as("target directory does not exist: " + targetDir.getName()).isTrue(); - SessionFactory sessionFactory = new TestSftpSessionFactory(); + TestSftpSessionFactory sessionFactory = new TestSftpSessionFactory(); FileTransferringMessageHandler handler = new FileTransferringMessageHandler<>(sessionFactory); handler.setRemoteDirectoryExpression(new LiteralExpression(targetDir.getName())); @@ -103,6 +104,8 @@ public void testHandleFileMessage() throws Exception { handler.handleMessage(new GenericMessage<>(srcFile)); assertThat(destFile.exists()).as("destination file was not created").isTrue(); + + sessionFactory.destroy(); } @Test @@ -111,7 +114,7 @@ public void testHandleStringMessage() throws Exception { if (file.exists()) { file.delete(); } - SessionFactory sessionFactory = new TestSftpSessionFactory(); + TestSftpSessionFactory sessionFactory = new TestSftpSessionFactory(); FileTransferringMessageHandler handler = new FileTransferringMessageHandler<>(sessionFactory); DefaultFileNameGenerator fGenerator = new DefaultFileNameGenerator(); @@ -127,6 +130,8 @@ public void testHandleStringMessage() throws Exception { byte[] inFile = FileCopyUtils.copyToByteArray(file); assertThat(new String(inFile)).isEqualTo("String data"); file.delete(); + + sessionFactory.destroy(); } @Test @@ -135,7 +140,7 @@ public void testHandleBytesMessage() throws Exception { if (file.exists()) { file.delete(); } - SessionFactory sessionFactory = new TestSftpSessionFactory(); + TestSftpSessionFactory sessionFactory = new TestSftpSessionFactory(); FileTransferringMessageHandler handler = new FileTransferringMessageHandler<>(sessionFactory); DefaultFileNameGenerator fGenerator = new DefaultFileNameGenerator(); @@ -151,6 +156,8 @@ public void testHandleBytesMessage() throws Exception { byte[] inFile = FileCopyUtils.copyToByteArray(file); assertThat(new String(inFile)).isEqualTo("byte[] data"); file.delete(); + + sessionFactory.destroy(); } @Test //INT-2275 @@ -225,7 +232,7 @@ public void testMkDir() throws Exception { @ParameterizedTest @ValueSource(booleans = {true, false}) - public void testSharedSession(boolean sharedSession) throws IOException { + public void testSharedSession(boolean sharedSession) throws Exception { try (SshServer server = SshServer.setUpDefaultServer()) { server.setPasswordAuthenticator((arg0, arg1, arg2) -> true); server.setPort(0); @@ -253,6 +260,8 @@ public void testSharedSession(boolean sharedSession) throws IOException { assertThat(TestUtils.getPropertyValue(s2, "sftpClient")) .isNotSameAs(TestUtils.getPropertyValue(s1, "sftpClient")); } + + f.destroy(); } } @@ -270,15 +279,15 @@ public void testExists() throws IOException { willReturn(new SftpClient.Attributes()) .given(sftpClient) - .lstat("/exist"); + .stat("/exist"); willThrow(new SftpException(SftpConstants.SSH_FX_NO_SUCH_FILE, "notExist")) .given(sftpClient) - .lstat("/notExist"); + .stat("/notExist"); willThrow(new SshException(SshConstants.SSH_OPEN_CONNECT_FAILED, "Connection lost.")) .given(sftpClient) - .lstat(and(not(eq("/exist")), not(eq("/notExist")))); + .stat(and(not(eq("/exist")), not(eq("/notExist")))); SftpSession sftpSession = new SftpSession(sftpClient); diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests-context.xml b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests-context.xml index 2e210d363b2..fe67b37235c 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests-context.xml +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests-context.xml @@ -1,15 +1,15 @@ - + @@ -18,87 +18,88 @@ + request-channel="inboundGet" + command="get" + command-options="-P" + expression="payload" + local-directory-expression="@extraConfig.targetLocalDirectoryName + #remoteDirectory.toUpperCase()" + local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')" + reply-channel="output"/> + request-channel="invalidDirExpression" + command="get" + expression="payload" + local-directory-expression="T(java.io.File).separator + #remoteDirectory + '?:'" + reply-channel="output"/> + request-channel="inboundMGet" + command="mget" + command-options="-P" + expression="payload" + local-directory-expression="@extraConfig.targetLocalDirectoryName + #remoteDirectory" + local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')" + reply-channel="output"/> + request-channel="inboundMGetRecursive" + command="mget" + expression="payload" + command-options="-R -P" + mode="REPLACE_IF_MODIFIED" + filter="dotStarDotTxtFilter" + local-directory-expression="@extraConfig.targetLocalDirectoryName + #remoteDirectory" + local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')" + reply-channel="output"/> + request-channel="inboundLSRecursive" + command="ls" + expression="payload" + command-options="-R -dirs" + mode="REPLACE_IF_MODIFIED" + filter="dotStarDotTxtFilter" + local-directory-expression="@extraConfig.targetLocalDirectoryName + #remoteDirectory" + local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')" + reply-channel="output"/> + request-channel="inboundLSRecursiveALL" + command="ls" + expression="payload" + command-options="-a -R -dirs" + mode="REPLACE_IF_MODIFIED" + filter="dotStarDotTxtFilter" + local-directory-expression="@extraConfig.targetLocalDirectoryName + #remoteDirectory" + local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')" + reply-channel="output"/> + request-channel="inboundLSRecursiveNoDirs" + command="ls" + expression="payload" + command-options="-R" + mode="REPLACE_IF_MODIFIED" + filter="persistentFilter" + local-directory-expression="@extraConfig.targetLocalDirectoryName + #remoteDirectory" + local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')" + reply-channel="output"/> - - + class="org.springframework.integration.sftp.filters.SftpRegexPatternFileListFilter"> + + - + @@ -107,89 +108,95 @@ + value="#{T(org.springframework.integration.file.remote.RemoteFileTestSupport).getScratchTempFolder().absolutePath}"/> + request-channel="inboundMGetRecursiveFiltered" + command="mget" + expression="payload" + command-options="-R" + filename-regex="(subSftpSource|.*1.txt)" + local-directory-expression="@extraConfig.targetLocalDirectoryName + #remoteDirectory" + local-filename-generator-expression="#remoteFileName.replaceFirst('sftpSource', 'localTarget')" + reply-channel="output"/> + request-channel="inboundMPut" + command="mput" + auto-create-directory="true" + filename-pattern="*.txt" + expression="payload" + chmod="600" + remote-directory="sftpTarget" + reply-channel="output"/> + request-channel="inboundMPutRecursive" + command="mput" + command-options="-R" + auto-create-directory="true" + filename-pattern="*.txt" + expression="payload" + remote-directory="sftpTarget" + reply-channel="output"/> - - + request-channel="inboundMPutRecursiveFiltered" + command="mput" + command-options="-R" + mput-regex="(.*1.txt|sub.*)" + auto-create-directory="true" + filename-pattern="*.txt" + expression="payload" + remote-directory="sftpTarget" + reply-channel="output"/> + + + session-factory="sftpSessionFactory" + channel="appending" + mode="APPEND" + use-temporary-file-name="false" + remote-directory="sftpTarget" + auto-create-directory="true" + remote-file-separator="/"/> - + + session-factory="sftpSessionFactory" + channel="ignoring" + mode="IGNORE" + remote-directory="sftpTarget" + auto-create-directory="true" + remote-file-separator="/"/> - + + session-factory="sftpSessionFactory" + channel="failing" + mode="FAIL" + remote-directory="sftpTarget" + auto-create-directory="true" + remote-file-separator="/"/> + + @@ -198,23 +205,23 @@ + request-channel="inboundGetStream" + command="get" + command-options="-stream" + expression="payload" + remote-directory="ftpTarget" + reply-channel="stream"/> - + + channel="markers"/> + expression="payload.mark.toString().equals('END') ? headers['closeableResource'].close() : null"/> session = this.sessionFactory.getSession(); String dir = "sftpSource/"; long modified = setModifiedOnSource1(); this.inboundGet.send(new GenericMessage(dir + " sftpSource1.txt")); @@ -181,7 +183,6 @@ public void testInt2866LocalDirectoryExpressionGET() { localFile = (File) result.getPayload(); assertThat(localFile.getPath().replaceAll(Matcher.quoteReplacement(File.separator), "/")) .contains(dir.toUpperCase()); - Session session2 = this.sessionFactory.getSession(); } @Test @@ -282,12 +283,12 @@ void testLSRecursive() { List files = (List) result.getPayload(); assertThat(files).hasSize(4); assertThat(files.stream() - .map(fi -> fi.getFilename()) - .collect(Collectors.toList())).contains( - " sftpSource1.txt", - "sftpSource2.txt", - "subSftpSource", - "subSftpSource/subSftpSource1.txt"); + .map(SftpFileInfo::getFilename)) + .contains( + " sftpSource1.txt", + "sftpSource2.txt", + "subSftpSource", + "subSftpSource/subSftpSource1.txt"); } @Test @@ -300,16 +301,16 @@ void testLSRecursiveALL() { List files = (List) result.getPayload(); assertThat(files).hasSize(8); assertThat(files.stream() - .map(fi -> fi.getFilename()) - .collect(Collectors.toList())).contains( - " sftpSource1.txt", - "sftpSource2.txt", - "subSftpSource", - "subSftpSource/subSftpSource1.txt", - ".", - "..", - "subSftpSource/.", - "subSftpSource/.."); + .map(SftpFileInfo::getFilename)) + .contains( + " sftpSource1.txt", + "sftpSource2.txt", + "subSftpSource", + "subSftpSource/subSftpSource1.txt", + ".", + "..", + "subSftpSource/.", + "subSftpSource/.."); } @Test @@ -322,11 +323,11 @@ void testLSRecursiveNoDirs() throws IOException { List files = (List) result.getPayload(); assertThat(files).hasSize(3); assertThat(files.stream() - .map(fi -> fi.getFilename()) - .collect(Collectors.toList())).contains( - " sftpSource1.txt", - "sftpSource2.txt", - "subSftpSource/subSftpSource1.txt"); + .map(SftpFileInfo::getFilename)) + .contains( + " sftpSource1.txt", + "sftpSource2.txt", + "subSftpSource/subSftpSource1.txt"); File newDeepFile = new File(this.sourceRemoteDirectory + "/subSftpSource/subSftpSource2.txt"); OutputStream fos = new FileOutputStream(newDeepFile); fos.write("test".getBytes()); @@ -381,12 +382,12 @@ public void testInt3100RawGET() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); FileCopyUtils.copy(session.readRaw("sftpSource/ sftpSource1.txt"), baos); assertThat(session.finalizeRaw()).isTrue(); - assertThat(new String(baos.toByteArray())).isEqualTo("source1"); + assertThat(baos.toString()).isEqualTo("source1"); baos = new ByteArrayOutputStream(); FileCopyUtils.copy(session.readRaw("sftpSource/sftpSource2.txt"), baos); assertThat(session.finalizeRaw()).isTrue(); - assertThat(new String(baos.toByteArray())).isEqualTo("source2"); + assertThat(baos.toString()).isEqualTo("source2"); session.close(); } @@ -405,12 +406,12 @@ public void testSftpCopy() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); FileCopyUtils.copy(session.readRaw("sftpTarget/sftpTarget2.txt"), baos); assertThat(session.finalizeRaw()).isTrue(); - assertThat(new String(baos.toByteArray())).isEqualTo("source2"); + assertThat(baos.toString()).isEqualTo("source2"); baos = new ByteArrayOutputStream(); FileCopyUtils.copy(session.readRaw("sftpSource/sftpSource2.txt"), baos); assertThat(session.finalizeRaw()).isTrue(); - assertThat(new String(baos.toByteArray())).isEqualTo("source2"); + assertThat(baos.toString()).isEqualTo("source2"); session.close(); } @@ -425,7 +426,8 @@ public void testInt3047ConcurrentSharedSession() throws Exception { PipedOutputStream out2 = new PipedOutputStream(pipe2); final CountDownLatch latch1 = new CountDownLatch(1); final CountDownLatch latch2 = new CountDownLatch(1); - Executors.newSingleThreadExecutor().execute(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { try { session1.write(pipe1, "foo.txt"); } @@ -434,7 +436,7 @@ public void testInt3047ConcurrentSharedSession() throws Exception { } latch1.countDown(); }); - Executors.newSingleThreadExecutor().execute(() -> { + executorService.execute(() -> { try { session2.write(pipe2, "bar.txt"); } @@ -458,12 +460,13 @@ public void testInt3047ConcurrentSharedSession() throws Exception { ByteArrayOutputStream bos2 = new ByteArrayOutputStream(); session1.read("foo.txt", bos1); session2.read("bar.txt", bos2); - assertThat(new String(bos1.toByteArray())).isEqualTo("ace"); - assertThat(new String(bos2.toByteArray())).isEqualTo("bdf"); + assertThat(bos1.toString()).isEqualTo("ace"); + assertThat(bos2.toString()).isEqualTo("bdf"); session1.remove("foo.txt"); session2.remove("bar.txt"); session1.close(); session2.close(); + executorService.shutdown(); } @Test @@ -620,6 +623,21 @@ public void testInt3088MPutRecursiveFiltered() { "sftpTarget/subLocalSource/subLocalSource1.txt"); } + @Test + public void autoCreateTemporaryDirectory() { + Message m = + MessageBuilder.withPayload("test") + .setHeader(FileHeaders.FILENAME, "test.txt") + .build(); + this.sendFileChannel.send(m); + + SftpClient.DirEntry[] files = this.template.execute(session -> session.list("sftpTarget")); + // ., .., and file itself + assertThat(files).hasSize(3).extracting(SftpClient.DirEntry::getFilename).contains("test.txt"); + files = this.template.execute(session -> session.list("sftpTarget_tmp")); + assertThat(files).hasSize(2); // . and .. + } + @Test public void testInt3412FileMode() { Message m = MessageBuilder.withPayload("foo") @@ -633,14 +651,9 @@ public void testInt3412FileMode() { ignoring.send(m); assertLength6(template); - try { - failing.send(m); - fail("Expected exception"); - } - catch (MessagingException e) { - assertThat(e.getCause().getCause().getMessage()).contains("The destination file already exists"); - } - + assertThatExceptionOfType(MessagingException.class) + .isThrownBy(() -> failing.send(m)) + .withStackTraceContaining("The destination file already exists"); } @Test @@ -676,15 +689,15 @@ public void testMessageSessionCallback() { assertThat(receive.getPayload()).isEqualTo("FOO"); } - private void assertLength6(SftpRemoteFileTemplate template) { + private static void assertLength6(SftpRemoteFileTemplate template) { SftpClient.DirEntry[] files = template.execute(session -> session.list("sftpTarget")); - assertThat(files.length).isEqualTo(3); + assertThat(files).hasSize(3); assertThat(files[2].getFilename()).isEqualTo("appending.txt"); assertThat(files[2].getAttributes().getSize()).isEqualTo(6); } @Test - public void testSessionExists() throws IOException { + public void testSessionExists() throws Exception { DefaultSftpSessionFactory sessionFactory = new DefaultSftpSessionFactory(); sessionFactory.setHost("localhost"); sessionFactory.setPort(port); @@ -702,6 +715,8 @@ public void testSessionExists() throws IOException { .isThrownBy(() -> session.exists("any")) .withRootCauseInstanceOf(IOException.class) .withStackTraceContaining("canonicalPath(any) client is closed"); + + sessionFactory.destroy(); } @SuppressWarnings("unused") @@ -750,7 +765,7 @@ public ApplicationEventListeningMessageProducer events() { public MessageChannel eventChannel() { return (msg, timeout) -> { if (this.latch != null) { - if (this.events.size() > 0 || msg.getPayload() instanceof SessionOpenedEvent) { + if (!this.events.isEmpty() || msg.getPayload() instanceof SessionOpenedEvent) { this.events.add((ApacheMinaSftpEvent) msg.getPayload()); if (msg.getPayload() instanceof SessionClosedEvent) { this.latch.countDown(); diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplateTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplateTests.java index acfed39d75e..5ee26f9aa92 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplateTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,6 +54,7 @@ /** * @author Gary Russell * @author Artem Bilan + * @author Darryl Smith * @since 4.1 */ @SpringJUnitConfig @@ -85,7 +86,7 @@ public void testINT3412AppendStatRmdir() { assertThat(template.exists("foo/foobar.txt")).isTrue(); template.executeWithClient((ClientCallbackWithoutResult) client -> { try { - SftpClient.Attributes file = client.lstat("foo/foobar.txt"); + SftpClient.Attributes file = client.stat("foo/foobar.txt"); assertThat(file.getSize()).isEqualTo(6); } catch (IOException e) { @@ -138,7 +139,7 @@ public void lsUserHome() throws IOException { } @Test - public void renameWithOldSftpVersion() { + public void renameWithOldSftpVersion() throws Exception { DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(false); factory.setHost("localhost"); factory.setPort(port); @@ -162,6 +163,8 @@ public void renameWithOldSftpVersion() { "sftpSource/subSftpSource/subSftpSource1.txt")); oldVersionSession.close(); + + factory.destroy(); } @Test diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpServerTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpServerTests.java index 1134ec420d9..0ad10e3eb69 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpServerTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpServerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,6 +50,7 @@ * @author Gary Russell * @author David Liu * @author Artem Bilan + * @author Darryl Smith * * @since 4.1 * @@ -76,6 +77,8 @@ public void testUcPw() throws Exception { f.setAllowUnknownKeys(true); Session session = f.getSession(); doTest(server, session); + + f.destroy(); } } @@ -111,6 +114,8 @@ private void testKeyExchange(String pubKey, String privKey, String passphrase) t f.setPrivateKeyPassphrase(passphrase); Session session = f.getSession(); doTest(server, session); + + f.destroy(); } } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpSessionFactoryTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpSessionFactoryTests.java index 4c81103a1c9..e85f004cc9f 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpSessionFactoryTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpSessionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,16 +24,24 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; import org.apache.sshd.client.SshClient; import org.apache.sshd.client.auth.password.PasswordIdentityProvider; import org.apache.sshd.client.channel.ClientChannel; import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.SshException; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.apache.sshd.sftp.client.SftpClient; +import org.apache.sshd.sftp.client.SftpErrorDataHandler; +import org.apache.sshd.sftp.client.SftpVersionSelector; import org.apache.sshd.sftp.client.impl.AbstractSftpClient; import org.apache.sshd.sftp.server.SftpSubsystemFactory; import org.junit.jupiter.api.Test; @@ -50,6 +58,7 @@ * @author Gary Russell * @author Artem Bilan * @author Auke Zaaiman + * @author Darryl Smith * * @since 3.0.2 */ @@ -101,11 +110,13 @@ public void testConnectFailSocketOpen() throws Exception { } assertThat(server.getActiveSessions().size()).isEqualTo(0); + + f.destroy(); } } @Test - public void concurrentGetSessionDoesntCauseFailure() throws IOException { + public void concurrentGetSessionDoesntCauseFailure() throws Exception { try (SshServer server = SshServer.setUpDefaultServer()) { server.setPasswordAuthenticator((arg0, arg1, arg2) -> true); server.setPort(0); @@ -134,11 +145,13 @@ public void concurrentGetSessionDoesntCauseFailure() throws IOException { .isNotEqualTo(concurrentSessions.get(2)); assertThat(concurrentSessions.get(1)).isNotEqualTo(concurrentSessions.get(2)); + + sftpSessionFactory.destroy(); } } @Test - void externallyProvidedSshClientShouldNotHaveItsConfigurationOverwritten() throws IOException { + void externallyProvidedSshClientShouldNotHaveItsConfigurationOverwritten() throws Exception { try (SshServer server = SshServer.setUpDefaultServer()) { server.setPasswordAuthenticator((arg0, arg1, arg2) -> true); server.setPort(0); @@ -156,11 +169,13 @@ void externallyProvidedSshClientShouldNotHaveItsConfigurationOverwritten() throw sftpSessionFactory.setUser("user"); assertThatNoException().isThrownBy(sftpSessionFactory::getSession); + + sftpSessionFactory.destroy(); } } @Test - void concurrentSessionListDoesntCauseFailure() throws IOException { + void concurrentSessionListDoesntCauseFailure() throws Exception { try (SshServer server = SshServer.setUpDefaultServer()) { server.setPasswordAuthenticator((arg0, arg1, arg2) -> true); server.setPort(0); @@ -192,11 +207,13 @@ void concurrentSessionListDoesntCauseFailure() throws IOException { .toList(); assertThat(dirEntries).hasSize(10); + + sftpSessionFactory.destroy(); } } @Test - void customTimeoutIsApplied() throws IOException { + void customTimeoutIsApplied() throws Exception { try (SshServer server = SshServer.setUpDefaultServer()) { server.setPasswordAuthenticator((arg0, arg1, arg2) -> true); server.setPort(0); @@ -215,6 +232,99 @@ void customTimeoutIsApplied() throws IOException { ClientChannel clientChannel = sftpSessionFactory.getSession().getClientInstance().getClientChannel(); assertThat(AbstractSftpClient.SFTP_CLIENT_CMD_TIMEOUT.getRequired(clientChannel)).hasSeconds(15); + + sftpSessionFactory.destroy(); + } + } + + @Test + void clientSessionIsClosedOnSessionClose() throws Exception { + try (SshServer server = SshServer.setUpDefaultServer()) { + server.setPasswordAuthenticator((arg0, arg1, arg2) -> true); + server.setPort(0); + server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); + server.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); + server.start(); + + DefaultSftpSessionFactory sftpSessionFactory = new DefaultSftpSessionFactory(); + sftpSessionFactory.setHost("localhost"); + sftpSessionFactory.setPort(server.getPort()); + sftpSessionFactory.setUser("user"); + sftpSessionFactory.setPassword("pass"); + sftpSessionFactory.setAllowUnknownKeys(true); + + SftpSession session = sftpSessionFactory.getSession(); + ClientSession clientSession = session.getClientInstance().getClientSession(); + + assertThat(session.isOpen()).isTrue(); + assertThat(clientSession.isOpen()).isTrue(); + + session.close(); + + assertThat(session.isOpen()).isFalse(); + assertThat(clientSession.isClosed()).isTrue(); + + sftpSessionFactory.destroy(); + } + } + + @Test + void sharedSessionConcurrentAccess() throws Exception { + try (SshServer server = SshServer.setUpDefaultServer()) { + server.setPasswordAuthenticator((arg0, arg1, arg2) -> true); + server.setPort(0); + server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); + server.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); + server.start(); + + AtomicInteger clientInstances = new AtomicInteger(); + + DefaultSftpSessionFactory sftpSessionFactory = new DefaultSftpSessionFactory(true) { + + @Override + protected SftpClient createSftpClient(ClientSession clientSession, + SftpVersionSelector initialVersionSelector, SftpErrorDataHandler errorDataHandler) + throws IOException { + + clientInstances.incrementAndGet(); + return super.createSftpClient(clientSession, initialVersionSelector, errorDataHandler); + } + + }; + sftpSessionFactory.setHost("localhost"); + sftpSessionFactory.setPort(server.getPort()); + sftpSessionFactory.setUser("user"); + sftpSessionFactory.setPassword("pass"); + sftpSessionFactory.setAllowUnknownKeys(true); + + ExecutorService executorService = Executors.newFixedThreadPool(10); + + CountDownLatch executionLatch = new CountDownLatch(20); + List errors = Collections.synchronizedList(new ArrayList<>()); + + for (int i = 0; i < 20; i++) { + executorService.execute(() -> { + try (SftpSession session = sftpSessionFactory.getSession()) { + session.list("."); + } + catch (Exception e) { + errors.add(e); + } + executionLatch.countDown(); + }); + } + + assertThat(executionLatch.await(10, TimeUnit.SECONDS)).isTrue(); + synchronized (errors) { + assertThat(errors).isEmpty(); + } + + assertThat(clientInstances).hasValue(1); + + executorService.shutdown(); + assertThat(executorService.awaitTermination(10, TimeUnit.SECONDS)).isTrue(); + + sftpSessionFactory.destroy(); } } diff --git a/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbConfig.java b/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbConfig.java index 27de4cef6cc..bbc8512d616 100644 --- a/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbConfig.java +++ b/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import java.net.URI; import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import jcifs.DialectVersion; @@ -25,7 +27,7 @@ import org.springframework.util.StringUtils; /** - * Data holder class for a SMB share configuration. + * Data holder class for an SMB share configuration. *

* SmbFile URLs syntax: * smb://[[[domain;]username[:password]@]server[:port]/[[share/[dir/]file]]][?[param=value[param2=value2[...]]] @@ -34,6 +36,7 @@ * @author Prafull Kumar Soni * @author Artem Bilan * @author Gregory Bragg + * @author Jelle Smits * * @since 6.0 */ @@ -163,16 +166,19 @@ public void setSmbMaxVersion(DialectVersion _smbMaxVersion) { this.smbMaxVersion = _smbMaxVersion; } - String getDomainUserPass(boolean _includePassword) { + String getDomainUserPass(boolean _includePassword, boolean _urlEncode) { String domainUserPass; + String username = _urlEncode ? URLEncoder.encode(this.username, StandardCharsets.UTF_8) : this.username; + String password = _urlEncode ? URLEncoder.encode(this.password, StandardCharsets.UTF_8) : this.password; if (StringUtils.hasText(this.domain)) { - domainUserPass = String.format("%s;%s", this.domain, this.username); + String domain = _urlEncode ? URLEncoder.encode(this.domain, StandardCharsets.UTF_8) : this.domain; + domainUserPass = String.format("%s;%s", domain, username); } else { - domainUserPass = this.username; + domainUserPass = username; } - if (StringUtils.hasText(this.password)) { - domainUserPass += ":" + (_includePassword ? this.password : "********"); + if (StringUtils.hasText(password)) { + domainUserPass += ":" + (_includePassword ? password : "********"); } return domainUserPass; } @@ -197,22 +203,52 @@ public final String getUrl() { } public final String getUrl(boolean _includePassword) { - String domainUserPass = getDomainUserPass(_includePassword); + return createUri(_includePassword).toASCIIString(); + } - String path = StringUtils.cleanPath(this.shareAndDir); + /** + * Return the url string for the share connection without encoding. + * Used in the {@link SmbShare} constructor delegation. + * @return the url string for the share connection without encoding. + * @since 6.3.8 + */ + public final String rawUrl() { + return rawUrl(true); + } - if (!path.startsWith("/")) { - path = "/" + path; - } + /** + * Return the url string for the share connection without encoding + * the host and path. The {@code domainUserPass} is encoded, as + * {@link java.net.URL} requires them to be encoded otherwise its parsing fails. + * Used in the {@link SmbShare} constructor delegation. + * @param _includePassword whether password has to be masked in credentials of URL. + * @return the url string for the share connection without encoding. + * @since 6.3.8 + */ + public final String rawUrl(boolean _includePassword) { + String domainUserPass = getDomainUserPass(_includePassword, true); + String path = cleanPath(); + return "smb://%s@%s%s".formatted(domainUserPass, getHostPort(), path); + } + private URI createUri(boolean _includePassword) { + String domainUserPass = getDomainUserPass(_includePassword, false); + String path = cleanPath(); try { - return new URI("smb", domainUserPass, this.host, this.port, path, null, null) - .toASCIIString(); + return new URI("smb", domainUserPass, this.host, this.port, path, null, null); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } + } + + private String cleanPath() { + String path = StringUtils.cleanPath(this.shareAndDir); + if (!path.startsWith("/")) { + path = "/" + path; + } + return path; } @Override diff --git a/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbSession.java b/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbSession.java index c60a9322e0c..0db839c493b 100644 --- a/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbSession.java +++ b/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbSession.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,10 +39,10 @@ * Implementation of the {@link Session} interface for Server Message Block (SMB) * also known as Common Internet File System (CIFS). The Samba project set out to * create non-Windows implementations of SMB. Often Samba is thus used synonymously to SMB. - * + *

* SMB is an application-layer network protocol that manages shared access to files, printers * and other networked resources. - * + *

* See Server Message Block * for more details. * @@ -53,6 +53,7 @@ * @author Prafull Kumar Soni * @author Gregory Bragg * @author Adam Jones + * @author Paolo Fosser * * @since 6.0 */ @@ -60,8 +61,6 @@ public class SmbSession implements Session { private static final LogAccessor logger = new LogAccessor(SmbSession.class); - private static final String FILE_SEPARATOR = System.getProperty("file.separator"); - private static final String SMB_FILE_SEPARATOR = "/"; private final SmbShare smbShare; @@ -118,33 +117,46 @@ public boolean remove(String _path) throws IOException { } /** - * Return the contents of the specified SMB resource as an array of SmbFile objects. + * Return the content of the specified SMB resource as an array of SmbFile objects. * In case the remote resource does not exist, an empty array is returned. - * @param _path path to a remote directory + * @param path path to a remote directory or remote file path * @return array of SmbFile objects - * @throws IOException on error conditions returned by a CIFS server or if the remote resource is not a directory. + * @throws IOException on error conditions returned by a CIFS server. */ @Override - public SmbFile[] list(String _path) throws IOException { - try { - SmbFile smbDir = createSmbDirectoryObject(_path); - if (!smbDir.exists()) { - logger.warn(() -> "Remote directory [" + _path + "] does not exist. Cannot list resources."); - return new SmbFile[0]; - } - else if (!smbDir.isDirectory()) { - throw new IOException("[" + _path + "] is not a directory. Cannot list resources."); + public SmbFile[] list(String path) throws IOException { + String remotePath = StringUtils.trimTrailingCharacter(path, '/'); + SmbFile[] files = null; + int lastIndex = StringUtils.hasText(remotePath) ? remotePath.lastIndexOf('/') : 0; + String remoteFileName = lastIndex > 0 ? remotePath.substring(lastIndex + 1) : null; + if (StringUtils.hasText(remoteFileName)) { + SmbFile remoteFile = createSmbFileObject(remotePath); + if (remoteFile.isFile()) { + files = new SmbFile[] {remoteFile}; } + } - SmbFile[] files = smbDir.listFiles(); + if (files == null) { + try { + SmbFile smbDir = createSmbDirectoryObject(remotePath); + if (!smbDir.exists()) { + logger.warn(() -> "Remote directory [" + remotePath + "] does not exist. Cannot list resources."); + return new SmbFile[0]; + } + else if (!smbDir.isDirectory()) { + throw new IOException("[" + remotePath + "] is not a directory. Cannot list resources."); + } + + files = smbDir.listFiles(); + } + catch (SmbException _ex) { + throw new IOException("Failed to list in [" + remotePath + "].", _ex); + } + } - logListedFiles(_path, files); + logListedFiles(remotePath, files); - return files; - } - catch (SmbException _ex) { - throw new IOException("Failed to list in [" + _path + "].", _ex); - } + return files; } /** @@ -324,7 +336,7 @@ public boolean isDirectory(String _path) throws IOException { * @throws IOException on error conditions returned by a CIFS server */ String mkdirs(String _path) throws IOException { - int idxPath = _path.lastIndexOf(FILE_SEPARATOR); + int idxPath = _path.lastIndexOf(SMB_FILE_SEPARATOR); if (idxPath > -1) { String path = _path.substring(0, idxPath + 1); mkdir(path); diff --git a/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbShare.java b/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbShare.java index d287a481e19..f96102937e2 100644 --- a/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbShare.java +++ b/spring-integration-smb/src/main/java/org/springframework/integration/smb/session/SmbShare.java @@ -63,7 +63,7 @@ public class SmbShare extends SmbFile { * @throws IOException if an invalid SMB URL was constructed by jCIFS */ public SmbShare(SmbConfig _smbConfig) throws IOException { - super(StringUtils.cleanPath(_smbConfig.validate().getUrl()), + super(StringUtils.cleanPath(_smbConfig.validate().rawUrl()), SingletonContext.getInstance().withCredentials( new NtlmPasswordAuthenticator( _smbConfig.getDomain(), _smbConfig.getUsername(), _smbConfig.getPassword()))); @@ -76,7 +76,7 @@ public SmbShare(SmbConfig _smbConfig) throws IOException { * @throws IOException if an invalid SMB URL was constructed by jCIFS */ public SmbShare(SmbConfig _smbConfig, CIFSContext _context) throws IOException { - super(StringUtils.cleanPath(_smbConfig.validate().getUrl()), _context); + super(StringUtils.cleanPath(_smbConfig.validate().rawUrl()), _context); } /** @@ -88,7 +88,7 @@ public SmbShare(SmbConfig _smbConfig, CIFSContext _context) throws IOException { * @throws IOException if an invalid property was set or an invalid SMB URL was constructed by jCIFS */ public SmbShare(SmbConfig _smbConfig, Properties _props) throws IOException { - super(StringUtils.cleanPath(_smbConfig.validate().getUrl()), + super(StringUtils.cleanPath(_smbConfig.validate().rawUrl()), new BaseContext( new PropertyConfiguration(_props)).withCredentials( new NtlmPasswordAuthenticator( diff --git a/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbMessageHistoryTests-context.xml b/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbMessageHistoryTests-context.xml index 9c9bf00a913..ecda2bf3478 100644 --- a/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbMessageHistoryTests-context.xml +++ b/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbMessageHistoryTests-context.xml @@ -1,30 +1,31 @@ - - - - - - + + + + + + + session-factory="smbSessionFactory" + channel="smbInboundChannel" + auto-create-local-directory="true" + local-directory="file:test-temp/local-5" + remote-directory="test-temp/remote-9" + auto-startup="false" + delete-remote-files="false"> diff --git a/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbMessageHistoryTests.java b/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbMessageHistoryTests.java index 643fbe3cd7e..90a30b1cf92 100644 --- a/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbMessageHistoryTests.java +++ b/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbMessageHistoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,12 @@ package org.springframework.integration.smb; import java.net.URI; -import java.net.URISyntaxException; +import java.net.URL; +import java.util.Properties; +import jcifs.CIFSContext; +import jcifs.config.PropertyConfiguration; +import jcifs.context.BaseContext; import org.junit.jupiter.api.Test; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -32,11 +36,12 @@ * @author Prafull Kumar Soni * @author Artem Bilan * @author Gregory Bragg + * @author Jelle Smits */ public class SmbMessageHistoryTests extends AbstractBaseTests { @Test - public void testMessageHistory() throws URISyntaxException { + public void testMessageHistory() throws Exception { try (ClassPathXmlApplicationContext applicationContext = getApplicationContext()) { SourcePollingChannelAdapter adapter = applicationContext .getBean("smbInboundChannelAdapter", SourcePollingChannelAdapter.class); @@ -47,8 +52,16 @@ public void testMessageHistory() throws URISyntaxException { String url = smbSessionFactory.getUrl(); URI uri = new URI(url); - assertThat("sambagu%40est:sambag%25uest").isEqualTo(uri.getRawUserInfo()); - assertThat("sambagu@est:sambag%uest").isEqualTo(uri.getUserInfo()); + assertThat(uri.getRawUserInfo()).isEqualTo("sambagu%40est:sambag%25uest"); + assertThat(uri.getUserInfo()).isEqualTo("sambagu@est:sambag%uest"); + assertThat(uri.getPath()).isEqualTo("/smb share/"); + assertThat(uri.getRawPath()).isEqualTo("/smb%20share/"); + + CIFSContext context = new BaseContext(new PropertyConfiguration(new Properties())); + URL rawUrl = new URL(null, smbSessionFactory.rawUrl(true), context.getUrlHandler()); + assertThat(rawUrl.getHost()).isEqualTo("localhost"); + assertThat(rawUrl.getUserInfo()).isEqualTo("sambagu%40est:sambag%25uest"); + assertThat(rawUrl.getPath()).isEqualTo("/smb share/"); } } diff --git a/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbParserInboundTests-context.xml b/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbParserInboundTests-context.xml index a3bced155cd..88483c27ac1 100644 --- a/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbParserInboundTests-context.xml +++ b/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbParserInboundTests-context.xml @@ -1,48 +1,49 @@ - - - - + + + + - + session-factory="smbSessionFactory" + channel="smbIn" + filename-pattern="foo" + local-directory="test-temp/local-10" + remote-directory="test-temp/remote-10" + auto-create-local-directory="true" + auto-startup="false" + delete-remote-files="false"> + - + session-factory="smbSessionFactory" + channel="smbIn" + filter="filter" + local-directory="test-temp" + remote-directory="test-temp/remote-11" + auto-create-local-directory="true" + auto-startup="false" + delete-remote-files="false"> + - + - + diff --git a/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbParserInboundTests.java b/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbParserInboundTests.java index 783d80fb39f..8d6f2dba314 100644 --- a/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbParserInboundTests.java +++ b/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbParserInboundTests.java @@ -60,9 +60,4 @@ public void cleanUp() { delete("test-temp/local-10", "test-temp/local-6"); } - public static void main(String[] _args) throws Exception { - new SmbParserInboundTests().cleanUp(); - runTests(SmbParserInboundTests.class, "testLocalFilesAutoCreationTrue", "testLocalFilesAutoCreationFalse"); - } - } diff --git a/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbTestSupport.java b/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbTestSupport.java index da823f0a030..b8020972572 100644 --- a/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbTestSupport.java +++ b/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbTestSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,6 @@ import org.springframework.integration.file.remote.session.Session; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.smb.session.SmbSessionFactory; -import org.springframework.integration.test.util.TestUtils; /** * Provides a connection to a Testcontainers-driven SMB Server for test cases. @@ -69,7 +68,7 @@ public class SmbTestSupport extends RemoteFileTestSupport { public static final String HOST = "127.0.0.1"; - public static final String SHARE_AND_DIR = "smb-share"; + public static final String SHARE_AND_DIR = "smb share"; public static final String USERNAME = "sambaguest"; @@ -101,22 +100,17 @@ public static void connectToSMBServer() throws IOException { try (Session smbFileSession = smbSessionFactory.getSession()) { smbFileSession.mkdir("smbTarget"); Charset charset = StandardCharsets.UTF_8; - smbFileSession.write(IOUtils.toInputStream("source1", charset), - TestUtils.applySystemFileSeparator("smbSource/smbSource1.txt")); - smbFileSession.write(IOUtils.toInputStream("source2", charset), - TestUtils.applySystemFileSeparator("smbSource/smbSource2.txt")); + smbFileSession.write(IOUtils.toInputStream("source1", charset), "smbSource/smbSource1.txt"); + smbFileSession.write(IOUtils.toInputStream("source2", charset), "smbSource/smbSource2.txt"); smbFileSession.write(IOUtils.toInputStream("", charset), "SMBSOURCE1.TXT.a"); smbFileSession.write(IOUtils.toInputStream("", charset), "SMBSOURCE2.TXT.a"); - - smbFileSession.write(IOUtils.toInputStream("subSource1", charset), - TestUtils.applySystemFileSeparator("smbSource/subSmbSource/subSmbSource1.txt")); - smbFileSession.write(IOUtils.toInputStream("subSource2", charset), - TestUtils.applySystemFileSeparator("smbSource/subSmbSource/subSmbSource2.txt")); + smbFileSession.write(IOUtils.toInputStream("subSource1", charset), "smbSource/subSmbSource/subSmbSource1.txt"); + smbFileSession.write(IOUtils.toInputStream("subSource2", charset), "smbSource/subSmbSource/subSmbSource2.txt"); } } public static String smbServerUrl() { - return smbSessionFactory.getUrl().replaceFirst('/' + SHARE_AND_DIR + '/', ""); + return smbSessionFactory.rawUrl().replaceFirst('/' + SHARE_AND_DIR + '/', ""); } public static SessionFactory sessionFactory() { diff --git a/spring-integration-smb/src/test/java/org/springframework/integration/smb/dsl/SmbTests.java b/spring-integration-smb/src/test/java/org/springframework/integration/smb/dsl/SmbTests.java index cfc4800b70a..846baff286b 100644 --- a/spring-integration-smb/src/test/java/org/springframework/integration/smb/dsl/SmbTests.java +++ b/spring-integration-smb/src/test/java/org/springframework/integration/smb/dsl/SmbTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -92,7 +92,7 @@ public void testSmbInboundFlow() { DirectoryScanner scanner = new DefaultDirectoryScanner(); IntegrationFlow flow = IntegrationFlow.from(Smb.inboundAdapter(sessionFactory()) .preserveTimestamp(true) - .remoteDirectory("smbSource") + .remoteDirectory("smbSource/subSmbSource/") .maxFetchSize(10) .scanner(scanner) .regexFilter(".*\\.txt$") @@ -115,13 +115,13 @@ public void testSmbInboundFlow() { Object payload = message.getPayload(); assertThat(payload).isInstanceOf(File.class); File file = (File) payload; - assertThat(file.getName()).isIn("SMBSOURCE1.TXT.a", "SMBSOURCE2.TXT.a"); + assertThat(file.getName()).isEqualTo("SUBSMBSOURCE1.TXT.a", "SUBSMBSOURCE2.TXT.a"); assertThat(file.getAbsolutePath()).contains("localTarget"); message = out.receive(10_000); assertThat(message).isNotNull(); file = (File) message.getPayload(); - assertThat(file.getName()).isIn("SMBSOURCE1.TXT.a", "SMBSOURCE2.TXT.a"); + assertThat(file.getName()).isIn("SUBSMBSOURCE1.TXT.a", "SUBSMBSOURCE2.TXT.a"); assertThat(file.getAbsolutePath()).contains("localTarget"); assertThat(out.receive(10)).isNull(); @@ -164,7 +164,7 @@ public void testSmbInboundStreamFlow() throws Exception { } @Test - public void testSmbOutboundFlow() { + public void testSmbOutboundFlow() throws SmbException { IntegrationFlow flow = f -> f .handle(Smb.outboundAdapter(sessionFactory(), FileExistsMode.REPLACE) .useTemporaryFileName(false) @@ -181,18 +181,13 @@ public void testSmbOutboundFlow() { SmbFile[] files = template.execute(session -> session.list(getTargetRemoteDirectory().getName())); assertThat(files).hasSize(1); - try { - assertThat(files[0].length()).isEqualTo(3); - } - catch (SmbException se) { - se.printStackTrace(); - } + assertThat(files[0].length()).isEqualTo(3); registration.destroy(); } @Test - public void testSmbOutboundFlowWithSmbRemoteTemplate() { + public void testSmbOutboundFlowWithSmbRemoteTemplate() throws SmbException { SmbRemoteFileTemplate smbTemplate = new SmbRemoteFileTemplate(sessionFactory()); IntegrationFlow flow = f -> f .handle(Smb.outboundAdapter(smbTemplate) @@ -209,18 +204,13 @@ public void testSmbOutboundFlowWithSmbRemoteTemplate() { SmbFile[] files = smbTemplate.execute(session -> session.list(getTargetRemoteDirectory().getName())); assertThat(files).hasSize(1); - try { - assertThat(files[0].length()).isEqualTo(3); - } - catch (SmbException se) { - se.printStackTrace(); - } + assertThat(files[0].length()).isEqualTo(3); registration.destroy(); } @Test - public void testSmbOutboundFlowWithSmbRemoteTemplateAndMode() { + public void testSmbOutboundFlowWithSmbRemoteTemplateAndMode() throws SmbException { SmbRemoteFileTemplate smbTemplate = new SmbRemoteFileTemplate(sessionFactory()); IntegrationFlow flow = f -> f .handle(Smb.outboundAdapter(smbTemplate, FileExistsMode.APPEND) @@ -242,18 +232,33 @@ public void testSmbOutboundFlowWithSmbRemoteTemplateAndMode() { SmbFile[] files = smbTemplate.execute(session -> session.list(getTargetRemoteDirectory().getName())); assertThat(files).hasSize(1); - try { - assertThat(files[0].length()).isEqualTo(9); - } - catch (SmbException se) { - se.printStackTrace(); - } + assertThat(files[0].length()).isEqualTo(9); registration.destroy(); } @Test public void testSmbGetFlow() { + QueueChannel out = new QueueChannel(); + IntegrationFlow flow = f -> f + .handle( + Smb.outboundGateway(sessionFactory(), AbstractRemoteFileOutboundGateway.Command.GET, "payload") + .localDirectoryExpression("'" + getTargetLocalDirectoryName() + "'")) + .channel(out); + IntegrationFlowRegistration registration = this.flowContext.registration(flow).register(); + String fileName = "smbSource/subSmbSource/subSmbSource2.txt"; + registration.getInputChannel().send(new GenericMessage<>(fileName)); + Message result = out.receive(10_000); + assertThat(result).isNotNull(); + + File sfis = (File) result.getPayload(); + assertThat(sfis).hasFileName("subSmbSource2.txt"); + + registration.destroy(); + } + + @Test + public void testSmbGetStreamFlow() throws IOException { QueueChannel out = new QueueChannel(); IntegrationFlow flow = f -> f .handle( @@ -271,14 +276,10 @@ public void testSmbGetFlow() { Message result = out.receive(10_000); assertThat(result).isNotNull(); - SmbFileInputStream sfis = (SmbFileInputStream) result.getPayload(); - assertThat(sfis).isNotNull(); - - try { - sfis.close(); - } - catch (IOException ioe) { + try (SmbFileInputStream sfis = (SmbFileInputStream) result.getPayload()) { + assertThat(sfis).isNotNull(); } + registration.destroy(); } diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java index efceadd475f..c777d4c9763 100644 --- a/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.SubscribableChannel; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -48,6 +49,7 @@ * @author Artem Bilan */ @RunWith(SpringJUnit4ClassRunner.class) +@DirtiesContext public abstract class AbstractRequestResponseScenarioTests { private List scenarios = null; diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java index 5d3a63183df..81e1a8a13ed 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import reactor.core.publisher.Mono; @@ -59,6 +60,7 @@ * * @author Artem Bilan * @author Yicheng Feng + * @author Alexander Hain * * @since 5.0 * @@ -111,7 +113,7 @@ public void afterSingletonsInstantiated() { } List getAutoStartupCandidates() { - return this.autoStartupCandidates; + return Collections.unmodifiableList(this.autoStartupCandidates); } /** diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizerFactory.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizerFactory.java index a29d2eca965..29387bb60da 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizerFactory.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,17 +18,20 @@ import java.util.List; -import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.TestContextAnnotationUtils; /** * The {@link ContextCustomizerFactory} implementation to produce a * {@link MockIntegrationContextCustomizer} if a {@link SpringIntegrationTest} annotation * is present on the test class. + *

+ * Honors the {@link org.springframework.test.context.NestedTestConfiguration} semantics. * * @author Artem Bilan + * @author Chris Bono * * @since 5.0 */ @@ -38,7 +41,7 @@ class MockIntegrationContextCustomizerFactory implements ContextCustomizerFactor public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - return AnnotatedElementUtils.hasAnnotation(testClass, SpringIntegrationTest.class) + return TestContextAnnotationUtils.hasAnnotation(testClass, SpringIntegrationTest.class) ? new MockIntegrationContextCustomizer() : null; } diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTest.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTest.java index c6f9dd13df8..02bd8d90abe 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTest.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,6 +51,8 @@ * * } * + *

+ * Honors the {@link org.springframework.test.context.NestedTestConfiguration} semantics. * * @author Artem Bilan * diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTestExecutionListener.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTestExecutionListener.java index 836fcec26e2..078dc0af97b 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTestExecutionListener.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/SpringIntegrationTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2022 the original author or authors. + * Copyright 2017-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +19,9 @@ import java.util.Arrays; import org.springframework.context.ApplicationContext; -import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.integration.endpoint.AbstractEndpoint; import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.test.context.TestExecutionListener; import org.springframework.util.PatternMatchUtils; @@ -39,7 +39,7 @@ class SpringIntegrationTestExecutionListener implements TestExecutionListener { @Override public void prepareTestInstance(TestContext testContext) { SpringIntegrationTest springIntegrationTest = - AnnotatedElementUtils.findMergedAnnotation(testContext.getTestClass(), SpringIntegrationTest.class); + TestContextAnnotationUtils.findMergedAnnotation(testContext.getTestClass(), SpringIntegrationTest.class); String[] patterns = springIntegrationTest != null ? springIntegrationTest.noAutoStartup() : new String[0]; diff --git a/spring-integration-test/src/test/java/org/springframework/integration/test/context/AbstractIntegrationTest.java b/spring-integration-test/src/test/java/org/springframework/integration/test/context/AbstractIntegrationTest.java new file mode 100644 index 00000000000..466cef50b25 --- /dev/null +++ b/spring-integration-test/src/test/java/org/springframework/integration/test/context/AbstractIntegrationTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.test.context; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.endpoint.AbstractEndpoint; +import org.springframework.integration.endpoint.ReactiveMessageSourceProducer; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * Base integration test that specifies a default {@link SpringIntegrationTest} + * to be inherited by concrete subclasses. + * + * @author Chris Bono + * + * @since 6.2.10 + */ +@SpringJUnitConfig +@SpringIntegrationTest(noAutoStartup = "*") +class AbstractIntegrationTest { + + @Configuration(proxyBeanMethods = false) + @EnableIntegration + static class MockEndpointConfig { + + @Bean + AbstractEndpoint mockEndpoint() { + ReactiveMessageSourceProducer endpoint = + new ReactiveMessageSourceProducer(() -> new GenericMessage<>("testFromMockEndpoint")); + endpoint.setOutputChannelName("nullChannel"); + return endpoint; + } + + } +} diff --git a/spring-integration-test/src/test/java/org/springframework/integration/test/context/NestedSpringIntegrationTestAnnotationTests.java b/spring-integration-test/src/test/java/org/springframework/integration/test/context/NestedSpringIntegrationTestAnnotationTests.java new file mode 100644 index 00000000000..a9c40970c18 --- /dev/null +++ b/spring-integration-test/src/test/java/org/springframework/integration/test/context/NestedSpringIntegrationTestAnnotationTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.test.context; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.integration.endpoint.AbstractEndpoint; +import org.springframework.lang.Nullable; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.NestedTestConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Concrete specialization of {@link AbstractIntegrationTest} used to + * test inherit and override behavior of {@link SpringIntegrationTest} + * when used with {@link Nested} and {@link NestedTestConfiguration}. + * + * @author Chris Bono + * @author Artem Bilan + * + * @since 6.2.10 + */ +class NestedSpringIntegrationTestAnnotationTests extends AbstractIntegrationTest { + + @Test + void annotationDefinedOnParentIsInheritedByDefault(@Autowired AbstractEndpoint mockEndpoint) { + assertThat(mockEndpoint.isRunning()).isFalse(); + } + + @Nested + class NestedTestDefaultEnclosingConfiguration { + + @Test + void annotationDefinedOnParentOfEnclosingIsInheritedByDefault(@Autowired AbstractEndpoint mockEndpoint) { + assertThat(mockEndpoint.isRunning()).isFalse(); + } + + } + + @Nested + @NestedTestConfiguration(NestedTestConfiguration.EnclosingConfiguration.INHERIT) + class NestedTestWithInheritEnclosingConfiguration { + + @Test + void annotationDefinedOnParentOfEnclosingIsInherited(@Autowired AbstractEndpoint mockEndpoint) { + assertThat(mockEndpoint.isRunning()).isFalse(); + } + + } + + @Nested + @NestedTestConfiguration(NestedTestConfiguration.EnclosingConfiguration.INHERIT) + @SpringIntegrationTest(noAutoStartup = "noSuchEndpointWithThisPatternExists") + class NestedTestWithInheritEnclosingConfigurationButOverrideAnnotation { + + @Test + void annotationDefinedOnParentOfEnclosingIsOverridden(@Autowired AbstractEndpoint mockEndpoint) { + assertThat(mockEndpoint.isRunning()).isTrue(); + } + + } + + @Nested + @NestedTestConfiguration(NestedTestConfiguration.EnclosingConfiguration.OVERRIDE) + @ContextConfiguration(classes = MockEndpointConfig.class) + class NestedTestWithOverrideEnclosingConfiguration { + + @Test + void annotationDefinedOnParentOfEnclosingIsIgnored(@Autowired AbstractEndpoint mockEndpoint, + @Nullable @Autowired MockIntegrationContext mockIntegrationContext) { + + assertThat(mockEndpoint.isRunning()).isTrue(); + assertThat(mockIntegrationContext).isNull(); + } + + } + +} diff --git a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/observation/WebFluxObservationPropagationTests.java b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/observation/WebFluxObservationPropagationTests.java index 8eb0e4476a3..92a985c5f73 100644 --- a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/observation/WebFluxObservationPropagationTests.java +++ b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/observation/WebFluxObservationPropagationTests.java @@ -36,26 +36,33 @@ import io.micrometer.tracing.test.simple.SpansAssert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.integration.channel.FluxMessageChannel; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.config.EnableIntegrationManagement; +import org.springframework.integration.core.MessagingTemplate; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.webflux.dsl.WebFlux; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; import org.springframework.messaging.PollableChannel; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.test.web.reactive.server.HttpHandlerConnector; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -81,6 +88,10 @@ public class WebFluxObservationPropagationTests { @Autowired private PollableChannel testChannel; + @Autowired + @Qualifier("webFluxRequestReplyClientFlow.input") + private MessageChannel webFluxRequestReplyClientFlowInput; + @BeforeEach void setup() { SPANS.clear(); @@ -122,10 +133,24 @@ void observationIsPropagatedWebFluxRequestReply() { .haveSameTraceId(); } + @Test + void observationIsPropagatedWebFluxClientRequestReply() { + String result = + new MessagingTemplate() + .convertSendAndReceive(this.webFluxRequestReplyClientFlowInput, "test", String.class); + + assertThat(result).isEqualTo("SOME REPLY"); + + // There is a race condition when we already have a reply, but the span in the last channel is not closed yet. + await().untilAsserted(() -> assertThat(SPANS.spans()).hasSize(5)); + SpansAssert.assertThat(SPANS.spans().stream().map(BraveFinishedSpan::fromBrave).collect(Collectors.toList())) + .haveSameTraceId(); + } + @Configuration @EnableWebFlux @EnableIntegration - @EnableIntegrationManagement(observationPatterns = "*") + @EnableIntegrationManagement(observationPatterns = "*,!notObserved*") public static class ContextConfiguration { @Bean @@ -188,6 +213,29 @@ FluxMessageChannel webFluxRequestChannel() { return new FluxMessageChannel(); } + @Bean + IntegrationFlow webFluxRequestReplyClientFlow(ObservationRegistry registry) { + ClientHttpConnector httpConnector = + new HttpHandlerConnector((request, response) -> { + response.setStatusCode(HttpStatus.OK); + + Mono replyData = Mono.just(response.bufferFactory().wrap("some reply".getBytes())); + + return response.writeWith(replyData) + .then(Mono.defer(response::setComplete)); + }); + WebClient webClient = + WebClient.builder() + .clientConnector(httpConnector) + .observationRegistry(registry) + .build(); + + return f -> f + .handle(WebFlux.outboundGateway(message -> "/someRequest", webClient) + .expectedResponseType(String.class)) + .transform(String::toUpperCase); + } + @Bean IntegrationFlow webFluxRequestReplyFlow( @Qualifier("webFluxRequestChannel") FluxMessageChannel webFluxRequestChannel) { @@ -200,6 +248,8 @@ IntegrationFlow webFluxRequestReplyFlow( .transformWith(t -> t .transformer(String::toLowerCase) .id("testTransformer")) + .channel("notObservedChannel") + .bridge(e -> e.id("notObservedEndpoint")) .get(); } diff --git a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java index 29893db7790..1cfaba45020 100644 --- a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java +++ b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/outbound/WebFluxRequestExecutingMessageHandlerTests.java @@ -32,6 +32,7 @@ import org.springframework.core.io.buffer.DataBufferLimitException; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpConnector; @@ -95,6 +96,47 @@ void testReactiveReturn() { .verify(Duration.ofSeconds(10)); } + @Test + void noContentLengthHeaderForGetMethod() { + ClientHttpConnector httpConnector = + new HttpHandlerConnector((request, response) -> { + assertThat(request.getHeaders()) + .doesNotContainKey(org.springframework.http.HttpHeaders.CONTENT_LENGTH); + response.setStatusCode(HttpStatus.OK); + return Mono.defer(response::setComplete); + }); + + WebClient webClient = WebClient.builder() + .clientConnector(httpConnector) + .build(); + + String destinationUri = "https://www.springsource.org/spring-integration"; + WebFluxRequestExecutingMessageHandler reactiveHandler = + new WebFluxRequestExecutingMessageHandler(destinationUri, webClient); + reactiveHandler.setHttpMethod(HttpMethod.GET); + + FluxMessageChannel ackChannel = new FluxMessageChannel(); + reactiveHandler.setOutputChannel(ackChannel); + String testPayload = "hello, world"; + Message testMessage = + MessageBuilder.withPayload(testPayload) + .setHeader(org.springframework.http.HttpHeaders.CONTENT_LENGTH, testPayload.length()) + .build(); + reactiveHandler.handleMessage(testMessage); + reactiveHandler.handleMessage(testMessage); + + StepVerifier.create(ackChannel, 2) + .assertNext(m -> + assertThat(m.getHeaders()) + .containsEntry(HttpHeaders.STATUS_CODE, HttpStatus.OK) + // The reply message headers are copied from the request message + .containsEntry(org.springframework.http.HttpHeaders.CONTENT_LENGTH, testPayload.length())) + .assertNext(m -> assertThat(m.getHeaders()).containsEntry(HttpHeaders.STATUS_CODE, HttpStatus.OK)) + .expectNoEvent(Duration.ofMillis(100)) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } + @Test void testReactiveErrorOneWay() { ClientHttpConnector httpConnector = diff --git a/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/IntegrationWebSocketContainer.java b/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/IntegrationWebSocketContainer.java index 1fad3076e8e..3feeb9c7231 100644 --- a/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/IntegrationWebSocketContainer.java +++ b/spring-integration-websocket/src/main/java/org/springframework/integration/websocket/IntegrationWebSocketContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; @@ -124,7 +125,7 @@ public void setSupportedProtocols(String... protocols) { public void addSupportedProtocols(String... protocols) { for (String protocol : protocols) { - this.supportedProtocols.add(protocol.toLowerCase()); + this.supportedProtocols.add(protocol.toLowerCase(Locale.ROOT)); } } diff --git a/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/dsl/ZeroMqMessageHandlerSpec.java b/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/dsl/ZeroMqMessageHandlerSpec.java index 24e14af7a77..b99fe26996c 100644 --- a/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/dsl/ZeroMqMessageHandlerSpec.java +++ b/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/dsl/ZeroMqMessageHandlerSpec.java @@ -36,6 +36,7 @@ * The {@link ReactiveMessageHandlerSpec} extension for {@link ZeroMqMessageHandler}. * * @author Artem Bilan + * @author Alessio Matricardi * * @since 5.4 */ @@ -126,6 +127,20 @@ public ZeroMqMessageHandlerSpec topic(String topic) { return this; } + /** + * Specify if the topic that {@link SocketType#PUB} socket is going to use for distributing messages into the + * subscriptions must be wrapped with an additional empty frame. + * It is ignored for all other {@link SocketType}s supported. + * This attribute is set to {@code true} by default. + * @param wrapTopic true if the topic must be wrapped with an additional empty frame. + * @return the spec + * @since 6.2.6 + */ + public ZeroMqMessageHandlerSpec wrapTopic(boolean wrapTopic) { + this.reactiveMessageHandler.wrapTopic(wrapTopic); + return this; + } + /** * Specify a {@link Function} to evaluate a topic a {@link SocketType#PUB} * is going to use for distributing messages into the diff --git a/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/dsl/ZeroMqMessageProducerSpec.java b/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/dsl/ZeroMqMessageProducerSpec.java index 9d53fad05f9..986bb4292eb 100644 --- a/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/dsl/ZeroMqMessageProducerSpec.java +++ b/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/dsl/ZeroMqMessageProducerSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ /** * @author Artem Bilan + * @author Alessio Matricardi * * @since 5.4 */ @@ -108,6 +109,20 @@ public ZeroMqMessageProducerSpec topics(String... topics) { return this; } + /** + * Specify if the topic + * that {@link SocketType#SUB} socket is going to receive is wrapped with an additional empty frame. + * It is ignored for all other {@link SocketType}s supported. + * This attribute is set to {@code true} by default. + * @param unwrapTopic true if the received topic is wrapped with an additional empty frame. + * @return the spec + * @since 6.2.6 + */ + public ZeroMqMessageProducerSpec unwrapTopic(boolean unwrapTopic) { + this.target.unwrapTopic(unwrapTopic); + return this; + } + /** * Configure an URL for {@link org.zeromq.ZMQ.Socket#connect(String)}. * @param connectUrl the URL to connect ZeroMq socket to. diff --git a/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/inbound/ZeroMqMessageProducer.java b/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/inbound/ZeroMqMessageProducer.java index 0f0d3c02850..d2c484c3e19 100644 --- a/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/inbound/ZeroMqMessageProducer.java +++ b/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/inbound/ZeroMqMessageProducer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.zeromq.SocketType; import org.zeromq.ZContext; +import org.zeromq.ZFrame; import org.zeromq.ZMQ; import org.zeromq.ZMsg; import reactor.core.publisher.Flux; @@ -54,6 +55,7 @@ * When the {@link SocketType#SUB} is used, the received topic is stored in the {@link ZeroMqHeaders#TOPIC}. * * @author Artem Bilan + * @author Alessio Matricardi * * @since 5.4 */ @@ -90,6 +92,8 @@ public class ZeroMqMessageProducer extends MessageProducerSupport { private volatile Mono socketMono; + private volatile boolean unwrapTopic = true; + public ZeroMqMessageProducer(ZContext context) { this(context, SocketType.PAIR); } @@ -189,6 +193,18 @@ public int getBoundPort() { return this.bindPort.get(); } + /** + * Specify if the topic + * that {@link SocketType#SUB} socket is going to receive is wrapped with an additional empty frame. + * It is ignored for all other {@link SocketType}s supported. + * This attribute is set to {@code true} by default. + * @param unwrapTopic true if the received topic is wrapped with an additional empty frame. + * @since 6.2.6 + */ + public void unwrapTopic(boolean unwrapTopic) { + this.unwrapTopic = unwrapTopic; + } + @Override public String getComponentType() { return "zeromq:inbound-channel-adapter"; @@ -284,7 +300,8 @@ private Mono> convertMessage(Mono msgMono) { return msgMono.map((msg) -> { Map headers = null; if (msg.size() > 1) { - headers = Collections.singletonMap(ZeroMqHeaders.TOPIC, msg.unwrap().getString(ZMQ.CHARSET)); + ZFrame topicFrame = this.unwrapTopic ? msg.unwrap() : msg.pop(); + headers = Collections.singletonMap(ZeroMqHeaders.TOPIC, topicFrame.getString(ZMQ.CHARSET)); } return this.messageMapper.toMessage(msg.getLast().getData(), headers); // NOSONAR }); diff --git a/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/outbound/ZeroMqMessageHandler.java b/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/outbound/ZeroMqMessageHandler.java index df6ac71d04a..2a668148318 100644 --- a/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/outbound/ZeroMqMessageHandler.java +++ b/spring-integration-zeromq/src/main/java/org/springframework/integration/zeromq/outbound/ZeroMqMessageHandler.java @@ -60,6 +60,7 @@ * the {@link ZMsg} is sent into a socket as is and it is not destroyed for possible further reusing. * * @author Artem Bilan + * @author Alessio Matricardi * * @since 5.4 */ @@ -88,6 +89,8 @@ public class ZeroMqMessageHandler extends AbstractReactiveMessageHandler private volatile Disposable socketMonoSubscriber; + private volatile boolean wrapTopic = true; + /** * Create an instance based on the provided {@link ZContext} and connection string. * @param context the {@link ZContext} to use for creating sockets. @@ -191,6 +194,18 @@ public void setTopicExpression(Expression topicExpression) { this.topicExpression = topicExpression; } + /** + * Specify if the topic that {@link SocketType#PUB} socket is going to use for distributing messages into the + * subscriptions must be wrapped with an additional empty frame. + * It is ignored for all other {@link SocketType}s supported. + * This attribute is set to {@code true} by default. + * @param wrapTopic true if the topic must be wrapped with an additional empty frame. + * @since 6.2.6 + */ + public void wrapTopic(boolean wrapTopic) { + this.wrapTopic = wrapTopic; + } + @Override public String getComponentType() { return "zeromq:outbound-channel-adapter"; @@ -244,7 +259,13 @@ protected Mono handleMessageInternal(Message message) { if (socket.base() instanceof Pub) { String topic = this.topicExpression.getValue(this.evaluationContext, message, String.class); if (topic != null) { - msg.wrap(new ZFrame(topic)); + ZFrame topicFrame = new ZFrame(topic); + if (this.wrapTopic) { + msg.wrap(topicFrame); + } + else { + msg.push(topicFrame); + } } } } diff --git a/spring-integration-zeromq/src/test/java/org/springframework/integration/zeromq/inbound/ZeroMqMessageProducerTests.java b/spring-integration-zeromq/src/test/java/org/springframework/integration/zeromq/inbound/ZeroMqMessageProducerTests.java index c80c99c230c..0847f8b751e 100644 --- a/spring-integration-zeromq/src/test/java/org/springframework/integration/zeromq/inbound/ZeroMqMessageProducerTests.java +++ b/spring-integration-zeromq/src/test/java/org/springframework/integration/zeromq/inbound/ZeroMqMessageProducerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.integration.channel.FluxMessageChannel; import org.springframework.integration.test.util.TestUtils; +import org.springframework.integration.zeromq.ZeroMqHeaders; import org.springframework.messaging.support.GenericMessage; import static org.assertj.core.api.Assertions.assertThat; @@ -40,6 +41,7 @@ /** * @author Artem Bilan + * @author Alessio Matricardi * * @since 5.4 */ @@ -88,7 +90,7 @@ void testMessageProducerForPair() { stepVerifier.verify(); - messageProducer.destroy(); + messageProducer.stop(); socket.close(); } @@ -142,7 +144,42 @@ void testMessageProducerForPubSubReceiveRaw() { stepVerifier.verify(Duration.ofSeconds(10)); - messageProducer.destroy(); + messageProducer.stop(); + socket.close(); + } + + @Test + void testMessageProducerForPubSubDisabledWrapTopic() { + String socketAddress = "inproc://messageProducerWrapTopic.test"; + ZMQ.Socket socket = CONTEXT.createSocket(SocketType.XPUB); + socket.bind(socketAddress); + + FluxMessageChannel outputChannel = new FluxMessageChannel(); + + StepVerifier stepVerifier = + StepVerifier.create(outputChannel) + .assertNext((message) -> assertThat(message.getHeaders()).containsEntry(ZeroMqHeaders.TOPIC, "testTopicWithNonWrappedTopic")) + .thenCancel() + .verifyLater(); + + ZeroMqMessageProducer messageProducer = new ZeroMqMessageProducer(CONTEXT, SocketType.SUB); + messageProducer.setOutputChannel(outputChannel); + messageProducer.setTopics("test"); + messageProducer.setConnectUrl(socketAddress); + messageProducer.setBeanFactory(mock(BeanFactory.class)); + messageProducer.unwrapTopic(false); + messageProducer.afterPropertiesSet(); + messageProducer.start(); + + assertThat(socket.recv()).isNotNull(); + + ZMsg msg = ZMsg.newStringMsg("test"); + msg.push("testTopicWithNonWrappedTopic"); + msg.send(socket); + + stepVerifier.verify(); + + messageProducer.stop(); socket.close(); } diff --git a/spring-integration-zeromq/src/test/java/org/springframework/integration/zeromq/outbound/ZeroMqMessageHandlerTests.java b/spring-integration-zeromq/src/test/java/org/springframework/integration/zeromq/outbound/ZeroMqMessageHandlerTests.java index 0d491ae487c..1431047c437 100644 --- a/spring-integration-zeromq/src/test/java/org/springframework/integration/zeromq/outbound/ZeroMqMessageHandlerTests.java +++ b/spring-integration-zeromq/src/test/java/org/springframework/integration/zeromq/outbound/ZeroMqMessageHandlerTests.java @@ -42,6 +42,7 @@ /** * @author Artem Bilan + * @author Alessio Matricardi * * @since 5.4 */ @@ -150,4 +151,40 @@ void testMessageHandlerForPushPullOverProxy() { proxy.destroy(); } + @Test + void testMessageHandlerForPubSubDisabledWrapTopic() { + ZMQ.Socket subSocket = CONTEXT.createSocket(SocketType.SUB); + subSocket.setReceiveTimeOut(0); + int port = subSocket.bindToRandomPort("tcp://*"); + subSocket.subscribe("test"); + + ZeroMqMessageHandler messageHandler = + new ZeroMqMessageHandler(CONTEXT, "tcp://localhost:" + port, SocketType.PUB); + messageHandler.setBeanFactory(mock(BeanFactory.class)); + messageHandler.setTopicExpression( + new FunctionExpression>((message) -> message.getHeaders().get("topic"))); + messageHandler.setMessageMapper(new EmbeddedJsonHeadersMessageMapper()); + messageHandler.wrapTopic(false); + messageHandler.afterPropertiesSet(); + messageHandler.start(); + + Message testMessage = MessageBuilder.withPayload("test").setHeader("topic", "testTopic").build(); + + await().atMost(Duration.ofSeconds(20)).pollDelay(Duration.ofMillis(100)) + .untilAsserted(() -> { + subSocket.subscribe("test"); + messageHandler.handleMessage(testMessage).subscribe(); + ZMsg msg = ZMsg.recvMsg(subSocket); + assertThat(msg).isNotNull(); + assertThat(msg.pop().getString(ZMQ.CHARSET)).isEqualTo("testTopic"); + Message capturedMessage = + new EmbeddedJsonHeadersMessageMapper().toMessage(msg.getFirst().getData()); + assertThat(capturedMessage).isEqualTo(testMessage); + msg.destroy(); + }); + + messageHandler.destroy(); + subSocket.close(); + } + } diff --git a/spring-integration-zookeeper/src/test/java/org/springframework/integration/zookeeper/lock/ZkLockRegistryTests.java b/spring-integration-zookeeper/src/test/java/org/springframework/integration/zookeeper/lock/ZkLockRegistryTests.java index c4cbec63cf4..e91590632ca 100644 --- a/spring-integration-zookeeper/src/test/java/org/springframework/integration/zookeeper/lock/ZkLockRegistryTests.java +++ b/spring-integration-zookeeper/src/test/java/org/springframework/integration/zookeeper/lock/ZkLockRegistryTests.java @@ -146,7 +146,8 @@ public void testTwoThreadsSecondFailsToGetLock() throws Exception { lock1.lockInterruptibly(); final AtomicBoolean locked = new AtomicBoolean(); final CountDownLatch latch = new CountDownLatch(1); - Future result = Executors.newSingleThreadExecutor().submit(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future result = executorService.submit(() -> { Lock lock2 = registry.obtain("foo"); locked.set(lock2.tryLock(200, TimeUnit.MILLISECONDS)); latch.countDown(); @@ -165,6 +166,7 @@ public void testTwoThreadsSecondFailsToGetLock() throws Exception { assertThat(ise).isInstanceOf(IllegalMonitorStateException.class); assertThat(((Exception) ise).getMessage()).contains("You do not own"); registry.destroy(); + executorService.shutdown(); } @Test @@ -176,7 +178,8 @@ public void testTwoThreads() throws Exception { final CountDownLatch latch2 = new CountDownLatch(1); final CountDownLatch latch3 = new CountDownLatch(1); lock1.lockInterruptibly(); - Executors.newSingleThreadExecutor().execute(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { Lock lock2 = registry.obtain("foo"); try { latch1.countDown(); @@ -199,6 +202,7 @@ public void testTwoThreads() throws Exception { assertThat(latch3.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(locked.get()).isTrue(); registry.destroy(); + executorService.shutdown(); } @Test @@ -211,7 +215,8 @@ public void testTwoThreadsDifferentRegistries() throws Exception { final CountDownLatch latch2 = new CountDownLatch(1); final CountDownLatch latch3 = new CountDownLatch(1); lock1.lockInterruptibly(); - Executors.newSingleThreadExecutor().execute(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(() -> { Lock lock2 = registry2.obtain("foo"); try { latch1.countDown(); @@ -235,6 +240,7 @@ public void testTwoThreadsDifferentRegistries() throws Exception { assertThat(locked.get()).isTrue(); registry1.destroy(); registry2.destroy(); + executorService.shutdown(); } @Test @@ -244,7 +250,8 @@ public void testTwoThreadsWrongOneUnlocks() throws Exception { lock.lockInterruptibly(); final AtomicBoolean locked = new AtomicBoolean(); final CountDownLatch latch = new CountDownLatch(1); - Future result = Executors.newSingleThreadExecutor().submit(() -> { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future result = executorService.submit(() -> { try { lock.unlock(); } @@ -261,6 +268,7 @@ public void testTwoThreadsWrongOneUnlocks() throws Exception { assertThat(imse).isInstanceOf(IllegalMonitorStateException.class); assertThat(((Exception) imse).getMessage()).contains("You do not own"); registry.destroy(); + executorService.shutdown(); } @Test diff --git a/src/reference/antora/modules/ROOT/pages/amqp.adoc b/src/reference/antora/modules/ROOT/pages/amqp.adoc index b6725a17068..827f4952aef 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp.adoc @@ -32,7 +32,7 @@ The following adapters are available: * xref:amqp/inbound-gateway.adoc[Inbound Gateway] * xref:amqp/outbound-channel-adapter.adoc[Outbound Channel Adapter] * xref:amqp/outbound-gateway.adoc[Outbound Gateway] -* xref:amqp-async-outbound-gateway[Async Outbound Gateway] +* xref:amqp/async-outbound-gateway.adoc[Async Outbound Gateway] * xref:amqp/rmq-streams.adoc#rmq-stream-inbound-channel-adapter[RabbitMQ Stream Queue Inbound Channel Adapter] * xref:amqp/rmq-streams.adoc#rmq-stream-outbound-channel-adapter[RabbitMQ Stream Queue Outbound Channel Adapter] diff --git a/src/reference/antora/modules/ROOT/pages/amqp/inbound-channel-adapter.adoc b/src/reference/antora/modules/ROOT/pages/amqp/inbound-channel-adapter.adoc index e8ae8d91de3..906bf071fa4 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/inbound-channel-adapter.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/inbound-channel-adapter.adoc @@ -87,7 +87,7 @@ XML:: task-executor="" <22> transaction-attribute="" <23> transaction-manager="" <24> - tx-size="" <25> + batch-size="" <25> consumers-per-queue <26> batch-mode="MESSAGES"/> <27> @@ -146,7 +146,7 @@ By default, this value is `Integer.MAX_VALUE`, meaning that this container start Optional. <17> Tells the AMQP broker how many messages to send to each consumer in a single request. Often, you can set this value high to improve throughput. -It should be greater than or equal to the transaction size (see the `tx-size` attribute, later in this list). +It should be greater than or equal to the transaction size (see the `batch-size` attribute, later in this list). Optional (defaults to `1`). <18> Receive timeout in milliseconds. Optional (defaults to `1000`). @@ -173,7 +173,7 @@ If the `channelTransacted` flag is `false`, no transaction semantics apply to th For further information, see https://docs.spring.io/spring-amqp/reference/html/%255Freference.html#%5Ftransactions[Transactions with Spring AMQP]. Optional. -<25> Tells the `SimpleMessageListenerContainer` how many messages to process in a single transaction (if the channel is transactional). +<25> Tells the `SimpleMessageListenerContainer` how many messages to process in a single request. For best results, it should be less than or equal to the value set in `prefetch-count`. Not allowed when 'consumers-per-queue' is set. Optional (defaults to `1`). diff --git a/src/reference/antora/modules/ROOT/pages/amqp/inbound-gateway.adoc b/src/reference/antora/modules/ROOT/pages/amqp/inbound-gateway.adoc index 7fcdf9617e4..cc2762a88ed 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/inbound-gateway.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/inbound-gateway.adoc @@ -10,7 +10,7 @@ Java DSL:: + [source, java, role="primary"] ---- -@Bean // return the upper cased payload +@Bean // return the upper-cased payload public IntegrationFlow amqpInboundGateway(ConnectionFactory connectionFactory) { return IntegrationFlow.from(Amqp.inboundGateway(connectionFactory, "foo")) .transform(String.class, String::toUpperCase) diff --git a/src/reference/antora/modules/ROOT/pages/barrier.adoc b/src/reference/antora/modules/ROOT/pages/barrier.adoc index 95ce680881d..f0fb5e1731a 100644 --- a/src/reference/antora/modules/ROOT/pages/barrier.adoc +++ b/src/reference/antora/modules/ROOT/pages/barrier.adoc @@ -75,5 +75,6 @@ XML:: Depending on which one has a message arrive first, either the thread sending a message to `in` or the thread sending a message to `release` waits for up to ten seconds until the other message arrives. When the message is released, the `out` channel is sent a message that combines the result of invoking the custom `MessageGroupProcessor` bean, named `myOutputProcessor`. If the main thread times out and a trigger arrives later, you can configure a discard channel to which the late trigger is sent. +The trigger message is also discarded if request message does not arrive in time. For an example of this component, see the https://github.com/spring-projects/spring-integration-samples/tree/main/basic/barrier[barrier sample application]. diff --git a/src/reference/antora/modules/ROOT/pages/configuration/global-properties.adoc b/src/reference/antora/modules/ROOT/pages/configuration/global-properties.adoc index acae7aa00a9..b993b28fd53 100644 --- a/src/reference/antora/modules/ROOT/pages/configuration/global-properties.adoc +++ b/src/reference/antora/modules/ROOT/pages/configuration/global-properties.adoc @@ -76,6 +76,6 @@ spring.integration.channels.autoCreate=true spring.integration.channels.maxBroadcastSubscribers=0x7fffffff spring.integration.readOnly.headers= spring.integration.messagingTemplate.throwExceptionOnLateReply=true -spring.integration.endpoints.defaultTimeout=-1 +spring.integration.endpoints.defaultTimeout=30000 ---- diff --git a/src/reference/antora/modules/ROOT/pages/hazelcast.adoc b/src/reference/antora/modules/ROOT/pages/hazelcast.adoc index 9d7c3449fcc..2736d81c322 100644 --- a/src/reference/antora/modules/ROOT/pages/hazelcast.adoc +++ b/src/reference/antora/modules/ROOT/pages/hazelcast.adoc @@ -604,7 +604,7 @@ The xref:channel/implementations.adoc#channel-implementations-queuechannel[`Queu ---- @Bean PollableChannel hazelcastQueueChannel(HazelcastInstance hazelcastInstance) { - return new QueueChannel(hazelcastInstance.Message>getQueue("springIntegrationQueue")); + return new QueueChannel(hazelcastInstance.getQueue("springIntegrationQueue")); } ---- diff --git a/src/reference/antora/modules/ROOT/pages/testing.adoc b/src/reference/antora/modules/ROOT/pages/testing.adoc index 4e6aae22da5..74017affd61 100644 --- a/src/reference/antora/modules/ROOT/pages/testing.adoc +++ b/src/reference/antora/modules/ROOT/pages/testing.adoc @@ -206,6 +206,8 @@ The endpoints are matched to the provided patterns, which support the following This is useful when we would like to not have real connections to the target systems from inbound channel adapters (for example an AMQP Inbound Gateway, JDBC Polling Channel Adapter, WebSocket Message Producer in client mode, and so on). +The `@SpringIntegrationTest` honors the `org.springframework.test.context.NestedTestConfiguration` semantics, hence it can be declared on the outer class (or even its super class) - and `@SpringIntegrationTest` environment will be available to inherited `@Nested` tests. + The `MockIntegrationContext` is meant to be used in the target test cases for modifications to beans in the real application context. For example, endpoints that have `autoStartup` overridden to `false` can be replaced with mocks, as the following example shows: diff --git a/src/reference/antora/modules/ROOT/pages/zeromq.adoc b/src/reference/antora/modules/ROOT/pages/zeromq.adoc index be9140b1f75..a0b85efdfee 100644 --- a/src/reference/antora/modules/ROOT/pages/zeromq.adoc +++ b/src/reference/antora/modules/ROOT/pages/zeromq.adoc @@ -119,6 +119,9 @@ If the `receiveRaw` option is set to `true`, a `ZMsg`, consumed from the socket, Otherwise, an `InboundMessageMapper` is used to convert the consumed data into a `Message`. If the received `ZMsg` is multi-frame, the first frame is treated as the `ZeroMqHeaders.TOPIC` header this ZeroMQ message was published to. +If the `unwrapTopic` option is set to `false`, the incoming message is considered to consist of two frames: the topic and the ZeroMQ message. +Otherwise, by default, the `ZMsg` is considered to consist of three frames: the first one containing the topic, the last frame containing the message, with an empty frame in the middle. + With `SocketType.SUB`, the `ZeroMqMessageProducer` uses the provided `topics` option for subscriptions; defaults to subscribe to all. Subscriptions can be adjusted at runtime using `subscribeToTopics()` and `unsubscribeFromTopics()` `@ManagedOperation` s. @@ -146,6 +149,10 @@ Only `SocketType.PAIR`, `SocketType.PUSH` and `SocketType.PUB` are supported. The `ZeroMqMessageHandler` only supports connecting the ZeroMQ socket; binding is not supported. When the `SocketType.PUB` is used, the `topicExpression` is evaluated against a request message to inject a topic frame into a ZeroMQ message if it is not null. The subscriber side (`SocketType.SUB`) must receive the topic frame first before parsing the actual data. + +If the `wrapTopic` option is set to `false`, the ZeroMQ message frame is sent after the injected topic, if present. +By default, an additional empty frame is sent between the topic and the message. + When the payload of the request message is a `ZMsg`, no conversion or topic extraction is performed: the `ZMsg` is sent into a socket as is and it is not destroyed for possible further reuse. Otherwise, an `OutboundMessageMapper` is used to convert a request message (or just its payload) into a ZeroMQ frame to publish. By default, a `ConvertingBytesMessageMapper` is used supplied with a `ConfigurableCompositeMessageConverter`.