From 5f5b1a57d7d45406c84d62b7a28dc95575e7250c Mon Sep 17 00:00:00 2001 From: wandi34 Date: Wed, 7 Aug 2024 14:17:54 +0200 Subject: [PATCH 01/26] SWS-1058 - Enable sign with cert chain and configuration of subjectDnConstraints (#1419) Co-authored-by: Andreas Winter --- .../wss4j2/Wss4jSecurityInterceptor.java | 34 ++++++++++++++++++ .../Wss4jMessageInterceptorSignTestCase.java | 34 ++++++++++++++++++ .../src/test/resources/private.jks | Bin 1807 -> 7217 bytes 3 files changed, 68 insertions(+) diff --git a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java index 3b457cf46..c04cfc58a 100644 --- a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java +++ b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java @@ -20,8 +20,11 @@ import java.security.Principal; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -59,6 +62,9 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; + /** * A WS-Security endpoint interceptor based on Apache's WSS4J. This interceptor supports messages created by the * {@link org.springframework.ws.soap.axiom.AxiomSoapMessageFactory} and the @@ -138,6 +144,7 @@ * @author Jamin Hitchcock * @author Rob Leland * @author Lars Uffmann + * @author Andreas Winter * @see Apache WSS4J 2.0 * @since 2.3.0 */ @@ -194,6 +201,8 @@ public class Wss4jSecurityInterceptor extends AbstractWsSecurityInterceptor impl // To maintain same behavior as default, this flag is set to true private boolean removeSecurityHeader = true; + private List signatureSubjectDnPatterns = emptyList(); + /** * Create a {@link WSSecurityEngine} by default. */ @@ -225,6 +234,15 @@ public void setSecurementActor(String securementActor) { handler.setOption(WSHandlerConstants.ACTOR, securementActor); } + /** + * Defines whether to use a single certificate or a whole certificate chain when constructing + * a BinarySecurityToken used for direct reference in signature. + * The default is "true", meaning that only a single certificate is used. + */ + public void setSecurementSignatureSingleCertificate(boolean useSingleCertificate) { + handler.setOption(WSHandlerConstants.USE_SINGLE_CERTIFICATE, useSingleCertificate); + } + public void setSecurementEncryptionCrypto(Crypto securementEncryptionCrypto) { handler.setSecurementEncryptionCrypto(securementEncryptionCrypto); } @@ -485,6 +503,19 @@ public void setValidationSignatureCrypto(Crypto signatureCrypto) { this.validationSignatureCrypto = signatureCrypto; } + /** + * Certificate constraints which will be applied to the subject DN of the certificate used for + * signature validation, after trust verification of the certificate chain associated with the + * certificate. + * + * @param patterns A list of regex patterns which will be applied to the subject DN. + * + * @see WSS4J configuration: SIG_SUBJECT_CERT_CONSTRAINTS + */ + public void setValidationSubjectDnConstraints(List patterns) { + signatureSubjectDnPatterns = patterns; + } + /** Whether to enable signatureConfirmation or not. By default signatureConfirmation is enabled */ public void setEnableSignatureConfirmation(boolean enableSignatureConfirmation) { @@ -670,6 +701,7 @@ protected RequestData initializeRequestData(MessageContext messageContext) { // allow for qualified password types for .Net interoperability requestData.setAllowNamespaceQualifiedPasswordTypes(true); + requestData.setSubjectCertConstraints(signatureSubjectDnPatterns); return requestData; } @@ -710,6 +742,8 @@ protected RequestData initializeValidationRequestData(MessageContext messageCont // allow for qualified password types for .Net interoperability requestData.setAllowNamespaceQualifiedPasswordTypes(true); + requestData.setSubjectCertConstraints(signatureSubjectDnPatterns); + return requestData; } diff --git a/spring-ws-security/src/test/java/org/springframework/ws/soap/security/wss4j2/Wss4jMessageInterceptorSignTestCase.java b/spring-ws-security/src/test/java/org/springframework/ws/soap/security/wss4j2/Wss4jMessageInterceptorSignTestCase.java index 0e8490af9..729221b73 100644 --- a/spring-ws-security/src/test/java/org/springframework/ws/soap/security/wss4j2/Wss4jMessageInterceptorSignTestCase.java +++ b/spring-ws-security/src/test/java/org/springframework/ws/soap/security/wss4j2/Wss4jMessageInterceptorSignTestCase.java @@ -18,7 +18,9 @@ import static org.assertj.core.api.Assertions.*; +import java.util.List; import java.util.Properties; +import java.util.regex.Pattern; import org.junit.jupiter.api.Test; import org.springframework.ws.WebServiceMessage; @@ -121,4 +123,36 @@ public void testSignResponseWithSignatureUser() throws Exception { assertXpathExists("Absent SignatureConfirmation element", "/SOAP-ENV:Envelope/SOAP-ENV:Header/wsse:Security/ds:Signature", document); } + + @Test + public void testValidateCertificateSubjectDnConstraintsShouldMatchSubject() throws Exception { + SoapMessage message = createSignedTestSoapMessage(); + MessageContext messageContext = getSoap11MessageContext(createSignedTestSoapMessage()); + interceptor.secureMessage(message, messageContext); + + interceptor.setValidationActions("Signature"); + interceptor.setValidationSubjectDnConstraints(List.of(Pattern.compile(".*"))); + assertThatCode(() ->interceptor.validateMessage(message, messageContext)).doesNotThrowAnyException(); + } + + @Test + public void testValidateCertificateSubjectDnConstraintsShouldFailForNotMatchingSubject() throws Exception { + SoapMessage message = createSignedTestSoapMessage(); + MessageContext messageContext = getSoap11MessageContext(createSignedTestSoapMessage()); + interceptor.secureMessage(message, messageContext); + + interceptor.setValidationActions("Signature"); + interceptor.setValidationSubjectDnConstraints(List.of(Pattern.compile("O=Some Other Company"))); + Throwable catched = catchThrowable(() -> interceptor.validateMessage(message, messageContext)); + assertThat(catched).isInstanceOf(Wss4jSecurityValidationException.class); + } + + private SoapMessage createSignedTestSoapMessage() throws Exception { + interceptor.setSecurementActions("Signature"); + interceptor.setSecurementSignatureKeyIdentifier("DirectReference"); + interceptor.setSecurementSignatureSingleCertificate(false); + interceptor.setSecurementPassword("123456"); + interceptor.setSecurementUsername("testkey"); + return loadSoap11Message("empty-soap.xml"); + } } diff --git a/spring-ws-security/src/test/resources/private.jks b/spring-ws-security/src/test/resources/private.jks index b3b10e36f8eb4b106279725d60034bdd9ebc4388..15a4ebafe8b2da377c7b2ec6a658bad8b7bf484e 100644 GIT binary patch delta 5494 zcmcJS2T)Vnx5kr@07)p)5g{OgbO<*TsTR6OC@NKI1d*=Nq#c@ofKn7u2nYltigct& z6DbxDAryl`l-`?EgYu$xZt?%_oA>52@67Bu=i7VE-gC~{zqP-W#8P$ycuTuLAP|Js zptJ@CG59)P@pX5;N_$S`ZbC)T`WMlW&{;G;JzR<&3#xk%TH^>-YrdtGS#RyUC@xVIbQfkNjwaG zOS2aq;^i|5*q-}z{f0hp`a!Xo!<*LzG0jcLN>=C1C95cCoTVeeRR>4Rg|9?P!KB}Z zTBPk)RU2H*^E%;zu8JyQDy!mJ6R=kv;b#Ux><3xwCBWc75n}-<{{i~!I8{ihe6(fQ z10W8w8K||9K{}hb{x(nXh}lY9voouCLNt3(_O?H8_Go**&RaAeCCrp8`zl^J-Tqme z;@2fX;>VtYo|LoAEf+S&aHO@q86*0Mlch>9=1LOA=J(#i{T+F&g$|s z<&;O_&uzC&Lho@KUGFom!x7>is?PF$2g;PWcJvb5aEJLqY7Dn{Tll#y&Q>$$z(l)u z3@;U7H0CGUjUZKcGSf3ox+%Z&()Vv2U=zhkQ{R(r8C2RtaNbinQFZrP;4$6E9L^U& zGq9ha2i-k*JH|VaE+R3HJYO_{nK~q=uJW{q_mwXUmf0zlCa@3$8JTHniq?8p3YEy} z9C-*=%WaW=7*frD_e!YJJ_r8psV2&R*;dtMGDh3@(W97c?x?%d$aeXM&n*3~@vAGH zLak##Q<`3f+7Ij#TCS>QvagXp=MDG*ZhB9#*9O{a)WhPgp7vU1my`61LL4yc)N+Yl zkhRP1L$u(c7Alp?bUqZMb2-kLSgmGj8u19xEtD8}!r078hKHkCBNh*pd}}#i0j`WG zhwW~g|oFwyo|Qzv=ba)%+|R?HJ=!Nr`4gmaNH86S<;N+m0gt4r&(0|Gl; z@Bj@{Rm{|kSrf#mA3h=7_8e2><*({(v8nC&gfH3B$azI85sP=>`eu)zq{e#kLzkc8 zK-xPx-&?!i=_O-3cQ1sWvR&p}hj+%Y$(r5-W{#WlNMhZst8zu}x@qKL?LK+v)=p>+ zOJ8bwyp>{ecl;J>rWKgPWmdcZbg<&k+gI1;?H|k3Cic#SC5lcba{>{O2++k%p2)k7{>?rSv(JMCz%yZSRR!YpMK z(JCMi-BWZVT^TwOaxaGt0){|1oyCn;R{W%M%iZs;zkEx}mSGkaXl8o2WQ=~yCNmg} zgn`hf<&kJOJ>6L-JqHBm_=jL*`a=M;A%;H{O(6a*kUzyfG|P_}=$L+{W+9v-8pXr> zn*v%%UO`^|Hw8syYnoL2n?zgwu@G%lUuSt{H1fv;j7-oUx&05x@IwNBX~eI?$X_Mh z?-Cse7XJG;f?*)KNH8mi){lThg2AB1QoGB|Dz+QUuI}as)aBVfuG&=7CHkWDbrteG zYDW@-8|%c=u3|M8YB@BLI?e&TK4rGuQnCg#egg9Pi!)0|w5?gKQP<(&*(OI67>46@=Pb{2M!9ufx zj>R>iuzY<5N7OE*T*jwptktu~?bsoVpgkSWl2yFI3wDX8=BOzRZ@y@pj=FfPGGtPv z8ZMtU2DUjrmO69;H7q}V(WhkhN*9C<3<47Z(f&WfCcp{???Rz;5LgbHl@t1q{P*zCwjeD$U=U;93;7yWr!}u* z;|*M%2^v==GwB~R--dDlPzf&L99FQas+X0;^vSVr5!oz9eJUJ`j$;=!a0GHqc=|qN z>Z^&+uvm}NUtqhhI-jNq-5GZHRxLTS#K&P~oeH}Xl6;xjqH9#3?~a4pSROB4_Li=s zO(k1HdHx7j-_Cp{YB##>s8@Jo1jNR-xYyG60jE~pc)Q8<(Ic$C0o@UqM=IR&$6~9r z_M=D$t9>mdzJ+Z8^C>q2d|X5p4ix9h!7l7b5c=S&*Y#9mkej{rsT$X>8mlXFr*C}< z5?c?NU>aGF!YFKbk}iHyk#1ZTT>!aPRmzf;w;~i@ZVJSS;J7JHy`Voa&ZJ?SPQw`Z zfiXbiSi4*d<;Ol9(DvvgM`r|$#453mM^XfWkBzrceK&q)WOW@qiBiag&VvRY*AssPSk5k_EAecm^ z#@`?puigIQ?z2TOocv9t**bNjQN`BiF=mu6^c4ZqXN^F#e#@y6NkMj@#VgG5hOO$)<*vLT_yhgAYT&I~2xF57V^)s*@S#lU zgdURBwFH<*{2`7jU4)$MYB@bZS%>Vrvy)J}&m!db!mc=rFd*w1s+T#Mtt%$xUAvTI z&V6|@je=jkR!7J{itl2Xnm-^I%zRI3mrxqT6+w^}Mb;Ue4Cr{yxl zI%CrCxOKQvh&)P}afPE2jBCdF? z*e%-9MVOzs2apgpmz1C~j%!t$-{NhPn|jWhv%DPns}i9dcRwqU#>w^nu@cdPF15~F zPOAU25;3)}5_o3nDUd5Nh0;~^`@Ue#<1l@TK!DpzQ?p(mYQwL%-%-~JtU7=KB4Rz9 zk?+5IRoQgUQ%%Cym}b{9s7()4^$VdG>6h{0)$!D^EVV(@C$^*G74{G?f2C7lIy;t> zN7(#!*II;{IX&AS!m$p19@*dXBRKEbO(CXnE(0PZrF@A_Qa4?u>CgmU`m>Y6z9E)9zk0%$4rMKTl&Qqfw zcz*^0`W2>L4E9XRq{KXBtb!_CNeImrR!rWRwn*L`;;u&vM$aam37Q6)!COn{v0ST^Wp6Rd`StA3uaM(%OL zib^_6%6lpGytossmlc(SdzaG^XC`jzy>nOu+;+%r!vcrZd^oXtE!@tP_)(3~iV{|l zwAdPc-P4g3caw#bT_Y6>u&vtS4;7hm@`XZ~; z)+6Czh#@28FQ*6MMOI6 zxMy(}Kjqv;ZtFei6n8{FPHPUg+43xN@X|fq$pcxL~D%x0Bd36q0Xk+2V^Ob{CdhR?tu`#LQ z>aSEi^ifQ)YII~NI9KFovLNfV740Rw5HeOAGGurCn4!C_z?@*(%XkbMnEgg43x{2w zdHoQ&?^G~@jAvzG(1=<8wBu^(U;_&E)jcew@o;nNBbA|WFb#E? zgqttFWy6R3CRdhIzpD5e$p`j1&^ze6IBB1Ce#sCpsHTS9pB2)ayCdV_4)=}q(c-p6 zl_z Date: Wed, 7 Aug 2024 08:44:47 -0500 Subject: [PATCH 02/26] Migrate build to Develocity Conventions Maven extension. (#1426) --- .gitignore | 1 - .mvn/develocity.xml | 30 ------------------------- .mvn/extensions.xml | 11 +++------ Jenkinsfile | 29 ++++++++---------------- ci/build-and-deploy-to-artifactory.sh | 3 --- ci/build-and-deploy-to-maven-central.sh | 3 --- ci/pipeline.properties | 3 +-- ci/test.sh | 3 --- settings.xml | 5 ----- 9 files changed, 13 insertions(+), 75 deletions(-) delete mode 100644 .mvn/develocity.xml diff --git a/.gitignore b/.gitignore index fc4ed6e86..365f5b205 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,4 @@ _site credentials.yml .flattened-pom.xml -.mvn/.gradle-enterprise .mvn/.develocity diff --git a/.mvn/develocity.xml b/.mvn/develocity.xml deleted file mode 100644 index b54815e75..000000000 --- a/.mvn/develocity.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - https://ge.spring.io - - - false - true - true - - #{{'0.0.0.0'}} - - - - - true - - - - - ${env.GRADLE_ENTERPRISE_CACHE_USERNAME} - ${env.GRADLE_ENTERPRISE_CACHE_PASSWORD} - - - true - #{env['GRADLE_ENTERPRISE_CACHE_USERNAME'] != null and env['GRADLE_ENTERPRISE_CACHE_PASSWORD'] != null} - - - \ No newline at end of file diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 854714cee..a36ba0531 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -1,13 +1,8 @@ - com.gradle - develocity-maven-extension - 1.21.3 - - - com.gradle - common-custom-user-data-maven-extension - 2.0 + io.spring.develocity.conventions + develocity-conventions-maven-extension + 0.0.19 \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index f479045e6..bc34f8b63 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -37,8 +37,7 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES')} environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}") - GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { @@ -63,8 +62,7 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES')} environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}") - GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { @@ -82,8 +80,7 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES')} environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}") - GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { @@ -101,8 +98,7 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES')} environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}") - GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { @@ -120,8 +116,7 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES')} environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}") - GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { @@ -137,8 +132,7 @@ pipeline { options { timeout(time: 30, unit: 'MINUTES')} environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}") - GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { script { @@ -161,8 +155,7 @@ pipeline { KEYRING = credentials('spring-signing-secring.gpg') PASSPHRASE = credentials('spring-gpg-passphrase') STAGING_PROFILE_ID = credentials('spring-data-release-deployment-maven-central-staging-profile-id') - GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}") - GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { @@ -234,8 +227,7 @@ pipeline { KEYRING = credentials('spring-signing-secring.gpg') PASSPHRASE = credentials('spring-gpg-passphrase') STAGING_PROFILE_ID = credentials('spring-data-release-deployment-maven-central-staging-profile-id') - GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}") - GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { @@ -294,8 +286,7 @@ pipeline { environment { ARTIFACTORY = credentials("${p['artifactory.credentials']}") - GRADLE_ENTERPRISE_CACHE = credentials("${p['gradle-enterprise-cache.credentials']}") - GRADLE_ENTERPRISE_ACCESS_KEY = credentials("${p['gradle-enterprise.access-key']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") } steps { @@ -303,8 +294,6 @@ pipeline { docker.withRegistry('', "${p['dockerhub.credentials']}") { docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.basic']) { sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ' + - 'GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USR} ' + - 'GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PSW} ' + './mvnw -s settings.xml -Pjakarta-ee-10,distribute,docs ' + '-Dartifactory.server=https://repo.spring.io ' + "-Dartifactory.username=${ARTIFACTORY_USR} " + diff --git a/ci/build-and-deploy-to-artifactory.sh b/ci/build-and-deploy-to-artifactory.sh index 5960f8b46..e8e5eb91b 100755 --- a/ci/build-and-deploy-to-artifactory.sh +++ b/ci/build-and-deploy-to-artifactory.sh @@ -4,9 +4,6 @@ set -euo pipefail RELEASE_TYPE=$1 -export GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USR} -export GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PSW} - echo 'Deploying to Artifactory...' MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" ./mvnw \ diff --git a/ci/build-and-deploy-to-maven-central.sh b/ci/build-and-deploy-to-maven-central.sh index 9eb2a5730..5a48d3762 100755 --- a/ci/build-and-deploy-to-maven-central.sh +++ b/ci/build-and-deploy-to-maven-central.sh @@ -5,9 +5,6 @@ set -euo pipefail PROJECT_VERSION=$1 STAGING_REPOSITORY_ID=$2 -export GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USR} -export GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PSW} - echo 'Staging on Maven Central...' GNUPGHOME=/tmp/gpghome diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 2aea08744..09915555f 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -18,5 +18,4 @@ docker.java.inside.basic=-v $HOME:/tmp/jenkins-home # Credentials artifactory.credentials=02bd1690-b54f-4c9f-819d-a77cb7a9822c dockerhub.credentials=hub.docker.com-springbuildmaster -gradle-enterprise-cache.credentials=gradle_enterprise_cache_user -gradle-enterprise.access-key=gradle_enterprise_secret_access_key +develocity.access-key=gradle_enterprise_secret_access_key diff --git a/ci/test.sh b/ci/test.sh index ba458fba6..5ec6309d8 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -2,9 +2,6 @@ set -euo pipefail -export GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USR} -export GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PSW} - MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" \ ./mvnw -s settings.xml \ -P${PROFILE} clean dependency:list test -Dsort -B -U diff --git a/settings.xml b/settings.xml index 28f0d1339..b474fd4ff 100644 --- a/settings.xml +++ b/settings.xml @@ -24,11 +24,6 @@ ${env.ARTIFACTORY_USR} ${env.ARTIFACTORY_PSW} - - ge.spring.io - ${env.GRADLE_ENTERPRISE_CACHE_USERNAME} - ${env.GRADLE_ENTERPRISE_CACHE_PASSWORD} - \ No newline at end of file From ec669b1e975fe60b0d06d55fb175800e6f67bca9 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:56:31 -0400 Subject: [PATCH 03/26] Upgrade WSS4J version (#1429) --- pom.xml | 2 +- .../ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java | 2 +- .../ws/soap/security/wss4j2/callback/package.html | 2 +- .../org/springframework/ws/soap/security/wss4j2/package.html | 2 +- .../ws/soap/security/wss4j2/support/package.html | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index ec40d5e69..653ced792 100644 --- a/pom.xml +++ b/pom.xml @@ -144,7 +144,7 @@ 2.1.0 6.5.1 1.6.3 - 2.4.3 + 3.0.3 3.0.0 3.0.4 2.3.1 diff --git a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java index c04cfc58a..989772f4b 100644 --- a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java +++ b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/Wss4jSecurityInterceptor.java @@ -145,7 +145,7 @@ * @author Rob Leland * @author Lars Uffmann * @author Andreas Winter - * @see Apache WSS4J 2.0 + * @see Apache WSS4J 2.0+ * @since 2.3.0 */ public class Wss4jSecurityInterceptor extends AbstractWsSecurityInterceptor implements InitializingBean { diff --git a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/callback/package.html b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/callback/package.html index 8335862b9..60a429c30 100644 --- a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/callback/package.html +++ b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/callback/package.html @@ -1,5 +1,5 @@ -Contains CallbackHandler implementations for WSS4J 2.0. +Contains CallbackHandler implementations for WSS4J 2.0+. \ No newline at end of file diff --git a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/package.html b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/package.html index 08abe8df9..f6157746c 100644 --- a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/package.html +++ b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/package.html @@ -1,6 +1,6 @@ -Contains classes for using the Apache WSS4J 2.0 WS-Security implementation +Contains classes for using the Apache WSS4J 2.0+ WS-Security implementation within Spring-WS. diff --git a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/support/package.html b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/support/package.html index d2e986762..f8340f6cf 100644 --- a/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/support/package.html +++ b/spring-ws-security/src/main/java/org/springframework/ws/soap/security/wss4j2/support/package.html @@ -1,5 +1,5 @@ -Contains support classes for working with WSS4J 2.0. +Contains support classes for working with WSS4J 2.0+. \ No newline at end of file From e8dc2c9c1e05cf245727ab8baa32ad1852f38e24 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Fri, 4 Oct 2024 17:06:25 +0200 Subject: [PATCH 04/26] Update to 4.1.0-SNAPSHOT (#1432) * Update to 4.1.0-SNAPSHOT Update Spring Framework to 6.1.13 Update Spring Security to 6.3.3 Fixes #1431 * Update to 4.1.0-SNAPSHOT Update Spring Framework to 6.1.13 Update Spring Security to 6.3.3 Update WSS4J to 3.0.3 Fixes #1431 --- pom.xml | 22 +++++++++++----------- spring-ws-bom/pom.xml | 12 ++++++------ spring-ws-core/pom.xml | 2 +- spring-ws-security/pom.xml | 2 +- spring-ws-support/pom.xml | 2 +- spring-ws-test/pom.xml | 2 +- spring-xml/pom.xml | 2 +- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pom.xml b/pom.xml index 653ced792..cbc803fe7 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.ws spring-ws - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT pom Spring Web Services @@ -124,28 +124,28 @@ 4.3.5 - 6.0.20 - 6.1.9 + 6.1.13 + 6.3.3 - 6.0.21-SNAPSHOT - 6.1.10-SNAPSHOT + 6.2.0-SNAPSHOT + 6.4.0-SNAPSHOT - 6.1.6 - 6.2.4 + 6.1.13 + 6.3.3 - 6.1.7-SNAPSHOT - 6.2.5-SNAPSHOT + 6.2.0-SNAPSHOT + 6.4.0-SNAPSHOT - 2.1.5 + 2.3.3 2.0.1 2.1.0 6.5.1 1.6.3 3.0.3 - 3.0.0 + 3.0.3 3.0.4 2.3.1 1.6 diff --git a/spring-ws-bom/pom.xml b/spring-ws-bom/pom.xml index 4750f6c14..743147571 100644 --- a/spring-ws-bom/pom.xml +++ b/spring-ws-bom/pom.xml @@ -6,7 +6,7 @@ org.springframework.ws spring-ws-bom - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT pom Spring Web Services - BOM @@ -71,27 +71,27 @@ org.springframework.ws spring-ws-core - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT org.springframework.ws spring-ws-security - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT org.springframework.ws spring-ws-support - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT org.springframework.ws spring-ws-test - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT org.springframework.ws spring-xml - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT diff --git a/spring-ws-core/pom.xml b/spring-ws-core/pom.xml index 16998873f..9b2d0f93f 100644 --- a/spring-ws-core/pom.xml +++ b/spring-ws-core/pom.xml @@ -7,7 +7,7 @@ org.springframework.ws spring-ws - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT spring-ws-core diff --git a/spring-ws-security/pom.xml b/spring-ws-security/pom.xml index b68e8392d..882d993fe 100644 --- a/spring-ws-security/pom.xml +++ b/spring-ws-security/pom.xml @@ -7,7 +7,7 @@ org.springframework.ws spring-ws - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT spring-ws-security diff --git a/spring-ws-support/pom.xml b/spring-ws-support/pom.xml index 737af9ab0..3fcbdef59 100644 --- a/spring-ws-support/pom.xml +++ b/spring-ws-support/pom.xml @@ -7,7 +7,7 @@ org.springframework.ws spring-ws - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT spring-ws-support diff --git a/spring-ws-test/pom.xml b/spring-ws-test/pom.xml index fcd59c699..e6ef5f4e5 100644 --- a/spring-ws-test/pom.xml +++ b/spring-ws-test/pom.xml @@ -7,7 +7,7 @@ org.springframework.ws spring-ws - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT spring-ws-test diff --git a/spring-xml/pom.xml b/spring-xml/pom.xml index bf6686365..58ab49a0f 100644 --- a/spring-xml/pom.xml +++ b/spring-xml/pom.xml @@ -7,7 +7,7 @@ org.springframework.ws spring-ws - 4.0.12-SNAPSHOT + 4.1.0-SNAPSHOT spring-xml From b45071b6d59680c59b771c8d9ab0fc180058db52 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Tue, 5 Nov 2024 09:55:05 +0200 Subject: [PATCH 05/26] Update HttpComponents5ClientFactory to use setConnectionTimeout instead setConnectionRequestTimeout to restore the behaviour as before. (#1437) Fixes #1436 --- .../ws/transport/http/HttpComponents5ClientFactory.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java index 7d23dc1de..8446561c7 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java @@ -211,13 +211,12 @@ public CloseableHttpClient getObject() throws Exception { if (this.connectionManagerBuilderCustomizer != null) { this.connectionManagerBuilderCustomizer.customize(connectionManagerBuilder); } - this.connectionManager = connectionManagerBuilder.build(); applyMaxConnectionsPerHost(connectionManager); RequestConfig.Builder requestConfigBuilder = RequestConfig.custom() // - .setConnectionRequestTimeout(Timeout.of(connectionTimeout)) // + .setConnectTimeout(Timeout.of(connectionTimeout)) // .setResponseTimeout(Timeout.of(readTimeout)); HttpClientBuilder httpClientBuilder = HttpClientBuilder.create() // From e14e483322ef5dbf88d05591602a403bc2c8fc5c Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Tue, 26 Nov 2024 16:03:32 +0200 Subject: [PATCH 06/26] Add default implementation and specification entries (#1441) See #1440 --- pom.xml | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index cbc803fe7..8efe8ff64 100644 --- a/pom.xml +++ b/pom.xml @@ -128,16 +128,16 @@ 6.3.3 - 6.2.0-SNAPSHOT - 6.4.0-SNAPSHOT + 6.1.14-SNAPSHOT + 6.3.4-SNAPSHOT - 6.1.13 - 6.3.3 + 6.2.0 + 6.4.1 - 6.2.0-SNAPSHOT - 6.4.0-SNAPSHOT + 6.2.1-SNAPSHOT + 6.4.2-SNAPSHOT 2.3.3 2.0.1 @@ -337,6 +337,19 @@ + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + true + + + + com.gradle develocity-maven-extension From 070d044e0430e344126aadd70ec27ad52f3c0890 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Tue, 17 Dec 2024 16:52:09 +0200 Subject: [PATCH 07/26] Fix problem with BeanPostProcessing (#1443) * Fix problem with BeanPostProcessing Change endpoint mapping from BeanPostProcessor to SmartInitializingSingleton Added Test and updated test.sh to invoke with separate profile. Fixes #1435 --- ci/test.sh | 4 + spring-ws-core/pom.xml | 109 ++++++++++++++++++ .../AnnotationActionEndpointMapping.java | 21 +--- .../ObservationInWsConfigurerTests.java | 74 ++++++++++++ 4 files changed, 193 insertions(+), 15 deletions(-) create mode 100644 spring-ws-core/src/test/java/org/springframework/ws/observation/ObservationInWsConfigurerTests.java diff --git a/ci/test.sh b/ci/test.sh index 5ec6309d8..dc5352f8c 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -5,3 +5,7 @@ set -euo pipefail MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" \ ./mvnw -s settings.xml \ -P${PROFILE} clean dependency:list test -Dsort -B -U +# Provides for testing org.springframework.ws.observation.ObservationInWsConfigurerTests separately +MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" \ + ./mvnw -s settings.xml \ + -P-default,observation clean dependency:list test -Dsort -B -U diff --git a/spring-ws-core/pom.xml b/spring-ws-core/pom.xml index 9b2d0f93f..23beaf950 100644 --- a/spring-ws-core/pom.xml +++ b/spring-ws-core/pom.xml @@ -248,6 +248,115 @@ + + default + + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + **/ObservationInWsConfigurerTests.* + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + **/ObservationInWsConfigurerTests.* + + + + + + + + observation + + 3.3.6 + + + + + org.springframework.boot + spring-boot-dependencies + pom + import + ${spring-boot.version} + + + + + + io.micrometer + micrometer-tracing-bridge-brave + 1.3.4 + test + + + org.springframework.boot + spring-boot-starter-web + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-actuator + test + + + org.assertj + assertj-core + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + **/wsdl/**/* + + + **/ObservationInWsConfigurerTests.* + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + true + + + spring-boot.run.profiles + observation + + + + **/ObservationInWsConfigurerTests.java + + + + + + docs diff --git a/spring-ws-core/src/main/java/org/springframework/ws/soap/addressing/server/AnnotationActionEndpointMapping.java b/spring-ws-core/src/main/java/org/springframework/ws/soap/addressing/server/AnnotationActionEndpointMapping.java index 465af85d5..f9905c5c4 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/soap/addressing/server/AnnotationActionEndpointMapping.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/soap/addressing/server/AnnotationActionEndpointMapping.java @@ -21,9 +21,7 @@ import java.net.URI; import java.net.URISyntaxException; -import org.springframework.aop.support.AopUtils; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.StringUtils; import org.springframework.ws.server.endpoint.MethodEndpoint; @@ -54,11 +52,12 @@ * incoming message. * * @author Arjen Poutsma + * @author Corneil du Plessis (with thanks to Chris Bono) * @see Action * @see Address * @since 1.5.0 */ -public class AnnotationActionEndpointMapping extends AbstractActionMethodEndpointMapping implements BeanPostProcessor { +public class AnnotationActionEndpointMapping extends AbstractActionMethodEndpointMapping implements SmartInitializingSingleton { /** Returns the 'endpoint' annotation type. Default is {@link Endpoint}. */ protected Class getEndpointAnnotationType() { @@ -133,16 +132,8 @@ private URI getActionUri(String action, MethodEndpoint methodEndpoint) { } } - @Override - public final Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - return bean; - } - - @Override - public final Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (AopUtils.getTargetClass(bean).getAnnotation(getEndpointAnnotationType()) != null) { - registerMethods(bean); - } - return bean; + public void afterSingletonsInstantiated() { + this.getApplicationContext().getBeansWithAnnotation(this.getEndpointAnnotationType()) + .values().forEach(this::registerMethods); } } diff --git a/spring-ws-core/src/test/java/org/springframework/ws/observation/ObservationInWsConfigurerTests.java b/spring-ws-core/src/test/java/org/springframework/ws/observation/ObservationInWsConfigurerTests.java new file mode 100644 index 000000000..5d0a8b259 --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/observation/ObservationInWsConfigurerTests.java @@ -0,0 +1,74 @@ +package org.springframework.ws.observation; + +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.ws.config.annotation.WsConfigurerAdapter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This test is executed by using observation Maven profile and explicitly excluding the default profile. + * This test relies on dependencies that cause problems with other tests in Spring WS Core. + * @author Corneil du Plessis + */ +@Profile("observation") +@SpringBootTest(classes = ObservationInWsConfigurerTests.WsTracingApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ObservationInWsConfigurerTests { + @LocalServerPort + private int port; + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void responseShouldNotBeEmpty() { + final ResponseEntity response = + restTemplate.getForEntity("http://localhost:" + port + "/test", String.class); + assertThat(response.getBody()).isNotEmpty(); + } + + @RestController + public static class TestEndpoint { + private static final Logger log = LoggerFactory.getLogger(TestEndpoint.class); + + @GetMapping("/test") + public String test() { + log.info("test"); + return MDC.get("spanId"); + } + } + + @Configuration + public static class WsConfig extends WsConfigurerAdapter { + private final ObservationRegistry observationRegistry; + + public WsConfig(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + } + + @SpringBootApplication + @Import(WsConfig.class) + public static class WsTracingApplication { + public static void main(String[] args) { + SpringApplication.run(WsTracingApplication.class, args); + } + + } +} From 00a18f6875146a80b130be7237ff06641bffd211 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 13:37:03 +0200 Subject: [PATCH 08/26] Update Develocity Maven Conventations extension to 0.0.22 (#1445) Updates `io.spring.develocity.conventions:develocity-conventions-maven:0.0.19` to `0.0.22` --- .mvn/extensions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index a36ba0531..fe8298f97 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -3,6 +3,6 @@ io.spring.develocity.conventions develocity-conventions-maven-extension - 0.0.19 + 0.0.22 \ No newline at end of file From 71f2505b5cae676643f1e888f9932dd95e0a97b3 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 13:51:19 +0200 Subject: [PATCH 09/26] Exclude jakarta.xml.bind-api from spring-oxm Excludes jakarta.xml.bind-api from spring-oxm to ensure consistent version is present in project. --- spring-ws-core/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spring-ws-core/pom.xml b/spring-ws-core/pom.xml index 23beaf950..14d5720c7 100644 --- a/spring-ws-core/pom.xml +++ b/spring-ws-core/pom.xml @@ -34,6 +34,12 @@ org.springframework spring-oxm + + + jakarta.xml.bind + jakarta.xml.bind-api + + org.springframework From 65c5abfc85d2e3440682b92396b9650433691851 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 14:06:01 +0200 Subject: [PATCH 10/26] Apply default profile to prevent observations test being compiled. --- Jenkinsfile | 2 +- ci/build-and-deploy-to-artifactory.sh | 2 +- ci/build-and-deploy-to-maven-central.sh | 2 +- ci/smoke-test-against-artifactory.sh | 2 +- ci/smoke-test-against-maven-central.sh | 2 +- ci/test.sh | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index bc34f8b63..713d4af11 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -294,7 +294,7 @@ pipeline { docker.withRegistry('', "${p['dockerhub.credentials']}") { docker.image(p['docker.java.main.image']).inside(p['docker.java.inside.basic']) { sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ' + - './mvnw -s settings.xml -Pjakarta-ee-10,distribute,docs ' + + './mvnw -s settings.xml -Pjakarta-ee-10,distribute,docs,default ' + '-Dartifactory.server=https://repo.spring.io ' + "-Dartifactory.username=${ARTIFACTORY_USR} " + "-Dartifactory.password=${ARTIFACTORY_PSW} " + diff --git a/ci/build-and-deploy-to-artifactory.sh b/ci/build-and-deploy-to-artifactory.sh index e8e5eb91b..c1abc1e7f 100755 --- a/ci/build-and-deploy-to-artifactory.sh +++ b/ci/build-and-deploy-to-artifactory.sh @@ -8,6 +8,6 @@ echo 'Deploying to Artifactory...' MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" ./mvnw \ -s settings.xml \ - -Pjakarta-ee-10,distribute,${RELEASE_TYPE} \ + -Pjakarta-ee-10,distribute,${RELEASE_TYPE},default \ -Dmaven.test.skip=true \ clean deploy -B diff --git a/ci/build-and-deploy-to-maven-central.sh b/ci/build-and-deploy-to-maven-central.sh index 5a48d3762..2c93cdf9e 100755 --- a/ci/build-and-deploy-to-maven-central.sh +++ b/ci/build-and-deploy-to-maven-central.sh @@ -15,7 +15,7 @@ cp $KEYRING $GNUPGHOME MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" ./mvnw \ -s settings.xml \ - -Pjakarta-ee-10,distribute,central \ + -Pjakarta-ee-10,distribute,central,default \ -Dmaven.test.skip=true \ -Dgpg.passphrase=${PASSPHRASE} \ -Dgpg.secretKeyring=${GNUPGHOME}/secring.gpg \ diff --git a/ci/smoke-test-against-artifactory.sh b/ci/smoke-test-against-artifactory.sh index d2255919c..a8a024aad 100755 --- a/ci/smoke-test-against-artifactory.sh +++ b/ci/smoke-test-against-artifactory.sh @@ -9,7 +9,7 @@ echo 'Smoke test against Artifactory...' cd smoke-tests MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" ./mvnw \ - -Partifactory \ + -Partifactory,default \ -Dspring-ws.version="${PROJECT_VERSION}" \ clean dependency:purge-local-repository verify -B -U diff --git a/ci/smoke-test-against-maven-central.sh b/ci/smoke-test-against-maven-central.sh index f39235b39..2993edeb3 100755 --- a/ci/smoke-test-against-maven-central.sh +++ b/ci/smoke-test-against-maven-central.sh @@ -10,7 +10,7 @@ echo 'Smoke test against Maven Central...' cd smoke-tests MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" ./mvnw \ - -Pmaven-central \ + -Pmaven-central,default \ -DstagingRepositoryId="${STAGING_REPOSITORY_ID}" \ -Dspring-ws.version="${PROJECT_VERSION}" \ clean dependency:purge-local-repository verify -B -U diff --git a/ci/test.sh b/ci/test.sh index dc5352f8c..28a0dce23 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -4,8 +4,8 @@ set -euo pipefail MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" \ ./mvnw -s settings.xml \ - -P${PROFILE} clean dependency:list test -Dsort -B -U + -P${PROFILE},default clean dependency:list test -Dsort -B -U # Provides for testing org.springframework.ws.observation.ObservationInWsConfigurerTests separately MAVEN_OPTS="-Duser.name=spring-builds+jenkins -Duser.home=/tmp/jenkins-home" \ ./mvnw -s settings.xml \ - -P-default,observation clean dependency:list test -Dsort -B -U + -P-default,${PROFILE},observation clean dependency:list test -Dsort -B -U From 0655531e56f46e6096b6b4c483fd185ce7c84776 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 14:21:17 +0200 Subject: [PATCH 11/26] Fix saaj-impl version. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8efe8ff64..6bb6260c9 100644 --- a/pom.xml +++ b/pom.xml @@ -119,7 +119,7 @@ 2.19.0 1.4.7 4.11.0 - 3.0.3 + 3.0.4 2.0.6 4.3.5 From 0bd5441f9a893b735d8ad56a06e64d38fa68fcd9 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 14:40:52 +0200 Subject: [PATCH 12/26] Fix jakarta-soap-api version. --- pom.xml | 8 ++++++-- spring-xml/pom.xml | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 6bb6260c9..4919841ed 100644 --- a/pom.xml +++ b/pom.xml @@ -109,7 +109,7 @@ 2.1.3 2.1.3 6.0.0 - 3.0.1 + 3.0.2 4.0.2 4.0.5 1.1.6 @@ -181,7 +181,11 @@ pom import - + + jakarta.xml.soap + jakarta.xml.soap-api + ${jakarta-soap.version} + org.eclipse.jetty jetty-bom diff --git a/spring-xml/pom.xml b/spring-xml/pom.xml index 58ab49a0f..8ee17386b 100644 --- a/spring-xml/pom.xml +++ b/spring-xml/pom.xml @@ -44,7 +44,11 @@ 4.4 test - + + com.sun.xml.messaging.saaj + saaj-impl + ${saaj-impl.version} + From d4b489ad95524c54886af8b98a9e08e6c34ceb9d Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 15:00:58 +0200 Subject: [PATCH 13/26] Update stax-ex version management --- pom.xml | 17 ++++++++++++++++- spring-ws-security/pom.xml | 1 - 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 4919841ed..fb8926dc8 100644 --- a/pom.xml +++ b/pom.xml @@ -199,7 +199,22 @@ guava 32.1.3-jre - + + com.sun.xml.messaging.saaj + saaj-impl + ${saaj-impl.version} + + + org.jvnet.staxex + stax-ex + ${stax.version} + + + jakarta.activation + jakarta.activation-api + + + diff --git a/spring-ws-security/pom.xml b/spring-ws-security/pom.xml index 882d993fe..c8638daad 100644 --- a/spring-ws-security/pom.xml +++ b/spring-ws-security/pom.xml @@ -178,7 +178,6 @@ org.jvnet.staxex stax-ex - ${stax.version} jakarta.activation From f6e9dfc976a41353211594482fe1830cb113db69 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 15:23:10 +0200 Subject: [PATCH 14/26] Update pipeline and docker image to 17.0.13_10-jdk-jammmy --- ci/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/Dockerfile b/ci/Dockerfile index 0a718839c..dcca3565a 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:17.0.6_10-jdk-focal +FROM eclipse-temurin:17.0.13_10-jdk-jammmy RUN apt-get update && apt-get install -y gpg From 21bc108f4c513532d65437e009f9e6c16b669d41 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 15:36:57 +0200 Subject: [PATCH 15/26] Update pipeline and docker image to 17-jdk-jammmy --- ci/Dockerfile | 2 +- ci/pipeline.properties | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ci/Dockerfile b/ci/Dockerfile index dcca3565a..775ba1a61 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:17.0.13_10-jdk-jammmy +FROM eclipse-temurin:17-jdk-jammmy RUN apt-get update && apt-get install -y gpg diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 09915555f..09e8a0025 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,7 +1,7 @@ # Java versions -java.legacy.tag=8u362-b09-jdk-focal -java.main.tag=17.0.6_10-jdk-focal -java.next.tag=20-jdk-jammy +java.legacy.tag=8-jdk-jammy +java.main.tag=17-jdk-jammy +java.next.tag=21-jdk-jammy # Docker container images - standard docker.java.legacy.image=library/eclipse-temurin:${java.legacy.tag} From 2ae681a8f3dbc255946e8d35d67f1ab70547a171 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 15:48:40 +0200 Subject: [PATCH 16/26] Update pipeline and docker image to 17-jdk-focal --- ci/Dockerfile | 2 +- ci/pipeline.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/Dockerfile b/ci/Dockerfile index 775ba1a61..ac4675070 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:17-jdk-jammmy +FROM eclipse-temurin:17-jdk-focal RUN apt-get update && apt-get install -y gpg diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 09e8a0025..33fae0457 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,6 +1,6 @@ # Java versions -java.legacy.tag=8-jdk-jammy -java.main.tag=17-jdk-jammy +java.legacy.tag=8-jdk-focal +java.main.tag=17-jdk-focal java.next.tag=21-jdk-jammy # Docker container images - standard From b4f3bb69fdce66fc919797fc82b3d42486e7931f Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 16:08:35 +0200 Subject: [PATCH 17/26] Update pipeline and docker image to 17-jdk-jammy --- ci/Dockerfile | 2 +- ci/pipeline.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/Dockerfile b/ci/Dockerfile index ac4675070..0a6137aab 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:17-jdk-focal +FROM eclipse-temurin:17-jdk-jammy RUN apt-get update && apt-get install -y gpg diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 33fae0457..f52f0d9b3 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,6 +1,6 @@ # Java versions java.legacy.tag=8-jdk-focal -java.main.tag=17-jdk-focal +java.main.tag=17-jdk-jammy java.next.tag=21-jdk-jammy # Docker container images - standard From 65ba6ee1a9a0a77932ccb7fe8d39ef5b2b45f2c3 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 16:15:53 +0200 Subject: [PATCH 18/26] Fix observation profile. --- spring-ws-core/pom.xml | 109 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/spring-ws-core/pom.xml b/spring-ws-core/pom.xml index 14d5720c7..e79beae2c 100644 --- a/spring-ws-core/pom.xml +++ b/spring-ws-core/pom.xml @@ -363,6 +363,115 @@ + + default + + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + **/ObservationInWsConfigurerTests.* + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + **/ObservationInWsConfigurerTests.* + + + + + + + + observation + + 3.3.6 + + + + + org.springframework.boot + spring-boot-dependencies + pom + import + ${spring-boot.version} + + + + + + io.micrometer + micrometer-tracing-bridge-brave + 1.3.4 + test + + + org.springframework.boot + spring-boot-starter-web + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-actuator + test + + + org.assertj + assertj-core + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + **/wsdl/**/* + + + **/ObservationInWsConfigurerTests.* + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + true + + + spring-boot.run.profiles + observation + + + + **/ObservationInWsConfigurerTests.java + + + + + + docs From ee314f1a30a594f8bc3a4f3c883ccb4597f753a3 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 16:23:19 +0200 Subject: [PATCH 19/26] Added Build Status --- README.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.adoc b/README.adoc index 00fdfc856..3fc63ee4f 100644 --- a/README.adoc +++ b/README.adoc @@ -5,6 +5,7 @@ image:https://spring.io/img/projects/logo-web-services.svg["Spring Web Services" image:https://jenkins.spring.io/buildStatus/icon?job=spring-ws%2Fmain)[link='https://jenkins.spring.io/job/spring-ws/job/main/'] +image:https://jenkins.spring.io/buildStatus/icon?job=spring-ws%2F4.0.x)[link='https://jenkins.spring.io/job/spring-ws/job/4.0.x/'] Spring Web Services is a product of the Spring community focused on creating document-driven Web services. Spring Web Services aims to facilitate From 2bd390e28c56fbbced22a5d0aecbe5d2bb513f07 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 16:32:46 +0200 Subject: [PATCH 20/26] Fix profile duplication error. --- spring-ws-core/pom.xml | 109 ----------------------------------------- 1 file changed, 109 deletions(-) diff --git a/spring-ws-core/pom.xml b/spring-ws-core/pom.xml index e79beae2c..14d5720c7 100644 --- a/spring-ws-core/pom.xml +++ b/spring-ws-core/pom.xml @@ -363,115 +363,6 @@ - - default - - true - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - - **/ObservationInWsConfigurerTests.* - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.2 - - - **/ObservationInWsConfigurerTests.* - - - - - - - - observation - - 3.3.6 - - - - - org.springframework.boot - spring-boot-dependencies - pom - import - ${spring-boot.version} - - - - - - io.micrometer - micrometer-tracing-bridge-brave - 1.3.4 - test - - - org.springframework.boot - spring-boot-starter-web - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-actuator - test - - - org.assertj - assertj-core - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - - **/wsdl/**/* - - - **/ObservationInWsConfigurerTests.* - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.2 - - true - - - spring-boot.run.profiles - observation - - - - **/ObservationInWsConfigurerTests.java - - - - - - docs From 13de5b39d7ce2ea79939d4ec9fb75b514829eb85 Mon Sep 17 00:00:00 2001 From: Corneil du Plessis Date: Wed, 18 Dec 2024 17:01:10 +0200 Subject: [PATCH 21/26] Reduce logo size [skip ci] --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 3fc63ee4f..db09bb860 100644 --- a/README.adoc +++ b/README.adoc @@ -1,5 +1,5 @@ :doctype: book -image:https://spring.io/img/projects/logo-web-services.svg["Spring Web Services", width=50%, link="https://spring.io/projects/spring-ws#learn"] +image:https://spring.io/img/projects/logo-web-services.svg["Spring Web Services", width=20%, link="https://spring.io/projects/spring-ws#learn"] = Spring Web Services From f8ba714a93020d1d02087681c9318cd61430249d Mon Sep 17 00:00:00 2001 From: Johan Kindgren Date: Wed, 18 Dec 2024 16:32:42 +0100 Subject: [PATCH 22/26] Add Observation implementation (#1438) * Add Observation implementation * Move versions to parent pom properties * Set micrometer dependency as optional * Add license header and documentation * Add Javadoc Changed name of key "localname" to "localpart" Removed superfluos method implementations Modified implementation of ContextualName * Updated ContextualName to match Span name conventions * Replaced Assert with WarnThenDebugLogger * Modified log-message * Added nullability annotations * Improved exeptionhandling and logging * Extract common logic to ObservationHelper * Fix for DomSource-handling See #1094 --- pom.xml | 1 + spring-ws-core/pom.xml | 12 + .../DefaultWebServiceTemplateConvention.java | 106 ++++ .../WebServiceObservationInterceptor.java | 152 ++++++ .../WebServiceTemplateConvention.java | 31 ++ .../WebServiceTemplateObservationContext.java | 102 ++++ ...rviceTemplateObservationDocumentation.java | 134 +++++ .../DefaultWebServiceEndpointConvention.java | 123 +++++ .../EndpointObservationDocumentation.java | 137 +++++ .../observation/ObservationInterceptor.java | 149 ++++++ .../WebServiceEndpointContext.java | 104 ++++ .../WebServiceEndpointConvention.java | 32 ++ .../ws/support/ObservationHelper.java | 139 +++++ ...iceTemplateObservationIntegrationTest.java | 474 ++++++++++++++++++ .../endpoint/observation/MyEndpoint.java | 72 +++ ...ObservationInterceptorIntegrationTest.java | 138 +++++ .../observation/WebServiceConfig.java | 77 +++ .../ws/support/ObservationHelperTest.java | 65 +++ 18 files changed, 2048 insertions(+) create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java create mode 100644 spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java create mode 100644 spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java create mode 100644 spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java create mode 100644 spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java create mode 100644 spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java create mode 100644 spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java diff --git a/pom.xml b/pom.xml index fb8926dc8..be6842688 100644 --- a/pom.xml +++ b/pom.xml @@ -154,6 +154,7 @@ 1.78 1.14.13 0.0.5 + 1.13.5 diff --git a/spring-ws-core/pom.xml b/spring-ws-core/pom.xml index 14d5720c7..51694f05c 100644 --- a/spring-ws-core/pom.xml +++ b/spring-ws-core/pom.xml @@ -66,6 +66,12 @@ + + io.micrometer + micrometer-observation + ${micrometer-observation.version} + true + org.springframework spring-test @@ -250,6 +256,12 @@ spring-webflux test + + io.micrometer + micrometer-observation-test + ${micrometer-observation.version} + test + diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java new file mode 100644 index 000000000..baf5452ed --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/DefaultWebServiceTemplateConvention.java @@ -0,0 +1,106 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.client.core.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import org.springframework.ws.client.core.observation.WebServiceTemplateObservationDocumentation.LowCardinalityKeyNames; + +/** + * ObservationConvention that describes how a WebServiceTemplate is observed. + * @author Johan Kindgren + */ +public class DefaultWebServiceTemplateConvention implements WebServiceTemplateConvention { + + private static final KeyValue EXCEPTION_NONE = KeyValue.of(LowCardinalityKeyNames.EXCEPTION, + KeyValue.NONE_VALUE); + private static final String NAME = "webservice.client"; + + @Override + public KeyValues getHighCardinalityKeyValues(WebServiceTemplateObservationContext context) { + if (context.getPath() != null) { + return KeyValues.of(path(context)); + } + return KeyValues.empty(); + } + + @Override + public KeyValues getLowCardinalityKeyValues(WebServiceTemplateObservationContext context) { + return KeyValues.of( + exception(context), + host(context), + localname(context), + namespace(context), + outcome(context), + soapAction(context)); + } + + private KeyValue path(WebServiceTemplateObservationContext context) { + + return WebServiceTemplateObservationDocumentation.HighCardinalityKeyNames + .PATH + .withValue(context.getPath()); + } + + private KeyValue localname(WebServiceTemplateObservationContext context) { + return LowCardinalityKeyNames + .LOCALPART + .withValue(context.getLocalPart()); + } + + private KeyValue namespace(WebServiceTemplateObservationContext context) { + return LowCardinalityKeyNames + .NAMESPACE + .withValue(context.getNamespace()); + } + private KeyValue host(WebServiceTemplateObservationContext context) { + return LowCardinalityKeyNames + .HOST + .withValue(context.getHost()); + } + + + private KeyValue outcome(WebServiceTemplateObservationContext context) { + return LowCardinalityKeyNames + .OUTCOME + .withValue(context.getOutcome()); + } + + private KeyValue soapAction(WebServiceTemplateObservationContext context) { + return LowCardinalityKeyNames + .SOAPACTION + .withValue(context.getSoapAction()); + } + + private KeyValue exception(WebServiceTemplateObservationContext context) { + if (context.getError() != null) { + return LowCardinalityKeyNames + .EXCEPTION + .withValue(context.getError().getClass().getSimpleName()); + } + return EXCEPTION_NONE; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getContextualName(WebServiceTemplateObservationContext context) { + return context.getContextualName(); + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java new file mode 100644 index 000000000..f545c44d2 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceObservationInterceptor.java @@ -0,0 +1,152 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.client.core.observation; + +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.ws.FaultAwareWebServiceMessage; +import org.springframework.ws.WebServiceMessage; +import org.springframework.ws.client.WebServiceClientException; +import org.springframework.ws.client.support.interceptor.ClientInterceptorAdapter; +import org.springframework.ws.context.MessageContext; +import org.springframework.ws.soap.SoapMessage; +import org.springframework.ws.support.ObservationHelper; +import org.springframework.ws.transport.HeadersAwareSenderWebServiceConnection; +import org.springframework.ws.transport.TransportConstants; +import org.springframework.ws.transport.WebServiceConnection; +import org.springframework.ws.transport.context.TransportContext; +import org.springframework.ws.transport.context.TransportContextHolder; + +import javax.xml.namespace.QName; +import javax.xml.transform.Source; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Interceptor that creates an Observation for each operation. + * + * @author Johan Kindgren + * @see Observation + * @see io.micrometer.observation.ObservationConvention + */ +public class WebServiceObservationInterceptor extends ClientInterceptorAdapter { + + private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(WebServiceObservationInterceptor.class); + private static final String OBSERVATION_KEY = "observation"; + private static final WebServiceTemplateConvention DEFAULT_CONVENTION = new DefaultWebServiceTemplateConvention(); + + private final ObservationRegistry observationRegistry; + + private final WebServiceTemplateConvention customConvention; + private final ObservationHelper observationHelper; + + public WebServiceObservationInterceptor( + @NonNull + ObservationRegistry observationRegistry, + @NonNull + ObservationHelper observationHelper, + @Nullable + WebServiceTemplateConvention customConvention) { + + this.observationRegistry = observationRegistry; + this.observationHelper = observationHelper; + this.customConvention = customConvention; + } + + + @Override + public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException { + + TransportContext transportContext = TransportContextHolder.getTransportContext(); + HeadersAwareSenderWebServiceConnection connection = + (HeadersAwareSenderWebServiceConnection) transportContext.getConnection(); + + Observation observation = WebServiceTemplateObservationDocumentation.WEB_SERVICE_TEMPLATE.start( + customConvention, + DEFAULT_CONVENTION, + () -> new WebServiceTemplateObservationContext(connection), + observationRegistry); + + messageContext.setProperty(OBSERVATION_KEY, observation); + + return true; + } + + @Override + public void afterCompletion(MessageContext messageContext, Exception ex) { + + Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY); + if (observation == null) { + WARN_THEN_DEBUG_LOGGER.log("Missing expected Observation in messageContext; the request will not be observed."); + return; + } + + WebServiceTemplateObservationContext context = (WebServiceTemplateObservationContext) observation.getContext(); + + WebServiceMessage request = messageContext.getRequest(); + WebServiceMessage response = messageContext.getResponse(); + + if (request instanceof SoapMessage soapMessage) { + + Source source = soapMessage.getSoapBody().getPayloadSource(); + QName root = observationHelper.getRootElement(source); + if (root != null) { + context.setLocalPart(root.getLocalPart()); + context.setNamespace(root.getNamespaceURI()); + } + if (soapMessage.getSoapAction() != null && !soapMessage.getSoapAction().equals(TransportConstants.EMPTY_SOAP_ACTION)) { + context.setSoapAction(soapMessage.getSoapAction()); + } + } + + if (ex == null) { + context.setOutcome("success"); + } else { + context.setError(ex); + context.setOutcome("fault"); + } + + if (response instanceof FaultAwareWebServiceMessage faultAwareResponse) { + if (faultAwareResponse.hasFault()) { + context.setOutcome("fault"); + } + } + + URI uri = getUriFromConnection(); + if (uri != null) { + context.setHost(uri.getHost()); + context.setPath(uri.getPath()); + } + + context.setContextualName("POST"); + + observation.stop(); + } + + URI getUriFromConnection() { + TransportContext transportContext = TransportContextHolder.getTransportContext(); + WebServiceConnection connection = transportContext.getConnection(); + try { + return connection.getUri(); + } catch (URISyntaxException e) { + return null; + } + } +} + diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java new file mode 100644 index 000000000..fe2d96daa --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateConvention.java @@ -0,0 +1,31 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.client.core.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * ObservationConvention that can be implemented to create a custom observation. + * @author Johan Kindgren + */ +public interface WebServiceTemplateConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof WebServiceTemplateObservationContext; + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java new file mode 100644 index 000000000..d58e4f2f3 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationContext.java @@ -0,0 +1,102 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.client.core.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; +import io.micrometer.observation.transport.RequestReplySenderContext; +import org.springframework.ws.transport.HeadersAwareSenderWebServiceConnection; +import org.springframework.ws.transport.TransportInputStream; + +import java.io.IOException; +/** + * ObservationContext used to instrument a WebServiceTemplate operation. + * @author Johan Kindgren + */ +public class WebServiceTemplateObservationContext extends RequestReplySenderContext { + + private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(WebServiceTemplateObservationContext.class); + + public static final String UNKNOWN = "unknown"; + private String outcome = UNKNOWN; + private String localPart = UNKNOWN; + private String namespace = UNKNOWN; + private String host = UNKNOWN; + private String soapAction = KeyValue.NONE_VALUE; + private String path = null; + + public WebServiceTemplateObservationContext(HeadersAwareSenderWebServiceConnection connection) { + super((carrier, key, value) -> { + + if (carrier != null) { + try { + carrier.addRequestHeader(key, value); + } catch (IOException e) { + WARN_THEN_DEBUG_LOGGER.log("Could not add key to carrier", e); + } + } + }); + setCarrier(connection); + } + + public String getOutcome() { + return outcome; + } + + public void setOutcome(String outcome) { + this.outcome = outcome; + } + + public String getLocalPart() { + return localPart; + } + + public void setLocalPart(String localPart) { + this.localPart = localPart; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getSoapAction() { + return soapAction; + } + + public void setSoapAction(String soapAction) { + this.soapAction = soapAction; + } + + public void setPath(String path) { + this.path = path; + } + + public String getPath() { + return path; + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java new file mode 100644 index 000000000..9c4ff4758 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationDocumentation.java @@ -0,0 +1,134 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.client.core.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * ObservationDocumentation for WebSeviceTemplate. + * + * @author Johan Kindgren + */ +enum WebServiceTemplateObservationDocumentation implements ObservationDocumentation { + /** + * This enum constant defines observation documentation for the WebServiceTemplate. + * It provides the default observation convention and low cardinality key names + * relevant to WebService operations. + */ + WEB_SERVICE_TEMPLATE { + + @Override + public Class> getDefaultConvention() { + return DefaultWebServiceTemplateConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityKeyNames.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + }; + + enum HighCardinalityKeyNames implements KeyName { + /** + * Path for the client request. + * Optional value. + */ + PATH { + @Override + public String asString() { + return "path"; + } + + @Override + public boolean isRequired() { + return false; + } + } + } + + /** + * Enum representing low cardinality key names for observing a WebServiceTemplate. + */ + enum LowCardinalityKeyNames implements KeyName { + + /** + * Name of the exception thrown during the exchange, + * or {@value KeyValue#NONE_VALUE} if no exception happened. + */ + EXCEPTION { + @Override + public String asString() { + return "exception"; + } + }, + + /** + * Outcome of the WebService exchange. + */ + OUTCOME { + @Override + public String asString() { + return "outcome"; + } + }, + /** + * Namespace of the WebService payload. + */ + NAMESPACE { + @Override + public String asString() { + return "namespace"; + } + }, + + /** + * Localpart of the WebService payload. + */ + LOCALPART { + @Override + public String asString() { + return "localpart"; + } + }, + /** + * Host for the WebService call. + */ + HOST { + @Override + public String asString() { + return "host"; + } + }, + /** + * Value from the SoapAction header. + */ + SOAPACTION { + @Override + public String asString() { + return "soapaction"; + } + } + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java new file mode 100644 index 000000000..21e453425 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/DefaultWebServiceEndpointConvention.java @@ -0,0 +1,123 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.server.endpoint.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; + +/** + * Default ObservationConvention for a WebService Endpoint. + * @author Johan Kindgren + */ +public class DefaultWebServiceEndpointConvention implements WebServiceEndpointConvention { + + private static final KeyValue EXCEPTION_NONE = KeyValue.of(EndpointObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, KeyValue.NONE_VALUE); + private static final String NAME = "webservice.server"; + + + @Override + public KeyValues getLowCardinalityKeyValues(WebServiceEndpointContext context) { + return KeyValues.of( + exception(context), + localPart(context), + namespace(context), + outcome(context), + path(context), + soapAction(context)); + } + + @Override + public KeyValues getHighCardinalityKeyValues(WebServiceEndpointContext context) { + if (context.getPathInfo() != null) { + return KeyValues.of(pathInfo(context)); + } + return KeyValues.empty(); + } + + private KeyValue localPart(WebServiceEndpointContext context) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .LOCALPART + .withValue(context.getLocalPart()); + } + + private KeyValue namespace(WebServiceEndpointContext context) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .NAMESPACE + .withValue(context.getNamespace()); + } + + + private KeyValue outcome(WebServiceEndpointContext context) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .OUTCOME + .withValue(context.getOutcome()); + } + + private KeyValue soapAction(WebServiceEndpointContext context) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .SOAPACTION + .withValue(context.getSoapAction()); + } + + private KeyValue exception(WebServiceEndpointContext context) { + if (context.getError() != null) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .EXCEPTION + .withValue(context.getError().getClass().getSimpleName()); + } else { + return EXCEPTION_NONE; + } + } + + private KeyValue path(WebServiceEndpointContext context) { + return EndpointObservationDocumentation + .LowCardinalityKeyNames + .PATH + .withValue(context.getPath()); + } + + private KeyValue pathInfo(WebServiceEndpointContext context) { + if (context.getPathInfo() != null) { + return EndpointObservationDocumentation + .HighCardinalityKeyNames + .PATH_INFO + .withValue(context.getPathInfo()); + } + return null; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getContextualName(WebServiceEndpointContext context) { + return context.getContextualName(); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof WebServiceEndpointContext; + } + +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java new file mode 100644 index 000000000..37d782657 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/EndpointObservationDocumentation.java @@ -0,0 +1,137 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.server.endpoint.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * ObservationDocumentation for a WebService Endpoint. + * + * @author Johan Kindgren + */ +enum EndpointObservationDocumentation implements ObservationDocumentation { + /** + * An enumeration for ObservationDocumentation related to WebService Endpoint. + * + * The {@code WEB_SERVICE_ENDPOINT} provides default conventions and low cardinality key names for + * observing a WebService endpoint. + * + * This implementation returns the {@link DefaultWebServiceEndpointConvention} class as the default convention, + * and an array of {@link LowCardinalityKeyNames} for low cardinality key names. + */ + WEB_SERVICE_ENDPOINT { + @Override + public Class> getDefaultConvention() { + return DefaultWebServiceEndpointConvention.class; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return LowCardinalityKeyNames.values(); + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + }; + + enum HighCardinalityKeyNames implements KeyName { + + /** + * Possible + */ + PATH_INFO { + @Override + public String asString() { + return "pathinfo"; + } + + @Override + public boolean isRequired() { + return false; + } + } + } + + /** + * Enum representing low cardinality key names for observing a WebService endpoint. + */ + enum LowCardinalityKeyNames implements KeyName { + + /** + * Name of the exception thrown during the exchange, + * or {@value KeyValue#NONE_VALUE} if no exception happened. + */ + EXCEPTION { + @Override + public String asString() { + return "exception"; + } + }, + + /** + * Outcome of the WebService exchange. + */ + OUTCOME { + @Override + public String asString() { + return "outcome"; + } + }, + /** + * Namespace of the WebService payload. + */ + NAMESPACE { + @Override + public String asString() { + return "namespace"; + } + }, + /** + * Localpart of the WebService payload. + */ + LOCALPART { + @Override + public String asString() { + return "localpart"; + } + }, + + /** + * Value from the SoapAction header. + */ + SOAPACTION { + @Override + public String asString() { + return "soapaction"; + } + }, + /** + * Path for the current request. + */ + PATH { + @Override + public String asString() { + return "path"; + } + } + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java new file mode 100644 index 000000000..7f11f0974 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptor.java @@ -0,0 +1,149 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.server.endpoint.observation; + +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.ws.FaultAwareWebServiceMessage; +import org.springframework.ws.WebServiceMessage; +import org.springframework.ws.context.MessageContext; +import org.springframework.ws.server.endpoint.interceptor.EndpointInterceptorAdapter; +import org.springframework.ws.soap.SoapMessage; +import org.springframework.ws.support.ObservationHelper; +import org.springframework.ws.transport.HeadersAwareReceiverWebServiceConnection; +import org.springframework.ws.transport.TransportConstants; +import org.springframework.ws.transport.context.TransportContext; +import org.springframework.ws.transport.context.TransportContextHolder; +import org.springframework.ws.transport.http.HttpServletConnection; + +import javax.xml.namespace.QName; +import javax.xml.transform.Source; + +/** + * Interceptor implementation that creates an observation for a WebService Endpoint. + * @author Johan Kindgren + */ +public class ObservationInterceptor extends EndpointInterceptorAdapter { + + private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(ObservationInterceptor.class); + private static final String OBSERVATION_KEY = "observation"; + private static final WebServiceEndpointConvention DEFAULT_CONVENTION = new DefaultWebServiceEndpointConvention(); + + private final ObservationRegistry observationRegistry; + private final ObservationHelper observationHelper; + private final WebServiceEndpointConvention customConvention; + + public ObservationInterceptor( + @NonNull + ObservationRegistry observationRegistry, + @NonNull + ObservationHelper observationHelper, + @Nullable + WebServiceEndpointConvention customConvention) { + this.observationRegistry = observationRegistry; + this.observationHelper = observationHelper; + this.customConvention = customConvention; + } + + @Override + public boolean handleRequest(MessageContext messageContext, Object endpoint) throws Exception { + + TransportContext transportContext = TransportContextHolder.getTransportContext(); + HeadersAwareReceiverWebServiceConnection connection = + (HeadersAwareReceiverWebServiceConnection) transportContext.getConnection(); + + Observation observation = EndpointObservationDocumentation.WEB_SERVICE_ENDPOINT.start( + customConvention, + DEFAULT_CONVENTION, + () -> new WebServiceEndpointContext(connection), + observationRegistry); + + messageContext.setProperty(OBSERVATION_KEY, observation); + + return true; + } + + @Override + public void afterCompletion(MessageContext messageContext, Object endpoint, @Nullable Exception ex) { + + Observation observation = (Observation) messageContext.getProperty(OBSERVATION_KEY); + if (observation == null) { + WARN_THEN_DEBUG_LOGGER.log("Missing expected Observation in messageContext; the request will not be observed."); + return; + } + + WebServiceEndpointContext context = (WebServiceEndpointContext) observation.getContext(); + + WebServiceMessage request = messageContext.getRequest(); + WebServiceMessage response = messageContext.getResponse(); + + if (request instanceof SoapMessage soapMessage) { + + Source source = soapMessage.getSoapBody().getPayloadSource(); + QName root = observationHelper.getRootElement(source); + if (root != null) { + context.setLocalPart(root.getLocalPart()); + context.setNamespace(root.getNamespaceURI()); + } + String action = soapMessage.getSoapAction(); + if (!TransportConstants.EMPTY_SOAP_ACTION.equals(action)) { + context.setSoapAction(soapMessage.getSoapAction()); + } else { + context.setSoapAction("none"); + } + } + + if (ex == null) { + context.setOutcome("success"); + } else { + context.setError(ex); + context.setOutcome("fault"); + } + + if (response instanceof FaultAwareWebServiceMessage faultAwareResponse) { + if (faultAwareResponse.hasFault()) { + context.setOutcome("fault"); + } + } + + TransportContext transportContext = TransportContextHolder.getTransportContext(); + HeadersAwareReceiverWebServiceConnection connection = + (HeadersAwareReceiverWebServiceConnection) transportContext.getConnection(); + + if (connection instanceof HttpServletConnection servletConnection) { + HttpServletRequest servletRequest = servletConnection.getHttpServletRequest(); + String servletPath = servletRequest.getServletPath(); + String pathInfo = servletRequest.getPathInfo(); + + if (pathInfo != null) { + context.setContextualName("POST " + servletPath + "/{pathInfo}"); + context.setPath(servletPath + "/{pathInfo}"); + context.setPathInfo(pathInfo); + } else { + context.setPath(servletPath); + context.setContextualName("POST " + servletPath); + } + } else { + context.setContextualName("POST"); + } + + observation.stop(); + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java new file mode 100644 index 000000000..c4d9d3d0f --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointContext.java @@ -0,0 +1,104 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.server.endpoint.observation; + +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; +import io.micrometer.observation.transport.RequestReplyReceiverContext; +import org.springframework.ws.transport.HeadersAwareReceiverWebServiceConnection; +import org.springframework.ws.transport.TransportInputStream; + +import java.io.IOException; +import java.util.Iterator; + +/** + * ObservationContext that describes how a WebService Endpoint is observed. + * @author Johan Kindgren + */ +public class WebServiceEndpointContext extends RequestReplyReceiverContext { + + private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(WebServiceEndpointContext.class); + private static final String UNKNOWN = "unknown"; + + private String outcome = UNKNOWN; + private String localPart = UNKNOWN; + private String namespace = UNKNOWN; + private String soapAction = UNKNOWN; + private String path = UNKNOWN; + private String pathInfo = null; + + public WebServiceEndpointContext(HeadersAwareReceiverWebServiceConnection connection) { + super((carrier, key) -> { + try { + Iterator headers = carrier.getRequestHeaders(key); + if (headers.hasNext()) { + return headers.next(); + } + } catch (IOException e) { + WARN_THEN_DEBUG_LOGGER.log("Could not read key from carrier", e); + } + return null; + }); + setCarrier(connection); + } + + public String getOutcome() { + return outcome; + } + + public void setOutcome(String outcome) { + this.outcome = outcome; + } + + public String getLocalPart() { + return localPart; + } + + public void setLocalPart(String localPart) { + this.localPart = localPart; + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getSoapAction() { + return soapAction; + } + + public void setSoapAction(String soapAction) { + this.soapAction = soapAction; + } + + public void setPath(String path) { + this.path = path; + } + + public String getPath() { + return path; + } + + public void setPathInfo(String pathInfo) { + this.pathInfo = pathInfo; + } + + public String getPathInfo() { + return pathInfo; + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java new file mode 100644 index 000000000..4b5dd65ef --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/server/endpoint/observation/WebServiceEndpointConvention.java @@ -0,0 +1,32 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.server.endpoint.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; + +/** + * ObservationConvention that describes how a WebService Endpoint is observed. + * @author Johan Kindgren + */ +public interface WebServiceEndpointConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Observation.Context context) { + return context instanceof WebServiceEndpointContext; + } + +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java b/spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java new file mode 100644 index 000000000..39f77716e --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/support/ObservationHelper.java @@ -0,0 +1,139 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.support; + +import io.micrometer.common.util.internal.logging.WarnThenDebugLogger; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ws.server.endpoint.observation.ObservationInterceptor; +import org.w3c.dom.Node; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.namespace.QName; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; +import java.io.IOException; + +/** + * Helper class for observation tasks. + * @author Johan Kindgren + */ +public class ObservationHelper { + + private final Log logger = LogFactory.getLog(getClass()); + + private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(ObservationInterceptor.class); + private static final QName UNKNOWN_Q_NAME = new QName("unknown", "unknow"); + + private final SAXParser saxParser; + + public ObservationHelper() { + SAXParserFactory parserFactory = SAXParserFactory.newNSInstance(); + SAXParser parser = null; + try { + parser = parserFactory.newSAXParser(); + } catch (ParserConfigurationException | SAXException e) { + logger.warn("Could not create SAX parser, observation keys for Root element can be reported as 'unknown'.", e); + } + saxParser = parser; + } + + + /** + * Try to find the root element QName for the given source. + * If it isn't possible to extract the QName, a QName with the values 'unknown:unknown' is returned. + */ + public QName getRootElement(Source source) { + if (source instanceof DOMSource) { + Node payload = ((DOMSource) source).getNode(); + if (payload.getNodeType() == Node.ELEMENT_NODE) { + return new QName(payload.getNamespaceURI(), payload.getLocalName()); + } + return UNKNOWN_Q_NAME; + } + if (source instanceof StreamSource) { + if (saxParser == null) { + WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); + return UNKNOWN_Q_NAME; + } + RootElementSAXHandler handler = new RootElementSAXHandler(); + try { + saxParser.parse(getInputSource((StreamSource) source), handler); + return handler.getRootElementName(); + } catch (SAXException | IOException e) { + WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); + return UNKNOWN_Q_NAME; + } + } + if (source instanceof SAXSource) { + if (saxParser == null) { + WARN_THEN_DEBUG_LOGGER.log("SaxParser not available, reporting Root element as 'unknown'"); + return UNKNOWN_Q_NAME; + } + RootElementSAXHandler handler = new RootElementSAXHandler(); + try { + saxParser.parse(getInputSource((SAXSource) source), handler); + return handler.getRootElementName(); + } catch (SAXException | IOException e) { + WARN_THEN_DEBUG_LOGGER.log("Exception while handling request, reporting Root element as 'unknown'", e); + return UNKNOWN_Q_NAME; + } + } + return UNKNOWN_Q_NAME; + } + + InputSource getInputSource(StreamSource source) { + + if (source.getInputStream() != null) { + return new InputSource(source.getInputStream()); + } + return new InputSource(source.getReader()); + } + + InputSource getInputSource(SAXSource source) { + return source.getInputSource(); + } + + + + /** + * DefaultHandler that extracts the root elements namespace and name. + * @author Johan Kindgren + */ + static class RootElementSAXHandler extends DefaultHandler { + + private QName rootElementName = null; + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) { + if (rootElementName == null) { + rootElementName = new QName(uri, localName); + } + } + + public QName getRootElementName() { + return rootElementName; + } + } +} diff --git a/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java new file mode 100644 index 000000000..3ca5e06ff --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/client/core/observation/WebServiceTemplateObservationIntegrationTest.java @@ -0,0 +1,474 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.client.core.observation; + +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; +import jakarta.activation.CommandMap; +import jakarta.activation.DataHandler; +import jakarta.activation.MailcapCommandMap; +import jakarta.mail.util.ByteArrayDataSource; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.xml.soap.*; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.XmlMappingException; +import org.springframework.ws.client.WebServiceTransportException; +import org.springframework.ws.client.core.AbstractSoap12WebServiceTemplateIntegrationTestCase; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.client.support.interceptor.ClientInterceptor; +import org.springframework.ws.soap.SoapMessage; +import org.springframework.ws.soap.client.SoapFaultClientException; +import org.springframework.ws.soap.saaj.SaajSoapMessageFactory; +import org.springframework.ws.support.ObservationHelper; +import org.springframework.ws.transport.http.HttpComponentsMessageSender; +import org.springframework.ws.transport.support.FreePortScanner; +import org.springframework.xml.transform.StringResult; +import org.springframework.xml.transform.StringSource; +import org.springframework.xml.transform.TransformerFactoryUtils; +import org.xmlunit.assertj.XmlAssert; + +import javax.xml.transform.Result; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.StringTokenizer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Verifies observation for a WebServiceTemplate + * @author Johan Kindgren + */ +public class WebServiceTemplateObservationIntegrationTest { + + private TestObservationRegistry observationRegistry; + private ObservationHelper observationHelper; + + private static Server jettyServer; + + private static String baseUrl; + + private WebServiceTemplate template; + + private String messagePayload = ""; + + @BeforeAll + public static void startJetty() throws Exception { + + int port = FreePortScanner.getFreePort(); + baseUrl = "http://localhost:" + port; + + jettyServer = new Server(port); + Connector connector = new ServerConnector(jettyServer); + jettyServer.addConnector(connector); + + ServletContextHandler jettyContext = new ServletContextHandler(); + jettyContext.setContextPath("/"); + + jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.EchoSoapServlet.class, "/soap/echo"); + jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.SoapReceiverFaultServlet.class, "/soap/receiverFault"); + jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.SoapSenderFaultServlet.class, "/soap/senderFault"); + jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.NoResponseSoapServlet.class, "/soap/noResponse"); + jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.AttachmentsServlet.class, "/soap/attachment"); + + ServletHolder notfound = jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.ErrorServlet.class, "/errors/notfound"); + notfound.setInitParameter("sc", "404"); + + ServletHolder errors = jettyContext.addServlet(AbstractSoap12WebServiceTemplateIntegrationTestCase.ErrorServlet.class, "/errors/server"); + errors.setInitParameter("sc", "500"); + + jettyServer.setHandler(jettyContext); + jettyServer.start(); + } + + @AfterAll + public static void stopJetty() throws Exception { + + if (jettyServer.isRunning()) { + jettyServer.stop(); + } + } + + /** + * A workaround for the faulty XmlDataContentHandler in the SAAJ RI, which cannot handle mime types such as "text/xml; + * charset=UTF-8", causing issues with Axiom. We basically reset the command map + */ + @BeforeEach + public void removeXmlDataContentHandler() throws SOAPException { + + MessageFactory messageFactory = MessageFactory.newInstance(); + SOAPMessage message = messageFactory.createMessage(); + message.createAttachmentPart(); + CommandMap.setDefaultCommandMap(new MailcapCommandMap()); + } + + @BeforeEach + public void createWebServiceTemplate() throws Exception { + observationRegistry = TestObservationRegistry.create(); + observationHelper = new ObservationHelper(); + + template = new WebServiceTemplate(new SaajSoapMessageFactory(MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL))); + template.setMessageSender(new HttpComponentsMessageSender()); + template.setInterceptors(new ClientInterceptor[]{ + new WebServiceObservationInterceptor(observationRegistry, observationHelper, null) + }); + } + + + @Test + public void sendSourceAndReceiveToResult() { + + StringResult result = new StringResult(); + boolean b = template.sendSourceAndReceiveToResult(baseUrl + "/soap/echo", new StringSource(messagePayload), result); + + assertThat(b).isTrue(); + XmlAssert.assertThat(result.toString()).and(messagePayload).ignoreWhitespace().areIdentical(); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/soap/echo") + .hasContextualNameEqualTo("POST") + ); + } + + @Test + public void sendSourceAndReceiveToResultNoResponse() { + + boolean b = template.sendSourceAndReceiveToResult(baseUrl + "/soap/noResponse", new StringSource(messagePayload), + new StringResult()); + assertThat(b).isFalse(); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/soap/noResponse") + .hasContextualNameEqualTo("POST") + ); + } + + + @Test + public void marshalSendAndReceiveNoResponse() throws TransformerConfigurationException { + + final Transformer transformer = TransformerFactoryUtils.newInstance().newTransformer(); + final Object requestObject = new Object(); + Marshaller marshaller = new Marshaller() { + + @Override + public void marshal(Object graph, Result result) throws XmlMappingException, IOException { + + assertThat(requestObject).isEqualTo(graph); + + try { + transformer.transform(new StringSource(messagePayload), result); + } catch (TransformerException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean supports(Class clazz) { + + assertThat(clazz).isEqualTo(Object.class); + return true; + } + }; + + template.setMarshaller(marshaller); + Object result = template.marshalSendAndReceive(baseUrl + "/soap/noResponse", requestObject); + + assertThat(result).isNull(); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/soap/noResponse") + .hasContextualNameEqualTo("POST") + ); + } + + @Test + public void notFound() { + + assertThatExceptionOfType(WebServiceTransportException.class) + .isThrownBy(() -> template.sendSourceAndReceiveToResult(baseUrl + "/errors/notfound", + new StringSource(messagePayload), new StringResult())); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "fault") + .hasLowCardinalityKeyValue("exception", "WebServiceTransportException") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/errors/notfound") + .hasContextualNameEqualTo("POST") + ); + + } + + @Test + public void receiverFault() { + + Result result = new StringResult(); + + assertThatExceptionOfType(SoapFaultClientException.class).isThrownBy(() -> template + .sendSourceAndReceiveToResult(baseUrl + "/soap/receiverFault", new StringSource(messagePayload), result)); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "fault") + .hasLowCardinalityKeyValue("exception", "SoapFaultClientException") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/soap/receiverFault") + .hasContextualNameEqualTo("POST") + ); + } + + @Test + public void senderFault() { + + Result result = new StringResult(); + + assertThatExceptionOfType(SoapFaultClientException.class).isThrownBy(() -> template + .sendSourceAndReceiveToResult(baseUrl + "/soap/senderFault", new StringSource(messagePayload), result)); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "fault") + .hasLowCardinalityKeyValue("exception", "SoapFaultClientException") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/soap/senderFault") + .hasContextualNameEqualTo("POST") + ); + } + + @Test + public void attachment() { + + template.sendSourceAndReceiveToResult(baseUrl + "/soap/attachment", new StringSource(messagePayload), message -> { + + SoapMessage soapMessage = (SoapMessage) message; + final String attachmentContent = "content"; + soapMessage.addAttachment("attachment-1", + new DataHandler(new ByteArrayDataSource(attachmentContent, "text/plain"))); + }, new StringResult()); + + TestObservationRegistryAssert.assertThat(observationRegistry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("host", "localhost") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "root") + .hasHighCardinalityKeyValue("path", "/soap/attachment") + .hasContextualNameEqualTo("POST") + ); + } + + /** + * Servlet that returns and error message for a given status code. + */ + @SuppressWarnings("serial") + public static class ErrorServlet extends HttpServlet { + + private int sc; + + private ErrorServlet(int sc) { + this.sc = sc; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.sendError(sc); + } + } + + /** + * Abstract SOAP Servlet + */ + @SuppressWarnings("serial") + public abstract static class AbstractSoapServlet extends HttpServlet { + + protected MessageFactory messageFactory = null; + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + + super.init(servletConfig); + + try { + messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL); + } catch (SOAPException ex) { + throw new ServletException("Unable to create message factory" + ex.getMessage()); + } + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException { + + try { + MimeHeaders headers = getHeaders(req); + SOAPMessage request = messageFactory.createMessage(headers, req.getInputStream()); + SOAPMessage reply = onMessage(request); + + if (reply != null) { + reply.saveChanges(); + SOAPBody replyBody = reply.getSOAPBody(); + if (!replyBody.hasFault()) { + resp.setStatus(HttpServletResponse.SC_OK); + } else { + if (replyBody.getFault().getFaultCodeAsQName().equals(SOAPConstants.SOAP_SENDER_FAULT)) { + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + + } else { + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + putHeaders(reply.getMimeHeaders(), resp); + reply.writeTo(resp.getOutputStream()); + } else { + resp.setStatus(HttpServletResponse.SC_ACCEPTED); + } + } catch (Exception ex) { + throw new ServletException("SAAJ POST failed " + ex.getMessage(), ex); + } + } + + private MimeHeaders getHeaders(HttpServletRequest httpServletRequest) { + + Enumeration enumeration = httpServletRequest.getHeaderNames(); + MimeHeaders headers = new MimeHeaders(); + + while (enumeration.hasMoreElements()) { + String headerName = (String) enumeration.nextElement(); + String headerValue = httpServletRequest.getHeader(headerName); + StringTokenizer values = new StringTokenizer(headerValue, ","); + while (values.hasMoreTokens()) { + headers.addHeader(headerName, values.nextToken().trim()); + } + } + + return headers; + } + + private void putHeaders(MimeHeaders headers, HttpServletResponse res) { + + Iterator it = headers.getAllHeaders(); + + while (it.hasNext()) { + MimeHeader header = (MimeHeader) it.next(); + String[] values = headers.getHeader(header.getName()); + for (String value : values) { + res.addHeader(header.getName(), value); + } + } + } + + protected abstract SOAPMessage onMessage(SOAPMessage message) throws SOAPException; + } + + @SuppressWarnings("serial") + public static class EchoSoapServlet extends AbstractSoap12WebServiceTemplateIntegrationTestCase.AbstractSoapServlet { + + @Override + protected SOAPMessage onMessage(SOAPMessage message) { + return message; + } + } + + @SuppressWarnings("serial") + public static class NoResponseSoapServlet extends AbstractSoap12WebServiceTemplateIntegrationTestCase.AbstractSoapServlet { + + @Override + protected SOAPMessage onMessage(SOAPMessage message) { + return null; + } + } + + @SuppressWarnings("serial") + public static class SoapReceiverFaultServlet extends AbstractSoap12WebServiceTemplateIntegrationTestCase.AbstractSoapServlet { + + @Override + protected SOAPMessage onMessage(SOAPMessage message) throws SOAPException { + + SOAPMessage response = messageFactory.createMessage(); + SOAPBody body = response.getSOAPBody(); + body.addFault(SOAPConstants.SOAP_RECEIVER_FAULT, "Receiver Fault"); + return response; + } + } + + @SuppressWarnings("serial") + public static class SoapSenderFaultServlet extends AbstractSoap12WebServiceTemplateIntegrationTestCase.AbstractSoapServlet { + + @Override + protected SOAPMessage onMessage(SOAPMessage message) throws SOAPException { + + SOAPMessage response = messageFactory.createMessage(); + SOAPBody body = response.getSOAPBody(); + body.addFault(SOAPConstants.SOAP_SENDER_FAULT, "Sender Fault"); + return response; + } + } + + @SuppressWarnings("serial") + public static class AttachmentsServlet extends AbstractSoap12WebServiceTemplateIntegrationTestCase.AbstractSoapServlet { + + @Override + protected SOAPMessage onMessage(SOAPMessage message) { + + assertThat(message.countAttachments()).isEqualTo(1); + return null; + } + } + +} diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java new file mode 100644 index 000000000..1540791ac --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/MyEndpoint.java @@ -0,0 +1,72 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.server.endpoint.observation; + +import jakarta.xml.bind.annotation.XmlRootElement; +import org.springframework.ws.server.endpoint.annotation.Endpoint; +import org.springframework.ws.server.endpoint.annotation.PayloadRoot; +import org.springframework.ws.server.endpoint.annotation.RequestPayload; +import org.springframework.ws.server.endpoint.annotation.ResponsePayload; + +/** + * Testing endpoint. + * @author Johan Kindgren + */ +@Endpoint +public class MyEndpoint { + + private static final String NAMESPACE_URI = "http://springframework.org/spring-ws"; + + @PayloadRoot(namespace = NAMESPACE_URI, localPart = "request") + @ResponsePayload + public MyResponse handleRequest(@RequestPayload MyRequest request) { + MyResponse myResponse = new MyResponse(); + myResponse.setMessage("Hello " + request.getName()); + return myResponse; + } + + + + @XmlRootElement(namespace = NAMESPACE_URI, name = "request") + static class MyRequest { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @XmlRootElement(namespace = NAMESPACE_URI, name = "response") + static class MyResponse { + + private String message; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} + + diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java new file mode 100644 index 000000000..47aa838c3 --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/ObservationInterceptorIntegrationTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.server.endpoint.observation; + +import io.micrometer.observation.tck.TestObservationRegistry; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.jupiter.api.*; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.transport.http.MessageDispatcherServlet; +import org.springframework.ws.transport.support.FreePortScanner; + +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerFactory; + +import static io.micrometer.observation.tck.TestObservationRegistryAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Verifies observation for a WebService Endpoint. + * @author Johan Kindgren + */ +public class ObservationInterceptorIntegrationTest { + + private static AnnotationConfigWebApplicationContext applicationContext; + private WebServiceTemplate webServiceTemplate; + private TestObservationRegistry registry; + private static Server server; + + private final String requestPayload = ""; + + private TransformerFactory transformerFactory = TransformerFactory.newInstance(); + private Transformer transformer; + + private static String baseUrl; + + @BeforeAll + public static void startServer() throws Exception { + + int port = FreePortScanner.getFreePort(); + + baseUrl = "http://localhost:" + port + "/ws"; + + server = new Server(port); + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/"); + + applicationContext = new AnnotationConfigWebApplicationContext(); + applicationContext.scan(WebServiceConfig.class.getPackage().getName()); + + MessageDispatcherServlet dispatcherServlet = new MessageDispatcherServlet(applicationContext); + dispatcherServlet.setTransformWsdlLocations(true); + + ServletHolder servletHolder = new ServletHolder(dispatcherServlet); + context.addServlet(servletHolder, "/ws/*"); + + server.setHandler(context); + server.start(); + } + + @AfterAll + static void tearDown() throws Exception { + applicationContext.close(); + server.stop(); + } + + @BeforeEach + void setUp() throws TransformerConfigurationException { + + webServiceTemplate = applicationContext.getBean(WebServiceTemplate.class); + registry = applicationContext.getBean(TestObservationRegistry.class); + + transformer = transformerFactory.newTransformer(); + } + + @Test + void testObservationInterceptorBehavior() { + + MyEndpoint.MyRequest request = new MyEndpoint.MyRequest(); + request.setName("John"); + MyEndpoint.MyResponse response = (MyEndpoint.MyResponse) webServiceTemplate.marshalSendAndReceive(baseUrl, request); + + // Assertions based on expected behavior of ObservationInterceptor + assertNotNull(response); + + assertThat(registry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "request") + .hasLowCardinalityKeyValue("soapaction", "none") + .hasLowCardinalityKeyValue("path", "/ws") + .hasContextualNameEqualTo("POST /ws") + .hasNameEqualTo("webservice.server") + ); + } + + @Test + void testPathWithVariable() { + + MyEndpoint.MyRequest request = new MyEndpoint.MyRequest(); + request.setName("John"); + MyEndpoint.MyResponse response = (MyEndpoint.MyResponse) webServiceTemplate.marshalSendAndReceive(baseUrl + "/1234", request); + + // Assertions based on expected behavior of ObservationInterceptor + assertNotNull(response); + + assertThat(registry).hasAnObservation(observationContextAssert -> + observationContextAssert + .hasLowCardinalityKeyValue("outcome", "success") + .hasLowCardinalityKeyValue("exception", "none") + .hasLowCardinalityKeyValue("namespace", "http://springframework.org/spring-ws") + .hasLowCardinalityKeyValue("localpart", "request") + .hasLowCardinalityKeyValue("soapaction", "none") + .hasLowCardinalityKeyValue("path", "/ws/{pathInfo}") + .hasContextualNameEqualTo("POST /ws/{pathInfo}") + .hasHighCardinalityKeyValue("pathinfo", "/1234") + .hasNameEqualTo("webservice.server") + ); + } +} \ No newline at end of file diff --git a/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java new file mode 100644 index 000000000..38026f7fa --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/server/endpoint/observation/WebServiceConfig.java @@ -0,0 +1,77 @@ +/* + * Copyright 2005-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 + * + * http://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.ws.server.endpoint.observation; + +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.oxm.jaxb.Jaxb2Marshaller; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.config.annotation.EnableWs; +import org.springframework.ws.config.annotation.WsConfigurerAdapter; +import org.springframework.ws.server.EndpointInterceptor; +import org.springframework.ws.support.ObservationHelper; + +import java.util.List; + +/** + * Verifies observation for a WebService Endpoint. + * @author Johan Kindgren + */ +@EnableWs +@Configuration +public class WebServiceConfig extends WsConfigurerAdapter { + + @Autowired + private ObservationRegistry observationRegistry; + + @Bean + public Jaxb2Marshaller marshaller() { + Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); + marshaller.setClassesToBeBound(MyEndpoint.MyRequest.class, MyEndpoint.MyResponse.class); + return marshaller; + } + + @Bean + public ObservationRegistry observationRegistry() { + return TestObservationRegistry.create(); + } + + @Bean + public WebServiceTemplate webServiceTemplate(Jaxb2Marshaller marshaller) { + WebServiceTemplate webServiceTemplate = new WebServiceTemplate(); + webServiceTemplate.setMarshaller(marshaller); + webServiceTemplate.setUnmarshaller(marshaller); + return webServiceTemplate; + } + + @Bean + public EndpointInterceptor observationInterceptor() { + return new ObservationInterceptor(observationRegistry, observationHelper(),null); // Replace with your actual interceptor + } + @Bean + public ObservationHelper observationHelper() { + return new ObservationHelper(); + } + + @Override + public void addInterceptors(List interceptors) { + interceptors.add(observationInterceptor()); + } + +} \ No newline at end of file diff --git a/spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java b/spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java new file mode 100644 index 000000000..0a1b756e4 --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/support/ObservationHelperTest.java @@ -0,0 +1,65 @@ +package org.springframework.ws.support; + +import org.dom4j.Namespace; +import org.dom4j.dom.DOMElement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.xml.transform.StringSource; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; +import org.xmlunit.builder.Input; + +import javax.xml.namespace.QName; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.sax.SAXSource; +import java.io.StringReader; + +import static org.assertj.core.api.Assertions.assertThat; + +class ObservationHelperTest { + + private ObservationHelper helper; + + @BeforeEach + void setUp() { + helper = new ObservationHelper(); + } + + @Test + void getRootElementStreamSource() { + + StringSource source = new StringSource(""); + + QName name = helper.getRootElement(source); + assertThat(name.getLocalPart()).isEqualTo("root"); + assertThat(name.getNamespaceURI()).isEqualTo("http://springframework.org/spring-ws"); + } + + @Test + void getRootElementDomSource() { + + DOMElement payloadElement = new DOMElement( + new org.dom4j.QName("root", + new Namespace(null, "http://springframework.org/spring-ws"))); + payloadElement.addElement("child"); + + QName name = helper.getRootElement(Input.from(payloadElement).build()); + assertThat(name.getLocalPart()).isEqualTo("root"); + assertThat(name.getNamespaceURI()).isEqualTo("http://springframework.org/spring-ws"); + } + + @Test + void getRootElementSaxSource() throws Exception { + StringReader reader = new StringReader(""); + + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser saxParser = factory.newSAXParser(); + XMLReader xmlReader = saxParser.getXMLReader(); + + SAXSource saxSource = new SAXSource(xmlReader, new InputSource(reader)); + QName name = helper.getRootElement(saxSource); + assertThat(name.getLocalPart()).isEqualTo("root"); + assertThat(name.getNamespaceURI()).isEqualTo("http://springframework.org/spring-ws"); + } +} \ No newline at end of file From 3214e8c5eede0cea876c94a93814cb984c20a532 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 16:10:02 +0200 Subject: [PATCH 23/26] Bump com.thoughtworks.xstream:xstream from 1.4.20 to 1.4.21 (#1444) Bumps [com.thoughtworks.xstream:xstream](https://github.com/x-stream/xstream) from 1.4.20 to 1.4.21. - [Release notes](https://github.com/x-stream/xstream/releases) - [Commits](https://github.com/x-stream/xstream/commits) --- updated-dependencies: - dependency-name: com.thoughtworks.xstream:xstream dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index be6842688..4a5fa8e04 100644 --- a/pom.xml +++ b/pom.xml @@ -682,7 +682,7 @@ com.thoughtworks.xstream xstream - 1.4.20 + 1.4.21 From 95c60718894f53958e54e2b5faf9775f426609c7 Mon Sep 17 00:00:00 2001 From: Todor Dinev Date: Tue, 4 Feb 2025 15:07:37 +0100 Subject: [PATCH 24/26] fix: Typo in introduction of security doc Signed-off-by: Todor Dinev --- src/main/asciidoctor/security.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/asciidoctor/security.adoc b/src/main/asciidoctor/security.adoc index c43ce797d..d0f8a3553 100644 --- a/src/main/asciidoctor/security.adoc +++ b/src/main/asciidoctor/security.adoc @@ -7,7 +7,7 @@ This chapter explains how to add WS-Security aspects to your Web services. We fo * *Digital signatures*: The digital signature of a message is a piece of information based on both the document and the signer's private key. It is created through the use of a hash function and a private signing function (encrypting with the signer's private key). -* *Encryption and Decryption*: Encryption is the process of transforming data into a form that is impossible to read without the appropriate key. It is mainly used to keep information hidden from anyone for whom it is not intended. Decryption is the reverse of encryption. It is the process of transforming encrypted data back into an readable form. +* *Encryption and Decryption*: Encryption is the process of transforming data into a form that is impossible to read without the appropriate key. It is mainly used to keep information hidden from anyone for whom it is not intended. Decryption is the reverse of encryption. It is the process of transforming encrypted data back into a readable form. These three areas are implemented by using the `XwsSecurityInterceptor` or `Wss4jSecurityInterceptor`, which we describe in <> and <>, respectively From eabc05a37f19ab8f884e41402edba077b2f7fe80 Mon Sep 17 00:00:00 2001 From: Todor Dinev Date: Tue, 4 Feb 2025 16:07:29 +0100 Subject: [PATCH 25/26] fix: Add space before backtick in security doc Signed-off-by: Todor Dinev --- src/main/asciidoctor/security.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/asciidoctor/security.adoc b/src/main/asciidoctor/security.adoc index d0f8a3553..f432f52cf 100644 --- a/src/main/asciidoctor/security.adoc +++ b/src/main/asciidoctor/security.adoc @@ -302,7 +302,7 @@ The `SpringDigestPasswordValidationCallbackHandler` requires a Spring Security ` [[security-certificate-authentication]] ==== Certificate Authentication -A more secure way of authentication uses X509 certificates. In this scenario, the SOAP message contains a`BinarySecurityToken`, which contains a Base 64-encoded version of a X509 certificate. The certificate is used by the recipient to authenticate. The certificate stored in the message is also used to sign the message (see <>). +A more secure way of authentication uses X509 certificates. In this scenario, the SOAP message contains a `BinarySecurityToken`, which contains a Base 64-encoded version of a X509 certificate. The certificate is used by the recipient to authenticate. The certificate stored in the message is also used to sign the message (see <>). To make sure that all incoming SOAP messages carry a`BinarySecurityToken`, the security policy file should contain a `RequireSignature` element. This element can further carry other elements, which are covered in <>. You can find a reference of possible child elements http://java.sun.com/webservices/docs/1.6/tutorial/doc/XWS-SecurityIntro4.html#wp565769[here]. The following listing shows how to define a `RequireSignature` element: From 075db8591b00972d3cb6f5fda94aa141cef54aac Mon Sep 17 00:00:00 2001 From: Todor Dinev Date: Tue, 4 Feb 2025 18:52:28 +0100 Subject: [PATCH 26/26] fix: Add space before backtick in security doc, again Signed-off-by: Todor Dinev --- src/main/asciidoctor/security.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/asciidoctor/security.adoc b/src/main/asciidoctor/security.adoc index f432f52cf..2aff6f425 100644 --- a/src/main/asciidoctor/security.adoc +++ b/src/main/asciidoctor/security.adoc @@ -304,7 +304,7 @@ The `SpringDigestPasswordValidationCallbackHandler` requires a Spring Security ` A more secure way of authentication uses X509 certificates. In this scenario, the SOAP message contains a `BinarySecurityToken`, which contains a Base 64-encoded version of a X509 certificate. The certificate is used by the recipient to authenticate. The certificate stored in the message is also used to sign the message (see <>). -To make sure that all incoming SOAP messages carry a`BinarySecurityToken`, the security policy file should contain a `RequireSignature` element. This element can further carry other elements, which are covered in <>. You can find a reference of possible child elements http://java.sun.com/webservices/docs/1.6/tutorial/doc/XWS-SecurityIntro4.html#wp565769[here]. The following listing shows how to define a `RequireSignature` element: +To make sure that all incoming SOAP messages carry a `BinarySecurityToken`, the security policy file should contain a `RequireSignature` element. This element can further carry other elements, which are covered in <>. You can find a reference of possible child elements http://java.sun.com/webservices/docs/1.6/tutorial/doc/XWS-SecurityIntro4.html#wp565769[here]. The following listing shows how to define a `RequireSignature` element: ==== [source,xml]