View Javadoc
1   /*
2    * Copyright 2004-2025 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    *    https://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  package com.ibatis.sqlmap.engine.mapping.result;
17  
18  import com.ibatis.common.resources.Resources;
19  
20  import java.util.ArrayDeque;
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.Deque;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Set;
27  
28  /**
29   * This class is used to create instances of result objects. It will use the configured ResultObjectFactory if there is
30   * one, otherwise it will use iBATIS' normal methods.
31   * <p>
32   * Note that this class is somewhat tightly coupled with SqlExecuter - SqlExecute must call the setStatementId() and
33   * setResultObjectFactory() methods before executing a statement. This is a result of using a ThreadLocal to hold the
34   * current configuration for the statement under execution. Using a ThreadLocal is a solution for IBATIS-366. Without a
35   * ThreadLocal, the current factory and statement id would have to be added to many method signatures - often in
36   * inappropriate places.
37   *
38   * @author Jeff Butler
39   */
40  public class ResultObjectFactoryUtil {
41  
42    /**
43     * Use a ThreadLocal to hold the current statementId and factory. This is much easier than passing these items all
44     * over the place, and it has no measurable impact on performance (I did a test with 100000 result rows and found no
45     * impact - Jeff Butler).
46     */
47    private static ThreadLocal<Deque<FactorySettings>> factorySettings = new ThreadLocal<>();
48  
49    /**
50     * Utility class - no instances.
51     */
52    private ResultObjectFactoryUtil() {
53      super();
54    }
55  
56    /**
57     * Algorithm:
58     * <ul>
59     * <li>If factory is null, then create object internally()</li>
60     * <li>Otherwise try to create object through factory</li>
61     * <li>If null returned from factory, then create object internally</li>
62     * </ul>
63     * This allows the factory to selectively create objects, also allows for the common possibility that a factory is not
64     * configured.
65     *
66     * @param clazz
67     *          the type of object to create
68     *
69     * @return a new instance of the specified class. The instance must be castable to the specified class.
70     *
71     * @throws InstantiationException
72     *           if the instance cannot be created. If you throw this Exception, iBATIS will throw a runtime exception in
73     *           response and will end.
74     * @throws IllegalAccessException
75     *           if the constructor cannot be accessed. If you throw this Exception, iBATIS will throw a runtime exception
76     *           in response and will end.
77     */
78    public static Object createObjectThroughFactory(Class clazz) throws InstantiationException, IllegalAccessException {
79  
80      FactorySettings fs = getCurrentFactorySettings();
81  
82      Object obj;
83      if (fs.getResultObjectFactory() == null) {
84        obj = createObjectInternally(clazz);
85      } else {
86        obj = fs.getResultObjectFactory().createInstance(fs.getStatementId(), clazz);
87        if (obj == null) {
88          obj = createObjectInternally(clazz);
89        }
90      }
91  
92      return obj;
93    }
94  
95    /**
96     * This method creates object using iBATIS' normal mechanism. We translate List and Collection to ArrayList, and Set
97     * to HashSet because these interfaces may be requested in nested resultMaps and we want to supply default
98     * implementations.
99     *
100    * @param clazz
101    *          the clazz
102    *
103    * @return the object
104    *
105    * @throws InstantiationException
106    *           the instantiation exception
107    * @throws IllegalAccessException
108    *           the illegal access exception
109    */
110   private static Object createObjectInternally(Class clazz) throws InstantiationException, IllegalAccessException {
111     Class classToCreate;
112     if (clazz == List.class || clazz == Collection.class) {
113       classToCreate = ArrayList.class;
114     } else if (clazz == Set.class) {
115       classToCreate = HashSet.class;
116     } else {
117       classToCreate = clazz;
118     }
119 
120     return Resources.instantiate(classToCreate);
121   }
122 
123   /**
124    * This method pushes a new result object factory configuration onto the stack. We use a stack because the method can
125    * be called in a "nested" fashion if there are sub-selects. Calls to this method should be equally balanced with
126    * calls to cleanupResultObjectFactory().
127    *
128    * @param resultObjectFactory
129    *          the result object factory
130    * @param statementId
131    *          the statement id
132    */
133   public static void setupResultObjectFactory(ResultObjectFactory resultObjectFactory, String statementId) {
134     Deque<FactorySettings> fss = factorySettings.get();
135     if (fss == null) {
136       fss = new ArrayDeque<>();
137       factorySettings.set(fss);
138     }
139 
140     FactorySettings fs = new FactorySettings();
141     fs.setResultObjectFactory(resultObjectFactory);
142     fs.setStatementId(statementId);
143     fss.push(fs);
144   }
145 
146   /**
147    * Removes the FactorySettings bound to the current thread to avoid classloader leak issues. This method pops the top
148    * item off the stack, and kills the stack if there are no items left.
149    */
150   public static void cleanupResultObjectFactory() {
151     Deque<FactorySettings> fss = factorySettings.get();
152     if (!fss.isEmpty()) {
153       fss.pop();
154     }
155 
156     if (fss.isEmpty()) {
157       factorySettings.remove();
158     }
159   }
160 
161   /**
162    * Gets the current factory settings.
163    *
164    * @return the current factory settings
165    */
166   private static FactorySettings getCurrentFactorySettings() {
167     Deque<FactorySettings> fss = factorySettings.get();
168     FactorySettings fs;
169     if (fss == null || fss.isEmpty()) {
170       // this shouldn't happen if the SqlExecuter is behaving correctly
171       fs = new FactorySettings();
172     } else {
173       fs = fss.peek();
174     }
175 
176     return fs;
177   }
178 
179   /**
180    * The Class FactorySettings.
181    */
182   private static class FactorySettings {
183 
184     /** The result object factory. */
185     private ResultObjectFactory resultObjectFactory;
186 
187     /** The statement id. */
188     private String statementId;
189 
190     /**
191      * Gets the result object factory.
192      *
193      * @return the result object factory
194      */
195     public ResultObjectFactory getResultObjectFactory() {
196       return resultObjectFactory;
197     }
198 
199     /**
200      * Sets the result object factory.
201      *
202      * @param resultObjectFactory
203      *          the new result object factory
204      */
205     public void setResultObjectFactory(ResultObjectFactory resultObjectFactory) {
206       this.resultObjectFactory = resultObjectFactory;
207     }
208 
209     /**
210      * Gets the statement id.
211      *
212      * @return the statement id
213      */
214     public String getStatementId() {
215       return statementId;
216     }
217 
218     /**
219      * Sets the statement id.
220      *
221      * @param statementId
222      *          the new statement id
223      */
224     public void setStatementId(String statementId) {
225       this.statementId = statementId;
226     }
227   }
228 }