diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9bbe8e593c2..bab0e69d15f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -39,3 +39,15 @@ updates: - "org.apache.tomcat.embed:tomcat-embed-websocket" - "org.xmlunit:xmlunit-assertj3" - "com.thoughtworks.xstream:xstream" + - "org.springframework.security*" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "saturday" + labels: ["type: task"] + groups: + development-dependencies: + patterns: + - "*" \ No newline at end of file diff --git a/.github/workflows/auto-cherry-pick.yml b/.github/workflows/auto-cherry-pick.yml new file mode 100644 index 00000000000..6ba14dde29d --- /dev/null +++ b/.github/workflows/auto-cherry-pick.yml @@ -0,0 +1,13 @@ +name: Auto Cherry-Pick + +on: + push: + branches: + - main + - '*.x' + +jobs: + cherry-pick-commit: + 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 c32d9bf190e..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@v1 + 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 f5535448724..6bb7b785fb4 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -2,16 +2,25 @@ name: CI SNAPSHOT on: workflow_dispatch: + push: branches: - main - '*.x' + schedule: + - cron: '0 5 * * *' + +concurrency: + group: group-snapshot-for-${{ github.ref }} + cancel-in-progress: true + jobs: build-snapshot: - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@v1 + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@v5 + with: + gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} secrets: - GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} \ No newline at end of file + 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 2c223d15822..2065ee71873 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,32 +1,19 @@ name: Deploy Docs + on: push: - branches-ignore: [ gh-pages ] - tags: '**' - repository_dispatch: - types: request-build-reference # legacy - #schedule: - #- cron: '0 10 * * *' # Once per day at 10am UTC + branches: + - '*.x' + - main + tags: + - '**' + workflow_dispatch: + permissions: actions: write + jobs: - build: - runs-on: ubuntu-latest + dispatch-docs-build: if: github.repository_owner == 'spring-projects' - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: docs-build - fetch-depth: 1 - - name: Dispatch (partial build) - if: github.ref_type == 'branch' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }} - - name: Dispatch (full build) - if: github.ref_type == 'tag' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) + 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 7dfa29e2e06..f513c72567a 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - '*.x' run-name: Merge Dependabot PR ${{ github.ref_name }} @@ -11,4 +12,7 @@ jobs: merge-dependabot-pr: permissions: write-all - uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v1 \ No newline at end of file + uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v5 + with: + mergeArguments: --auto --squash + autoMergeSnapshots: true \ No newline at end of file diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 945b2f94d92..c9f71e8bdf1 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -4,7 +4,8 @@ on: pull_request: branches: - main + - '*.x' jobs: build-pull-request: - uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v1 + uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa2cfbcdd9f..be8a9e40c67 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,17 +12,16 @@ jobs: contents: write issues: write - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v1 + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v5 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - 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 }} - OSSRH_URL: ${{ secrets.OSSRH_URL }} + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} 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 432e2876da3..eceb0143c7e 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -9,9 +9,7 @@ on: type: string env: - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - GRADLE_ENTERPRISE_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/build.gradle b/build.gradle index a18d90b0926..4bf24cf5a71 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.9.10' + ext.kotlinVersion = '1.9.25' ext.isCI = System.getenv('GITHUB_ACTION') repositories { gradlePluginPortal() @@ -17,11 +17,13 @@ buildscript { plugins { id 'base' id 'io.spring.nohttp' version '0.0.11' apply false - id 'org.ajoberstar.grgit' version '5.2.1' - id 'io.spring.dependency-management' version '1.1.4' - id 'org.jetbrains.dokka' version "$kotlinVersion" + id 'org.ajoberstar.grgit' version '5.2.2' + id 'io.spring.dependency-management' version '1.1.6' + 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.26' + id 'io.freefair.aggregate-javadoc' version '8.6' } if (isCI) { @@ -47,17 +49,17 @@ ext { files() .from { files(grgit.status().unstaged.modified) - .filter { f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } + .filter { f -> f.name.endsWith('.java') || f.name.endsWith('.kt') || f.name.endsWith('.groovy') } } modifiedFiles.finalizeValueOnRead() apacheSshdVersion = '2.11.0' artemisVersion = '2.31.2' - aspectjVersion = '1.9.21' + aspectjVersion = '1.9.22.1' assertjVersion = '3.24.2' - assertkVersion = '0.28.0' - avroVersion = '1.11.3' - awaitilityVersion = '4.2.0' + assertkVersion = '0.28.1' + avroVersion = '1.11.4' + awaitilityVersion = '4.2.2' camelVersion = '4.2.0' commonsDbcp2Version = '2.11.0' commonsIoVersion = '2.15.1' @@ -65,60 +67,59 @@ ext { curatorVersion = '5.5.0' debeziumVersion = '2.4.2.Final' derbyVersion = '10.16.1.1' - findbugsVersion = '3.0.1' ftpServerVersion = '1.2.0' - graalvmVersion = '23.0.2' - greenmailVersion = '2.1.0-alpha-3' - groovyVersion = '4.0.16' + graalvmVersion = '23.0.6' + greenmailVersion = '2.1.1' + groovyVersion = '4.0.24' hamcrestVersion = '2.2' - hazelcastVersion = '5.3.6' - hibernateVersion = '6.4.1.Final' - hsqldbVersion = '2.7.2' + hazelcastVersion = '5.3.8' + hibernateVersion = '6.4.10.Final' + hsqldbVersion = '2.7.4' h2Version = '2.2.224' - jacksonVersion = '2.15.3' - jaxbVersion = '4.0.4' - jcifsVersion = '2.1.37' + jacksonVersion = '2.15.4' + jaxbVersion = '4.0.5' + jcifsVersion = '2.1.39' jeroMqVersion = '0.5.4' jmsApiVersion = '3.1.0' jpaApiVersion = '3.1.0' - jrubyVersion = '9.4.5.0' + jrubyVersion = '9.4.9.0' jsonpathVersion = '2.8.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.10.1' - jythonVersion = '2.7.3' + junitJupiterVersion = '5.10.5' + jythonVersion = '2.7.4' kotlinCoroutinesVersion = '1.7.3' kryoVersion = '5.5.0' - lettuceVersion = '6.3.0.RELEASE' + lettuceVersion = '6.3.2.RELEASE' log4jVersion = '2.21.1' - mailVersion = '2.0.2' - micrometerTracingVersion = '1.2.1' - micrometerVersion = '1.12.1' + mailVersion = '2.0.3' + micrometerTracingVersion = '1.2.12' + micrometerVersion = '1.12.13' mockitoVersion = '5.7.0' - mongoDriverVersion = '4.11.1' + mongoDriverVersion = '4.11.5' mysqlVersion = '8.0.33' oracleVersion = '23.3.0.23.09' pahoMqttClientVersion = '1.2.5' - postgresVersion = '42.6.0' - protobufVersion = '3.25.1' + postgresVersion = '42.6.2' + protobufVersion = '3.25.5' r2dbch2Version = '1.0.0.RELEASE' - reactorVersion = '2023.0.1' + reactorVersion = '2023.0.12' resilience4jVersion = '2.1.0' romeToolsVersion = '2.1.0' rsocketVersion = '1.1.4' servletApiVersion = '6.0.0' - smackVersion = '4.4.7' - springAmqpVersion = '3.1.1' - springDataVersion = '2023.1.1' - springGraphqlVersion = '1.2.4' - springKafkaVersion = '3.1.1' - springRetryVersion = '2.0.5' - springSecurityVersion = '6.2.1' - springVersion = '6.1.2' - springWsVersion = '4.0.9' - testcontainersVersion = '1.19.3' - tomcatVersion = '10.1.17' + smackVersion = '4.4.8' + springAmqpVersion = '3.1.8' + springDataVersion = '2023.1.12' + springGraphqlVersion = '1.2.9' + springKafkaVersion = '3.1.10' + springRetryVersion = '2.0.10' + springSecurityVersion = '6.2.8' + springVersion = '6.1.15' + springWsVersion = '4.0.11' + testcontainersVersion = '1.19.8' + tomcatVersion = '10.1.33' xmlUnitVersion = '2.9.1' - xstreamVersion = '1.4.20' + xstreamVersion = '1.4.21' ztZipVersion = '1.16' javaProjects = subprojects - project(':spring-integration-bom') @@ -194,6 +195,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 { @@ -208,9 +229,6 @@ configure(javaProjects) { subproject -> registerFeature('optional') { usingSourceSet(sourceSets.main) } - registerFeature('provided') { - usingSourceSet(sourceSets.main) - } } compileJava { @@ -246,6 +264,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' @@ -256,9 +278,10 @@ configure(javaProjects) { subproject -> api project(':spring-integration-core') } - // JSR-305 only used for non-required meta-annotations - compileOnly "com.google.code.findbugs:annotations:$findbugsVersion" - testImplementation "com.google.code.findbugs:annotations:$findbugsVersion" + def spotbugsAnnotations = "com.github.spotbugs:spotbugs-annotations:${spotbugs.toolVersion.get()}" + compileOnly spotbugsAnnotations + testCompileOnly spotbugsAnnotations + testImplementation("org.awaitility:awaitility:$awaitilityVersion") { exclude group: 'org.hamcrest' } @@ -306,7 +329,7 @@ configure(javaProjects) { subproject -> def beginningYear = matcher[0][1] if (now != beginningYear && now != matcher[0][2]) { def years = "$beginningYear-$now" - def sourceCode = file.text + def sourceCode = file.getText('UTF-8') sourceCode = sourceCode.replaceFirst(/20\d\d(-20\d\d)?/, years) file.text = sourceCode println "Copyright updated for file: $file" @@ -321,11 +344,6 @@ configure(javaProjects) { subproject -> compileKotlin.dependsOn updateCopyrights - test { - maxHeapSize = '2g' - jvmArgs '-XX:+HeapDumpOnOutOfMemoryError' - } - tasks.register('testAll', Test) { dependsOn['check'] } gradle.taskGraph.whenReady { graph -> @@ -334,7 +352,7 @@ configure(javaProjects) { subproject -> } } - tasks.withType(Test).all { + tasks.withType(Test).configureEach { // suppress all console output during testing unless running `gradle -i` logging.captureStandardOutput(LogLevel.INFO) @@ -346,6 +364,8 @@ configure(javaProjects) { subproject -> useJUnitPlatform() + reports.junitXml.required = false + enableAssertions = false } @@ -418,14 +438,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' } } } @@ -534,14 +548,14 @@ 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')) classpath.from(sourceSets['main'].runtimeClasspath) externalDocumentationLink { url.set(new URL("https://docs.spring.io/spring-integration/docs/$version/api/")) - packageListUrl.set(file("$buildDir/docs/javadoc/element-list").toURI().toURL()) + packageListUrl.set(file('build/docs/javadoc/element-list').toURI().toURL()) } externalDocumentationLink { url.set(new URL('https://projectreactor.io/docs/core/release/api/')) @@ -661,7 +675,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' @@ -720,7 +734,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' @@ -766,7 +780,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" @@ -832,8 +846,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.js:js:$graalvmVersion" + provided "org.graalvm.sdk:graal-sdk:$graalvmVersion" + provided "org.graalvm.js:js:$graalvmVersion" testImplementation "org.jruby:jruby-complete:$jrubyVersion" testImplementation 'org.apache.groovy:groovy-jsr223' @@ -960,7 +974,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" @@ -985,7 +999,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') { @@ -1081,15 +1095,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 @@ -1097,13 +1115,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 { @@ -1160,7 +1179,7 @@ tasks.register('docsZip', Zip) { include 'changelog.txt' } - from(api) { + from(javadoc) { into 'api' } diff --git a/gradle.properties b/gradle.properties index f0b1227b89f..178ea5855d1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.2.2-SNAPSHOT +version=6.2.12-SNAPSHOT org.gradle.jvmargs=-Xmx1536M -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.properties b/gradle/wrapper/gradle-wrapper.properties index db8c3baafe3..4baf5a11d45 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=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index 275cc4c81ec..e017200c2fb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,12 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + plugins { - id 'com.gradle.enterprise' version '3.15.1' - id 'io.spring.ge.conventions' version '0.0.14' + id 'io.spring.develocity.conventions' version '0.0.22' } rootProject.name = 'spring-integration' diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/channel/ChannelTests.java b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/channel/ChannelTests.java index 53629cc6741..0bd5c843a86 100644 --- a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/channel/ChannelTests.java +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/channel/ChannelTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2023 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.Set; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; import org.junit.After; import org.junit.ClassRule; @@ -134,19 +135,23 @@ public void pubSubLostConnectionTest() throws Exception { private void waitForNewConsumer(PublishSubscribeAmqpChannel channel, BlockingQueueConsumer consumer) throws Exception { - final Object consumersMonitor = TestUtils.getPropertyValue(channel, "container.consumersMonitor"); + Lock consumersLock = TestUtils.getPropertyValue(channel, "container.consumersLock", Lock.class); int n = 0; while (n++ < 100) { Set consumers = TestUtils .getPropertyValue(channel, "container.consumers", Set.class); - synchronized (consumersMonitor) { + consumersLock.lock(); + try { if (!consumers.isEmpty()) { BlockingQueueConsumer newConsumer = consumers.iterator().next(); - if (newConsumer != consumer && newConsumer.getConsumerTags().size() > 0) { + if (newConsumer != consumer && !newConsumer.getConsumerTags().isEmpty()) { break; } } } + finally { + consumersLock.unlock(); + } Thread.sleep(100); } assertThat(n < 100).as("Failed to restart consumer").isTrue(); 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 34d80b52d59..7be166ca12e 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-2023 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. @@ -28,13 +28,13 @@ 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.core.log.LogMessage; import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.integration.StaticMessageHeaderAccessor; -import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.support.MutableMessageBuilder; +import org.springframework.integration.util.IntegrationReactiveUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageDeliveryException; import org.springframework.util.Assert; @@ -52,12 +52,8 @@ public class FluxMessageChannel extends AbstractMessageChannel implements Publisher>, ReactiveStreamsSubscribableChannel { - private final Scheduler scheduler = Schedulers.boundedElastic(); - private final Sinks.Many> sink = Sinks.many().multicast().onBackpressureBuffer(1, false); - private final Sinks.Many subscribedSignal = Sinks.many().replay().limit(1); - private final Disposable.Composite upstreamSubscriptions = Disposables.composite(); private volatile boolean active = true; @@ -83,7 +79,14 @@ protected boolean doSend(Message message, long timeout) { } private boolean tryEmitMessage(Message message) { - return switch (this.sink.tryEmitNext(message)) { + Message messageToEmit = message; + ContextView contextView = IntegrationReactiveUtils.captureReactorContext(); + if (!contextView.isEmpty()) { + messageToEmit = MutableMessageBuilder.fromMessage(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 -> @@ -97,18 +100,9 @@ private boolean tryEmitMessage(Message message) { @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) { @@ -138,8 +132,11 @@ private void disposeUpstreamSubscription(AtomicReference disposableR public void subscribeTo(Publisher> publisher) { Flux upstreamPublisher = Flux.from(publisher) - .delaySubscription(this.subscribedSignal.asFlux().filter(Boolean::booleanValue).next()) - .publishOn(this.scheduler) + .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)) @@ -154,7 +151,7 @@ private void sendReactiveMessage(Message message) { // We have just restored Reactor context, so no need in a header anymore. if (messageToSend.getHeaders().containsKey(IntegrationMessageHeaderAccessor.REACTOR_CONTEXT)) { messageToSend = - MessageBuilder.fromMessage(message) + MutableMessageBuilder.fromMessage(message) .removeHeader(IntegrationMessageHeaderAccessor.REACTOR_CONTEXT) .build(); } @@ -174,9 +171,7 @@ 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(); super.destroy(); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/channel/interceptor/ObservationPropagationChannelInterceptor.java b/spring-integration-core/src/main/java/org/springframework/integration/channel/interceptor/ObservationPropagationChannelInterceptor.java index 543cc2aa6e6..97f32885fad 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/channel/interceptor/ObservationPropagationChannelInterceptor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/channel/interceptor/ObservationPropagationChannelInterceptor.java @@ -32,11 +32,22 @@ * implementation responsible for an {@link Observation} propagation from one message * flow's thread to another through the {@link MessageChannel}s involved in the flow. * Opens a new {@link Observation.Scope} on another thread and cleans up it in the end. + *

+ * NOTE: This interceptor is proven to be wrong since an existing observation usually is closed + * on the sender side before the message is consumed on the receiver side. + * Therefore, it is better to have a {@code sender} observation on this channel, + * and then {@code receiver} observation on a subscriber for this channel. + * This way a tracing information is stored into message headers passing this channel. + * Such an approach also eliminate a problem with persistent message channels where + * an {@link Observation} is not serializable to be stored into database as a part of the message. * * @author Artem Bilan * * @since 6.0 + * + * @deprecated since 6.1.7 for removal in 6.4 in favor of enabling observation on the channel and its consumer. */ +@Deprecated(since = "6.1.7", forRemoval = true) public class ObservationPropagationChannelInterceptor extends ThreadStatePropagationChannelInterceptor { private final ThreadLocal scopes = new ThreadLocal<>(); 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 19408c21249..21cbeecbe1a 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; @@ -105,14 +105,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/DefaultConfiguringBeanFactoryPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.java index e5782bb7e7b..96bd94dc24e 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/DefaultConfiguringBeanFactoryPostProcessor.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. @@ -33,6 +33,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.SmartLifecycle; import org.springframework.core.Ordered; import org.springframework.core.log.LogAccessor; import org.springframework.integration.channel.ChannelUtils; @@ -276,6 +277,7 @@ private void registerTaskScheduler() { IntegrationProperties.TASK_SCHEDULER_POOL_SIZE)) .addPropertyValue("threadNamePrefix", "task-scheduler-") .addPropertyValue("rejectedExecutionHandler", new CallerRunsPolicy()) + .addPropertyValue("phase", SmartLifecycle.DEFAULT_PHASE / 2) .addPropertyReference("errorHandler", ChannelUtils.MESSAGE_PUBLISHING_ERROR_HANDLER_BEAN_NAME) .getBeanDefinition(); diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/GatewayProxyInstantiationPostProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/GatewayProxyInstantiationPostProcessor.java index d5e15aa9f7d..4566265a9bc 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/GatewayProxyInstantiationPostProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/GatewayProxyInstantiationPostProcessor.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,7 @@ package org.springframework.integration.config; +import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; @@ -44,7 +45,7 @@ * @see AnnotationGatewayProxyFactoryBean */ class GatewayProxyInstantiationPostProcessor implements - InstantiationAwareBeanPostProcessor, BeanRegistrationAotProcessor, ApplicationContextAware { + InstantiationAwareBeanPostProcessor, BeanRegistrationAotProcessor, ApplicationContextAware, AopInfrastructureBean { private final BeanDefinitionRegistry registry; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/GlobalChannelInterceptorProcessor.java b/spring-integration-core/src/main/java/org/springframework/integration/config/GlobalChannelInterceptorProcessor.java index f9cf8f748fe..747df83abf3 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/GlobalChannelInterceptorProcessor.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/GlobalChannelInterceptorProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -54,7 +55,7 @@ * @since 2.0 */ public final class GlobalChannelInterceptorProcessor - implements BeanFactoryAware, SmartInitializingSingleton, BeanPostProcessor { + implements BeanFactoryAware, SmartInitializingSingleton, BeanPostProcessor, AopInfrastructureBean { private static final Log LOGGER = LogFactory.getLog(GlobalChannelInterceptorProcessor.class); 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/BaseIntegrationFlowDefinition.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/BaseIntegrationFlowDefinition.java index c2b8ea86265..ab976321ce3 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/BaseIntegrationFlowDefinition.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/BaseIntegrationFlowDefinition.java @@ -29,6 +29,7 @@ import reactor.util.function.Tuple2; import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; @@ -3047,7 +3048,7 @@ protected static Object extractProxyTarget(@Nullable Object target) { } } - public static final class ReplyProducerCleaner implements DestructionAwareBeanPostProcessor { + public static final class ReplyProducerCleaner implements DestructionAwareBeanPostProcessor, AopInfrastructureBean { @Override public boolean requiresDestruction(Object bean) { diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/MessageProducerSpec.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/MessageProducerSpec.java index e01ac331d16..6887badb286 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/MessageProducerSpec.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/MessageProducerSpec.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. @@ -70,6 +70,20 @@ public S autoStartup(boolean autoStartup) { return _this(); } + /** + * Specify the role for the endpoint. + * Such endpoints can be started/stopped as a group. + * @param role the role for this endpoint. + * @return the endpoint spec + * @since 6.1.8 + * @see org.springframework.context.SmartLifecycle + * @see org.springframework.integration.support.SmartLifecycleRoleController + */ + public S role(String role) { + this.target.setRole(role); + return _this(); + } + /** * Specify the {@code outputChannel} for the * {@link org.springframework.integration.core.MessageProducer}. diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/PublisherIntegrationFlow.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/PublisherIntegrationFlow.java index f37a4196292..41461e3dae6 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/PublisherIntegrationFlow.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/PublisherIntegrationFlow.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 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. @@ -22,6 +22,7 @@ import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; +import org.springframework.integration.endpoint.AbstractEndpoint; import org.springframework.messaging.Message; /** @@ -48,8 +49,11 @@ class PublisherIntegrationFlow extends StandardIntegrationFlow implements Pub if (autoStartOnSubscribe) { flux = flux.doOnSubscribe((sub) -> start()); for (Object component : integrationComponents.keySet()) { - if (component instanceof EndpointSpec) { - ((EndpointSpec) component).autoStartup(false); + if (component instanceof EndpointSpec endpointSpec) { + endpointSpec.autoStartup(false); + } + else if (component instanceof AbstractEndpoint endpoint) { + endpoint.setAutoStartup(false); } } } 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 0c1cbb058d0..60354c96c37 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 @@ -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. @@ -19,6 +19,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.NameMatchMethodPointcutAdvisor; import org.springframework.beans.BeansException; @@ -90,7 +91,7 @@ * @since 5.0 */ public class IntegrationFlowBeanPostProcessor - implements BeanPostProcessor, ApplicationContextAware, SmartInitializingSingleton { + implements BeanPostProcessor, ApplicationContextAware, SmartInitializingSingleton, AopInfrastructureBean { private ConfigurableApplicationContext applicationContext; diff --git a/spring-integration-core/src/main/java/org/springframework/integration/endpoint/ReactiveStreamsConsumer.java b/spring-integration-core/src/main/java/org/springframework/integration/endpoint/ReactiveStreamsConsumer.java index e36b8ce3322..b9a8a41970c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/endpoint/ReactiveStreamsConsumer.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/endpoint/ReactiveStreamsConsumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 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. @@ -26,13 +26,17 @@ import reactor.core.Disposable; import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; +import reactor.util.context.ContextView; import org.springframework.context.Lifecycle; +import org.springframework.integration.IntegrationMessageHeaderAccessor; +import org.springframework.integration.StaticMessageHeaderAccessor; import org.springframework.integration.channel.ChannelUtils; import org.springframework.integration.channel.NullChannel; import org.springframework.integration.core.MessageProducer; import org.springframework.integration.handler.ReactiveMessageHandlerAdapter; import org.springframework.integration.router.MessageRouter; +import org.springframework.integration.support.MutableMessageBuilder; import org.springframework.integration.util.IntegrationReactiveUtils; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; @@ -181,7 +185,9 @@ protected void doStart() { if (this.reactiveMessageHandler != null) { this.subscription = fluxFromChannel - .flatMap(this.reactiveMessageHandler::handleMessage) + .flatMap((message) -> + this.reactiveMessageHandler.handleMessage(message) + .contextWrite(StaticMessageHeaderAccessor.getReactorContext(message))) .onErrorContinue((ex, data) -> this.errorHandler.handleError(ex)) .subscribe(); } @@ -289,10 +295,33 @@ protected void hookOnSubscribe(Subscription subscription) { this.delegate.onSubscribe(subscription); } + @SuppressWarnings("try") @Override - protected void hookOnNext(Message value) { + protected void hookOnNext(Message message) { + Message messageToDeliver = message; + + if (IntegrationReactiveUtils.isContextPropagationPresent) { + ContextView reactorContext = message.getHeaders() + .get(IntegrationMessageHeaderAccessor.REACTOR_CONTEXT, ContextView.class); + + if (reactorContext != null) { + messageToDeliver = + MutableMessageBuilder.fromMessage(message) + .removeHeader(IntegrationMessageHeaderAccessor.REACTOR_CONTEXT) + .build(); + + try (AutoCloseable scope = IntegrationReactiveUtils.setThreadLocalsFromReactorContext(reactorContext)) { + this.delegate.onNext(messageToDeliver); + } + catch (Exception ex) { + this.errorHandler.handleError(ex); + } + return; + } + } + try { - this.delegate.onNext(value); + this.delegate.onNext(messageToDeliver); } catch (Exception ex) { this.errorHandler.handleError(ex); 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 151a5cf6161..5de14c912f3 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 @@ -424,6 +424,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 6bf0d3432ed..d461e8e6a57 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. @@ -29,6 +29,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; @@ -46,6 +47,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; @@ -365,6 +367,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; @@ -376,7 +379,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/DelayHandler.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/DelayHandler.java index b24bf5b5179..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 @@ -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. @@ -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 */ @@ -455,7 +458,14 @@ private void releaseMessageAfterDelay(final Message message, long delay) { .withPayload(messageWrapper) .copyHeaders(message.getHeaders()) .build(); - this.messageStore.addMessageToGroup(this.messageGroupId, delayedMessage); + + this.lock.lock(); + try { + this.messageStore.addMessageToGroup(this.messageGroupId, delayedMessage); + } + finally { + this.lock.unlock(); + } } Runnable releaseTask = releaseTaskForMessage(delayedMessage); @@ -495,15 +505,21 @@ private Runnable releaseTaskForMessage(Message delayedMessage) { } private Message getMessageById(UUID messageId) { - Message theMessage = this.messageStore.getMessageFromGroup(this.messageGroupId, messageId); + this.lock.lock(); + try { + Message theMessage = this.messageStore.getMessageFromGroup(this.messageGroupId, messageId); - if (theMessage == null) { - logger.debug(() -> "No message in the Message Store for id: " + messageId + - ". Likely another instance has already released it."); - return null; + if (theMessage == null) { + logger.debug(() -> "No message in the Message Store for id: " + messageId + + ". Likely another instance has already released it."); + return null; + } + else { + return theMessage; + } } - else { - return theMessage; + finally { + this.lock.unlock(); } } @@ -568,9 +584,16 @@ protected void rescheduleAt(Message message, Date startTime) { } private void doReleaseMessage(Message message) { - if (this.messageStore.removeMessageFromGroupById(this.messageGroupId, message.getHeaders().getId()) - || this.deliveries.get(ObjectUtils.getIdentityHexString(message)).get() > 0) { + boolean removed; + this.lock.lock(); + try { + removed = this.messageStore.removeMessageFromGroupById(this.messageGroupId, message.getHeaders().getId()); + } + finally { + this.lock.unlock(); + } + if (removed || this.deliveries.get(ObjectUtils.getIdentityHexString(message)).get() > 0) { handleMessageInternal(message); } else { @@ -581,7 +604,13 @@ private void doReleaseMessage(Message message) { @Override public int getDelayedMessageCount() { - return this.messageStore.messageGroupSize(this.messageGroupId); + this.lock.lock(); + try { + return this.messageStore.messageGroupSize(this.messageGroupId); + } + finally { + this.lock.unlock(); + } } /** @@ -662,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/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/support/MutableMessageHeaders.java b/spring-integration-core/src/main/java/org/springframework/integration/support/MutableMessageHeaders.java index 17300507124..eda3b895da8 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; @@ -32,6 +34,7 @@ * @author David Turanski * @author Artem Bilan * @author Nathan Kurtyka + * @author Mitchell McDonald * * @since 4.2 */ @@ -74,6 +77,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/IntegrationReactiveUtils.java b/spring-integration-core/src/main/java/org/springframework/integration/util/IntegrationReactiveUtils.java index a27b98aa98d..68cf4d31c2b 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/util/IntegrationReactiveUtils.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/util/IntegrationReactiveUtils.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. @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.concurrent.locks.LockSupport; +import io.micrometer.context.ContextSnapshotFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; @@ -26,17 +27,23 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import reactor.core.scheduler.Schedulers; +import reactor.util.context.Context; +import reactor.util.context.ContextView; import reactor.util.retry.Retry; +import org.springframework.integration.IntegrationMessageHeaderAccessor; import org.springframework.integration.StaticMessageHeaderAccessor; import org.springframework.integration.acks.AckUtils; import org.springframework.integration.core.MessageSource; +import org.springframework.integration.support.MutableMessageBuilder; +import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; import org.springframework.messaging.PollableChannel; import org.springframework.messaging.SubscribableChannel; +import org.springframework.util.ClassUtils; /** * Utilities for adapting integration components to/from reactive types. @@ -60,9 +67,47 @@ public final class IntegrationReactiveUtils { */ public static final Duration DEFAULT_DELAY_WHEN_EMPTY = Duration.ofSeconds(1); + /** + * The indicator that {@code io.micrometer:context-propagation} library is on classpath. + * @since 6.2.5 + */ + public static final boolean isContextPropagationPresent = ClassUtils.isPresent( + "io.micrometer.context.ContextSnapshot", IntegrationReactiveUtils.class.getClassLoader()); + + private static final ContextSnapshotFactory CONTEXT_SNAPSHOT_FACTORY = + isContextPropagationPresent ? ContextSnapshotFactory.builder().build() : null; + private IntegrationReactiveUtils() { } + /** + * Capture a Reactor {@link ContextView} from the current thread local state + * according to the {@link ContextSnapshotFactory} logic. + * If no {@code io.micrometer:context-propagation} library is on classpath, + * the {@link Context#empty()} is returned. + * @return the Reactor {@link ContextView} from the current thread local state or {@link Context#empty()}. + * @since 6.2.5 + */ + public static ContextView captureReactorContext() { + return isContextPropagationPresent + ? CONTEXT_SNAPSHOT_FACTORY.captureAll().updateContext(Context.empty()) + : Context.empty(); + } + + /** + * Populate thread local variables from the provided Reactor {@link ContextView} + * according to the {@link ContextSnapshotFactory} logic. + * @param context the Reactor {@link ContextView} to populate from. + * @return the {@link io.micrometer.context.ContextSnapshot.Scope} as a {@link AutoCloseable} + * to not pollute the target classpath. Can be cast if necessary. + * Or null if there is no {@code io.micrometer:context-propagation} library is on classpath. + * @since 6.2.5 + */ + @Nullable + public static AutoCloseable setThreadLocalsFromReactorContext(ContextView context) { + return isContextPropagationPresent ? CONTEXT_SNAPSHOT_FACTORY.setThreadLocalsFrom(context) : null; + } + /** * Wrap a provided {@link MessageSource} into a {@link Flux} for pulling the on demand. * When {@link MessageSource#receive()} returns {@code null}, the source {@link Mono} @@ -137,8 +182,15 @@ private static Flux> adaptSubscribableChannelToPublisher(Subscrib return Flux.defer(() -> { Sinks.Many> sink = Sinks.many().unicast().onBackpressureError(); MessageHandler messageHandler = (message) -> { + Message messageToEmit = message; + ContextView contextView = IntegrationReactiveUtils.captureReactorContext(); + if (!contextView.isEmpty()) { + messageToEmit = MutableMessageBuilder.fromMessage(message) + .setHeader(IntegrationMessageHeaderAccessor.REACTOR_CONTEXT, contextView) + .build(); + } while (true) { - switch (sink.tryEmitNext((Message) message)) { + switch (sink.tryEmitNext((Message) messageToEmit)) { case FAIL_NON_SERIALIZED: case FAIL_OVERFLOW: LockSupport.parkNanos(1000); // NOSONAR 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 83316561cda..5292f278d09 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,12 +19,12 @@ 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.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; @@ -157,8 +157,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(); @@ -166,18 +164,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(); @@ -212,6 +210,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 8add898c447..24ae91d18e3 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,8 +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/aop/AnnotationConfigRegistrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aop/AnnotationConfigRegistrationTests.java index 359e25d9942..9bcdddbc295 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aop/AnnotationConfigRegistrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aop/AnnotationConfigRegistrationTests.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.aop; -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.Publisher; @@ -25,8 +24,8 @@ import org.springframework.messaging.Message; import org.springframework.messaging.handler.annotation.Header; 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; @@ -37,8 +36,8 @@ * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class AnnotationConfigRegistrationTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingAnnotationUsageTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingAnnotationUsageTests-context.xml index 0cac76e5a18..71af0768126 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingAnnotationUsageTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingAnnotationUsageTests-context.xml @@ -1,9 +1,7 @@ diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingAnnotationUsageTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingAnnotationUsageTests.java index 00152283e30..ec2a517587a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingAnnotationUsageTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingAnnotationUsageTests.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,8 @@ package org.springframework.integration.aop; -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.Publisher; @@ -25,8 +25,8 @@ import org.springframework.messaging.Message; import org.springframework.messaging.handler.annotation.Header; 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; @@ -37,8 +37,8 @@ * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class MessagePublishingAnnotationUsageTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorTests.java index 732c3c591e7..f15efc2077a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorTests.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. @@ -20,8 +20,8 @@ import java.util.HashMap; import java.util.Map; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -49,7 +49,7 @@ public class MessagePublishingInterceptorTests { private DefaultListableBeanFactory beanFactory; - @Before + @BeforeEach public void setup() { beanFactory = new DefaultListableBeanFactory(); channelResolver = new BeanFactoryChannelResolver(beanFactory); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorUsageTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorUsageTests-context.xml index 4082c7cfeee..2c0029a889a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorUsageTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorUsageTests-context.xml @@ -31,7 +31,9 @@ - + + + @@ -42,16 +44,6 @@ - - - - - - - - - diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorUsageTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorUsageTests.java index 3f3516e6d75..444c58ca6ae 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorUsageTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aop/MessagePublishingInterceptorUsageTests.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,14 @@ package org.springframework.integration.aop; -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.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; import static org.assertj.core.api.Assertions.assertThat; @@ -34,8 +34,8 @@ * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class MessagePublishingInterceptorUsageTests { @Autowired diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aop/MethodAnnotationPublisherMetadataSourceTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aop/MethodAnnotationPublisherMetadataSourceTests.java index 0e5f7df80ab..cc958bb4e36 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aop/MethodAnnotationPublisherMetadataSourceTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aop/MethodAnnotationPublisherMetadataSourceTests.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. @@ -22,7 +22,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AliasFor; import org.springframework.expression.Expression; @@ -32,6 +32,8 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * @author Mark Fisher @@ -104,10 +106,11 @@ public void voidReturnWithValidPayloadExpression() { assertThat(payloadExpression).isEqualTo("#method"); } - @Test(expected = IllegalArgumentException.class) + @Test public void voidReturnWithInvalidPayloadExpression() { Method method = getMethod("methodWithVoidReturnAndReturnValueAsPayload"); - source.getExpressionForPayload(method); + assertThatIllegalArgumentException() + .isThrownBy(() -> source.getExpressionForPayload(method)); } @Test @@ -117,22 +120,25 @@ public void voidReturnAndParameterPayloadAnnotation() { assertThat(payloadExpression).isEqualTo("#args[0]"); } - @Test(expected = IllegalStateException.class) + @Test public void voidReturnAndParameterPayloadAnnotationWithExpression() { Method method = getMethod("methodWithVoidReturnAndParameterPayloadAnnotationWithExpression", String.class); - source.getExpressionForPayload(method).getExpressionString(); + assertThatIllegalStateException() + .isThrownBy(() -> source.getExpressionForPayload(method).getExpressionString()); } - @Test(expected = IllegalStateException.class) + @Test public void voidReturnAndParameterPayloadAnnotationWithValue() { Method method = getMethod("methodWithVoidReturnAndParameterPayloadAnnotationWithValue", String.class); - source.getExpressionForPayload(method).getExpressionString(); + assertThatIllegalStateException() + .isThrownBy(() -> source.getExpressionForPayload(method).getExpressionString()); } - @Test(expected = IllegalArgumentException.class) + @Test public void voidReturnAndNoPayloadAnnotation() { Method method = getMethod("methodWithVoidReturnAndNoPayloadAnnotation", String.class); - source.getExpressionForPayload(method); + assertThatIllegalArgumentException() + .isThrownBy(() -> source.getExpressionForPayload(method)); } @Test diff --git a/spring-integration-core/src/test/java/org/springframework/integration/aop/PublisherExpressionTests.java b/spring-integration-core/src/test/java/org/springframework/integration/aop/PublisherExpressionTests.java index e256fe3edf6..0b44559c187 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/aop/PublisherExpressionTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/aop/PublisherExpressionTests.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,9 @@ package org.springframework.integration.aop; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.aop.framework.ProxyFactory; import org.springframework.context.support.StaticApplicationContext; @@ -36,7 +36,7 @@ /** * @author Mark Fisher * @author Gary Russell - * @author Artme Bilan + * @author Artem Bilan * * @since 2.0 */ @@ -45,7 +45,7 @@ public class PublisherExpressionTests { private final StaticApplicationContext context = new StaticApplicationContext(); - @Before + @BeforeEach public void setup() throws Exception { this.context.registerSingleton("testChannel", QueueChannel.class); IntegrationEvaluationContextFactoryBean factory = new IntegrationEvaluationContextFactoryBean(); @@ -57,7 +57,7 @@ public void setup() throws Exception { this.context.getBeanFactory().registerSingleton("foo", "foo"); } - @After + @AfterEach public void tearDown() { this.context.close(); } 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/channel/interceptor/ObservationPropagationChannelInterceptorTests.java b/spring-integration-core/src/test/java/org/springframework/integration/channel/interceptor/ObservationPropagationChannelInterceptorTests.java deleted file mode 100644 index 7295d8a8ab8..00000000000 --- a/spring-integration-core/src/test/java/org/springframework/integration/channel/interceptor/ObservationPropagationChannelInterceptorTests.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * Copyright 2022 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.channel.interceptor; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -import io.micrometer.common.KeyValues; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.core.tck.MeterRegistryAssert; -import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationHandler; -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.observation.tck.TestObservationRegistry; -import io.micrometer.observation.tck.TestObservationRegistryAssert; -import io.micrometer.tracing.Span; -import io.micrometer.tracing.TraceContext; -import io.micrometer.tracing.Tracer; -import io.micrometer.tracing.handler.DefaultTracingObservationHandler; -import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; -import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; -import io.micrometer.tracing.propagation.Propagator; -import io.micrometer.tracing.test.simple.SimpleTracer; -import io.micrometer.tracing.test.simple.SpansAssert; -import io.micrometer.tracing.test.simple.TracerAssert; -import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.BeforeEach; -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.annotation.BridgeTo; -import org.springframework.integration.annotation.Poller; -import org.springframework.integration.channel.DirectChannel; -import org.springframework.integration.channel.ExecutorChannel; -import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.config.EnableIntegration; -import org.springframework.integration.config.GlobalChannelInterceptor; -import org.springframework.integration.handler.BridgeHandler; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.integration.support.management.observation.IntegrationObservation; -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; -import org.springframework.messaging.PollableChannel; -import org.springframework.messaging.SubscribableChannel; -import org.springframework.messaging.support.ChannelInterceptor; -import org.springframework.messaging.support.GenericMessage; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Artem Bilan - * - * @since 6.0 - */ -@SpringJUnitConfig -public class ObservationPropagationChannelInterceptorTests { - - @Autowired - ObservationRegistry observationRegistry; - - @Autowired - MeterRegistry meterRegistry; - - @Autowired - SimpleTracer simpleTracer; - - @Autowired - SubscribableChannel directChannel; - - @Autowired - SubscribableChannel executorChannel; - - @Autowired - PollableChannel queueChannel; - - @Autowired - DirectChannel testConsumer; - - @Autowired - ExecutorChannel testTracingChannel; - - @BeforeEach - void setup() { - this.simpleTracer.getSpans().clear(); - } - - @Test - void observationPropagatedOverDirectChannel() throws InterruptedException { - AtomicReference scopeReference = new AtomicReference<>(); - CountDownLatch handleLatch = new CountDownLatch(1); - this.directChannel.subscribe(m -> { - scopeReference.set(this.observationRegistry.getCurrentObservationScope()); - handleLatch.countDown(); - }); - - AtomicReference originalScope = new AtomicReference<>(); - - Observation.createNotStarted("test1", this.observationRegistry) - .observe(() -> { - originalScope.set(this.observationRegistry.getCurrentObservationScope()); - this.directChannel.send(new GenericMessage<>("test")); - }); - - assertThat(handleLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(scopeReference.get()) - .isNotNull() - .isSameAs(originalScope.get()); - - TestObservationRegistryAssert.assertThat(this.observationRegistry) - .doesNotHaveAnyRemainingCurrentObservation(); - - TracerAssert.assertThat(this.simpleTracer) - .onlySpan() - .hasNameEqualTo("test1"); - } - - @Test - void observationPropagatedOverExecutorChannel() throws InterruptedException { - AtomicReference scopeReference = new AtomicReference<>(); - CountDownLatch handleLatch = new CountDownLatch(1); - this.executorChannel.subscribe(m -> { - scopeReference.set(this.observationRegistry.getCurrentObservationScope()); - handleLatch.countDown(); - }); - - AtomicReference originalScope = new AtomicReference<>(); - - Observation.createNotStarted("test2", this.observationRegistry) - .observe(() -> { - originalScope.set(this.observationRegistry.getCurrentObservationScope()); - this.executorChannel.send(new GenericMessage<>("test")); - }); - - assertThat(handleLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(scopeReference.get()) - .isNotNull() - .isNotSameAs(originalScope.get()); - - assertThat(scopeReference.get().getCurrentObservation()) - .isSameAs(originalScope.get().getCurrentObservation()); - - TestObservationRegistryAssert.assertThat(this.observationRegistry) - .doesNotHaveAnyRemainingCurrentObservation(); - - TracerAssert.assertThat(this.simpleTracer) - .onlySpan() - .hasNameEqualTo("test2"); - } - - @Test - void observationPropagatedOverQueueChannel() throws InterruptedException { - AtomicReference scopeReference = new AtomicReference<>(); - CountDownLatch handleLatch = new CountDownLatch(1); - this.testConsumer.subscribe(m -> { - scopeReference.set(this.observationRegistry.getCurrentObservationScope()); - handleLatch.countDown(); - }); - - AtomicReference originalScope = new AtomicReference<>(); - - Observation.createNotStarted("test3", this.observationRegistry) - .observe(() -> { - originalScope.set(this.observationRegistry.getCurrentObservationScope()); - this.queueChannel.send(new GenericMessage<>("test")); - }); - - assertThat(handleLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(scopeReference.get()) - .isNotNull() - .isNotSameAs(originalScope.get()); - - assertThat(scopeReference.get().getCurrentObservation()) - .isSameAs(originalScope.get().getCurrentObservation()); - - TestObservationRegistryAssert.assertThat(this.observationRegistry) - .doesNotHaveAnyRemainingCurrentObservation(); - - TracerAssert.assertThat(this.simpleTracer) - .onlySpan() - .hasNameEqualTo("test3"); - } - - @Test - void observationContextPropagatedOverExecutorChannel() { - BridgeHandler handler = new BridgeHandler(); - handler.registerObservationRegistry(this.observationRegistry); - handler.setBeanName("testBridge"); - this.testTracingChannel.subscribe(handler); - - QueueChannel replyChannel = new QueueChannel(); - - Message message = - MessageBuilder.withPayload("test") - .setHeader(MessageHeaders.REPLY_CHANNEL, replyChannel) - .build(); - - this.testTracingChannel.send(message); - - Message receive = replyChannel.receive(); - - assertThat(receive).isNotNull() - .extracting(Message::getHeaders) - .asInstanceOf(InstanceOfAssertFactories.MAP) - .containsEntry("foo", "some foo value") - .containsEntry("bar", "some bar value"); - - TestObservationRegistryAssert.assertThat(this.observationRegistry) - .doesNotHaveAnyRemainingCurrentObservation(); - - TracerAssert.assertThat(this.simpleTracer) - .reportedSpans() - .hasSize(2) - .satisfies(simpleSpans -> SpansAssert.assertThat(simpleSpans) - .assertThatASpanWithNameEqualTo("testTracingChannel send") - .hasTag("spring.integration.type", "producer") - .hasTag("spring.integration.name", "testTracingChannel") - .hasKindEqualTo(Span.Kind.PRODUCER) - .backToSpans() - .assertThatASpanWithNameEqualTo("testBridge receive") - .hasTag("foo", "some foo value") - .hasTag("bar", "some bar value") - .hasTag("spring.integration.type", "handler") - .hasTag("spring.integration.name", "testBridge") - .hasKindEqualTo(Span.Kind.CONSUMER)); - - - MeterRegistryAssert.assertThat(this.meterRegistry) - .hasTimerWithNameAndTags("spring.integration.handler", - KeyValues.of(IntegrationObservation.HandlerTags.COMPONENT_NAME.asString(), "testBridge", - IntegrationObservation.HandlerTags.COMPONENT_TYPE.asString(), "handler", - "error", "none")); - - assertThat(this.meterRegistry.get("spring.integration.handler").timer().count()).isEqualTo(1); - } - - @Configuration - @EnableIntegration - public static class ContextConfiguration { - - @Bean - SimpleTracer simpleTracer() { - return new SimpleTracer(); - } - - @Bean - MeterRegistry meterRegistry() { - return new SimpleMeterRegistry(); - } - - @Bean - ObservationRegistry observationRegistry(Tracer tracer, Propagator propagator, MeterRegistry meterRegistry) { - TestObservationRegistry observationRegistry = TestObservationRegistry.create(); - observationRegistry.observationConfig() - .observationHandler(new DefaultMeterObservationHandler(meterRegistry)) - .observationHandler( - // Composite will pick the first matching handler - new ObservationHandler.FirstMatchingCompositeObservationHandler( - // This is responsible for creating a child span on the sender side - new PropagatingSenderTracingObservationHandler<>(tracer, propagator), - // This is responsible for creating a span on the receiver side - new PropagatingReceiverTracingObservationHandler<>(tracer, propagator), - // This is responsible for creating a default span - new DefaultTracingObservationHandler(tracer))); - return observationRegistry; - } - - @Bean - @GlobalChannelInterceptor(patterns = "*Channel") - public ChannelInterceptor observationPropagationInterceptor(ObservationRegistry observationRegistry) { - return new ObservationPropagationChannelInterceptor(observationRegistry); - } - - @Bean - @BridgeTo(value = "testConsumer", poller = @Poller(fixedDelay = "100")) - public PollableChannel queueChannel() { - return new QueueChannel(); - } - - @Bean - public SubscribableChannel executorChannel() { - return new ExecutorChannel(Executors.newSingleThreadExecutor()); - } - - @Bean - public SubscribableChannel directChannel() { - return new DirectChannel(); - } - - @Bean - public DirectChannel testConsumer() { - return new DirectChannel(); - } - - @Bean - public ExecutorChannel testTracingChannel(ObservationRegistry observationRegistry) { - ExecutorChannel channel = new ExecutorChannel(Executors.newSingleThreadExecutor()); - channel.registerObservationRegistry(observationRegistry); - return channel; - } - - @Bean - public Propagator propagator(Tracer tracer) { - return new Propagator() { - - // List of headers required for tracing propagation - @Override - public List fields() { - return Arrays.asList("foo", "bar"); - } - - // This is called on the producer side when the message is being sent - // Normally we would pass information from tracing context - for tests we don't need to - @Override - public void inject(TraceContext context, @Nullable C carrier, Setter setter) { - setter.set(carrier, "foo", "some foo value"); - setter.set(carrier, "bar", "some bar value"); - } - - // This is called on the consumer side when the message is consumed - // Normally we would use tools like Extractor from tracing but for tests we are just manually creating a span - @Override - public Span.Builder extract(C carrier, Getter getter) { - String foo = getter.get(carrier, "foo"); - String bar = getter.get(carrier, "bar"); - return tracer.spanBuilder().tag("foo", foo).tag("bar", bar); - } - }; - } - - } - -} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/channel/reactive/FluxMessageChannelTests.java b/spring-integration-core/src/test/java/org/springframework/integration/channel/reactive/FluxMessageChannelTests.java index 0072b58f449..bb9b81fe5fb 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/channel/reactive/FluxMessageChannelTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/channel/reactive/FluxMessageChannelTests.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. @@ -164,7 +164,7 @@ void noMemoryLeakInFluxMessageChannelForVolatilePublishers() { Disposable.Composite upstreamSubscriptions = TestUtils.getPropertyValue(messageChannel, "upstreamSubscriptions", Disposable.Composite.class); - assertThat(upstreamSubscriptions.size()).isEqualTo(0); + await().untilAsserted(() -> assertThat(upstreamSubscriptions.size()).isEqualTo(0)); } @Configuration diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests-context.xml index 6ed6b5c754c..15a048a1275 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/AggregatorParserTests-context.xml @@ -1,19 +1,17 @@ - + @@ -40,96 +38,99 @@ - + input-channel="completelyDefinedAggregatorInput" + output-channel="outputChannel" + discard-channel="discardChannel" + ref="aggregatorBean" + release-strategy="releaseStrategy" + correlation-strategy="correlationStrategy" + send-timeout="86420000" + send-partial-result-on-expiry="true" + expire-groups-upon-completion="true" + expire-groups-upon-timeout="false" + empty-group-min-timeout="123" + group-timeout="456" + lock-registry="lockRegistry" + scheduler="scheduler" + message-store="store" + pop-sequence="false" + order="5" + expire-duration="10000" + expire-timeout="250"> + - + + + + input-channel="aggregatorWithExpressionsInput" + output-channel="aggregatorWithExpressionsOutput" + expression="?[payload.startsWith('1')].![payload]" + release-strategy-expression="#root.size()>2" + correlation-strategy-expression="headers['foo']"/> + ref="adderBean" + method="add" + input-channel="aggregatorWithReferenceAndMethodInput" + output-channel="outputChannel"/> + input-channel="aggregatorWithPojoReleaseStrategyInput" + output-channel="outputChannel" + ref="adderBean" + method="add" + release-strategy="pojoReleaseStrategy" + release-strategy-method="checkCompletenessAsList"/> + input-channel="aggregatorWithPojoReleaseStrategyInputAsCollection" + output-channel="outputChannel" + ref="adderBean" + method="add" + release-strategy="pojoReleaseStrategy" + release-strategy-method="checkCompletenessAsCollection"/> + input-channel="aggregatorWithExpressionsAndPojoAggregatorInput" + ref="aggregatorBean" + release-strategy-expression="size() == 2" + correlation-strategy-expression="headers['foo']" + empty-group-min-timeout="60000"/> + class="org.springframework.integration.config.TestAggregatorBean"/> + class="org.springframework.integration.aggregator.SimpleMessageGroupProcessor"/> + class="org.springframework.integration.config.AggregatorParserTests$MyMGP"/> - + + class="org.springframework.integration.config.TestReleaseStrategy"/> - + class="org.springframework.integration.config.MaxValueReleaseStrategy"> + 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 f46a82cbf2a..9288bcf62de 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,37 +16,52 @@ 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/DelayerParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerParserTests-context.xml index a6cf20ac4af..3bad3c9e88b 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerParserTests-context.xml @@ -75,6 +75,7 @@ diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests-context.xml index f91966bafdb..6702c4d75a4 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/DelayerUsageTests-context.xml @@ -49,6 +49,7 @@ 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 85% 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 58846801b98..d992eae1ac6 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 @@ -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,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; public void setValue(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 8458c3bbf34..b6cf706aab4 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 ee7ffa71b89..00854bfef59 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 0558a09d96f..e377d27006a 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 28ce07d694d..9eceb433853 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 @@ -115,19 +114,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-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests-context.xml index 946938031e7..636ed22f952 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/GatewayParserTests-context.xml @@ -24,10 +24,14 @@ default-request-channel="requestChannel" proxy-default-methods="true"/> + + + + + default-reply-channel="receiveChannel" + default-reply-timeout="5000"/> ("foo")); TestService service = (TestService) context.getBean("solicitResponse"); String result = service.solicitResponse(); @@ -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(); } 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 0a429e28dbe..19e462139cb 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 88f3e69a2d9..790db6b4c38 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 @@ -157,7 +156,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); } @@ -313,18 +312,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 0b0fa1e157c..a54fb754e9b 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 1acb2735651..7ec4f6802dd 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 0ddcddc79ae..859b9f203a6 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,13 +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)); } 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 9fde9767031..a752985f5dd 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 04d517a9c16..58ac541d746 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 b4faec55b84..6a81da4958b 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 @@ -70,7 +72,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/config/xml/superParentApplicationContext.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/superParentApplicationContext.xml index 9bbe4122381..8126b804c47 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/xml/superParentApplicationContext.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/xml/superParentApplicationContext.xml @@ -7,6 +7,8 @@ https://www.springframework.org/schema/integration/spring-integration.xsd"> + class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler"> + + 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 217d6db882e..c30fe72ebb4 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; @@ -433,8 +434,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); } @@ -444,6 +445,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 91866e17385..cf9649b5580 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 { @@ -54,9 +59,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 84700eb808a..228bea15a6e 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; @@ -48,6 +49,7 @@ * @since 5.0 */ @SpringJUnitConfig +@DirtiesContext public class LambdaMessageProcessorTests { @Autowired 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 8b63145d1bd..67ef2aab9d2 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 @@ -300,6 +300,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 384c82ce5c0..a86b1011508 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-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. @@ -41,6 +41,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.Lifecycle; +import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -586,6 +587,7 @@ public PollerSpec poller() { @Bean(name = IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME) public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPhase(SmartLifecycle.DEFAULT_PHASE / 2); threadPoolTaskScheduler.setPoolSize(100); return threadPoolTaskScheduler; } @@ -925,7 +927,9 @@ public IntegrationFlow dedicatedPollingThreadFlow() { @Bean public TaskScheduler dedicatedTaskScheduler() { - return new ThreadPoolTaskScheduler(); + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPhase(SmartLifecycle.DEFAULT_PHASE / 2); + return threadPoolTaskScheduler; } @Bean 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 e267a1f5c0a..20617e3acd6 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-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. @@ -34,6 +34,7 @@ import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -45,8 +46,11 @@ import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.MessageChannels; +import org.springframework.integration.dsl.MessageProducerSpec; import org.springframework.integration.dsl.context.IntegrationFlowContext; import org.springframework.integration.endpoint.AbstractEndpoint; +import org.springframework.integration.endpoint.MessageProducerSupport; +import org.springframework.integration.endpoint.ReactiveMessageSourceProducer; import org.springframework.integration.endpoint.ReactiveStreamsConsumer; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -240,6 +244,27 @@ void fixedSubscriberChannelFlowTest() throws InterruptedException { assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); } + @Autowired + MessageProducerSupport testMessageProducer; + + @Autowired + Publisher> messageProducerFlow; + + @Test + void messageProducerIsNotStartedAutomatically() { + assertThat(this.testMessageProducer.isRunning()).isFalse(); + + Flux flux = + Flux.from(this.messageProducerFlow) + .map(Message::getPayload); + + StepVerifier.create(flux) + .expectNext("test") + .expectNext("test") + .thenCancel() + .verify(Duration.ofSeconds(10)); + } + @Configuration @EnableIntegration public static class ContextConfiguration { @@ -287,6 +312,26 @@ public Publisher> fixedSubscriberChannelFlow() { .toReactivePublisher(); } + @Bean + public Publisher> messageProducerFlow() { + TestMessageProducerSpec testMessageProducerSpec = + new TestMessageProducerSpec(new ReactiveMessageSourceProducer(() -> new GenericMessage<>("test"))) + .id("testMessageProducer"); + + return IntegrationFlow + .from(testMessageProducerSpec) + .toReactivePublisher(true); + } + + } + + private static class TestMessageProducerSpec + extends MessageProducerSpec { + + TestMessageProducerSpec(ReactiveMessageSourceProducer producer) { + super(producer); + } + } } 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/ExpressionEvaluatingMessageSourceIntegrationTests.java b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ExpressionEvaluatingMessageSourceIntegrationTests.java index 35f5d4c5fc7..d6806fd1c4a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ExpressionEvaluatingMessageSourceIntegrationTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/ExpressionEvaluatingMessageSourceIntegrationTests.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. @@ -80,6 +80,7 @@ public void test() throws Exception { for (int i = 0; i < 3; i++) { messages.add(channel.receive(1000)); } + adapter.stop(); scheduler.destroy(); Message message1 = messages.get(0); assertThat(message1.getPayload()).isEqualTo("test-1"); 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/PollingLifecycleTests.java b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/PollingLifecycleTests.java index 3cc371c4aaa..f5ee4eca7e3 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/endpoint/PollingLifecycleTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/endpoint/PollingLifecycleTests.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. @@ -29,18 +29,21 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.Lifecycle; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.integration.channel.NullChannel; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.config.SourcePollingChannelAdapterFactoryBean; import org.springframework.integration.config.TestErrorHandler; import org.springframework.integration.core.MessageSource; import org.springframework.integration.scheduling.PollerMetadata; +import org.springframework.integration.util.TestDefaultAnnotationConfiguration; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; import org.springframework.messaging.support.GenericMessage; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.support.PeriodicTrigger; +import org.springframework.util.StopWatch; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.atMost; @@ -220,4 +223,24 @@ public boolean isRunning() { assertThat(stopInvoked.get()).isTrue(); } + @Test + public void theScheduledPollingTaskIsCancelledNotCausingApplicationContextStopDeadLock() { + var context = new AnnotationConfigApplicationContext(); + context.register(TestDefaultAnnotationConfiguration.class); + + PollingConsumer consumer = new PollingConsumer(new QueueChannel(), (m) -> { }); + consumer.setTrigger(new PeriodicTrigger(Duration.ofSeconds(10))); + consumer.setReceiveTimeout(30_000); + + context.registerBean(PollingConsumer.class, () -> consumer); + context.refresh(); + + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + context.close(); + stopWatch.stop(); + + assertThat(stopWatch.getTotalTimeMillis()).isLessThan(10_000); + } + } 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 bc6184859ae..bdf3f26cc03 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 b9ec6fedb17..855be5dc0b1 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 4c3ad46f4c0..26ea6b15c9c 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 b19a7c8ad0c..5e095df2081 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 c5f3c7ea377..acb313ed46d 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 ff1f76814de..bfcb70a99d2 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 @@ -49,10 +49,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 08a436018f4..3ef3d335c6d 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,32 +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 accb6507c25..eb7ff39d6ac 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 78dceb6cba8..9d8a7208c23 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 @@ -51,7 +52,7 @@ public class HeaderEnrichedGatewayTests { @Test - public void validateHeaderValueMappings() throws Exception { + public void validateHeaderValueMappings() { testPayload = "hello"; gatewayWithHeaderValues.sendString((String) testPayload); Message message1 = channel.receive(0); @@ -78,7 +79,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 cb167a9534d..11571201e10 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; /** @@ -37,8 +37,8 @@ * @author Artem Bilan * */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig +@DirtiesContext public class InnerGatewayWithChainTests { @Autowired @@ -81,10 +81,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 dc8abbc8074..59e93e36310 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 b3544cdda1f..f51d666e352 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 ff92c76f88a..b417c1335fe 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 0301b7b91dd..3fc2752eada 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 @@ -40,32 +40,32 @@ 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 33d679368d5..49089441d58 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 @@ -81,14 +80,6 @@ public void shouldInvokeMockedSingleMethodOnClass() { verify(singleMethodOnClass).move("SingleMethodOnClass"); } - public static class SingleMethodAcceptingHeaderOnClass { - - public String move(@Header("s") String s) { - return s; - } - - } - @Autowired SingleMethodAcceptingHeaderOnClass singleMethodAcceptingHeaderOnClass; @@ -100,4 +91,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/ExpressionEvaluatingRequestHandlerAdviceTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/ExpressionEvaluatingRequestHandlerAdviceTests.java index a450b0bfdda..a75cf177452 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 @@ -31,6 +31,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; @@ -43,6 +44,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 75a844fd793..b489aad16c3 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 @@ -117,7 +115,7 @@ public static void setUp() throws IOException { @Test - public void testInt3139JsonPathTransformer() throws IOException { + public void testInt3139JsonPathTransformer() { this.transformerInput.send(testMessage); Message receive = this.output.receive(10000); assertThat(receive).isNotNull(); @@ -128,20 +126,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 @@ -166,13 +158,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(); @@ -187,7 +175,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 3d599afc284..cef067c7bbd 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 f649892e706..504270a0bca 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 a80358f36b2..75de85e8844 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 @@ -69,18 +68,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 1e464325348..7e349ac8b79 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 0ab0d66deb9..2e8006fef4a 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 c9a176f0e97..1ca6066160d 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 e8fb7529406..a69dba5bd8f 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 3d9b25bc8ce..5376000d7e6 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,8 @@ public void pojoRouter() { assertThat(this.testBean.isRunning()).isTrue(); } - private static class TestBean { + private record TestBean(String name) { - private final String name; - - TestBean(String name) { - this.name = name; - } - - public String getName() { - return this.name; - } } @@ -145,7 +136,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 5291838743f..695a3cc86ac 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/scattergather/config/ScatterGatherTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/scattergather/config/ScatterGatherTests-context.xml index 5754fce70f5..54afb9c533b 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/scattergather/config/ScatterGatherTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/scattergather/config/ScatterGatherTests-context.xml @@ -29,7 +29,8 @@ - + 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 c194cb814b9..99bef77882d 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 7a58116e4b4..9a2bd9ffb20 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/store/MessageStoreReaperTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/store/MessageStoreReaperTests-context.xml index 4ca53e86c02..593993de63e 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/store/MessageStoreReaperTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/store/MessageStoreReaperTests-context.xml @@ -1,9 +1,9 @@ @@ -23,7 +23,10 @@ - + + + @@ -35,21 +38,21 @@ - + - + + discard-channel="discards" + message-store="messageStore3"/> - + - + 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 16b1a24032c..dfe923cf5d9 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,11 +31,13 @@ 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 */ @@ -105,4 +112,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 a7cbb6fe03b..2d2c2b4c2d0 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() { @@ -37,6 +44,5 @@ 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 e572ebf29d1..56452a9aeeb 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 @@ -18,37 +18,43 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import io.micrometer.common.KeyValues; import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.tracing.Span; import io.micrometer.tracing.test.SampleTestRunner; import io.micrometer.tracing.test.simple.SpansAssert; +import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.integration.annotation.BridgeTo; import org.springframework.integration.annotation.EndpointId; import org.springframework.integration.annotation.Poller; import org.springframework.integration.annotation.ServiceActivator; import org.springframework.integration.channel.NullChannel; +import org.springframework.integration.channel.PublishSubscribeChannel; import org.springframework.integration.channel.QueueChannel; -import org.springframework.integration.channel.interceptor.ObservationPropagationChannelInterceptor; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.config.EnableIntegrationManagement; -import org.springframework.integration.config.GlobalChannelInterceptor; +import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.gateway.MessagingGatewaySupport; import org.springframework.integration.handler.BridgeHandler; 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.ChannelInterceptor; +import org.springframework.messaging.support.ErrorMessage; import org.springframework.messaging.support.GenericMessage; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; /** * @author Artem Bilan @@ -96,6 +102,20 @@ public SampleTestRunnerConsumer yourCode() { assertThat(receive).isNull(); 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()) @@ -113,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(3); + .hasSize(5); MeterRegistryAssert.assertThat(getMeterRegistry()) .hasTimerWithNameAndTags("spring.integration.handler", @@ -129,19 +149,13 @@ public SampleTestRunnerConsumer yourCode() { @EnableIntegration @EnableIntegrationManagement( observationPatterns = { - "${spring.integration.management.observation-patterns:testInboundGateway,skippedObservationInboundGateway,queueChannel,observedEndpoint}", + "${spring.integration.management.observation-patterns:testInboundGateway,skippedObservationInboundGateway,queueChannel,myErrorChannel,observedEndpoint,publishSubscribeChannel}", "${spring.integration.management.observation-patterns:}" }) public static class ObservationIntegrationTestConfiguration { CountDownLatch observedHandlerLatch = new CountDownLatch(1); - @Bean - @GlobalChannelInterceptor - public ChannelInterceptor observationPropagationInterceptor(ObservationRegistry observationRegistry) { - return new ObservationPropagationChannelInterceptor(observationRegistry); - } - @Bean TestMessagingGatewaySupport testInboundGateway(@Qualifier("queueChannel") PollableChannel queueChannel) { TestMessagingGatewaySupport messagingGatewaySupport = new TestMessagingGatewaySupport(); @@ -177,8 +191,21 @@ TestMessagingGatewaySupport skippedObservationInboundGateway() { @ServiceActivator(inputChannel = "queueChannel", poller = @Poller(fixedDelay = "100"), adviceChain = "observedHandlerAdvice") - BridgeHandler bridgeHandler() { - return new BridgeHandler(); + BridgeHandler bridgeHandler(PublishSubscribeChannel publishSubscribeChannel) { + BridgeHandler bridgeHandler = new BridgeHandler(); + bridgeHandler.setOutputChannel(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 @@ -193,6 +220,26 @@ HandleMessageAdvice observedHandlerAdvice() { }; } + @Bean + @BridgeTo + PublishSubscribeChannel publishSubscribeChannel() { + return new PublishSubscribeChannel(); + } + + AtomicReference observationReference = new AtomicReference<>(); + + @Bean + IntegrationFlow handleReactiveFlow(PublishSubscribeChannel publishSubscribeChannel, + ObservationRegistry observationRegistry) { + + return IntegrationFlow.from(publishSubscribeChannel) + .handleReactive(m -> + Mono.just("Hi There") + .doOnSuccess(val -> + observationReference.set(observationRegistry.getCurrentObservation())) + .then()); + } + } private static class TestMessagingGatewaySupport extends MessagingGatewaySupport { 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 3b0e0ab7c0f..f727e4e7624 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,18 +35,20 @@ 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 49629f36593..98aa08436fb 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 fe3a4f71754..03d9e1f41ae 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 5e2e60285ab..87d33455245 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 a5e77288309..d4e2ee27e35 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-core/src/test/java/org/springframework/integration/util/TestDefaultAnnotationConfiguration.java b/spring-integration-core/src/test/java/org/springframework/integration/util/TestDefaultAnnotationConfiguration.java new file mode 100644 index 00000000000..fdd8afa32fd --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/util/TestDefaultAnnotationConfiguration.java @@ -0,0 +1,25 @@ +/* + * 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.util; + +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.config.EnableIntegration; + +@Configuration +@EnableIntegration +public class TestDefaultAnnotationConfiguration { +} 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 d0815f4205d..efeab4b2172 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; @@ -389,10 +388,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 7d6f9f7ba6c..009e243f231 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 @@ -630,6 +630,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/aop/StandardRotationPolicy.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/aop/StandardRotationPolicy.java index de4c9fdde7d..6f584a81ee7 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/aop/StandardRotationPolicy.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/aop/StandardRotationPolicy.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 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. @@ -24,7 +24,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.log.LogMessage; import org.springframework.integration.core.MessageSource; +import org.springframework.integration.endpoint.AbstractFetchLimitingMessageSource; import org.springframework.integration.file.remote.AbstractRemoteFileStreamingMessageSource; import org.springframework.integration.file.remote.session.DelegatingSessionFactory; import org.springframework.integration.file.remote.synchronizer.AbstractInboundFileSynchronizingMessageSource; @@ -33,9 +35,9 @@ /** * Standard rotation policy; iterates over key/directory pairs; when the end is reached, * starts again at the beginning. If the fair option is true the rotation occurs on every - * poll, regardless of result. Otherwise rotation occurs when the current pair returns no + * poll, regardless of result. Otherwise, rotation occurs when the current pair returns no * message. - * + *

* Subclasses implement {@code onRotation(MessageSource source)} to configure the * {@link MessageSource} on each rotation. * @@ -78,6 +80,12 @@ public StandardRotationPolicy(DelegatingSessionFactory factory, List source) { if (this.fair || !this.initialized) { configureSource(source); + if (this.fair && !this.initialized + && source instanceof AbstractFetchLimitingMessageSource fetchLimitingMessageSource) { + + this.logger.info(LogMessage.format("Enforce 'maxFetchSize = 1' for '%s' in the 'fair' mode", source)); + fetchLimitingMessageSource.setMaxFetchSize(1); + } this.initialized = true; } if (this.logger.isTraceEnabled()) { @@ -142,11 +150,11 @@ protected void configureSource(MessageSource source) { * @param source the MessageSource. */ protected void onRotation(MessageSource source) { - if (source instanceof AbstractRemoteFileStreamingMessageSource) { - ((AbstractRemoteFileStreamingMessageSource) source).setRemoteDirectory(this.current.getDirectory()); + if (source instanceof AbstractRemoteFileStreamingMessageSource streamingMessageSource) { + streamingMessageSource.setRemoteDirectory(this.current.getDirectory()); } - else if (source instanceof AbstractInboundFileSynchronizingMessageSource) { - ((AbstractInboundFileSynchronizingMessageSource) source).getSynchronizer() + else if (source instanceof AbstractInboundFileSynchronizingMessageSource synchronizingMessageSource) { + synchronizingMessageSource.getSynchronizer() .setRemoteDirectory(this.current.getDirectory()); } } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/synchronizer/AbstractInboundFileSynchronizer.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/synchronizer/AbstractInboundFileSynchronizer.java index 0c285ee1090..bffe5e31ca8 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/synchronizer/AbstractInboundFileSynchronizer.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/synchronizer/AbstractInboundFileSynchronizer.java @@ -444,7 +444,7 @@ protected void rollbackFromFileToListEnd(List filteredFiles, F file) { } } - protected boolean copyFileToLocalDirectory(String remoteDirectoryPath, // NOSONAR + protected boolean copyFileToLocalDirectory(@Nullable String remoteDirectoryPath, // NOSONAR @Nullable EvaluationContext localFileEvaluationContext, F remoteFile, File localDirectory, Session session) throws IOException { @@ -503,9 +503,15 @@ protected boolean copyFileToLocalDirectory(String remoteDirectoryPath, // NOSONA String host = hostPort.substring(0, colonIndex); String port = hostPort.substring(colonIndex + 1); try { + String remoteDir = "/"; + if (remoteDirectoryPath != null) { + remoteDir = + remoteDirectoryPath.charAt(0) == '/' + ? remoteDirectoryPath : + '/' + remoteDirectoryPath; + } String remoteFileMetadata = - new URI(protocol(), null, host, Integer.parseInt(port), - '/' + remoteDirectoryPath, null, remoteFileName) + new URI(protocol(), null, host, Integer.parseInt(port), remoteDir, null, remoteFileName) .toString(); this.remoteFileMetadataStore.put(buildMetadataKey(localFile), remoteFileMetadata); } 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 3e7d3762252..de3c12b492d 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 @@ -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. @@ -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; @@ -302,6 +304,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(); @@ -543,6 +562,7 @@ public void noFlushAppend() throws Exception { } assertThat(flushes.get()).isGreaterThanOrEqualTo(2); handler.stop(); + taskScheduler.destroy(); } @Test @@ -586,6 +606,7 @@ protected BufferedOutputStream createOutputStream(File fileToWriteTo, boolean ap verify(out).write(any(byte[].class), anyInt(), anyInt()); assertThat(closeWhileWriting.get()).isFalse(); handler.stop(); + taskScheduler.destroy(); } @Test diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileTailInboundChannelAdapterParserTests-context.xml b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileTailInboundChannelAdapterParserTests-context.xml index 631a000eccf..6eb052a52da 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileTailInboundChannelAdapterParserTests-context.xml +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileTailInboundChannelAdapterParserTests-context.xml @@ -1,14 +1,13 @@ @@ -16,37 +15,37 @@ + channel="input" + task-executor="exec" + file="/tmp/baz" + auto-startup="true" + phase="123" + error-channel="tailErrorChannel"/> + channel="input" + native-options="-F -n 6" + task-executor="exec" + task-scheduler="scheduler" + enable-status-reader="false" + file-delay="456" + file="/tmp/foo" + auto-startup="true" + delay="${empty}" + end="${empty}" + reopen="${empty}" + phase="123"/> + channel="input" + native-options="${empty}" + task-executor="exec" + file="/tmp/bar" + delay="${foo}" + file-delay="10000" + idle-event-interval="10000" + auto-startup="false" + phase="123"/> @@ -56,20 +55,23 @@ + channel="input" + task-executor="exec" + file="/tmp/qux" + delay="2000" + file-delay="10000" + end="false" + reopen="true" + auto-startup="false" + phase="123"/> - + - + + + - + diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileTailInboundChannelAdapterParserTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileTailInboundChannelAdapterParserTests.java index b2da04b9406..e942de017a8 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileTailInboundChannelAdapterParserTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/config/FileTailInboundChannelAdapterParserTests.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. @@ -66,7 +66,7 @@ public class FileTailInboundChannelAdapterParserTests { private TaskExecutor exec; @Autowired - private TaskScheduler sched; + private TaskScheduler scheduler; @Autowired private MessageChannel tailErrorChannel; @@ -95,7 +95,7 @@ public void testNative() { assertThat(normalizedName).isEqualTo("/tmp/foo"); assertThat(TestUtils.getPropertyValue(nativeAdapter, "command")).isEqualTo("tail -F -n 6 " + fileName); assertThat(TestUtils.getPropertyValue(nativeAdapter, "taskExecutor")).isSameAs(exec); - assertThat(TestUtils.getPropertyValue(nativeAdapter, "taskScheduler")).isSameAs(sched); + assertThat(TestUtils.getPropertyValue(nativeAdapter, "taskScheduler")).isSameAs(scheduler); assertThat(TestUtils.getPropertyValue(nativeAdapter, "autoStartup", Boolean.class)).isTrue(); assertThat(TestUtils.getPropertyValue(nativeAdapter, "enableStatusReader", Boolean.class)).isFalse(); assertThat(TestUtils.getPropertyValue(nativeAdapter, "phase")).isEqualTo(123); 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 6cd121c82f1..b0b4d1aadf6 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 @@ -23,6 +23,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; @@ -83,8 +84,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(); @@ -93,6 +94,7 @@ public boolean replace(String key, String oldValue, String newValue) { file.delete(); filter.close(); + executorService.shutdown(); } @Override diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/tail/FileTailingMessageProducerTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/tail/FileTailingMessageProducerTests.java index d33ce7e928c..fd031a8bd21 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/tail/FileTailingMessageProducerTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/tail/FileTailingMessageProducerTests.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. @@ -144,8 +144,7 @@ public void testIdleEvent() throws Exception { if (event instanceof FileTailingIdleEvent) { idleCountDownLatch.countDown(); } - if (event instanceof FileTailingEvent) { - FileTailingEvent fileTailingEvent = (FileTailingEvent) event; + if (event instanceof FileTailingEvent fileTailingEvent) { if (fileTailingEvent.getMessage().contains("File not found")) { fileExistCountDownLatch.countDown(); } @@ -184,6 +183,7 @@ public boolean exists() { assertThat(eventRaised).as("idle event did not emit").isTrue(); adapter.stop(); file.delete(); + taskScheduler.destroy(); } private void testGuts(FileTailingMessageProducerSupport adapter, String field) throws Exception { diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/inbound/RotatingServersTests.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/inbound/RotatingServersTests.java index a85254787a4..39deac9eb05 100644 --- a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/inbound/RotatingServersTests.java +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/inbound/RotatingServersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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. @@ -405,8 +405,7 @@ public RotatingServerAdvice advice() { public IntegrationFlow flow() { return IntegrationFlow.from(Ftp.inboundStreamingAdapter(new FtpRemoteFileTemplate(sf())) .filter(new FtpPersistentAcceptOnceFileListFilter(new SimpleMetadataStore(), "rotate")) - .remoteDirectory(".") - .maxFetchSize(1), + .remoteDirectory("."), e -> e.poller(Pollers.fixedDelay(1).advice(advice()))) .channel(MessageChannels.queue("files")) .get(); diff --git a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusIntegrationTests-context.xml b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusIntegrationTests-context.xml index 66ff2b4cc40..e920798e7f8 100644 --- a/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusIntegrationTests-context.xml +++ b/spring-integration-groovy/src/test/java/org/springframework/integration/groovy/GroovyControlBusIntegrationTests-context.xml @@ -19,7 +19,10 @@ - + + + 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/config/HttpIntegrationConfigurationInitializer.java b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpIntegrationConfigurationInitializer.java index f8c14709bf7..d287b1c52a2 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpIntegrationConfigurationInitializer.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpIntegrationConfigurationInitializer.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. @@ -23,8 +23,10 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.integration.config.IntegrationConfigurationInitializer; +import org.springframework.integration.http.inbound.DynamicRequestMappingBeanPostProcessor; import org.springframework.integration.http.inbound.IntegrationRequestMappingHandlerMapping; /** @@ -58,16 +60,25 @@ public void initialize(ConfigurableListableBeanFactory beanFactory) throws Beans *

In addition, checks if the {@code javax.servlet.Servlet} class is present on the classpath. * When Spring Integration HTTP is used only as an HTTP client, there is no reason to use and register * the HTTP server components. + *

+ * Also registers a {@link DynamicRequestMappingBeanPostProcessor} for dynamically added HTTP inbound endpoints. */ private void registerRequestMappingHandlerMappingIfNecessary(BeanDefinitionRegistry registry) { - if (HttpContextUtils.WEB_MVC_PRESENT && - !registry.containsBeanDefinition(HttpContextUtils.HANDLER_MAPPING_BEAN_NAME)) { - BeanDefinitionBuilder requestMappingBuilder = - BeanDefinitionBuilder.genericBeanDefinition(IntegrationRequestMappingHandlerMapping.class) - .addPropertyValue("order", 0) - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - registry.registerBeanDefinition(HttpContextUtils.HANDLER_MAPPING_BEAN_NAME, - requestMappingBuilder.getBeanDefinition()); + if (HttpContextUtils.WEB_MVC_PRESENT) { + if (!registry.containsBeanDefinition(HttpContextUtils.HANDLER_MAPPING_BEAN_NAME)) { + BeanDefinitionBuilder requestMappingBuilder = + BeanDefinitionBuilder.genericBeanDefinition(IntegrationRequestMappingHandlerMapping.class) + .addPropertyValue("order", 0) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(HttpContextUtils.HANDLER_MAPPING_BEAN_NAME, + requestMappingBuilder.getBeanDefinition()); + } + + BeanDefinitionReaderUtils.registerWithGeneratedName( + BeanDefinitionBuilder.genericBeanDefinition(DynamicRequestMappingBeanPostProcessor.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) + .getBeanDefinition(), + registry); } } diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/DynamicRequestMappingBeanPostProcessor.java b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/DynamicRequestMappingBeanPostProcessor.java new file mode 100644 index 00000000000..c82c38ae061 --- /dev/null +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/DynamicRequestMappingBeanPostProcessor.java @@ -0,0 +1,85 @@ +/* + * 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.http.inbound; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; + +/** + * A {@link DestructionAwareBeanPostProcessor} to register request mapping + * created at runtime (e.g. via + * {@link org.springframework.integration.dsl.context.IntegrationFlowContext}) + * by {@link HttpRequestHandlingEndpointSupport} instances + * into the {@link IntegrationRequestMappingHandlerMapping}. + * These mappings are also removed when respective {@link HttpRequestHandlingEndpointSupport} + * bean is destroyed. + * + * @author Artem Bilan + * + * @since 6.2.5 + */ +public class DynamicRequestMappingBeanPostProcessor + implements BeanFactoryAware, DestructionAwareBeanPostProcessor, SmartInitializingSingleton { + + private BeanFactory beanFactory; + + private IntegrationRequestMappingHandlerMapping integrationRequestMappingHandlerMapping; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + @Override + public void afterSingletonsInstantiated() { + this.integrationRequestMappingHandlerMapping = + this.beanFactory.getBean(IntegrationRequestMappingHandlerMapping.class); + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (this.integrationRequestMappingHandlerMapping != null && isHandler(bean.getClass())) { + this.integrationRequestMappingHandlerMapping.detectHandlerMethods(bean); + } + return bean; + } + + @Override + public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { + if (isHandler(bean.getClass())) { + RequestMappingInfo mapping = + this.integrationRequestMappingHandlerMapping.getMappingForEndpoint((BaseHttpInboundEndpoint) bean); + if (mapping != null) { + this.integrationRequestMappingHandlerMapping.unregisterMapping(mapping); + } + } + } + + @Override + public boolean requiresDestruction(Object bean) { + return isHandler(bean.getClass()); + } + + private boolean isHandler(Class beanType) { + return HttpRequestHandlingEndpointSupport.class.isAssignableFrom(beanType); + } + +} diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java index 88eebd38583..83ee5a4b360 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/IntegrationRequestMappingHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2021 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. @@ -26,8 +26,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; @@ -73,11 +71,6 @@ * {@link org.springframework.web.servlet.HandlerMapping} * compromise implementation between method-level annotations and component-level * (e.g. Spring Integration XML) configurations. - *

- * Starting with version 5.1, this class implements {@link DestructionAwareBeanPostProcessor} to - * register HTTP endpoints at runtime for dynamically declared beans, e.g. via - * {@link org.springframework.integration.dsl.context.IntegrationFlowContext}, and unregister - * them during the {@link BaseHttpInboundEndpoint} destruction. *

* This class extends the Spring MVC {@link RequestMappingHandlerMapping} class, inheriting * most of its logic, especially {@link #handleNoMatch(java.util.Set, String, HttpServletRequest)}, @@ -96,7 +89,7 @@ * @see RequestMappingHandlerMapping */ public final class IntegrationRequestMappingHandlerMapping extends RequestMappingHandlerMapping - implements ApplicationListener, DestructionAwareBeanPostProcessor { + implements ApplicationListener { private static final Method HANDLE_REQUEST_METHOD = ReflectionUtils.findMethod(HttpRequestHandler.class, "handleRequest", HttpServletRequest.class, @@ -104,30 +97,6 @@ public final class IntegrationRequestMappingHandlerMapping extends RequestMappin private final AtomicBoolean initialized = new AtomicBoolean(); - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (this.initialized.get() && isHandler(bean.getClass())) { - detectHandlerMethods(bean); - } - - return bean; - } - - @Override - public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { - if (isHandler(bean.getClass())) { - RequestMappingInfo mapping = getMappingForEndpoint((BaseHttpInboundEndpoint) bean); - if (mapping != null) { - unregisterMapping(mapping); - } - } - } - - @Override - public boolean requiresDestruction(Object bean) { - return isHandler(bean.getClass()); - } - @Override protected boolean isHandler(Class beanType) { return HttpRequestHandlingEndpointSupport.class.isAssignableFrom(beanType); @@ -229,7 +198,7 @@ private static CorsConfiguration buildCorsConfiguration(CrossOrigin crossOrigin, * @see RequestMappingHandlerMapping#getMappingForMethod */ @Nullable - private RequestMappingInfo getMappingForEndpoint(BaseHttpInboundEndpoint endpoint) { + RequestMappingInfo getMappingForEndpoint(BaseHttpInboundEndpoint endpoint) { final RequestMapping requestMapping = endpoint.getRequestMapping(); if (ObjectUtils.isEmpty(requestMapping.getPathPatterns())) { 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/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 f8ef6f00c29..0d9e0fcfe16 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 @@ -383,6 +384,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/TcpNetServerConnectionFactory.java b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNetServerConnectionFactory.java index e6b44ef9afe..38109990611 100644 --- a/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNetServerConnectionFactory.java +++ b/spring-integration-ip/src/main/java/org/springframework/integration/ip/tcp/connection/TcpNetServerConnectionFactory.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. @@ -116,7 +116,7 @@ public void run() { } try { setupServerSocket(); - while (true) { + while (isActive()) { acceptConnectionAndExecute(); } } diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserTests-context.xml b/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserTests-context.xml index a82f086a915..7676d7feede 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserTests-context.xml +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserTests-context.xml @@ -399,7 +399,7 @@ client-mode="true" retry-interval="123000" auto-startup="false" - scheduler="sched" /> + scheduler="scheduler" /> - + + + diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests.java index 8f106d27bc6..b4ec8243012 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/config/ParserUnitTests.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. @@ -236,7 +236,7 @@ public class ParserUnitTests { TcpInboundGateway inGatewayClientMode; @Autowired - TaskScheduler sched; + TaskScheduler scheduler; @Autowired @Qualifier("tcpOutClientMode.handler") @@ -624,7 +624,7 @@ public void testInClientMode() { assertThat(dfa.getPropertyValue("clientConnectionFactory")).isSameAs(cfC3); assertThat(dfa.getPropertyValue("serverConnectionFactory")).isNull(); assertThat(dfa.getPropertyValue("isClientMode")).isEqualTo(Boolean.TRUE); - assertThat(dfa.getPropertyValue("taskScheduler")).isSameAs(sched); + assertThat(dfa.getPropertyValue("taskScheduler")).isSameAs(scheduler); assertThat(dfa.getPropertyValue("retryInterval")).isEqualTo(123000L); } @@ -634,7 +634,7 @@ public void testOutClientMode() { assertThat(dfa.getPropertyValue("clientConnectionFactory")).isSameAs(cfC4); assertThat(dfa.getPropertyValue("serverConnectionFactory")).isNull(); assertThat(dfa.getPropertyValue("isClientMode")).isEqualTo(Boolean.TRUE); - assertThat(dfa.getPropertyValue("taskScheduler")).isSameAs(sched); + assertThat(dfa.getPropertyValue("taskScheduler")).isSameAs(scheduler); assertThat(dfa.getPropertyValue("retryInterval")).isEqualTo(124000L); } @@ -644,7 +644,7 @@ public void testInGatewayClientMode() { assertThat(dfa.getPropertyValue("clientConnectionFactory")).isSameAs(cfC5); assertThat(dfa.getPropertyValue("serverConnectionFactory")).isNull(); assertThat(dfa.getPropertyValue("isClientMode")).isEqualTo(Boolean.TRUE); - assertThat(dfa.getPropertyValue("taskScheduler")).isSameAs(sched); + assertThat(dfa.getPropertyValue("taskScheduler")).isSameAs(scheduler); assertThat(dfa.getPropertyValue("retryInterval")).isEqualTo(125000L); } 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 9e5501d08ca..74323443fc1 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; @@ -241,6 +244,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 { @@ -323,8 +368,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()); } @@ -342,8 +388,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()); } @@ -375,6 +422,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/TcpInboundGatewayTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/TcpInboundGatewayTests.java index 0b25cc4cdd7..d242830720e 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/TcpInboundGatewayTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/TcpInboundGatewayTests.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. @@ -193,6 +193,7 @@ public void testNetClientMode() throws Exception { assertThat(done.get()).isTrue(); gateway.stop(); executorService.shutdown(); + taskScheduler.destroy(); } @Test diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/TcpReceivingChannelAdapterTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/TcpReceivingChannelAdapterTests.java index 106fe3774a4..6dd33f0f876 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/TcpReceivingChannelAdapterTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/TcpReceivingChannelAdapterTests.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. @@ -35,7 +35,7 @@ import javax.net.ServerSocketFactory; import javax.net.SocketFactory; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.core.serializer.DefaultDeserializer; @@ -150,6 +150,7 @@ public void testNetClientMode() throws Exception { latch2.countDown(); ccf.stop(); serverSocket.get().close(); + taskScheduler.destroy(); } @Test diff --git a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/TcpSendingMessageHandlerTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/TcpSendingMessageHandlerTests.java index c553b7204c6..92f18072f37 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/TcpSendingMessageHandlerTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/TcpSendingMessageHandlerTests.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. @@ -39,7 +39,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.BeanFactory; @@ -217,11 +217,12 @@ public void testNetCrLfClientMode() throws Exception { adapter.stop(); ccf.stop(); serverSocket.get().close(); + taskScheduler.destroy(); } @Test public void testNioCrLf() throws Exception { - final AtomicReference serverSocket = new AtomicReference(); + final AtomicReference serverSocket = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); final AtomicBoolean done = new AtomicBoolean(); this.executor.execute(() -> { 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/ConnectionFactoryTests.java b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/ConnectionFactoryTests.java index b94a5c71f83..c298fcf994d 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/ConnectionFactoryTests.java +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/ConnectionFactoryTests.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. @@ -222,12 +222,6 @@ public void testObtainConnectionIds(AbstractServerConnectionFactory serverFactor scheduler.shutdown(); } - @Test - public void testEarlyCloseNet() throws Exception { - AbstractServerConnectionFactory factory = new TcpNetServerConnectionFactory(0); - testEarlyClose(factory, "serverSocket", " stopped before accept"); - } - @Test public void testEarlyCloseNio() throws Exception { AbstractServerConnectionFactory factory = new TcpNioServerConnectionFactory(0); 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 5685d263387..1b99e2c0389 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 @@ -297,7 +297,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<>(); @@ -333,14 +333,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(); @@ -532,9 +532,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 1925c3202f6..35d20fdcd4c 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 @@ -29,8 +29,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.context.ApplicationEvent; @@ -45,12 +44,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; @@ -62,9 +61,6 @@ */ public class DeserializationTests { - @Rule - public LongRunningIntegrationTest longRunningIntegrationTest = new LongRunningIntegrationTest(); - @Test public void testReadLength() throws Exception { ServerSocket server = ServerSocketFactory.getDefault().createServerSocket(0); @@ -245,18 +241,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")) { - e.printStackTrace(); - fail("Unexpected IO Error:" + e.getMessage()); - } - } + assertThatIOException() + .isThrownBy(() -> serializer.deserialize(socket.getInputStream())) + .withMessageStartingWith("Read timed out"); server.close(); latch.countDown(); } @@ -270,17 +259,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")) { - e.printStackTrace(); - fail("Unexpected IO Error:" + e.getMessage()); - } - } + serializer.setMaxMessageSize(16); + assertThatIOException() + .isThrownBy(() -> serializer.deserialize(socket.getInputStream())) + .withMessageStartingWith("CRLF not found"); server.close(); latch.countDown(); } @@ -317,7 +299,8 @@ public void deserializationEvents() throws Exception { 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); @@ -389,7 +372,7 @@ public void testTimeoutWhileDecoding(AbstractByteArraySerializer deserializer, S 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/udp/testIp-common-context.xml b/spring-integration-ip/src/test/java/org/springframework/integration/ip/udp/testIp-common-context.xml index 1fd1828d31a..e7d3740517c 100644 --- a/spring-integration-ip/src/test/java/org/springframework/integration/ip/udp/testIp-common-context.xml +++ b/spring-integration-ip/src/test/java/org/springframework/integration/ip/udp/testIp-common-context.xml @@ -21,6 +21,7 @@ + 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 d0a5cd82902..9c67161c9ae 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 @@ -18,6 +18,8 @@ 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; @@ -64,6 +66,7 @@ * @author Artem Bilan * @author Igor Lovich * @author Christian Tzolov + * @author Johannes Edmeier * * @since 6.0 */ @@ -88,6 +91,8 @@ public final class PostgresChannelMessageTableSubscriber implements SmartLifecyc @Nullable private volatile PgConnection connection; + private Duration notificationTimeout = Duration.ofSeconds(60); + /** * Create a new subscriber using the {@link JdbcChannelMessageStore#DEFAULT_TABLE_PREFIX}. * @param connectionSupplier The connection supplier for the targeted Postgres database. @@ -136,6 +141,19 @@ public void setTaskExecutor(AsyncTaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } + /** + * Set the timeout for the notification polling. + * If for the specified duration no notificiation are received the underlying connection is closed and re-established. + * Setting a value of {@code Duration.ZERO} will disable the timeout and wait forever. + * This might cause problems in DB failover scenarios. + * @param notificationTimeout the timeout for the notification polling. + * @since 6.1.8 + */ + public void setNotificationTimeout(Duration notificationTimeout) { + Assert.notNull(notificationTimeout, "'notificationTimeout' must not be null."); + this.notificationTimeout = notificationTimeout; + } + /** * Add a new subscription to this subscriber. * @param subscription The subscription to register. @@ -195,7 +213,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 { @@ -213,24 +231,29 @@ private void doStart(CountDownLatch startingLatch) { while (isActive()) { startingLatch.countDown(); - PGNotification[] notifications = conn.getNotifications(0); + PGNotification[] notifications = conn.getNotifications((int) this.notificationTimeout.toMillis()); // Unfortunately, there is no good way of interrupting a notification // poll but by closing its connection. if (!isActive()) { return; } - if (notifications != null) { - for (PGNotification notification : notifications) { - String parameter = notification.getParameter(); - Set subscriptions = this.subscriptionsMap.get(parameter); - if (subscriptions == null) { - continue; - } - for (Subscription subscription : subscriptions) { - subscription.notifyUpdate(); - } + if ((notifications == null || notifications.length == 0) && !conn.isValid(1)) { + //We did not receive any notifications within the timeout period. + //If the connection is still valid, we will continue polling + //Otherwise, we will close the connection and re-establish it. + break; + } + for (PGNotification notification : notifications) { + String parameter = notification.getParameter(); + Set subscriptions = this.subscriptionsMap.get(parameter); + if (subscriptions == null) { + continue; + } + for (Subscription subscription : subscriptions) { + subscription.notifyUpdate(); } } + } } finally { 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 cb560358611..e442a6d1259 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-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. @@ -16,6 +16,7 @@ package org.springframework.integration.jdbc.lock; +import java.sql.Timestamp; import java.time.Duration; import java.time.LocalDateTime; import java.time.ZoneOffset; @@ -438,11 +439,15 @@ public boolean renew(String lock) { return Boolean.TRUE.equals(result); } - private LocalDateTime ttlEpochMillis() { - return epochMillis().minus(this.ttl); + private Timestamp ttlEpochMillis() { + return Timestamp.valueOf(currentTime().minus(this.ttl)); } - private static LocalDateTime epochMillis() { + private static Timestamp epochMillis() { + return Timestamp.valueOf(currentTime()); + } + + private static LocalDateTime currentTime() { return LocalDateTime.now(ZoneOffset.UTC); } 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 32ae228c508..ec03945c119 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-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. @@ -23,9 +23,11 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.SmartLifecycle; import org.springframework.core.log.LogAccessor; -import org.springframework.dao.DuplicateKeyException; +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); } @@ -247,7 +255,7 @@ private int tryToPutIfAbsent(String key, String value) { ps.setString(5, this.region); // NOSONAR magic number }); } - catch (DuplicateKeyException 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 e1c9b4dc3d9..e01bee4d270 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 @@ -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. @@ -38,7 +38,7 @@ import org.springframework.core.serializer.Deserializer; import org.springframework.core.serializer.Serializer; import org.springframework.core.serializer.support.SerializingConverter; -import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.integration.jdbc.store.channel.ChannelMessageStorePreparedStatementSetter; import org.springframework.integration.jdbc.store.channel.ChannelMessageStoreQueryProvider; import org.springframework.integration.jdbc.store.channel.MessageRowMapper; @@ -473,7 +473,7 @@ public MessageGroup addMessageToGroup(Object groupId, final Message message) ps -> this.preparedStatementSetter.setValues(ps, message, groupId, this.region, this.priorityEnabled)); } - catch (@SuppressWarnings("unused") DuplicateKeyException e) { + catch (@SuppressWarnings("unused") DataIntegrityViolationException ex) { LOGGER.debug(() -> "The Message with id [" + getKey(message.getHeaders().getId()) + "] already exists.\n" + "Ignoring INSERT..."); 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 82% 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 bf88968390d..55f826081b4 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,15 +14,19 @@ * limitations under the License. */ -package org.springframework.integration.jdbc.channel; +package org.springframework.integration.jdbc.postgres; import java.sql.DriverManager; +import java.sql.SQLException; +import java.time.Duration; import java.util.ArrayList; 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.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import javax.sql.DataSource; @@ -40,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; @@ -62,6 +69,7 @@ * @author Artem Bilan * @author Igor Lovich * @author Adama Sorho + * @author Johannes Edmeier * * @since 6.0 */ @@ -102,15 +110,14 @@ CREATE FUNCTION INT_CHANNEL_MESSAGE_NOTIFY_FCT() private String groupId; + private ConnectionSupplier connectionSupplier; + @BeforeEach void setUp(TestInfo testInfo) { // Not initiated as a bean to allow for registrations prior and post the life cycle - this.postgresChannelMessageTableSubscriber = - new PostgresChannelMessageTableSubscriber(() -> - DriverManager.getConnection(POSTGRES_CONTAINER.getJdbcUrl(), - POSTGRES_CONTAINER.getUsername(), - POSTGRES_CONTAINER.getPassword()) - .unwrap(PgConnection.class)); + this.connectionSupplier = new ConnectionSupplier(); + this.postgresChannelMessageTableSubscriber = new PostgresChannelMessageTableSubscriber(connectionSupplier); + this.postgresChannelMessageTableSubscriber.setNotificationTimeout(Duration.ofSeconds(5)); this.taskExecutor = new ThreadPoolTaskExecutor(); @@ -263,6 +270,37 @@ void testRetryOnErrorDuringDispatch(boolean transactionsEnabled) throws Interrup assertThat(payloads).containsExactly("1"); } + @Test + public void testRenewConnection() throws Exception { + CountDownLatch latch = new CountDownLatch(2); + List payloads = new ArrayList<>(); + CountDownLatch connectionLatch = new CountDownLatch(2); + AtomicBoolean connectionCloseState = new AtomicBoolean(); + connectionSupplier.onGetConnection = conn -> { + connectionLatch.countDown(); + if (connectionCloseState.compareAndSet(false, true)) { + try { + conn.close(); + } + catch (Exception e) { + //nop + } + } + }; + postgresChannelMessageTableSubscriber.start(); + postgresSubscribableChannel.subscribe(message -> { + payloads.add(message.getPayload()); + latch.countDown(); + }); + + assertThat(connectionLatch.await(10, TimeUnit.SECONDS)).isTrue(); + + messageStore.addMessageToGroup(groupId, new GenericMessage<>("1")); + messageStore.addMessageToGroup(groupId, new GenericMessage<>("2")); + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(payloads).containsExactlyInAnyOrder("1", "2"); + } + @Configuration @EnableIntegration public static class Config { @@ -302,4 +340,22 @@ public JdbcChannelMessageStore jdbcChannelMessageStore(DataSource dataSource) { } + private static class ConnectionSupplier implements PgConnectionSupplier { + + Consumer onGetConnection; + + @Override + public PgConnection get() throws SQLException { + var conn = DriverManager.getConnection(POSTGRES_CONTAINER.getJdbcUrl(), + POSTGRES_CONTAINER.getUsername(), + POSTGRES_CONTAINER.getPassword()) + .unwrap(PgConnection.class); + if (this.onGetConnection != null) { + this.onGetConnection.accept(conn); + } + return conn; + } + + } + } 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/test/java/org/springframework/integration/jms/JmsOutboundGatewayTests.java b/spring-integration-jms/src/test/java/org/springframework/integration/jms/JmsOutboundGatewayTests.java index 2760f471058..b5c01190007 100644 --- a/spring-integration-jms/src/test/java/org/springframework/integration/jms/JmsOutboundGatewayTests.java +++ b/spring-integration-jms/src/test/java/org/springframework/integration/jms/JmsOutboundGatewayTests.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. @@ -149,6 +149,7 @@ public void testReplyContainerRecovery() throws Exception { finally { gateway.stop(); exec.shutdownNow(); + taskScheduler.destroy(); } } diff --git a/spring-integration-jms/src/test/java/org/springframework/integration/jms/dsl/JmsTests.java b/spring-integration-jms/src/test/java/org/springframework/integration/jms/dsl/JmsTests.java index 19066acea23..ca919851ef1 100644 --- a/spring-integration-jms/src/test/java/org/springframework/integration/jms/dsl/JmsTests.java +++ b/spring-integration-jms/src/test/java/org/springframework/integration/jms/dsl/JmsTests.java @@ -522,6 +522,7 @@ public Message preSend(Message message, MessageChannel channel) { public IntegrationFlow jmsMessageDrivenRedeliveryFlow() { return IntegrationFlow .from(Jms.messageDrivenChannelAdapter(amqFactory) + .role("someRole") .errorChannel("errorChannelForRedelivery") .destination("jmsMessageDrivenRedelivery") .configureListenerContainer(c -> c diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/lifecycle-source.xml b/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/lifecycle-source.xml index f3e5f614445..5883f8564e3 100644 --- a/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/lifecycle-source.xml +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/lifecycle-source.xml @@ -1,52 +1,56 @@ + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> - + - + - + - + - + - + - + - + - + + + - + - + - - + + - - + + - + diff --git a/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/self-destruction-context.xml b/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/self-destruction-context.xml index e2fd9b34693..cbbfcef7999 100644 --- a/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/self-destruction-context.xml +++ b/spring-integration-jmx/src/test/java/org/springframework/integration/monitor/self-destruction-context.xml @@ -25,7 +25,10 @@ - + + + diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/HibernateJpaOperationsTests.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/HibernateJpaOperationsTests.java index 46e4a9477cd..78436dd4ebe 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/HibernateJpaOperationsTests.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/core/HibernateJpaOperationsTests.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. @@ -207,7 +207,6 @@ public void testMerge() { entityManager.flush(); assertThat(savedStudent).isNotNull(); assertThat(savedStudent.getRollNumber()).isNotNull(); - assertThat(student.getRollNumber()).isEqualTo(savedStudent.getRollNumber()); assertThat(student != savedStudent).isTrue(); } diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/dsl/JpaTests.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/dsl/JpaTests.java index 8dc50ddd719..dfff5b01675 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/dsl/JpaTests.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/dsl/JpaTests.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. @@ -166,7 +166,6 @@ public void testUpdatingGatewayFlow() { StudentDomain mergedStudent = (StudentDomain) receive.getPayload(); assertThat(mergedStudent.getFirstName()).isEqualTo(student.getFirstName()); assertThat(mergedStudent.getRollNumber()).isNotNull(); - assertThat(student.getRollNumber()).isEqualTo(mergedStudent.getRollNumber()); } @Test diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundChannelAdapterTests.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundChannelAdapterTests.java index a6a28bc9643..d597160fe78 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundChannelAdapterTests.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundChannelAdapterTests.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. @@ -102,7 +102,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { List results2 = this.jdbcTemplate.queryForList("Select * from Student"); assertThat(results2).hasSize(4); - assertThat(testStudent.getRollNumber()).isNotNull(); + assertThat(results2.get(0)).extracting("rollNumber").isNotNull(); } @Test @@ -134,7 +134,7 @@ protected void doInTransactionWithoutResult(TransactionStatus status) { List results2 = this.jdbcTemplate.queryForList("Select * from Student"); assertThat(results2).hasSize(4); - assertThat(testStudent.getRollNumber()).isNotNull(); + assertThat(results2.get(0)).extracting("rollNumber").isNotNull(); } @Test diff --git a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundChannelAdapterTransactionalTests.java b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundChannelAdapterTransactionalTests.java index 32a5f818126..f618ccf217c 100644 --- a/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundChannelAdapterTransactionalTests.java +++ b/spring-integration-jpa/src/test/java/org/springframework/integration/jpa/outbound/JpaOutboundChannelAdapterTransactionalTests.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. @@ -72,7 +72,7 @@ public void saveEntityWithTransaction() { .queryForList("Select * from Student"); assertThat(results2).hasSize(4); - assertThat(testStudent.getRollNumber()).isNotNull(); + assertThat(results2.get(0)).extracting("rollNumber").isNotNull(); } } 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..a85c0b51ec0 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-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. @@ -67,6 +67,7 @@ * @author Artem Bilan * @author Dominik Simmen * @author Yuxin Wang + * @author Filip Hrisafov */ public abstract class AbstractMailReceiver extends IntegrationObjectSupport implements MailReceiver, DisposableBean { @@ -502,18 +503,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 eb301b1f777..ed9fa1fb48c 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 d7167bb696e..42cdeed5ee0 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()); } @@ -442,7 +453,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 @@ -983,6 +994,30 @@ 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 { @@ -1019,4 +1054,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-mongodb/src/main/java/org/springframework/integration/mongodb/store/AbstractConfigurableMongoDbMessageStore.java b/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/store/AbstractConfigurableMongoDbMessageStore.java index 518fb85f2dd..d7cbd4259f6 100644 --- a/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/store/AbstractConfigurableMongoDbMessageStore.java +++ b/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/store/AbstractConfigurableMongoDbMessageStore.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,7 +30,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.FindAndModifyOptions; @@ -237,7 +237,7 @@ protected void addMessageDocument(final MessageDocument document) { try { this.mongoTemplate.insert(document, this.collectionName); } - catch (DuplicateKeyException e) { + catch (DataIntegrityViolationException e) { if (this.logger.isDebugEnabled()) { this.logger.debug("The Message with id [" + document.getMessageId() + "] already exists.\n" + "Ignoring INSERT and SELECT existing..."); diff --git a/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/store/MongoDbMessageStore.java b/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/store/MongoDbMessageStore.java index 4a29fa15443..a3222ebac5f 100644 --- a/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/store/MongoDbMessageStore.java +++ b/spring-integration-mongodb/src/main/java/org/springframework/integration/mongodb/store/MongoDbMessageStore.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. @@ -16,6 +16,7 @@ package org.springframework.integration.mongodb.store; +import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -25,7 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Properties; import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -36,6 +36,7 @@ import org.bson.conversions.Bson; import org.bson.types.Binary; +import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanClassLoaderAware; @@ -342,7 +343,7 @@ public void removeMessagesFromGroup(Object groupId, Collection> messa ids.clear(); } } - if (ids.size() > 0) { + if (!ids.isEmpty()) { bulkRemove(groupId, ids); } updateGroup(groupId, lastModifiedUpdate()); @@ -573,6 +574,7 @@ void setCustomConverters(Object... customConverters) { public void afterPropertiesSet() { List converters = new ArrayList<>(); converters.add(new MessageHistoryToDocumentConverter()); + converters.add(new DocumentToMessageHistoryConverter()); converters.add(new DocumentToGenericMessageConverter()); converters.add(new DocumentToMutableMessageConverter()); DocumentToErrorMessageConverter docToErrorMessageConverter = new DocumentToErrorMessageConverter(); @@ -720,30 +722,55 @@ private Object extractPayload(Bson source) { @WritingConverter - private static class MessageHistoryToDocumentConverter implements Converter { + private static final class MessageHistoryToDocumentConverter implements Converter { MessageHistoryToDocumentConverter() { } @Override public Document convert(MessageHistory source) { - BasicDBList dbList = new BasicDBList(); - for (Properties properties : source) { - Document historyProperty = new Document() - .append(MessageHistory.NAME_PROPERTY, properties.getProperty(MessageHistory.NAME_PROPERTY)) - .append(MessageHistory.TYPE_PROPERTY, properties.getProperty(MessageHistory.TYPE_PROPERTY)) - .append(MessageHistory.TIMESTAMP_PROPERTY, - properties.getProperty(MessageHistory.TIMESTAMP_PROPERTY)); - dbList.add(historyProperty); - } - return new Document("components", dbList) + return new Document("components", source) .append("_class", MessageHistory.class.getName()); } } @ReadingConverter - private class DocumentToGenericMessageConverter implements Converter> { + private static final class DocumentToMessageHistoryConverter implements Converter { + + private static final Constructor MESSAGE_HISTORY_CONSTRUCTOR; + + static { + try { + MESSAGE_HISTORY_CONSTRUCTOR = MessageHistory.class.getDeclaredConstructor(List.class); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException(ex); + } + } + + DocumentToMessageHistoryConverter() { + } + + @Override + @SuppressWarnings("unchecked") + public MessageHistory convert(Document source) { + List components = (List) source.get("components"); + List historyEntries = new ArrayList<>(components.size()); + for (Document component : components) { + MessageHistory.Entry entry = new MessageHistory.Entry(); + for (Entry componentEntry : component.entrySet()) { + entry.setProperty(componentEntry.getKey(), componentEntry.getValue().toString()); + } + historyEntries.add(entry); + } + return BeanUtils.instantiateClass(MESSAGE_HISTORY_CONSTRUCTOR, historyEntries); + } + + } + + @ReadingConverter + private final class DocumentToGenericMessageConverter implements Converter> { DocumentToGenericMessageConverter() { } @@ -783,7 +810,7 @@ public MutableMessage convert(Document source) { } @ReadingConverter - private class DocumentToAdviceMessageConverter implements Converter> { + private final class DocumentToAdviceMessageConverter implements Converter> { DocumentToAdviceMessageConverter() { } @@ -820,7 +847,7 @@ public AdviceMessage convert(Document source) { } @ReadingConverter - private class DocumentToErrorMessageConverter implements Converter { + private final class DocumentToErrorMessageConverter implements Converter { private final AllowListDeserializingConverter deserializingConverter = new AllowListDeserializingConverter(); @@ -843,7 +870,7 @@ public ErrorMessage convert(Document source) { } @WritingConverter - private static class ThrowableToBytesConverter implements Converter { + private static final class ThrowableToBytesConverter implements Converter { private final Converter serializingConverter = new SerializingConverter(); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/aot/MqttRuntimeHints.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/aot/MqttRuntimeHints.java new file mode 100644 index 00000000000..e591b80378c --- /dev/null +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/aot/MqttRuntimeHints.java @@ -0,0 +1,57 @@ +/* + * 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.mqtt.aot; + +import java.util.stream.Stream; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link RuntimeHintsRegistrar} for Spring Integration MQTT module. + * + * @author Artem Bilan + * + * @since 6.1.9 + */ +class MqttRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + ReflectionHints reflectionHints = hints.reflection(); + // TODO until the real fix in Paho library. + Stream.of("org.eclipse.paho.client.mqttv3.MqttAsyncClient", "org.eclipse.paho.mqttv5.client.MqttAsyncClient") + .filter((typeName) -> ClassUtils.isPresent(typeName, classLoader)) + .map((typeName) -> loadClassByName(typeName, classLoader)) + .flatMap((type) -> Stream.ofNullable(ReflectionUtils.findMethod(type, "stopReconnectCycle"))) + .forEach(method -> reflectionHints.registerMethod(method, ExecutableMode.INVOKE)); + } + + private static Class loadClassByName(String typeName, ClassLoader classLoader) { + try { + return ClassUtils.forName(typeName, classLoader); + } + catch (ClassNotFoundException ex) { + throw new IllegalArgumentException(ex); + } + } + +} diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/aot/package-info.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/aot/package-info.java new file mode 100644 index 00000000000..69f0b3b5aeb --- /dev/null +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/aot/package-info.java @@ -0,0 +1,6 @@ +/** + * Provides classes to support Spring AOT. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.integration.mqtt.aot; diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java index 6a9a0249e14..8fb3f991827 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv3ClientManager.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. @@ -26,6 +26,7 @@ import org.eclipse.paho.client.mqttv3.MqttMessage; import org.springframework.integration.mqtt.event.MqttConnectionFailedEvent; +import org.springframework.integration.mqtt.support.MqttUtils; import org.springframework.util.Assert; /** @@ -149,6 +150,9 @@ public void stop() { } try { client.disconnectForcibly(getDisconnectCompletionTimeout()); + if (getConnectionInfo().isAutomaticReconnect()) { + MqttUtils.stopClientReconnectCycle(client); + } } catch (MqttException e) { logger.error("Could not disconnect from the client", e); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java index a89b34aa609..36bc028b70a 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/core/Mqttv5ClientManager.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. @@ -28,6 +28,7 @@ import org.eclipse.paho.mqttv5.common.packet.MqttProperties; import org.springframework.integration.mqtt.event.MqttConnectionFailedEvent; +import org.springframework.integration.mqtt.support.MqttUtils; import org.springframework.util.Assert; /** @@ -151,6 +152,9 @@ public void stop() { try { client.disconnectForcibly(getDisconnectCompletionTimeout()); + if (getConnectionInfo().isAutomaticReconnect()) { + MqttUtils.stopClientReconnectCycle(client); + } } catch (MqttException e) { logger.error("Could not disconnect from the client", e); diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java index f28df29eb99..0e3ca3e20d6 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/inbound/MqttPahoMessageDrivenChannelAdapter.java @@ -228,6 +228,9 @@ protected void doStop() { try { this.client.disconnectForcibly(getDisconnectCompletionTimeout()); + if (getConnectionInfo().isAutomaticReconnect()) { + MqttUtils.stopClientReconnectCycle(this.client); + } } catch (MqttException ex) { logger.error(ex, "Exception while disconnecting"); 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 b589da35fc6..6599c4e6490 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 @@ -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. @@ -17,13 +17,14 @@ package org.springframework.integration.mqtt.inbound; import java.util.Arrays; +import java.util.ConcurrentModificationException; +import java.util.List; import java.util.Map; 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; @@ -49,6 +50,7 @@ import org.springframework.integration.mqtt.support.MqttHeaderMapper; import org.springframework.integration.mqtt.support.MqttHeaders; import org.springframework.integration.mqtt.support.MqttMessageConverter; +import org.springframework.integration.mqtt.support.MqttUtils; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; @@ -83,7 +85,7 @@ public class Mqttv5PahoMessageDrivenChannelAdapter extends AbstractMqttMessageDrivenChannelAdapter implements MqttCallback, MqttComponent { - private final Lock lock = new ReentrantLock(); + private final Lock lock = new ReentrantLock(); private final MqttConnectionOptions connectionOptions; @@ -239,13 +241,16 @@ 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; } if (getClientManager() == null) { this.mqttClient.disconnectForcibly(getDisconnectCompletionTimeout()); + if (getConnectionInfo().isAutomaticReconnect()) { + MqttUtils.stopClientReconnectCycle(this.mqttClient); + } } } } @@ -257,12 +262,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) { @@ -276,7 +291,11 @@ public void addTopic(String topic, int qos) { try { super.addTopic(topic, qos); if (this.mqttClient != null && this.mqttClient.isConnected()) { - this.mqttClient.subscribe(new MqttSubscription(topic, qos), this::messageArrived) + MqttProperties subscriptionProperties = new MqttProperties(); + // Make use of mqttSession.getNextSubscriptionIdentifier() if available in connection + subscriptionProperties.setSubscriptionIdentifiers(List.of(0)); + this.mqttClient.subscribe(new MqttSubscription[] {new MqttSubscription(topic, qos)}, + null, null, this::messageArrived, subscriptionProperties) .waitForCompletion(getCompletionTimeout()); } } @@ -293,7 +312,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); } @@ -401,14 +420,13 @@ private void subscribe() { } int[] requestedQos = getQos(); - MqttSubscription[] subscriptions = IntStream.range(0, topics.length) + MqttSubscription[] mqttSubscriptions = IntStream.range(0, topics.length) .mapToObj(i -> new MqttSubscription(topics[i], requestedQos[i])) .toArray(MqttSubscription[]::new); - IMqttMessageListener listener = this::messageArrived; - IMqttMessageListener[] listeners = IntStream.range(0, topics.length) - .mapToObj(t -> listener) - .toArray(IMqttMessageListener[]::new); - this.mqttClient.subscribe(subscriptions, null, null, listeners, null) + MqttProperties subscriptionProperties = new MqttProperties(); + // 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(topics); logger.debug(message); @@ -439,7 +457,6 @@ private static String obtainServerUrlFromOptions(MqttConnectionOptions connectio return serverURIs[0]; } - /** * Used to complete message arrival when {@link #isManualAcks()} is true. */ diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java index 317a9b9604a..3a565d76cc4 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/outbound/MqttPahoMessageHandler.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. @@ -176,6 +176,9 @@ protected void doStop() { IMqttAsyncClient theClient = this.client; if (theClient != null) { theClient.disconnect().waitForCompletion(getDisconnectCompletionTimeout()); + if (getConnectionInfo().isAutomaticReconnect()) { + MqttUtils.stopClientReconnectCycle(theClient); + } theClient.close(); this.client = null; } 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 6f62a04764c..680907b5808 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 @@ -42,6 +42,7 @@ import org.springframework.integration.mqtt.event.MqttProtocolErrorEvent; import org.springframework.integration.mqtt.support.MqttHeaderMapper; import org.springframework.integration.mqtt.support.MqttMessageConverter; +import org.springframework.integration.mqtt.support.MqttUtils; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandlingException; @@ -185,6 +186,9 @@ protected void doStop() { try { if (getClientManager() == null) { this.mqttClient.disconnect().waitForCompletion(getDisconnectCompletionTimeout()); + if (getConnectionInfo().isAutomaticReconnect()) { + MqttUtils.stopClientReconnectCycle(this.mqttClient); + } } } catch (MqttException ex) { @@ -263,7 +267,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/main/java/org/springframework/integration/mqtt/support/MqttHeaderMapper.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/support/MqttHeaderMapper.java index 528d4990ffa..61308dfef4b 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/support/MqttHeaderMapper.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/support/MqttHeaderMapper.java @@ -122,13 +122,13 @@ private static boolean shouldMapHeader(String headerName, String[] patterns) { if (patterns != null && patterns.length > 0) { for (String pattern : patterns) { if (PatternMatchUtils.simpleMatch(pattern, headerName)) { - LOGGER.debug(LogMessage.format("headerName=[{0}] WILL be mapped, matched pattern={1}", + LOGGER.debug(LogMessage.format("headerName=[%s] WILL be mapped, matched pattern=%s", headerName, pattern)); return true; } } } - LOGGER.debug(LogMessage.format("headerName=[{0}] WILL NOT be mapped", headerName)); + LOGGER.debug(LogMessage.format("headerName=[%s] WILL NOT be mapped", headerName)); return false; } diff --git a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/support/MqttUtils.java b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/support/MqttUtils.java index b9bb5bba066..4621de15497 100644 --- a/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/support/MqttUtils.java +++ b/spring-integration-mqtt/src/main/java/org/springframework/integration/mqtt/support/MqttUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 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. @@ -16,9 +16,13 @@ package org.springframework.integration.mqtt.support; +import java.lang.reflect.Method; + import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.springframework.beans.BeanUtils; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; /** * MQTT Utilities. @@ -30,6 +34,38 @@ */ public final class MqttUtils { + private static final boolean PAHO_MQTTV3_PRESENT = + ClassUtils.isPresent("org.eclipse.paho.client.mqttv3.MqttAsyncClient", null); + + private static final boolean PAHO_MQTTV5_PRESENT = + ClassUtils.isPresent("org.eclipse.paho.mqttv5.client.MqttAsyncClient", null); + + private static final Method V3_STOP_RECONNECT_CYCLE_METHOD; + + private static final Method V5_STOP_RECONNECT_CYCLE_METHOD; + + static { + if (PAHO_MQTTV3_PRESENT) { + V3_STOP_RECONNECT_CYCLE_METHOD = + ReflectionUtils.findMethod(org.eclipse.paho.client.mqttv3.MqttAsyncClient.class, + "stopReconnectCycle"); + ReflectionUtils.makeAccessible(V3_STOP_RECONNECT_CYCLE_METHOD); + } + else { + V3_STOP_RECONNECT_CYCLE_METHOD = null; + } + + if (PAHO_MQTTV5_PRESENT) { + V5_STOP_RECONNECT_CYCLE_METHOD = + ReflectionUtils.findMethod(org.eclipse.paho.mqttv5.client.MqttAsyncClient.class, + "stopReconnectCycle"); + ReflectionUtils.makeAccessible(V5_STOP_RECONNECT_CYCLE_METHOD); + } + else { + V5_STOP_RECONNECT_CYCLE_METHOD = null; + } + } + private MqttUtils() { } @@ -47,4 +83,26 @@ public static MqttConnectOptions cloneConnectOptions(MqttConnectOptions options) return options2; } + /** + * Perform a {@code stopReconnectCycle()} (via reflection) method on the provided client + * to clean up resources on client stop. + * TODO until the real fix in Paho library. + * @param client the MQTTv3 Paho client instance. + * @since 6.1.9 + */ + public static void stopClientReconnectCycle(org.eclipse.paho.client.mqttv3.IMqttAsyncClient client) { + ReflectionUtils.invokeMethod(V3_STOP_RECONNECT_CYCLE_METHOD, client); + } + + /** + * Perform a {@code stopReconnectCycle()} (via reflection) method on the provided client + * to clean up resources on client stop. + * TODO until the real fix in Paho library. + * @param client the MQTTv5 Paho client instance. + * @since 6.1.9 + */ + public static void stopClientReconnectCycle(org.eclipse.paho.mqttv5.client.IMqttAsyncClient client) { + ReflectionUtils.invokeMethod(V5_STOP_RECONNECT_CYCLE_METHOD, client); + } + } diff --git a/spring-integration-mqtt/src/main/resources/META-INF/spring/aot.factories b/spring-integration-mqtt/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000000..a7392134fd3 --- /dev/null +++ b/spring-integration-mqtt/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.integration.mqtt.aot.MqttRuntimeHints diff --git a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java index 501f37ef659..8d14687a0ca 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/MqttAdapterTests.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. @@ -488,8 +488,8 @@ public void testDifferentQos() throws Exception { given(token.getGrantedQos()).willReturn(new int[] {2, 0}); willReturn(token).given(client).subscribe(any(String[].class), any(int[].class), any()); - MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("foo", "bar", factory, - "baz", "fix"); + MqttPahoMessageDrivenChannelAdapter adapter = + new MqttPahoMessageDrivenChannelAdapter("tcp://mqtt.host", "bar", factory, "baz", "fix"); AtomicReference method = new AtomicReference<>(); ReflectionUtils.doWithMethods(MqttPahoMessageDrivenChannelAdapter.class, m -> { m.setAccessible(true); 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 1e582ffb2e1..78699e9f75d 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 @@ -58,7 +58,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)); } @@ -72,11 +72,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"}); @@ -86,9 +87,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/Mqttv5BackToBackTests.java b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/Mqttv5BackToBackTests.java index 427ec5efbe5..c9bf5d1f328 100644 --- a/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/Mqttv5BackToBackTests.java +++ b/spring-integration-mqtt/src/test/java/org/springframework/integration/mqtt/Mqttv5BackToBackTests.java @@ -74,6 +74,9 @@ public class Mqttv5BackToBackTests implements MosquittoContainerTest { @Autowired private Config config; + @Autowired + private Mqttv5PahoMessageDrivenChannelAdapter mqttv5MessageDrivenChannelAdapter; + @Test //GH-3732 public void testNoNpeIsNotThrownInCaseDoInitIsNotInvokedBeforeTopicAddition() { Mqttv5PahoMessageDrivenChannelAdapter channelAdapter = new Mqttv5PahoMessageDrivenChannelAdapter("tcp://mock-url.com:8091", "mock-client-id", "123"); @@ -115,6 +118,36 @@ public void testSimpleMqttv5Interaction() { .hasAtLeastOneElementOfType(MqttMessageSentEvent.class) .hasAtLeastOneElementOfType(MqttMessageDeliveredEvent.class) .hasAtLeastOneElementOfType(MqttSubscribedEvent.class); + + this.mqttv5MessageDrivenChannelAdapter.addTopic("anotherTopic"); + + testPayload = "another payload"; + + this.mqttOutFlowInput.send( + MessageBuilder.withPayload(testPayload) + .setHeader(MqttHeaders.TOPIC, "anotherTopic") + .build()); + + receive = this.fromMqttChannel.receive(10_000); + + assertThat(receive).isNotNull(); + assertThat(receive.getPayload()).isEqualTo(testPayload); + } + + @Test + public void testSharedTopicMqttv5Interaction() { + this.mqttv5MessageDrivenChannelAdapter.addTopic("$share/group/testTopic"); + + String testPayload = "shared topic payload"; + this.mqttOutFlowInput.send( + MessageBuilder.withPayload(testPayload) + .setHeader(MqttHeaders.TOPIC, "testTopic") + .build()); + + Message receive = this.fromMqttChannel.receive(10_000); + + assertThat(receive).isNotNull(); + assertThat(receive.getPayload()).isEqualTo(testPayload); } 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 729b855fb2b..c3c6bbd7e38 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 @@ -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. @@ -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,8 @@ * @author Eddie Cho * @author Myeonghyeon Lee * @author Roman Zabaluev + * @author Alex Peelman + * @author Oleksandr Ichanskyi * * @since 4.0 * @@ -98,8 +101,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) { @@ -198,8 +205,6 @@ private void setupUnlockMessageListener(RedisConnectionFactory connectionFactory public void setExecutor(Executor executor) { this.executor = executor; this.executorExplicitlySet = true; - this.redisMessageListenerContainer.setTaskExecutor(this.executor); - this.redisMessageListenerContainer.setSubscriptionExecutor(this.executor); } /** @@ -211,6 +216,16 @@ 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.2.10 + */ + public void setIdleBetweenTries(Duration idleBetweenTries) { + Assert.notNull(idleBetweenTries, "'idleBetweenTries' must not be null"); + this.idleBetweenTries = idleBetweenTries; + } /** * Set {@link RedisLockType} mode to work in. @@ -282,7 +297,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, @@ -660,7 +675,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 @@ -745,7 +760,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; } @@ -753,7 +768,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 1da003ac884..ec33d8b74a9 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 { @@ -317,7 +320,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"); } @@ -401,11 +404,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); @@ -418,22 +427,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()); @@ -486,43 +494,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 971a496fe8b..32e234dc4eb 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 @@ -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. @@ -41,6 +41,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -52,6 +53,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.Mockito.mock; /** * @author Gary Russell @@ -225,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(); @@ -246,6 +250,7 @@ void testTwoThreadsSecondFailsToGetLock(RedisLockType testRedisLockType) throws registry.expireUnusedOlderThan(-1000); assertThat(getRedisLockRegistryLocks(registry)).isEmpty(); registry.destroy(); + executorService.shutdown(); } @ParameterizedTest @@ -260,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(); @@ -286,6 +292,7 @@ void testTwoThreads(RedisLockType testRedisLockType) throws Exception { registry.expireUnusedOlderThan(-1000); assertThat(getRedisLockRegistryLocks(registry)).isEmpty(); registry.destroy(); + executorService.shutdown(); } @ParameterizedTest @@ -302,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(); @@ -337,6 +345,7 @@ void testTwoThreadsDifferentRegistries(RedisLockType testRedisLockType) throws E assertThat(getRedisLockRegistryLocks(registry2)).isEmpty(); registry1.destroy(); registry2.destroy(); + executorService.shutdown(); } @ParameterizedTest @@ -348,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(); } @@ -367,6 +377,7 @@ void testTwoThreadsWrongOneUnlocks(RedisLockType testRedisLockType) throws Excep registry.expireUnusedOlderThan(-1000); assertThat(getRedisLockRegistryLocks(registry)).isEmpty(); registry.destroy(); + executorService.shutdown(); } @ParameterizedTest @@ -479,7 +490,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; @@ -488,6 +500,7 @@ void testExpireNotChanged(RedisLockType testRedisLockType) throws Exception { assertThat(getExpire(registry, "foo")).isEqualTo(expire); lock.unlock(); registry.destroy(); + executorService.shutdown(); } @ParameterizedTest @@ -875,6 +888,13 @@ void testTwoThreadsRemoveAndObtainSameLockSimultaneously(RedisLockType testRedis registry.destroy(); } + @Test + void testInitialiseWithCustomExecutor() { + RedisLockRegistry redisLockRegistry = new RedisLockRegistry(redisConnectionFactory, "registryKey"); + redisLockRegistry.setRedisLockType(RedisLockType.PUB_SUB_LOCK); + assertThatNoException().isThrownBy(() -> redisLockRegistry.setExecutor(mock())); + } + private Long getExpire(RedisLockRegistry registry, String lockKey) { StringRedisTemplate template = createTemplate(); String registryKey = TestUtils.getPropertyValue(registry, "registryKey", String.class); 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..c38c1ddfc0a 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. @@ -49,6 +49,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 +75,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(); @@ -421,6 +423,13 @@ 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(); + } + } + /** * 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 80883331af7..94d1af1d4b1 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-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. @@ -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,6 +48,7 @@ * @author Gary Russell * @author Artem Bilan * @author Christian Tzolov + * @author Darryl Smith * @since 2.0 */ public class SftpSession implements Session { @@ -107,23 +109,24 @@ public Stream doList(String path) throws IOException { remoteDir = remotePath; } } - remoteDir = - !remoteDir.isEmpty() && remoteDir.charAt(0) == '/' - ? remoteDir - : this.sftpClient.canonicalPath(remoteDir); + remoteDir = normalizePath(remoteDir); return StreamSupport.stream(this.sftpClient.readDir(remoteDir).spliterator(), false) .filter((entry) -> !isPattern || PatternMatchUtils.simpleMatch(remoteFile, entry.getFilename())); } @Override public void read(String source, OutputStream os) throws IOException { - InputStream is = this.sftpClient.read(source); + InputStream is = readRaw(source); FileCopyUtils.copy(is, os); } @Override public InputStream readRaw(String source) throws IOException { - return this.sftpClient.read(source); + return this.sftpClient.read(normalizePath(source)); + } + + private String normalizePath(String path) throws IOException { + return !path.isEmpty() && path.charAt(0) == '/' ? path : this.sftpClient.canonicalPath(path); } @Override @@ -155,6 +158,16 @@ public void 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 @@ -188,7 +201,7 @@ public boolean rmdir(String remoteDirectory) throws IOException { @Override public boolean exists(String path) { try { - this.sftpClient.lstat(path); + this.sftpClient.lstat(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 9828fe7ad4c..b60098c4a4f 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,13 +72,16 @@ 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") + .remoteDirectory("/sftpSource") .regexFilter(".*\\.txt$") .localFilenameExpression("#this.toUpperCase() + '.a'") .localDirectory(getTargetLocalDirectory()) @@ -91,6 +97,7 @@ public void testSftpInboundFlow() { File file = (File) payload; assertThat(file.getName()).isEqualTo(" SFTPSOURCE1.TXT.a"); assertThat(file.getAbsolutePath()).contains("localTarget"); + assertThat(message.getHeaders()).containsEntry(FileHeaders.REMOTE_DIRECTORY, "/sftpSource"); message = out.receive(10_000); assertThat(message).isNotNull(); @@ -105,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))) @@ -132,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")); @@ -142,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); @@ -153,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 + "']") @@ -174,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 +207,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 +218,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 +237,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 +266,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")); @@ -278,7 +285,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); @@ -302,6 +309,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 f425c91191b..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 @@ -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. @@ -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 { @@ -193,8 +196,11 @@ public SftpSession getSession() { String[] files = new File("remote-test-dir").list(); for (String fileName : files) { - when(sftpClient.read("remote-test-dir/" + fileName)) - .thenReturn(new FileInputStream("remote-test-dir/" + fileName)); + String remoteFilePath = "remote-test-dir/" + fileName; + when(sftpClient.canonicalPath(remoteFilePath)) + .thenReturn("/" + remoteFilePath); + when(sftpClient.read("/" + remoteFilePath)) + .thenReturn(new FileInputStream(remoteFilePath)); } when(sftpClient.readDir("/remote-test-dir")).thenReturn(this.sftpEntries); 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 928df96cc0c..6ebf61fd32a 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-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. @@ -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(); } } @@ -260,17 +269,25 @@ public void testSharedSession(boolean sharedSession) throws IOException { public void testExists() throws IOException { SftpClient sftpClient = mock(SftpClient.class); + willReturn("/exist") + .given(sftpClient) + .canonicalPath("exist"); + + willReturn("/notExist") + .given(sftpClient) + .canonicalPath("notExist"); + willReturn(new SftpClient.Attributes()) .given(sftpClient) - .lstat(eq("exist")); + .lstat("/exist"); willThrow(new SftpException(SftpConstants.SSH_FX_NO_SUCH_FILE, "notExist")) .given(sftpClient) - .lstat(eq("notExist")); + .lstat("/notExist"); willThrow(new SshException(SshConstants.SSH_OPEN_CONNECT_FAILED, "Connection lost.")) .given(sftpClient) - .lstat(and(not(eq("exist")), not(eq("notExist")))); + .lstat(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.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java index 2af7fbb44eb..a1debf3dc9f 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.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. @@ -28,6 +28,7 @@ import java.util.ArrayList; 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.regex.Matcher; @@ -86,6 +87,7 @@ /** * @author Artem Bilan * @author Gary Russell + * @author Darryl Smith * * @since 3.0 */ @@ -425,7 +427,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 +437,7 @@ public void testInt3047ConcurrentSharedSession() throws Exception { } latch1.countDown(); }); - Executors.newSingleThreadExecutor().execute(() -> { + executorService.execute(() -> { try { session2.write(pipe2, "bar.txt"); } @@ -464,6 +467,7 @@ public void testInt3047ConcurrentSharedSession() throws Exception { session2.remove("bar.txt"); session1.close(); session2.close(); + executorService.shutdown(); } @Test @@ -684,7 +688,7 @@ private void assertLength6(SftpRemoteFileTemplate template) { } @Test - public void testSessionExists() throws IOException { + public void testSessionExists() throws Exception { DefaultSftpSessionFactory sessionFactory = new DefaultSftpSessionFactory(); sessionFactory.setHost("localhost"); sessionFactory.setPort(port); @@ -701,7 +705,9 @@ public void testSessionExists() throws IOException { assertThatExceptionOfType(UncheckedIOException.class) .isThrownBy(() -> session.exists("any")) .withRootCauseInstanceOf(IOException.class) - .withStackTraceContaining("lstat(any) client is closed"); + .withStackTraceContaining("canonicalPath(any) client is closed"); + + sessionFactory.destroy(); } @SuppressWarnings("unused") 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..9511f52c555 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-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. @@ -54,6 +54,7 @@ /** * @author Gary Russell * @author Artem Bilan + * @author Darryl Smith * @since 4.1 */ @SpringJUnitConfig @@ -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..f17045e3df3 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-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.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; @@ -50,6 +51,7 @@ * @author Gary Russell * @author Artem Bilan * @author Auke Zaaiman + * @author Darryl Smith * * @since 3.0.2 */ @@ -101,11 +103,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 +138,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 +162,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 +200,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,7 +225,39 @@ 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(); + } + } } 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/test/java/org/springframework/integration/smb/SmbTestSupport.java b/spring-integration-smb/src/test/java/org/springframework/integration/smb/SmbTestSupport.java index da823f0a030..a641226173e 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. @@ -101,17 +100,12 @@ 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"); } } 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-stomp/src/test/java/org/springframework/integration/stomp/client/StompServerIntegrationTests.java b/spring-integration-stomp/src/test/java/org/springframework/integration/stomp/client/StompServerIntegrationTests.java index 7e09afec50c..adc3ad0d607 100644 --- a/spring-integration-stomp/src/test/java/org/springframework/integration/stomp/client/StompServerIntegrationTests.java +++ b/spring-integration-stomp/src/test/java/org/springframework/integration/stomp/client/StompServerIntegrationTests.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. @@ -69,6 +69,8 @@ public class StompServerIntegrationTests { private static final EmbeddedActiveMQ broker = new EmbeddedActiveMQ(); + private static final ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + private static ReactorNettyTcpStompClient stompClient; @BeforeAll @@ -89,7 +91,6 @@ public static void setup() throws Exception { stompClient = new ReactorNettyTcpStompClient("127.0.0.1", TransportConstants.DEFAULT_STOMP_PORT); stompClient.setMessageConverter(new PassThruMessageConverter()); - ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.afterPropertiesSet(); stompClient.setTaskScheduler(taskScheduler); stompClient.setReceiptTimeLimit(5000); @@ -99,6 +100,7 @@ public static void setup() throws Exception { public static void teardown() throws Exception { stompClient.shutdown(); broker.stop(); + taskScheduler.destroy(); } @Test 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-support/src/main/java/org/springframework/integration/test/util/TestUtils.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/TestUtils.java index 5e86082e3a0..9a2a09b3c4e 100644 --- a/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/TestUtils.java +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/TestUtils.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. @@ -36,6 +36,7 @@ import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.SmartLifecycle; import org.springframework.context.support.GenericApplicationContext; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.lang.Nullable; @@ -61,6 +62,27 @@ public abstract class TestUtils { private static final Log LOGGER = LogFactory.getLog(TestUtils.class); + /** + * Obtain a value for the property from the provide object + * and try to cast it to the provided type. + * Supports nested properties via period delimiter. + * @param root the object to obtain the property value + * @param propertyPath the property name to obtain a value. + * @param type the expected value type. + * @param the expected value type. + * Can be nested path defined by the period. + * @return the value of the property or null + * @see DirectFieldAccessor + */ + @SuppressWarnings("unchecked") + public static T getPropertyValue(Object root, String propertyPath, Class type) { + Object value = getPropertyValue(root, propertyPath); + if (value != null) { + Assert.isAssignable(type, value.getClass()); + } + return (T) value; + } + /** * Obtain a value for the property from the provide object. * Supports nested properties via period delimiter. @@ -90,27 +112,6 @@ else if (i == tokens.length - 1) { return value; } - /** - * Obtain a value for the property from the provide object - * and try to cast it to the provided type. - * Supports nested properties via period delimiter. - * @param root the object to obtain the property value - * @param propertyPath the property name to obtain a value. - * @param type the expected value type. - * @param the expected value type. - * Can be nested path defined by the period. - * @return the value of the property or null - * @see DirectFieldAccessor - */ - @SuppressWarnings("unchecked") - public static T getPropertyValue(Object root, String propertyPath, Class type) { - Object value = getPropertyValue(root, propertyPath); - if (value != null) { - Assert.isAssignable(type, value.getClass()); - } - return (T) value; - } - /** * Create a {@link TestApplicationContext} instance * supplied with the basic Spring Integration infrastructure. @@ -140,6 +141,7 @@ public static ThreadPoolTaskScheduler createTaskScheduler(int poolSize) { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(poolSize); scheduler.setRejectedExecutionHandler(new CallerRunsPolicy()); + scheduler.setPhase(SmartLifecycle.DEFAULT_PHASE / 2); scheduler.afterPropertiesSet(); return scheduler; } @@ -216,6 +218,7 @@ private String getComponentNameIfNamed(final MessageChannel channel) { */ public static Properties locateComponentInHistory(List history, String componentName, int startingIndex) { + Assert.notNull(history, "'history' must not be null"); Assert.isTrue(StringUtils.hasText(componentName), "'componentName' must be provided"); Assert.isTrue(startingIndex < history.size(), "'startingIndex' can not be greater then size of history"); 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 fcfa5edaa21..130bf8454b2 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 @@ -23,7 +23,6 @@ import brave.test.TestSpanHandler; import io.micrometer.observation.ObservationHandler; import io.micrometer.observation.ObservationRegistry; -import io.micrometer.observation.tck.TestObservationRegistryAssert; import io.micrometer.tracing.Tracer; import io.micrometer.tracing.brave.bridge.BraveBaggageManager; import io.micrometer.tracing.brave.bridge.BraveCurrentTraceContext; @@ -37,29 +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.channel.interceptor.ObservationPropagationChannelInterceptor; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.config.EnableIntegrationManagement; -import org.springframework.integration.config.GlobalChannelInterceptor; +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.messaging.support.ChannelInterceptor; 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; @@ -85,6 +88,10 @@ public class WebFluxObservationPropagationTests { @Autowired private PollableChannel testChannel; + @Autowired + @Qualifier("webFluxRequestReplyClientFlow.input") + private MessageChannel webFluxRequestReplyClientFlowInput; + @BeforeEach void setup() { SPANS.clear(); @@ -104,12 +111,8 @@ void observationIsPropagatedFromWebFluxToServiceActivator() { .extracting(Message::getPayload) .isEqualTo("Received data: " + testData); - TestObservationRegistryAssert.assertThat(this.observationRegistry) - .hasRemainingCurrentObservation(); - - this.observationRegistry.getCurrentObservation().stop(); - - assertThat(SPANS.spans()).hasSize(6); + // 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(6)); SpansAssert.assertThat(SPANS.spans().stream().map(BraveFinishedSpan::fromBrave).collect(Collectors.toList())) .haveSameTraceId(); } @@ -130,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 @@ -179,12 +196,6 @@ WebTestClient webTestClient(ObservationRegistry registry, ApplicationContext app return WebTestClient.bindToServer(new HttpHandlerConnector(httpHandler)).build(); } - @Bean - @GlobalChannelInterceptor - public ChannelInterceptor observationPropagationInterceptor(ObservationRegistry observationRegistry) { - return new ObservationPropagationChannelInterceptor(observationRegistry); - } - @Bean IntegrationFlow webFluxFlow() { return IntegrationFlow @@ -203,6 +214,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) { @@ -215,6 +249,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 93aded3ba42..61ac121676c 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 57506db768b..46163164557 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 */ @@ -127,6 +128,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 4a2fe185c06..df1c5ce708e 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. @@ -192,6 +195,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"; @@ -245,7 +260,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 237f9c1751d..0287037cafc 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 @@ -43,6 +43,7 @@ /** * @author Artem Bilan + * @author Alessio Matricardi * * @since 5.4 */ @@ -151,4 +152,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 9baa9188908..04aa7b7f735 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/antora-playbook.yml b/src/reference/antora/antora-playbook.yml index 40bd9ab64a7..fbe36a48672 100644 --- a/src/reference/antora/antora-playbook.yml +++ b/src/reference/antora/antora-playbook.yml @@ -4,10 +4,11 @@ antora: extensions: - '@springio/antora-extensions/partial-build-extension' + # atlas-extension must be before latest-version-extension so latest versions are applied to imported versions + - '@antora/atlas-extension' - require: '@springio/antora-extensions/latest-version-extension' - require: '@springio/antora-extensions/inject-collector-cache-config-extension' - '@antora/collector-extension' - - '@antora/atlas-extension' - require: '@springio/antora-extensions/root-component-extension' root_component_name: 'integration' site: 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/codec.adoc b/src/reference/antora/modules/ROOT/pages/codec.adoc index 28718762982..1e8c4531d5a 100644 --- a/src/reference/antora/modules/ROOT/pages/codec.adoc +++ b/src/reference/antora/modules/ROOT/pages/codec.adoc @@ -94,7 +94,7 @@ You can override these framework defaults by configuring the registrars mentione ==== Using a Custom Kryo Serializer If you need custom serialization, see the https://github.com/EsotericSoftware/kryo[Kryo] documentation, because you need to use the native API to do the customization. -For an example, see the https://github.com/spring-projects/spring-integration/blob/main/spring-integration-core/src/main/java/org/springframework/integration/codec/kryo/MessageCodec.java[`MessageCodec`] implementation. +For an example, see the `org.springframework.integration.codec.kryo.MessageCodec` implementation. [[implementing-kryoserializable]] ==== Implementing KryoSerializable @@ -107,7 +107,6 @@ The following example shows a custom Kryo serializer: [source,java] ---- public class Address implements KryoSerializable { - ... @Override public void write(Kryo kryo, Output output) { 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/content-enrichment.adoc b/src/reference/antora/modules/ROOT/pages/content-enrichment.adoc index e6e31159e2b..9c49bf2e626 100644 --- a/src/reference/antora/modules/ROOT/pages/content-enrichment.adoc +++ b/src/reference/antora/modules/ROOT/pages/content-enrichment.adoc @@ -106,7 +106,7 @@ You can similarly point to a Groovy script, as the following example shows: [[spel-support]] === SpEL Support -In Spring Integration 2.0, we introduced the convenience of the https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions[Spring Expression Language (SpEL)] to help configure many different components. +In Spring Integration 2.0, we introduced the convenience of the https://docs.spring.io/spring-framework/reference/core/expressions.html[Spring Expression Language (SpEL)] to help configure many different components. The header enricher is one of them. Look again at the POJO example shown earlier. You can see that the computation logic to determine the header value is pretty simple. @@ -182,7 +182,7 @@ Starting with Spring Integration 3.0, a new sub-element `>getQueue("springIntegrationQueue")); + return new QueueChannel(hazelcastInstance.getQueue("springIntegrationQueue")); } ---- diff --git a/src/reference/antora/modules/ROOT/pages/index.adoc b/src/reference/antora/modules/ROOT/pages/index.adoc index cffeb1030a4..937ba56641a 100644 --- a/src/reference/antora/modules/ROOT/pages/index.adoc +++ b/src/reference/antora/modules/ROOT/pages/index.adoc @@ -5,7 +5,7 @@ Welcome to the Spring Integration reference documentation! [horizontal] xref:preface.adoc#preface[Preface] :: General project info, conventions -xref:whats-new.adoc#whats-new[What's New] :: Features and changes made in the current version +xref:whats-new.adoc#whats-new-part[What's New] :: Features and changes made in the current version xref:overview.adoc[Overview] :: Introduction to EIP and its implementation in the project xref:core.adoc[Core Messaging] :: Main project abstractions and components xref:overview.adoc#overview-components-message[Message] :: Details about the message abstraction implementations diff --git a/src/reference/antora/modules/ROOT/pages/jdbc/inbound-channel-adapter.adoc b/src/reference/antora/modules/ROOT/pages/jdbc/inbound-channel-adapter.adoc index 6354377d248..0dd6ec540f0 100644 --- a/src/reference/antora/modules/ROOT/pages/jdbc/inbound-channel-adapter.adoc +++ b/src/reference/antora/modules/ROOT/pages/jdbc/inbound-channel-adapter.adoc @@ -62,7 +62,7 @@ The `value` in each parameter expression can be any valid SpEL expression. The `#root` object for the expression evaluation is the constructor argument defined on the `parameterSource` bean. It is static for all evaluations (in the preceding example, an empty `String`). -Starting with version 5.0, you ca supply `ExpressionEvaluatingSqlParameterSourceFactory` with `sqlParameterTypes` to specify the target SQL type for the particular parameter. +Starting with version 5.0, you can supply `ExpressionEvaluatingSqlParameterSourceFactory` with `sqlParameterTypes` to specify the target SQL type for the particular parameter. The following example provides SQL types for the parameters being used in the query: diff --git a/src/reference/antora/modules/ROOT/pages/jdbc/message-store.adoc b/src/reference/antora/modules/ROOT/pages/jdbc/message-store.adoc index bea90bd0ca3..d52974c508b 100644 --- a/src/reference/antora/modules/ROOT/pages/jdbc/message-store.adoc +++ b/src/reference/antora/modules/ROOT/pages/jdbc/message-store.adoc @@ -21,7 +21,7 @@ Before starting to use JDBC message store components, you should provision a tar Spring Integration ships with some sample scripts that can be used to initialize a database. In the `spring-integration-jdbc` JAR file, you can find scripts in the `org.springframework.integration.jdbc` package. It provides an example create and an example drop script for a range of common database platforms. -A common way to use these scripts is to reference them in a https://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-intializing-datasource[Spring JDBC data source initializer]. +A common way to use these scripts is to reference them in a https://docs.spring.io/spring-framework/reference/data-access/jdbc/initializing-datasource.html[Spring JDBC data source initializer]. Note that the scripts are provided as samples and as specifications of the required table and column names. You may find that you need to enhance them for production use (for, example, by adding index declarations). @@ -126,7 +126,7 @@ When polling a message channel, you have the option to configure the associated [IMPORTANT] ==== -Keep in mind, though, that if you use a JDBC backed message channel and you plan to poll the channel and consequently the message store transactionally with multiple threads, you should ensure that you use a relational database that supports https://en.wikipedia.org/wiki/Multiversion_concurrency_control[Multiversion Concurrency Control] (MVCC). +Keep in mind, though, that if you use a JDBC backed message channel, and you plan to poll the channel and consequently the message store transactional with multiple threads, you should ensure that you use a relational database that supports https://en.wikipedia.org/wiki/Multiversion_concurrency_control[Multiversion Concurrency Control] (MVCC). Otherwise, locking may be an issue and the performance, when using multiple threads, may not materialize as expected. For example, Apache Derby is problematic in that regard. diff --git a/src/reference/antora/modules/ROOT/pages/jdbc/outbound-channel-adapter.adoc b/src/reference/antora/modules/ROOT/pages/jdbc/outbound-channel-adapter.adoc index 2cc537d1f31..6c63c71bdc2 100644 --- a/src/reference/antora/modules/ROOT/pages/jdbc/outbound-channel-adapter.adoc +++ b/src/reference/antora/modules/ROOT/pages/jdbc/outbound-channel-adapter.adoc @@ -7,7 +7,7 @@ By default, the message payload and headers are available as input parameters to [source,xml] ---- ---- diff --git a/src/reference/antora/modules/ROOT/pages/jdbc/stored-procedures.adoc b/src/reference/antora/modules/ROOT/pages/jdbc/stored-procedures.adoc index 551d17adb05..faa95054a34 100644 --- a/src/reference/antora/modules/ROOT/pages/jdbc/stored-procedures.adoc +++ b/src/reference/antora/modules/ROOT/pages/jdbc/stored-procedures.adoc @@ -35,7 +35,7 @@ If you want to execute stored functions instead, the following databases are ful ===== Even though your particular database may not be fully supported, chances are that you can use the stored procedure Spring Integration components quite successfully anyway, provided your RDBMS supports stored procedures or stored functions. -As a matter of fact, some provided integration tests use the https://www.h2database.com/[H2 database]. +As a matter of fact, some provided integration tests use the https://www.h2database.com[H2 database]. Nevertheless, it is very important to thoroughly test those usage scenarios. ===== @@ -105,7 +105,7 @@ This means that, by using a default `BeanPropertySqlParameterSourceFactory`, the However, if procedure parameters are passed in, this property (by default) evaluates to `false`. `ProcedureParameter` lets SpEL Expressions be provided. Therefore, it is highly beneficial to have access to the entire `Message`. -The property is set on the underlying `StoredProcExecutor`. +The property set on the underlying `StoredProcExecutor`. Optional. [[sp-common-config-subelements]] diff --git a/src/reference/antora/modules/ROOT/pages/jpa/namespace-support.adoc b/src/reference/antora/modules/ROOT/pages/jpa/namespace-support.adoc index 54d4be482ae..27683223c51 100644 --- a/src/reference/antora/modules/ROOT/pages/jpa/namespace-support.adoc +++ b/src/reference/antora/modules/ROOT/pages/jpa/namespace-support.adoc @@ -83,7 +83,7 @@ The following example shows how to set an expression-based parameter: ---- Value-based Parameters:: -The following example shows how to set an value-based parameter: +The following example shows how to set a value-based parameter: + [source,xml] ---- @@ -143,7 +143,7 @@ You can also configure this JPA operation to run as a new transaction, as the fo ---- In the preceding example, the transactional element of the outbound gateway or adapter specifies the transaction attributes. -It is optional to define this child element if you have `DirectChannel` as an input channel to the adapter and you want the adapter to execute the operations in the same transaction context as the caller. +It is optional to define this child element if you have `DirectChannel` as an input channel to the adapter, and you want the adapter to execute the operations in the same transaction context as the caller. If, however, you use an `ExecutorChannel`, you must have the `transactional` element, because the invoking client's transaction context is not propagated. NOTE: Unlike the `transactional` element of the poller, which is defined in Spring Integration's namespace, the `transactional` element for the outbound gateway or adapter is defined in the JPA namespace. diff --git a/src/reference/antora/modules/ROOT/pages/jpa/outbound-channel-adapter.adoc b/src/reference/antora/modules/ROOT/pages/jpa/outbound-channel-adapter.adoc index 65e5ad56056..673b7e8c2fe 100644 --- a/src/reference/antora/modules/ROOT/pages/jpa/outbound-channel-adapter.adoc +++ b/src/reference/antora/modules/ROOT/pages/jpa/outbound-channel-adapter.adoc @@ -249,7 +249,7 @@ The default value is set to `0`, which means "'no flush'". This attribute is geared towards messages with `Iterable` payloads. For instance, if `flush-size` is set to `3`, then `entityManager.flush()` is called after every third entity. Furthermore, `entityManager.flush()` is called once more after the entire loop. -If the 'flush-size' attribute is specified with a value greater than '0', you need not configure the `flush` attribute. +If the 'flush-size' attribute specified with a value greater than '0', you need not configure the `flush` attribute. <15> Set this value to 'true' if you want to clear the persistence context immediately after each flush operation. The attribute's value is applied only if the `flush` attribute is set to `true` or if the `flush-size` attribute is set to a value greater than `0`. <16> If set to `true`, the payload of the message is used as a source for parameters. diff --git a/src/reference/antora/modules/ROOT/pages/jpa/outbound-gateways.adoc b/src/reference/antora/modules/ROOT/pages/jpa/outbound-gateways.adoc index 17d7059918c..ffec9828dea 100644 --- a/src/reference/antora/modules/ROOT/pages/jpa/outbound-gateways.adoc +++ b/src/reference/antora/modules/ROOT/pages/jpa/outbound-gateways.adoc @@ -317,8 +317,8 @@ All other attributes are disallowed if you use `id-expression`. Optional. <2> A boolean flag indicating whether the select operation is expected to return a single result or a `List` of results. If this flag is set to `true`, a single entity is sent as the payload of the message. -If multiple entities are returned, an exception is thrown. -If `false`, the `List` of entities is sent as the payload of the message. +If multiple entities are returned, an exception thrown. +If `false`, the `List` of entities sent as the payload of the message. It defaults to `false`. Optional. <3> This non-zero, non-negative integer value tells the adapter not to select more than the specified number of rows on execution of the select operation. diff --git a/src/reference/antora/modules/ROOT/pages/metrics.adoc b/src/reference/antora/modules/ROOT/pages/metrics.adoc index de13bad7111..4f0fe2be96d 100644 --- a/src/reference/antora/modules/ROOT/pages/metrics.adoc +++ b/src/reference/antora/modules/ROOT/pages/metrics.adoc @@ -170,7 +170,7 @@ It uses the `IntegrationObservation.GATEWAY` API; * An `AbstractMessageChannel.send()` operation is the only Spring Integration API where it produces messages. So, it is treated as a `PRODUCER` span type and uses the `IntegrationObservation.PRODCUER` API. This makes more sense when a channel is a distributed implementation (e.g. `PublishSubscribeKafkaChannel` or `ZeroMqChannel`) and trace information has to be added to the message. -So, the `IntegrationObservation.PRODUCER` observation is based on a `MessageSenderContext` where Spring Integration supplies a `MutableMessage` to allow a subsequent tracing `Propagator` to add headers so they are available to the consumer; +So, the `IntegrationObservation.PRODUCER` observation is based on a `MessageSenderContext` where Spring Integration supplies a `MutableMessage` to allow a subsequent tracing `Propagator` to add headers, so they are available to the consumer; * An `AbstractMessageHandler` is a `CONSUMER` span type and uses the `IntegrationObservation.HANDLER` API. An observation production on the `IntegrationManagement` components can be customized via `ObservationConvention` configuration. @@ -188,10 +188,10 @@ include::partial$conventions.adoc[] [[observation-propagation]] == Observation Propagation -To supply a connected chain of spans in one trace, independently of the nature of the messaging flow, Spring Integration provides an `ObservationPropagationChannelInterceptor` implementation. -This can be configured on `MessageChannnel` beans individually or as a `@GlobalChannelInterceptor` with respective `MessageChannnel` bean names pattern matching. -The goal of this interceptor is to propagate an `Observation` from the producer thread to the consumer one independently of the `MessageChannnel` implementation and nature. -A `DirectChannel`, though, is ignored since its consumer is executed directly on the producer thread. +To supply a connected chain of spans in one trace, independently of the nature of the messaging flow, even if a `MessageChannel` is persistent and distributed, the observation must be enabled on this channel and on consumers (subscribers) for this channel. +This way, the tracing information is stored in the message headers before it is propagated to a consumer thread or persisted into the database. +This is done via mentioned above `MessageSenderContext`. +The consumer (a `MessageHandler`) side restores tracing information from those headers using a `MessageReceiverContext` and starts a new child `Observation`. [[spring-integration-jmx-support]] == Spring Integration JMX Support diff --git a/src/reference/antora/modules/ROOT/pages/testing.adoc b/src/reference/antora/modules/ROOT/pages/testing.adoc index b1d19df9ca9..a8592d8b75e 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`.