ResultObjectFactoryUtil.java

/*
 * Copyright 2004-2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ibatis.sqlmap.engine.mapping.result;

import com.ibatis.common.resources.Resources;

import java.util.*;

/**
 * This class is used to create instances of result objects. It will use the configured ResultObjectFactory if there is
 * one, otherwise it will use iBATIS' normal methods.
 * <p>
 * Note that this class is somewhat tightly coupled with SqlExecuter - SqlExecute must call the setStatementId() and
 * setResultObjectFactory() methods before executing a statement. This is a result of using a ThreadLocal to hold the
 * current configuration for the statement under execution. Using a ThreadLocal is a solution for IBATIS-366. Without a
 * ThreadLocal, the current factory and statement id would have to be added to many method signatures - often in
 * inappropriate places.
 *
 * @author Jeff Butler
 */
public class ResultObjectFactoryUtil {

  /**
   * Use a ThreadLocal to hold the current statementId and factory. This is much easier than passing these items all
   * over the place, and it has no measurable impact on performance (I did a test with 100000 result rows and found no
   * impact - Jeff Butler).
   */
  private static ThreadLocal<Deque<FactorySettings>> factorySettings = new ThreadLocal<>();

  /**
   * Utility class - no instances.
   */
  private ResultObjectFactoryUtil() {
    super();
  }

  /**
   * Algorithm:
   * <ul>
   * <li>If factory is null, then create object internally()</li>
   * <li>Otherwise try to create object through factory</li>
   * <li>If null returned from factory, then create object internally</li>
   * </ul>
   * This allows the factory to selectively create objects, also allows for the common possibility that a factory is not
   * configured.
   *
   * @param clazz
   *          the type of object to create
   *
   * @return a new instance of the specified class. The instance must be castable to the specified class.
   *
   * @throws InstantiationException
   *           if the instance cannot be created. If you throw this Exception, iBATIS will throw a runtime exception in
   *           response and will end.
   * @throws IllegalAccessException
   *           if the constructor cannot be accessed. If you throw this Exception, iBATIS will throw a runtime exception
   *           in response and will end.
   */
  public static Object createObjectThroughFactory(Class clazz) throws InstantiationException, IllegalAccessException {

    FactorySettings fs = getCurrentFactorySettings();

    Object obj;
    if (fs.getResultObjectFactory() == null) {
      obj = createObjectInternally(clazz);
    } else {
      obj = fs.getResultObjectFactory().createInstance(fs.getStatementId(), clazz);
      if (obj == null) {
        obj = createObjectInternally(clazz);
      }
    }

    return obj;
  }

  /**
   * This method creates object using iBATIS' normal mechanism. We translate List and Collection to ArrayList, and Set
   * to HashSet because these interfaces may be requested in nested resultMaps and we want to supply default
   * implementations.
   *
   * @param clazz
   *          the clazz
   *
   * @return the object
   *
   * @throws InstantiationException
   *           the instantiation exception
   * @throws IllegalAccessException
   *           the illegal access exception
   */
  private static Object createObjectInternally(Class clazz) throws InstantiationException, IllegalAccessException {
    Class classToCreate;
    if (clazz == List.class || clazz == Collection.class) {
      classToCreate = ArrayList.class;
    } else if (clazz == Set.class) {
      classToCreate = HashSet.class;
    } else {
      classToCreate = clazz;
    }

    return Resources.instantiate(classToCreate);
  }

  /**
   * This method pushes a new result object factory configuration onto the stack. We use a stack because the method can
   * be called in a "nested" fashion if there are sub-selects. Calls to this method should be equally balanced with
   * calls to cleanupResultObjectFactory().
   *
   * @param resultObjectFactory
   *          the result object factory
   * @param statementId
   *          the statement id
   */
  public static void setupResultObjectFactory(ResultObjectFactory resultObjectFactory, String statementId) {
    Deque<FactorySettings> fss = factorySettings.get();
    if (fss == null) {
      fss = new ArrayDeque<>();
      factorySettings.set(fss);
    }

    FactorySettings fs = new FactorySettings();
    fs.setResultObjectFactory(resultObjectFactory);
    fs.setStatementId(statementId);
    fss.push(fs);
  }

  /**
   * Removes the FactorySettings bound to the current thread to avoid classloader leak issues. This method pops the top
   * item off the stack, and kills the stack if there are no items left.
   */
  public static void cleanupResultObjectFactory() {
    Deque<FactorySettings> fss = factorySettings.get();
    if (!fss.isEmpty()) {
      fss.pop();
    }

    if (fss.isEmpty()) {
      factorySettings.remove();
    }
  }

  /**
   * Gets the current factory settings.
   *
   * @return the current factory settings
   */
  private static FactorySettings getCurrentFactorySettings() {
    Deque<FactorySettings> fss = factorySettings.get();
    FactorySettings fs;
    if (fss == null || fss.isEmpty()) {
      // this shouldn't happen if the SqlExecuter is behaving correctly
      fs = new FactorySettings();
    } else {
      fs = fss.peek();
    }

    return fs;
  }

  /**
   * The Class FactorySettings.
   */
  private static class FactorySettings {

    /** The result object factory. */
    private ResultObjectFactory resultObjectFactory;

    /** The statement id. */
    private String statementId;

    /**
     * Gets the result object factory.
     *
     * @return the result object factory
     */
    public ResultObjectFactory getResultObjectFactory() {
      return resultObjectFactory;
    }

    /**
     * Sets the result object factory.
     *
     * @param resultObjectFactory
     *          the new result object factory
     */
    public void setResultObjectFactory(ResultObjectFactory resultObjectFactory) {
      this.resultObjectFactory = resultObjectFactory;
    }

    /**
     * Gets the statement id.
     *
     * @return the statement id
     */
    public String getStatementId() {
      return statementId;
    }

    /**
     * Sets the statement id.
     *
     * @param statementId
     *          the new statement id
     */
    public void setStatementId(String statementId) {
      this.statementId = statementId;
    }
  }
}