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.jdbc.datasource;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.sql.Connection;
022import java.sql.SQLException;
023
024import javax.sql.DataSource;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028
029import org.springframework.lang.Nullable;
030import org.springframework.util.Assert;
031import org.springframework.util.ReflectionUtils;
032import org.springframework.util.StringUtils;
033
034/**
035 * {@link DataSource} implementation that delegates all calls to a WebSphere
036 * target {@link DataSource}, typically obtained from JNDI, applying a current
037 * isolation level and/or current user credentials to every Connection obtained
038 * from it.
039 *
040 * <p>Uses IBM-specific API to get a JDBC Connection with a specific isolation
041 * level (and read-only flag) from a WebSphere DataSource
042 * (<a href="https://publib.boulder.ibm.com/infocenter/wasinfo/v5r1//topic/com.ibm.websphere.base.doc/info/aes/ae/rdat_extiapi.html">IBM code example</a>).
043 * Supports the transaction-specific isolation level exposed by
044 * {@link org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionIsolationLevel()}.
045 * It's also possible to specify a default isolation level, to be applied when the
046 * current Spring-managed transaction does not define a specific isolation level.
047 *
048 * <p>Usage example, defining the target DataSource as an inner-bean JNDI lookup
049 * (of course, you can link to any WebSphere DataSource through a bean reference):
050 *
051 * <pre class="code">
052 * &lt;bean id="myDataSource" class="org.springframework.jdbc.datasource.WebSphereDataSourceAdapter"&gt;
053 *   &lt;property name="targetDataSource"&gt;
054 *     &lt;bean class="org.springframework.jndi.JndiObjectFactoryBean"&gt;
055 *       &lt;property name="jndiName" value="jdbc/myds"/&gt;
056 *     &lt;/bean&gt;
057 *   &lt;/property&gt;
058 * &lt;/bean&gt;</pre>
059 *
060 * Thanks to Ricardo Olivieri for submitting the original implementation
061 * of this approach!
062 *
063 * @author Juergen Hoeller
064 * @author <a href="mailto:[email protected]">Lari Hotari</a>
065 * @author <a href="mailto:[email protected]">Ricardo N. Olivieri</a>
066 * @since 2.0.3
067 * @see com.ibm.websphere.rsadapter.JDBCConnectionSpec
068 * @see com.ibm.websphere.rsadapter.WSDataSource#getConnection(com.ibm.websphere.rsadapter.JDBCConnectionSpec)
069 * @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionIsolationLevel()
070 * @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly()
071 */
072public class WebSphereDataSourceAdapter extends IsolationLevelDataSourceAdapter {
073
074        protected final Log logger = LogFactory.getLog(getClass());
075
076        private Class<?> wsDataSourceClass;
077
078        private Method newJdbcConnSpecMethod;
079
080        private Method wsDataSourceGetConnectionMethod;
081
082        private Method setTransactionIsolationMethod;
083
084        private Method setReadOnlyMethod;
085
086        private Method setUserNameMethod;
087
088        private Method setPasswordMethod;
089
090
091        /**
092         * This constructor retrieves the WebSphere JDBC connection spec API,
093         * so we can get obtain specific WebSphere Connections using reflection.
094         */
095        public WebSphereDataSourceAdapter() {
096                try {
097                        this.wsDataSourceClass = getClass().getClassLoader().loadClass("com.ibm.websphere.rsadapter.WSDataSource");
098                        Class<?> jdbcConnSpecClass = getClass().getClassLoader().loadClass("com.ibm.websphere.rsadapter.JDBCConnectionSpec");
099                        Class<?> wsrraFactoryClass = getClass().getClassLoader().loadClass("com.ibm.websphere.rsadapter.WSRRAFactory");
100                        this.newJdbcConnSpecMethod = wsrraFactoryClass.getMethod("createJDBCConnectionSpec");
101                        this.wsDataSourceGetConnectionMethod =
102                                        this.wsDataSourceClass.getMethod("getConnection", jdbcConnSpecClass);
103                        this.setTransactionIsolationMethod =
104                                        jdbcConnSpecClass.getMethod("setTransactionIsolation", int.class);
105                        this.setReadOnlyMethod = jdbcConnSpecClass.getMethod("setReadOnly", Boolean.class);
106                        this.setUserNameMethod = jdbcConnSpecClass.getMethod("setUserName", String.class);
107                        this.setPasswordMethod = jdbcConnSpecClass.getMethod("setPassword", String.class);
108                }
109                catch (Exception ex) {
110                        throw new IllegalStateException(
111                                        "Could not initialize WebSphereDataSourceAdapter because WebSphere API classes are not available: " + ex);
112                }
113        }
114
115        /**
116         * Checks that the specified 'targetDataSource' actually is
117         * a WebSphere WSDataSource.
118         */
119        @Override
120        public void afterPropertiesSet() {
121                super.afterPropertiesSet();
122
123                if (!this.wsDataSourceClass.isInstance(getTargetDataSource())) {
124                        throw new IllegalStateException(
125                                        "Specified 'targetDataSource' is not a WebSphere WSDataSource: " + getTargetDataSource());
126                }
127        }
128
129
130        /**
131         * Builds a WebSphere JDBCConnectionSpec object for the current settings
132         * and calls {@code WSDataSource.getConnection(JDBCConnectionSpec)}.
133         * @see #createConnectionSpec
134         * @see com.ibm.websphere.rsadapter.WSDataSource#getConnection(com.ibm.websphere.rsadapter.JDBCConnectionSpec)
135         */
136        @Override
137        protected Connection doGetConnection(@Nullable String username, @Nullable String password) throws SQLException {
138                // Create JDBCConnectionSpec using current isolation level value and read-only flag.
139                Object connSpec = createConnectionSpec(
140                                getCurrentIsolationLevel(), getCurrentReadOnlyFlag(), username, password);
141                if (logger.isDebugEnabled()) {
142                        logger.debug("Obtaining JDBC Connection from WebSphere DataSource [" +
143                                        getTargetDataSource() + "], using ConnectionSpec [" + connSpec + "]");
144                }
145                // Create Connection through invoking WSDataSource.getConnection(JDBCConnectionSpec)
146                Connection con = (Connection) invokeJdbcMethod(
147                                this.wsDataSourceGetConnectionMethod, obtainTargetDataSource(), connSpec);
148                Assert.state(con != null, "No Connection");
149                return con;
150        }
151
152        /**
153         * Create a WebSphere {@code JDBCConnectionSpec} object for the given characteristics.
154         * <p>The default implementation uses reflection to apply the given settings.
155         * Can be overridden in subclasses to customize the JDBCConnectionSpec object
156         * (<a href="https://publib.boulder.ibm.com/infocenter/wasinfo/v6r0/topic/com.ibm.websphere.javadoc.doc/public_html/api/com/ibm/websphere/rsadapter/JDBCConnectionSpec.html">JDBCConnectionSpec javadoc</a>;
157         * <a href="https://www.ibm.com/developerworks/websphere/library/techarticles/0404_tang/0404_tang.html">IBM developerWorks article</a>).
158         * @param isolationLevel the isolation level to apply (or {@code null} if none)
159         * @param readOnlyFlag the read-only flag to apply (or {@code null} if none)
160         * @param username the username to apply ({@code null} or empty indicates the default)
161         * @param password the password to apply (may be {@code null} or empty)
162         * @throws SQLException if thrown by JDBCConnectionSpec API methods
163         * @see com.ibm.websphere.rsadapter.JDBCConnectionSpec
164         */
165        protected Object createConnectionSpec(@Nullable Integer isolationLevel, @Nullable Boolean readOnlyFlag,
166                        @Nullable String username, @Nullable String password) throws SQLException {
167
168                Object connSpec = invokeJdbcMethod(this.newJdbcConnSpecMethod, null);
169                Assert.state(connSpec != null, "No JDBCConnectionSpec");
170                if (isolationLevel != null) {
171                        invokeJdbcMethod(this.setTransactionIsolationMethod, connSpec, isolationLevel);
172                }
173                if (readOnlyFlag != null) {
174                        invokeJdbcMethod(this.setReadOnlyMethod, connSpec, readOnlyFlag);
175                }
176                // If the username is empty, we'll simply let the target DataSource
177                // use its default credentials.
178                if (StringUtils.hasLength(username)) {
179                        invokeJdbcMethod(this.setUserNameMethod, connSpec, username);
180                        invokeJdbcMethod(this.setPasswordMethod, connSpec, password);
181                }
182                return connSpec;
183        }
184
185
186        @Nullable
187        private static Object invokeJdbcMethod(Method method, @Nullable Object target, @Nullable Object... args)
188                        throws SQLException {
189                try {
190                        return method.invoke(target, args);
191                }
192                catch (IllegalAccessException ex) {
193                        ReflectionUtils.handleReflectionException(ex);
194                }
195                catch (InvocationTargetException ex) {
196                        if (ex.getTargetException() instanceof SQLException) {
197                                throw (SQLException) ex.getTargetException();
198                        }
199                        ReflectionUtils.handleInvocationTargetException(ex);
200                }
201                throw new IllegalStateException("Should never get here");
202        }
203
204}