001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.orm.ibatis;
018:
019: import java.io.IOException;
020: import java.io.InputStream;
021: import java.io.InputStreamReader;
022: import java.util.Properties;
023:
024: import javax.sql.DataSource;
025:
026: import com.ibatis.sqlmap.client.SqlMapClient;
027: import com.ibatis.sqlmap.client.SqlMapClientBuilder;
028: import com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient;
029: import com.ibatis.sqlmap.engine.transaction.TransactionConfig;
030: import com.ibatis.sqlmap.engine.transaction.TransactionManager;
031: import com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig;
032:
033: import org.springframework.beans.factory.FactoryBean;
034: import org.springframework.beans.factory.InitializingBean;
035: import org.springframework.core.io.Resource;
036: import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
037: import org.springframework.jdbc.support.lob.LobHandler;
038: import org.springframework.util.ClassUtils;
039:
040: /**
041: * {@link org.springframework.beans.factory.FactoryBean} that creates an
042: * iBATIS {@link com.ibatis.sqlmap.client.SqlMapClient}. This is the usual
043: * way to set up a shared iBATIS SqlMapClient in a Spring application context;
044: * the SqlMapClient can then be passed to iBATIS-based DAOs via dependency
045: * injection.
046: *
047: * <p>Either {@link org.springframework.jdbc.datasource.DataSourceTransactionManager}
048: * or {@link org.springframework.transaction.jta.JtaTransactionManager} can be
049: * used for transaction demarcation in combination with a SqlMapClient,
050: * with JTA only necessary for transactions which span multiple databases.
051: *
052: * <p>Allows for specifying a DataSource at the SqlMapClient level. This
053: * is preferable to per-DAO DataSource references, as it allows for lazy
054: * loading and avoids repeated DataSource references in every DAO.
055: *
056: * <p>Note: As of Spring 2.0.2, this class explicitly supports iBATIS 2.3.
057: * Backwards compatibility with iBATIS 2.1 and 2.2 is preserved for the
058: * time being, through corresponding reflective checks.
059: *
060: * @author Juergen Hoeller
061: * @since 24.02.2004
062: * @see #setConfigLocation
063: * @see #setDataSource
064: * @see SqlMapClientTemplate#setSqlMapClient
065: * @see SqlMapClientTemplate#setDataSource
066: */
067: public class SqlMapClientFactoryBean implements FactoryBean,
068: InitializingBean {
069:
070: // Determine whether the SqlMapClientBuilder.buildSqlMapClient(InputStream)
071: // method is available, for use in the "buildSqlMapClient" template method.
072: private final static boolean buildSqlMapClientWithInputStreamMethodAvailable = ClassUtils
073: .hasMethod(SqlMapClientBuilder.class, "buildSqlMapClient",
074: new Class[] { InputStream.class });
075:
076: // Determine whether the SqlMapClientBuilder.buildSqlMapClient(InputStream, Properties)
077: // method is available, for use in the "buildSqlMapClient" template method.
078: private final static boolean buildSqlMapClientWithInputStreamAndPropertiesMethodAvailable = ClassUtils
079: .hasMethod(SqlMapClientBuilder.class, "buildSqlMapClient",
080: new Class[] { InputStream.class, Properties.class });
081:
082: private static final ThreadLocal configTimeLobHandlerHolder = new ThreadLocal();
083:
084: /**
085: * Return the LobHandler for the currently configured iBATIS SqlMapClient,
086: * to be used by TypeHandler implementations like ClobStringTypeHandler.
087: * <p>This instance will be set before initialization of the corresponding
088: * SqlMapClient, and reset immediately afterwards. It is thus only available
089: * during configuration.
090: * @see #setLobHandler
091: * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler
092: * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler
093: * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler
094: */
095: public static LobHandler getConfigTimeLobHandler() {
096: return (LobHandler) configTimeLobHandlerHolder.get();
097: }
098:
099: private Resource configLocation;
100:
101: private Properties sqlMapClientProperties;
102:
103: private DataSource dataSource;
104:
105: private boolean useTransactionAwareDataSource = true;
106:
107: private Class transactionConfigClass = ExternalTransactionConfig.class;
108:
109: private Properties transactionConfigProperties;
110:
111: private LobHandler lobHandler;
112:
113: private SqlMapClient sqlMapClient;
114:
115: public SqlMapClientFactoryBean() {
116: this .transactionConfigProperties = new Properties();
117: this .transactionConfigProperties.setProperty(
118: "SetAutoCommitAllowed", "false");
119: }
120:
121: /**
122: * Set the location of the iBATIS SqlMapClient config file.
123: * A typical value is "WEB-INF/sql-map-config.xml".
124: */
125: public void setConfigLocation(Resource configLocation) {
126: this .configLocation = configLocation;
127: }
128:
129: /**
130: * Set optional properties to be passed into the SqlMapClientBuilder, as
131: * alternative to a <code><properties></code> tag in the sql-map-config.xml
132: * file. Will be used to resolve placeholders in the config file.
133: * @see #setConfigLocation
134: * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient(java.io.Reader, java.util.Properties)
135: */
136: public void setSqlMapClientProperties(
137: Properties sqlMapClientProperties) {
138: this .sqlMapClientProperties = sqlMapClientProperties;
139: }
140:
141: /**
142: * Set the DataSource to be used by iBATIS SQL Maps. This will be passed to the
143: * SqlMapClient as part of a TransactionConfig instance.
144: * <p>If specified, this will override corresponding settings in the SqlMapClient
145: * properties. Usually, you will specify DataSource and transaction configuration
146: * <i>either</i> here <i>or</i> in SqlMapClient properties.
147: * <p>Specifying a DataSource for the SqlMapClient rather than for each individual
148: * DAO allows for lazy loading, for example when using PaginatedList results.
149: * <p>With a DataSource passed in here, you don't need to specify one for each DAO.
150: * Passing the SqlMapClient to the DAOs is enough, as it already carries a DataSource.
151: * Thus, it's recommended to specify the DataSource at this central location only.
152: * <p>Thanks to Brandon Goodin from the iBATIS team for the hint on how to make
153: * this work with Spring's integration strategy!
154: * @see #setTransactionConfigClass
155: * @see #setTransactionConfigProperties
156: * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource
157: * @see SqlMapClientTemplate#setDataSource
158: * @see SqlMapClientTemplate#queryForPaginatedList
159: */
160: public void setDataSource(DataSource dataSource) {
161: this .dataSource = dataSource;
162: }
163:
164: /**
165: * Set whether to use a transaction-aware DataSource for the SqlMapClient,
166: * i.e. whether to automatically wrap the passed-in DataSource with Spring's
167: * TransactionAwareDataSourceProxy.
168: * <p>Default is "true": When the SqlMapClient performs direct database operations
169: * outside of Spring's SqlMapClientTemplate (for example, lazy loading or direct
170: * SqlMapClient access), it will still participate in active Spring-managed
171: * transactions.
172: * <p>As a further effect, using a transaction-aware DataSource will apply
173: * remaining transaction timeouts to all created JDBC Statements. This means
174: * that all operations performed by the SqlMapClient will automatically
175: * participate in Spring-managed transaction timeouts.
176: * <p>Turn this flag off to get raw DataSource handling, without Spring transaction
177: * checks. Operations on Spring's SqlMapClientTemplate will still detect
178: * Spring-managed transactions, but lazy loading or direct SqlMapClient access won't.
179: * @see #setDataSource
180: * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
181: * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
182: * @see SqlMapClientTemplate
183: * @see com.ibatis.sqlmap.client.SqlMapClient
184: */
185: public void setUseTransactionAwareDataSource(
186: boolean useTransactionAwareDataSource) {
187: this .useTransactionAwareDataSource = useTransactionAwareDataSource;
188: }
189:
190: /**
191: * Set the iBATIS TransactionConfig class to use. Default is
192: * <code>com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig</code>.
193: * <p>Will only get applied when using a Spring-managed DataSource.
194: * An instance of this class will get populated with the given DataSource
195: * and initialized with the given properties.
196: * <p>The default ExternalTransactionConfig is appropriate if there is
197: * external transaction management that the SqlMapClient should participate
198: * in: be it Spring transaction management, EJB CMT or plain JTA. This
199: * should be the typical scenario. If there is no active transaction,
200: * SqlMapClient operations will execute SQL statements non-transactionally.
201: * <p>JdbcTransactionConfig or JtaTransactionConfig is only necessary
202: * when using the iBATIS SqlMapTransactionManager API instead of external
203: * transactions. If there is no explicit transaction, SqlMapClient operations
204: * will automatically start a transaction for their own scope (in contrast
205: * to the external transaction mode, see above).
206: * <p><b>It is strongly recommended to use iBATIS SQL Maps with Spring
207: * transaction management (or EJB CMT).</b> In this case, the default
208: * ExternalTransactionConfig is fine. Lazy loading and SQL Maps operations
209: * without explicit transaction demarcation will execute non-transactionally.
210: * <p>Even with Spring transaction management, it might be desirable to
211: * specify JdbcTransactionConfig: This will still participate in existing
212: * Spring-managed transactions, but lazy loading and operations without
213: * explicit transaction demaration will execute in their own auto-started
214: * transactions. However, this is usually not necessary.
215: * @see #setDataSource
216: * @see #setTransactionConfigProperties
217: * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig
218: * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig
219: * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig
220: * @see com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig
221: * @see com.ibatis.sqlmap.client.SqlMapTransactionManager
222: */
223: public void setTransactionConfigClass(Class transactionConfigClass) {
224: if (transactionConfigClass == null
225: || !TransactionConfig.class
226: .isAssignableFrom(transactionConfigClass)) {
227: throw new IllegalArgumentException(
228: "Invalid transactionConfigClass: does not implement "
229: + "com.ibatis.sqlmap.engine.transaction.TransactionConfig");
230: }
231: this .transactionConfigClass = transactionConfigClass;
232: }
233:
234: /**
235: * Set properties to be passed to the TransactionConfig instance used
236: * by this SqlMapClient. Supported properties depend on the concrete
237: * TransactionConfig implementation used:
238: * <p><ul>
239: * <li><b>ExternalTransactionConfig</b> supports "DefaultAutoCommit"
240: * (default: false) and "SetAutoCommitAllowed" (default: true).
241: * Note that Spring uses SetAutoCommitAllowed = false as default,
242: * in contrast to the iBATIS default, to always keep the original
243: * autoCommit value as provided by the connection pool.
244: * <li><b>JdbcTransactionConfig</b> does not supported any properties.
245: * <li><b>JtaTransactionConfig</b> supports "UserTransaction"
246: * (no default), specifying the JNDI location of the JTA UserTransaction
247: * (usually "java:comp/UserTransaction").
248: * </ul>
249: * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig#initialize
250: * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig
251: * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig
252: * @see com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig
253: */
254: public void setTransactionConfigProperties(
255: Properties transactionConfigProperties) {
256: this .transactionConfigProperties = transactionConfigProperties;
257: }
258:
259: /**
260: * Set the LobHandler to be used by the SqlMapClient.
261: * Will be exposed at config time for TypeHandler implementations.
262: * @see #getConfigTimeLobHandler
263: * @see com.ibatis.sqlmap.engine.type.TypeHandler
264: * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler
265: * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler
266: * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler
267: */
268: public void setLobHandler(LobHandler lobHandler) {
269: this .lobHandler = lobHandler;
270: }
271:
272: public void afterPropertiesSet() throws Exception {
273: if (this .configLocation == null) {
274: throw new IllegalArgumentException(
275: "configLocation is required");
276: }
277:
278: if (this .lobHandler != null) {
279: // Make given LobHandler available for SqlMapClient configuration.
280: // Do early because because mapping resource might refer to custom types.
281: configTimeLobHandlerHolder.set(this .lobHandler);
282: }
283:
284: try {
285: this .sqlMapClient = buildSqlMapClient(this .configLocation,
286: this .sqlMapClientProperties);
287:
288: // Tell the SqlMapClient to use the given DataSource, if any.
289: if (this .dataSource != null) {
290: TransactionConfig transactionConfig = (TransactionConfig) this .transactionConfigClass
291: .newInstance();
292: DataSource dataSourceToUse = this .dataSource;
293: if (this .useTransactionAwareDataSource
294: && !(this .dataSource instanceof TransactionAwareDataSourceProxy)) {
295: dataSourceToUse = new TransactionAwareDataSourceProxy(
296: this .dataSource);
297: }
298: transactionConfig.setDataSource(dataSourceToUse);
299: transactionConfig
300: .initialize(this .transactionConfigProperties);
301: applyTransactionConfig(this .sqlMapClient,
302: transactionConfig);
303: }
304: }
305:
306: finally {
307: if (this .lobHandler != null) {
308: // Reset LobHandler holder.
309: configTimeLobHandlerHolder.set(null);
310: }
311: }
312: }
313:
314: /**
315: * Build a SqlMapClient instance based on the given standard configuration.
316: * <p>The default implementation uses the standard iBATIS {@link SqlMapClientBuilder}
317: * API to build a SqlMapClient instance based on an InputStream (if possible,
318: * on iBATIS 2.3 and higher) or on a Reader (on iBATIS up to version 2.2).
319: * @param configLocation the config file to load from
320: * @param properties the SqlMapClient properties (if any)
321: * @return the SqlMapClient instance (never <code>null</code>)
322: * @throws IOException if loading the config file failed
323: * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient
324: */
325: protected SqlMapClient buildSqlMapClient(Resource configLocation,
326: Properties properties) throws IOException {
327: InputStream is = configLocation.getInputStream();
328: if (properties != null) {
329: if (buildSqlMapClientWithInputStreamAndPropertiesMethodAvailable) {
330: return SqlMapClientBuilder.buildSqlMapClient(is,
331: properties);
332: } else {
333: return SqlMapClientBuilder.buildSqlMapClient(
334: new InputStreamReader(is), properties);
335: }
336: } else {
337: if (buildSqlMapClientWithInputStreamMethodAvailable) {
338: return SqlMapClientBuilder.buildSqlMapClient(is);
339: } else {
340: return SqlMapClientBuilder
341: .buildSqlMapClient(new InputStreamReader(is));
342: }
343: }
344: }
345:
346: /**
347: * Apply the given iBATIS TransactionConfig to the SqlMapClient.
348: * <p>The default implementation casts to ExtendedSqlMapClient, retrieves the maximum
349: * number of concurrent transactions from the SqlMapExecutorDelegate, and sets
350: * an iBATIS TransactionManager with the given TransactionConfig.
351: * @param sqlMapClient the SqlMapClient to apply the TransactionConfig to
352: * @param transactionConfig the iBATIS TransactionConfig to apply
353: * @see com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient
354: * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#getMaxTransactions
355: * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#setTxManager
356: */
357: protected void applyTransactionConfig(SqlMapClient sqlMapClient,
358: TransactionConfig transactionConfig) {
359: if (!(this .sqlMapClient instanceof ExtendedSqlMapClient)) {
360: throw new IllegalArgumentException(
361: "Cannot set TransactionConfig with DataSource for SqlMapClient if not of type "
362: + "ExtendedSqlMapClient: "
363: + this .sqlMapClient);
364: }
365: ExtendedSqlMapClient extendedClient = (ExtendedSqlMapClient) this .sqlMapClient;
366: transactionConfig
367: .setMaximumConcurrentTransactions(extendedClient
368: .getDelegate().getMaxTransactions());
369: extendedClient.getDelegate().setTxManager(
370: new TransactionManager(transactionConfig));
371: }
372:
373: public Object getObject() {
374: return this .sqlMapClient;
375: }
376:
377: public Class getObjectType() {
378: return (this .sqlMapClient != null ? this .sqlMapClient
379: .getClass() : SqlMapClient.class);
380: }
381:
382: public boolean isSingleton() {
383: return true;
384: }
385:
386: }
|