001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.jms.connection;
018
019import javax.jms.Connection;
020import javax.jms.ConnectionFactory;
021import javax.jms.JMSContext;
022import javax.jms.JMSException;
023import javax.jms.QueueConnection;
024import javax.jms.QueueConnectionFactory;
025import javax.jms.TopicConnection;
026import javax.jms.TopicConnectionFactory;
027
028import org.springframework.beans.factory.InitializingBean;
029import org.springframework.core.NamedThreadLocal;
030import org.springframework.lang.Nullable;
031import org.springframework.util.Assert;
032import org.springframework.util.StringUtils;
033
034/**
035 * An adapter for a target JMS {@link javax.jms.ConnectionFactory}, applying the
036 * given user credentials to every standard {@code createConnection()} call,
037 * that is, implicitly invoking {@code createConnection(username, password)}
038 * on the target. All other methods simply delegate to the corresponding methods
039 * of the target ConnectionFactory.
040 *
041 * <p>Can be used to proxy a target JNDI ConnectionFactory that does not have user
042 * credentials configured. Client code can work with the ConnectionFactory without
043 * passing in username and password on every {@code createConnection()} call.
044 *
045 * <p>In the following example, client code can simply transparently work
046 * with the preconfigured "myConnectionFactory", implicitly accessing
047 * "myTargetConnectionFactory" with the specified user credentials.
048 *
049 * <pre class="code">
050 * &lt;bean id="myTargetConnectionFactory" class="org.springframework.jndi.JndiObjectFactoryBean"&gt;
051 *   &lt;property name="jndiName" value="java:comp/env/jms/mycf"/&gt;
052 * &lt;/bean&gt;
053 *
054 * &lt;bean id="myConnectionFactory" class="org.springframework.jms.connection.UserCredentialsConnectionFactoryAdapter"&gt;
055 *   &lt;property name="targetConnectionFactory" ref="myTargetConnectionFactory"/&gt;
056 *   &lt;property name="username" value="myusername"/&gt;
057 *   &lt;property name="password" value="mypassword"/&gt;
058 * &lt;/bean></pre>
059 *
060 * <p>If the "username" is empty, this proxy will simply delegate to the standard
061 * {@code createConnection()} method of the target ConnectionFactory.
062 * This can be used to keep a UserCredentialsConnectionFactoryAdapter bean
063 * definition just for the <i>option</i> of implicitly passing in user credentials
064 * if the particular target ConnectionFactory requires it.
065 *
066 * <p>As of Spring Framework 5, this class delegates JMS 2.0 {@code JMSContext}
067 * calls and therefore requires the JMS 2.0 API to be present at runtime.
068 * It may nevertheless run against a JMS 1.1 driver (bound to the JMS 2.0 API)
069 * as long as no actual JMS 2.0 calls are triggered by the application's setup.
070 *
071 * @author Juergen Hoeller
072 * @since 1.2
073 * @see #createConnection
074 * @see #createQueueConnection
075 * @see #createTopicConnection
076 */
077public class UserCredentialsConnectionFactoryAdapter
078                implements ConnectionFactory, QueueConnectionFactory, TopicConnectionFactory, InitializingBean {
079
080        @Nullable
081        private ConnectionFactory targetConnectionFactory;
082
083        @Nullable
084        private String username;
085
086        @Nullable
087        private String password;
088
089        private final ThreadLocal<JmsUserCredentials> threadBoundCredentials =
090                        new NamedThreadLocal<>("Current JMS user credentials");
091
092
093        /**
094         * Set the target ConnectionFactory that this ConnectionFactory should delegate to.
095         */
096        public void setTargetConnectionFactory(ConnectionFactory targetConnectionFactory) {
097                Assert.notNull(targetConnectionFactory, "'targetConnectionFactory' must not be null");
098                this.targetConnectionFactory = targetConnectionFactory;
099        }
100
101        /**
102         * Set the username that this adapter should use for retrieving Connections.
103         * Default is no specific user.
104         */
105        public void setUsername(String username) {
106                this.username = username;
107        }
108
109        /**
110         * Set the password that this adapter should use for retrieving Connections.
111         * Default is no specific password.
112         */
113        public void setPassword(String password) {
114                this.password = password;
115        }
116
117        @Override
118        public void afterPropertiesSet() {
119                if (this.targetConnectionFactory == null) {
120                        throw new IllegalArgumentException("Property 'targetConnectionFactory' is required");
121                }
122        }
123
124
125        /**
126         * Set user credententials for this proxy and the current thread.
127         * The given username and password will be applied to all subsequent
128         * {@code createConnection()} calls on this ConnectionFactory proxy.
129         * <p>This will override any statically specified user credentials,
130         * that is, values of the "username" and "password" bean properties.
131         * @param username the username to apply
132         * @param password the password to apply
133         * @see #removeCredentialsFromCurrentThread
134         */
135        public void setCredentialsForCurrentThread(String username, String password) {
136                this.threadBoundCredentials.set(new JmsUserCredentials(username, password));
137        }
138
139        /**
140         * Remove any user credentials for this proxy from the current thread.
141         * Statically specified user credentials apply again afterwards.
142         * @see #setCredentialsForCurrentThread
143         */
144        public void removeCredentialsFromCurrentThread() {
145                this.threadBoundCredentials.remove();
146        }
147
148
149        /**
150         * Determine whether there are currently thread-bound credentials,
151         * using them if available, falling back to the statically specified
152         * username and password (i.e. values of the bean properties) otherwise.
153         * @see #doCreateConnection
154         */
155        @Override
156        public final Connection createConnection() throws JMSException {
157                JmsUserCredentials threadCredentials = this.threadBoundCredentials.get();
158                if (threadCredentials != null) {
159                        return doCreateConnection(threadCredentials.username, threadCredentials.password);
160                }
161                else {
162                        return doCreateConnection(this.username, this.password);
163                }
164        }
165
166        /**
167         * Delegate the call straight to the target ConnectionFactory.
168         */
169        @Override
170        public Connection createConnection(String username, String password) throws JMSException {
171                return doCreateConnection(username, password);
172        }
173
174        /**
175         * This implementation delegates to the {@code createConnection(username, password)}
176         * method of the target ConnectionFactory, passing in the specified user credentials.
177         * If the specified username is empty, it will simply delegate to the standard
178         * {@code createConnection()} method of the target ConnectionFactory.
179         * @param username the username to use
180         * @param password the password to use
181         * @return the Connection
182         * @see javax.jms.ConnectionFactory#createConnection(String, String)
183         * @see javax.jms.ConnectionFactory#createConnection()
184         */
185        protected Connection doCreateConnection(@Nullable String username, @Nullable String password) throws JMSException {
186                ConnectionFactory target = obtainTargetConnectionFactory();
187                if (StringUtils.hasLength(username)) {
188                        return target.createConnection(username, password);
189                }
190                else {
191                        return target.createConnection();
192                }
193        }
194
195        /**
196         * Determine whether there are currently thread-bound credentials,
197         * using them if available, falling back to the statically specified
198         * username and password (i.e. values of the bean properties) else.
199         * @see #doCreateQueueConnection
200         */
201        @Override
202        public final QueueConnection createQueueConnection() throws JMSException {
203                JmsUserCredentials threadCredentials = this.threadBoundCredentials.get();
204                if (threadCredentials != null) {
205                        return doCreateQueueConnection(threadCredentials.username, threadCredentials.password);
206                }
207                else {
208                        return doCreateQueueConnection(this.username, this.password);
209                }
210        }
211
212        /**
213         * Delegate the call straight to the target QueueConnectionFactory.
214         */
215        @Override
216        public QueueConnection createQueueConnection(String username, String password) throws JMSException {
217                return doCreateQueueConnection(username, password);
218        }
219
220        /**
221         * This implementation delegates to the {@code createQueueConnection(username, password)}
222         * method of the target QueueConnectionFactory, passing in the specified user credentials.
223         * If the specified username is empty, it will simply delegate to the standard
224         * {@code createQueueConnection()} method of the target ConnectionFactory.
225         * @param username the username to use
226         * @param password the password to use
227         * @return the Connection
228         * @see javax.jms.QueueConnectionFactory#createQueueConnection(String, String)
229         * @see javax.jms.QueueConnectionFactory#createQueueConnection()
230         */
231        protected QueueConnection doCreateQueueConnection(
232                        @Nullable String username, @Nullable String password) throws JMSException {
233
234                ConnectionFactory target = obtainTargetConnectionFactory();
235                if (!(target instanceof QueueConnectionFactory)) {
236                        throw new javax.jms.IllegalStateException("'targetConnectionFactory' is not a QueueConnectionFactory");
237                }
238                QueueConnectionFactory queueFactory = (QueueConnectionFactory) target;
239                if (StringUtils.hasLength(username)) {
240                        return queueFactory.createQueueConnection(username, password);
241                }
242                else {
243                        return queueFactory.createQueueConnection();
244                }
245        }
246
247        /**
248         * Determine whether there are currently thread-bound credentials,
249         * using them if available, falling back to the statically specified
250         * username and password (i.e. values of the bean properties) else.
251         * @see #doCreateTopicConnection
252         */
253        @Override
254        public final TopicConnection createTopicConnection() throws JMSException {
255                JmsUserCredentials threadCredentials = this.threadBoundCredentials.get();
256                if (threadCredentials != null) {
257                        return doCreateTopicConnection(threadCredentials.username, threadCredentials.password);
258                }
259                else {
260                        return doCreateTopicConnection(this.username, this.password);
261                }
262        }
263
264        /**
265         * Delegate the call straight to the target TopicConnectionFactory.
266         */
267        @Override
268        public TopicConnection createTopicConnection(String username, String password) throws JMSException {
269                return doCreateTopicConnection(username, password);
270        }
271
272        /**
273         * This implementation delegates to the {@code createTopicConnection(username, password)}
274         * method of the target TopicConnectionFactory, passing in the specified user credentials.
275         * If the specified username is empty, it will simply delegate to the standard
276         * {@code createTopicConnection()} method of the target ConnectionFactory.
277         * @param username the username to use
278         * @param password the password to use
279         * @return the Connection
280         * @see javax.jms.TopicConnectionFactory#createTopicConnection(String, String)
281         * @see javax.jms.TopicConnectionFactory#createTopicConnection()
282         */
283        protected TopicConnection doCreateTopicConnection(
284                        @Nullable String username, @Nullable String password) throws JMSException {
285
286                ConnectionFactory target = obtainTargetConnectionFactory();
287                if (!(target instanceof TopicConnectionFactory)) {
288                        throw new javax.jms.IllegalStateException("'targetConnectionFactory' is not a TopicConnectionFactory");
289                }
290                TopicConnectionFactory queueFactory = (TopicConnectionFactory) target;
291                if (StringUtils.hasLength(username)) {
292                        return queueFactory.createTopicConnection(username, password);
293                }
294                else {
295                        return queueFactory.createTopicConnection();
296                }
297        }
298
299        @Override
300        public JMSContext createContext() {
301                return obtainTargetConnectionFactory().createContext();
302        }
303
304        @Override
305        public JMSContext createContext(String userName, String password) {
306                return obtainTargetConnectionFactory().createContext(userName, password);
307        }
308
309        @Override
310        public JMSContext createContext(String userName, String password, int sessionMode) {
311                return obtainTargetConnectionFactory().createContext(userName, password, sessionMode);
312        }
313
314        @Override
315        public JMSContext createContext(int sessionMode) {
316                return obtainTargetConnectionFactory().createContext(sessionMode);
317        }
318
319        private ConnectionFactory obtainTargetConnectionFactory() {
320                Assert.state(this.targetConnectionFactory != null, "'targetConnectionFactory' is required");
321                return this.targetConnectionFactory;
322        }
323
324
325        /**
326         * Inner class used as ThreadLocal value.
327         */
328        private static final class JmsUserCredentials {
329
330                public final String username;
331
332                public final String password;
333
334                public JmsUserCredentials(String username, String password) {
335                        this.username = username;
336                        this.password = password;
337                }
338
339                @Override
340                public String toString() {
341                        return "JmsUserCredentials[username='" + this.username + "',password='" + this.password + "']";
342                }
343        }
344
345}