1 /*
2 * SPDX-License-Identifier: Apache-2.0
3 * See LICENSE file for details.
4 *
5 * Copyright 2015-2026 the original author or authors.
6 */
7 package org.springframework.orm.ibatis;
8
9 import com.ibatis.common.xml.NodeletException;
10 import com.ibatis.sqlmap.client.SqlMapClient;
11 import com.ibatis.sqlmap.client.SqlMapClientBuilder;
12 import com.ibatis.sqlmap.engine.builder.xml.SqlMapConfigParser;
13 import com.ibatis.sqlmap.engine.builder.xml.SqlMapParser;
14 import com.ibatis.sqlmap.engine.builder.xml.XmlParserState;
15 import com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient;
16 import com.ibatis.sqlmap.engine.transaction.TransactionConfig;
17 import com.ibatis.sqlmap.engine.transaction.TransactionManager;
18 import com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig;
19
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.lang.reflect.Field;
23 import java.util.Properties;
24
25 import javax.sql.DataSource;
26
27 import org.springframework.beans.factory.FactoryBean;
28 import org.springframework.beans.factory.InitializingBean;
29 import org.springframework.core.io.Resource;
30 import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
31 import org.springframework.jdbc.support.lob.LobHandler;
32 import org.springframework.util.ObjectUtils;
33
34 /**
35 * {@link org.springframework.beans.factory.FactoryBean} that creates an iBATIS
36 * {@link com.ibatis.sqlmap.client.SqlMapClient}. This is the usual way to set up a shared iBATIS SqlMapClient in a
37 * Spring application context; the SqlMapClient can then be passed to iBATIS-based DAOs via dependency injection.
38 * <p>
39 * Either {@link org.springframework.jdbc.datasource.DataSourceTransactionManager} or
40 * {@link org.springframework.transaction.jta.JtaTransactionManager} can be used for transaction demarcation in
41 * combination with a SqlMapClient, with JTA only necessary for transactions which span multiple databases.
42 * <p>
43 * Allows for specifying a DataSource at the SqlMapClient level. This is preferable to per-DAO DataSource references, as
44 * it allows for lazy loading and avoids repeated DataSource references in every DAO.
45 * <p>
46 * <b>Note:</b> As of Spring 2.5.5, this class (finally) requires iBATIS 2.3 or higher. The new "mappingLocations"
47 * feature requires iBATIS 2.3.2.
48 *
49 * @author Juergen Hoeller
50 *
51 * @since 24.02.2004
52 *
53 * @see #setConfigLocation
54 * @see #setDataSource
55 * @see SqlMapClientTemplate#setSqlMapClient
56 * @see SqlMapClientTemplate#setDataSource
57 *
58 * @deprecated as of Spring 3.2, in favor of the native Spring support in the Mybatis follow-up project
59 * (https://mybatis.org/)
60 */
61 @Deprecated
62 public class SqlMapClientFactoryBean implements FactoryBean<SqlMapClient>, InitializingBean {
63
64 private static final ThreadLocal<LobHandler> configTimeLobHandlerHolder = new ThreadLocal<>();
65
66 /**
67 * Return the LobHandler for the currently configured iBATIS SqlMapClient, to be used by TypeHandler implementations
68 * like ClobStringTypeHandler.
69 * <p>
70 * This instance will be set before initialization of the corresponding SqlMapClient, and reset immediately
71 * afterwards. It is thus only available during configuration.
72 *
73 * @see #setLobHandler
74 * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler
75 * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler
76 * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler
77 */
78 public static LobHandler getConfigTimeLobHandler() {
79 return configTimeLobHandlerHolder.get();
80 }
81
82 private Resource[] configLocations;
83
84 private Resource[] mappingLocations;
85
86 private Properties sqlMapClientProperties;
87
88 private DataSource dataSource;
89
90 private boolean useTransactionAwareDataSource = true;
91
92 private Class<? extends TransactionConfig> transactionConfigClass = ExternalTransactionConfig.class;
93
94 private Properties transactionConfigProperties;
95
96 private LobHandler lobHandler;
97
98 private SqlMapClient sqlMapClient;
99
100 public SqlMapClientFactoryBean() {
101 this.transactionConfigProperties = new Properties();
102 this.transactionConfigProperties.setProperty("SetAutoCommitAllowed", "false");
103 }
104
105 /**
106 * Set the location of the iBATIS SqlMapClient config file. A typical value is "WEB-INF/sql-map-config.xml".
107 *
108 * @see #setConfigLocations
109 */
110 public void setConfigLocation(Resource configLocation) {
111 this.configLocations = (configLocation != null ? new Resource[] { configLocation } : null);
112 }
113
114 /**
115 * Set multiple locations of iBATIS SqlMapClient config files that are going to be merged into one unified
116 * configuration at runtime.
117 *
118 * @param configLocations
119 * the config locations
120 */
121 public void setConfigLocations(Resource[] configLocations) {
122 this.configLocations = configLocations;
123 }
124
125 /**
126 * Set locations of iBATIS sql-map mapping files that are going to be merged into the SqlMapClient configuration at
127 * runtime.
128 * <p>
129 * This is an alternative to specifying "<sqlMap>" entries in a sql-map-client config file. This property being
130 * based on Spring's resource abstraction also allows for specifying resource patterns here: e.g. "/myApp/*-map.xml".
131 * <p>
132 * Note that this feature requires iBATIS 2.3.2; it will not work with any previous iBATIS version.
133 *
134 * @param mappingLocations
135 * the mapping locations
136 */
137 public void setMappingLocations(Resource[] mappingLocations) {
138 this.mappingLocations = mappingLocations;
139 }
140
141 /**
142 * Set optional properties to be passed into the SqlMapClientBuilder, as alternative to a {@code <properties>}
143 * tag in the sql-map-config.xml file. Will be used to resolve placeholders in the config file.
144 *
145 * @param sqlMapClientProperties
146 * the client properties
147 *
148 * @see #setConfigLocation
149 * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient(java.io.InputStream, java.util.Properties)
150 */
151 public void setSqlMapClientProperties(Properties sqlMapClientProperties) {
152 this.sqlMapClientProperties = sqlMapClientProperties;
153 }
154
155 /**
156 * Set the DataSource to be used by iBATIS SQL Maps. This will be passed to the SqlMapClient as part of a
157 * TransactionConfig instance.
158 * <p>
159 * If specified, this will override corresponding settings in the SqlMapClient properties. Usually, you will specify
160 * DataSource and transaction configuration <i>either</i> here <i>or</i> in SqlMapClient properties.
161 * <p>
162 * Specifying a DataSource for the SqlMapClient rather than for each individual DAO allows for lazy loading, for
163 * example when using PaginatedList results.
164 * <p>
165 * With a DataSource passed in here, you don't need to specify one for each DAO. Passing the SqlMapClient to the DAOs
166 * is enough, as it already carries a DataSource. Thus, it's recommended to specify the DataSource at this central
167 * location only.
168 * <p>
169 * Thanks to Brandon Goodin from the iBATIS team for the hint on how to make this work with Spring's integration
170 * strategy!
171 *
172 * @param dataSource
173 * the data source
174 *
175 * @see #setTransactionConfigClass
176 * @see #setTransactionConfigProperties
177 * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource
178 * @see SqlMapClientTemplate#setDataSource
179 */
180 public void setDataSource(DataSource dataSource) {
181 this.dataSource = dataSource;
182 }
183
184 /**
185 * Set whether to use a transaction-aware DataSource for the SqlMapClient, i.e. whether to automatically wrap the
186 * passed-in DataSource with Spring's TransactionAwareDataSourceProxy.
187 * <p>
188 * Default is "true": When the SqlMapClient performs direct database operations outside of Spring's
189 * SqlMapClientTemplate (for example, lazy loading or direct SqlMapClient access), it will still participate in active
190 * Spring-managed transactions.
191 * <p>
192 * As a further effect, using a transaction-aware DataSource will apply remaining transaction timeouts to all created
193 * JDBC Statements. This means that all operations performed by the SqlMapClient will automatically participate in
194 * Spring-managed transaction timeouts.
195 * <p>
196 * Turn this flag off to get raw DataSource handling, without Spring transaction checks. Operations on Spring's
197 * SqlMapClientTemplate will still detect Spring-managed transactions, but lazy loading or direct SqlMapClient access
198 * won't.
199 *
200 * @param useTransactionAwareDataSource
201 * whether to use transaction aware datasource
202 *
203 * @see #setDataSource
204 * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
205 * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
206 * @see SqlMapClientTemplate
207 * @see com.ibatis.sqlmap.client.SqlMapClient
208 */
209 public void setUseTransactionAwareDataSource(boolean useTransactionAwareDataSource) {
210 this.useTransactionAwareDataSource = useTransactionAwareDataSource;
211 }
212
213 /**
214 * Set the iBATIS TransactionConfig class to use. Default is
215 * {@code com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig}.
216 * <p>
217 * Will only get applied when using a Spring-managed DataSource. An instance of this class will get populated with the
218 * given DataSource and initialized with the given properties.
219 * <p>
220 * The default ExternalTransactionConfig is appropriate if there is external transaction management that the
221 * SqlMapClient should participate in: be it Spring transaction management, EJB CMT or plain JTA. This should be the
222 * typical scenario. If there is no active transaction, SqlMapClient operations will execute SQL statements
223 * non-transactionally.
224 * <p>
225 * JdbcTransactionConfig or JakartaTransactionConfig/JavaxTransactionConfig is only necessary when using the iBATIS
226 * SqlMapTransactionManager API instead of external transactions. If there is no explicit transaction, SqlMapClient
227 * operations will automatically start a transaction for their own scope (in contrast to the external transaction
228 * mode, see above).
229 * <p>
230 * <b>It is strongly recommended to use iBATIS SQL Maps with Spring transaction management (or EJB CMT).</b> In this
231 * case, the default ExternalTransactionConfig is fine. Lazy loading and SQL Maps operations without explicit
232 * transaction demarcation will execute non-transactionally.
233 * <p>
234 * Even with Spring transaction management, it might be desirable to specify JdbcTransactionConfig: This will still
235 * participate in existing Spring-managed transactions, but lazy loading and operations without explicit transaction
236 * demaration will execute in their own auto-started transactions. However, this is usually not necessary.
237 *
238 * @param transactionConfigClass
239 * the transaction config class
240 *
241 * @see #setDataSource
242 * @see #setTransactionConfigProperties
243 * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig
244 * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig
245 * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig
246 * @see com.ibatis.sqlmap.engine.transaction.jta.JakartaTransactionConfig
247 * @see com.ibatis.sqlmap.engine.transaction.jta.JavaxTransactionConfig
248 * @see com.ibatis.sqlmap.client.SqlMapTransactionManager
249 */
250 public void setTransactionConfigClass(Class<? extends TransactionConfig> transactionConfigClass) {
251 if (transactionConfigClass == null || !TransactionConfig.class.isAssignableFrom(transactionConfigClass)) {
252 throw new IllegalArgumentException("Invalid transactionConfigClass: does not implement "
253 + "com.ibatis.sqlmap.engine.transaction.TransactionConfig");
254 }
255 this.transactionConfigClass = transactionConfigClass;
256 }
257
258 /**
259 * Set properties to be passed to the TransactionConfig instance used by this SqlMapClient. Supported properties
260 * depend on the concrete TransactionConfig implementation used:
261 * <p>
262 * <ul>
263 * <li><b>ExternalTransactionConfig</b> supports "DefaultAutoCommit" (default: false) and "SetAutoCommitAllowed"
264 * (default: true). Note that Spring uses SetAutoCommitAllowed = false as default, in contrast to the iBATIS default,
265 * to always keep the original autoCommit value as provided by the connection pool.
266 * <li><b>JdbcTransactionConfig</b> does not supported any properties.
267 * <li><b>JakartaTransactionConfig</b> (jakarta namespace) and <b>JavaxTransactionConfig</b> (javax namespace) both
268 * support "UserTransaction" (no default), specifying the JNDI location of the JTA UserTransaction (usually
269 * "java:comp/UserTransaction").
270 * </ul>
271 *
272 * @param transactionConfigProperties
273 * the transaction config properties
274 *
275 * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig#initialize
276 * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig
277 * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig
278 * @see com.ibatis.sqlmap.engine.transaction.jta.JakartaTransactionConfig
279 * @see com.ibatis.sqlmap.engine.transaction.jta.JavaxTransactionConfig
280 */
281 public void setTransactionConfigProperties(Properties transactionConfigProperties) {
282 this.transactionConfigProperties = transactionConfigProperties;
283 }
284
285 /**
286 * Set the LobHandler to be used by the SqlMapClient. Will be exposed at config time for TypeHandler implementations.
287 *
288 * @param lobHandler
289 * the lob handler
290 *
291 * @see #getConfigTimeLobHandler
292 * @see com.ibatis.sqlmap.engine.type.TypeHandler
293 * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler
294 * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler
295 * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler
296 */
297 public void setLobHandler(LobHandler lobHandler) {
298 this.lobHandler = lobHandler;
299 }
300
301 @Override
302 public void afterPropertiesSet() throws Exception {
303 if (this.lobHandler != null) {
304 // Make given LobHandler available for SqlMapClient configuration.
305 // Do early because mapping resource might refer to custom types.
306 configTimeLobHandlerHolder.set(this.lobHandler);
307 }
308
309 try {
310 this.sqlMapClient = buildSqlMapClient(this.configLocations, this.mappingLocations, this.sqlMapClientProperties);
311
312 // Tell the SqlMapClient to use the given DataSource, if any.
313 if (this.dataSource != null) {
314 TransactionConfig transactionConfig = this.transactionConfigClass.getDeclaredConstructor().newInstance();
315 DataSource dataSourceToUse = this.dataSource;
316 if (this.useTransactionAwareDataSource && !(this.dataSource instanceof TransactionAwareDataSourceProxy)) {
317 dataSourceToUse = new TransactionAwareDataSourceProxy(this.dataSource);
318 }
319 transactionConfig.setDataSource(dataSourceToUse);
320 transactionConfig.setProperties(this.transactionConfigProperties);
321 applyTransactionConfig(this.sqlMapClient, transactionConfig);
322 }
323 } finally {
324 if (this.lobHandler != null) {
325 // Reset LobHandler holder.
326 configTimeLobHandlerHolder.remove();
327 }
328 }
329 }
330
331 /**
332 * Build a SqlMapClient instance based on the given standard configuration.
333 * <p>
334 * The default implementation uses the standard iBATIS {@link SqlMapClientBuilder} API to build a SqlMapClient
335 * instance based on an InputStream.
336 *
337 * @param configLocations
338 * the config files to load from
339 * @param mappingLocations
340 * the mapping files to load from
341 * @param properties
342 * the SqlMapClient properties (if any)
343 *
344 * @return the SqlMapClient instance (never {@code null})
345 *
346 * @throws IOException
347 * if loading the config file failed
348 *
349 * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient
350 */
351 protected SqlMapClient buildSqlMapClient(Resource[] configLocations, Resource[] mappingLocations,
352 Properties properties) throws IOException {
353
354 if (ObjectUtils.isEmpty(configLocations)) {
355 throw new IllegalArgumentException("At least 1 'configLocation' entry is required");
356 }
357
358 SqlMapClient client = null;
359 SqlMapConfigParser configParser = new SqlMapConfigParser();
360 for (Resource configLocation : configLocations) {
361 InputStream is = configLocation.getInputStream();
362 try {
363 client = configParser.parse(is, properties);
364 } catch (RuntimeException ex) {
365 throw new IOException("Failed to parse config resource: " + configLocation, ex.getCause());
366 }
367 }
368
369 if (mappingLocations != null) {
370 SqlMapParser mapParser = SqlMapParserFactory.createSqlMapParser(configParser);
371 for (Resource mappingLocation : mappingLocations) {
372 try {
373 mapParser.parse(mappingLocation.getInputStream());
374 } catch (NodeletException ex) {
375 throw new IOException("Failed to parse mapping resource: " + mappingLocation, ex);
376 }
377 }
378 }
379
380 return client;
381 }
382
383 /**
384 * Apply the given iBATIS TransactionConfig to the SqlMapClient.
385 * <p>
386 * The default implementation casts to ExtendedSqlMapClient, retrieves the maximum number of concurrent transactions
387 * from the SqlMapExecutorDelegate, and sets an iBATIS TransactionManager with the given TransactionConfig.
388 *
389 * @param sqlMapClient
390 * the SqlMapClient to apply the TransactionConfig to
391 * @param transactionConfig
392 * the iBATIS TransactionConfig to apply
393 *
394 * @see com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient
395 * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#getMaxTransactions
396 * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#setTxManager
397 */
398 protected void applyTransactionConfig(SqlMapClient sqlMapClient, TransactionConfig transactionConfig) {
399 if (!(sqlMapClient instanceof ExtendedSqlMapClient)) {
400 throw new IllegalArgumentException("Cannot set TransactionConfig with DataSource for SqlMapClient if not of type "
401 + "ExtendedSqlMapClient: " + sqlMapClient);
402 }
403 ExtendedSqlMapClient extendedClient = (ExtendedSqlMapClient) sqlMapClient;
404 extendedClient.getDelegate().setTxManager(new TransactionManager(transactionConfig));
405 }
406
407 @Override
408 public SqlMapClient getObject() {
409 return this.sqlMapClient;
410 }
411
412 @Override
413 public Class<? extends SqlMapClient> getObjectType() {
414 return (this.sqlMapClient != null ? this.sqlMapClient.getClass() : SqlMapClient.class);
415 }
416
417 @Override
418 public boolean isSingleton() {
419 return true;
420 }
421
422 /**
423 * Inner class to avoid hard-coded iBATIS 2.3.2 dependency (XmlParserState class).
424 */
425 private static class SqlMapParserFactory {
426
427 public static SqlMapParser createSqlMapParser(SqlMapConfigParser configParser) {
428 // Ideally: XmlParserState state = configParser.getState();
429 XmlParserState state;
430 try {
431 Field stateField = SqlMapConfigParser.class.getDeclaredField("state");
432 stateField.setAccessible(true);
433 state = (XmlParserState) stateField.get(configParser);
434 } catch (Exception ex) {
435 throw new IllegalStateException("iBATIS 2.3.2 'state' field not found in SqlMapConfigParser class - "
436 + "please upgrade to IBATIS 2.3.2 or higher in order to use the new 'mappingLocations' feature. " + ex);
437 }
438 return new SqlMapParser(state);
439 }
440 }
441
442 }