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.resultset;
17  
18  import java.lang.reflect.Constructor;
19  import java.sql.CallableStatement;
20  import java.sql.ResultSet;
21  import java.sql.SQLException;
22  import java.sql.Statement;
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.HashSet;
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.cache.CacheKey;
32  import org.apache.ibatis.executor.ErrorContext;
33  import org.apache.ibatis.executor.Executor;
34  import org.apache.ibatis.executor.ExecutorException;
35  import org.apache.ibatis.executor.loader.ResultLoader;
36  import org.apache.ibatis.executor.loader.ResultLoaderMap;
37  import org.apache.ibatis.executor.parameter.ParameterHandler;
38  import org.apache.ibatis.executor.result.DefaultResultContext;
39  import org.apache.ibatis.executor.result.DefaultResultHandler;
40  import org.apache.ibatis.executor.result.ResultMapException;
41  import org.apache.ibatis.mapping.BoundSql;
42  import org.apache.ibatis.mapping.Discriminator;
43  import org.apache.ibatis.mapping.MappedStatement;
44  import org.apache.ibatis.mapping.ParameterMapping;
45  import org.apache.ibatis.mapping.ParameterMode;
46  import org.apache.ibatis.mapping.ResultMap;
47  import org.apache.ibatis.mapping.ResultMapping;
48  import org.apache.ibatis.reflection.MetaClass;
49  import org.apache.ibatis.reflection.MetaObject;
50  import org.apache.ibatis.reflection.ReflectorFactory;
51  import org.apache.ibatis.reflection.factory.ObjectFactory;
52  import org.apache.ibatis.session.AutoMappingBehavior;
53  import org.apache.ibatis.session.Configuration;
54  import org.apache.ibatis.session.ResultContext;
55  import org.apache.ibatis.session.ResultHandler;
56  import org.apache.ibatis.session.RowBounds;
57  import org.apache.ibatis.type.TypeHandler;
58  import org.apache.ibatis.type.TypeHandlerRegistry;
59  
60  /**
61   * @author Clinton Begin
62   * @author Eduardo Macarron
63   */
64  public class DefaultResultSetHandler implements ResultSetHandler {
65  
66    private static final Object DEFERED = new Object();
67  
68    private final Executor executor;
69    private final Configuration configuration;
70    private final MappedStatement mappedStatement;
71    private final RowBounds rowBounds;
72    private final ParameterHandler parameterHandler;
73    private final ResultHandler<?> resultHandler;
74    private final BoundSql boundSql;
75    private final TypeHandlerRegistry typeHandlerRegistry;
76    private final ObjectFactory objectFactory;
77    private final ReflectorFactory reflectorFactory;
78  
79    // nested resultmaps
80    private final Map<CacheKey, Object> nestedResultObjects = new HashMap<CacheKey, Object>();
81    private final Map<CacheKey, Object> ancestorObjects = new HashMap<CacheKey, Object>();
82    private final Map<String, String> ancestorColumnPrefix = new HashMap<String, String>();
83  
84    // multiple resultsets
85    private final Map<String, ResultMapping> nextResultMaps = new HashMap<String, ResultMapping>();
86    private final Map<CacheKey, List<PendingRelation>> pendingRelations = new HashMap<CacheKey, List<PendingRelation>>();
87  
88    private static class PendingRelation {
89      public MetaObject metaObject;
90      public ResultMapping propertyMapping;
91    }
92  
93    public DefaultResultSetHandler(Executor executor, MappedStatement mappedStatement, ParameterHandler parameterHandler, ResultHandler<?> resultHandler, BoundSql boundSql,
94        RowBounds rowBounds) {
95      this.executor = executor;
96      this.configuration = mappedStatement.getConfiguration();
97      this.mappedStatement = mappedStatement;
98      this.rowBounds = rowBounds;
99      this.parameterHandler = parameterHandler;
100     this.boundSql = boundSql;
101     this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
102     this.objectFactory = configuration.getObjectFactory();
103     this.reflectorFactory = configuration.getReflectorFactory();
104     this.resultHandler = resultHandler;
105   }
106 
107   //
108   // HANDLE OUTPUT PARAMETER
109   //
110 
111   @Override
112   public void handleOutputParameters(CallableStatement cs) throws SQLException {
113     final Object parameterObject = parameterHandler.getParameterObject();
114     final MetaObject metaParam = configuration.newMetaObject(parameterObject);
115     final List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
116     for (int i = 0; i < parameterMappings.size(); i++) {
117       final ParameterMapping parameterMapping = parameterMappings.get(i);
118       if (parameterMapping.getMode() == ParameterMode.OUT || parameterMapping.getMode() == ParameterMode.INOUT) {
119         if (ResultSet.class.equals(parameterMapping.getJavaType())) {
120           handleRefCursorOutputParameter((ResultSet) cs.getObject(i + 1), parameterMapping, metaParam);
121         } else {
122           final TypeHandler<?> typeHandler = parameterMapping.getTypeHandler();
123           metaParam.setValue(parameterMapping.getProperty(), typeHandler.getResult(cs, i + 1));
124         }
125       }
126     }
127   }
128 
129   private void handleRefCursorOutputParameter(ResultSet rs, ParameterMapping parameterMapping, MetaObject metaParam) throws SQLException {
130     try {
131       final String resultMapId = parameterMapping.getResultMapId();
132       final ResultMap resultMap = configuration.getResultMap(resultMapId);
133       final DefaultResultHandler resultHandler = new DefaultResultHandler(objectFactory);
134       final ResultSetWrapper rsw = new ResultSetWrapper(rs, configuration);
135       handleRowValues(rsw, resultMap, resultHandler, new RowBounds(), null);
136       metaParam.setValue(parameterMapping.getProperty(), resultHandler.getResultList());
137     } finally {
138       // issue #228 (close resultsets)
139       closeResultSet(rs);
140     }
141   }
142 
143   //
144   // HANDLE RESULT SETS
145   //
146   @Override
147   public List<Object> handleResultSets(Statement stmt) throws SQLException {
148     ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
149 
150     final List<Object> multipleResults = new ArrayList<Object>();
151 
152     int resultSetCount = 0;
153     ResultSetWrapper rsw = getFirstResultSet(stmt);
154 
155     List<ResultMap> resultMaps = mappedStatement.getResultMaps();
156     int resultMapCount = resultMaps.size();
157     validateResultMapsCount(rsw, resultMapCount);
158     while (rsw != null && resultMapCount > resultSetCount) {
159       ResultMap resultMap = resultMaps.get(resultSetCount);
160       handleResultSet(rsw, resultMap, multipleResults, null);
161       rsw = getNextResultSet(stmt);
162       cleanUpAfterHandlingResultSet();
163       resultSetCount++;
164     }
165 
166     String[] resultSets = mappedStatement.getResulSets();
167     if (resultSets != null) {
168       while (rsw != null && resultSetCount < resultSets.length) {
169         ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
170         if (parentMapping != null) {
171           String nestedResultMapId = parentMapping.getNestedResultMapId();
172           ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
173           handleResultSet(rsw, resultMap, null, parentMapping);
174         }
175         rsw = getNextResultSet(stmt);
176         cleanUpAfterHandlingResultSet();
177         resultSetCount++;
178       }
179     }
180 
181     return collapseSingleResultList(multipleResults);
182   }
183 
184   private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
185     ResultSet rs = stmt.getResultSet();
186     while (rs == null) {
187       // move forward to get the first resultset in case the driver
188       // doesn't return the resultset as the first result (HSQLDB 2.1)
189       if (stmt.getMoreResults()) {
190         rs = stmt.getResultSet();
191       } else {
192         if (stmt.getUpdateCount() == -1) {
193           // no more results. Must be no resultset
194           break;
195         }
196       }
197     }
198     return rs != null ? new ResultSetWrapper(rs, configuration) : null;
199   }
200 
201   private ResultSetWrapper getNextResultSet(Statement stmt) throws SQLException {
202     // Making this method tolerant of bad JDBC drivers
203     try {
204       if (stmt.getConnection().getMetaData().supportsMultipleResultSets()) {
205         // Crazy Standard JDBC way of determining if there are more results
206         if (!((!stmt.getMoreResults()) && (stmt.getUpdateCount() == -1))) {
207           ResultSet rs = stmt.getResultSet();
208           return rs != null ? new ResultSetWrapper(rs, configuration) : null;
209         }
210       }
211     } catch (Exception e) {
212       // Intentionally ignored.
213     }
214     return null;
215   }
216 
217   private void closeResultSet(ResultSet rs) {
218     try {
219       if (rs != null) {
220         rs.close();
221       }
222     } catch (SQLException e) {
223       // ignore
224     }
225   }
226 
227   private void cleanUpAfterHandlingResultSet() {
228     nestedResultObjects.clear();
229     ancestorColumnPrefix.clear();
230   }
231 
232   private void validateResultMapsCount(ResultSetWrapper rsw, int resultMapCount) {
233     if (rsw != null && resultMapCount < 1) {
234       throw new ExecutorException("A query was run and no Result Maps were found for the Mapped Statement '" + mappedStatement.getId()
235           + "'.  It's likely that neither a Result Type nor a Result Map was specified.");
236     }
237   }
238 
239   private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
240     try {
241       if (parentMapping != null) {
242         handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
243       } else {
244         if (resultHandler == null) {
245           DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
246           handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
247           multipleResults.add(defaultResultHandler.getResultList());
248         } else {
249           handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
250         }
251       }
252     } finally {
253       // issue #228 (close resultsets)
254       closeResultSet(rsw.getResultSet());
255     }
256   }
257 
258   @SuppressWarnings("unchecked")
259   private List<Object> collapseSingleResultList(List<Object> multipleResults) {
260     return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
261   }
262 
263   //
264   // HANDLE ROWS FOR SIMPLE RESULTMAP
265   //
266 
267   private void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
268     if (resultMap.hasNestedResultMaps()) {
269       ensureNoRowBounds();
270       checkResultHandler();
271       handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
272     } else {
273       handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
274     }
275   }
276 
277   private void ensureNoRowBounds() {
278     if (configuration.isSafeRowBoundsEnabled() && rowBounds != null && (rowBounds.getLimit() < RowBounds.NO_ROW_LIMIT || rowBounds.getOffset() > RowBounds.NO_ROW_OFFSET)) {
279       throw new ExecutorException("Mapped Statements with nested result mappings cannot be safely constrained by RowBounds. "
280           + "Use safeRowBoundsEnabled=false setting to bypass this check.");
281     }
282   }
283 
284   protected void checkResultHandler() {
285     if (resultHandler != null && configuration.isSafeResultHandlerEnabled() && !mappedStatement.isResultOrdered()) {
286       throw new ExecutorException("Mapped Statements with nested result mappings cannot be safely used with a custom ResultHandler. "
287           + "Use safeResultHandlerEnabled=false setting to bypass this check "
288           + "or ensure your statement returns ordered data and set resultOrdered=true on it.");
289     }
290   }
291 
292   private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
293       throws SQLException {
294     DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
295     skipRows(rsw.getResultSet(), rowBounds);
296     while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
297       ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
298       Object rowValue = getRowValue(rsw, discriminatedResultMap);
299       storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
300     }
301   }
302 
303   private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
304     if (parentMapping != null) {
305       linkToParents(rs, parentMapping, rowValue);
306     } else {
307       callResultHandler(resultHandler, resultContext, rowValue);
308     }
309   }
310 
311   @SuppressWarnings("unchecked" /* because ResultHandler<?> is always ResultHandler<Object>*/)
312   private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
313     resultContext.nextResultObject(rowValue);
314     ((ResultHandler<Object>)resultHandler).handleResult(resultContext);
315   }
316 
317   private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) throws SQLException {
318     return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
319   }
320 
321   private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
322     if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
323       if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
324         rs.absolute(rowBounds.getOffset());
325       }
326     } else {
327       for (int i = 0; i < rowBounds.getOffset(); i++) {
328         rs.next();
329       }
330     }
331   }
332 
333   //
334   // GET VALUE FROM ROW FOR SIMPLE RESULT MAP
335   //
336 
337   private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
338     final ResultLoaderMap lazyLoader = new ResultLoaderMap();
339     Object resultObject = createResultObject(rsw, resultMap, lazyLoader, null);
340     if (resultObject != null && !typeHandlerRegistry.hasTypeHandler(resultMap.getType())) {
341       final MetaObject metaObject = configuration.newMetaObject(resultObject);
342       boolean foundValues = !resultMap.getConstructorResultMappings().isEmpty();
343       if (shouldApplyAutomaticMappings(resultMap, false)) {
344         foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
345       }
346       foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
347       foundValues = lazyLoader.size() > 0 || foundValues;
348       resultObject = foundValues ? resultObject : null;
349       return resultObject;
350     }
351     return resultObject;
352   }
353 
354   private boolean shouldApplyAutomaticMappings(ResultMap resultMap, boolean isNested) {
355     if (resultMap.getAutoMapping() != null) {
356       return resultMap.getAutoMapping();
357     } else {
358       if (isNested) {
359         return AutoMappingBehavior.FULL == configuration.getAutoMappingBehavior();
360       } else {
361         return AutoMappingBehavior.NONE != configuration.getAutoMappingBehavior();
362       }
363     }
364   }
365 
366   //
367   // PROPERTY MAPPINGS
368   //
369 
370   private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
371       throws SQLException {
372     final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
373     boolean foundValues = false;
374     final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
375     for (ResultMapping propertyMapping : propertyMappings) {
376       String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
377       if (propertyMapping.getNestedResultMapId() != null) {
378         // the user added a column attribute to a nested result map, ignore it
379         column = null;
380       }
381       if (propertyMapping.isCompositeResult()
382           || (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
383           || propertyMapping.getResultSet() != null) {
384         Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
385         // issue #541 make property optional
386         final String property = propertyMapping.getProperty();
387         // issue #377, call setter on nulls
388         if (value != DEFERED
389             && property != null
390             && (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive()))) {
391           metaObject.setValue(property, value);
392         }
393         if (value != null || value == DEFERED) {
394           foundValues = true;
395         }
396       }
397     }
398     return foundValues;
399   }
400 
401   private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
402       throws SQLException {
403     if (propertyMapping.getNestedQueryId() != null) {
404       return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
405     } else if (propertyMapping.getResultSet() != null) {
406       addPendingChildRelation(rs, metaResultObject, propertyMapping);   // TODO is that OK?
407       return DEFERED;
408     } else {
409       final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
410       final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
411       return typeHandler.getResult(rs, column);
412     }
413   }
414 
415   private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
416     final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
417     boolean foundValues = false;
418     for (String columnName : unmappedColumnNames) {
419       String propertyName = columnName;
420       if (columnPrefix != null && !columnPrefix.isEmpty()) {
421         // When columnPrefix is specified,
422         // ignore columns without the prefix.
423         if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
424           propertyName = columnName.substring(columnPrefix.length());
425         } else {
426           continue;
427         }
428       }
429       final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
430       if (property != null && metaObject.hasSetter(property)) {
431         final Class<?> propertyType = metaObject.getSetterType(property);
432         if (typeHandlerRegistry.hasTypeHandler(propertyType)) {
433           final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName);
434           final Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
435           // issue #377, call setter on nulls
436           if (value != null || configuration.isCallSettersOnNulls()) {
437             if (value != null || !propertyType.isPrimitive()) {
438               metaObject.setValue(property, value);
439             }
440             foundValues = true;
441           }
442         }
443       }
444     }
445     return foundValues;
446   }
447 
448   // MULTIPLE RESULT SETS
449 
450   private void linkToParents(ResultSet rs, ResultMapping parentMapping, Object rowValue) throws SQLException {
451     CacheKey parentKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(), parentMapping.getForeignColumn());
452     List<PendingRelation> parents = pendingRelations.get(parentKey);
453     if (parents != null) {
454       for (PendingRelation parent : parents) {
455         if (parent != null && rowValue != null) {
456             linkObjects(parent.metaObject, parent.propertyMapping, rowValue);
457         }
458       }
459     }
460   }
461 
462   private void addPendingChildRelation(ResultSet rs, MetaObject metaResultObject, ResultMapping parentMapping) throws SQLException {
463     CacheKey cacheKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(), parentMapping.getColumn());
464     PendingRelation deferLoad = new PendingRelation();
465     deferLoad.metaObject = metaResultObject;
466     deferLoad.propertyMapping = parentMapping;
467     List<PendingRelation> relations = pendingRelations.get(cacheKey);
468     // issue #255
469     if (relations == null) {
470       relations = new ArrayList<DefaultResultSetHandler.PendingRelation>();
471       pendingRelations.put(cacheKey, relations);
472     }
473     relations.add(deferLoad);
474     ResultMapping previous = nextResultMaps.get(parentMapping.getResultSet());
475     if (previous == null) {
476       nextResultMaps.put(parentMapping.getResultSet(), parentMapping);
477     } else {
478       if (!previous.equals(parentMapping)) {
479         throw new ExecutorException("Two different properties are mapped to the same resultSet");
480       }
481     }
482   }
483 
484   private CacheKey createKeyForMultipleResults(ResultSet rs, ResultMapping resultMapping, String names, String columns) throws SQLException {
485     CacheKey cacheKey = new CacheKey();
486     cacheKey.update(resultMapping);
487     if (columns != null && names != null) {
488       String[] columnsArray = columns.split(",");
489       String[] namesArray = names.split(",");
490       for (int i = 0 ; i < columnsArray.length ; i++) {
491         Object value = rs.getString(columnsArray[i]);
492         if (value != null) {
493           cacheKey.update(namesArray[i]);
494           cacheKey.update(value);
495         }
496       }
497     }
498     return cacheKey;
499   }
500 
501   //
502   // INSTANTIATION & CONSTRUCTOR MAPPING
503   //
504 
505   private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
506     final List<Class<?>> constructorArgTypes = new ArrayList<Class<?>>();
507     final List<Object> constructorArgs = new ArrayList<Object>();
508     final Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
509     if (resultObject != null && !typeHandlerRegistry.hasTypeHandler(resultMap.getType())) {
510       final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
511       for (ResultMapping propertyMapping : propertyMappings) {
512         // issue gcode #109 && issue #149
513         if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
514           return configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
515         }
516       }
517     }
518     return resultObject;
519   }
520 
521   private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
522       throws SQLException {
523     final Class<?> resultType = resultMap.getType();
524     final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
525     final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
526     if (typeHandlerRegistry.hasTypeHandler(resultType)) {
527       return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
528     } else if (!constructorMappings.isEmpty()) {
529       return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
530     } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
531       return objectFactory.create(resultType);
532     } else if (shouldApplyAutomaticMappings(resultMap, false)) {
533       return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);
534     }
535     throw new ExecutorException("Do not know how to create an instance of " + resultType);
536   }
537 
538   Object createParameterizedResultObject(ResultSetWrapper rsw, Class<?> resultType, List<ResultMapping> constructorMappings,
539       List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix) {
540     boolean foundValues = false;
541     for (ResultMapping constructorMapping : constructorMappings) {
542       final Class<?> parameterType = constructorMapping.getJavaType();
543       final String column = constructorMapping.getColumn();
544       final Object value;
545       try {
546         if (constructorMapping.getNestedQueryId() != null) {
547           value = getNestedQueryConstructorValue(rsw.getResultSet(), constructorMapping, columnPrefix);
548         } else if (constructorMapping.getNestedResultMapId() != null) {
549           final ResultMap resultMap = configuration.getResultMap(constructorMapping.getNestedResultMapId());
550           value = getRowValue(rsw, resultMap);
551         } else {
552           final TypeHandler<?> typeHandler = constructorMapping.getTypeHandler();
553           value = typeHandler.getResult(rsw.getResultSet(), prependPrefix(column, columnPrefix));
554         }
555       } catch (ResultMapException e) {
556         throw new ExecutorException("Could not process result for mapping: " + constructorMapping, e);
557       } catch (SQLException e) {
558         throw new ExecutorException("Could not process result for mapping: " + constructorMapping, e);
559       }
560       constructorArgTypes.add(parameterType);
561       constructorArgs.add(value);
562       foundValues = value != null || foundValues;
563     }
564     return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
565   }
566 
567   private Object createByConstructorSignature(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs,
568       String columnPrefix) throws SQLException {
569     for (Constructor<?> constructor : resultType.getDeclaredConstructors()) {
570       if (typeNames(constructor.getParameterTypes()).equals(rsw.getClassNames())) {
571         boolean foundValues = false;
572         for (int i = 0; i < constructor.getParameterTypes().length; i++) {
573           Class<?> parameterType = constructor.getParameterTypes()[i];
574           String columnName = rsw.getColumnNames().get(i);
575           TypeHandler<?> typeHandler = rsw.getTypeHandler(parameterType, columnName);
576           Object value = typeHandler.getResult(rsw.getResultSet(), prependPrefix(columnName, columnPrefix));
577           constructorArgTypes.add(parameterType);
578           constructorArgs.add(value);
579           foundValues = value != null || foundValues;
580         }
581         return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
582       }
583     }
584     throw new ExecutorException("No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames());
585   }
586 
587   private List<String> typeNames(Class<?>[] parameterTypes) {
588     List<String> names = new ArrayList<String>();
589     for (Class<?> type : parameterTypes) {
590       names.add(type.getName());
591     }
592     return names;
593   }
594 
595   private Object createPrimitiveResultObject(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
596     final Class<?> resultType = resultMap.getType();
597     final String columnName;
598     if (!resultMap.getResultMappings().isEmpty()) {
599       final List<ResultMapping> resultMappingList = resultMap.getResultMappings();
600       final ResultMapping mapping = resultMappingList.get(0);
601       columnName = prependPrefix(mapping.getColumn(), columnPrefix);
602     } else {
603       columnName = rsw.getColumnNames().get(0);
604     }
605     final TypeHandler<?> typeHandler = rsw.getTypeHandler(resultType, columnName);
606     return typeHandler.getResult(rsw.getResultSet(), columnName);
607   }
608 
609   //
610   // NESTED QUERY
611   //
612 
613   private Object getNestedQueryConstructorValue(ResultSet rs, ResultMapping constructorMapping, String columnPrefix) throws SQLException {
614     final String nestedQueryId = constructorMapping.getNestedQueryId();
615     final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
616     final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
617     final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, constructorMapping, nestedQueryParameterType, columnPrefix);
618     Object value = null;
619     if (nestedQueryParameterObject != null) {
620       final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
621       final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
622       final Class<?> targetType = constructorMapping.getJavaType();
623       final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
624       value = resultLoader.loadResult();
625     }
626     return value;
627   }
628 
629   private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
630       throws SQLException {
631     final String nestedQueryId = propertyMapping.getNestedQueryId();
632     final String property = propertyMapping.getProperty();
633     final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
634     final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
635     final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
636     Object value = null;
637     if (nestedQueryParameterObject != null) {
638       final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
639       final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
640       final Class<?> targetType = propertyMapping.getJavaType();
641       if (executor.isCached(nestedQuery, key)) {
642         executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
643         value = DEFERED;
644       } else {
645         final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
646         if (propertyMapping.isLazy()) {
647           lazyLoader.addLoader(property, metaResultObject, resultLoader);
648           value = DEFERED;
649         } else {
650           value = resultLoader.loadResult();
651         }
652       }
653     }
654     return value;
655   }
656 
657   private Object prepareParameterForNestedQuery(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
658     if (resultMapping.isCompositeResult()) {
659       return prepareCompositeKeyParameter(rs, resultMapping, parameterType, columnPrefix);
660     } else {
661       return prepareSimpleKeyParameter(rs, resultMapping, parameterType, columnPrefix);
662     }
663   }
664 
665   private Object prepareSimpleKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
666     final TypeHandler<?> typeHandler;
667     if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
668       typeHandler = typeHandlerRegistry.getTypeHandler(parameterType);
669     } else {
670       typeHandler = typeHandlerRegistry.getUnknownTypeHandler();
671     }
672     return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
673   }
674 
675   private Object prepareCompositeKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
676     final Object parameterObject = instantiateParameterObject(parameterType);
677     final MetaObject metaObject = configuration.newMetaObject(parameterObject);
678     boolean foundValues = false;
679     for (ResultMapping innerResultMapping : resultMapping.getComposites()) {
680       final Class<?> propType = metaObject.getSetterType(innerResultMapping.getProperty());
681       final TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(propType);
682       final Object propValue = typeHandler.getResult(rs, prependPrefix(innerResultMapping.getColumn(), columnPrefix));
683       // issue #353 & #560 do not execute nested query if key is null
684       if (propValue != null) {
685         metaObject.setValue(innerResultMapping.getProperty(), propValue);
686         foundValues = true;
687       }
688     }
689     return foundValues ? parameterObject : null;
690   }
691 
692   private Object instantiateParameterObject(Class<?> parameterType) {
693     if (parameterType == null) {
694       return new HashMap<Object, Object>();
695     } else {
696       return objectFactory.create(parameterType);
697     }
698   }
699 
700   //
701   // DISCRIMINATOR
702   //
703 
704   public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException {
705     Set<String> pastDiscriminators = new HashSet<String>();
706     Discriminator discriminator = resultMap.getDiscriminator();
707     while (discriminator != null) {
708       final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
709       final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
710       if (configuration.hasResultMap(discriminatedMapId)) {
711         resultMap = configuration.getResultMap(discriminatedMapId);
712         Discriminator lastDiscriminator = discriminator;
713         discriminator = resultMap.getDiscriminator();
714         if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
715           break;
716         }
717       } else {
718         break;
719       }
720     }
721     return resultMap;
722   }
723 
724   private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix) throws SQLException {
725     final ResultMapping resultMapping = discriminator.getResultMapping();
726     final TypeHandler<?> typeHandler = resultMapping.getTypeHandler();
727     return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
728   }
729 
730   private String prependPrefix(String columnName, String prefix) {
731     if (columnName == null || columnName.length() == 0 || prefix == null || prefix.length() == 0) {
732       return columnName;
733     }
734     return prefix + columnName;
735   }
736 
737   //
738   // HANDLE NESTED RESULT MAPS
739   //
740 
741   private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
742     final DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
743     skipRows(rsw.getResultSet(), rowBounds);
744     Object rowValue = null;
745     while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
746       final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
747       final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
748       Object partialObject = nestedResultObjects.get(rowKey);
749       // issue #577 && #542
750       if (mappedStatement.isResultOrdered()) {
751         if (partialObject == null && rowValue != null) {
752           nestedResultObjects.clear();
753           storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
754         }
755         rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, rowKey, null, partialObject);
756       } else {
757         rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, rowKey, null, partialObject);
758         if (partialObject == null) {
759           storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
760         }
761       }
762     }
763     if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
764       storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
765     }
766   }
767 
768   //
769   // GET VALUE FROM ROW FOR NESTED RESULT MAP
770   //
771 
772   private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, CacheKey absoluteKey, String columnPrefix, Object partialObject) throws SQLException {
773     final String resultMapId = resultMap.getId();
774     Object resultObject = partialObject;
775     if (resultObject != null) {
776       final MetaObject metaObject = configuration.newMetaObject(resultObject);
777       putAncestor(absoluteKey, resultObject, resultMapId, columnPrefix);
778       applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
779       ancestorObjects.remove(absoluteKey);
780     } else {
781       final ResultLoaderMap lazyLoader = new ResultLoaderMap();
782       resultObject = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
783       if (resultObject != null && !typeHandlerRegistry.hasTypeHandler(resultMap.getType())) {
784         final MetaObject metaObject = configuration.newMetaObject(resultObject);
785         boolean foundValues = !resultMap.getConstructorResultMappings().isEmpty();
786         if (shouldApplyAutomaticMappings(resultMap, true)) {
787           foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
788         }
789         foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
790         putAncestor(absoluteKey, resultObject, resultMapId, columnPrefix);
791         foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;
792         ancestorObjects.remove(absoluteKey);
793         foundValues = lazyLoader.size() > 0 || foundValues;
794         resultObject = foundValues ? resultObject : null;
795       }
796       if (combinedKey != CacheKey.NULL_CACHE_KEY) {
797         nestedResultObjects.put(combinedKey, resultObject);
798       }
799     }
800     return resultObject;
801   }
802 
803   private void putAncestor(CacheKey rowKey, Object resultObject, String resultMapId, String columnPrefix) {
804     if (!ancestorColumnPrefix.containsKey(resultMapId)) {
805       ancestorColumnPrefix.put(resultMapId, columnPrefix);
806     }
807     ancestorObjects.put(rowKey, resultObject);
808   }
809 
810   //
811   // NESTED RESULT MAP (JOIN MAPPING)
812   //
813 
814   private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject) {
815     boolean foundValues = false;
816     for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
817       final String nestedResultMapId = resultMapping.getNestedResultMapId();
818       if (nestedResultMapId != null && resultMapping.getResultSet() == null) {
819         try {
820           final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
821           final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
822           CacheKey rowKey = null;
823           Object ancestorObject = null;
824           if (ancestorColumnPrefix.containsKey(nestedResultMapId)) {
825             rowKey = createRowKey(nestedResultMap, rsw, ancestorColumnPrefix.get(nestedResultMapId));
826             ancestorObject = ancestorObjects.get(rowKey);
827           }
828           if (ancestorObject != null) {
829             if (newObject) {
830               linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
831             }
832           } else {
833             rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
834             final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
835             Object rowValue = nestedResultObjects.get(combinedKey);
836             boolean knownValue = (rowValue != null);
837             instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory            
838             if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw.getResultSet())) {
839               rowValue = getRowValue(rsw, nestedResultMap, combinedKey, rowKey, columnPrefix, rowValue);
840               if (rowValue != null && !knownValue) {
841                 linkObjects(metaObject, resultMapping, rowValue);
842                 foundValues = true;
843               }
844             }
845           }
846         } catch (SQLException e) {
847           throw new ExecutorException("Error getting nested result map values for '" + resultMapping.getProperty() + "'.  Cause: " + e, e);
848         }
849       }
850     }
851     return foundValues;
852   }
853 
854   private String getColumnPrefix(String parentPrefix, ResultMapping resultMapping) {
855     final StringBuilder columnPrefixBuilder = new StringBuilder();
856     if (parentPrefix != null) {
857       columnPrefixBuilder.append(parentPrefix);
858     }
859     if (resultMapping.getColumnPrefix() != null) {
860       columnPrefixBuilder.append(resultMapping.getColumnPrefix());
861     }
862     return columnPrefixBuilder.length() == 0 ? null : columnPrefixBuilder.toString().toUpperCase(Locale.ENGLISH);
863   }
864 
865   private boolean anyNotNullColumnHasValue(ResultMapping resultMapping, String columnPrefix, ResultSet rs) throws SQLException {
866     Set<String> notNullColumns = resultMapping.getNotNullColumns();
867     boolean anyNotNullColumnHasValue = true;
868     if (notNullColumns != null && !notNullColumns.isEmpty()) {
869       anyNotNullColumnHasValue = false;
870       for (String column: notNullColumns) {
871         rs.getObject(prependPrefix(column, columnPrefix));
872         if (!rs.wasNull()) {
873           anyNotNullColumnHasValue = true;
874           break;
875         }
876       }
877     }
878     return anyNotNullColumnHasValue;
879   }
880 
881   private ResultMap getNestedResultMap(ResultSet rs, String nestedResultMapId, String columnPrefix) throws SQLException {
882     ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId);
883     return resolveDiscriminatedResultMap(rs, nestedResultMap, columnPrefix);
884   }
885 
886   //
887   // UNIQUE RESULT KEY
888   //
889 
890   private CacheKey createRowKey(ResultMap resultMap, ResultSetWrapper rsw, String columnPrefix) throws SQLException {
891     final CacheKey cacheKey = new CacheKey();
892     cacheKey.update(resultMap.getId());
893     List<ResultMapping> resultMappings = getResultMappingsForRowKey(resultMap);
894     if (resultMappings.size() == 0) {
895       if (Map.class.isAssignableFrom(resultMap.getType())) {
896         createRowKeyForMap(rsw, cacheKey);
897       } else {
898         createRowKeyForUnmappedProperties(resultMap, rsw, cacheKey, columnPrefix);
899       }
900     } else {
901       createRowKeyForMappedProperties(resultMap, rsw, cacheKey, resultMappings, columnPrefix);
902     }
903     return cacheKey;
904   }
905 
906   private CacheKey combineKeys(CacheKey rowKey, CacheKey parentRowKey) {
907     if (rowKey.getUpdateCount() > 1 && parentRowKey.getUpdateCount() > 1) {
908       CacheKey combinedKey;
909       try {
910         combinedKey = rowKey.clone();
911       } catch (CloneNotSupportedException e) {
912         throw new ExecutorException("Error cloning cache key.  Cause: " + e, e);
913       }
914       combinedKey.update(parentRowKey);
915       return combinedKey;
916     }
917     return CacheKey.NULL_CACHE_KEY;
918   }
919 
920   private List<ResultMapping> getResultMappingsForRowKey(ResultMap resultMap) {
921     List<ResultMapping> resultMappings = resultMap.getIdResultMappings();
922     if (resultMappings.size() == 0) {
923       resultMappings = resultMap.getPropertyResultMappings();
924     }
925     return resultMappings;
926   }
927 
928   private void createRowKeyForMappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey, List<ResultMapping> resultMappings, String columnPrefix) throws SQLException {
929     for (ResultMapping resultMapping : resultMappings) {
930       if (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null) {
931         // Issue #392
932         final ResultMap nestedResultMap = configuration.getResultMap(resultMapping.getNestedResultMapId());
933         createRowKeyForMappedProperties(nestedResultMap, rsw, cacheKey, nestedResultMap.getConstructorResultMappings(),
934             prependPrefix(resultMapping.getColumnPrefix(), columnPrefix));
935       } else if (resultMapping.getNestedQueryId() == null) {
936         final String column = prependPrefix(resultMapping.getColumn(), columnPrefix);
937         final TypeHandler<?> th = resultMapping.getTypeHandler();
938         List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
939         // Issue #114
940         if (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) {
941           final Object value = th.getResult(rsw.getResultSet(), column);
942           if (value != null) {
943             cacheKey.update(column);
944             cacheKey.update(value);
945           }
946         }
947       }
948     }
949   }
950 
951   private void createRowKeyForUnmappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey, String columnPrefix) throws SQLException {
952     final MetaClass metaType = MetaClass.forClass(resultMap.getType(), reflectorFactory);
953     List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
954     for (String column : unmappedColumnNames) {
955       String property = column;
956       if (columnPrefix != null && !columnPrefix.isEmpty()) {
957         // When columnPrefix is specified, ignore columns without the prefix.
958         if (column.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
959           property = column.substring(columnPrefix.length());
960         } else {
961           continue;
962         }
963       }
964       if (metaType.findProperty(property, configuration.isMapUnderscoreToCamelCase()) != null) {
965         String value = rsw.getResultSet().getString(column);
966         if (value != null) {
967           cacheKey.update(column);
968           cacheKey.update(value);
969         }
970       }
971     }
972   }
973 
974   private void createRowKeyForMap(ResultSetWrapper rsw, CacheKey cacheKey) throws SQLException {
975     List<String> columnNames = rsw.getColumnNames();
976     for (String columnName : columnNames) {
977       final String value = rsw.getResultSet().getString(columnName);
978       if (value != null) {
979         cacheKey.update(columnName);
980         cacheKey.update(value);
981       }
982     }
983   }
984 
985   private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
986     final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
987     if (collectionProperty != null) {
988       final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
989       targetMetaObject.add(rowValue);
990     } else {
991       metaObject.setValue(resultMapping.getProperty(), rowValue);
992     }
993   }
994 
995   private Object instantiateCollectionPropertyIfAppropriate(ResultMapping resultMapping, MetaObject metaObject) {
996     final String propertyName = resultMapping.getProperty();
997     Object propertyValue = metaObject.getValue(propertyName);
998     if (propertyValue == null) {
999       Class<?> type = resultMapping.getJavaType();
1000       if (type == null) {
1001         type = metaObject.getSetterType(propertyName);
1002       }
1003       try {
1004         if (objectFactory.isCollection(type)) {
1005           propertyValue = objectFactory.create(type);
1006           metaObject.setValue(propertyName, propertyValue);
1007           return propertyValue;
1008         }
1009       } catch (Exception e) {
1010         throw new ExecutorException("Error instantiating collection property for result '" + resultMapping.getProperty() + "'.  Cause: " + e, e);
1011       }
1012     } else if (objectFactory.isCollection(propertyValue.getClass())) {
1013       return propertyValue;
1014     }
1015     return null;
1016   }
1017 
1018 }