SqlMapClientFactoryBean.java

/*
 * SPDX-License-Identifier: Apache-2.0
 * See LICENSE file for details.
 *
 * Copyright 2015-2026 the original author or authors.
 */
package org.springframework.orm.ibatis;

import com.ibatis.common.xml.NodeletException;
import com.ibatis.sqlmap.client.SqlMapClient;
import com.ibatis.sqlmap.client.SqlMapClientBuilder;
import com.ibatis.sqlmap.engine.builder.xml.SqlMapConfigParser;
import com.ibatis.sqlmap.engine.builder.xml.SqlMapParser;
import com.ibatis.sqlmap.engine.builder.xml.XmlParserState;
import com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient;
import com.ibatis.sqlmap.engine.transaction.TransactionConfig;
import com.ibatis.sqlmap.engine.transaction.TransactionManager;
import com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Properties;

import javax.sql.DataSource;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.util.ObjectUtils;

/**
 * {@link org.springframework.beans.factory.FactoryBean} that creates an iBATIS
 * {@link com.ibatis.sqlmap.client.SqlMapClient}. This is the usual way to set up a shared iBATIS SqlMapClient in a
 * Spring application context; the SqlMapClient can then be passed to iBATIS-based DAOs via dependency injection.
 * <p>
 * Either {@link org.springframework.jdbc.datasource.DataSourceTransactionManager} or
 * {@link org.springframework.transaction.jta.JtaTransactionManager} can be used for transaction demarcation in
 * combination with a SqlMapClient, with JTA only necessary for transactions which span multiple databases.
 * <p>
 * Allows for specifying a DataSource at the SqlMapClient level. This is preferable to per-DAO DataSource references, as
 * it allows for lazy loading and avoids repeated DataSource references in every DAO.
 * <p>
 * <b>Note:</b> As of Spring 2.5.5, this class (finally) requires iBATIS 2.3 or higher. The new "mappingLocations"
 * feature requires iBATIS 2.3.2.
 *
 * @author Juergen Hoeller
 *
 * @since 24.02.2004
 *
 * @see #setConfigLocation
 * @see #setDataSource
 * @see SqlMapClientTemplate#setSqlMapClient
 * @see SqlMapClientTemplate#setDataSource
 *
 * @deprecated as of Spring 3.2, in favor of the native Spring support in the Mybatis follow-up project
 *             (https://mybatis.org/)
 */
@Deprecated
public class SqlMapClientFactoryBean implements FactoryBean<SqlMapClient>, InitializingBean {

  private static final ThreadLocal<LobHandler> configTimeLobHandlerHolder = new ThreadLocal<>();

  /**
   * Return the LobHandler for the currently configured iBATIS SqlMapClient, to be used by TypeHandler implementations
   * like ClobStringTypeHandler.
   * <p>
   * This instance will be set before initialization of the corresponding SqlMapClient, and reset immediately
   * afterwards. It is thus only available during configuration.
   *
   * @see #setLobHandler
   * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler
   * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler
   * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler
   */
  public static LobHandler getConfigTimeLobHandler() {
    return configTimeLobHandlerHolder.get();
  }

  private Resource[] configLocations;

  private Resource[] mappingLocations;

  private Properties sqlMapClientProperties;

  private DataSource dataSource;

  private boolean useTransactionAwareDataSource = true;

  private Class<? extends TransactionConfig> transactionConfigClass = ExternalTransactionConfig.class;

  private Properties transactionConfigProperties;

  private LobHandler lobHandler;

  private SqlMapClient sqlMapClient;

  public SqlMapClientFactoryBean() {
    this.transactionConfigProperties = new Properties();
    this.transactionConfigProperties.setProperty("SetAutoCommitAllowed", "false");
  }

  /**
   * Set the location of the iBATIS SqlMapClient config file. A typical value is "WEB-INF/sql-map-config.xml".
   *
   * @see #setConfigLocations
   */
  public void setConfigLocation(Resource configLocation) {
    this.configLocations = (configLocation != null ? new Resource[] { configLocation } : null);
  }

  /**
   * Set multiple locations of iBATIS SqlMapClient config files that are going to be merged into one unified
   * configuration at runtime.
   *
   * @param configLocations
   *          the config locations
   */
  public void setConfigLocations(Resource[] configLocations) {
    this.configLocations = configLocations;
  }

  /**
   * Set locations of iBATIS sql-map mapping files that are going to be merged into the SqlMapClient configuration at
   * runtime.
   * <p>
   * This is an alternative to specifying "&lt;sqlMap&gt;" entries in a sql-map-client config file. This property being
   * based on Spring's resource abstraction also allows for specifying resource patterns here: e.g. "/myApp/*-map.xml".
   * <p>
   * Note that this feature requires iBATIS 2.3.2; it will not work with any previous iBATIS version.
   *
   * @param mappingLocations
   *          the mapping locations
   */
  public void setMappingLocations(Resource[] mappingLocations) {
    this.mappingLocations = mappingLocations;
  }

  /**
   * Set optional properties to be passed into the SqlMapClientBuilder, as alternative to a {@code &lt;properties&gt;}
   * tag in the sql-map-config.xml file. Will be used to resolve placeholders in the config file.
   *
   * @param sqlMapClientProperties
   *          the client properties
   *
   * @see #setConfigLocation
   * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient(java.io.InputStream, java.util.Properties)
   */
  public void setSqlMapClientProperties(Properties sqlMapClientProperties) {
    this.sqlMapClientProperties = sqlMapClientProperties;
  }

  /**
   * Set the DataSource to be used by iBATIS SQL Maps. This will be passed to the SqlMapClient as part of a
   * TransactionConfig instance.
   * <p>
   * If specified, this will override corresponding settings in the SqlMapClient properties. Usually, you will specify
   * DataSource and transaction configuration <i>either</i> here <i>or</i> in SqlMapClient properties.
   * <p>
   * Specifying a DataSource for the SqlMapClient rather than for each individual DAO allows for lazy loading, for
   * example when using PaginatedList results.
   * <p>
   * With a DataSource passed in here, you don't need to specify one for each DAO. Passing the SqlMapClient to the DAOs
   * is enough, as it already carries a DataSource. Thus, it's recommended to specify the DataSource at this central
   * location only.
   * <p>
   * Thanks to Brandon Goodin from the iBATIS team for the hint on how to make this work with Spring's integration
   * strategy!
   *
   * @param dataSource
   *          the data source
   *
   * @see #setTransactionConfigClass
   * @see #setTransactionConfigProperties
   * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource
   * @see SqlMapClientTemplate#setDataSource
   */
  public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  /**
   * Set whether to use a transaction-aware DataSource for the SqlMapClient, i.e. whether to automatically wrap the
   * passed-in DataSource with Spring's TransactionAwareDataSourceProxy.
   * <p>
   * Default is "true": When the SqlMapClient performs direct database operations outside of Spring's
   * SqlMapClientTemplate (for example, lazy loading or direct SqlMapClient access), it will still participate in active
   * Spring-managed transactions.
   * <p>
   * As a further effect, using a transaction-aware DataSource will apply remaining transaction timeouts to all created
   * JDBC Statements. This means that all operations performed by the SqlMapClient will automatically participate in
   * Spring-managed transaction timeouts.
   * <p>
   * Turn this flag off to get raw DataSource handling, without Spring transaction checks. Operations on Spring's
   * SqlMapClientTemplate will still detect Spring-managed transactions, but lazy loading or direct SqlMapClient access
   * won't.
   *
   * @param useTransactionAwareDataSource
   *          whether to use transaction aware datasource
   *
   * @see #setDataSource
   * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
   * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
   * @see SqlMapClientTemplate
   * @see com.ibatis.sqlmap.client.SqlMapClient
   */
  public void setUseTransactionAwareDataSource(boolean useTransactionAwareDataSource) {
    this.useTransactionAwareDataSource = useTransactionAwareDataSource;
  }

  /**
   * Set the iBATIS TransactionConfig class to use. Default is
   * {@code com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig}.
   * <p>
   * Will only get applied when using a Spring-managed DataSource. An instance of this class will get populated with the
   * given DataSource and initialized with the given properties.
   * <p>
   * The default ExternalTransactionConfig is appropriate if there is external transaction management that the
   * SqlMapClient should participate in: be it Spring transaction management, EJB CMT or plain JTA. This should be the
   * typical scenario. If there is no active transaction, SqlMapClient operations will execute SQL statements
   * non-transactionally.
   * <p>
   * JdbcTransactionConfig or JakartaTransactionConfig/JavaxTransactionConfig is only necessary when using the iBATIS
   * SqlMapTransactionManager API instead of external transactions. If there is no explicit transaction, SqlMapClient
   * operations will automatically start a transaction for their own scope (in contrast to the external transaction
   * mode, see above).
   * <p>
   * <b>It is strongly recommended to use iBATIS SQL Maps with Spring transaction management (or EJB CMT).</b> In this
   * case, the default ExternalTransactionConfig is fine. Lazy loading and SQL Maps operations without explicit
   * transaction demarcation will execute non-transactionally.
   * <p>
   * Even with Spring transaction management, it might be desirable to specify JdbcTransactionConfig: This will still
   * participate in existing Spring-managed transactions, but lazy loading and operations without explicit transaction
   * demaration will execute in their own auto-started transactions. However, this is usually not necessary.
   *
   * @param transactionConfigClass
   *          the transaction config class
   *
   * @see #setDataSource
   * @see #setTransactionConfigProperties
   * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig
   * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig
   * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig
   * @see com.ibatis.sqlmap.engine.transaction.jta.JakartaTransactionConfig
   * @see com.ibatis.sqlmap.engine.transaction.jta.JavaxTransactionConfig
   * @see com.ibatis.sqlmap.client.SqlMapTransactionManager
   */
  public void setTransactionConfigClass(Class<? extends TransactionConfig> transactionConfigClass) {
    if (transactionConfigClass == null || !TransactionConfig.class.isAssignableFrom(transactionConfigClass)) {
      throw new IllegalArgumentException("Invalid transactionConfigClass: does not implement "
          + "com.ibatis.sqlmap.engine.transaction.TransactionConfig");
    }
    this.transactionConfigClass = transactionConfigClass;
  }

  /**
   * Set properties to be passed to the TransactionConfig instance used by this SqlMapClient. Supported properties
   * depend on the concrete TransactionConfig implementation used:
   * <p>
   * <ul>
   * <li><b>ExternalTransactionConfig</b> supports "DefaultAutoCommit" (default: false) and "SetAutoCommitAllowed"
   * (default: true). Note that Spring uses SetAutoCommitAllowed = false as default, in contrast to the iBATIS default,
   * to always keep the original autoCommit value as provided by the connection pool.
   * <li><b>JdbcTransactionConfig</b> does not supported any properties.
   * <li><b>JakartaTransactionConfig</b> (jakarta namespace) and <b>JavaxTransactionConfig</b> (javax namespace) both
   * support "UserTransaction" (no default), specifying the JNDI location of the JTA UserTransaction (usually
   * "java:comp/UserTransaction").
   * </ul>
   *
   * @param transactionConfigProperties
   *          the transaction config properties
   *
   * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig#initialize
   * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig
   * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig
   * @see com.ibatis.sqlmap.engine.transaction.jta.JakartaTransactionConfig
   * @see com.ibatis.sqlmap.engine.transaction.jta.JavaxTransactionConfig
   */
  public void setTransactionConfigProperties(Properties transactionConfigProperties) {
    this.transactionConfigProperties = transactionConfigProperties;
  }

  /**
   * Set the LobHandler to be used by the SqlMapClient. Will be exposed at config time for TypeHandler implementations.
   *
   * @param lobHandler
   *          the lob handler
   *
   * @see #getConfigTimeLobHandler
   * @see com.ibatis.sqlmap.engine.type.TypeHandler
   * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler
   * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler
   * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler
   */
  public void setLobHandler(LobHandler lobHandler) {
    this.lobHandler = lobHandler;
  }

  @Override
  public void afterPropertiesSet() throws Exception {
    if (this.lobHandler != null) {
      // Make given LobHandler available for SqlMapClient configuration.
      // Do early because mapping resource might refer to custom types.
      configTimeLobHandlerHolder.set(this.lobHandler);
    }

    try {
      this.sqlMapClient = buildSqlMapClient(this.configLocations, this.mappingLocations, this.sqlMapClientProperties);

      // Tell the SqlMapClient to use the given DataSource, if any.
      if (this.dataSource != null) {
        TransactionConfig transactionConfig = this.transactionConfigClass.getDeclaredConstructor().newInstance();
        DataSource dataSourceToUse = this.dataSource;
        if (this.useTransactionAwareDataSource && !(this.dataSource instanceof TransactionAwareDataSourceProxy)) {
          dataSourceToUse = new TransactionAwareDataSourceProxy(this.dataSource);
        }
        transactionConfig.setDataSource(dataSourceToUse);
        transactionConfig.setProperties(this.transactionConfigProperties);
        applyTransactionConfig(this.sqlMapClient, transactionConfig);
      }
    } finally {
      if (this.lobHandler != null) {
        // Reset LobHandler holder.
        configTimeLobHandlerHolder.remove();
      }
    }
  }

  /**
   * Build a SqlMapClient instance based on the given standard configuration.
   * <p>
   * The default implementation uses the standard iBATIS {@link SqlMapClientBuilder} API to build a SqlMapClient
   * instance based on an InputStream.
   *
   * @param configLocations
   *          the config files to load from
   * @param mappingLocations
   *          the mapping files to load from
   * @param properties
   *          the SqlMapClient properties (if any)
   *
   * @return the SqlMapClient instance (never {@code null})
   *
   * @throws IOException
   *           if loading the config file failed
   *
   * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient
   */
  protected SqlMapClient buildSqlMapClient(Resource[] configLocations, Resource[] mappingLocations,
      Properties properties) throws IOException {

    if (ObjectUtils.isEmpty(configLocations)) {
      throw new IllegalArgumentException("At least 1 'configLocation' entry is required");
    }

    SqlMapClient client = null;
    SqlMapConfigParser configParser = new SqlMapConfigParser();
    for (Resource configLocation : configLocations) {
      InputStream is = configLocation.getInputStream();
      try {
        client = configParser.parse(is, properties);
      } catch (RuntimeException ex) {
        throw new IOException("Failed to parse config resource: " + configLocation, ex.getCause());
      }
    }

    if (mappingLocations != null) {
      SqlMapParser mapParser = SqlMapParserFactory.createSqlMapParser(configParser);
      for (Resource mappingLocation : mappingLocations) {
        try {
          mapParser.parse(mappingLocation.getInputStream());
        } catch (NodeletException ex) {
          throw new IOException("Failed to parse mapping resource: " + mappingLocation, ex);
        }
      }
    }

    return client;
  }

  /**
   * Apply the given iBATIS TransactionConfig to the SqlMapClient.
   * <p>
   * The default implementation casts to ExtendedSqlMapClient, retrieves the maximum number of concurrent transactions
   * from the SqlMapExecutorDelegate, and sets an iBATIS TransactionManager with the given TransactionConfig.
   *
   * @param sqlMapClient
   *          the SqlMapClient to apply the TransactionConfig to
   * @param transactionConfig
   *          the iBATIS TransactionConfig to apply
   *
   * @see com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient
   * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#getMaxTransactions
   * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#setTxManager
   */
  protected void applyTransactionConfig(SqlMapClient sqlMapClient, TransactionConfig transactionConfig) {
    if (!(sqlMapClient instanceof ExtendedSqlMapClient)) {
      throw new IllegalArgumentException("Cannot set TransactionConfig with DataSource for SqlMapClient if not of type "
          + "ExtendedSqlMapClient: " + sqlMapClient);
    }
    ExtendedSqlMapClient extendedClient = (ExtendedSqlMapClient) sqlMapClient;
    extendedClient.getDelegate().setTxManager(new TransactionManager(transactionConfig));
  }

  @Override
  public SqlMapClient getObject() {
    return this.sqlMapClient;
  }

  @Override
  public Class<? extends SqlMapClient> getObjectType() {
    return (this.sqlMapClient != null ? this.sqlMapClient.getClass() : SqlMapClient.class);
  }

  @Override
  public boolean isSingleton() {
    return true;
  }

  /**
   * Inner class to avoid hard-coded iBATIS 2.3.2 dependency (XmlParserState class).
   */
  private static class SqlMapParserFactory {

    public static SqlMapParser createSqlMapParser(SqlMapConfigParser configParser) {
      // Ideally: XmlParserState state = configParser.getState();
      XmlParserState state;
      try {
        Field stateField = SqlMapConfigParser.class.getDeclaredField("state");
        stateField.setAccessible(true);
        state = (XmlParserState) stateField.get(configParser);
      } catch (Exception ex) {
        throw new IllegalStateException("iBATIS 2.3.2 'state' field not found in SqlMapConfigParser class - "
            + "please upgrade to IBATIS 2.3.2 or higher in order to use the new 'mappingLocations' feature. " + ex);
      }
      return new SqlMapParser(state);
    }
  }

}