diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java b/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java index ab2b6369422..5d5868e8b9c 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,14 +33,16 @@ import org.springframework.util.StringUtils; /** * An adapter for a target JMS {@link jakarta.jms.ConnectionFactory}, applying the - * given user credentials to every standard {@code createConnection()} call, - * that is, implicitly invoking {@code createConnection(username, password)} - * on the target. All other methods simply delegate to the corresponding methods - * of the target ConnectionFactory. + * given user credentials to every standard methods that can also be used with + * authentication, this {@code createConnection()} and {@code createContext()}. In + * other words, it is implicitly invoking {@code createConnection(username, password)} or + * {@code createContext(username, password)}} on the target. All other methods simply + * delegate to the corresponding methods of the target ConnectionFactory. * *
Can be used to proxy a target JNDI ConnectionFactory that does not have user * credentials configured. Client code can work with the ConnectionFactory without - * passing in username and password on every {@code createConnection()} call. + * passing in username and password on every {@code createConnection()} and + * {@code createContext()} call. * *
In the following example, client code can simply transparently work * with the preconfigured "myConnectionFactory", implicitly accessing @@ -58,9 +60,9 @@ import org.springframework.util.StringUtils; * </bean> * *
If the "username" is empty, this proxy will simply delegate to the standard - * {@code createConnection()} method of the target ConnectionFactory. - * This can be used to keep a UserCredentialsConnectionFactoryAdapter bean - * definition just for the option of implicitly passing in user credentials + * {@code createConnection()} or {@code createContext()} method of the target + * ConnectionFactory. This can be used to keep a UserCredentialsConnectionFactoryAdapter + * bean definition just for the option of implicitly passing in user credentials * if the particular target ConnectionFactory requires it. * *
As of Spring Framework 5, this class delegates JMS 2.0 {@code JMSContext} @@ -69,8 +71,10 @@ import org.springframework.util.StringUtils; * as long as no actual JMS 2.0 calls are triggered by the application's setup. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 1.2 * @see #createConnection + * @see #createContext * @see #createQueueConnection * @see #createTopicConnection */ @@ -296,7 +300,22 @@ public class UserCredentialsConnectionFactoryAdapter @Override public JMSContext createContext() { - return obtainTargetConnectionFactory().createContext(); + JmsUserCredentials threadCredentials = this.threadBoundCredentials.get(); + if (threadCredentials != null) { + return doCreateContext(threadCredentials.username, threadCredentials.password); + } + else { + return doCreateContext(this.username, this.password); + } + } + + protected JMSContext doCreateContext(@Nullable String username, @Nullable String password) { + if (StringUtils.hasLength(username)) { + return obtainTargetConnectionFactory().createContext(username, password); + } + else { + return obtainTargetConnectionFactory().createContext(); + } } @Override @@ -311,7 +330,22 @@ public class UserCredentialsConnectionFactoryAdapter @Override public JMSContext createContext(int sessionMode) { - return obtainTargetConnectionFactory().createContext(sessionMode); + JmsUserCredentials threadCredentials = this.threadBoundCredentials.get(); + if (threadCredentials != null) { + return doCreateContext(threadCredentials.username, threadCredentials.password, sessionMode); + } + else { + return doCreateContext(this.username, this.password, sessionMode); + } + } + + protected JMSContext doCreateContext(@Nullable String username, @Nullable String password, int sessionMode) { + if (StringUtils.hasLength(username)) { + return obtainTargetConnectionFactory().createContext(username, password, sessionMode); + } + else { + return obtainTargetConnectionFactory().createContext(sessionMode); + } } private ConnectionFactory obtainTargetConnectionFactory() { diff --git a/spring-jms/src/test/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapterTests.java b/spring-jms/src/test/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapterTests.java new file mode 100644 index 00000000000..f3ea7f97a04 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapterTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jms.connection; + +import jakarta.jms.ConnectionFactory; +import jakarta.jms.JMSContext; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests for {@link UserCredentialsConnectionFactoryAdapter}. + * + * @author Stephane Nicoll + */ +class UserCredentialsConnectionFactoryAdapterTests { + + private static final JMSContext MOCK_CONTEXT = mock(JMSContext.class); + + private final ConnectionFactory target; + + private final UserCredentialsConnectionFactoryAdapter adapter; + + UserCredentialsConnectionFactoryAdapterTests() { + this.target = mock(ConnectionFactory.class); + this.adapter = new UserCredentialsConnectionFactoryAdapter(); + this.adapter.setTargetConnectionFactory(this.target); + } + + @Test + void createContextWhenNoAuthentication() { + given(this.target.createContext()).willReturn(MOCK_CONTEXT); + assertThat(this.adapter.createContext()).isSameAs(MOCK_CONTEXT); + verify(this.target).createContext(); + verifyNoMoreInteractions(this.target); + } + + @Test + void createContextWhenAuthentication() { + this.adapter.setUsername("user"); + this.adapter.setPassword("password"); + given(this.target.createContext("user", "password")).willReturn(MOCK_CONTEXT); + assertThat(this.adapter.createContext()).isSameAs(MOCK_CONTEXT); + verify(this.target).createContext("user", "password"); + verifyNoMoreInteractions(this.target); + } + + @Test + void createContextWhenThreadLevelAuthentication() { + this.adapter.setCredentialsForCurrentThread("user", "password"); + given(this.target.createContext("user", "password")).willReturn(MOCK_CONTEXT); + assertThat(this.adapter.createContext()).isSameAs(MOCK_CONTEXT); + verify(this.target).createContext("user", "password"); + verifyNoMoreInteractions(this.target); + } + + @Test + void createContextWhenAuthenticationAndThreadLevelAuthentication() { + this.adapter.setCredentialsForCurrentThread("specific", "secret"); + this.adapter.setUsername("user"); + this.adapter.setPassword("password"); + given(this.target.createContext("specific", "secret")).willReturn(MOCK_CONTEXT); + assertThat(this.adapter.createContext()).isSameAs(MOCK_CONTEXT); + verify(this.target).createContext("specific", "secret"); + verifyNoMoreInteractions(this.target); + } + + @Test + void createContextWithSessionModeWhenNoAuthentication() { + given(this.target.createContext(1)).willReturn(MOCK_CONTEXT); + assertThat(this.adapter.createContext(1)).isSameAs(MOCK_CONTEXT); + verify(this.target).createContext(1); + verifyNoMoreInteractions(this.target); + } + + @Test + void createContextWithSessionModeWhenAuthentication() { + this.adapter.setUsername("user"); + this.adapter.setPassword("password"); + given(this.target.createContext("user", "password", 1)).willReturn(MOCK_CONTEXT); + assertThat(this.adapter.createContext(1)).isSameAs(MOCK_CONTEXT); + verify(this.target).createContext("user", "password", 1); + verifyNoMoreInteractions(this.target); + } + + @Test + void createContextWithSessionModeWhenThreadLevelAuthentication() { + this.adapter.setCredentialsForCurrentThread("user", "password"); + given(this.target.createContext("user", "password", 1)).willReturn(MOCK_CONTEXT); + assertThat(this.adapter.createContext(1)).isSameAs(MOCK_CONTEXT); + verify(this.target).createContext("user", "password", 1); + verifyNoMoreInteractions(this.target); + } + + @Test + void createContextWithSessionModeWhenAuthenticationAndThreadLevelAuthentication() { + this.adapter.setCredentialsForCurrentThread("specific", "secret"); + this.adapter.setUsername("user"); + this.adapter.setPassword("password"); + given(this.target.createContext("specific", "secret", 1)).willReturn(MOCK_CONTEXT); + assertThat(this.adapter.createContext(1)).isSameAs(MOCK_CONTEXT); + verify(this.target).createContext("specific", "secret", 1); + verifyNoMoreInteractions(this.target); + } + + @Test + void createContextWithUsernamePasswordIgnoresAuthentication() { + this.adapter.setUsername("user"); + this.adapter.setPassword("password"); + given(this.target.createContext("specific", "secret")).willReturn(MOCK_CONTEXT); + assertThat(this.adapter.createContext("specific", "secret")).isSameAs(MOCK_CONTEXT); + verify(this.target).createContext("specific", "secret"); + verifyNoMoreInteractions(this.target); + } + + @Test + void createContextWithSessionModeAndUsernamePasswordIgnoresAuthentication() { + this.adapter.setUsername("user"); + this.adapter.setPassword("password"); + given(this.target.createContext("specific", "secret", 1)).willReturn(MOCK_CONTEXT); + assertThat(this.adapter.createContext("specific", "secret", 1)).isSameAs(MOCK_CONTEXT); + verify(this.target).createContext("specific", "secret", 1); + verifyNoMoreInteractions(this.target); + } + +}