001/*
002 *  Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.mybatisflex.core.mybatis;
017
018import com.mybatisflex.core.util.StringUtil;
019import org.apache.ibatis.annotations.AutomapConstructor;
020import org.apache.ibatis.annotations.Param;
021import org.apache.ibatis.binding.MapperMethod.ParamMap;
022import org.apache.ibatis.cache.CacheKey;
023import org.apache.ibatis.cursor.Cursor;
024import org.apache.ibatis.cursor.defaults.DefaultCursor;
025import org.apache.ibatis.executor.ErrorContext;
026import org.apache.ibatis.executor.Executor;
027import org.apache.ibatis.executor.ExecutorException;
028import org.apache.ibatis.executor.loader.ResultLoader;
029import org.apache.ibatis.executor.loader.ResultLoaderMap;
030import org.apache.ibatis.executor.parameter.ParameterHandler;
031import org.apache.ibatis.executor.result.DefaultResultContext;
032import org.apache.ibatis.executor.result.DefaultResultHandler;
033import org.apache.ibatis.executor.result.ResultMapException;
034import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
035import org.apache.ibatis.executor.resultset.ResultSetWrapper;
036import org.apache.ibatis.mapping.*;
037import org.apache.ibatis.reflection.MetaClass;
038import org.apache.ibatis.reflection.MetaObject;
039import org.apache.ibatis.reflection.ReflectorFactory;
040import org.apache.ibatis.reflection.factory.ObjectFactory;
041import org.apache.ibatis.session.*;
042import org.apache.ibatis.type.JdbcType;
043import org.apache.ibatis.type.TypeHandler;
044import org.apache.ibatis.type.TypeHandlerRegistry;
045import org.apache.ibatis.util.MapUtil;
046
047import java.lang.reflect.Constructor;
048import java.lang.reflect.Parameter;
049import java.sql.CallableStatement;
050import java.sql.ResultSet;
051import java.sql.SQLException;
052import java.sql.Statement;
053import java.text.MessageFormat;
054import java.util.*;
055
056/**
057 * 复制于 DefaultResultSetHandler,并开放若干方法,方便子类重写
058 * @author Clinton Begin
059 * @author Eduardo Macarron
060 * @author Iwao AVE!
061 * @author Kazuki Shimizu
062 * @author Michael
063 */
064public class FlexDefaultResultSetHandler extends DefaultResultSetHandler {
065
066    private static final Object DEFERRED = new Object();
067
068    private final Executor executor;
069    private final Configuration configuration;
070    private final MappedStatement mappedStatement;
071    private final RowBounds rowBounds;
072    private final ParameterHandler parameterHandler;
073    private final ResultHandler<?> resultHandler;
074    private final BoundSql boundSql;
075    private final TypeHandlerRegistry typeHandlerRegistry;
076    private final ObjectFactory objectFactory;
077    private final ReflectorFactory reflectorFactory;
078
079    // nested resultmaps
080    private final Map<CacheKey, Object> nestedResultObjects = new HashMap<>();
081    private final Map<String, Object> ancestorObjects = new HashMap<>();
082    private Object previousRowValue;
083
084    // multiple resultsets
085    private final Map<String, ResultMapping> nextResultMaps = new HashMap<>();
086    private final Map<CacheKey, List<PendingRelation>> pendingRelations = new HashMap<>();
087
088    // Cached Automappings
089    private final Map<String, List<UnMappedColumnAutoMapping>> autoMappingsCache = new HashMap<>();
090    private final Map<String, List<String>> constructorAutoMappingColumns = new HashMap<>();
091
092    // temporary marking flag that indicate using constructor mapping (use field to reduce memory usage)
093    private boolean useConstructorMappings;
094
095    private static class PendingRelation {
096        public MetaObject metaObject;
097        public ResultMapping propertyMapping;
098    }
099
100    private static class UnMappedColumnAutoMapping {
101        private final String column;
102        private final String property;
103        private final TypeHandler<?> typeHandler;
104        private final boolean primitive;
105
106        public UnMappedColumnAutoMapping(String column, String property, TypeHandler<?> typeHandler, boolean primitive) {
107            this.column = column;
108            this.property = property;
109            this.typeHandler = typeHandler;
110            this.primitive = primitive;
111        }
112    }
113
114    public FlexDefaultResultSetHandler(Executor executor, MappedStatement mappedStatement, ParameterHandler parameterHandler,
115                                       ResultHandler<?> resultHandler, BoundSql boundSql, RowBounds rowBounds) {
116        super(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
117        this.executor = executor;
118        this.configuration = mappedStatement.getConfiguration();
119        this.mappedStatement = mappedStatement;
120        this.rowBounds = rowBounds;
121        this.parameterHandler = parameterHandler;
122        this.boundSql = boundSql;
123        this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
124        this.objectFactory = configuration.getObjectFactory();
125        this.reflectorFactory = configuration.getReflectorFactory();
126        this.resultHandler = resultHandler;
127    }
128
129    //
130    // HANDLE OUTPUT PARAMETER
131    //
132
133    @Override
134    public void handleOutputParameters(CallableStatement cs) throws SQLException {
135        final Object parameterObject = parameterHandler.getParameterObject();
136        final MetaObject metaParam = configuration.newMetaObject(parameterObject);
137        final List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
138        for (int i = 0; i < parameterMappings.size(); i++) {
139            final ParameterMapping parameterMapping = parameterMappings.get(i);
140            if (parameterMapping.getMode() == ParameterMode.OUT || parameterMapping.getMode() == ParameterMode.INOUT) {
141                if (ResultSet.class.equals(parameterMapping.getJavaType())) {
142                    handleRefCursorOutputParameter((ResultSet) cs.getObject(i + 1), parameterMapping, metaParam);
143                } else {
144                    final TypeHandler<?> typeHandler = parameterMapping.getTypeHandler();
145                    metaParam.setValue(parameterMapping.getProperty(), typeHandler.getResult(cs, i + 1));
146                }
147            }
148        }
149    }
150
151    private void handleRefCursorOutputParameter(ResultSet rs, ParameterMapping parameterMapping, MetaObject metaParam)
152        throws SQLException {
153        if (rs == null) {
154            return;
155        }
156        try {
157            final String resultMapId = parameterMapping.getResultMapId();
158            final ResultMap resultMap = configuration.getResultMap(resultMapId);
159            final ResultSetWrapper rsw = new ResultSetWrapper(rs, configuration);
160            if (this.resultHandler == null) {
161                final DefaultResultHandler resultHandler = new DefaultResultHandler(objectFactory);
162                handleRowValues(rsw, resultMap, resultHandler, new RowBounds(), null);
163                metaParam.setValue(parameterMapping.getProperty(), resultHandler.getResultList());
164            } else {
165                handleRowValues(rsw, resultMap, resultHandler, new RowBounds(), null);
166            }
167        } finally {
168            // issue #228 (close resultsets)
169            closeResultSet(rs);
170        }
171    }
172
173    //
174    // HANDLE RESULT SETS
175    //
176    @Override
177    public List<Object> handleResultSets(Statement stmt) throws SQLException {
178        ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
179
180        final List<Object> multipleResults = new ArrayList<>();
181
182        int resultSetCount = 0;
183        ResultSetWrapper rsw = getFirstResultSet(stmt);
184
185        List<ResultMap> resultMaps = mappedStatement.getResultMaps();
186        int resultMapCount = resultMaps.size();
187        validateResultMapsCount(rsw, resultMapCount);
188        while (rsw != null && resultMapCount > resultSetCount) {
189            ResultMap resultMap = resultMaps.get(resultSetCount);
190            handleResultSet(rsw, resultMap, multipleResults, null);
191            rsw = getNextResultSet(stmt);
192            cleanUpAfterHandlingResultSet();
193            resultSetCount++;
194        }
195
196        String[] resultSets = mappedStatement.getResultSets();
197        if (resultSets != null) {
198            while (rsw != null && resultSetCount < resultSets.length) {
199                ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
200                if (parentMapping != null) {
201                    String nestedResultMapId = parentMapping.getNestedResultMapId();
202                    ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
203                    handleResultSet(rsw, resultMap, null, parentMapping);
204                }
205                rsw = getNextResultSet(stmt);
206                cleanUpAfterHandlingResultSet();
207                resultSetCount++;
208            }
209        }
210
211        return collapseSingleResultList(multipleResults);
212    }
213
214    @Override
215    public <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException {
216        ErrorContext.instance().activity("handling cursor results").object(mappedStatement.getId());
217
218        ResultSetWrapper rsw = getFirstResultSet(stmt);
219
220        List<ResultMap> resultMaps = mappedStatement.getResultMaps();
221
222        int resultMapCount = resultMaps.size();
223        validateResultMapsCount(rsw, resultMapCount);
224        if (resultMapCount != 1) {
225            throw new ExecutorException("Cursor results cannot be mapped to multiple resultMaps");
226        }
227
228        ResultMap resultMap = resultMaps.get(0);
229        return new DefaultCursor<>(this, resultMap, rsw, rowBounds);
230    }
231
232    private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
233        ResultSet rs = stmt.getResultSet();
234        while (rs == null) {
235            // move forward to get the first resultset in case the driver
236            // doesn't return the resultset as the first result (HSQLDB 2.1)
237            if (stmt.getMoreResults()) {
238                rs = stmt.getResultSet();
239            } else if (stmt.getUpdateCount() == -1) {
240                // no more results. Must be no resultset
241                break;
242            }
243        }
244        return rs != null ? new ResultSetWrapper(rs, configuration) : null;
245    }
246
247    private ResultSetWrapper getNextResultSet(Statement stmt) {
248        // Making this method tolerant of bad JDBC drivers
249        try {
250            // Crazy Standard JDBC way of determining if there are more results
251            if (stmt.getConnection().getMetaData().supportsMultipleResultSets()
252                && (stmt.getMoreResults() || (stmt.getUpdateCount() != -1))) {
253                ResultSet rs = stmt.getResultSet();
254                if (rs == null) {
255                    return getNextResultSet(stmt);
256                }
257                return new ResultSetWrapper(rs, configuration);
258            }
259        } catch (Exception e) {
260            // Intentionally ignored.
261        }
262        return null;
263    }
264
265    private void closeResultSet(ResultSet rs) {
266        try {
267            if (rs != null) {
268                rs.close();
269            }
270        } catch (SQLException e) {
271            // ignore
272        }
273    }
274
275    private void cleanUpAfterHandlingResultSet() {
276        nestedResultObjects.clear();
277    }
278
279    private void validateResultMapsCount(ResultSetWrapper rsw, int resultMapCount) {
280        if (rsw != null && resultMapCount < 1) {
281            throw new ExecutorException(
282                "A query was run and no Result Maps were found for the Mapped Statement '" + mappedStatement.getId()
283                    + "'. 'resultType' or 'resultMap' must be specified when there is no corresponding method.");
284        }
285    }
286
287    private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults,
288                                 ResultMapping parentMapping) throws SQLException {
289        try {
290            if (parentMapping != null) {
291                handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
292            } else if (resultHandler == null) {
293                DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
294                handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
295                multipleResults.add(defaultResultHandler.getResultList());
296            } else {
297                handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
298            }
299        } finally {
300            // issue #228 (close resultsets)
301            closeResultSet(rsw.getResultSet());
302        }
303    }
304
305    @SuppressWarnings("unchecked")
306    private List<Object> collapseSingleResultList(List<Object> multipleResults) {
307        return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
308    }
309
310    //
311    // HANDLE ROWS FOR SIMPLE RESULTMAP
312    //
313
314    @Override
315    public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler,
316                                RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
317        if (resultMap.hasNestedResultMaps()) {
318            ensureNoRowBounds();
319            checkResultHandler();
320            handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
321        } else {
322            handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
323        }
324    }
325
326    private void ensureNoRowBounds() {
327        if (configuration.isSafeRowBoundsEnabled() && rowBounds != null
328            && (rowBounds.getLimit() < RowBounds.NO_ROW_LIMIT || rowBounds.getOffset() > RowBounds.NO_ROW_OFFSET)) {
329            throw new ExecutorException(
330                "Mapped Statements with nested result mappings cannot be safely constrained by RowBounds. "
331                    + "Use safeRowBoundsEnabled=false setting to bypass this check.");
332        }
333    }
334
335    @Override
336    protected void checkResultHandler() {
337        if (resultHandler != null && configuration.isSafeResultHandlerEnabled() && !mappedStatement.isResultOrdered()) {
338            throw new ExecutorException(
339                "Mapped Statements with nested result mappings cannot be safely used with a custom ResultHandler. "
340                    + "Use safeResultHandlerEnabled=false setting to bypass this check "
341                    + "or ensure your statement returns ordered data and set resultOrdered=true on it.");
342        }
343    }
344
345    private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
346                                                   ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
347        DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
348        ResultSet resultSet = rsw.getResultSet();
349        skipRows(resultSet, rowBounds);
350        while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
351            ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
352            Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
353            storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
354        }
355    }
356
357    private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue,
358                             ResultMapping parentMapping, ResultSet rs) throws SQLException {
359        if (parentMapping != null) {
360            linkToParents(rs, parentMapping, rowValue);
361        } else {
362            callResultHandler(resultHandler, resultContext, rowValue);
363        }
364    }
365
366    @SuppressWarnings("unchecked" /* because ResultHandler<?> is always ResultHandler<Object> */)
367    private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext,
368                                   Object rowValue) {
369        resultContext.nextResultObject(rowValue);
370        ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
371    }
372
373    private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) {
374        return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
375    }
376
377    private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
378        if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
379            if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
380                rs.absolute(rowBounds.getOffset());
381            }
382        } else {
383            for (int i = 0; i < rowBounds.getOffset(); i++) {
384                if (!rs.next()) {
385                    break;
386                }
387            }
388        }
389    }
390
391    //
392    // GET VALUE FROM ROW FOR SIMPLE RESULT MAP
393    //
394
395    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
396        final ResultLoaderMap lazyLoader = new ResultLoaderMap();
397        Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
398        if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
399            final MetaObject metaObject = configuration.newMetaObject(rowValue);
400            boolean foundValues = this.useConstructorMappings;
401            if (shouldApplyAutomaticMappings(resultMap, false)) {
402                foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
403            }
404            foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
405            foundValues = lazyLoader.size() > 0 || foundValues;
406            rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
407        }
408        return rowValue;
409    }
410
411    //
412    // GET VALUE FROM ROW FOR NESTED RESULT MAP
413    //
414
415    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix,
416                               Object partialObject) throws SQLException {
417        final String resultMapId = resultMap.getId();
418        Object rowValue = partialObject;
419        if (rowValue != null) {
420            final MetaObject metaObject = configuration.newMetaObject(rowValue);
421            putAncestor(rowValue, resultMapId);
422            applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
423            ancestorObjects.remove(resultMapId);
424        } else {
425            final ResultLoaderMap lazyLoader = new ResultLoaderMap();
426            rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
427            if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
428                final MetaObject metaObject = configuration.newMetaObject(rowValue);
429                boolean foundValues = this.useConstructorMappings;
430                if (shouldApplyAutomaticMappings(resultMap, true)) {
431                    foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
432                }
433                foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
434                putAncestor(rowValue, resultMapId);
435                foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true)
436                    || foundValues;
437                ancestorObjects.remove(resultMapId);
438                foundValues = lazyLoader.size() > 0 || foundValues;
439                rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
440            }
441            if (combinedKey != CacheKey.NULL_CACHE_KEY) {
442                nestedResultObjects.put(combinedKey, rowValue);
443            }
444        }
445        return rowValue;
446    }
447
448    private void putAncestor(Object resultObject, String resultMapId) {
449        ancestorObjects.put(resultMapId, resultObject);
450    }
451
452    private boolean shouldApplyAutomaticMappings(ResultMap resultMap, boolean isNested) {
453        if (resultMap.getAutoMapping() != null) {
454            return resultMap.getAutoMapping();
455        }
456        if (isNested) {
457            return AutoMappingBehavior.FULL == configuration.getAutoMappingBehavior();
458        } else {
459            return AutoMappingBehavior.NONE != configuration.getAutoMappingBehavior();
460        }
461    }
462
463    //
464    // PROPERTY MAPPINGS
465    //
466
467    private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
468                                          ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
469        final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
470        boolean foundValues = false;
471        final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
472        for (ResultMapping propertyMapping : propertyMappings) {
473            String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
474            if (propertyMapping.getNestedResultMapId() != null) {
475                // the user added a column attribute to a nested result map, ignore it
476                column = null;
477            }
478            if (propertyMapping.isCompositeResult()
479                || column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))
480                || propertyMapping.getResultSet() != null) {
481                Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader,
482                    columnPrefix);
483                // issue #541 make property optional
484                final String property = propertyMapping.getProperty();
485                if (property == null) {
486                    continue;
487                }
488                if (value == DEFERRED) {
489                    foundValues = true;
490                    continue;
491                }
492                if (value != null) {
493                    foundValues = true;
494                }
495                if (value != null
496                    || configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive()) {
497                    // gcode issue #377, call setter on nulls (value is not 'found')
498                    metaObject.setValue(property, value);
499                }
500            }
501        }
502        return foundValues;
503    }
504
505    private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
506                                           ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
507        if (propertyMapping.getNestedQueryId() != null) {
508            return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
509        }
510        if (propertyMapping.getResultSet() != null) {
511            addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?
512            return DEFERRED;
513        } else {
514            final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
515            final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
516            return typeHandler.getResult(rs, column);
517        }
518    }
519
520    private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap,
521                                                                    MetaObject metaObject, String columnPrefix) throws SQLException {
522        final String mapKey = resultMap.getId() + ":" + columnPrefix;
523        List<UnMappedColumnAutoMapping> autoMapping = autoMappingsCache.get(mapKey);
524        if (autoMapping == null) {
525            autoMapping = new ArrayList<>();
526            final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
527            // Remove the entry to release the memory
528            List<String> mappedInConstructorAutoMapping = constructorAutoMappingColumns.remove(mapKey);
529            if (mappedInConstructorAutoMapping != null) {
530                unmappedColumnNames.removeAll(mappedInConstructorAutoMapping);
531            }
532            for (String columnName : unmappedColumnNames) {
533                String propertyName = columnName;
534                if (columnPrefix != null && !columnPrefix.isEmpty()) {
535                    // When columnPrefix is specified,
536                    // ignore columns without the prefix.
537                    if (!columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
538                        continue;
539                    }
540                    propertyName = columnName.substring(columnPrefix.length());
541                }
542                final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
543                if (property != null && metaObject.hasSetter(property)) {
544                    if (resultMap.getMappedProperties().contains(property)) {
545                        continue;
546                    }
547                    final Class<?> propertyType = metaObject.getSetterType(property);
548                    if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) {
549                        final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName);
550                        autoMapping
551                            .add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive()));
552                    } else {
553                        configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName, property,
554                            propertyType);
555                    }
556                } else {
557                    configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName,
558                        property != null ? property : propertyName, null);
559                }
560            }
561            autoMappingsCache.put(mapKey, autoMapping);
562        }
563        return autoMapping;
564    }
565
566    private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
567                                           String columnPrefix) throws SQLException {
568        List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
569        boolean foundValues = false;
570        if (!autoMapping.isEmpty()) {
571            for (UnMappedColumnAutoMapping mapping : autoMapping) {
572                final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
573                if (value != null) {
574                    foundValues = true;
575                }
576                if (value != null || configuration.isCallSettersOnNulls() && !mapping.primitive) {
577                    // gcode issue #377, call setter on nulls (value is not 'found')
578                    metaObject.setValue(mapping.property, value);
579                }
580            }
581        }
582        return foundValues;
583    }
584
585    // MULTIPLE RESULT SETS
586
587    private void linkToParents(ResultSet rs, ResultMapping parentMapping, Object rowValue) throws SQLException {
588        CacheKey parentKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(),
589            parentMapping.getForeignColumn());
590        List<PendingRelation> parents = pendingRelations.get(parentKey);
591        if (parents != null) {
592            for (PendingRelation parent : parents) {
593                if (parent != null && rowValue != null) {
594                    linkObjects(parent.metaObject, parent.propertyMapping, rowValue);
595                }
596            }
597        }
598    }
599
600    private void addPendingChildRelation(ResultSet rs, MetaObject metaResultObject, ResultMapping parentMapping)
601        throws SQLException {
602        CacheKey cacheKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(),
603            parentMapping.getColumn());
604        PendingRelation deferLoad = new PendingRelation();
605        deferLoad.metaObject = metaResultObject;
606        deferLoad.propertyMapping = parentMapping;
607        List<PendingRelation> relations = MapUtil.computeIfAbsent(pendingRelations, cacheKey, k -> new ArrayList<>());
608        // issue #255
609        relations.add(deferLoad);
610        ResultMapping previous = nextResultMaps.get(parentMapping.getResultSet());
611        if (previous == null) {
612            nextResultMaps.put(parentMapping.getResultSet(), parentMapping);
613        } else if (!previous.equals(parentMapping)) {
614            throw new ExecutorException("Two different properties are mapped to the same resultSet");
615        }
616    }
617
618    private CacheKey createKeyForMultipleResults(ResultSet rs, ResultMapping resultMapping, String names, String columns)
619        throws SQLException {
620        CacheKey cacheKey = new CacheKey();
621        cacheKey.update(resultMapping);
622        if (columns != null && names != null) {
623            String[] columnsArray = columns.split(",");
624            String[] namesArray = names.split(",");
625            for (int i = 0; i < columnsArray.length; i++) {
626                Object value = rs.getString(columnsArray[i]);
627                if (value != null) {
628                    cacheKey.update(namesArray[i]);
629                    cacheKey.update(value);
630                }
631            }
632        }
633        return cacheKey;
634    }
635
636    //
637    // INSTANTIATION & CONSTRUCTOR MAPPING
638    //
639
640    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader,
641                                      String columnPrefix) throws SQLException {
642        this.useConstructorMappings = false; // reset previous mapping result
643        final List<Class<?>> constructorArgTypes = new ArrayList<>();
644        final List<Object> constructorArgs = new ArrayList<>();
645        Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
646        if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
647            final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
648            for (ResultMapping propertyMapping : propertyMappings) {
649                // issue gcode #109 && issue #149
650                if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
651                    resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration,
652                        objectFactory, constructorArgTypes, constructorArgs);
653                    break;
654                }
655            }
656        }
657        this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
658        return resultObject;
659    }
660
661    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes,
662                                      List<Object> constructorArgs, String columnPrefix) throws SQLException {
663        final Class<?> resultType = resultMap.getType();
664        final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
665        final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
666        if (hasTypeHandlerForResultObject(rsw, resultType)) {
667            return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
668        }
669        if (!constructorMappings.isEmpty()) {
670            return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs,
671                columnPrefix);
672        } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
673            return objectFactory.create(resultType);
674        } else if (shouldApplyAutomaticMappings(resultMap, false)) {
675            return createByConstructorSignature(rsw, resultMap, columnPrefix, resultType, constructorArgTypes,
676                constructorArgs);
677        }
678        throw new ExecutorException("Do not know how to create an instance of " + resultType);
679    }
680
681    Object createParameterizedResultObject(ResultSetWrapper rsw, Class<?> resultType,
682                                           List<ResultMapping> constructorMappings, List<Class<?>> constructorArgTypes, List<Object> constructorArgs,
683                                           String columnPrefix) {
684        boolean foundValues = false;
685        for (ResultMapping constructorMapping : constructorMappings) {
686            final Class<?> parameterType = constructorMapping.getJavaType();
687            final String column = constructorMapping.getColumn();
688            final Object value;
689            try {
690                if (constructorMapping.getNestedQueryId() != null) {
691                    value = getNestedQueryConstructorValue(rsw.getResultSet(), constructorMapping, columnPrefix);
692                } else if (constructorMapping.getNestedResultMapId() != null) {
693                    final ResultMap resultMap = configuration.getResultMap(constructorMapping.getNestedResultMapId());
694                    value = getRowValue(rsw, resultMap, getColumnPrefix(columnPrefix, constructorMapping));
695                } else {
696                    final TypeHandler<?> typeHandler = constructorMapping.getTypeHandler();
697                    value = typeHandler.getResult(rsw.getResultSet(), prependPrefix(column, columnPrefix));
698                }
699            } catch (ResultMapException | SQLException e) {
700                throw new ExecutorException("Could not process result for mapping: " + constructorMapping, e);
701            }
702            constructorArgTypes.add(parameterType);
703            constructorArgs.add(value);
704            foundValues = value != null || foundValues;
705        }
706        return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
707    }
708
709    private Object createByConstructorSignature(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix,
710                                                Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws SQLException {
711        return applyConstructorAutomapping(rsw, resultMap, columnPrefix, resultType, constructorArgTypes, constructorArgs,
712            findConstructorForAutomapping(resultType, rsw).orElseThrow(() -> new ExecutorException(
713                "No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames())));
714    }
715
716    private Optional<Constructor<?>> findConstructorForAutomapping(final Class<?> resultType, ResultSetWrapper rsw) {
717        Constructor<?>[] constructors = resultType.getDeclaredConstructors();
718        if (constructors.length == 1) {
719            return Optional.of(constructors[0]);
720        }
721        Optional<Constructor<?>> annotated = Arrays.stream(constructors)
722            .filter(x -> x.isAnnotationPresent(AutomapConstructor.class)).reduce((x, y) -> {
723                throw new ExecutorException("@AutomapConstructor should be used in only one constructor.");
724            });
725        if (annotated.isPresent()) {
726            return annotated;
727        }
728        if (configuration.isArgNameBasedConstructorAutoMapping()) {
729            // Finding-best-match type implementation is possible,
730            // but using @AutomapConstructor seems sufficient.
731            throw new ExecutorException(MessageFormat.format(
732                "'argNameBasedConstructorAutoMapping' is enabled and the class ''{0}'' has multiple constructors, so @AutomapConstructor must be added to one of the constructors.",
733                resultType.getName()));
734        } else {
735            return Arrays.stream(constructors).filter(x -> findUsableConstructorByArgTypes(x, rsw.getJdbcTypes())).findAny();
736        }
737    }
738
739    private boolean findUsableConstructorByArgTypes(final Constructor<?> constructor, final List<JdbcType> jdbcTypes) {
740        final Class<?>[] parameterTypes = constructor.getParameterTypes();
741        if (parameterTypes.length != jdbcTypes.size()) {
742            return false;
743        }
744        for (int i = 0; i < parameterTypes.length; i++) {
745            if (!typeHandlerRegistry.hasTypeHandler(parameterTypes[i], jdbcTypes.get(i))) {
746                return false;
747            }
748        }
749        return true;
750    }
751
752    private Object applyConstructorAutomapping(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix,
753                                               Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor)
754        throws SQLException {
755        boolean foundValues = false;
756        if (configuration.isArgNameBasedConstructorAutoMapping()) {
757            foundValues = applyArgNameBasedConstructorAutoMapping(rsw, resultMap, columnPrefix, constructorArgTypes,
758                constructorArgs, constructor, foundValues);
759        } else {
760            foundValues = applyColumnOrderBasedConstructorAutomapping(rsw, constructorArgTypes, constructorArgs, constructor,
761                foundValues);
762        }
763        return foundValues || configuration.isReturnInstanceForEmptyRow()
764            ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
765    }
766
767    private boolean applyColumnOrderBasedConstructorAutomapping(ResultSetWrapper rsw, List<Class<?>> constructorArgTypes,
768                                                                List<Object> constructorArgs, Constructor<?> constructor, boolean foundValues) throws SQLException {
769        for (int i = 0; i < constructor.getParameterTypes().length; i++) {
770            Class<?> parameterType = constructor.getParameterTypes()[i];
771
772            // https://github.com/mybatis-flex/mybatis-flex/pull/201
773            // Parameter parameter = constructor.getParameters()[i];
774            // String columnName = StringUtil.camelToUnderline(parameter.getName());
775
776            String columnName = rsw.getColumnNames().get(i);
777            TypeHandler<?> typeHandler = rsw.getTypeHandler(parameterType, columnName);
778            Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
779            constructorArgTypes.add(parameterType);
780            constructorArgs.add(value);
781            foundValues = value != null || foundValues;
782        }
783        return foundValues;
784    }
785
786    private boolean applyArgNameBasedConstructorAutoMapping(ResultSetWrapper rsw, ResultMap resultMap,
787                                                            String columnPrefix, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor,
788                                                            boolean foundValues) throws SQLException {
789        List<String> missingArgs = null;
790        Parameter[] params = constructor.getParameters();
791        for (Parameter param : params) {
792            boolean columnNotFound = true;
793            Param paramAnno = param.getAnnotation(Param.class);
794            String paramName = paramAnno == null ? param.getName() : paramAnno.value();
795            for (String columnName : rsw.getColumnNames()) {
796                if (columnMatchesParam(columnName, paramName, columnPrefix)) {
797                    Class<?> paramType = param.getType();
798                    TypeHandler<?> typeHandler = rsw.getTypeHandler(paramType, columnName);
799                    Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
800                    constructorArgTypes.add(paramType);
801                    constructorArgs.add(value);
802                    final String mapKey = resultMap.getId() + ":" + columnPrefix;
803                    if (!autoMappingsCache.containsKey(mapKey)) {
804                        MapUtil.computeIfAbsent(constructorAutoMappingColumns, mapKey, k -> new ArrayList<>()).add(columnName);
805                    }
806                    columnNotFound = false;
807                    foundValues = value != null || foundValues;
808                }
809            }
810            if (columnNotFound) {
811                if (missingArgs == null) {
812                    missingArgs = new ArrayList<>();
813                }
814                missingArgs.add(paramName);
815            }
816        }
817        if (foundValues && constructorArgs.size() < params.length) {
818            throw new ExecutorException(MessageFormat.format(
819                "Constructor auto-mapping of ''{1}'' failed " + "because ''{0}'' were not found in the result set; "
820                    + "Available columns are ''{2}'' and mapUnderscoreToCamelCase is ''{3}''.",
821                missingArgs, constructor, rsw.getColumnNames(), configuration.isMapUnderscoreToCamelCase()));
822        }
823        return foundValues;
824    }
825
826    private boolean columnMatchesParam(String columnName, String paramName, String columnPrefix) {
827        if (columnPrefix != null) {
828            if (!columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
829                return false;
830            }
831            columnName = columnName.substring(columnPrefix.length());
832        }
833        return paramName
834            .equalsIgnoreCase(configuration.isMapUnderscoreToCamelCase() ? columnName.replace("_", "") : columnName);
835    }
836
837    protected Object createPrimitiveResultObject(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix)
838        throws SQLException {
839        final Class<?> resultType = resultMap.getType();
840        final String columnName;
841        if (!resultMap.getResultMappings().isEmpty()) {
842            final List<ResultMapping> resultMappingList = resultMap.getResultMappings();
843            final ResultMapping mapping = resultMappingList.get(0);
844            columnName = prependPrefix(mapping.getColumn(), columnPrefix);
845        } else {
846            columnName = rsw.getColumnNames().get(0);
847        }
848        final TypeHandler<?> typeHandler = rsw.getTypeHandler(resultType, columnName);
849        return typeHandler.getResult(rsw.getResultSet(), columnName);
850    }
851
852    //
853    // NESTED QUERY
854    //
855
856    private Object getNestedQueryConstructorValue(ResultSet rs, ResultMapping constructorMapping, String columnPrefix)
857        throws SQLException {
858        final String nestedQueryId = constructorMapping.getNestedQueryId();
859        final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
860        final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
861        final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, constructorMapping,
862            nestedQueryParameterType, columnPrefix);
863        Object value = null;
864        if (nestedQueryParameterObject != null) {
865            final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
866            final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT,
867                nestedBoundSql);
868            final Class<?> targetType = constructorMapping.getJavaType();
869            final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
870                nestedQueryParameterObject, targetType, key, nestedBoundSql);
871            value = resultLoader.loadResult();
872        }
873        return value;
874    }
875
876    private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
877                                              ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
878        final String nestedQueryId = propertyMapping.getNestedQueryId();
879        final String property = propertyMapping.getProperty();
880        final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
881        final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
882        final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping,
883            nestedQueryParameterType, columnPrefix);
884        Object value = null;
885        if (nestedQueryParameterObject != null) {
886            final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
887            final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT,
888                nestedBoundSql);
889            final Class<?> targetType = propertyMapping.getJavaType();
890            if (executor.isCached(nestedQuery, key)) {
891                executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
892                value = DEFERRED;
893            } else {
894                final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
895                    nestedQueryParameterObject, targetType, key, nestedBoundSql);
896                if (propertyMapping.isLazy()) {
897                    lazyLoader.addLoader(property, metaResultObject, resultLoader);
898                    value = DEFERRED;
899                } else {
900                    value = resultLoader.loadResult();
901                }
902            }
903        }
904        return value;
905    }
906
907    private Object prepareParameterForNestedQuery(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
908                                                  String columnPrefix) throws SQLException {
909        if (resultMapping.isCompositeResult()) {
910            return prepareCompositeKeyParameter(rs, resultMapping, parameterType, columnPrefix);
911        }
912        return prepareSimpleKeyParameter(rs, resultMapping, parameterType, columnPrefix);
913    }
914
915    private Object prepareSimpleKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
916                                             String columnPrefix) throws SQLException {
917        final TypeHandler<?> typeHandler;
918        if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
919            typeHandler = typeHandlerRegistry.getTypeHandler(parameterType);
920        } else {
921            typeHandler = typeHandlerRegistry.getUnknownTypeHandler();
922        }
923        return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
924    }
925
926    private Object prepareCompositeKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
927                                                String columnPrefix) throws SQLException {
928        final Object parameterObject = instantiateParameterObject(parameterType);
929        final MetaObject metaObject = configuration.newMetaObject(parameterObject);
930        boolean foundValues = false;
931        for (ResultMapping innerResultMapping : resultMapping.getComposites()) {
932            final Class<?> propType = metaObject.getSetterType(innerResultMapping.getProperty());
933            final TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(propType);
934            final Object propValue = typeHandler.getResult(rs, prependPrefix(innerResultMapping.getColumn(), columnPrefix));
935            // issue #353 & #560 do not execute nested query if key is null
936            if (propValue != null) {
937                metaObject.setValue(innerResultMapping.getProperty(), propValue);
938                foundValues = true;
939            }
940        }
941        return foundValues ? parameterObject : null;
942    }
943
944    private Object instantiateParameterObject(Class<?> parameterType) {
945        if (parameterType == null) {
946            return new HashMap<>();
947        }
948        if (ParamMap.class.equals(parameterType)) {
949            return new HashMap<>(); // issue #649
950        } else {
951            return objectFactory.create(parameterType);
952        }
953    }
954
955    //
956    // DISCRIMINATOR
957    //
958
959    @Override
960    public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix)
961        throws SQLException {
962        Set<String> pastDiscriminators = new HashSet<>();
963        Discriminator discriminator = resultMap.getDiscriminator();
964        while (discriminator != null) {
965            final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
966            final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
967            if (!configuration.hasResultMap(discriminatedMapId)) {
968                break;
969            }
970            resultMap = configuration.getResultMap(discriminatedMapId);
971            Discriminator lastDiscriminator = discriminator;
972            discriminator = resultMap.getDiscriminator();
973            if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
974                break;
975            }
976        }
977        return resultMap;
978    }
979
980    private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix)
981        throws SQLException {
982        final ResultMapping resultMapping = discriminator.getResultMapping();
983        final TypeHandler<?> typeHandler = resultMapping.getTypeHandler();
984        return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
985    }
986
987
988    protected String prependPrefix(String columnName, String prefix) {
989        if (columnName == null || columnName.length() == 0 || prefix == null || prefix.length() == 0) {
990            return columnName;
991        }
992        return prefix + columnName;
993    }
994
995    //
996    // HANDLE NESTED RESULT MAPS
997    //
998
999    private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap,
1000                                                   ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
1001        final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
1002        ResultSet resultSet = rsw.getResultSet();
1003        skipRows(resultSet, rowBounds);
1004        Object rowValue = previousRowValue;
1005        while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
1006            final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
1007            final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
1008            Object partialObject = nestedResultObjects.get(rowKey);
1009            // issue #577 && #542
1010            if (mappedStatement.isResultOrdered()) {
1011                if (partialObject == null && rowValue != null) {
1012                    nestedResultObjects.clear();
1013                    storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1014                }
1015                rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
1016            } else {
1017                rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
1018                if (partialObject == null) {
1019                    storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1020                }
1021            }
1022        }
1023        if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
1024            storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1025            previousRowValue = null;
1026        } else if (rowValue != null) {
1027            previousRowValue = rowValue;
1028        }
1029    }
1030
1031    //
1032    // NESTED RESULT MAP (JOIN MAPPING)
1033    //
1034
1035    private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
1036                                              String parentPrefix, CacheKey parentRowKey, boolean newObject) {
1037        boolean foundValues = false;
1038        for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
1039            final String nestedResultMapId = resultMapping.getNestedResultMapId();
1040            if (nestedResultMapId != null && resultMapping.getResultSet() == null) {
1041                try {
1042                    final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
1043                    final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
1044                    if (resultMapping.getColumnPrefix() == null) {
1045                        // try to fill circular reference only when columnPrefix
1046                        // is not specified for the nested result map (issue #215)
1047                        Object ancestorObject = ancestorObjects.get(nestedResultMapId);
1048                        if (ancestorObject != null) {
1049                            if (newObject) {
1050                                linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
1051                            }
1052                            continue;
1053                        }
1054                    }
1055                    final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
1056                    final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
1057                    Object rowValue = nestedResultObjects.get(combinedKey);
1058                    boolean knownValue = rowValue != null;
1059                    instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
1060                    if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
1061                        rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
1062                        if (rowValue != null && !knownValue) {
1063                            linkObjects(metaObject, resultMapping, rowValue);
1064                            foundValues = true;
1065                        }
1066                    }
1067                } catch (SQLException e) {
1068                    throw new ExecutorException(
1069                        "Error getting nested result map values for '" + resultMapping.getProperty() + "'.  Cause: " + e, e);
1070                }
1071            }
1072        }
1073        return foundValues;
1074    }
1075
1076    private String getColumnPrefix(String parentPrefix, ResultMapping resultMapping) {
1077        final StringBuilder columnPrefixBuilder = new StringBuilder();
1078        if (parentPrefix != null) {
1079            columnPrefixBuilder.append(parentPrefix);
1080        }
1081        if (resultMapping.getColumnPrefix() != null) {
1082            columnPrefixBuilder.append(resultMapping.getColumnPrefix());
1083        }
1084        return columnPrefixBuilder.length() == 0 ? null : columnPrefixBuilder.toString().toUpperCase(Locale.ENGLISH);
1085    }
1086
1087    private boolean anyNotNullColumnHasValue(ResultMapping resultMapping, String columnPrefix, ResultSetWrapper rsw)
1088        throws SQLException {
1089        Set<String> notNullColumns = resultMapping.getNotNullColumns();
1090        if (notNullColumns != null && !notNullColumns.isEmpty()) {
1091            ResultSet rs = rsw.getResultSet();
1092            for (String column : notNullColumns) {
1093                rs.getObject(prependPrefix(column, columnPrefix));
1094                if (!rs.wasNull()) {
1095                    return true;
1096                }
1097            }
1098            return false;
1099        }
1100        if (columnPrefix != null) {
1101            for (String columnName : rsw.getColumnNames()) {
1102                if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix.toUpperCase(Locale.ENGLISH))) {
1103                    return true;
1104                }
1105            }
1106            return false;
1107        }
1108        return true;
1109    }
1110
1111    private ResultMap getNestedResultMap(ResultSet rs, String nestedResultMapId, String columnPrefix)
1112        throws SQLException {
1113        ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId);
1114        return resolveDiscriminatedResultMap(rs, nestedResultMap, columnPrefix);
1115    }
1116
1117    //
1118    // UNIQUE RESULT KEY
1119    //
1120
1121    private CacheKey createRowKey(ResultMap resultMap, ResultSetWrapper rsw, String columnPrefix) throws SQLException {
1122        final CacheKey cacheKey = new CacheKey();
1123        cacheKey.update(resultMap.getId());
1124        List<ResultMapping> resultMappings = getResultMappingsForRowKey(resultMap);
1125        if (resultMappings.isEmpty()) {
1126            if (Map.class.isAssignableFrom(resultMap.getType())) {
1127                createRowKeyForMap(rsw, cacheKey);
1128            } else {
1129                createRowKeyForUnmappedProperties(resultMap, rsw, cacheKey, columnPrefix);
1130            }
1131        } else {
1132            createRowKeyForMappedProperties(resultMap, rsw, cacheKey, resultMappings, columnPrefix);
1133        }
1134        if (cacheKey.getUpdateCount() < 2) {
1135            return CacheKey.NULL_CACHE_KEY;
1136        }
1137        return cacheKey;
1138    }
1139
1140    private CacheKey combineKeys(CacheKey rowKey, CacheKey parentRowKey) {
1141        if (rowKey.getUpdateCount() > 1 && parentRowKey.getUpdateCount() > 1) {
1142            CacheKey combinedKey;
1143            try {
1144                combinedKey = rowKey.clone();
1145            } catch (CloneNotSupportedException e) {
1146                throw new ExecutorException("Error cloning cache key.  Cause: " + e, e);
1147            }
1148            combinedKey.update(parentRowKey);
1149            return combinedKey;
1150        }
1151        return CacheKey.NULL_CACHE_KEY;
1152    }
1153
1154    private List<ResultMapping> getResultMappingsForRowKey(ResultMap resultMap) {
1155        List<ResultMapping> resultMappings = resultMap.getIdResultMappings();
1156        if (resultMappings.isEmpty()) {
1157            resultMappings = resultMap.getPropertyResultMappings();
1158        }
1159        return resultMappings;
1160    }
1161
1162    private void createRowKeyForMappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey,
1163                                                 List<ResultMapping> resultMappings, String columnPrefix) throws SQLException {
1164        for (ResultMapping resultMapping : resultMappings) {
1165            if (resultMapping.isSimple()) {
1166                final String column = prependPrefix(resultMapping.getColumn(), columnPrefix);
1167                final TypeHandler<?> th = resultMapping.getTypeHandler();
1168                List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
1169                // Issue #114
1170                if (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) {
1171                    final Object value = th.getResult(rsw.getResultSet(), column);
1172                    if (value != null || configuration.isReturnInstanceForEmptyRow()) {
1173                        cacheKey.update(column);
1174                        cacheKey.update(value);
1175                    }
1176                }
1177            }
1178        }
1179    }
1180
1181    private void createRowKeyForUnmappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey,
1182                                                   String columnPrefix) throws SQLException {
1183        final MetaClass metaType = MetaClass.forClass(resultMap.getType(), reflectorFactory);
1184        List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
1185        for (String column : unmappedColumnNames) {
1186            String property = column;
1187            if (columnPrefix != null && !columnPrefix.isEmpty()) {
1188                // When columnPrefix is specified, ignore columns without the prefix.
1189                if (!column.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
1190                    continue;
1191                }
1192                property = column.substring(columnPrefix.length());
1193            }
1194            if (metaType.findProperty(property, configuration.isMapUnderscoreToCamelCase()) != null) {
1195                String value = rsw.getResultSet().getString(column);
1196                if (value != null) {
1197                    cacheKey.update(column);
1198                    cacheKey.update(value);
1199                }
1200            }
1201        }
1202    }
1203
1204    private void createRowKeyForMap(ResultSetWrapper rsw, CacheKey cacheKey) throws SQLException {
1205        List<String> columnNames = rsw.getColumnNames();
1206        for (String columnName : columnNames) {
1207            final String value = rsw.getResultSet().getString(columnName);
1208            if (value != null) {
1209                cacheKey.update(columnName);
1210                cacheKey.update(value);
1211            }
1212        }
1213    }
1214
1215    private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
1216        final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
1217        if (collectionProperty != null) {
1218            final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
1219            targetMetaObject.add(rowValue);
1220        } else {
1221            metaObject.setValue(resultMapping.getProperty(), rowValue);
1222        }
1223    }
1224
1225    private Object instantiateCollectionPropertyIfAppropriate(ResultMapping resultMapping, MetaObject metaObject) {
1226        final String propertyName = resultMapping.getProperty();
1227        Object propertyValue = metaObject.getValue(propertyName);
1228        if (propertyValue == null) {
1229            Class<?> type = resultMapping.getJavaType();
1230            if (type == null) {
1231                type = metaObject.getSetterType(propertyName);
1232            }
1233            try {
1234                if (objectFactory.isCollection(type)) {
1235                    propertyValue = objectFactory.create(type);
1236                    metaObject.setValue(propertyName, propertyValue);
1237                    return propertyValue;
1238                }
1239            } catch (Exception e) {
1240                throw new ExecutorException(
1241                    "Error instantiating collection property for result '" + resultMapping.getProperty() + "'.  Cause: " + e,
1242                    e);
1243            }
1244        } else if (objectFactory.isCollection(propertyValue.getClass())) {
1245            return propertyValue;
1246        }
1247        return null;
1248    }
1249
1250    private boolean hasTypeHandlerForResultObject(ResultSetWrapper rsw, Class<?> resultType) {
1251        if (rsw.getColumnNames().size() == 1) {
1252            return typeHandlerRegistry.hasTypeHandler(resultType, rsw.getJdbcType(rsw.getColumnNames().get(0)));
1253        }
1254        return typeHandlerRegistry.hasTypeHandler(resultType);
1255    }
1256
1257}