View Javadoc
1   /**
2    *    Copyright 2009-2015 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    *       http://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 org.apache.ibatis.executor.loader;
17  
18  import java.io.Serializable;
19  import java.lang.reflect.Method;
20  import java.lang.reflect.Modifier;
21  import java.security.AccessController;
22  import java.security.PrivilegedActionException;
23  import java.security.PrivilegedExceptionAction;
24  import java.sql.SQLException;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Locale;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import org.apache.ibatis.executor.BaseExecutor;
32  import org.apache.ibatis.executor.BatchResult;
33  import org.apache.ibatis.executor.ExecutorException;
34  import org.apache.ibatis.logging.Log;
35  import org.apache.ibatis.logging.LogFactory;
36  import org.apache.ibatis.mapping.BoundSql;
37  import org.apache.ibatis.mapping.MappedStatement;
38  import org.apache.ibatis.reflection.MetaObject;
39  import org.apache.ibatis.session.Configuration;
40  import org.apache.ibatis.session.ResultHandler;
41  import org.apache.ibatis.session.RowBounds;
42  
43  /**
44   * @author Clinton Begin
45   * @author Franta Mejta
46   */
47  public class ResultLoaderMap {
48  
49    private final Map<String, LoadPair> loaderMap = new HashMap<String, LoadPair>();
50  
51    public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
52      String upperFirst = getUppercaseFirstProperty(property);
53      if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
54        throw new ExecutorException("Nested lazy loaded result property '" + property +
55                "' for query id '" + resultLoader.mappedStatement.getId() +
56                " already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map.");
57      }
58      loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
59    }
60  
61    public final Map<String, LoadPair> getProperties() {
62      return new HashMap<String, LoadPair>(this.loaderMap);
63    }
64  
65    public Set<String> getPropertyNames() {
66      return loaderMap.keySet();
67    }
68  
69    public int size() {
70      return loaderMap.size();
71    }
72  
73    public boolean hasLoader(String property) {
74      return loaderMap.containsKey(property.toUpperCase(Locale.ENGLISH));
75    }
76  
77    public boolean load(String property) throws SQLException {
78      LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
79      if (pair != null) {
80        pair.load();
81        return true;
82      }
83      return false;
84    }
85  
86    public void loadAll() throws SQLException {
87      final Set<String> methodNameSet = loaderMap.keySet();
88      String[] methodNames = methodNameSet.toArray(new String[methodNameSet.size()]);
89      for (String methodName : methodNames) {
90        load(methodName);
91      }
92    }
93  
94    private static String getUppercaseFirstProperty(String property) {
95      String[] parts = property.split("\\.");
96      return parts[0].toUpperCase(Locale.ENGLISH);
97    }
98  
99    /**
100    * Property which was not loaded yet.
101    */
102   public static class LoadPair implements Serializable {
103 
104     private static final long serialVersionUID = 20130412;
105     /**
106      * Name of factory method which returns database connection.
107      */
108     private static final String FACTORY_METHOD = "getConfiguration";
109     /**
110      * Object to check whether we went through serialization..
111      */
112     private final transient Object serializationCheck = new Object();
113     /**
114      * Meta object which sets loaded properties.
115      */
116     private transient MetaObject metaResultObject;
117     /**
118      * Result loader which loads unread properties.
119      */
120     private transient ResultLoader resultLoader;
121     /**
122      * Wow, logger.
123      */
124     private transient Log log;
125     /**
126      * Factory class through which we get database connection.
127      */
128     private Class<?> configurationFactory;
129     /**
130      * Name of the unread property.
131      */
132     private String property;
133     /**
134      * ID of SQL statement which loads the property.
135      */
136     private String mappedStatement;
137     /**
138      * Parameter of the sql statement.
139      */
140     private Serializable mappedParameter;
141 
142     private LoadPair(final String property, MetaObject metaResultObject, ResultLoader resultLoader) {
143       this.property = property;
144       this.metaResultObject = metaResultObject;
145       this.resultLoader = resultLoader;
146 
147       /* Save required information only if original object can be serialized. */
148       if (metaResultObject != null && metaResultObject.getOriginalObject() instanceof Serializable) {
149         final Object mappedStatementParameter = resultLoader.parameterObject;
150 
151         /* @todo May the parameter be null? */
152         if (mappedStatementParameter instanceof Serializable) {
153           this.mappedStatement = resultLoader.mappedStatement.getId();
154           this.mappedParameter = (Serializable) mappedStatementParameter;
155 
156           this.configurationFactory = resultLoader.configuration.getConfigurationFactory();
157         } else {
158           Log log = this.getLogger();
159           if (log.isDebugEnabled()) {
160             log.debug("Property [" + this.property + "] of ["
161                     + metaResultObject.getOriginalObject().getClass() + "] cannot be loaded "
162                     + "after deserialization. Make sure it's loaded before serializing "
163                     + "forenamed object.");
164           }
165         }
166       }
167     }
168 
169     public void load() throws SQLException {
170       /* These field should not be null unless the loadpair was serialized.
171        * Yet in that case this method should not be called. */
172       if (this.metaResultObject == null) {
173         throw new IllegalArgumentException("metaResultObject is null");
174       }
175       if (this.resultLoader == null) {
176         throw new IllegalArgumentException("resultLoader is null");
177       }
178 
179       this.load(null);
180     }
181 
182     public void load(final Object userObject) throws SQLException {
183       if (this.metaResultObject == null || this.resultLoader == null) {
184         if (this.mappedParameter == null) {
185           throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
186                   + "required parameter of mapped statement ["
187                   + this.mappedStatement + "] is not serializable.");
188         }
189 
190         final Configuration config = this.getConfiguration();
191         final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
192         if (ms == null) {
193           throw new ExecutorException("Cannot lazy load property [" + this.property
194                   + "] of deserialized object [" + userObject.getClass()
195                   + "] because configuration does not contain statement ["
196                   + this.mappedStatement + "]");
197         }
198 
199         this.metaResultObject = config.newMetaObject(userObject);
200         this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
201                 metaResultObject.getSetterType(this.property), null, null);
202       }
203 
204       /* We are using a new executor because we may be (and likely are) on a new thread
205        * and executors aren't thread safe. (Is this sufficient?)
206        *
207        * A better approach would be making executors thread safe. */
208       if (this.serializationCheck == null) {
209         final ResultLoader old = this.resultLoader;
210         this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
211                 old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
212       }
213 
214       this.metaResultObject.setValue(property, this.resultLoader.loadResult());
215     }
216 
217     private Configuration getConfiguration() {
218       if (this.configurationFactory == null) {
219         throw new ExecutorException("Cannot get Configuration as configuration factory was not set.");
220       }
221 
222       Object configurationObject = null;
223       try {
224         final Method factoryMethod = this.configurationFactory.getDeclaredMethod(FACTORY_METHOD);
225         if (!Modifier.isStatic(factoryMethod.getModifiers())) {
226           throw new ExecutorException("Cannot get Configuration as factory method ["
227                   + this.configurationFactory + "]#["
228                   + FACTORY_METHOD + "] is not static.");
229         }
230 
231         if (!factoryMethod.isAccessible()) {
232           configurationObject = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
233             @Override
234             public Object run() throws Exception {
235               try {
236                 factoryMethod.setAccessible(true);
237                 return factoryMethod.invoke(null);
238               } finally {
239                 factoryMethod.setAccessible(false);
240               }
241             }
242           });
243         } else {
244           configurationObject = factoryMethod.invoke(null);
245         }
246       } catch (final NoSuchMethodException ex) {
247         throw new ExecutorException("Cannot get Configuration as factory class ["
248                 + this.configurationFactory + "] is missing factory method of name ["
249                 + FACTORY_METHOD + "].", ex);
250       } catch (final PrivilegedActionException ex) {
251         throw new ExecutorException("Cannot get Configuration as factory method ["
252                 + this.configurationFactory + "]#["
253                 + FACTORY_METHOD + "] threw an exception.", ex.getCause());
254       } catch (final Exception ex) {
255         throw new ExecutorException("Cannot get Configuration as factory method ["
256                 + this.configurationFactory + "]#["
257                 + FACTORY_METHOD + "] threw an exception.", ex);
258       }
259 
260       if (!(configurationObject instanceof Configuration)) {
261         throw new ExecutorException("Cannot get Configuration as factory method ["
262                 + this.configurationFactory + "]#["
263                 + FACTORY_METHOD + "] didn't return [" + Configuration.class + "] but ["
264                 + (configurationObject == null ? "null" : configurationObject.getClass()) + "].");
265       }
266 
267       return Configuration.class.cast(configurationObject);
268     }
269 
270     private Log getLogger() {
271       if (this.log == null) {
272         this.log = LogFactory.getLog(this.getClass());
273       }
274       return this.log;
275     }
276   }
277 
278   private static final class ClosedExecutor extends BaseExecutor {
279 
280     public ClosedExecutor() {
281       super(null, null);
282     }
283 
284     @Override
285     public boolean isClosed() {
286       return true;
287     }
288 
289     @Override
290     protected int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
291       throw new UnsupportedOperationException("Not supported.");
292     }
293 
294     @Override
295     protected List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
296       throw new UnsupportedOperationException("Not supported.");
297     }
298 
299     @Override
300     protected <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
301       throw new UnsupportedOperationException("Not supported.");
302     }
303   }
304 }