1 | /* |
2 | * Copyright 2002-2008 the original author or authors. |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | * you may not use this file except in compliance with the License. |
6 | * You may obtain a copy of the License at |
7 | * |
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | * |
10 | * Unless required by applicable law or agreed to in writing, software |
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. |
15 | */ |
16 | |
17 | package org.springframework.batch.item.database; |
18 | |
19 | import java.io.PrintWriter; |
20 | import java.lang.reflect.InvocationHandler; |
21 | import java.lang.reflect.InvocationTargetException; |
22 | import java.lang.reflect.Method; |
23 | import java.lang.reflect.Proxy; |
24 | import java.sql.Connection; |
25 | import java.sql.SQLException; |
26 | import java.util.logging.Logger; |
27 | |
28 | import javax.sql.DataSource; |
29 | |
30 | import org.springframework.beans.factory.InitializingBean; |
31 | import org.springframework.jdbc.datasource.ConnectionProxy; |
32 | import org.springframework.jdbc.datasource.DataSourceUtils; |
33 | import org.springframework.jdbc.datasource.SmartDataSource; |
34 | import org.springframework.transaction.support.TransactionSynchronizationManager; |
35 | import org.springframework.util.Assert; |
36 | |
37 | /** |
38 | * Implementation of {@link SmartDataSource} that is capable of keeping a single |
39 | * JDBC Connection which is NOT closed after each use even if |
40 | * {@link Connection#close()} is called. |
41 | * |
42 | * The connection can be kept open over multiple transactions when used together |
43 | * with any of Spring's |
44 | * {@link org.springframework.transaction.PlatformTransactionManager} |
45 | * implementations. |
46 | * |
47 | * <p> |
48 | * Loosely based on the SingleConnectionDataSource implementation in Spring |
49 | * Core. Intended to be used with the {@link JdbcCursorItemReader} to provide a |
50 | * connection that remains open across transaction boundaries, It remains open |
51 | * for the life of the cursor, and can be shared with the main transaction of |
52 | * the rest of the step processing. |
53 | * |
54 | * <p> |
55 | * Once close suppression has been turned on for a connection, it will be |
56 | * returned for the first {@link #getConnection()} call. Any subsequent calls to |
57 | * {@link #getConnection()} will retrieve a new connection from the wrapped |
58 | * {@link DataSource} until the {@link DataSourceUtils} queries whether the |
59 | * connection should be closed or not by calling |
60 | * {@link #shouldClose(Connection)} for the close-suppressed {@link Connection}. |
61 | * At that point the cycle starts over again, and the next |
62 | * {@link #getConnection()} call will have the {@link Connection} that is being |
63 | * close-suppressed returned. This allows the use of the close-suppressed |
64 | * {@link Connection} to be the main {@link Connection} for an extended data |
65 | * access process. The close suppression is turned off by calling |
66 | * {@link #stopCloseSuppression(Connection)}. |
67 | * |
68 | * <p> |
69 | * This class is not multi-threading capable. |
70 | * |
71 | * <p> |
72 | * The connection returned will be a close-suppressing proxy instead of the |
73 | * physical {@link Connection}. Be aware that you will not be able to cast this |
74 | * to a native <code>OracleConnection</code> or the like anymore; you need to |
75 | * use a {@link org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor}. |
76 | * |
77 | * @author Thomas Risberg |
78 | * @see #getConnection() |
79 | * @see java.sql.Connection#close() |
80 | * @see DataSourceUtils#releaseConnection |
81 | * @see org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor |
82 | * @since 2.0 |
83 | */ |
84 | public class ExtendedConnectionDataSourceProxy implements SmartDataSource, InitializingBean { |
85 | |
86 | /** Provided DataSource */ |
87 | private DataSource dataSource; |
88 | |
89 | /** The connection to suppress close calls for */ |
90 | private Connection closeSuppressedConnection = null; |
91 | |
92 | /** The connection to suppress close calls for */ |
93 | private boolean borrowedConnection = false; |
94 | |
95 | /** Synchronization monitor for the shared Connection */ |
96 | private final Object connectionMonitor = new Object(); |
97 | |
98 | /** |
99 | * No arg constructor for use when configured using JavaBean style. |
100 | */ |
101 | public ExtendedConnectionDataSourceProxy() { |
102 | } |
103 | |
104 | /** |
105 | * Constructor that takes as a parameter with the {&link DataSource} to be |
106 | * wrapped. |
107 | */ |
108 | public ExtendedConnectionDataSourceProxy(DataSource dataSource) { |
109 | this.dataSource = dataSource; |
110 | } |
111 | |
112 | /** |
113 | * Setter for the {&link DataSource} that is to be wrapped. |
114 | * |
115 | * @param dataSource the DataSource |
116 | */ |
117 | public void setDataSource(DataSource dataSource) { |
118 | this.dataSource = dataSource; |
119 | } |
120 | |
121 | /** |
122 | * @see SmartDataSource |
123 | */ |
124 | public boolean shouldClose(Connection connection) { |
125 | boolean shouldClose = !isCloseSuppressionActive(connection); |
126 | if (borrowedConnection && closeSuppressedConnection.equals(connection)) { |
127 | borrowedConnection = false; |
128 | } |
129 | return shouldClose; |
130 | } |
131 | |
132 | /** |
133 | * Return the status of close suppression being activated for a given |
134 | * {@link Connection} |
135 | * |
136 | * @param connection the {@link Connection} that the close suppression |
137 | * status is requested for |
138 | * @return true or false |
139 | */ |
140 | public boolean isCloseSuppressionActive(Connection connection) { |
141 | if (connection == null) { |
142 | return false; |
143 | } |
144 | return connection.equals(closeSuppressedConnection) ? true : false; |
145 | } |
146 | |
147 | /** |
148 | * |
149 | * @param connection the {@link Connection} that close suppression is |
150 | * requested for |
151 | */ |
152 | public void startCloseSuppression(Connection connection) { |
153 | synchronized (this.connectionMonitor) { |
154 | closeSuppressedConnection = connection; |
155 | if (TransactionSynchronizationManager.isActualTransactionActive()) { |
156 | borrowedConnection = true; |
157 | } |
158 | } |
159 | } |
160 | |
161 | /** |
162 | * |
163 | * @param connection the {@link Connection} that close suppression should be |
164 | * turned off for |
165 | */ |
166 | public void stopCloseSuppression(Connection connection) { |
167 | synchronized (this.connectionMonitor) { |
168 | closeSuppressedConnection = null; |
169 | borrowedConnection = false; |
170 | } |
171 | } |
172 | |
173 | public Connection getConnection() throws SQLException { |
174 | synchronized (this.connectionMonitor) { |
175 | return initConnection(null, null); |
176 | } |
177 | } |
178 | |
179 | public Connection getConnection(String username, String password) throws SQLException { |
180 | synchronized (this.connectionMonitor) { |
181 | return initConnection(username, password); |
182 | } |
183 | } |
184 | |
185 | private boolean completeCloseCall(Connection connection) { |
186 | if (borrowedConnection && closeSuppressedConnection.equals(connection)) { |
187 | borrowedConnection = false; |
188 | } |
189 | return isCloseSuppressionActive(connection); |
190 | } |
191 | |
192 | private Connection initConnection(String username, String password) throws SQLException { |
193 | if (closeSuppressedConnection != null) { |
194 | if (!borrowedConnection) { |
195 | borrowedConnection = true; |
196 | return closeSuppressedConnection; |
197 | } |
198 | } |
199 | Connection target; |
200 | if (username != null) { |
201 | target = dataSource.getConnection(username, password); |
202 | } |
203 | else { |
204 | target = dataSource.getConnection(); |
205 | } |
206 | Connection connection = getCloseSuppressingConnectionProxy(target); |
207 | return connection; |
208 | } |
209 | |
210 | public PrintWriter getLogWriter() throws SQLException { |
211 | return dataSource.getLogWriter(); |
212 | } |
213 | |
214 | public int getLoginTimeout() throws SQLException { |
215 | return dataSource.getLoginTimeout(); |
216 | } |
217 | |
218 | public void setLogWriter(PrintWriter out) throws SQLException { |
219 | dataSource.setLogWriter(out); |
220 | } |
221 | |
222 | public void setLoginTimeout(int seconds) throws SQLException { |
223 | dataSource.setLoginTimeout(seconds); |
224 | } |
225 | |
226 | /** |
227 | * Wrap the given Connection with a proxy that delegates every method call |
228 | * to it but suppresses close calls. |
229 | * @param target the original Connection to wrap |
230 | * @return the wrapped Connection |
231 | */ |
232 | protected Connection getCloseSuppressingConnectionProxy(Connection target) { |
233 | return (Connection) Proxy.newProxyInstance(ConnectionProxy.class.getClassLoader(), |
234 | new Class[] { ConnectionProxy.class }, new CloseSuppressingInvocationHandler(target, this)); |
235 | } |
236 | |
237 | /** |
238 | * Invocation handler that suppresses close calls on JDBC Connections until |
239 | * the associated instance of the ExtendedConnectionDataSourceProxy |
240 | * determines the connection should actually be closed. |
241 | */ |
242 | private static class CloseSuppressingInvocationHandler implements InvocationHandler { |
243 | |
244 | private final Connection target; |
245 | |
246 | private final ExtendedConnectionDataSourceProxy dataSource; |
247 | |
248 | public CloseSuppressingInvocationHandler(Connection target, ExtendedConnectionDataSourceProxy dataSource) { |
249 | this.dataSource = dataSource; |
250 | this.target = target; |
251 | } |
252 | |
253 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { |
254 | // Invocation on ConnectionProxy interface coming in... |
255 | |
256 | if (method.getName().equals("equals")) { |
257 | // Only consider equal when proxies are identical. |
258 | return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); |
259 | } |
260 | else if (method.getName().equals("hashCode")) { |
261 | // Use hashCode of Connection proxy. |
262 | return new Integer(System.identityHashCode(proxy)); |
263 | } |
264 | else if (method.getName().equals("close")) { |
265 | // Handle close method: don't pass the call on if we are |
266 | // suppressing close calls. |
267 | if (dataSource.completeCloseCall((Connection) proxy)) { |
268 | return null; |
269 | } |
270 | else { |
271 | target.close(); |
272 | return null; |
273 | } |
274 | } |
275 | else if (method.getName().equals("getTargetConnection")) { |
276 | // Handle getTargetConnection method: return underlying |
277 | // Connection. |
278 | return this.target; |
279 | } |
280 | |
281 | // Invoke method on target Connection. |
282 | try { |
283 | return method.invoke(this.target, args); |
284 | } |
285 | catch (InvocationTargetException ex) { |
286 | throw ex.getTargetException(); |
287 | } |
288 | } |
289 | } |
290 | |
291 | /** |
292 | * Performs only a 'shallow' non-recursive check of self's and delegate's |
293 | * class to retain Java 5 compatibility. |
294 | */ |
295 | public boolean isWrapperFor(Class<?> iface) throws SQLException { |
296 | if (iface.isAssignableFrom(SmartDataSource.class) || iface.isAssignableFrom(dataSource.getClass())) { |
297 | return true; |
298 | } |
299 | return false; |
300 | } |
301 | |
302 | /** |
303 | * Returns either self or delegate (in this order) if one of them can be |
304 | * cast to supplied parameter class. Does *not* support recursive unwrapping |
305 | * of the delegate to retain Java 5 compatibility. |
306 | */ |
307 | public <T> T unwrap(Class<T> iface) throws SQLException { |
308 | if (iface.isAssignableFrom(SmartDataSource.class)) { |
309 | @SuppressWarnings("unchecked") |
310 | T casted = (T) this; |
311 | return casted; |
312 | } |
313 | else if (iface.isAssignableFrom(dataSource.getClass())) { |
314 | @SuppressWarnings("unchecked") |
315 | T casted = (T) dataSource; |
316 | return casted; |
317 | } |
318 | throw new SQLException("Unsupported class " + iface.getSimpleName()); |
319 | } |
320 | |
321 | public void afterPropertiesSet() throws Exception { |
322 | Assert.notNull(dataSource); |
323 | } |
324 | |
325 | /** |
326 | * Added due to JDK 7 compatibility, sadly a proper implementation |
327 | * that would throw SqlFeatureNotSupportedException is not possible |
328 | * in Java 5 (the class was added in Java 6). |
329 | */ |
330 | public Logger getParentLogger() { |
331 | throw new UnsupportedOperationException(); |
332 | } |
333 | |
334 | } |